By now, you might have already gone through some of the examples provided by Qt and have a rough idea on how to create a Qt application. However, most Qt examples are designed around a particular usage or to demonstrate a specific class. And often, it's unclear how to expand and scale them. The purpose of this post is to demonstrate how to structure and architect a typical reasonably large Qt application. We'll start from scratch by setting up the file structure and later on show how a MVC-variant can be used to separate the core components. By the time you've read through this post, you should have a pretty good understanding on how to organise your files and components and how to scale the project into a larger application. This is the seventh post in the series "Crash course in Qt for C++ developers" covering How to organise and structure a Qt application. The other topics are listed below. This post kicks off a mini series consisting of three parts, which are all based on an example application. The first part covers the structure and design of said application. The following two posts will discuss two front-ends using different technologies: one with Quick/QML and another using Widgets. I recommend to read through the posts and subsequently try to recreate the application from scratch. I believe this is a good way to learn and will enforce you to possibly make mistakes along the way and then learn from them. In addition, it might be helpful to poke around a bit in the code while reading through. The example application is a simple code editor and the source code can be found on GitHub. Before we jump into the design, it also goes without saying that there are many different ways to shape the architecture of a Qt application. The solution presented here is only one of many, and it's most likely not a perfect fit for all domains. However, I do want to emphasise that it is important to consider the structure and architecture early on in the development cycle. If there is no structure in place, the code can quickly become unmanageable and difficult to scale. Similarly to software development processes, it's less important which one is followed, as long as one is adopted. Let's now dive into the example application. We'll begin with the file structure: As you can see, there are three folders in the root directory, each containing a project: The lib-directory, which is the logic and data behind the application, and two GUI front-ends: ui-qml and ui-widgets. By separating the logic and data into an isolated unit, there will be a clear line drawn between the UI and the logic. There are several benefits to this approach, e.g.: Likewise, by digging into the lib-directory from the root, we can find three different directories: public, src and tests. The public directory includes all the header files exposed by the library and used externally by the front-ends. The src includes the remaining header files as well as all the source files. Not surprisingly, tests are found in the tests directory. Those tests are part of a standalone application and are used to verify the public interface. This design encourages black-box testing as tests can be written without the source code, but simultaneously allows white-box testing as it's part of the same project. The tests in the example application use QTest but can easily be replaced by Google Test, Catch2 or Boost.Test or whatever your favourite test framework is. By drilling further down the file structure into the public and src directories, we can here find controllers and models. Those two directories contain - wait for it - the different model and controller classes. They are comprised of the flow, the logic and the data of the application. The concept MVC has previously been covered in a post and won't be discussed here in details. However, if you haven't already, I would recommend to read it through before moving on to the next section as we're about to dive into the architecture. In order to understand how the architecture is implemented, let's start by looking at a screenshot of the application: The application consists of one window, or rather one view called the Main View. The view is composed of three components: The menu at the top, the file navigation to the left and lastly, the editor to the right. This allows a modular design where a view can be assembled using a set of components. And if it isn't obvious, the benefit of this design is that those components can be reused between views. Each component and each view is connected to a single controller. The controllers in this design are different from the traditional definition (i.e. as defined in Smalltalk). The controllers here can be considered routers that receive user interactions but also manage the flow of the application by forwarding calls and signals to the relevant model(s) and other controller(s). Note that models might be shared between the components and views. The components subscribe to changes made on the model(s) in order to correctly present the current state and data. See the dependency diagram below: In the example application, the Main View is hooked up to the Confusing? Perhaps an example might help. To better understand the dynamic of this, let's look at the We have here defined two properties in the model which are exposed to the front-end: The As you can see, the relevant signals in the As you might have guessed, each signal corresponds to a user-action performed in the menu area: The Perhaps you noted that the As the name implies, the Similar to the previous MVC post this might feel a bit overwhelming at first. However, we'll come back to the example application in the next two posts and it will hopefully make more sense by then. That said, I would suggest you to go through the remaining components in the source code and investigate a bit. Possibly you could add another component and see how it would fit into the design? If you're interested in more information on how to structure a Qt application, I would recommend to try to get hold of the Learn Qt 5 book written by Nicholas Sheriff. The book goes through a full example application created from scratch and incorporates similar techniques as described in this post. In addition to this, if you're interested in adapting Facebook's Flux design, Benlau's quickflux has got you covered. Note that the structure presented here is only a starting point. As the project grows additional separation will most likely be required. For example, the GUI components might need to decouple from the front-end and placed into a standalone library in order to be re-used by multiple applications. Similarly, it might be beneficial to split the back-end and separate controllers and models into multiple libraries to better serve different applications. In the next post we'll cover the QML front-end for the example application in details. We'll learn more about the QML syntax and semantics and how it communicates with the C++-backend. We'll also explore how we can structure it in a scalable manner. See you next time!
File structure
clean-editor (root)
├── CMakeLists.txt
├── lib
│ ├── CMakeLists.txt
│ ├── public
│ │ ├── controllers
│ │ ├── models
│ │ └── ...
│ ├── src
│ │ ├── controllers
│ │ ├── models
│ │ └── ...
│ └── tests
│ ├── CMakeLists.txt
│ └── src
├── ui-qml
│ ├── CMakeLists.txt
│ └── ...
└── ui-widgets (not implemented yet)
├── CMakeLists.txt
└── ...
Overview of the example application
Model-View/Component-Controller
MainController
which handles the communication between all the components and their respective controllers: the MenuController
, the FileNavigationController
and the EditorController
. One of those components is the Menu Component which is using the MenuController
. The MenuController
receives the user-interactions and routes the actions to the MenuModel
or emits signals to the parent subscriber, i.e. the MainController
. The File Navigation Component and the Editor Component both work in a similar manner as the Menu Component. MenuModel/MenuController example - Show me some code!
MenuModel
/MenuController
pair and how they interact. Let's assume that the front-end forwards the user interactions to the controller and updates when changes are made to the model. Let's look at the model first: class CLEAN_EDITOR_EXPORT MenuModel : public QObject {
Q_OBJECT
Q_DISABLE_COPY_MOVE(MenuModel)
Q_PROPERTY(QString title READ title NOTIFY titleChanged)
Q_PROPERTY(bool isNewFile READ isNewFile NOTIFY isNewFileChanged)
public:
explicit MenuModel(QObject* parent = nullptr);
void setDocument(CleanEditor::Logic::DocumentHandler &document_handler);
QString title() const;
bool isNewFile() const;
Q_SIGNALS:
void titleChanged();
void isNewFileChanged();
private:
QPointer<CleanEditor::Logic::DocumentHandler> document_handler_;
};
title
, representing the name of the currently opened document, see file-handler.cpp in the screenshot above. isNewFile
, which states whether the currently opened document is new, i.e. not opened and/or not saved. This is used by the UI to decide if a save file-dialog should be shown when saving. The dialog is only needed if the file is new (or if "save as" is requested).MenuModel
uses the currently opened document, a DocumentHandler
, in order to detect changes and to store the aforementioned data. The implementation of setDocoument()
looks like this: void MenuModel::setDocument(DocumentHandler& document_handler) {
if (document_handler_ == &document_handler) {
return;
}
if (document_handler_) {
disconnect(document_handler_.data(), &DocumentHandler::fileUrlChanged, this, &MenuModel::titleChanged);
disconnect(document_handler_.data(), &DocumentHandler::isNewFileChanged, this, &MenuModel::isNewFileChanged);
}
document_handler_ = &document_handler;
if (!document_handler_) {
return;
}
connect(document_handler_.data(), &DocumentHandler::fileUrlChanged, this, &MenuModel::titleChanged);
connect(document_handler_.data(), &DocumentHandler::isNewFileChanged, this, &MenuModel::isNewFileChanged);
emit titleChanged();
emit isNewFileChanged();
}
DocumentHandler
are forwarded to the model which will in turn notify any listener, i.e. the Menu Component in this scenario. The setDocument()
is invoked by the controller MenuController
, so let's look at it next. class CLEAN_EDITOR_EXPORT MenuController : public QObject {
Q_OBJECT
Q_DISABLE_COPY_MOVE(MenuController)
public:
explicit MenuController(QObject* parent = nullptr);
void setModel(CleanEditor::Models::MenuModel& model);
void setDocument(CleanEditor::Logic::DocumentHandler& document_handler);
Q_SIGNALS:
void newFileClicked();
void openFileClicked(const QUrl& file_url);
void saveFileClicked();
void saveAsFileClicked(const QUrl& file_url);
private:
CleanEditor::Models::MenuModel* model_{nullptr};
};
newFileClicked()
, openFileClicked(...)
, saveFileClicked()
and saveAsFileClicked(...)
. Perhaps you also observed that only three icons are available in the menu? This is because the saveFile
action doesn't have a dedicated icon; it's emitted when using a keyboard shortcut instead. MenuController
's signals are not handled directly within the MenuController
but propagated up to the MainController
. The MainController
then redirects the flow to a DocumentsModel
which handles the actions for creating, opening and saving files. The reason the MenuController
doesn't directly own and write to the DocumentsModel
is because this particular model is shared between several components and should preferably only be handled by one controller, namely the parent MainController
. I've found this constraint to be very helpful in order to understand and track the flow of the application. It's especially helpful when debugging.model
is created outside the controller and then passed in through the setModel
-function. The main reason for this is that the model could potentially be swapped for another one. By using the dependency inversion principle we can decouple the components even further and use different models for the same controller. This technique is utilised in the EditorController
where we need two different models in order to use it for both the QML component and the C++ widget:class CLEAN_EDITOR_EXPORT EditorController : public QObject {
...
void setModel(CleanEditor::Models::AbstractEditorModel& model);
...
};
AbstractEditorModel
is an abstract class. An alternative to using an abstract/interface class is to utilise the Strategy pattern with std::function if composition is preferred over inheritance. Wrap up