A Boilerplate Application

The best way to understand Qt is to start from a small example. This application creates a simple "Hello World!" string and writes it into a file using Unicode characters.

  1. #include <QCoreApplication>
  2. #include <QString>
  3. #include <QFile>
  4. #include <QDir>
  5. #include <QTextStream>
  6. #include <QDebug>
  7. int main(int argc, char *argv[])
  8. {
  9. QCoreApplication app(argc, argv);
  10. // prepare the message
  11. QString message("Hello World!");
  12. // prepare a file in the users home directory named out.txt
  13. QFile file(QDir::home().absoluteFilePath("out.txt"));
  14. // try to open the file in write mode
  15. if(!file.open(QIODevice::WriteOnly)) {
  16. qWarning() << "Can not open file with write access";
  17. return -1;
  18. }
  19. // as we handle text we need to use proper text codecs
  20. QTextStream stream(&file);
  21. // write message to file via the text stream
  22. stream << message;
  23. // do not start the eventloop as this would wait for external IO
  24. // app.exec();
  25. // no need to close file, closes automatically when scope ends
  26. return 0;
  27. }

The example demonstrates the use of file access and the how to write text to a a file using text codecs using a text stream. For binary data, there is a cross-platform binary stream called QDataStream that takes care of endianess and other details. The different classes we use are included using their class name at the top of the file. You can also include classes using the module and class name e.g. #include <QtCore/QFile>. For the lazy, there is also the possibility to include all the clases from a module using #include <QtCore>. For instance, in QtCore you have the most common classes used for an application that are not UI related. Have a look at the QtCore class listA Boilerplate Application - 图1 (opens new window) or the QtCore overviewA Boilerplate Application - 图2 (opens new window).

You build the application using CMake and make. CMake reads a project file, CMakeLists.txt and generates a Makefile which is used to build the application. CMake supports other build systems too, for example ninja. The project file is platform independent and CMake has some rules to apply the platform specific settings to the generated makefile. The project can also contain platform scopes for platform-specific rules, which are required in some specific cases.

