Crash course in Qt for C++ developers, Part 7

alex  07 Jan, 2019
cpp  Qt6  crash-course  architecture  file-structure 

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.

  1. Events and the main event loop
  2. Meta-object system (including QObject and MOC)
  3. Signals and slots - communication between objects
  4. Hierarchy and memory management
  5. MVC or rather model/view and delegate programming
  6. Choose your camp Quick/QML-camp or Widgets-camp
  7. How to organise and structure a Qt application
  8. Qt Quick/QML example
  9. Qt Widgets example
  10. Tooling, e.g. Qt Creator
  11. Remaining good-to-know topics
  12. Where to go from here?

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:

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 
    └── ... 

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.:

  • The library is easy to test and verify since no GUI elements are involved.
  • The library can be shared with multiple applications using different technologies (e.g. different programming languages) and interfaces (e.g. GUI and CLI).
  • The library can be loaded dynamically by the application. This allows modifications to the library without needing to recompile the application, i.e. allowing a drop-in replacement. Obviously, this only works if the ABI is the same between builds.

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.

Overview of the example application

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.

Model-View/Component-Controller

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 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.

Confusing? Perhaps an example might help.

To better understand the dynamic of this, let's look at the 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_; 
}; 

We have here defined two properties in the model which are exposed to the front-end:

  • The title, representing the name of the currently opened document, see file-handler.cpp in the screenshot above.
  • The 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).

The 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(); 
} 

As you can see, the relevant signals in the 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}; 
}; 

As you might have guessed, each signal corresponds to a user-action performed in the menu area: 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.

The 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.

Perhaps you noted that the 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); 
... 
}; 

As the name implies, the 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

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!

Subscribe to feed
RSS - Atom - JSON
Share the post