Here is an example of a simple project file generated by Qt Creator. Notice that Qt attempts to create a file that is compatible with both Qt 5 and Qt 6, as well as various platforms such as Android, OS X and such.

  1. cmake_minimum_required(VERSION 3.14)
  2. project(projectname VERSION 0.1 LANGUAGES CXX)
  3. set(CMAKE_INCLUDE_CURRENT_DIR ON)
  4. set(CMAKE_AUTOUIC ON)
  5. set(CMAKE_AUTOMOC ON)
  6. set(CMAKE_AUTORCC ON)
  7. set(CMAKE_CXX_STANDARD 11)
  8. set(CMAKE_CXX_STANDARD_REQUIRED ON)
  9. # QtCreator supports the following variables for Android, which are identical to qmake Android variables.
  10. # Check https://doc.qt.io/qt/deployment-android.html for more information.
  11. # They need to be set before the find_package(...) calls below.
  12. #if(ANDROID)
  13. # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
  14. # if (ANDROID_ABI STREQUAL "armeabi-v7a")
  15. # set(ANDROID_EXTRA_LIBS
  16. # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so
  17. # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so)
  18. # endif()
  19. #endif()
  20. find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Quick REQUIRED)
  21. find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Quick REQUIRED)
  22. set(PROJECT_SOURCES
  23. main.cpp
  24. qml.qrc
  25. )
  26. if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
  27. qt_add_executable(projectname
  28. MANUAL_FINALIZATION
  29. ${PROJECT_SOURCES}
  30. )
  31. else()
  32. if(ANDROID)
  33. add_library(projectname SHARED
  34. ${PROJECT_SOURCES}
  35. )
  36. else()
  37. add_executable(projectname
  38. ${PROJECT_SOURCES}
  39. )
  40. endif()
  41. endif()
  42. target_compile_definitions(projectname
  43. PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
  44. target_link_libraries(projectname
  45. PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Quick)
  46. set_target_properties(projectname PROPERTIES
  47. MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
  48. MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
  49. MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
  50. )
  51. if(QT_VERSION_MAJOR EQUAL 6)
  52. qt_import_qml_plugins(projectname)
  53. qt_finalize_executable(projectname)
  54. endif()

We will not go into the depths of this file. Just remember Qt uses CMake’s CMakeLists.txt files to generate platform-specific makefiles, that are then used to build a project. In the build system section we will have a look at more basic, handwritten, CMake files.

The simple code example above just writes the text and exits the application. For a command line tool, this is good enough. For a user interface you need an event loop which waits for user input and somehow schedules draw operations. Here follows the same example now uses a button to trigger the writing.

Our main.cpp surprisingly got smaller. We moved code into an own class to be able to use Qt’s signal and slots for the user input, i.e. to handle the button click. The signal and slot mechanism normally needs an object instance as you will see shortly, but it can also be used with C++ lambdas.

  1. #include <QtCore>
  2. #include <QtGui>
  3. #include <QtWidgets>
  4. #include "mainwindow.h"
  5. int main(int argc, char** argv)
  6. {
  7. QApplication app(argc, argv);
  8. MainWindow win;
  9. win.resize(320, 240);
  10. win.setVisible(true);
  11. return app.exec();
  12. }

In the main function we create the application object, a window, and then start the event loop using exec(). For now, the application sits in the event loop and waits for user input.

  1. int main(int argc, char** argv)
  2. {
  3. QApplication app(argc, argv); // init application
  4. // create the ui
  5. return app.exec(); // execute event loop
  6. }

Using Qt, you can build user interfaces in both QML and Widgets. In this book we focus on QML, but in this chapter, we will look at Widgets. This lets us create the program only C++.

image

The main window itself is a widget. It becomes a top-level window as it does not have any parent. This comes from how Qt sees a user interface as a tree of UI elements. In this case, the main window is the root element, thus becomes a window, while the push button, that is a child of the main window, becomes a widget inside the window.

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3. #include <QtWidgets>
  4. class MainWindow : public QMainWindow
  5. {
  6. public:
  7. MainWindow(QWidget* parent=0);
  8. ~MainWindow();
  9. public slots:
  10. void storeContent();
  11. private:
  12. QPushButton *m_button;
  13. };
  14. #endif // MAINWINDOW_H

Additionally, we define a public slot called storeContent() in a custom section in the header file. Slots can be public, protected, or private, and can be called just like any other class method. You may also encounter a signals section with a set of signal signatures. These methods should not be called and must not be implemented. Both signals and slots are handled by the Qt meta information system and can be introspected and called dynamically at run-time.

The purpose of the storeContent() slot is that is is called when the button is clicked. Let’s make that happen!

  1. #include "mainwindow.h"
  2. MainWindow::MainWindow(QWidget *parent)
  3. : QMainWindow(parent)
  4. {
  5. m_button = new QPushButton("Store Content", this);
  6. setCentralWidget(m_button);
  7. connect(m_button, &QPushButton::clicked, this, &MainWindow::storeContent);
  8. }
  9. MainWindow::~MainWindow()
  10. {
  11. }
  12. void MainWindow::storeContent()
  13. {
  14. qDebug() << "... store content";
  15. QString message("Hello World!");
  16. QFile file(QDir::home().absoluteFilePath("out.txt"));
  17. if(!file.open(QIODevice::WriteOnly)) {
  18. qWarning() << "Can not open file with write access";
  19. return;
  20. }
  21. QTextStream stream(&file);
  22. stream << message;
  23. }

In the main window, we first create the push button and then register the signal clicked() with the slot storeContent() using the connect method. Every time the signal clicked is emitted the slot storeContent() is called. And now, the two objects communicate via signal and slots despite not being aware of each other. This is called loose coupling and is made possible using the QObject base class which most Qt classes derive from.