644 27 5MB
English Pages 526 [514] Year 2016
Mastering Qt 5
Master application development by writing succinct, robust, and reusable code with Qt 5
Guillaume Lazar Robin Penea
BIRMINGHAM - MUMBAI
Mastering Qt 5 Copyright © 2016 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the authors, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information. First published: December 2016 Production reference: 1121216
Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK.
ISBN 978-1-78646-712-6 www.packtpub.com
Credits Authors
Copy Editor
Guillaume Lazar Robin Penea
Safis Editing
Reviewer
Project Coordinator
Ray Rischpater
Izzat Contractor
Commissioning Editor
Proofreader
Kunal Parikh
Safis Editing
Acquisition Editor
Indexer
Sonali Vernekar
Rekha Nair
Content Development Editor
Production Coordinator
Rohit Kumar Singh
Aparna Bhagat
Technical Editor Vivek Pala
About the Authors Guillaume Lazar is a software engineer living near Paris in France. He has worked for different companies, from startups to multinationals, for the last 7 years. He took the opportunity to observe and learn from many team organizations and technologies. He eventually founded his own software development company, NeuronalMotion, at the age of 27 with his friend Robin Penea in 2014. Information about it and its open source projects can be found on the comapny's website, www.neuronalmotion.com. NeuronalMotion represents to Guillaume "a different way of working." The current hierarchical organization that applies to most companies seems obsolete to him. With his own company, he wants to try something different, where each employee will be autonomous. Although he defines himself as a Qt framework lover, he likes to mix different technologies and platforms. He also spends time on game development, machine learning, and electronics, because "things" become "alive." Creating examples and writing chapters for this book was a serious challenge this year. Some sections were fundamentally changed to match Qt evolution. I would not have been able to achieve this task alone. I wish to thank Robin Penea, my friend and the coauthor of this book. He gave his all to create with me a robust Qt book with modern C++. Our review team, Rohit Kumar Singh, Ray Rischpater, Quentin Canu, Chistophe Dongieux, and Hugo Loi, have done excellent work: they corrected us and raised some important issues. I also want to thank Packt for the good follow up during the writing process. Finally, I thank my family and, particularly, Sophie, my girlfriend, for her support.
Robin Penea is a software engineer living in France. He has worked for several companies over the last 7 years, on many different technologies (ranging from server application development to embedded programming). He founded NeuronalMotion with Guillaume Lazar in 2014 to implement his vision of entrepreneurship and project development. He aims to achieve a self-organized company. More information is available at www.neuronalmotion.com. Besides developing with Qt, he likes to tinker with the Android AOSP framework, learn new programming language paradigms, and discover how to interweave technology and real life. This book would not have existed without Guillaume Lazar, my friend and the coauthor of the book. He was truly dedicated to designing and crafting the best possible book. I wish to thank our reviewers, Rohit Kumar Singh, Ray Rischpater, Quentin Canu, Christophe Dongieux, and Hugo Loi. Their input was invaluable and definitely raised the quality of the book. Packt played a sensible role, listening to our requests and changes, guiding us through the writing, and monitoring our mental states during this year with benevolence. I also wish to thank my father, Calin, for believing in me. A last word for Flore, my girlfriend, who kept my motivation up and supported me.
About the Reviewer Ray Rischpater is a software engineer and author with over 20 years of industry experience and sixteen books to his credit. Beginning with software development for mobile platforms, he s done work in several areas, including mobile, web services, building tools for mapping and geospatial engineering, and applying machine learning to geospatial processing problems. During this time, he s participated in the development of Internet technologies and custom applications for Newton, Magic Cap, Palm OS, Java ME, Qualcomm BREW, Apple iOS, and Google Android, as well as several proprietary platforms. Over the last six years, his focus has shifted from mobile applications and services to building tools, software processes, and machine learning models to facilitate mapping the real world. Presently, he s employed as a software engineering manager at Uber, doing work on mapping and machine learning. When not writing for or about software development, he enjoys making music, hiking, and photography with his family and friends in and around the San Lorenzo Valley in central California. When he s able, he also provides public service through amateur radio as the licensed Amateur Extra station KF6GPE. He is the author of Microsoft Mapping with Carmen Au, by Apress (published in 2013) and Application Development with Qt Creator, Second Edition, by Packt (published in 2014). He has written several other books, including Beginning Java ME Platform (Beginning from Novice to Professional), by Apress (published in 2008), Software Development for the QUALCOMM BREW Platform, by Apress (published in 2003), Wireless Web Development, First Edition, by Apress (published in 2002), and Wireless Web Development, Second Edition, by Apress (published in 2004). I d like to thank my wife, Meg Given, for her patience as I worked on yet another book project.
www.PacktPub.com For support files and downloads related to your book, please visit www.PacktPub.com. Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.PacktPub.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at [email protected] for more details. At www.PacktPub.com, you can also read a collection of free technical articles, sign up for a range of free newsletters and receive exclusive discounts and offers on Packt books and eBooks.
https://www.packtpub.com/mapt
Get the most in-demand software skills with Mapt. Mapt gives you full access to all Packt books and video courses, as well as industry-leading tools to help you plan your personal development and advance your career.
Why subscribe? Fully searchable across every book published by Packt Copy and paste, print, and bookmark content On demand and accessible via a web browser
Table of Contents Preface Chapter 1: Get Your Qt Feet Wet Creating a project MainWindow structure Qt Designer Signals and slots Custom QWidget Adding a task Using a QDialog Distributing code responsibility Emitting a custom signal using lambdas Simplifying with the auto type and a range-based for loop Summary
Chapter 2: Discovering QMake Secrets Designing a cross-platform project Adding the Windows implementation Adding the Linux implementation Adding the Mac OS implementation Transforming SysInfo into a singleton Exploring Qt Charts CpuWidget using QCharts Memory using Qcharts The .pro file in depth Under the hood of qmake Beneath Q_OBJECT and signals/slots Summary
Chapter 3: Dividing Your Project and Ruling Your Code Designing a maintainable project Defining data classes Storing your data in a database Protecting your code with a smart pointer Implementing the model Summary
1 8 9 12 15 17 19 23 26 28 29 34 38 39 39 42 48 53 56 58 60 64 70 74 77 80 81 82 86 89 97 104 117
Chapter 4: Conquering the Desktop UI Creating a GUI linked to a core shared library Listing your albums with AlbumListWidget Creating a ThumbnailProxyModel Displaying the selected album with AlbumWidget Enhancing thumbnails with PictureDelegate Displaying a picture with PictureWidget Composing your Gallery app Summary
Chapter 5: Dominating the Mobile UI Starting with Qt Quick and QML Checking your development environment Creating a Qt Quick project Preparing your Qt Quick gallery entry point Displaying albums with ListView Theming the application with a QML singleton Loading a database on mobile Creating a new album from a custom InputDialog Loading images with an ImageProvider Displaying thumbnails in a GridView Swiping through full resolution pictures Summary
118 118 123 128 132 141 145 147 151 152 153 162 163 166 169 172 176 178 182 187 191 194
Chapter 6: Even Qt Deserves a Slice of Raspberry Pi
195
Discovering Qt3D Configuring Qt for your Raspberry Pi Creating an entry point for your Qt3D code Setting up the scene Assembling your Qt3D entities Preparing the board game Crafting entities from the factory Building a snake engine in JavaScript Varying the HUD with QML states Profiling your QML application Summary
196 201 206 210 214 220 222 225 233 237 242
Chapter 7: Third-Party Libraries Without a Headache
243
Creating your Qt Designer plugin Configuring the project for Windows [ ii ]
244 246
Configuring the project for Linux Configuring the project for Mac Implementing your OpenCV filters Designing the UI with FilterWidget Exposing your plugin to Qt Designer Using your Qt Designer plugin Building the image-filter application Summary
248 249 250 254 259 262 265 273
Chapter 8: Animations - It's Alive, Alive!
274
Preparing an SDK Creating your plugins Loading your plugins dynamically Using the plugins inside the application Discovering the Animation Framework Making your thumbnails jump Fading the picture in Flashing the thumbnail in a sequence Summary
Chapter 9: Keeping Your Sanity with Multithreading Discovering QThread Flying over Qt multithreading technologies Architecting the Mandelbrot project Defining a Job class with QRunnable Using QThreadPool in MandelbrotCalculator Displaying the fractal with MandelbrotWidget Summary
Chapter 10: Need IPC? Get Your Minions to Work Architecturing an IPC project Laying down the foundations with an SDK Working with QDataStream and QTcpSocket Interacting with sockets in the worker Interacting with sockets from the application Building your own QTcpServer Summary
274 278 284 288 292 296 298 301 306 307 307 313 317 319 325 330 339 340 348 352 356 363 373 379 388
Chapter 11: Having Fun with Serialization
389
Architecting the drum machine project Creating a drum track
389 392
[ iii ]
Making your objects serializable with QVariant Serializing objects in JSON format Serializing objects in XML format Serializing objects in binary format Playing low latency sounds with QSoundEffect Triggering a QButton with your keyboard Bringing PlaybackWorker to life Accepting mouse drag and drop events Summary
Chapter 12: You Shall (Not) Pass with QTest Discovering Qt Test Executing your tests Writing factorized tests with datasets Benchmarking your code Testing your GUI Spying on your application with QSignalSpy Summary
Chapter 13: All Packed and Ready to Deploy Packaging your application Packaging for Windows Packaging for Linux with a distribution package Packaging for Linux with AppImage Packaging for Mac OS X Packaging for Android Packaging for iOS Summary
Chapter 14: Qt Hat Tips and Tricks
399 405 409 417 418 419 421 424 427 428 429 435 439 444 447 450 451 452 453 454 457 463 465 468 474 475 476
Managing your workspace with sessions Searching with the Locator Increasing the compilation speed Examining the memory with Qt Creator Generating random numbers Silencing unused variable warnings Logging custom objects to QDebug Improving log messages Saving your logs to a file Generating a command-line interface Sending and receiving HTTP data [ iv ]
477 477 479 479 483 483 485 486 488 489 491
Summary
496
Index
497
[v]
Preface C++ is a powerful language. Coupled with Qt, you have in your hands a cross-platform framework that allies performance and ease of use. Qt is a vast framework that provides tools in many areas (GUI, threads, networking, and so on). 25 years after its inception, Qt continues to evolve and grow with each release. This book aims to teach you how to squeeze the best out of Qt with the new C++14 additions (lambdas, smart pointers, enum classes, and so on). These two technologies together bring you a safe and powerful development toolbox. Throughout the book, we try to emphasize a clean architecture that lets you create and maintain your application in a complex environment. Each chapter is based on an example project that is the basis of all the discussion. Here are some tasters about what we will see in this book: Uncover qmake secrets Take a deep dive in the model/view architecture and study how you can build a complex application with this pattern Study QML and Qt Quick applications in mobile Develop Qt 3D components using QML and JavaScript Show how to develop plugins and SDKs using Qt Cover the multi-threading technologies provided by Qt Build an IPC mechanism using sockets Serialize data using XML, JSON, and binary format We'll cover all this and much, much more. Note that you can take a look at Chapter 14, Qt Hat Tips and Tricks, whenever you want if you want to get some development candies and see some code snippets that might make your development more pleasurable. And most importantly, have fun writing Qt applications!
Preface
What this book covers
Chapter 1, Get Your Qt Feet Wet, lays the fundamentals of Qt and refreshes your memory
with a todo application. This chapter covers the Qt project structure, how to use the designer, basic principles of the signals and slots mechanism, and introduces new features of C++14. Chapter 2, Discovering QMake Secrets, takes a deep dive in the heart of the Qt compilation
system: qmake. This chapter will help you understand how it works, how to use it, and how you can structure a Qt application with platform-specific code by designing a system monitoring application. Chapter 3, Dividing Your Project and Ruling Your Code, analyzes the Qt model/view
architecture and how a project can be organized by developing a custom library with the core logic of the application. The project example is a persistent gallery application. Chapter 4, Conquering the Desktop UI, studies the UI perspective of the model/view
architecture with a Qt Widget application relying on the library completed in the previous chapter. Chapter 5, Dominating the Mobile UI, adds the missing part of the gallery application with
the mobile version (Android and iOS); the chapter covers it with the use of QML, Qt Quick controls, and QML / C++ interactions. Chapter 6, Even Qt Deserves a Slice of Raspberry Pi, continues to the road on Qt Quick application with the Qt 3D perspective. This chapter covers how to build a 3D snake game targeted at the Raspberry Pi. Chapter 7, Third-Party Libraries Without a Headache, covers how a third-party library can be integrated in a Qt project. OpenCV will be integrated with an image filter application that also provides a custom QDesigner plugin. Chapter 8, Animations, It s Alive, Alive!, extends the image filter application by adding
animations and the ability to distribute a custom SDK to let other developers add their own filters. Chapter 9, Keeping Your Sanity with Multithreading, investigates the multithreading facilities
provided by Qt by building a multithreaded Mandelbrot fractal drawing application.
[2]
Preface Chapter 10, Need IPC? Get Your Minions to Work, broadens the Mandelbrot fractal application by moving the calculation to other processes and managing the communication using sockets. Chapter 11, Having Fun with Serialization, covers multiple serialization formats (JSON,
XML, and binary) inside a drum machine application in which you can record and load sound loops. Chapter 12, You Shall (Not) Pass with QTest, adds tests to the drum machine application and
studies how the Qt Test frameworks can be used to make unit tests, benchmarking, and GUI events simulation Chapter 13, All Packed and Ready to Deploy, gives insights into how to package an
application on all desktop OSes (Windows, Linux, and Mac OS) and mobile platforms (Android and iOS).
Chapter 14, Qt Hat Tips and Tricks, gathers some tips and tricks to develop with Qt with pleasure. It shows how to manage sessions in Qt Creator, useful Qt Creator keyboard shortcuts, how you can customize the logging, save it to disk, and much more.
What you need for this book
All the code in this book can be compiled and run from Qt Creator using Qt 5.7. You can do it from your preferred OS: Windows, Linux, or Mac OS. About the mobile-specific chapters, either an Android or an iOS device works, but it is not mandatory (the simulator/emulator can be enough). Chapter 6, Even Qt Deserves a Slice of Raspberry Pi, offers to build an application running on a Raspberry Pi. Although it is more fun if we can do it with a real Raspberry Pi, it is not necessary to have one to complete the chapter.
Who this book is for
This book will appeal to developers and programmers who would like to build GUI-based application. You should be fluent with C++ and the object-oriented paradigm. Qt knowledge is recommended, but is not necessary.
[3]
Preface
Conventions
In this book, you will find a number of text styles that distinguish between different kinds of information. Here are some examples of these styles and an explanation of their meaning. Code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles are shown as follows: "The qmake command is executed with the project .pro file." A block of code is set as follows: void MemoryWidget::updateSeries() { double memoryUsed = SysInfo::instance().memoryUsed(); mSeries->append(mPointPositionX++, memoryUsed); if (mSeries->count() > CHART_X_RANGE_COUNT) { QChart* chart = chartView().chart(); chart->scroll(chart->plotArea().width() / CHART_X_RANGE_MAX, 0); mSeries->remove(0); } }
When we wish to draw your attention to a particular part of a code block, the relevant lines or items are set in bold: windows { SOURCES += SysInfoWindowsImpl.cpp HEADERS += SysInfoWindowsImpl.h debug { SOURCES += DebugClass.cpp HEADERS += DebugClass.h } }
Any command-line input or output is written as follows: /path/to/qt/installation/5.7/gcc_64/bin/qmake -makefile -o Makefile /path/to/sysinfoproject/ch02-sysinfo.pro
[4]
Preface
New terms and important words are shown in bold. Words that you see on the screen, for example, in menus or dialog boxes, appear in the text like this: "In Qt Creator, when you click on the Build button, qmake is invoked." Warnings or important notes appear in a box like this.
Tips and tricks appear like this.
Reader feedback
Feedback from our readers is always welcome. Let us know what you think about this book-what you liked or disliked. Reader feedback is important for us as it helps us develop titles that you will really get the most out of. To send us general feedback, simply email [email protected], and mention the book's title in the subject of your message. If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, see our author guide at www.packtpub.com/authors.
Customer support
Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase.
Downloading the example code
You can download the example code files for this book from your account at http://www.p acktpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.c om/supportand register to have the files e-mailed directly to you.
[5]
Preface
You can download the code files by following these steps: 1. 2. 3. 4. 5. 6. 7.
Log in or register to our website using your e-mail address and password. Hover the mouse pointer on the SUPPORT tab at the top. Click on Code Downloads & Errata. Enter the name of the book in the Search box. Select the book for which you're looking to download the code files. Choose from the drop-down menu where you purchased this book from. Click on Code Download.
Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of: WinRAR / 7-Zip for Windows Zipeg / iZip / UnRarX for Mac 7-Zip / PeaZip for Linux The code bundle for the book is also hosted on GitHub at https://github.com/PacktPubl ishing/Mastering-Qt-5. We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!
Errata
Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you find a mistake in one of our books-maybe a mistake in the text or the codewe would be grateful if you could report this to us. By doing so, you can save other readers from frustration and help us improve subsequent versions of this book. If you find any errata, please report them by visiting http://www.packtpub.com/submit-errata, selecting your book, clicking on the Errata Submission Form link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata will be uploaded to our website or added to any list of existing errata under the Errata section of that title. To view the previously submitted errata, go to https://www.packtpub.com/books/conten t/supportand enter the name of the book in the search field. The required information will
appear under the Errata section.
[6]
Preface
Piracy
Piracy of copyrighted material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works in any form on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy. Please contact us at [email protected] with a link to the suspected pirated material. We appreciate your help in protecting our authors and our ability to bring you valuable content.
Questions
If you have a problem with any aspect of this book, you can contact us at [email protected], and we will do our best to address the problem.
[7]
1
Get Your Qt Feet Wet If you know C++ but have never touched Qt, or if you have made some intermediate Qt applications, this chapter will ensure that your Qt foundations are safe before studying advanced concepts in the following chapters. We will teach you to create a simple todo application using Qt Creator. This application will display a list of tasks that you can create/update/delete. We will cover the Qt Creator and Qt Designer interfaces, an introduction to the signal/slot mechanism, the creation of a custom widget with custom signals/slots, and its integration into your application. You will implement a todo app using new C++14 semantics: lambdas, auto variables, and for loops. Each one of these concepts will be explained in depth and will be used throughout the book. At the end of this chapter, you will be able to create a desktop application with a flexible UI using Qt widgets and new C++ semantics. In this chapter, we will cover the following topics: Qt project basic structure Qt Designer interface UI fundamentals Signals and slots Custom QWidget C++14 lambda, auto, for each
Get Your Qt Feet Wet
Creating a project
The first thing to do is to start Qt Creator. In Qt Creator, you can create a new Qt project via File | New File or Project | Application | Qt Widgets Application | Choose. The wizard will then guide you through four steps: 1. 2. 3. 4.
Location: You must choose a project name and a location. Kits: Target platforms that your project aims at (Desktop, Android, and so on). Details: Base class information and name for the generated class. Summary: Allows you to configure your new project as a subproject and automatically add it to a version control system.
Even if all default values can be kept, please at least set a useful project name such as “todo” or “TodoApp.” We won't blame you if you want to call it “Untitled” or “Hello world.” Once done, Qt Creator will generate several files that you can see in the Projects hierarchy view:
The .pro file is Qt's configuration project file. As Qt adds specific file formats and C++ keywords, an intermediate build step is performed, parsing all files to generate final files. This process is done by qmake, an executable from the Qt SDK. It will also generate the final Makefiles for your project.
[9]
Get Your Qt Feet Wet
A basic .pro file generally contains: Qt modules used (core, gui, and so on) Target name (todo, todo.exe, and so on) Project template (app, lib, and so on) Sources, headers, and forms There are some great features that come with Qt and C++14. This book will showcase them in all its projects. For GCC and CLANG compilers, you must add CONFIG += c++14 to the .pro file to enable C++14 on a Qt project, as shown in the following code: QT CONFIG
+= core gui += c++14
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = todo TEMPLATE = app SOURCES += main.cpp \ MainWindow.cpp HEADERS
+= MainWindow.h \
FORMS
+= MainWindow.ui \
The MainWindow.h and MainWindow.cpp files are the headers/sources for the MainWindow class. These files contain the default GUI generated by the wizard. The MainWindow.ui file is your UI design file in XML format. It can be edited more easily with Qt Designer. This tool is a WYSIWYG (What You See Is What You Get) editor that helps you to add and adjust your graphical components (widgets). Here is the main.cpp file, with its well-known function: #include "MainWindow.h" #include int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec();
[ 10 ]
Get Your Qt Feet Wet }
As usual, the main.cpp file contains the program entry point. It will, by default, perform two actions: Instantiate and show your main window Instantiate a QApplication and execute the blocking main event loop This is the bottom-left toolbar for Qt Creator:
Use it to build and start your todo application in debug mode: 1. Check that the project is in Debug build mode. 2. Use the hammer button to build your project. 3. Start debugging using the green Play button with a little blue bug. You will discover a wonderful and beautifully empty window. We will rectify this after explaining how this MainWindow is constructed:
[ 11 ]
Get Your Qt Feet Wet
An empty MainWindow screenshot
Qt tip Press Ctrl + B (for Windows/Linux) or Command + B (for Mac) to build your project Press F5 (for Windows / Linux) or Command +R (for Mac) to run your application in debug mode
MainWindow structure
This generated class is a perfect yet simple example of Qt framework usage; we will dissect it together. As mentioned previously, the MainWindow.ui file describes your UI design and MainWindow.h/ MainWindow.cpp is the C++ object where you can manipulate the UI with code. It is important to take a look at the header file MainWindow.h. Our MainWindow object inherits from Qt's QMainWindow class: #include namespace Ui { class MainWindow; }
[ 12 ]
Get Your Qt Feet Wet class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private: Ui::MainWindow *ui; };
As our class inherits from the QMainWindow class, on top of the header file, we add the corresponding include. The second part is the forward declaration of the Ui::MainWindow, as we only declare a pointer. The Q_OBJECT can look a little strange to a non-Qt developer. This macro allows the class to define its own signals/slots and more globally Qt's meta-object system. These features will be covered later in this chapter. This class defines a public constructor and destructor. The latter is pretty common. But the constructor takes a parameter parent. This parameter is a QWidget pointer that is null by default. A QWidget is a UI component. It can be a label, a textbox, a button, and so on. If you define a parent-child relationship between your window, layout, and other UI widgets, memory management of your application will be easier. Indeed, in this case, deleting the parent is enough because its destructor will take care of also deleting its child, which in turn will delete its children and so on. Our MainWindow class extends QMainWindow from the Qt framework. We have a ui member variable in the private fields. The type is a pointer of Ui::MainWindow, which is defined in the ui_MainWindow.h file generated by Qt. It's the C++ transcription of the UI design file MainWindow.ui. The ui member variable will allow you to interact with your UI components (QLabel, QPushButton, and so on) from C++, as shown in the following figure:
[ 13 ]
Get Your Qt Feet Wet
C++ tip If your class only uses pointers or references for a class type, you can avoid including the header by using forward declaration. That will drastically reduce compilation time. Now that the header part is done, we can talk about the MainWindow.cpp source file. In the following code snippet, the first include is our class header. The second one is the include required by the generated class Ui::MainWindow. This include is required as we only use a forward declaration in the header: #include "MainWindow.h" #include "ui_MainWindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this);
In many cases, Qt generates a good piece of code using the initializer list. The parent argument is used to call the superclass constructor QMainWindow. Our private member variable ui is also initialized now. Now that ui is initialized, we must call the setupUi function to initialize all widgets used by the MainWindow.ui design file: As we initialize a pointer in the constructor, it must be cleaned in the destructor: MainWindow::~MainWindow() { delete ui; }
[ 14 ]
Get Your Qt Feet Wet
Qt Designer
Qt Designer is a major tool for developing Qt applications. This WYSIWYG editor will help you easily design your GUI. If you switch between Edit mode and Design mode for the MainWindow.ui file, you will see the real XML content and the designer:
The designer displays several parts: Form Editor: This is a visual representation of the form (empty for now) Widget Box: This contains all widgets that can be used with your form Object Inspector: This displays your form as a hierarchical tree Property Editor: This enumerates the properties of the selected widget Action Editor/Signal & Slots Editor: This handles connections between your objects It's time to embellish this empty window! Let's drag and drop a Label widget from the Display Widgets section on the form. You can change the name and the text properties from the properties editor.
[ 15 ]
Get Your Qt Feet Wet
As we are making a todo application, we suggest these properties: objectName: statusLabel text: Status: 0 todo/0 done
This label will later display the count of todo tasks and the count of tasks already done. OK, save, build, and start your application. You should now see your new label in the window. You can now add a push button with those properties: objectName: addTaskButton text: Add task
You should get a result close to this:
Qt tip You can edit the text property of a widget directly on your form by double-clicking on it!
[ 16 ]
Get Your Qt Feet Wet
Signals and slots
The Qt framework brings a flexible message exchange mechanism through three concepts: signals, slots, and connections: A signal is a message sent by an object A slot is a function that will be called when this signal is triggered The connect function specifies which signal is linked to which slot Qt already provides signals and slots for its classes, which you can use in your application. For example, QPushButton has a signal clicked(), which will be triggered when the user clicks on the button. The QApplication class has a slot quit() function, which can be called when you want to terminate your application. Here is why you will love Qt signals and slots: A slot remains an ordinary function, so you can call it yourself A single signal can be linked to different slots A single slot can be called by different linked signals A connection can be made between a signal and a slot from different objects, and even between objects living inside different threads! Keep in mind that, to be able to connect a signal to a slot, their methods' signatures must match. The count, order, and type of arguments must be identical. Note that signals and slots never return values. This is the syntax of a Qt connection: connect(sender, &Sender::signalName, receiver, &Receiver::slotName);
The first test that we can do to use this wonderful mechanism is to connect an existing signal with an existing slot. We will add this connect call to the MainWindow constructor: MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); connect(ui->addTaskButton, &QPushButton::clicked, QApplication::instance(), &QApplication::quit); }
[ 17 ]
Get Your Qt Feet Wet
Let's analyze how a connection is done: sender: This is the object that will send the signal. In our example, it is the QPushButton named addTaskButton added from the UI designer. &Sender::signalName: This is the pointer to the member signal function. Here,
we want do something when the clicked signal is triggered. receiver: This is the object that will receive and handle the signal. In our case, it is the QApplication object created in main.cpp. &Receiver::slotName: This is a pointer to one of the receiver's member slot functions. In this example, we use the built-in quit() slot from Qapplication, which will exit the application. You can now compile and run this short example. You will terminate the application if you click on the addTaskButton of your MainWindow. Qt tip You can connect a signal to another signal. The second signal will be emitted when the first one is triggered. Now that you know how to connect a signal to an existing slot, let's see how to declare and implement a custom addTask() slot in our MainWindow class. This slot will be called when the user clicks on ui->addTaskButton. This is the updated MainWindow.h: class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); public slots: void addTask(); private: Ui::MainWindow *ui; };
[ 18 ]
Get Your Qt Feet Wet
Qt uses a specific slot keyword to identify slots. Since a slot is a function, you can always adjust the visibility (public, protected or private) depending on your needs. Add this slot implementation in the MainWindow.cpp file: void MainWindow::addTask() { qDebug() setupUi(this); connect(ui->addTaskButton, &QPushButton::clicked, this, &MainWindow::addTask); }
Since we now use qDebug() in out slot, we must include . The updated connect now calls our custom slot instead of quitting the application. Build and run the application. If you click on the button, you will see your debug message inside the Qt Creator Application Output tab.
Custom QWidget
We now have to create the Task class that will hold our data (task name and completed status). This class will have its form file separated from MainWindow. Qt Creator provides an automatic tool to generate a base class and the associated form.
[ 19 ]
Get Your Qt Feet Wet
Click on File | New File or Project | Qt | Qt Designer Form Class. There are several form templates; you will recognize Main Window, which Qt Creator created for us when we started the todo app project. Select Widget and name the class Task, then click on Next. Here is a summary of what Qt Creator will do: 1. Create a Task.h file and a Task.cpp file. 2. Create the associated Task.ui and do the plumbing to connect it to Task.h. 3. Add these three new files to todo.pro so they can be compiled. Finish and, voilà, the Task class is ready to be filled. We will jump into the Task.ui first. Start by dragging and dropping a Check Box (put checkbox in the objectName) and a Push Button (objectName = removeButton):
My alignment looks great, let's ship this to the customers!
Unless you have a pixel-perfect eye, your items are not very well aligned. You need to indicate how your widgets should be laid out and how they should react when the window geometry changes (for example, when the user resizes the window). For this, Qt has several default layout classes: Vertical Layout: In this layout, widgets are vertically stacked Horizontal Layout: In this layout, widgets are horizontally stacked Grid Layout: In this layout, widgets are arranged in a grid that can be
subdivided into smaller cells Form Layout: In this layout, widgets are arranged like a web form, a label, and an input
[ 20 ]
Get Your Qt Feet Wet
Every layout will try to constrain all widgets to occupy equal surfaces. It will either change the widgets' shape or add extra margins, depending on each widget's constraints. A Check Box will not be stretched but a Push Button will. In our Task object, we want this to be horizontally stacked. In the Form Editor tab, rightclick on the window and select Lay out | Lay out Horizontally. Each time you add a new widget in this layout, it will be arranged horizontally. Now add a Push Button (objectName = editButton) line just after the checkbox object. The Form Editor window offers a realistic preview of how your UI will render. If you stretch the window now, you can observe how each widget will react to this event. When resizing horizontally, you can note that the push buttons are stretched. It looks bad. We need something to “hint” to the layout that these buttons should not be stretched. Enter the Spacer widget. Take the Horizontal Spacer in the widget box and drop it after the checkbox object:
A spacer is a special widget that tries to push (horizontally or vertically) adjacent widgets to force them to take up as little space as possible. The editButton and removeButton objects now take up only the space of their text and will be pushed to the edge of the window when it is resized. You can add sub layouts of any type in a form (vertical, horizontal, grid, form) and create a complex-looking application with a combination of widgets, spacers, and layouts. These tools are targeted at designing a good-looking desktop application that can react properly to different window geometries. The Designer part is finished, so we can switch to the Task source code. Since we created a Qt Designer Form class, Task is closely linked to its UI. We will use this as leverage to store our model in a single place. When we create a Task object, it has to have a name: #ifndef TASK_H #define TASK_H
[ 21 ]
Get Your Qt Feet Wet #include #include namespace Ui { class Task; } class Task : public QWidget { Q_OBJECT public: explicit Task(const QString& name, QWidget *parent = 0); ~Task(); void setName(const QString& name); QString name() const; bool isCompleted() const; private: Ui::Task *ui; }; #endif // TASK_H
The constructor specifies a name, and as you can see there are no private fields storing any state of the object. All this will be done in the form part. We also added some getters and setters that will interact with the form. It is better to have a model completely separated from the UI, but our example is simple enough to merge them. Moreover, Task implementation details are hidden from the outside world and can still be refactored later on. Here is the content of the Task.cpp file: #include "Task.h" #include "ui_Task.h" Task::Task(const QString& name, QWidget *parent) : QWidget(parent), ui(new Ui::Task) { ui->setupUi(this); setName(name); } Task::~Task() { delete ui; }
[ 22 ]
Get Your Qt Feet Wet void Task::setName(const QString& name) { ui->checkbox->setText(name); } QString Task::name() const { return ui->checkbox->text(); } bool Task::isCompleted() const { return ui->checkbox->isChecked(); }
The implementation is straightforward; we store the information in our ui->checkbox and the name() and isCompleted() getters take their data from the ui->checkbox.
Adding a task
We will now rearrange the layout of MainWindow to be able to display our todo tasks. Right now, there is no widget where we can display our tasks. Open the MainWindow.ui file and edit it to get the following result:
[ 23 ]
Get Your Qt Feet Wet
If we detail the content, we have: A vertical layout for centralWidget containing the toolbarLayout file and the tasksLayout file. A vertical spacer pushing these layouts to the top, forcing them to take up the smallest possible space. We got rid of menuBar, mainToolBar, and statusBar. Qt Creator created them automatically, we simply don't need them for our purposes. You can guess their uses from their names. Do not forget to rename the MainWindow title to Todo by selecting the MainWindow in the Object Inspector window and editing the Qwidget | windowTitle property. Your app deserves to be named properly. Qt Tip Press Shift + F4 in Designer mode to switch between the form editor and the source. Now that the MainWindow UI is ready to welcome tasks, let's switch to the code part. The application has to keep track of the new tasks. Add the following in the MainWindow.h file: #include #include "Task.h" class MainWindow : public QMainWindow { // MAINWINDOW_H public slots: void addTask(); private: Ui::MainWindow *ui; QVector mTasks; };
The QVector is the Qt container class providing a dynamic array, which is an equivalent of the std::vector. As a general rule, STL containers are more customizable but might miss some features compared to Qt containers. If you use C++11 smart pointers, you should favor std containers, but we will get into that later.
[ 24 ]
Get Your Qt Feet Wet
In the Qt documentation of QVector, you might stumble upon the following statement: “For most purposes, QList is the right class to use“. There is a debate about this in the Qt community: Do you often need to insert objects larger than a pointer at the beginning or in the middle of your array? Use a QList class. Need contiguous memory allocation? Less CPU and memory overhead? Use a QVector class. The already added slot addTask() will now be called each time we want to add a new Task object to the mTasks function. Let's fill our QVector tasks each time addTaskButton is clicked. First, we connect the clicked() signal in the MainWindow.cpp file: MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), mTasks() { ui->setupUi(this); connect(ui->addTaskButton, &QPushButton::clicked, this, &MainWindow::addTask); };
C++ tip As a best practice, try to always initialize member variables in the initializer list and respect the order of variable declarations. Your code will run faster and you will avoid unnecessary variable copies. Take a look at the standard C++ documentation at https://isocpp.org/wiki/faq/ctor s#init-lists. The body of the addTask() function should look like this: void MainWindow::addTask() { qDebug() tasksLayout->addWidget(task); }
[ 25 ]
Get Your Qt Feet Wet
We created a new task and added it to our mTask vector. Because Task is a QWidget, we also added it directly to the tasksLayout. An important thing to note here is that we never managed this new task's memory. Where is the delete task instruction? This is a key feature of the Qt Framework we started to broach earlier in the chapter; the QObject class parenting automatically handles object destruction. In our case, the ui->tasksLayout->addWidget(task) call has an interesting side-effect; the ownership of the task is transferred to tasksLayout. The QObject* parent defined in Task constructor is now tasksLayout, and the Task destructor will be called when tasksLayout releases its own memory by recursively iterating through its children and calling their destructor. This will happen at this precise moment: MainWindow::~MainWindow() { delete ui; }
When MainWindow is released (remember, it's a stack variable allocated in the main.cpp file), it will call delete ui, which in turn will bring down the whole QObject hierarchy. This feature has interesting consequences. First, if you use the QObject parenting model in your application, you will have much less memory to manage. Second, it can collide with some new C++11 semantics, specifically the smart pointers. We will get into that in later chapters.
Using a QDialog
We deserve something better than an untitled task. The user needs to define its name when it's created. The easiest path would be to display a dialog where the user can input the task name. Fortunately Qt offers us a very configurable dialog that fits perfectly in addTask(): #include ... void MainWindow::addTask() { bool ok; QString name = QInputDialog::getText(this, tr("Add task"), tr("Task name"), QLineEdit::Normal, tr("Untitled task"), &ok); if (ok && !name.isEmpty()) {
[ 26 ]
Get Your Qt Feet Wet qDebug() tasksLayout->addWidget(task); } }
The QinputDialog::getText function is a static blocking function that displays the dialog. When the user validates/cancels the dialog, the code continues. If we run the application and try to add a new task, we'll see this:
The QInputDialog::getText signature looks like this: QString QinputDialog::getText( QWidget* parent, const QString& title, const QString& label, QLineEdit::EchoMode mode = QLineEdit::Normal, const QString& text = QString(), bool* ok = 0, ...)
Let's break it down: parent: This is the parent widget (MainWindow) to which the QinputDialog is attached. This is another instance of the QObject class's parenting model. title: This is the title displayed in the window title. In our example, we use tr("Add task"), which is how Qt handles i18n in your code. We will see
later on how to provide multiple translations for a given string. label: This is the label displayed right above the input text field. mode: This is how the input field is rendered (password mode will hide the text). ok: This is a pointer to a variable that is set to true if the user presses OK and to false if the user presses Cancel. QString: The returned QString is what the user has typed. There are a few more optional parameters we can safely ignore for our example. [ 27 ]
Get Your Qt Feet Wet
Distributing code responsibility
Great, the user can now specify the task name when it's created. What if he makes an error when typing the name? The next logical step is to rename the task after we created it. We'll take a slightly different approach. We want our Task to be as autonomous as possible. If we attach it to another component (rather than MainWindow), this renaming feature has to keep working. Thus, this responsibility has to be given to the Task class: // In Task.h public slots: void rename(); // In Task.cpp #include Task::Task(const QString& name, QWidget *parent) : QWidget(parent), ui(new Ui::Task) { ui->setupUi(this); setName(name); connect(ui->editButton, &QPushButton::clicked, this, &Task::rename); } ... void Task::rename() { bool ok; QString value = QInputDialog::getText(this, tr("Edit task"), tr("Task name"), QLineEdit::Normal, this->name(), &ok); if (ok && !value.isEmpty()) { setName(value); } }
We add a public slot rename() to connect it to a signal. The body of rename() reuses what we had previously covered with QInputDialog. The only difference is the QInputDialog default value, which is the current task name. When setName(value) is called, the UI is instantly refreshed with the new value; there's nothing to synchronize or update, the Qt main loop will do its job. The nice thing is that Task::rename() is completely autonomous. Nothing has been modified in MainWindow, so we have effectively zero coupling between our Task and the parent QWidget. [ 28 ]
Get Your Qt Feet Wet
Emitting a custom signal using lambdas
The remove task is straightforward to implement, but we'll study some new concepts along the way. The Task has to notify its owner and parent (MainWindow) that the removeTaskButtonQPushButton has been clicked. We'll implement this by defining a custom signal removed in Task.h files: class Task : public QWidget { ... public slots: void rename(); signals: void removed(Task* task); ... };
Like we did for the slots, we have to add the Qt keyword signals in our header. Since a signal is used only to notify another class, the public keyword is not needed (it even raises a compilation error). A signal is simply a notification sent to the receiver (the connected slot); it implies that there is no function body for the removed(Task* task) function. We added the task parameter to allow the receiver to know which task asked to be removed. The next step is to emit the removed signal upon the removeButton click. This is done in the Task.cpp file: Task::Task(const QString& name, QWidget *parent) : QWidget(parent), ui(new Ui::Task) { ui->setupUi(this); ... connect(ui->removeButton, &QPushButton::clicked, [this] { emit removed(this); }); }
This code excerpt shows a very interesting feature of C++11: lambdas. In our example, the lambda is the following part: [this] { emit removed(this); });
[ 29 ]
Get Your Qt Feet Wet
What we did here is to connect the clicked signal to an anonymous inline function, a lambda. Qt allows signal relaying by connecting a signal to another signal if their signatures match. It's not the case here; the clicked signal has no parameter and the removed signal needs a Task*. A lambda avoids the declaration of a verbose slot in Task. Qt 5 accepts a lambda instead of a slot in a connect, and both syntaxes can be used. Our lambda executes a single line of code: emit removed(this). Emit is a Qt macro that will immediately trigger the connected slot with what we passed in a parameter. As we said earlier, removed(Task* this) has no function body, its purpose is to notify the registered slot of an event. Lambdas are a great addition to C++. They offer a very practical way of defining short functions in your code. Technically, a lambda is the construction of a closure capable of capturing variables in its scope. The full syntax goes like this: [ capture-list ] ( params ) -> ret { body }
Let's study each part of this statement: capture-list: This defines what variables will be visible inside the lambda
scope. params: This is the function parameters type list that can be passed to the lambda scope. There are no parameters in our case, We might have written [this] () { ... }, but C++11 lets us skip the parentheses altogether. ret: This is the return type of the lambda function. Just like params, this parameter can be omitted if the return type is void. body: This is obviously your code body where you have access to your capturelist, and params, and which must return a variable with a type ret.
In our example, we captured the this pointer to be able to: Have a reference on the removed() function, which is a part of the Task class. If we did not capture this, the compiler would have shouted error: 'this' was not captured for this lambda function emit removed(this);. Pass this to the removed signal; the caller needs to know which task triggered removed.
[ 30 ]
Get Your Qt Feet Wet
The capture-list relies on standard C++ semantics: capture variables by copy or by reference. Let us say that we wanted to print a log of the constructor parameter name and we capture it by reference in our lambda: connect(ui->removeButton, &QPushButton::clicked, [this, &name] { qDebug() removeButton, &QPushButton::clicked, [this, name] { qDebug() QString: The returned value of the lambda will be a QString. return "------- " + taskName.toUpper(): the body of our lambda. We return a concatenation of a string and the uppercase version of the parameter taskName. As you can see, string manipulation becomes a lot easier with Qt. (name): Here comes the catch. Now that the lambda function is defined, we can call it passing the name parameter. In a single instruction, we define it and call it. The QDebug() function will simply print the result.
The real benefit of this lambda will emerge if we are able to assign it to a variable and call it multiple times. C++ is statically typed, so we must provide the type of our lambda variable. In the language specification, a lambda type cannot be explicitly defined. We'll see soon how we can do it with C++11. For now, let's finish our remove feature. Task now emits the removed() signal. This signal has to be consumed by MainWindow: // in MainWindow.h public slots: void addTask(); void removeTask(Task* task); // In MainWindow.cpp void MainWindow::addTask() { ... if (ok && !name.isEmpty()) { qDebug() tasksLayout->removeWidget(task); task->setParent(0); delete task; }
The MainWindow::removeTask() must match the signal signature. The connection is made when the task is created. The interesting part comes in the implementation of MainWindow::removeTask(). The task is first removed from the mTasks vector. It is then removed from tasksLayout. Here, tasksLayout releases its ownership of task (that is, tasksLayout ceases to be the task class's parent). So far so good. The next two lines are interesting. The ownership transfer does not completely release the task class ownership. If we commented these lines, here is how removeTask() will look: void MainWindow::removeTask(Task* task) { mTasks.removeOne(task); ui->tasksLayout->removeWidget(task); // task->setParent(0); // delete task; }
If you add a log message in Task destructor and execute the program, this log message will be displayed. Nonetheless, the Qt documentation tells us in Qlayout::removeWidget part: The ownership of a widget remains the same as when it was added. Instead, what really happens is that the task class's parent becomes centralWidget, the tasksLayout class's parent. We want Qt to forget everything about task, that's why we call task->setParent(0). We can then safely delete it and call it a day.
[ 33 ]
Get Your Qt Feet Wet
Simplifying with the auto type and a rangebased for loop
The final step to a complete CRUD of our tasks is to implement the completed task feature. We'll implement the following: Click on the checkbox to mark the task as completed Strike the task name Update the status label in MainWindow The checkbox click handling follows the same pattern as removed: // In Task.h signals: void removed(Task* task); void statusChanged(Task* task); private slots: void checked(bool checked); // in Task.cpp Task::Task(const QString& name, QWidget *parent) : QWidget(parent), ui(new Ui::Task) { ... connect(ui->checkbox, &QCheckBox::toggled, this, &Task::checked); } ... void Task::checked(bool checked) { QFont font(ui->checkbox->font()); font.setStrikeOut(checked); ui->checkbox->setFont(font); emit statusChanged(this); }
[ 34 ]
Get Your Qt Feet Wet
We define a slot checked(bool checked) that will be connected to the checkbox::toggled signal. In our slot checked(), we strike out the checkbox text according to the bool checked value. This is done using the QFont class. We create a copy font from the checkbox->font(), modify it, and assign it back to ui->checkbox. If the original font was in bold, with a special size, its appearance would be guaranteed to stay the same. Play around with the font object in Qt Designer. Select the checkbox in the Task.ui file and go to Properties Editor | QWidget | font.
The last instruction notifies MainWindow that the Task status has changed. The signal name is statusChanged, rather than checkboxChecked, to hide the implementation details of the task. Add the following code in the MainWindow.h file: // In MainWindow.h public: void updateStatus(); public slots: void addTask(); void removeTask(Task* task); void taskStatusChanged(Task* task); // In MainWindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), mTasks() { ... updateStatus(); } } void MainWindow::addTask() { ... if (ok && !name.isEmpty()) { ... connect(task, &Task::removed, this, &MainWindow::removeTask); connect(task, &Task::statusChanged, this, &MainWindow::taskStatusChanged); mTasks.append(task); ui->tasksLayout->addWidget(task);
[ 35 ]
Get Your Qt Feet Wet updateStatus(); } } void MainWindow::removeTask(Task* task) { ... delete task; updateStatus(); } void MainWindow::taskStatusChanged(Task* /*task*/) { updateStatus(); } void MainWindow::updateStatus() { int completedCount = 0; for(auto t : mTasks) { if (t->isCompleted()) { completedCount++; } } int todoCount = mTasks.size() - completedCount; ui->statusLabel->setText( QString("Status: %1 todo / %2 completed") .arg(todoCount) .arg(completedCount)); }
We defined a slot taskStatusChanged, which is connected when a task is created. The single instruction of this slot is to call updateStatus(). This function iterates through the tasks and updates the statusLabel. The updateStatus() function is called upon task creation and deletion. In updateStatus(), we meet more new C++11 semantics: for(auto t : mTasks) ... }
{
[ 36 ]
Get Your Qt Feet Wet
The for keyword lets us loop over a range-based container. Because QVector is an iterable container, we can use it here. The range declaration (auto t) is the type and variable name that will be assigned at each iteration. The range expression (mTasks) is simply the container on which the process will be done. Qt provides a custom implementation of the for (namely foreach) loop targeted at prior versions of C++; you don't need it anymore. The auto keyword is another great new semantic. The compiler deduces the variable type automatically based on the initializer. It relieves a lot of pain for cryptic iterators such as this: std::vector::const_iterator iterator = mTasks.toStdVector() .stdTasks.begin(); // how many neurones did you save? auto autoIter = stdTasks.begin();
Since C++14, auto can even be used for function return types. It's a fabulous tool, but use it sparingly. If you put auto, the type should be obvious from the signature name/variable name. The auto keyword can be combined with const and references. You can write a for loop like this: for (const auto & t : mTasks) { ... }.
Remember our half bread lambda? With all the covered features, we can write: auto prettyName = [] (const QString& taskName) -> QString { return "-------- " + taskName.toUpper(); }; connect(ui->removeButton, &QPushButton::clicked, [this, name, prettyName] { qDebug() setupUi(this); SysInfo::instance().init(); ui->centralWidget->layout()->addWidget(&mCpuWidget); }
We simply declare mCpuWidget in the MainWindow.h file, initialize it, and add it to MainWindow->centralWidget->layout. If you now run the application, you should see something like this:
[ 63 ]
Discovering QMake Secrets
Even though it looks cool, this donut is a bit static and does not reflect the CPU usage. Thanks to the architecture we built with the SysInfo and SysInfoWidget classes, the remaining part will be implemented swiftly. Switch back to the CpuWidget.cpp file and implement the updateSeries() function with the following body: void CpuWidget::updateSeries() { double cpuLoadAverage = SysInfo::instance().cpuLoadAverage(); mSeries->clear(); mSeries->append("Load", cpuLoadAverage); mSeries->append("Free", 100.0 - cpuLoadAverage); }
First, we get a reference to our SysInfo singleton. We then retrieve the current average CPU load in the cpuLoadAverage variable. We have to feed this data to our mSeries. The mSeries object is a QPieCharts, which implies that we just want a snapshot of the current CPU average load. Past history is not meaningful with this kind of graph; that's why we clear the mSeries data with the mSeries->clear() syntax, and append the cpuLoadAverage variable and then the free part (100.0 - cpuLoadAverage). The nice thing to note is that, in the CpuWidget class, we don't have to worry about refreshing. All the work is done in the SysInfoWidget subclass with all the whistles and bells of the QTimer class. In a SysInfoWidget subclass, we only have to concentrate on the valuable specific code: what data should be displayed and what kind of graph is used to display it. If you look at the whole CpuWidget class, it is very short. The next SysInfoWidget subclass, MemoryWidget, will also be very concise, as well as quick to implement.
Memory using Qcharts
Our second SysInfoWidget is a MemoryWidget class. This widget will display a history of the data so that we can see how the memory consumption evolves over time. To display this data, we will use a QLineSeries class from the Qt Chart module. Create the MemoryWidget class and follow the same pattern we used for CpuWidget: #include #include "SysInfoWidget.h" class MemoryWidget : public SysInfoWidget
[ 64 ]
Discovering QMake Secrets { Q_OBJECT public: explicit MemoryWidget(QWidget *parent = 0); protected slots: void updateSeries() override; private: QtCharts::QLineSeries* mSeries; qint64 mPointPositionX; };
Instead of a being a QPieSeries*, mSeries is a type of QLineSeries* which will be linked to the chart object in a very similar fashion to MemoryWidget.cpp: #include "MemoryWidget.h" #include using namespace QtCharts; const int CHART_X_RANGE_COUNT = 50; const int CHART_X_RANGE_MAX = CHART_X_RANGE_COUNT - 1; MemoryWidget::MemoryWidget(QWidget *parent) : SysInfoWidget(parent), mSeries(new QlineSeries(this)), mPointPositionX(0) { QAreaSeries* areaSeries = new QAreaSeries(mSeries); QChart* chart = chartView().chart(); chart->addSeries(areaSeries); chart->setTitle("Memory used"); chart->createDefaultAxes(); chart->axisX()->setVisible(false); chart->axisX()->setRange(0, CHART_X_RANGE_MAX); chart->axisY()->setRange(0, 100); } void MemoryWidget::updateSeries() { }
[ 65 ]
Discovering QMake Secrets
The mSeries data is, as usual, initialized in the initializer list. The mPointPositionX is an unsigned long long (using the Qt notation qint64) variable that will track the last X position of our data set. This huge value is used to make sure that mPointPositionX never overflows. We then use an intermediate areaSeries that takes ownership of mSeries upon its initialization in QAreaSeries* areaSeries = new QareaSeries(mSeries). areaSeries is then added to the chart object at chart->addSeries(areaSeries). We do not want to display a single line in our QChart; instead we want to display an area that represents the used memory percentage. That is why we use an areaSeries type. Nonetheless, we will still update the mSeries data when adding new points to the dataset in the updateSeries() function. The areaSeries type will automatically handle them and deliver them to the chart object. After chart->addSeries(areaSeries), we configure the chart display: The chart->createDefaultAxes() function creates an X and Y axis based on the areaSeries type. If we used a 3D series, the createDefaultAxes() function would have added a Z axis. Hide the X axis tick values with chart->axisX()->setVisible(false) (intermediate values displayed at the bottom of the axis). In our MemoryWidget class, this information is not relevant. To define the number of points we want to display–the size of the display history–we call chart->axisX()->setRange(0, CHART_X_RANGE_MAX). Here we use a constant to make it easier to modify this value afterwards. Seeing the value at the top of the file, we avoid having to skim through MemoryWidget.cpp, searching where this value is used to update it. chart->axisY()->setRange(0, 100) defines the maximum range of the Y axis, which is a percentage, based on the value returned by the SysInfo::memoryUsed() function. The chart is now properly configured. We now have to feed it by filling the updateSeries() body: void MemoryWidget::updateSeries() { double memoryUsed = SysInfo::instance().memoryUsed(); mSeries->append(mPointPositionX++, memoryUsed); if (mSeries->count() > CHART_X_RANGE_COUNT) { QChart* chart = chartView().chart(); chart->scroll(chart->plotArea().width() / CHART_X_RANGE_MAX, 0);
[ 66 ]
Discovering QMake Secrets mSeries->remove(0); } }
We first retrieve the latest memory percentage used and append it to mSeries at the X coordinate mPointPositionX (we post-increment it for the next updateSeries() call) and Y coordinate memoryUsed. As we want to keep a history of mSeries, mSeries->clear() is never called. However, what will happen when we add more than CHART_X_RANGE_COUNT points? The visible “window” on the chart is static and the points will be added outside. This means that we will see the memory usage only for the first CHART_X_RANGE_MAX points and then, nothing. Fortunately, QChart provides a function to scroll inside the view to move the visible window. We start to handle this case only when the dataset is bigger than the visible window, meaning if (mSeries->count() > CHART_X_RANGE_COUNT). We then remove the point at the index 0 with mSeries->remove(0) to ensure that the widget will not store an infinite dataset. A SysInfo application that monitors the memory usage and has itself a memory leak is a bit sad. The syntax chart->scroll(chart->plotArea().width() / CHART_X_RANGE_MAX, 0) will then scroll to the latest point on the X axis and nothing on Y. The chart->scroll(dx, dy) expects coordinates expressed in our series coordinates. That is the reason why we have to retrieve the char->plotArea() divided by CHART_X_RANGE_MAX , the X axis unit. We can now add the MemoryWidget class in MainWindow: // In MainWindow.h #include "CpuWidget.h" #include "MemoryWidget.h" ... private: Ui::MainWindow *ui; CpuWidget mCpuWidget; MemoryWidget mMemoryWidget; }; // In MainWindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), mCpuWidget(this), mMemoryWidget(this)
[ 67 ]
Discovering QMake Secrets { ui->setupUi(this); SysInfo::instance().init(); ui->centralWidget->layout()->addWidget(&mCpuWidget); ui->centralWidget->layout()->addWidget(&mMemoryWidget); }
Just as we did for CPUWidget, add a new member named mMemoryWidget to MainWindow and add it to the centralWidget layout with the ui->centralWidget->layout()->addWidget(&mMemoryWidget) syntax. Compile, run the application, and wait a few seconds. You should see something close to this:
The MemoryWidget class works fine, but it looks a bit dull. We can customize it very easily with Qt. The goal is to have a bold line at the top of the memory area and a nice gradient from top to bottom. We just have to modify the areaSeries class in the MemoryWidget.cpp file: #include #include #include
[ 68 ]
Discovering QMake Secrets #include "SysInfo.h" using namespace QtCharts; const const const const
int int int int
CHART_X_RANGE_MAX = 50; COLOR_DARK_BLUE = 0x209fdf; COLOR_LIGHT_BLUE = 0xbfdfef; PEN_WIDTH = 3;
MemoryWidget::MemoryWidget(QWidget *parent) : SysInfoWidget(parent), mSeries(new QLineSeries(this)) { QPen pen(COLOR_DARK_BLUE); pen.setWidth(PEN_WIDTH); QLinearGradient gradient(QPointF(0, 0), QPointF(0, 1)); gradient.setColorAt(1.0, COLOR_DARK_BLUE); gradient.setColorAt(0.0, COLOR_LIGHT_BLUE); gradient.setCoordinateMode(QGradient::ObjectBoundingMode); QAreaSeries* areaSeries = new QAreaSeries(mSeries); areaSeries->setPen(pen); areaSeries->setBrush(gradient); QChart* chart = chartView().chart(); ... }
The QPen pen function is a part of the QPainter API. It is the foundation on which Qt relies to do most of the GUI drawing. This includes the whole QWidget API (QLabel, QPushButton, QLayout, and so on). For the pen, we just have to specify its color and width, and then apply it to the areaSeries class with areaSeries->setPen(pen). The principle is the same for the gradient. We define the starting point (QPointF(0, 0)) and the final point (QPointF(0, 1)) before specifying the color at each end of the vertical gradient. The QGradient::ObjectBoundingMode parameter defines how the start/final coordinates are mapped to the object. With the QAreaSeries class, we want the gradient coordinates to match the whole QareaSeries class. These coordinates are normalized coordinates, meaning that 0 is the start and 1 is the end of the shape: The [0.0] coordinates will point to the top left corner of the QAreaSeries class The [1.0] coordinates will point to the bottom left corner of the QAreaSeries class
[ 69 ]
Discovering QMake Secrets
A last build and run, and the SysInfo application will look like this:
A memory leak or starting a virtual machine is a great way to make your memory go crazy
The SysInfo application is now finished, and we even added some visual polish. You can explore the QGradient classes and the QPainter API if you want to further customize the widget to your taste.
The .pro file in depth
When you click on the Build button, what exactly is Qt Creator doing? How does Qt handle the compilation of the different platforms with a single .pro file? What does the Q_OBJECT macro imply exactly? We will dig into each of these questions in the following sections. Our example case will be the SysInfo application we just completed, and we will study what Qt is doing under the hood.
[ 70 ]
Discovering QMake Secrets
We can start this study by digging into the .pro file. It is the main entry point in compiling any Qt project. Basically, a .pro file is a qmake project file describing the sources and headers used by the project. It is a platform-agnostic definition of a Makefile. First, we can cover the different qmake keywords used in the ch02-sysinfo application: #------------------------------------------------# # Project created by QtCreator 2016-03-24T16:25:01 # #------------------------------------------------QT += core gui charts CONFIG += C++14 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = ch02-sysinfo TEMPLATE = app
Each of these functions has specific roles: #: This is the prefix needed to comment on a line. Yes, we generated the project
on 2016-03-24-crazy, huh? QT: This is a list of the Qt modules used in the project. In the platform-specific Makefile, each of the values will include the module headers and the corresponding library link. CONFIG: This is a list of configuration options for the project. Here, we configure the support of C++14 in the Makefile. TARGET: This is the name of the target output file. TEMPLATE: This is the project template used when generating the Makefile.app tells qmake to generate a Makefile targeted for a binary. If you are building a library, use the lib value. In the ch02-sysinfo application, we started to use platform-specific compilation rules using the intuitive scope mechanism: windows { SOURCES += SysInfoWindowsImpl.cpp HEADERS += SysInfoWindowsImpl.h }
[ 71 ]
Discovering QMake Secrets
If you had to do this with a Makefile, you would probably lose some hair before doing it right (being bald is not an excuse). This syntax is simple yet powerful, and is also used for conditional statements. Let's say you wanted to build some files on debug only. You would have written the following: windows { SOURCES += SysInfoWindowsImpl.cpp HEADERS += SysInfoWindowsImpl.h debug { SOURCES += DebugClass.cpp HEADERS += DebugClass.h } }
Nesting the debug scope inside windows is the equivalent of if (windows && debug). The scoping mechanism is even more flexible; you can have the OR Boolean operator condition with this syntax: windows|unix { SOURCES += SysInfoWindowsAndLinux.cpp }
You can even have else if/else statements: windows|unix { SOURCES += SysInfoWindowsAndLinux.cpp } else:macx { SOURCES += SysInfoMacImpl.cpp } else { SOURCES += UltimateGenericSources.cpp }
In this code snippet, we also see the use of the += operator. The qmake tool provides a wide range of operators to modify the behavior of variables: =: This operator sets the variable to the value. The syntax SOURCES = SysInfoWindowsImpl.cpp would have assigned the singleSysInfoWindowsImpl.cpp value to the SOURCES variable. +=: This operator adds the value to a list of values. This is what we commonly use in HEADERS, SOURCES, CONFIG, and so on. -=: This operator removes the value from the list. You can, for example, add a DEFINE = DEBUG_FLAG syntax in the common section and in a platformspecific scope (say a Windows release) remove it with the DEFINE -= DEBUG_FLAG syntax. [ 72 ]
Discovering QMake Secrets
*=: This operator adds the value to the list only if it is not already present. The DEFINE *= DEBUG_FLAG syntax adds the DEBUG_FLAG value only once. ~=: This operator replaces any values that match a regular expression with the specified value, DEFINE ~= s/DEBUG_FLAG/debug.
You can also define variables in the .pro file and reuse them in different places. We can simplify this with the use of the qmake message() function: COMPILE_MSG = "Compiling on" windows { SOURCES += SysInfoWindowsImpl.cpp HEADERS += SysInfoWindowsImpl.h message($$COMPILE_MSG windows) } linux { SOURCES += SysInfoLinuxImpl.cpp HEADERS += SysInfoLinuxImpl.h message($$COMPILE_MSG linux) } macx { SOURCES += SysInfoMacImpl.cpp HEADERS += SysInfoMacImpl.h message($$COMPILE_MSG mac) }
If you build the project, you will see your platform-specific message each time you build the project in the General Messages tab (you can access this tab from Window | Output Panes | General Messages). Here, we defined a COMPILE_MSG variable and referenced it when calling message($$COMPILE_MSG windows). This offers interesting possibilities when you need to compile external libraries from your .pro file. You can then aggregate all the sources in a variable, combine it with the call to a specific compiler, and so on. If your scope-specific statement is a single line, you can use the following syntax to describe it:
windows:message($$COMPILE_MSG windows)
[ 73 ]
Discovering QMake Secrets
Besides message(), there are a few other helpful functions: error(string): This function displays the string and exits the compilation
immediately. exists(filename): This function tests the existence of the filename. qmake also provides the ! operator, which means you can write !exist(myfile) { ... }. include(filename): This function includes the content of another .pro file. It gives you the ability to slice your .pro files into more modular components. This will prove very useful when you have multiple .pro files for a single big project. All the built-in functions are described at http://doc.qt.io/qt-5/qmake
-test-function-reference.html.
Under the hood of qmake
As we said earlier, qmake is the foundation of the Qt framework compilation system. In Qt Creator, when you click on the Build button, qmake is invoked. Let's study what qmake is exactly doing by calling it ourselves on the CLI (Command Line Interface). Create a temporary directory where you will store the generated files. We are working on a Linux box, but this is transposable on any OS. We chose /tmp/sysinfo. Using the CLI, navigate to this new directory and execute the following command: /path/to/qt/installation/5.7/gcc_64/bin/qmake -makefile -o Makefile /path/to/sysinfoproject/ch02-sysinfo.pro
This command will execute qmake in the -makefile mode to generate a Makefile based on your sysinfo.pro file. If you skim through the Makefile content, you will see many things we covered earlier in the .pro section. The link to Qt modules, headers of different modules, inclusion of the headers and sources files of your project, and so on.
[ 74 ]
Discovering QMake Secrets
Now, let's build this Makefile by simply typing the make command. This command will generate the binary ch02-sysinfo (based on the TARGET value of the .pro file). If you look at the list of files now present in /tmp/sysinfo: $ ls -1 ch02-sysinfo CpuWidget.o main.o MainWindow.o Makefile MemoryWidget.o moc_CpuWidget.cpp moc_CpuWidget.o moc_MainWindow.cpp moc_MainWindow.o moc_MemoryWidget.cpp moc_MemoryWidget.o moc_SysInfoWidget.cpp moc_SysInfoWidget.o SysInfoLinuxImpl.o SysInfo.o SysInfoWidget.o ui_MainWindow.h
Now this is very interesting, we find all our sources compiled in the usual .o extension (SysInfo.o, SysInfoWidget.o, and so on) but there are also a lot of other files prefixed with moc_. Here lies another keystone of the Qt framework: the Meta Object Compiler. Every time you use the signal/slot system, you have to include the macro Q_OBJECT in your header. Each time you emit a signal or receive one in a slot and you did not write any specific code to handle it, Qt took care of it. This is done by generating an intermediate implementation of your class (the moc_*.cpp file) containing everything Qt needs to properly handle your signals and slots.
[ 75 ]
Discovering QMake Secrets
A picture is worth a thousand words. Here is the complete compilation pipeline for a standard qmake project:
The blue boxes refer to commands and the wavy boxes are documents (sources or final binary). Let's walk through the steps: 1. The qmake command is executed with the project .pro file. It generates a Makefile based on the project file. 2. The make command is executed, which will call other commands to generate intermediate files. 3. The uic command stands for User Interface Compiler. It takes all the .ui files (which are basically an XML description of your interface) and generates the corresponding ui_*.h header that you include in your own .cpp (in our ch02sysinfo project, it is in MainWindow.cpp). 4. The moc command takes every class containing the Q_OBJECT macro (paired with the superclass QObject) and generates the intermediate moc_*.cpp files, which include everything needed to make the signal/slot framework work. 5. The g++ command is executed, compiling all your sources' files and intermediate moc files into .o files before finally linking everything in the binary ch02-sysinfo.
[ 76 ]
Discovering QMake Secrets
Note that if you add a Q_OBJECT macro after the creation of a class, sometimes the compiler will complain about your signals and slots. To fix this, simply run the qmake command from Build | Run qmake. You can now see that this stems from the fact that the Makefile has to be regenerated to include the generation of the new intermediate moc file. Generally, source code generation is regarded as bad practice in the developer community. Qt has been criticized on this topic for a long time. We always fear that the machines does some kind of voodoo behind our back. Unfortunately, C++ does not offer any practical way of doing code introspection (namely reflection), and the signal and slots mechanism needs some kind of metadata about your class to resolve your signals and slots. This could have been done partly with the C++ template system, but this solution seemed to Qt to be much less readable, portable, usable, and robust. You also need an excellent compiler support for templates. This cannot be assumed in the wild world of C++ compilers. The moc system is now fully mature. There are some very specific edge cases where it could bring trouble (some have reported problems in very specific situations with Visual Studio), but even so, we think that the gain of this feature largely outweighs the possibly encountered issues. The signal/slot system is a marvel to work with, and if you look at the beginnings of Qt, the system has been present from the very first releases. Adding the functor notation in Qt 5 (which gives a compile time check of the validity of your connect()) combined with C++11 lambas makes it a real delight.
Beneath Q_OBJECT and signals/slots
The Qt building system should be clearer now. Still, the Q_OBJECT macro and the signal/slot/emit keywords are still black boxes. Let's dive into Q_OBJECT. The truth lies in the source code; Q_OBJECT is defined in the file qobjectdefs.h (in Qt 5.7): #define Q_OBJECT \ public: \ // skipped details static const QMetaObject staticMetaObject; \ virtual const QMetaObject *metaObject() const; \ virtual void *qt_metacast(const char *); \ virtual int qt_metacall(QMetaObject::Call, int, void **); \ QT_TR_FUNCTIONS \ private: \ // skipped details qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
[ 77 ]
Discovering QMake Secrets
This macro defines some static functions and a static QMetaObject. The body of these static functions is implemented in the generated moc file. We will not drown you in the gory details of the QMetaObject class. The role of this class is to store all the metainformation for the QObject subclass. It also maintains a correspondence table between the signals and slots of your class, and to the signals and slots of any connected class. Each signal and each slot is assigned with a unique index: The metaObject() function returns the &staticMetaObject for a normal Qt class and a dynamicMetaObject when working with QML objects. The qt_metacast() function performs a dynamic cast using the name of the class. This function is required because Qt does not rely on standard C++ RTTI (Runtime Type Information) to retrieve meta data about an object or a class. The qt_metacall()directly calls an internal signal or slot by its index. Because an index is used rather than a pointer, there is no pointer dereferencing, and the generated switch case can be heavily optimized by the compiler (the compiler can directly include the jump instruction to the specific case very early on, avoiding a lot of branch evaluation). Thus, the execution of the signal/slot mechanism is quite fast. Qt also adds non-standard C++ keywords to manage the signal/slot mechanism, namely signals, slots, and emit. Let's see what is behind each one and see how everything fits inside a connect() function. The slots and signals keywords are also defined in qobjectdefs.h: # #
define slots define signals public
That is right: slots points to nothing and the signals keyword is just a placeholder for the public keyword. All your signals/slots are just… functions. The signals keyword is forced to be public to make sure that your signal functions are visible outside of your class (what is the point of a private signal anyway?). The Qt magic is simply the ability to emit a signal keyword to any connected slot keyword without knowing the detail of the class implementing this slot. Everything is done through the QMetaObject class implementation in the moc file. When a signal keyword is emitted, the function QMetaObject::activate() is called with the changed value and the signals index.
[ 78 ]
Discovering QMake Secrets
The last definition to study is emit: # define emit
So many definitions of nothing, it is almost absurd! The emit keyword is completely useless from a code perspective; moc plainly ignores it and nothing particular happens with it afterwards. It is merely a hint for the developer to notice he is working with signal/slots rather than plain functions. To trigger a slot, you must connect your signal keyword to it using the QObject::connect() function. This function creates a new Connection instance that is defined in qobject_p.h: struct Connection { QObject *sender; QObject *receiver; union { StaticMetaCallFunction callFunction; QtPrivate::QSlotObjectBase *slotObj; }; // The next pointer for the singly-linked ConnectionList Connection *nextConnectionList; //senders linked list Connection *next; Connection **prev; ... };
The Connection instance stores a pointer to the signal emitter class (sender), the slot receiver class (receiver), and the indexes of the connected signal and slot keywords. When a signal is emitted, every connected slot must be called. To be able to do this, every QObject has a linked list of Connection instances for each of its signal, and the same linked list of Connection for each of its slot keywords. This pair of linked lists allows Qt to properly walk through each dependent slot/signal couple to trigger the right functions using the indexes. The same reasoning is used to handle the receiver destruction: Qt walks through the double linked list and removes the object from where it was connected. This walk happens in the famous UI thread, where the whole message loop is processed and every connected signal/slot is triggered according to the possible events (mouse, keyboard, network, and so on). Because the QThread class inherits the QObject, any QThread can use the signal/slot mechanism. Additionally, the signals keyword can be posted to other threads where they will be processed in the receiving threads' event loop. [ 79 ]
Discovering QMake Secrets
Summary
In this chapter, we created a cross-platform SysInfo application. We covered the singleton and the strategy pattern to have a neat code organization with platform-specific code. You learned to use the Qt Charts module to display system information in real time. Finally, we took a deep dive into the qmake command to see how Qt implements the signal/slot mechanism, and to see what is hidden behind Qt-specific keywords (emit, signals, and slots). By now, you should have a clear picture of how Qt works and how you can tackle a crossplatform application. In the next chapter, we will look at how you can split a bigger project in order to keep your sanity as a maintainer. We will study a fundamental pattern in Qt–the Model/View–and discover how to use a database with Qt.
[ 80 ]
3
Dividing Your Project and Ruling Your Code The last chapter delved into qmake to study what lies beneath the signal/slot system and covered a reasonable approach to implementing platform-specific code. This chapter wants to show you how a project can be properly divided to enjoy the maximum leverage from the Qt framework. To do this, you will create a gallery application that handles albums and pictures. You will be able to create, read, update and delete any album and display the pictures in a grid of thumbnails or in full resolution. Everything will be persisted in a SQL database. This chapter lays the foundations of the gallery by creating a core library that will be used in the following two chapters: Chapter 4, Conquering the Desktop UI, and Chapter 5, Dominating the Mobile UI. This chapter covers the following topics: Application/library project separation Database interaction with Qt Smart pointers with C++14 Model/View architecture in Qt with an implementation of the model
Dividing Your Project and Ruling Your Code
Designing a maintainable project
The first step in designing a maintainable project is to properly split it in clearly defined modules. A common approach is to separate the engine from the user interface. This separation forces you to reduce coupling between the different parts of your code and make it more modular. This is exactly the approach we will take with the gallery application. The project will be divided into three sub-projects:
The sub-projects are as follows: gallery-core: This is a library containing the core of the application logic: the data classes (or business classes), persistent storage (in SQL), and the model that makes the storage available to the UI through a single entry point. gallery-desktop: This is a Qt widgets application that will depend on the gallery-core library to retrieve data and display it to the user. This project will be covered in Chapter 4, Conquering the Desktop UI. gallery-mobile: This is a QML application targeted at mobile platforms (Android and iOS). It will also rely on gallery-core. This project will be covered in Chapter 5, Dominating the Mobile UI. As you can see, each layer has a single responsibility. This principle is applied to both the project structure and the code organization. Throughout these three projects, we will endeavor to live up to the motto of the chapter: “Divide your project and rule your code”. To separate your Qt project this way, we will create a different kind of project, a Subdirs project: 1. Click on File | New File or Project. 2. In the Projects types, select Other Project | Subdirs Project | Choose.
[ 82 ]
Dividing Your Project and Ruling Your Code
3. Name it ch03-gallery-core and then click on Choose. 4. Select your latest Qt Desktop Kit, and then click on Next | Finish & Add Subproject. Here, Qt Creator created the parent project, ch03-gallery-core, which will host our three sub-projects (gallery-core, gallery-desktop, and gallery-mobile). The parent project has neither code nor a compilation unit in itself, it is simply a convenient way to group multiple .pro projects and express the dependencies between them. The next step is to create the first subdir project, which Qt Creator proposed immediately when you clicked on Finish & Add Subproject. We will start with gallery-core: 1. 2. 3. 4. 5.
Select Library in the Projects tab. Select C++ Library. Choose the Shared Library type, and name it gallery-core, and click on Next. Select the modules, QtCore, and QtSql, and then click on Next. Type Album in the Class name field, and click on Next. Qt Creator will generate the basic skeleton of a library with this class as an example. 6. Check that the project is properly added as a sub-project of ch03-gallerycore.pro and click on Finish.
Before delving into gallery-core code, let's study what Qt Creator just made for us. Open the parent .pro file, ch03-gallery-core.pro: TEMPLATE = subdirs SUBDIRS += \ gallery-core
Until now, we used the TEMPLATE = app syntax in our .pro files. The subdirs project template indicates to Qt that it should search for sub-projects to compile. When we added the gallery-core project to ch03-gallery-core.pro, Qt Creator added it to the SUBDIRS variable. As you can see, SUBDIRS is a list, so you can add as many subprojects as you want.
[ 83 ]
Dividing Your Project and Ruling Your Code
When compiling ch03-gallery-core.pro, Qt will scan each SUBDIRS value to compile them. We can now switch to gallery-core.pro: QT QT
+= sql -= gui
TARGET = gallery-core TEMPLATE = lib DEFINES += GALLERYCORE_LIBRARY SOURCES += Album.cpp HEADERS += Album.h\ gallery-core_global.h unix { target.path = /usr/lib INSTALLS += target }
Let's see how this works: The QT has appended the sql module and removed the gui module. By default, QtGui is always included and has to be removed explicitly. The TEMPLATE value is different, again. We use lib to tell qmake to generate a Makefile that will output a shared library named gallery-core (as specified by the TARGET variable). The DEFINES += GALLERY_CORE_LIBRARY syntax is a compilation flag that lets the compiler know when it should import or export library symbols. We will come back soon to this notion. The HEADERS contains our first class Album.h, but also another generated header: gallery-core_global.h. This file is syntactic sugar provided by Qt to ease the pain of a cross-platform library. The unix { ... } scope specifies the installation destination of the library. This platform scope is generated because we created the project on Linux. By default it will try to install the library in the system library path (/usr/lib). Please remove the unix scope altogether, we don't need to make the library available system-wide.
[ 84 ]
Dividing Your Project and Ruling Your Code
To have a better understanding of the cross-platform shared object issue, you can open gallery-core_global.h: #include #if defined(GALLERYCORE_LIBRARY) # define GALLERYCORESHARED_EXPORT Q_DECL_EXPORT #else # define GALLERYCORESHARED_EXPORT Q_DECL_IMPORT #endif
We encounter again the GALLERYCORE_LIBRARY defined in gallery-core.pro file. Qt Creator generated a useful piece of code for us: the cross-platform way to handle symbol visibility in a shared library. When your application links to a shared library, symbol functions, variables, or classes must be marked in a special way to be visible by the application using the shared library. The default visibility of a symbol depends on the platform. Some platforms will hide symbols by default, other platforms will make them public. Of course, each platform and compiler has its own macros to express this public/private notion. To obviate the whole #ifdef windows #else boilerplate code, Qt provides a Q_DECL_EXPORT (if we are compiling the library) and Q_DECL_IMPORT (if we are compiling your application using the shared library). Thus, throughout the symbols you want to mark as public, you just have to use the GALLERYCORESHARED_EXPORT macro. An example is available in the Album.h file: #ifndef ALBUM_H #define ALBUM_H #include "gallery-core_global.h" class GALLERYCORESHARED_EXPORT Album { public: Album(); }; #endif // ALBUM_H
[ 85 ]
Dividing Your Project and Ruling Your Code
You include the proper gallery-core_global.h file to have access to the macro and you use it just after the class keyword. It does not pollute your code too much and is still crossplatform. Another possibility is to make a Statically Linked Library. This path is interesting if you want fewer dependencies to handle (a single binary is always easier to deploy). There are several downsides: Increased compilation time: each time you modify the library, the application will have to be recompiled as well. Tighter coupling, multiple applications cannot link to your library. Each one of them must embed it.
Defining data classes
We are building our gallery from the ground up. We will start with the implementation of our data classes to be able to properly write the database layer. The application aims to organize pictures into albums. Hence, the two obvious classes are Album and Picture. In our example, an album simply has a name. A Picture class must belong to an Album class and has a file path (the path on your filesystem where the original file is located). The Album class has already been created on project creation. Open the Album.h file and update it to include the following implementation: #include #include "gallery-core_global.h" class GALLERYCORESHARED_EXPORT Album { public: explicit Album(const QString& name = ""); int id() const; void setId(int id); QString name() const; void setName(const QString& name); private: int mId; QString mName; };
[ 86 ]
Dividing Your Project and Ruling Your Code
As you can see, the Album class contains only a mId variable (the database ID) and a mName variable. In a typical OOP (Object Oriented Paradigm) fashion, the Album class would have had a QVectormPictures field. We did not do it on purpose. By decoupling these two objects, we will have more flexibility when we want to load an album without pulling the potential thousands of associated pictures. The other problem in having mPictures in the Album class is that the developer (you or anybody else) using this code will ask himself: when is mPictures loaded? Should I do a partial load of the Album and have an incomplete Album or should I always load Album with every picture in it? By completely removing the field, the question ceases to exist, and the code is simpler to grasp. The developer knows intuitively that he will have to explicitly load the pictures if he want them; otherwise, he can continue with this simple Album class. The getters and setters are obvious enough; we will let you implement them without showing them to you. We will only take a look at the Album class' constructor in Album.cpp: Album::Album(const QString& name) : mId(-1), mName(name) { }
The mId variable is initialized to -1 to be sure that, by default, an invalid id is used, and the mName variable is assigned a name value. We can now proceed to the Picture class. Create a new C++ class named Picture and open Picture.h to modify it like so: #include #include #include "gallery-core_global.h" class GALLERYCORESHARED_EXPORT Picture { public: Picture(const QString& filePath = ""); Picture(const QUrl& fileUrl); int id() const; void setId(int id); int albumId() const; void setAlbumId(int albumId);
[ 87 ]
Dividing Your Project and Ruling Your Code QUrl fileUrl() const; void setFileUrl(const QUrl& fileUrl); private: int mId; int mAlbumId; QUrl mFileUrl; };
Do not forget to add the GALLERYCORESHARED_EXPORT macro right before the class keyword to export the class from the library. As a data structure, Picture has a mId variable, belongs to a mAlbumId variable, and has a mUrl value. We use the QUrl type to make path manipulation easier to use depending on the platform (desktop or mobile). Let's take a look at Picture.cpp: #include "Picture.h" Picture::Picture(const QString& filePath) : Picture(QUrl::fromLocalFile(filePath)) { } Picture::Picture(const QUrl& fileUrl) : mId(-1), mAlbumId(-1), mFileUrl(fileUrl) { } QUrl Picture::fileUrl() const { return mFileUrl; } void Picture::setFileUrl(const QUrl& fileUrl) { mFileUrl = fileUrl; }
In the first constructor, the static function, QUrl::fromLocalFile, is called to provide a QUrl object to the other constructor, which takes a QUrl parameter. The ability to call other constructors is a nice addition in C++11.
[ 88 ]
Dividing Your Project and Ruling Your Code
Storing your data in a database
Now that the data classes are ready, we can proceed to implement the database layer. Qt provides a ready-to-use sql module. Various databases are supported in Qt using SQL database drivers. In gallery-desktop, we will use the SQLITE3 driver, which is included in the sql module and perfectly fits the use case: A very simple database schema: No need for complex queries Very few or no concurrent transactions: No need for a complex transaction model A single-purpose database: No need to spawn a system service, the database is stored in a single file and does not need to be accessed by multiple applications The database will be accessed from multiple locations; we need to have a single entry point for it. Create a new C++ class named DatabaseManager and modify DatabaseManager.h to look like this: #include class QSqlDatabase; const QString DATABASE_FILENAME = "gallery.db"; class DatabaseManager { public: static DatabaseManager& instance(); ~DatabaseManager(); protected: DatabaseManager(const QString& path = DATABASE_FILENAME); DatabaseManager& operator=(const DatabaseManager& rhs); private: QSqlDatabase* mDatabase; };
The first thing to notice is that we implement the singleton pattern in the DatabaseManager class, like we did in the Transforming SysInfo in a singleton section from Chapter 2, Discovering QMake Secrets. The DatabaseManager class will open the connection in the mDatabase field and lend it to other possible classes.
[ 89 ]
Dividing Your Project and Ruling Your Code
Also, QSqlDatabase is forward-declared and used as a pointer for the mDatabase field. We could have included the QSqlDatabase header, but we would have had a non-desired side-effect: every file, which includes DatabaseManager, must also include QSqlDatabase. Thus, if we ever have some transitive inclusion in our application (which links to the gallery-core library), the application is forced to enable the sql module. As a consequence, the storage layer leaks through the library. The application should not have any knowledge about the storage layer implementation. For all the application cares, it could be in SQL, XML, or anything else; the library is a black box that should honor the contract and persist the data. Let's switch to DatabaseManager.cpp and open the database connection: #include "DatabaseManager.h" #include DatabaseManager& DatabaseManager::instance() { static DatabaseManager singleton; return singleton; } DatabaseManager::DatabaseManager(const QString& path) : mDatabase(new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE"))) { mDatabase->setDatabaseName(path); mDatabase->open(); } DatabaseManager::~DatabaseManager() { mDatabase->close(); delete mDatabase; }
The correct database driver is selected on the mDatabase field initialization with the QSqlDatabase::addDatabase("QSQLITE") function call. The following steps are just a matter of configuring the database name (which is incidentally the file path in SQLITE3) and opening the connection with the mDatabase->open() function. In the DatabaseManager destructor, the connection is closed and the mDatabase pointer is properly deleted.
[ 90 ]
Dividing Your Project and Ruling Your Code
The database link is now opened; all we have to do is to execute our Album and Picture queries. Implementing the CRUD (Create/Read/Update/Delete) for both our data classes in DatabaseManager would quickly push DatabaseManager.cpp to be several hundreds of lines long. Add a few more tables and you can already see what a monster DatabaseManager would turn into. For this reason, each of our data classes will have a dedicated database class, responsible for all the database CRUD operations. We will start with the Album class; create a new C++ class named AlbumDao (data access object) and update AlbumDao.h: class QSqlDatabase; class AlbumDao { public: AlbumDao(QSqlDatabase& database); void init() const; private: QSqlDatabase& mDatabase; };
The AlbumDao class's constructor takes a QSqlDatabase& parameter. This parameter is the database connection that will be used for all the SQL queries done by the AlbumDao class. The init() function aims to create the albums table and should be called when mDatabase is opened. Let's see the implementation of AlbumDao.cpp: #include #include #include "DatabaseManager.h" AlbumDao::AlbumDao(QSqlDatabase& database) : mDatabase(database) { } void AlbumDao::init() const { if (!mDatabase.tables().contains("albums")) { QSqlQuery query(mDatabase); query.exec("CREATE TABLE albums (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); }
[ 91 ]
Dividing Your Project and Ruling Your Code }
As usual, the mDatabase field is initialized with the database parameter. In the init() function, we can see a real SQL request in action. If the table albums class does not exist, a QSqlQuery query is created that will use the mDatabase connection to be executed. If you omit mDatabase, the query will use a default anonymous connection. The query.exec() function is the simplest manner of executing a query: you simply pass the QString type of your query and it's done. Here we create the albums table with the fields matching the data class Album (id and name). The QSqlQuery::exec() function returns a bool value that indicates if the request has been successful. In your production code, always check this value. You can further investigate the error with QSqlQuery::lastError(). An example is available in the source code of the chapter in DatabaseManager::debugQuery(). The skeleton of AlbumDao class is done. The next step is to link it to the DatabaseManager class. Update the DatabaseManager class like so: // In DatabaseManager.h #include "AlbumDao.h" ... private: QSqlDatabase* mDatabase; public: const AlbumDao albumDao; }; // In DatabaseManager.cpp DatabaseManager::DatabaseManager(const QString& path) : mDatabase(new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE"))), albumDao(*mDatabase) { mDatabase->setDatabaseName(path); mDatabase->open(); albumDao.init(); }
[ 92 ]
Dividing Your Project and Ruling Your Code
The albumDao field is declared as a public const AlbumDao in the DatabaseManager.h file. This needs some explanation: The public visibility is to give access to DatabaseManager clients to the albumDao field. The API becomes intuitive enough; if you want to make a database operation on an album, just call DatabaseManager::instance().albumDao. The const keyword is to make sure that nobody can modify albumDao. Because it is public, we cannot guarantee the safety of the object (anybody could modify the object). As a side-effect, we force every public function of AlbumDao to be const. This makes sense; after all, the AlbumDao field could have been a namespace with a bunch of functions. It is more convenient for it to be a class because we can keep the reference to the database connection with the mDatabase field. In the DatabaseManager constructor, the albumDao class is initialized with the mDatabase dereferenced pointer. The albumDao.init() function is called after the database connection has opened. We can now proceed to implement more interesting SQL queries. We can start with the creation of a new album in the AlbumDao class: // In AlbumDao.h class QSqlDatabase; class Album; class AlbumDao { public: AlbumDao(QSqlDatabase& database); void init() const; void addAlbum(Album& album) const; ... }; // In AlbumDao.cpp #include #include #include ... void AlbumDao::addAlbum(Album& album) const
[ 93 ]
Dividing Your Project and Ruling Your Code { QSqlQuery query(mDatabase); query.prepare("INSERT INTO albums (name) VALUES (:name)"); query.bindValue(":name", album.name()); query.exec(); album.setId(query.lastInsertId().toInt()); }
The addAlbum() function takes an album parameter to extract its information and execute the corresponding query. Here, we approach the prepared query notion: the query.prepare() function takes a query parameter which contains placeholders for parameters provided later. We will provide the name parameter with the syntax :name. Two syntaxes are supported: Oracle style with a colon-name (for example, :name) or ODBC style with a question mark (for example, ?name). We then bind the bind :name syntax to the value of the album.name() function. Because QSqlQuery::bind() expects a QVariant as a parameter value, we have to add the include directive to this class. In a nutshell, a QVariant is a generic data holder that accepts a wide range of primitive types (char, int, double, and so on) and complex types (QString, QByteArray, QUrl, and so on). When the query.exec() function is executed, the bound values are properly replaced. The prepare() statement technique makes the code more robust to SQL injection (injecting a hidden request would fail) and more readable. The execution of the query modifies the state of the object query itself. The QSqlQuery query is not simply a SQL query executor, it also contains the state of the active query. We can retrieve information about the query with the query.lastInsertId() function, which returns a QVariant value containing the ID of the album row we just inserted. This id is given to the album provided in the addAlbum() parameter. Because we modify album, we cannot mark the parameter as const. Being strict about the const correctness of your code is a good hint for a fellow developer, who can deduce that your function might (or not) modify the passed parameter. The remaining update and delete operations follow strictly the same pattern used for addAlbum(). We will just provide the expected function signatures in the next code snippet. Please refer to the source code of the chapter for the complete implementation. However, we need to implement the request to retrieve all the albums in the database. This one deserves a closer look: // In AlbumDao.h
[ 94 ]
Dividing Your Project and Ruling Your Code #include ... void addAlbum(Album& album) const; void updateAlbum(const Album& album) const; void removeAlbum(int id) const; QVector albums() const; ... }; // In AlbumDao.cpp QVector AlbumDao::albums() const { QSqlQuery query("SELECT * FROM albums", mDatabase); query.exec(); QVector list; while(query.next()) { Album* album = new Album(); album->setId(query.value("id").toInt()); album->setName(query.value("name").toString()); list.append(album); } return list; }
The albums() function must return a QVector value. If we take a look at the body of the function, we see yet another property of QSqlQuery. To walk through multiple rows for a given request, query handles an internal cursor pointing to the current row. We can then proceed to create a new Album*() function and fill it with the row data with the query.value() statement, which takes a column name parameter and returns a QVariant value that is casted to the proper type. This new album parameter is appended to the list and, finally, this list is returned to the caller. The PictureDao class is very similar to the AlbumDao class, both in usage and implementation. The main difference is that a picture has a foreign key to an album. The PictureDao function must be conditioned by an albumId parameter. The following code snippet shows the PictureDao header and the init() function: // In PictureDao.h #include class QSqlDatabase; class Picture; class PictureDao {
[ 95 ]
Dividing Your Project and Ruling Your Code public: explicit PictureDao(QSqlDatabase& database); void init() const; void addPictureInAlbum(int albumId, Picture& picture) const; void removePicture(int id) const; void removePicturesForAlbum(int albumId) const; QVector picturesForAlbum(int albumId) const; private: QSqlDatabase& mDatabase; }; // In PictureDao.cpp void PictureDao::init() const { if (!mDatabase.tables().contains("pictures")) { QSqlQuery query(mDatabase); query.exec(QString("CREATE TABLE pictures") + " (id INTEGER PRIMARY KEY AUTOINCREMENT, " + "album_id INTEGER, " + "url TEXT)"); } }
As you can see, multiple functions take an albumId parameter to make the link between the picture and the owning album parameter. In the init() function, the foreign key is expressed in the album_id INTEGER syntax. SQLITE3 does not have a proper foreign key type. It is a very simple database and there is no strict constraint for this type of field; a simple integer is used. Finally, the PictureDao function is added in the DatabaseManager class exactly as we did for albumDao. One could argue that, if there are a lot of Dao classes, adding a const Dao member in the DatabaseManager class and calling the init() function quickly becomes cumbersome. A possible solution could be to make an abstract Dao class, with a pure virtual init() function. The DatabaseManager class would have a Dao registry, which maps each Dao to a QString key with a QHash mDaos. The init() function call would then be called in a for loop and a Dao object would be accessed using the QString key. This is outside the scope of this project, but is nevertheless an interesting approach.
[ 96 ]
Dividing Your Project and Ruling Your Code
Protecting your code with a smart pointer
The code we just described is fully functional, but, it can be strengthened, specifically with the function, AlbumDao::albums(). In this function, we iterate through the database rows and create a new Album to fill a list. We can zoom in on this specific code section: QVector list; while(query.next()) { Album* album = new Album(); album->setId(query.value("id").toInt()); album->setName(query.value("name").toString()); list.append(album); } return list;
Let's say that the name column has been renamed to title. If you forget to update query.value("name"), you might run into trouble. The Qt framework does not rely on exceptions, but this cannot be said for every API available in the wild. An exception here would cause a memory leak: the Album* album function has been allocated on the heap but not released. To handle this, you would have to surround the risky code with a try catch statement and deallocate the album parameter if an exception has been thrown. Maybe this error should bubble up; hence, your try catch statement is only there to handle the potential memory leak. Can you picture the spaghetti code weaving in front of you? The real issue with pointers is the uncertainty of their ownership. Once it has been allocated, who is the owner of a pointer? Who is responsible for deallocating the object? When you pass a pointer as a parameter, when does the caller retain the ownership or release it to the callee? Since C++11, a major milestone has been reached in memory management: the smart pointer feature has been stabilized and can greatly improve the safety of your code. The goal is to explicitly indicate the ownership of a pointer through simple template semantics. There are three types of smart pointer: The unique_ptr pointer indicates that the owner is the only owner of the pointer The shared_ptr pointer indicates that the pointer's ownership is shared among several clients The weak_ptr pointer indicates that the pointer does not belong to the client For now, we will focus on the unique_ptr pointer to understand smart pointer mechanics.
[ 97 ]
Dividing Your Project and Ruling Your Code
A unique_ptr pointer is simply a variable allocated on the stack that takes the ownership of the pointer you provide with it. Let's allocate an Album with this semantic: #include void foo() { Album* albumPointer = new Album(); std::unique_ptr album(albumPointer); album->setName("Unique Album"); }
The whole smart pointer API is available in the memory header. When we declared album as a unique_ptr, we did two things: We allocated on the stack a unique_ptr. The unique_ptr pointer relies on templates to check at compile time the validity of the pointer type. We granted the ownership of albumPointer memory to album. From this point on, album is the owner of the pointer. This simple line has important ramifications. First and foremost, you do not have to worry anymore about the pointer life cycle. Because a unique_ptr pointer is allocated on the stack, it will be destroyed as soon as it goes out of scope. In this example, when we exit foo(), album will be removed from the stack. The unique_ptr implementation will take care of calling the Album destructor and deallocating the memory. Secondly, you explicitly indicate the ownership of your pointer at compile time. Nobody can deallocate the albumPointer content if they do not voluntarily fiddle with your unique_ptr pointer. Your fellow developers will also know with a single glance who is the owner of your pointer. Note that, even though album is a type of unique_ptr, you can still call Album functions (for example, album->setName()) using the -> operator. This is possible thanks to the overload of this operator. The usage of the unique_ptr pointer becomes transparent. Well, this use case is nice, but the purpose of a pointer is to be able to allocate a chunk of memory and share it. Let's say the foo() function allocates the album unique_ptr pointer and then transfers the ownership to bar(). This would look like this: void foo() { std::unique_ptr album(new Album()); bar(std::move(album)); }
[ 98 ]
Dividing Your Project and Ruling Your Code void bar(std::unique_ptr barAlbum) { qDebug() (arrow) when working with a unique_ptr pointer: The -> operator dereferences to the pointer members and lets your call function on your real object The . operator gives you access to the unique_ptr object functions The unique_ptr pointer provides various functions. Among the most important are: The get() function returns the raw pointer. The album.get() returns an Album* value. The release() function releases the ownership of the pointer and returns the raw pointer. The album.release() function returns an Album* value. The reset(pointer p = pointer()) function destroys the currently managed pointer and takes ownership of the given parameter. An example would be the barAlbum.reset() function, which destroys the currently owned Album*. With a parameter, barAlbum.reset(new Album()) also destroys the owned object and takes the ownership of the provided parameter.
[ 99 ]
Dividing Your Project and Ruling Your Code
Finally, you can dereference the object with the * operation, meaning *album will return an Album& value. This dereferencing is convenient, but you will see that the more a smart pointer is used, the less you will need it. Most of the time, you will replace a raw pointer with the following syntax: void bar(std::unique_ptr& barAlbum);
Because we pass the unique_ptr by reference, bar() does not take ownership of the pointer and will not try do deallocate it upon its exit. With this, there is no need to use move(album) in foo(); the bar() function will just do operations on the album parameter but will not take its ownership. Now, let's consider shared_ptr. A shared_ptr pointer keeps a reference counter on a pointer. Each time a shared_ptr pointer references the same object, the counter is incremented; when this shared_ptr pointer goes out of scope, the counter is decremented. When the counter reaches zero, the object is deallocated. Let's rewrite our foo()/bar() example with a shared_ptr pointer: #include void foo() { std::shared_ptr album(new Album()); // ref counter = 1 bar(album); // ref counter = 2 } // ref counter = 0 void bar(std::shared_ptr barAlbum) { qDebug() push_back(move(album)). Until that line, the album is “owned” by the while scope, the move gives the ownership to the list. At the last instruction, return list, we should have written move(list), but C++11 accepts the direct return and will automatically make the move() function for us. What we covered in this section is that the AlbumDao class is completely matched in PictureDao. Please refer to the source code of the chapter to see the full PictureDao class implementation.
Implementing the model
The data is ready to be exposed to potential clients (the applications that will display and edit its content). However, a direct connection between the client and the database will make a very strong coupling. If we decide to switch to another storage type, the view would have to be rewritten, partially at least. This is where the model comes to our rescue. It is an abstract layer that communicates with the data (our database) and exposes this data to the client in a data-specific, implementation-agnostic form. This approach is a direct offspring of the MVC (Model View Controller) concept. Let's recapitulate how MVC works: The Model manages the data. It is responsible for requesting for the data and updating it. The View displays the data to the user. The Controller interacts with both the Model and the View. It is responsible for feeding the View with the correct data and sending commands to the Model based on the user interaction received from the View. This paradigm enables swapping various parts without disturbing the others. Multiple views can display the same data, the data layer can be changed, and the upper parts will not be aware of it.
[ 104 ]
Dividing Your Project and Ruling Your Code
Qt combines the View and the Controller to form the Model/View architecture. The separation of the storage and the presentation is retained while being simpler to implement than a full MVC approach. To allow editing and view customization, Qt introduces the concept of Delegate, which is connected to both the Model and the View:
The Qt documentation about Model/View is truly plethoric. It is nevertheless easy to get lost in the details; it feels sometimes a bit overwhelming. We will try to clear things up by implementing the AlbumModel class and seeing how it works. Qt offers various Model sub-classes that all extend from QAbstractItemModel. Before starting the implementation, we have to carefully choose which base class will be extended. Keep in mind that our data are variations on lists: we will have a list of albums, and each album will have a list of pictures. Let's see what Qt offers us: QAbstractItemModel: This class is the most abstract, and therefore, the most
complex, to implement. We will have to redefine a lot of functions to properly use it. QStringListModel: This class is a model that supplies strings to views. It is too simple. Our model is more complex (we have custom objects). QSqlTableModel (or QSqLQueryModel): This class is a very interesting contender. It automatically handles multiple SQL queries. On the other hand, it works only for very simple table schemas. In the pictures table, for example, the album_id foreign key makes it very hard to fit this model. You might save some lines of code, but if feels like trying to shoehorn a round peg into a square hole. [ 105 ]
Dividing Your Project and Ruling Your Code
QAbstractListModel: This class provides a model that offers one-dimensional lists. This fits nicely with our requirements, saves a lot of key strokes, and is still flexible enough.
We will go with the QabstractListModel class and create a new C++ class named AlbumModel. Update the AlbumModel.h file to look like this: #include #include #include #include
#include "gallery-core_global.h" #include "Album.h" #include "DatabaseManager.h" class GALLERYCORESHARED_EXPORT AlbumModel : public QAbstractListModel { Q_OBJECT public: enum Roles { IdRole = Qt::UserRole + 1, NameRole, }; AlbumModel(QObject* parent = 0); QModelIndex addAlbum(const Album& album); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; bool removeRows(int row, int count, const QModelIndex& parent) override; QHash roleNames() const override; private: bool isIndexValid(const QModelIndex& index) const; private: DatabaseManager& mDb; std::unique_ptr mAlbums; };
[ 106 ]
Dividing Your Project and Ruling Your Code
The AlbumModel class extends the QAbstractListModel class and has only two members: mDb: This is the link to the database. In the Model/View schema, the model will communicate with the data layer through mDb. mAlbums: This acts as a buffer that will avoid hitting the database too much. The type should remind you of what we wrote for AlbumDao::albums() with the
smart pointers.
The only specific functions the AlbumModel class has are addAlbum() and isIndexValid(). The rest are overrides of QAbstractListModel functions. We will go through each of these functions to understand how a model works. First, let's see how the AlbumModel class is constructed in the AlbumModel.cpp file: AlbumModel::AlbumModel(QObject* parent) : QAbstractListModel(parent), mDb(DatabaseManager::instance()), mAlbums(mDb.albumDao.albums()) { }
The mDb file is initialized with the DatabaseManager singleton address, and, after that, we see the now famous AlbumDao::albums() in action. The vector type is returned and initializes mAlbums. This syntax make the ownership transfer automatic without any need for an explicit call to the std::move() function. If there are any stored albums in the database, mAlbums is immediately filled with those. Each time the model interacts with the view (to notify us about changes or to serve data), mAlbums will be used. Because it is in memory only, reading will be very fast. Of course, we have to be careful about maintaining mAlbum coherently with the database state, but everything will stay inside the AlbumModel inner mechanics. As we said earlier, the model aims to be the central point to interact with the data. Each time the data changes, the model will emit a signal to notify the view; each time the view wants to display data, it will request the model for it. The AlbumModel class overrides everything needed for read and write access. The read functions are: rowCount(): This function is used to get the list size data(): This function is used to get a specific piece of information about the data
to display roleNames(): This function is used to indicate to the framework the name for each “role”. We will explain in a few paragraphs what a role is [ 107 ]
Dividing Your Project and Ruling Your Code
The editing functions are: setData(): This function is used to update data removeRows(): This function is used to remove data
We will start with the read part, where the view asks the model for the data. Because we will display a list of albums, the first thing the view should know is how many items are available. This is done in the rowCount() function: int AlbumModel::rowCount(const QModelIndex& parent) const { return mAlbums->size(); }
Being our buffer object, using mAlbums->size() is perfect. There is no need to query the database, as mAlbums is already filled with all the albums of the database. The rowCount() function has an unknown parameter: a const QModelIndex& parent. Here, it is not used, but we have to explain what lies beneath this type before continuing our journey in the AlbumModel class. The QModelIndex class is a central notion of the Model/View framework in Qt. It is a lightweight object used to locate data within a model. We use a simple QAbstractListModel class, but Qt is able to handle three representation types:
There is no better explanation than an official Qt diagram
[ 108 ]
Dividing Your Project and Ruling Your Code
Let's now see the models in detail: List Model: In this model, the data is stored in a one-dimensional array (rows) Table Model: In this model, the data is stored in a two-dimensional array (rows and columns) Tree Model: In this model, the data is stored in a hierarchical relationship (parent/children) To handle all these model types, Qt came up with the QModelIndex class, which is an abstract way of dealing with them. The QModelIndex class has the functions for each of the use cases: row(), column(), and parent()/child(). Each instance of a QModelIndex is meant to be short-lived: the model might be updated and thus the index will become invalid. The model will produce indexes according to its data type and will provide these indexes to the view. The view will then use them to query back new data to the model without needing to know if an index.row() function corresponds to a database row or a vector index. We can see the index parameter in action with the implementation of data(): QVariant AlbumModel::data(const QModelIndex& index, int role) const { if (!isIndexValid(index)) { return QVariant(); } const Album& album = *mAlbums->at(index.row()); switch (role) { case Roles::IdRole: return album.id(); case Roles::NameRole: case Qt::DisplayRole: return album.name(); default: return QVariant(); } }
The view will ask for data with two parameters: an index and a role. As we have already covered the index, we can focus on the role responsibility.
[ 109 ]
Dividing Your Project and Ruling Your Code
When the data is displayed, it will probably be an aggregation of multiple data. For example, displaying the picture will consist of a thumbnail and the picture name. Each one of these data elements needs to be retrieved by the view. The role parameter fills this need, it associates each data element to a tag for the view to know what category of data is shown. Qt provides various default roles (DisplayRole, DecorationRole, EditRole, and so on) and you can define your own if needed. This is what we did in the AlbumModel.h file with the enum Roles: we added an IdRole and a NameRole. The body of the data() function is now within our reach! We first test the validity of the index with a helper function, isIndexValid(). Take a look at the source code of the chapter to see what it does in detail. The view asked for data at a specific index: we retrieve the album row at the given index with *mAlbums->at(index.row()). This returns a unique_ptr value at the index.row() index and we dereference it to have an Album&. The const modifier is interesting here because we are in a read function, and it makes no sense to modify the album row. The const modifier adds this check at compile time. The switch on the role parameter tells us what data category should be returned. The data() function returns a QVariant value, which is the Awiss Army Knife of types in Qt. We can safely return the album.id(), album.name(), or a default QVariant() if we do not handle the specified role. The last read function to cover is roleNames(): QHash AlbumModel::roleNames() const { QHash roles; roles[Roles::IdRole] = "id"; roles[Roles::NameRole] = "name"; return roles; }
At this level of abstraction, we do not know what type of view will be used to display our data. If the views are written in QML, they will need some meta-information about the data structure. The roleNames() function provides this information so the role names can be accessed via QML. If you are writing for a desktop widget view only, you can safely ignore this function. The library we are currently building will be used for QML; this is why we override this function.
[ 110 ]
Dividing Your Project and Ruling Your Code
The reading part of the model is now over. The client view has everything it needs to properly query and display the data. We shall now investigate the editing part of AlbumModel. We will start with the creation of a new album. The view will build a new Album object and pass it to Album::addAlbum() to be properly persisted: QModelIndex AlbumModel::addAlbum(const Album& album) { int rowIndex = rowCount(); beginInsertRows(QModelIndex(), rowIndex, rowIndex); unique_ptr newAlbum(new Album(album)); mDb.albumDao.addAlbum(*newAlbum); mAlbums->push_back(move(newAlbum)); endInsertRows(); return index(rowIndex, 0); }
Indexes are a way to navigate within the model data. This first thing we do is to determinate what will be the index of this new album by getting the mAlbums size with rowCount(). From here, we start to use specific model functions: beginInsertRows() and endInsertRows(). These functions wrap real data modifications. Their purpose is to automatically trigger signals for whoever might be interested: beginInsertRows(): This function informs that rows are about to change for
the given indexes endInsertRows(): This function informs that rows have been changed
The first parameter of the beginInsertRows() function is the parent for this new element. The root for a model is always an empty QModelIndex() constructor. Because we do not handle any hierarchical relationship in AlbumModel, it is safe to always add the new element to the root. The following parameters are the first and last modified indexes. We insert a single element per call, so we provide rowIndex twice. To illustrate the usage of this signal, a view might, for example, display a loading message telling the user “Saving 5 new albums”. For endInsertRows(), the interested view might hide the saving message and display “Save finished”.
[ 111 ]
Dividing Your Project and Ruling Your Code
This may look strange at first, but it enables Qt to handle automatically a lot of signaling for us and in a generic way. You will see very soon how well this works when designing the UI of the application in Chapter 4, Conquering the Desktop UI. The real insertion begins after the beginInsertRows() instruction. We start by creating a copy of the album row with unique_ptr newAlbum. This object is then inserted in the database with mDb.albumDao.addAlbum(*newAlbum). Do not forget that the AlbumDao::addAlbum() function also modifies the passed album by setting its mId to the last SQLITE3-inserted ID. Finally, newAlbum is added to mAlbums and its ownership is transferred as well with std::move(). The return gives the index object of this new album, which is simply the row wrapped in a QModelIndex object. Let's continue the editing functions with setData(): bool AlbumModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (!isIndexValid(index) || role != Roles::NameRole) { return false; } Album& album = *mAlbums->at(index.row()); album.setName(value.toString()); mDb.albumDao.updateAlbum(album); emit dataChanged(index, index); return true; }
This function is called when the view wants to update the data. The signature is very similar to data(), with the additional parameter value. The body also follows the same logic. Here, the album row is an Album&, without the const keyword. The only possible value to edit is the name, which is done on the object and then persisted to the database. We have to emit ourselves the dataChanged() signal to notify whoever is interested that a row changed for the given indexes (the start index and end index). This powerful mechanism centralizes all the states of the data, enabling possible views (album list and current album detail for example) to be automatically refreshed.
[ 112 ]
Dividing Your Project and Ruling Your Code
The return of the function simply indicates if the data update was successful. In a production application, you should test the database processing success and return the relevant value. Finally, the last editing function we will cover is removeRows(): bool AlbumModel::removeRows(int row, int count, const QModelIndex& parent) { if (row < 0 || row >= rowCount() || count < 0 || (row + count) > rowCount()) { return false; } beginRemoveRows(parent, row, row + count - 1); int countLeft = count; while (countLeft--) { const Album& album = *mAlbums->at(row + countLeft); mDb.albumDao.removeAlbum(album.id()); } mAlbums->erase(mAlbums->begin() + row, mAlbums->begin() + row + count); endRemoveRows(); return true; }
The function signature should start to look familiar by now. When a view wants to remove rows, it has to provide the starting row, the number of rows to delete, and the parent of the row. After that, just as we did for addAlbum(), we wrap the effective removal with two functions: The beginRemoveRows() function, which expects the parent, the starting index, and the last index The endRemoveRows() function, which simply triggers automatic signals in the model framework The rest of the function is not very hard to follow. We loop on the rows left to delete and, for each one, we delete it from the database and remove it from mAlbums. We simply retrieve the album from our in-memory mAlbums vector and process the real database deletion with mDb.albumDao.removeAlbum(album.id()).
[ 113 ]
Dividing Your Project and Ruling Your Code
The AlbumModel class is now completely covered. You can now create a new C++ class and name it PictureModel. We will not cover the PictureModel class in so much detail. The major parts are the same (you simply swap the data class Album for Picture). There is however one main difference: PictureModel always handles pictures for a given album. This design choice illustrates how two models can be linked with only some simple signals. Here is the updated version of PictureModel.h: #include #include #include #include "gallery-core_global.h" #include "Picture.h" class Album; class DatabaseManager; class AlbumModel; class GALLERYCORESHARED_EXPORT PictureModel : public QAbstractListModel { Q_OBJECT public: enum PictureRole { FilePathRole = Qt::UserRole + 1 }; PictureModel(const AlbumModel& albumModel, QObject* parent = 0); QModelIndex addPicture(const Picture& picture); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role) const override; bool removeRows(int row, int count, const QModelIndex& parent) override; void setAlbumId(int albumId); void clearAlbum(); public slots: void deletePicturesForAlbum(); private: void loadPictures(int albumId);
[ 114 ]
Dividing Your Project and Ruling Your Code bool isIndexValid(const QModelIndex& index) const; private: DatabaseManager& mDb; int mAlbumId; std::unique_ptr mPictures; };
The interesting parts are those concerning the album. As you can see, the constructor expects an AlbumModel. This class also stores the current mAlbumId to be able to request the pictures for a given album only. Let's see what the constructor really does: PictureModel::PictureModel(const AlbumModel& albumModel, QObject* parent) : QAbstractListModel(parent), mDb(DatabaseManager::instance()), mAlbumId(-1), mPictures(new vector()) { connect(&albumModel, &AlbumModel::rowsRemoved, this, &PictureModel::deletePicturesForAlbum); }
As you can see, the albumModel class is used only to connect a signal to our slot deletePicturesForAlbum() which is self-explanatory. This makes sure that the database is always valid: a picture should be deleted if the owning album is deleted. This will be done automatically when AlbumModel emits the rowsRemoved signal. Now, mPictures is not initialized with all the pictures of the database. Because we chose to restrict PictureModel to work on the pictures for a given album, we do not know at the construction of PictureModel which album to choose. The loading can only be done when the album is selected, in setAlbumId(): void PictureModel::setAlbumId(int albumId) { beginResetModel(); mAlbumId = albumId; loadPictures(mAlbumId); endResetModel(); }
[ 115 ]
Dividing Your Project and Ruling Your Code
When the album changes, we completely reload PictureModel. The reloading phase is wrapped with the beginResetModel() and endResetModel() functions. They notify any attached views that their state should be reset as well. Any previous data (for example, QModelIndex) reported from the model becomes invalid. The loadPictures() function is quite straightforward: void PictureModel::loadPictures(int albumId) { if (albumId data(index, PictureModel::Roles::FilePathRole).toString();
[ 131 ]
Conquering the Desktop UI return *mThumbnails[filepath]; }
For any role that is not Qt::DecorationRole, the parent class data() is called. In our case, this triggers the data() function from the original model, PictureModel. After that, when data() must return a thumbnail, the filepath of the picture referenced by the index is retrieved and used to return the QPixmap object of mThumbnails. Luckily for us, QPixmap can be implicitly cast to QVariant, so we do not have anything special to do here. The last function to cover in the ThumbnailProxyModel class is the pictureModel() function: PictureModel* ThumbnailProxyModel::pictureModel() const { return static_cast(sourceModel()); }
Classes that will interact with ThumbnailProxyModel will need to call some functions that are specific to PictureModel to create or delete pictures. This function is a helper to centralize the cast of the sourceModel to PictureModel*. As a side note, we could have tried to generate thumbnails on-the-fly to avoid a possible initial bottleneck during the album loading (and the call to generateThumbnails()). However, data() is a const function, meaning that it cannot modify the ThumbnailProxyModel instance. This rules out any way of generating a thumbnail in the data() function and storing it in mThumbnails. As you can see, QIdentityProxyModel, and more generally QAbstractProxyModel, are valuable tools to add behavior to an existing model without breaking it. In our case, this is enforced by design in so far as the PictureModel class is defined in gallery-core rather than gallery-desktop. Modifying PictureModel implies modifying gallerycore and potentially breaking its behavior for other users of the library. This approach lets us keep things cleanly separated.
Displaying the selected album with AlbumWidget
This widget will display the data of the selected album from AlbumListWidget. Some buttons will allow us to interact with this album. [ 132 ]
Conquering the Desktop UI
Here is the layout of the AlbumWidget.ui file:
The top frame, albumInfoFrame, with a horizontal layout, contains: albumName: This object displays the album's name (Lorem ipsum in the
designer) addPicturesButton: This object allows the user to add pictures selecting files editButton: This object is used to rename the album deleteButton: This object is used to delete the album
The bottom element, thumbnailListView, is a QListView. This list view represents items from PictureModel. By default, QListView is able to display a picture next to text requesting Qt::DisplayRole and Qt::DecorationRole from the model. Take a look at the header AlbumWidget.h file: #include #include namespace Ui { class AlbumWidget; } class class class class
AlbumModel; PictureModel; QItemSelectionModel; ThumbnailProxyModel;
class AlbumWidget : public QWidget { Q_OBJECT
[ 133 ]
Conquering the Desktop UI public: explicit AlbumWidget(QWidget *parent = 0); ~AlbumWidget(); void void void void
setAlbumModel(AlbumModel* albumModel); setAlbumSelectionModel(QItemSelectionModel* albumSelectionModel); setPictureModel(ThumbnailProxyModel* pictureModel); setPictureSelectionModel(QItemSelectionModel* selectionModel);
signals: void pictureActivated(const QModelIndex& index); private slots: void deleteAlbum(); void editAlbum(); void addPictures(); private: void clearUi(); void loadAlbum(const QModelIndex& albumIndex); private: Ui::AlbumWidget* ui; AlbumModel* mAlbumModel; QItemSelectionModel* mAlbumSelectionModel; ThumbnailProxyModel* mPictureModel; QItemSelectionModel* mPictureSelectionModel; };
As this widget needs to deal with Album and Picture data, this class has AlbumModel and ThumbnailProxyModel setters. We also want to know and share the model selection with other widgets and views (that is, AlbumListWidget). That is why we also have Album and Picture model selection setters. The signal pictureActivated() will be triggered when the user double-clicks on a thumbnail. We will see later how MainWindow will connect to this signal to display the picture at full size. The private slots, deleteAlbum(), editAlbum(), and addPictures(), will be called when the user clicks on one of these buttons.
[ 134 ]
Conquering the Desktop UI
Finally, the loadAlbum() function will be called to update the UI for a specific album. The clearUi()function will be useful to clear all information displayed by this widget UI. Take a look at the beginning of the implementation in the AlbumWidget.cpp file: #include "AlbumWidget.h" #include "ui_AlbumWidget.h" #include #include #include "AlbumModel.h" #include "PictureModel.h" AlbumWidget::AlbumWidget(QWidget *parent) : QWidget(parent), ui(new Ui::AlbumWidget), mAlbumModel(nullptr), mAlbumSelectionModel(nullptr), mPictureModel(nullptr), mPictureSelectionModel(nullptr) { ui->setupUi(this); clearUi(); ui->thumbnailListView->setSpacing(5); ui->thumbnailListView->setResizeMode(QListView::Adjust); ui->thumbnailListView->setFlow(QListView::LeftToRight); ui->thumbnailListView->setWrapping(true); connect(ui->thumbnailListView, &QListView::doubleClicked, this, &AlbumWidget::pictureActivated); connect(ui->deleteButton, &QPushButton::clicked, this, &AlbumWidget::deleteAlbum); connect(ui->editButton, &QPushButton::clicked, this, &AlbumWidget::editAlbum); connect(ui->addPicturesButton, &QPushButton::clicked, this, &AlbumWidget::addPictures); } AlbumWidget::~AlbumWidget() { delete ui; }
[ 135 ]
Conquering the Desktop UI
The constructor configures thumbnailListView, our QListView that will display thumbnails of the current selected album. We set here various parameters: setSpacing(): In this parameter, by default items are glued to each other. You can add spacing between them. setResizeMode(): This parameter dynamically lays out items when the view is resized. By default, items keep their original placement even if the view is resized. setFlow(): This parameter specifies the list direction. Here we want to display items from left to right. By default, the direction is TopToBottom. setWrapping(): This parameter allows an item to wrap when there is not enough space to display it in the visible area. By default, wrapping is not allowed and scrollbars will be displayed.
The end of the constructor performs all the signal connections related to the UI. The first one is a good example of signal relaying, explained in Chapter 1, Get Your Qt Feet Wet. We connect the QListView::doubleClicked signal to our class signal, AlbumWidget::pictureActivated. Other connections are common; we want to call a specific slot when the user clicks on a button. As always in the Qt Designer Form Class, the destructor will delete the member variable ui. Let's see the AlbumModel setter implementation: void AlbumWidget::setAlbumModel(AlbumModel* albumModel) { mAlbumModel = albumModel; connect(mAlbumModel, &QAbstractItemModel::dataChanged, [this] (const QModelIndex &topLeft) { if (topLeft == mAlbumSelectionModel->currentIndex()) { loadAlbum(topLeft); } }); } void AlbumWidget::setAlbumSelectionModel(QItemSelectionModel* albumSelectionModel) { mAlbumSelectionModel = albumSelectionModel; connect(mAlbumSelectionModel, &QItemSelectionModel::selectionChanged, [this] (const QItemSelection &selected) { if (selected.isEmpty()) {
[ 136 ]
Conquering the Desktop UI clearUi(); return; } loadAlbum(selected.indexes().first()); }); }
If the selected album's data changed, we need to update the UI with the loadAlbum() function. A test is performed to ensure that the updated album is the currently selected one. Notice that the QAbstractItemModel::dataChanged() function has three parameters but the lambda slot syntax allows us to omit unused parameters. Our AlbumWidget component must update its UI according to the currently selected album. As we share the same selection model, each time the user selects an album from AlbumListWidget, the signal QItemSelectionModel::selectionChanged is triggered. In this case, we update the UI by calling the loadAlbum() function. As we do not support album multi-selection, we can restrict the process to the first selected element. If the selection is empty, we simply clear the UI. It is now the turn of the PictureModel setter implementation: void AlbumWidget::setPictureModel(PictureModel* pictureModel) { mPictureModel = pictureModel; ui->thumbnailListView->setModel(mPictureModel); } void AlbumWidget::setPictureSelectionModel(QItemSelectionModel* selectionModel) { ui->thumbnailListView->setSelectionModel(selectionModel); }
It is very simple here. We set the model and the selection model of thumbnailListView, our QListView that will display the selected album's thumbnails. We also keep the picture model to manipulate the data later on. We can now cover the features one by one. Let's start with album deletion: void AlbumWidget::deleteAlbum() { if (mAlbumSelectionModel->selectedIndexes().isEmpty()) { return; } int row = mAlbumSelectionModel->currentIndex().row(); mAlbumModel->removeRow(row);
[ 137 ]
Conquering the Desktop UI // Try to select the previous album QModelIndex previousModelIndex = mAlbumModel->index(row - 1, 0); if(previousModelIndex.isValid()) { mAlbumSelectionModel->setCurrentIndex(previousModelIndex, QItemSelectionModel::SelectCurrent); return; } // Try to select the next album QModelIndex nextModelIndex = mAlbumModel->index(row, 0); if(nextModelIndex.isValid()) { mAlbumSelectionModel->setCurrentIndex(nextModelIndex, QItemSelectionModel::SelectCurrent); return; } }
The most important task in the deleteAlbum() function is to retrieve the current row index from mAlbumSelectionModel. Then, we can request mAlbumModel to delete this row. The rest of the function will only try to automatically select the previous or the next album. Once again, as we shared the same selection model, AlbumListWidget will automatically update its album selection. The following snippet shows the album rename feature: void AlbumWidget::editAlbum() { if (mAlbumSelectionModel->selectedIndexes().isEmpty()) { return; } QModelIndex currentAlbumIndex = mAlbumSelectionModel->selectedIndexes().first(); QString oldAlbumName = mAlbumModel->data(currentAlbumIndex, AlbumModel::Roles::NameRole).toString(); bool ok; QString newName = QInputDialog::getText(this, "Album's name", "Change Album name", QLineEdit::Normal, oldAlbumName, &ok); if (ok && !newName.isEmpty()) {
[ 138 ]
Conquering the Desktop UI mAlbumModel->setData(currentAlbumIndex, newName, AlbumModel::Roles::NameRole); } }
Here, again the QInputDialog class will help us to implement a feature. You should be confident with its behavior now. This function performs three steps: 1. Retrieve the current name from album model. 2. Generate a great input dialog. 3. Request the album model to update the name As you can see, the generic functions data() and setData() from the models are very powerful when combined with ItemDataRole. As already explained, we do not directly update our UI; this will be automatically performed because setData() emits a signal, dataChanged(), which AlbumWidget handles. The last feature allows us to add some new picture files in the current album: void AlbumWidget::addPictures() { QStringList filenames = QFileDialog::getOpenFileNames(this, "Add pictures", QDir::homePath(), "Picture files (*.jpg *.png)"); if (!filenames.isEmpty()) { QModelIndex lastModelIndex; for (auto filename : filenames) { Picture picture(filename); lastModelIndex = mPictureModel->pictureModel()->addPicture(picture); } ui->thumbnailListView->setCurrentIndex(lastModelIndex); } }
[ 139 ]
Conquering the Desktop UI
The QFileDialog class is used here to help the user select several picture files. For each filename, we create a Picture data holder, like we have already seen in this chapter for album creation. Then we can request mPictureModel to add this picture in the current album. Note that, because mPictureModel is a ThumbnailProxyModel class, we have to retrieve the real PictureModel using the helper function, pictureModel(). As the function addPicture() returns us the corresponding QModelIndex, we finally select the most recently added picture in thumbnailListView. Let's complete AlbumWidget.cpp: void AlbumWidget::clearUi() { ui->albumName->setText(""); ui->deleteButton->setVisible(false); ui->editButton->setVisible(false); ui->addPicturesButton->setVisible(false); } void AlbumWidget::loadAlbum(const QModelIndex& albumIndex) { mPictureModel->pictureModel()->setAlbumId(mAlbumModel->data(albumIndex, AlbumModel::Roles::IdRole).toInt()); ui->albumName->setText(mAlbumModel->data(albumIndex, Qt::DisplayRole).toString()); ui->deleteButton->setVisible(true); ui->editButton->setVisible(true); ui->addPicturesButton->setVisible(true); }
The clearUi()function clears the album's name and hides the buttons, while the loadAlbum() function retrieves the Qt::DisplayRole (the album's name) and displays the buttons.
[ 140 ]
Conquering the Desktop UI
Enhancing thumbnails with PictureDelegate
By default, a QListView class will request Qt::DisplayRole and Qt::DecorationRole to display text and a picture for each item. Thus, we already have a visual result, for free, that looks like this:
However, our Gallery application deserves better thumbnail rendering. Hopefully, we can easily customize it using the view's delegate concept. A QListView class provides a default item rendering. We can do our own item rendering by creating a class that inherits QStyledItemDelegate. The aim is to paint your dream thumbnails with a name banner like the following screenshot:
[ 141 ]
Conquering the Desktop UI
Let's take a look at PictureDelegate.h: #include class PictureDelegate : public QStyledItemDelegate { Q_OBJECT public: PictureDelegate(QObject* parent = 0); void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; };
That is right, we only have to override two functions. The most important function, paint(), will allow us to paint the item like we want. The sizeHint() function will be used to specify the item size. We can now see the painter work in PictureDelegate.cpp: #include "PictureDelegate.h" #include const const const const const
unsigned unsigned unsigned unsigned unsigned
int int int int int
BANNER_HEIGHT = 20; BANNER_COLOR = 0x303030; BANNER_ALPHA = 200; BANNER_TEXT_COLOR = 0xffffff; HIGHLIGHT_ALPHA = 100;
PictureDelegate::PictureDelegate(QObject* parent) : QStyledItemDelegate(parent) { } void PictureDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { painter->save(); QPixmap pixmap = index.model()->data(index, Qt::DecorationRole).value(); painter->drawPixmap(option.rect.x(), option.rect.y(), pixmap); QRect bannerRect = QRect(option.rect.x(), option.rect.y(),
[ 142 ]
Conquering the Desktop UI pixmap.width(), BANNER_HEIGHT); QColor bannerColor = QColor(BANNER_COLOR); bannerColor.setAlpha(BANNER_ALPHA); painter->fillRect(bannerRect, bannerColor); QString filename = index.model()->data(index, Qt::DisplayRole).toString(); painter->setPen(BANNER_TEXT_COLOR); painter->drawText(bannerRect, Qt::AlignCenter, filename); if (option.state.testFlag(QStyle::State_Selected)) { QColor selectedColor = option.palette.highlight().color(); selectedColor.setAlpha(HIGHLIGHT_ALPHA); painter->fillRect(option.rect, selectedColor); } painter->restore(); }
Each time QListView needs to display an item, this delegate's paint() function will be called. The paint system can be seen as layers that you paint one on top of each other. The QPainter class allows us to paint anything we want: circles, pies, rectangles, text, and so on. The item area can be retrieved with option.rect(). Here are the steps: 1. It is easy to break the painter state passed in the parameter list, thus we must save the painter state with painter->save() before doing anything, to be able to restore it when we have finished our drawing. 2. Retrieve the item thumbnail and draw it with the QPainter::drawPixmap() function. 3. Paint a translucent gray banner on top of the thumbnail with the QPainter::fillRect() function. 4. Retrieve the item display name and draw it on the banner using the QPainter::drawText() function. 5. If the item is selected, we paint a translucent rectangle on the top using the highlight color from the item. 6. We restore the painter state to its original state. If you want to draw a more complex item, check the QPainter official documentation at doc.qt.io/qt-5/qpainter.html.
[ 143 ]
Conquering the Desktop UI
This is the sizeHint() function's implementation: QSize PictureDelegate::sizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const { const QPixmap& pixmap = index.model()->data(index, Qt::DecorationRole).value(); return pixmap.size(); }
This one is easier. We want the item's size to be equal to the thumbnail size. As we kept the aspect ratio of the thumbnail during its creation in Picture::setFilePath(), thumbnails can have a different width and height. Hence, we basically retrieve the thumbnail and return its size. When you create an item delegate, avoid directly inheriting the QItemDelegate class and instead inherit QStyledItemDelegate. This last one supports Qt style sheets, allowing you to easily customize the rendering. Now that PictureDelegate is ready, we can configure our thumbnailListView to use it, updating the AlbumWidget.cpp file like this: AlbumWidget::AlbumWidget(QWidget *parent) : QWidget(parent), ui(new Ui::AlbumWidget), mAlbumModel(nullptr), mAlbumSelectionModel(nullptr), mPictureModel(nullptr), mPictureSelectionModel(nullptr) { ui->setupUi(this); clearUi(); ui->thumbnailListView->setSpacing(5); ui->thumbnailListView->setResizeMode(QListView::Adjust); ui->thumbnailListView->setFlow(QListView::LeftToRight); ui->thumbnailListView->setWrapping(true); ui->thumbnailListView->setItemDelegate( new PictureDelegate(this)); ... }
[ 144 ]
Conquering the Desktop UI
Qt tip An item delegate can also manage the editing process with the QStyledItemDelegate::createEditor() function.
Displaying a picture with PictureWidget
This widget will be called to display a picture at its full size. We also add some buttons to go to the previous/next picture or delete the current one. Let's start to analyze the PictureWidget.ui form, here is the design view:
Here are the details: backButton: This object requests to display the gallery deleteButton: This object removes the picture from the album nameLabel: This object displays the picture name nextButton: This object selects the next picture in the album previousButton: This object selects the previous picture in the album pictureLabel: This object displays the picture
We can now take a look at the header PictureWidget.h: #include #include namespace Ui { class PictureWidget; } class PictureModel; class QItemSelectionModel; class ThumbnailProxyModel;
[ 145 ]
Conquering the Desktop UI class PictureWidget : public QWidget { Q_OBJECT public: explicit PictureWidget(QWidget *parent = 0); ~PictureWidget(); void setModel(ThumbnailProxyModel* model); void setSelectionModel(QItemSelectionModel* selectionModel); signals: void backToGallery(); protected: void resizeEvent(QResizeEvent* event) override; private slots: void deletePicture(); void loadPicture(const QItemSelection& selected); private: void updatePicturePixmap(); private: Ui::PictureWidget* ui; ThumbnailProxyModel* mModel; QItemSelectionModel* mSelectionModel; QPixmap mPixmap; };
No surprises here, we have the ThumbnailProxyModel* and QItemSelectionModel* setters in the PictureWidget class. The signal backToGallery() is triggered when the user clicks on the backButton object. It will be handled by MainWindow to display again the gallery. We override resizeEvent() to ensure that we always use all the visible area to display the picture. The deletePicture()slot will process the deletion when the user clicks on the corresponding button. The loadPicture()function will be called to update the UI with the specified picture. Finally, updatePicturePixmap() is a helper function to display the picture according to the current widget size. This widget is really similar to the others. As a result, we will not put the full implementation code of PictureWidget.cpp here. You can check the full source code example if needed.
[ 146 ]
Conquering the Desktop UI
Let's see how this widget is able to always display the picture at its full size in PictureWidget.cpp: void PictureWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); updatePicturePixmap(); } void PictureWidget::updatePicturePixmap() { if (mPixmap.isNull()) { return; } ui->pictureLabel->setPixmap(mPixmap.scaled(ui->pictureLabel->size(), Qt::KeepAspectRatio)); }
So, every time the widget is resized, we call updatePicturePixmap(). The mPixmap variable is the full-size picture from PictureModel. This function will scale the picture to the pictureLabel size, keeping the aspect ratio. You can freely resize the window and enjoy your picture with the biggest possible size.
Composing your Gallery app
Alright, we completed AlbumListWidget, AlbumWidget, and PictureWidget. If you remember correctly, AlbumListWidget and AlbumWidget are contained in a widget called GalleryWidget. Let's take a look at the GalleryWidget.ui file:
[ 147 ]
Conquering the Desktop UI
This widget does not contain any standard Qt widgets but only our created widgets. Qt provides two ways to use your own widgets in the Qt designer: Promoting widgets: This is the fastest and easiest way Creating widget plugin for Qt designer: This is more powerful but more complex In this chapter, we will use the first way, which consists of placing a generic QWidget as a placeholder and then promoting it to our custom widget class. You can follow these steps to add the albumListWidget and the albumWidget objects to the GalleryWidget.ui file from the Qt designer: 1. 2. 3. 4. 5. 6.
Drag and drop a Widget from Containers to your form. Set the objectName (for example, albumListWidget) from the Property Editor. Select Promote to… from the widget contextual menu. Set the promoted class name (for example, AlbumWidget). Check that header file is correct (for example, AlbumWidget.h). Click on the Add button and then click on Promote.
If you fail your widget promotion, you can always reverse it with Demote to QWidget from the contextual menu. There is nothing really exciting in the header and implementation of GalleryWidget. We only provide setters for the model and model selection of Album and Picture to forward them to albumListWidget and albumWidget. This class also relays the signal pictureActivated from albumWidget. Please check the full source code if needed. This is the final part of this chapter. We will now analyze MainWindow. Nothing is done in MainWindow.ui because everything is handled in the code. This is MainWindow.h: #include #include namespace Ui { class MainWindow; } class GalleryWidget; class PictureWidget; class MainWindow : public QMainWindow { Q_OBJECT
[ 148 ]
Conquering the Desktop UI public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); public slots: void displayGallery(); void displayPicture(const QModelIndex& index); private: Ui::MainWindow *ui; GalleryWidget* mGalleryWidget; PictureWidget* mPictureWidget; QStackedWidget* mStackedWidget; };
The two slots, displayGallery() and displayPicture(), will be used to switch the display between the gallery (album list with the album and thumbnail) and the picture ( full-size). The QStackedWidget class can contain various widgets but display only one at a time. Let's take a look to the beginning of the constructor in the MainWindow.cpp file: ui->setupUi(this); AlbumModel* albumModel = new AlbumModel(this); QItemSelectionModel* albumSelectionModel = new QItemSelectionModel(albumModel, this); mGalleryWidget->setAlbumModel(albumModel); mGalleryWidget->setAlbumSelectionModel(albumSelectionModel);
First, we initialize the UI by calling ui->setupUi(). Then we create AlbumModel and its QItemSelectionModel. Finally, we call the setters of GalleryWidget that will dispatch them to the AlbumListWidget and AlbumWidget objects. Continuing our analysis of this constructor: PictureModel* pictureModel = new PictureModel(*albumModel, this); ThumbnailProxyModel* thumbnailModel = new ThumbnailProxyModel(this); thumbnailModel->setSourceModel(pictureModel); QItemSelectionModel* pictureSelectionModel = new QItemSelectionModel(pictureModel, this); mGalleryWidget->setPictureModel(thumbnailModel); mGalleryWidget->setPictureSelectionModel(pictureSelectionModel); mPictureWidget->setModel(thumbnailModel); mPictureWidget->setSelectionModel(pictureSelectionModel);
[ 149 ]
Conquering the Desktop UI
The behavior with Picture is close to the previous one with Album. But we also share ThumbnailProxyModel, which is initialized from PictureModel, and its QItemSelectionModel with PictureWidget. The constructor now performs the signal/slot connections: connect(mGalleryWidget, &GalleryWidget::pictureActivated, this, &MainWindow::displayPicture); connect(mPictureWidget, &PictureWidget::backToGallery, this, &MainWindow::displayGallery);
Do you remember the pictureActivated() function? This signal is emitted when you double-click on a thumbnail in albumWidget. We can now connect it to our displayPicture slot, which will switch the display with the picture at its full size. Do not forget to also connect the backToGallery signal emitted when the user clicks on the backButton from PictureWidget. It will switch again to display the gallery. The last part of the constructor is easy: mStackedWidget->addWidget(mGalleryWidget); mStackedWidget->addWidget(mPictureWidget); displayGallery(); setCentralWidget(mStackedWidget);
We add our two widgets, mGalleryWidget and mPictureWidget, to the mStackedWidget class. When the application starts, we want to display the gallery, so we call our own slot displayGallery(). Finally, we define mStackedWidget as the main window's central widget. To finish this chapter, let's see what happens in these two magic slots that allows to switch the display when the user requests it: void MainWindow::displayGallery() { mStackedWidget->setCurrentWidget(mGalleryWidget); } void MainWindow::displayPicture(const QModelIndex& /*index*/) { mStackedWidget->setCurrentWidget(mPictureWidget); }
[ 150 ]
Conquering the Desktop UI
That seems ridiculously easy. We just request mStackedWidget to select the corresponding widget. As PictureWidget shares the same selection model with other views, we can even ignore the index variable.
Summary
The real separation between data and representation is not always an easy task. Dividing the core and the GUI in two different projects is a good practice. It will force you to design separated layers in your application. At first sight, the Qt model/view system can appear complex. But this chapter taught you how powerful it can be and how easy it is to use. Thanks to the Qt framework, the persistence of data in a database can be done without headaches. This chapter built on top of the foundations laid with the gallery-core library. In the next chapter, we will reuse the same core library and create a mobile UI with Qt Quick in QML.
[ 151 ]
5
Dominating the Mobile UI In Chapter 3, Dividing Your Project and Ruling Your Code, we created a strong core library to handle a picture gallery. We will now use this gallery-core library to create a mobile application. We will teach you how to create a Qt Quick project from scratch. You will create custom Qt Quick views with QML. This chapter will also cover how your QML views can communicate with the C++ library. At the end of this chapter, your gallery application will run on your mobile (Android or iOS) with a dedicated GUI compliant with touch devices. This application will offer the same features as the desktop application. This chapter covers the following topics: Creating a Qt Quick project QML Qt Quick controls Qt for mobile (Android and iOS) Calling C++ functions from QML
Dominating the Mobile UI
Starting with Qt Quick and QML
Qt Quick is another way of creating applications with Qt. You can use it to create a complete application in place of Qt Widgets. The Qt Quick module provides transitions, animations, and visual effects. You can also customize graphical effects with shaders. This module is especially efficient at making software for devices using touchscreens. Qt Quick uses a dedicated language: Qt Modeling Language (QML). It is a declarative language; the syntax is close to the JSON (JavaScript Object Notation) syntax. Furthermore, QML also supports JavaScript expressions inline or in a separate file. Let's begin with a simple example of a Qt Quick application using QML. Create a new file called main.qml with this code snippet: import QtQuick 2.5 import QtQuick.Window 2.2 Window { visible: true width: 640; height: 480 // A nice red rectangle Rectangle { width: 200; height: 200 color: "red" } }
Qt 5 provides a nice tool called qmlscene to prototype a QML user interface. You can find the binary file in your Qt installation folder, for example: Qt/5.7/gcc_64/bin/qmlscene. To load your main.qml file, you can run the tool and select the file, or use the CLI with the .qml file in an argument: qmlscene main.qml. You should see something like this:
[ 153 ]
Dominating the Mobile UI
To use a Qt Quick module, you need to import it. The syntax is easy: import
In this example we import QtQuick, which is the common module that will provide basic components (Rectangle, Image, Text) and we also import the QtQuick.Window module that will provide the main window application (Window).
[ 154 ]
Dominating the Mobile UI
A QML component can have properties. For example, we set the width property of the Window class to the value 640. Here is the generic syntax: { : }
We can now update main.qml file with some new rectangles: import QtQuick 2.5 import QtQuick.Window 2.2 Window { visible: true width: 640; height: 480 Rectangle { width: 200; height: 200 color: "red" } Rectangle { width: 200; height: 200 color: "green" x: 100; y: 100 Rectangle { width: 50; height: 50 color: "blue" x: 100; y: 100 } } }
[ 155 ]
Dominating the Mobile UI
Here is the visual result:
Your QML file describes the UI as a hierarchy of components. The hierarchy below the Window element is the following: Red Rectangle Green Rectangle Blue Rectangle Each nested item will always have its x, y coordinates relative to its parent. To structure your application, you can build reusable QML components. You can easily create a new component. All QML components must have a single root item. Let's build a new MyToolbar component by creating a new file called MyToolbar.qml: import QtQuick 2.5 import QtQuick 2.5 Rectangle { color: "gray" height: 50
[ 156 ]
Dominating the Mobile UI Rectangle { id: purpleRectangle width: 50; height: parent.height color: "purple" radius: 10 } Text { anchors.left: purpleRectangle.right anchors.right: parent.right text: "Dominate the Mobile UI" font.pointSize: 30 } }
The gray Rectangle element will be our root item used as background. We also created two items: A purple Rectangle element that can be identified with the ID purpleRectangle. The height of this item will be the height of its parent, that is, the gray Rectangle element. A Text item. In this case, we use anchors. It will help us to layout items without using hardcoded coordinates. The left of the Text item will be aligned with the right of purpleRectangle, and the right of the Text item will be aligned with the right of the parent (the gray Rectangle element). Qt Quick provides a lot of anchors: left, horizontalCenter, right, top, verticalCenter, and bottom. You can also use convenience anchors such as fill or centerIn. For more information on anchors, take a look at http://doc.qt.io/qt-5/qtq uick-positioning-anchors.html. You can use MyToolbar in your window by updating your main.qml: Window { ... MyToolbar { width: parent.width } }
[ 157 ]
Dominating the Mobile UI
We set the width to the parent width. Like this, the toolbar fills the window's width. Here is the result:
Anchors are great to align specific items, but if you want to layout several items in grid, row, or column fashion, you can use the QtQuick.layouts module. Here is an example of the updated main.qml: import QtQuick 2.5 import QtQuick.Window 2.2 import QtQuick.Layouts 1.3 Window { visible: true width: 640; height: 480 MyToolbar { id: myToolbar width: parent.width } RowLayout { anchors.top: myToolbar.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom
[ 158 ]
Dominating the Mobile UI Rectangle { width: 200; height: 200; color: "red" } Rectangle { width: 200; height: 200 color: "green" } Rectangle { width: 50; height: 50; color: "blue" } } }
You should get something like this:
As you can see, we use a RowLayout element that fits under the myToolbar and to its parent, a Window element. This item provides a way to dynamically layout items in a row. Qt Quick also provides other layout items: GridLayout and ColumnLayout.
[ 159 ]
Dominating the Mobile UI
Your custom component can also expose custom properties that can be modified outside of the component itself. You can do it by adding the property attribute. Please update MyToolbar.qml: import QtQuick 2.5 Rectangle { property color iconColor: "purple" property alias title: label.text color: "gray" height: 50 Rectangle { id: purpleRectangle width: 50; height: parent.height color: iconColor radius: 10 } Text { id: label anchors.left: purpleRectangle.right anchors.right: parent.right text: "Dominate the Mobile UI" font.pointSize: 30 } }
The iconColor is a really new property that is a fully-fledged variable. We also update the Rectangle attribute to use this property as color. The title property is only an alias, you can see it as a pointer to update the label.text property. From outside you can use these attributes with the same syntax; please update the main.qml file with the following snippet: import QtQuick 2.5 import QtQuick.Window 2.2 import QtQuick.Layouts 1.3 Window { visible: true width: 640; height: 480 MyToolbar { id: myToolbar
[ 160 ]
Dominating the Mobile UI width: parent.width title: "Dominate Qt Quick" iconColor: "yellow" } ... }
You should get a nice updated toolbar like this one:
We have covered the basics of QML, now we are going to proceed to mobile application development using QML.
[ 161 ]
Dominating the Mobile UI
Checking your development environment To be able to create a Qt application for Android, you must have:
A device with Android v2.3.3 (API 10) or later Android SDK Android NDK JDK Qt Prebuilt Components for Android x86 (from the Qt Maintenance Tool) Qt Prebuilt Components for Android ARMv7 (from the Qt Maintenance Tool) To be able to create a Qt application for iOS, you must have: A device with iOS 5.0 or later A Mac desktop computer Xcode Qt for iOS (from Qt Maintenance Tool) When starting, Qt Creator will detect and create Android and iOS Qt kits. You can check your existing kits from Tools | Options | Build & Run | Kits, as shown in the following screenshot:
[ 162 ]
Dominating the Mobile UI
Creating a Qt Quick project
This chapter will follow the same project structure we covered in Chapter 4, Conquering the Desktop UI: a parent project ch05-gallery-mobile.pro will host our two subprojects, gallery-core and the new gallery-mobile. In Qt creator, you can create a Qt Quick subproject from File | New File or Project | Application | Qt Quick Controls Application | Choose. The wizard will allow you to customize your project creation: Location Details
Kits
Summary
Choose a project name (gallery-mobile) and a location Deselect With ui.qml file Deselect Enable native styling Select your desktop kit Select at least one mobile kit Be sure to add gallery-mobile as a subproject of ch05-gallerymobile.pro
Let's take some time to explain why we created our project with these options. The first thing to analyze is the application template. By default, Qt Quick only provides basic QML components (Rectangle, Image, Text, and so on). Advanced components will be handled by Qt Quick modules. For this project we will use Qt Quick Controls (ApplicationWindow, Button, TextField, and so on). That is why we chose to begin with a Qt Quick Controls application. Keep in mind that you can always import and use Qt Quick modules later. In this chapter, we will not use the Qt Quick Designer. As a consequence, .ui.qml files are not required. Even if the designer can help a lot, it is good to understand and write QML files yourself. The desktop “native styling” is disabled because this project mainly targets mobile platforms. Moreover, disabling “native styling” avoids heavy dependency on the Qt widgets module.
[ 163 ]
Dominating the Mobile UI
Finally, we select at least two kits. The first one is our desktop kit. The other kits are the mobile platforms you target. We usually use the following development workflow: Fast iterations on desktop Check and fix behavior on mobile emulator/simulator Real test on the mobile device Deployment on a real device is generally longer so you can do most development with the desktop kit. The mobile kits will allow you to check your application behavior on a real mobile device or on an emulator (for example with a Qt Android x86 kit). Let's talk about the files automatically generated by the wizard. Here is the main.cpp file: #include #include int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
We use here QGuiApplication and not QApplication because we do not use Qt widgets in this project. Then, we create the QML engine and load qrc:/mail.qml. As you may have guessed (with the qrc:/ prefix), this QML file is in a Qt resource file. You can open the qml.qrc file to find the main.qml: import QtQuick 2.5 import QtQuick.Controls 1.4 ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Hello World") menuBar: MenuBar { Menu { title: qsTr("File") MenuItem { text: qsTr("&Open")
[ 164 ]
Dominating the Mobile UI onTriggered: console.log("Open action triggered"); } MenuItem { text: qsTr("Exit") onTriggered: Qt.quit(); } } } Label { text: qsTr("Hello World") anchors.centerIn: parent } }
The first thing to do is to import types used in the file. Notice the module version at the end of each import. The QtQuick module will import basic QML elements (Rectangle, Image, and so on) while the QtQuick.Controls module will import advanced QML elements from the QtQuick Controls submodule (ApplicationWindow, MenuBar, MenuItem, Label, and so on). Then, we define the root element of type ApplicationWindow. It provides a top-level application window with the following items: MenuBar, ToolBar and StatusBar. The properties visible, width, height, and title of ApplicationWindow are primitive types. The syntax is simple and intelligible. The menuBar property is more complex. This MenuBar property is composed of a Menu file, itself composed of two MenuItems: Open and Exit. A MenuItem emits a triggered()signal each time it is activated. In this case, the MenuItem file will log a message on the console. The exit MenuItem terminates the application. Finally, a Label displaying “Hello World” is added in the content area of our ApplicationWindow type. Positioning items with anchors is useful. In our case the label is centered vertically and horizontally in its parent, ApplicationWindow. Before going ahead, check that this sample runs correctly on your desktop and on your mobile.
[ 165 ]
Dominating the Mobile UI
Preparing your Qt Quick gallery entry point
First of all, you need to link this project to our gallery-core library. We already covered how to link an internal library in Chapter 4, Conquering the Desktop UI. For more details, refer to it. This is the updated gallery-mobile.pro file: TEMPLATE = app QT += qml quick sql svg CONFIG += c++11 SOURCES += main.cpp RESOURCES += gallery.qrc LIBS += -L$$OUT_PWD/../gallery-core/ -lgallery-core INCLUDEPATH += $$PWD/../gallery-core DEPENDPATH += $$PWD/../gallery-core contains(ANDROID_TARGET_ARCH,x86) { ANDROID_EXTRA_LIBS = \ $$[QT_INSTALL_LIBS]/libQt5Sql.so }
Please notice that we made several changes here: We added the sql module to deploy the dependency on your mobile device We added the svg module for the button icons The qml.qrc file has been renamed in gallery.qrc We linked the gallery-core library By default, the sql shared object (libQt5Sql.so) will not be deployed on your Android x86 device. You have to explicitly include it in your .pro file. You can now use classes from the gallery-core library in our gallery-mobile application. Let's see how to bind C++ models with QML. This is the updated main.cpp: #include #include #include #include
#include "AlbumModel.h" #include "PictureModel.h"
[ 166 ]
Dominating the Mobile UI int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); AlbumModel albumModel; PictureModel pictureModel(albumModel); QQmlApplicationEngine engine; QQmlContext* context = engine.rootContext(); context->setContextProperty("albumModel", &albumModel); context->setContextProperty("pictureModel", &pictureModel); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); return app.exec(); }
Our models will be instantiated in C++ and exposed to QML using the root QQmlContext object. The setContextProperty() function allows us to bind a C++ QObject to a QML property. The first argument will be the QML property name. We are only binding a C++ object to a QML property; the context object does not take ownership of this object. Let's now talk about the mobile application itself. We will define three pages with specific roles: AlbumListPage
Displays existing albums Album creation Album selection AlbumPage
Displays existing pictures as thumbnails Adds pictures in album Album rename Album deletion Picture selection PicturePage
Displays selected picture Picture selection Picture deletion
[ 167 ]
Dominating the Mobile UI
To handle the navigation, we will use a StackView component from Qt Quick Controls. This QML component implements a stack-based navigation. You can push a page when you want to display it. When the user requests to go back, you can pop it. Here is the workflow using a StackView component for our gallery mobile application. The page with the solid border is the page currently displayed on screen:
This is the implementation of main.qml: import QtQuick 2.6 import QtQuick.Controls 2.0 ApplicationWindow { readonly property alias pageStack: stackView id: app visible: true width: 768 height: 1280 StackView { id: stackView anchors.fill: parent initialItem: AlbumListPage {} } onClosing: { if (Qt.platform.os == "android") { if (stackView.depth > 1) { close.accepted = false stackView.pop() } }
[ 168 ]
Dominating the Mobile UI } }
This main file is really simple. The application is constructed around the StackView component. We set the id property to allow our StackView to be identified and referred to by other QML objects. The anchors property will set stackView to fill its parent, the ApplicationWindow type. Finally, we set the initialItem property to a page, AlbumListPage that will be implemented soon. On Android, onClosing will be executed each time the user presses the back button. To mimic a native Android application, we will first pop the last stacked page before really closing the application. At the top of the file, we define a property alias for the stackView. A property alias is a simple reference to another existing property. This alias will be useful to access stackView from other QML components. To prevent a QML component to crush the stackView we are using the readonly keyword. After initialization, the components can
access the property but not change its value.
Displaying albums with ListView
Let's make our first page for this mobile application! Create a file in gallery.qrc called AlbumListPage.qml. Here is the page header implementation: import QtQuick 2.0 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 Page { header: ToolBar { Label { Layout.fillWidth: true text: "Albums" font.pointSize: 30 } } ... }
[ 169 ]
Dominating the Mobile UI
A Page is a container control with a header and footer. In this application, we will only use the header item. We assign a ToolBar to the header property. The height of this toolbar will be handled by Qt and will be adjusted depending on the target platform. In this first simple implementation, we only put a Label displaying the text “Albums”. Add a ListView element to this page after the header initialization: ListView { id: albumList model: albumModel spacing: 5 anchors.fill: parent delegate: Rectangle { width: parent.width height: 120 color: "#d0d1d2" Text { text: name font.pointSize: 16 color: "#000000" anchors.verticalCenter: parent.verticalCenter } } }
The Qt Quick ListView is the Qt Widget QListView equivalent. It displays a list of items from a provided model. We set the model property to value albumModel. This refers to the C++ model from main.cpp file accessible from QML because we used the setContextProperty() function. In Qt Quick, you must provide a delegate to describe how a row will be displayed. In this case, a row will only display the album's name with a Text item. Accessing the album's name in QML is easy because our AlbumModel model exposes its role list to QML. Let's refresh your memory concerning the overridden roleNames() function of AlbumModel: QHash AlbumModel::roleNames() const { QHash roles; roles[Roles::IdRole] = "id"; roles[Roles::NameRole] = "name"; return roles; }
[ 170 ]
Dominating the Mobile UI
So each time your delegate from Qt Quick uses the name role, it will call the AlbumModel function data() with the correct role integer and return the correct album name string. To handle the mouse, click on a row and add a MouseArea element on the delegate: ListView { ... delegate: Rectangle { ... MouseArea { anchors.fill: parent onClicked: { albumList.currentIndex = index pictureModel.setAlbumId(id) pageStack.push("qrc:/qml/AlbumPage.qml", { albumName: name, albumRowIndex: index }) } } } }
The MouseArea is an invisible item that can be used with any visible item to handle mouse events. This also applies to a simple touch on a phone touch screen. Here we tell the MouseArea element to take the full area of the parent Rectangle. In our case, we only perform tasks on the clicked signal. We update the currentIndex of the ListView with index. This index is a special role containing the index of the item in the model. When the user clicks, we will tell pictureModel to load the selected album with the pictureModel.setAlbumId(id) call. We will see soon how QML can call C++ methods. Finally, we push AlbumPage on pageStack property. The push() function allows us to set a list of QML properties using a {key: value, … } syntax. Each property will be copied into the pushed item. Here the name and the index will be copied in the albumName and albumRowIndex properties of AlbumPage. It is a simple yet powerful way to instantiate a QML page with properties arguments. From your QML code, you can only call some specific C++ methods: Properties (using Q_PROPERTY) Public slot Function decorated as invokable (using Q_INVOKABLE) [ 171 ]
Dominating the Mobile UI
In this case we will decorate PictureModel::setAlbumId() as Q_INVOKABLE, please update the PictureModel.h file: class GALLERYCORESHARED_EXPORT PictureModel : public QAbstractListModel { Q_OBJECT public: ... Q_INVOKABLE void setAlbumId(int albumId); ... };
Theming the application with a QML singleton
Styling and theming a QML application can be done in various ways. In this chapter, we will declare a QML singleton with the theme data used by custom components. Moreover, we will also create a custom Page component to handle the toolbar and its default item (back button and page's title). Please create a new Style.qml file: pragma Singleton import QtQuick 2.0 QtObject { property color text: "#000000" property property property property
color color color color
windowBackground: "#eff0f1" toolbarBackground: "#eff0f1" pageBackground: "#fcfcfc" buttonBackground: "#d0d1d2"
property color itemHighlight: "#3daee9" }
We declare a QtObject component that will only contain our theme properties. A QtObject is a non-visual QML component.
[ 172 ]
Dominating the Mobile UI
Declaring a singleton type in QML requires two steps. First you need to use the pragma singleton, it will indicate the use of a single instance of the component. The second step is to register it. This can be done in C++ or by creating a qmldir file. Let's see the second step. Create a new plain-text file called qmldir: singleton Style 1.0 Style.qml
This simple line will declare a QML singleton type named Style with the version 1.0 from the file named Style.qml. It is now time to use these theme properties in custom components. Let's see a simple example. Create a new QML file called ToolBarTheme.qml: import QtQuick 2.0 import QtQuick.Controls 2.0 import "." ToolBar { background: Rectangle { color: Style.toolbarBackground } }
This QML object describes a customized ToolBar. Here, the background element is a simple Rectangle with our color. We can easily access our singleton Style and its theme property using Style.toolbarBackground. QML Singletons require an explicit import to load the qmldir file. The import "." is a workaround for this Qt bug. For more information, please check https://bugreports.qt.io/browse/QTBUG-34418. We will now create a QML file PageTheme.qml, with the aim of containing all the code related to the page's toolbar and theme: import QtQuick 2.0 import import import import
QtQuick.Layouts 1.3 Qt.labs.controls 1.0 QtQuick.Controls 2.0 "."
Page {
[ 173 ]
Dominating the Mobile UI property alias toolbarButtons: buttonsLoader.sourceComponent property alias toolbarTitle: titleLabel.text header: ToolBarTheme { RowLayout { anchors.fill: parent ToolButton { background: Image { source: "qrc:/res/icons/back.svg" } onClicked: { if (stackView.depth > 1) { stackView.pop() } } } Label { id: titleLabel Layout.fillWidth: true color: Style.text elide: Text.ElideRight font.pointSize: 30 } Loader { Layout.alignment: Qt.AlignRight id: buttonsLoader } } } Rectangle { color: Style.pageBackground anchors.fill: parent } }
This PageTheme element will customize the page's header. We use our previously created ToolBarTheme. This toolbar only contains a RowLayout element to display items horizontally in one row. This layout contains three elements: ToolButton: This is the “back” that displays an image from gallery.qrc and pops the current page if required
[ 174 ]
Dominating the Mobile UI
Label: This is the element that displays the page title Loader: This is the element that allows a page to dynamically add specific items
in this generic toolbar
The Loader element owns a sourceComponent property. In this application, this property can be assigned by PageTheme pages to add specific buttons. These buttons will be instantiated at runtime. The PageTheme pages also contain a Rectangle element that fits the parent and configures the page background color using the Style.pageBackground. Now that our Style.qml and PageTheme.qml files are ready, we can update the AlbumListPage.qml file to use it: import QtQuick 2.6 import QtQuick.Controls 2.0 import "." PageTheme { toolbarTitle: "Albums" ListView { id: albumList model: albumModel spacing: 5 anchors.fill: parent delegate: Rectangle { width: parent.width height: 120 color: Style.buttonBackground Text { text: name font.pointSize: 16 color: Style.text anchors.verticalCenter: parent.verticalCenter } ... } } }
[ 175 ]
Dominating the Mobile UI
Now that AlbumListPage is a PageTheme element, we do not manipulate header directly. We only need to set the property toolbarTitle to display a nice “Albums” text in the toolbar. We can also enjoy nice colors using properties from the Style singleton. By centralizing the theme properties in a single file, you can easily change the look and feel of your application. The source code of the project also contains a dark theme.
Loading a database on mobile
Before continuing the UI implementation, we have to take care of the database deployment on mobile. Spoiler: this will not be fun. We have to jump back to DatabaseManager.cpp in the gallery-core project: DatabaseManager& DatabaseManager::instance() { return singleton; } DatabaseManager::DatabaseManager(const QString& path) : mDatabase(new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE"))), albumDao(*mDatabase), pictureDao(*mDatabase) { mDatabase->setDatabaseName(path); ... }
Whereas on Desktop, the SQLite3 database is created at the instruction mDatabase->setDatabaseName(), on mobile it does not work at all. This is due to the fact that the filesystem is very specific on each mobile platform (Android and iOS). An application has only access to a narrow sandbox where it cannot mess with the rest of the filesystem. All the files inside the application directory must have specific file permissions. If we let SQLite3 create the database file, it will not have the right permission and the OS will block the database from opening. As a consequence, the database will not be properly created and your data cannot be persisted. When using the native API, this is not a problem since the OS takes care of the proper configuration of the database. Because we are developing with Qt, we do not have easy access to this API (except by using JNI or other black magic). A workaround is to embed a “ready-to-use” database in the application's package and copy it at the right filesystem path with the correct rights. [ 176 ]
Dominating the Mobile UI
This database should contain an empty created database without any content. The database is available in the source code of the chapter (you can also generate it from the source code of Chapter 4, Conquering the Desktop UI). You can add it to the gallery.qrc file. Because our layers are clearly defined, we just have to modify the DatabaseManager::instance() implementation to handle this case: DatabaseManager& DatabaseManager::instance() { #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) QFile assetDbFile(":/database/" + DATABASE_FILENAME); QString destinationDbFile = QStandardPaths::writableLocation( QStandardPaths::AppLocalDataLocation) .append("/" + DATABASE_FILENAME); if (!QFile::exists(destinationDbFile)) { assetDbFile.copy(destinationDbFile); Qfile::setPermissions(destinationDbFile, QFile::WriteOwner | QFile::ReadOwner); } } static DatabaseManager singleton(destinationDbFile); #else static DatabaseManager singleton; #endif return singleton; }
We first retrieve the platform-specific path of the application with a nifty Qt class: QStandardPaths. This class return paths for multiple types (AppLocalDataLocation, DocumentsLocation, PicturesLocation, and so on). The database should be stored in the application data directory. If the file does not exist, we copy it from our assets. Finally, the permissions of the file are modified to ensure that the OS does not block the opening of the database (due to permissions not being restrictive enough). When everything is done, the DatabaseManager singleton is instantiated with the correct database file path and the constructor can open this database transparently. In the iOS Simulator, the QStandardPaths::writableLocation() function will not return the proper path. Since iOS 8, the simulator's storage path on the host has changed and Qt does not reflect this. For more information, please check out https://bugreports.qt.io/browse /QTCREATORBUG-13655. [ 177 ]
Dominating the Mobile UI
These workarounds were not trivial. This shows the limitations of a cross-platform application on mobile. Each platform has its own very specific way of handling the filesystem and deploying its content. Even if we manage to write platform agnostic code in QML, we still have to deal with differences between the OSes.
Creating a new album from a custom InputDialog
The AlbumListPage needs some data to display. The next step is to be able to add a new album. To do this, at some point we will have to call an AlbumModel function from QML to add this new album. Before building the UI, we have to make a small modification in gallery-core. The AlbumModel function is already available in QML. However, we cannot directly call AlbumModel::addAlbum(const Album& album) from the QML code; the QML engine will not recognize the function and will throw an error TypeError: Property 'addAlbum' of object AlbumModel(…) is not a function. This can be fixed by simply decorating the desired function with the Q_INVOKABLE macro (as we did for PictureModel::setAlbumId()). Nonetheless, there is another issue here: Album is a C++ class which is not recognized in QML. If we wanted to have full access to Album in QML, it would involve important modifications to the class: Force Album class to inherit from the QObject class. Add a Q_PROPERTY macro to specify which property of the class should be accessible from QML. Add multiple constructors (copy constructor, QObject* parent, and so on). Force AlbumModel::addAlbum() function to take an Album* rather than an Album&. For complex objects (that is, not primitive types), QML can only handle pointers. This is not a big problem, but using references instead of pointers tends to make the code safer. These modifications are perfectly reasonable if the class is heavily manipulated in QML. Our use case is very limited: we only want to create a new album. Throughout the application, we will rely on the native Model/View API to display the album data and nothing specific to Album will be used.
[ 178 ]
Dominating the Mobile UI
For all these reasons, we will simply add a wrapper function in AlbumModel: // In AlbumModel.h ... QModelIndex addAlbum(const Album& album); Q_INVOKABLE void addAlbumFromName(const QString& name); ... // In AlbumModel.cpp void AlbumModel::addAlbumFromName(const QString& name) { addAlbum(Album(name)); }
The new function addAlbumFromName() just wraps the call to addAlbum() with the desired album name parameter. It can be called from the QML with the Q_INVOKABLE macro. We can now switch back to the UI in the gallery-mobile project. We will add this album using a QML Dialog. QtQuick provides various default implementations of dialogs: ColorDialog: This dialog is used to choose a color Dialog: This dialog is uses the generic dialog with standard buttons (equivalent of a QDialog) FileDialog: This dialog is used to choose a file from the local filesystem FontDialog: This dialog is used to choose a font MessageDialog: This dialog is used to display a message
You would have expected to see an InputDialog in this list (as we used the QInputDialog widget in Chapter 4, Conquering the Desktop UI) but Qt Quick does not have it. Create a new QML File (Qt Quick 2) and name it InputDialog.qml. The content should look like so: import import import import import import
QtQuick 2.6 QtQuick.Layouts 1.3 Qt.labs.controls 1.0 QtQuick.Dialogs 1.2 QtQuick.Window 2.2 "."
Dialog { property string label: "New item" property string hint: ""
[ 179 ]
Dominating the Mobile UI property alias editText : editTextItem standardButtons: StandardButton.Ok | StandardButton.Cancel onVisibleChanged: { editTextItem.focus = true editTextItem.selectAll() } onButtonClicked: { Qt.inputMethod.hide(); } Rectangle { implicitWidth: parent.width implicitHeight: 100 ColumnLayout { Text { id: labelItem text: label color: Style.text } TextInput { id: editTextItem inputMethodHints: Qt.ImhPreferUppercase text: hint color: Style.text } } } }
In this custom InputDialog, we take the generic Qt Quick Dialog and modify it to contain our TextInput item referenced by the ID editTextItem. We also added a labelItem just above editTextItem to describe the expected input. There are several things to note in this dialog. First, because we want this dialog to be generic, it has to be configurable. The caller should be able to provide parameters to display its specific data. This is done with the three properties at the top of the Dialog element: label: This property configures the displayed text in labelItem. hint: This property is the default text displayed in editTextItem. editText: This property references the “local” editTextItem element. This will
let the caller retrieve the value when the dialog is closed. [ 180 ]
Dominating the Mobile UI
We also configure the Dialog element to automatically use the platform buttons to validate or cancel the dialog with standardButtons: StandardButton.Ok | StandardButton.Cancel syntax. Finally, to make the dialog a bit more user-friendly, editTextItem has the focus when the Dialog element becomes visible and the text is selected. These two steps are done in the onVisibleChanged() callback function. When the dialog is hidden (that is, Ok or Cancel has been clicked), we hide the virtual keyboard with Qt.InputMethod.hide(). The InputDialog is ready to be used! Open AlbumListPage.qml and modify it like so: PageTheme { toolbarTitle: "Albums" toolbarButtons: ToolButton { background: Image { source: "qrc:/res/icons/album-add.svg" } onClicked: { newAlbumDialog.open() } } InputDialog { id: newAlbumDialog title: "New album" label: "Album name:" hint: "My Album" onAccepted: { albumModel.addAlbumFromName(editText.text) } }
We add InputDialog with the ID newAlbumDialog inside PageTheme element. We define all our custom properties: title, label, and hint. When the user clicks on the Ok button, the onAccepted() function is called. Here, it is a simple matter of calling the wrapper function addAlbumFromName() in the AlbumModel element with the entered text. This Dialog element is not visible by default, we open it by adding a ToolButton in toolbarButtons. This ToolButton will be added at the far right of the header as we specified in the PageTheme.qml file. To match mobile standards, we simply use a custom icon inside that button rather than text.
[ 181 ]
Dominating the Mobile UI
Here you can see that it is possible to reference images stored in the .qrc file with the syntax qrc:/res/icons/album-add.svg. We use SVG files to have scalable icons, but you are free to use your own icons for the gallery-mobile application. When the user clicks on the ToolButton, the onClicked() function is called, where we open newAlbumDialog. On our reference device, a Nexus 5X, this is how it looks:
When the user clicks on the OK button, the whole Model/View pipeline starts to work. This new album is persisted, the AlbumModel element emits the correct signals to notify our ListView, albumList, to refresh itself. We are starting to leverage the power of our gallery-core, which can be used in a desktop application and a mobile application without rewriting a significant portion of the engine code.
Loading images with an ImageProvider
It is now time to display the thumbnails for our freshly persisted album. These thumbnails have to be loaded somehow. Because our application is targeted at mobile devices, we cannot afford to freeze the UI thread while loading thumbnails. We would either hog the CPU or be killed by the OS, neither of which are desirable destinies for gallery-mobile. Qt provides a very handy class to handle the image loading: QQuickImageProvider. The QQuickImageProvider class provides an interface to load the QPixmap class in your QML code in an asynchronous manner. This class automatically spawns threads to load the QPixmap class and you simply have to implement the function requestPixmap(). There is more to it, QQuickImageProvider caches by default the requested pixmap to avoid hitting the data source too much.
[ 182 ]
Dominating the Mobile UI
Our thumbnails must be loaded from the PictureModel element, which gives access to the fileUrl of a given Picture. Our implementation of rQQuickImageProvider will have to get the QPixmap class for a row index in PicturelModel. Create a new C++ class named PictureImageProvider, and modify PictureImageProvider.h like this: #include class PictureModel; class PictureImageProvider : public QQuickImageProvider { public: PictureImageProvider(PictureModel* pictureModel); QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; private: PictureModel* mPictureModel; };
A pointer to the PictureModel element has to be provided in the constructor to be able to retrieve fileUrl. We override requestPixmap(), which takes an id parameter in its parameters list (the size and requestedSize can be safely ignored for now). This id parameter will be provided in the QML code when we want to load a picture. For a given Image in QML, the PictureImageProvider class will be called like so: Image { source: "image://pictures/" + index }
Let's break it down: image: This is the scheme for the URL source of the image. This tells Qt to work with an image provider to load the image. pictures: This is the identifier of the image provider. We will link the PictureImageProvider class and this identifier at the initialization of QmlEngine in main.cpp. index: This is the ID of the image. Here it is the row index of the picture. This corresponds to the id parameter in requestPixmap().
We already know that we want to display a picture in two modes: thumbnail and full resolution. In both cases, a QQuickImageProvider class will be used. These two modes have a very similar behavior: they will request PictureModel for fileUrl and return the loaded QPixmap. [ 183 ]
Dominating the Mobile UI
There is a pattern here. We can easily encapsulate these two modes in PictureImageProvider. The only thing we have to know is when the caller wants a thumbnail or a full resolution QPixmap. This can be easily done by making the id parameter more explicit. We are going to implement the requestPixmap() function to be able to be called in two ways: images://pictures//full: Using this syntax to retrieve the full resolution picture images://pictures//thumbnail: Using this syntax to retrieve the thumbnail version of the picture
If the index value was 0, these two calls would set the ID to 0/full or 0/thumbnail in requestPixmap(). Let's see the implementation in PictureImageProvider.cpp: #include "PictureModel.h" PictureImageProvider::PictureImageProvider(PictureModel* pictureModel) : QQuickImageProvider(QQuickImageProvider::Pixmap), mPictureModel(pictureModel) { } QPixmap PictureImageProvider::requestPixmap(const QString& id, QSize* /*size*/, const QSize& /*requestedSize*/) { QStringList query = id.split('/'); if (!mPictureModel || query.size() < 2) { return QPixmap(); } int row = query[0].toInt(); QString pictureSize = query[1]; QUrl fileUrl = mPictureModel->data(mPictureModel->index(row, 0), PictureModel::Roles::UrlRole).toUrl(); return ?? // Patience, the mystery will be soon unraveled }
We start by calling the QQuickImageProvider constructor with the QQuickImageProvider::Pixmap parameter to configure QQuickImageProvider to call requestPixmap(). The QQuickImageProvider constructor supports various image types (QImage, QPixmap, QSGTexture, QQuickImageResponse) and each one has its specific requestXXX() function. [ 184 ]
Dominating the Mobile UI
In the requestPixmap() function, we start by splitting this ID with the / separator. From here, we retrieve the row values and the desired pictureSize. The fileUrl is loaded by simply calling the mPictureModel::data() function with the right parameters. We used the exact same call in Chapter 4, Conquering the Desktop UI. Great, we know which fileUrl should be loaded and what the desired dimension is. However, we have one last thing to handle. Because we manipulate a row and not a database ID, we will have the same request URL for two different pictures, which are in different albums. Remember that PictureModel loads a list of pictures for a given Album. We should picture (pun intended) the situation. For an album called Holidays, the request URL will be images://pictures/0/thumbnail to load the first picture. It will be the same URL for another album Pets, which will load the first picture with images://pictures/0/thumbnail. As we said earlier, QQuickImageProvider automatically generates a cache which will avoid subsequent calls to requestPixmap() for the same URL. Thus, we will always serve the same picture, no matter which album is selected. This constraint forces us to disable the cache in PictureImageProvider and to roll out our own cache. This is an interesting thing to do; here is a possible implementation: // In PictureImageProvider.h #include #include ... public: static const QSize THUMBNAIL_SIZE; QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; QPixmap* pictureFromCache(const QString& filepath, const QString& pictureSize); private: PictureModel* mPictureModel; QCache mPicturesCache; }; // In const const const
PictureImageProvider.cpp QString PICTURE_SIZE_FULL = "full"; QString PICTURE_SIZE_THUMBNAIL = "thumbnail"; QSize PictureImageProvider::THUMBNAIL_SIZE = QSize(350, 350);
[ 185 ]
Dominating the Mobile UI QPixmap PictureImageProvider::requestPixmap(const QString& id, QSize* /*size*/, const QSize& /*requestedSize*/) { ... return *pictureFromCache(fileUrl.toLocalFile(), pictureSize); } QPixmap* PictureImageProvider::pictureFromCache(const QString& filepath, const QString& pictureSize) { QString key = QStringList{ pictureSize, filepath } .join("-"); QPixmap* cachePicture = nullptr; if (!mPicturesCache.contains(pictureSize)) { QPixmap originalPicture(filepath); if (pictureSize == PICTURE_SIZE_THUMBNAIL) { cachePicture = new QPixmap(originalPicture .scaled(THUMBNAIL_SIZE, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else if (pictureSize == PICTURE_SIZE_FULL) { cachePicture = new QPixmap(originalPicture); } mPicturesCache.insert(key, cachePicture); } else { cachePicture = mPicturesCache[pictureSize]; } return cachePicture; }
This new pictureFromCache() function aims to store the generated QPixmap in mPicturesCache and return the proper QPixmap. The mPicturesCache class relies on a QCache; this class lets us store data in a key/value fashion with the possibility to assign a cost for each entry. This cost should roughly map the memory cost of the object (by default, cost = 1). When QCache is instantiated, it is initialized with a maxCost value (by default 100). When the cost of the sum of all objects' exceeds the maxCost, QCache starts deleting objects to make room for the new objects, starting with the less recently accessed objects.
[ 186 ]
Dominating the Mobile UI
In the pictureFromCache() function, we first generate a key composed of the fileUrl and the pictureSize before trying to retrieve the QPixmap from the cache. If it is not present, the proper QPixmap (scaled to THUMBNAIL_SIZE macro if needed) will be generated and stored inside the cache. The mPicturesCache class becomes the owner of this QPixmap. The last step to complete the PictureImageProvider class is to make it available in the QML context. This is done in main.cpp: #include "AlbumModel.h" #include "PictureModel.h" #include "PictureImageProvider.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); ... QQmlContext* context = engine.rootContext(); context->setContextProperty("thumbnailSize", PictureImageProvider::THUMBNAIL_SIZE.width()); context->setContextProperty("albumModel", &albumModel); context->setContextProperty("pictureModel", &pictureModel); engine.addImageProvider("pictures", new PictureImageProvider(&pictureModel)); ... }
The PictureImageProvider class is added to the QML engine with engine.addImageProvider(). The first argument will be the provider identifier in QML. Note that the engine takes ownership of the passed PictureImageProvider. One last thing, the thumbnailSize parameter is also passed to engine, it will constrain the thumbnails to be displayed with the specified size in the QML code.
Displaying thumbnails in a GridView
The next step is to display these thumbnails. Create a new QML file named AlbumPage.qml: import import import import
QtQuick 2.6 QtQuick.Layouts 1.3 QtQuick.Controls 2.0 "."
[ 187 ]
Dominating the Mobile UI PageTheme { property string albumName property int albumRowIndex toolbarTitle: albumName GridView { id: thumbnailList model: pictureModel anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 cellWidth : thumbnailSize cellHeight: thumbnailSize delegate: Rectangle { width: thumbnailList.cellWidth - 10 height: thumbnailList.cellHeight - 10 color: "transparent" Image { id: thumbnail anchors.fill: parent fillMode: Image.PreserveAspectFit cache: false source: "image://pictures/" + index + "/thumbnail" } } } }
This new PageTheme element defines two properties: albumName and albumRowIndex. The albumName property is used to update the title in toolbarTitle; albumRowIndex will be used to interact with AlbumModel in order to rename or delete the album from the current page. To display thumbnails, we rely on a GridView element which will layout the thumbnails in a grid of cells. This thumbnailList item uses the pictureModel to request its data. The delegate is simply a Rectangle element with a single Image inside. This Rectangle element is slightly smaller than the thumbnailList.cellWidth or thumbnailList.cellHeight. The GridView element does not provide a spacing property (like ListView) for some room between each item. Thus, we simulate it by using a smaller area to display the content.
[ 188 ]
Dominating the Mobile UI
The Image item will try to take all the available space with anchors.fill: parent but will still keep the aspect ratio of the provided picture with fillMode: Image.PreserveAspectFit. You recognize the source attribute where the current delegate index is provided to retrieve the thumbnail. Finally, the cache: false attribute ensures that the PictureImageProvider class will not try to use the native cache. To display AlbumPage.qml, we have to update the stackView (located in main.qml). Remember that stackView has been declared as a property (pageStack), it is thus accessible from any QML file. The AlbumPage element will be displayed when the user clicks on the MouseArea element for a given Album value in AlbumListPage.qml. We will now give the ability to the user to add a new picture. To do this, we will rely on a QtQuick Dialog: FileDialog. Here is the updated version of AlbumPage.qml: import import import import import
QtQuick 2.6 QtQuick.Layouts 1.3 QtQuick.Controls 2.0 QtQuick.Dialogs 1.2 "."
PageTheme { property string albumName property int albumRowIndex toolbarTitle: albumName toolbarButtons: RowLayout { ToolButton { background: Image { source: "qrc:/res/icons/photo-add.svg" } onClicked: { dialog.open() } } } FileDialog { id: dialog title: "Open file" folder: shortcuts.pictures onAccepted: { var pictureUrl = dialog.fileUrl pictureModel.addPictureFromUrl(pictureUrl)
[ 189 ]
Dominating the Mobile UI dialog.close() } } GridView { ... }
The FileDialog element is straightforward to implement. By using the folder: shortcuts.pictures property, QtQuick will automatically position the FileDialog element in the platform-specific pictures directory. Even better, on iOS it will open the native photo application where you can pick your own picture. When the user validates his picture choice, the path is available in the onAccepted()function with the dialog.fileUrl field, which we stored in the pictureUrl variable. This pictureUrl variable is then passed to a new wrapper function of PictureModel: addPictureFromUrl(). The pattern used is exactly the same as we did for AlbumModel::addAlbumFromName(): a Q_INVOKABLE wrapper function around PictureModel::addPicture(). The only missing parts of AlbumPage are the delete album and rename album features. They follow patterns we already covered. The deletion will be done using a wrapper function in AlbumModel, and the rename reuses the InputDialog we created for AlbumListPage.qml. Please refer to the source code of the chapter to see the implementation for these features. This is how the thumbnails will look on an Android device:
[ 190 ]
Dominating the Mobile UI
Swiping through full resolution pictures
The last page we have to implement in gallery-mobile is the full resolution picture page. In Chapter 4, Conquering the Desktop UI, we navigated through the pictures using previous/next buttons. In this chapter, we target the mobile platform. Therefore, the navigation should be done using a touch-based gesture: a fling. Here is the implementation of this new PicturePage.qml file: import import import import
QtQuick 2.0 QtQuick.Layouts 1.3 QtQuick.Controls 2.0 "."
PageTheme { property string pictureName property int pictureIndex toolbarTitle: pictureName ListView { id: pictureListView model: pictureModel anchors.fill: parent spacing: 5 orientation: Qt.Horizontal snapMode: ListView.SnapOneItem currentIndex: pictureIndex Component.onCompleted: { positionViewAtIndex(currentIndex, ListView.SnapPosition) } delegate: Rectangle { property int itemIndex: index property string itemName: name width: ListView.view.width == 0 ? parent.width : ListView.view.width height: pictureListView.height color: "transparent" Image { fillMode: Image.PreserveAspectFit cache: false
[ 191 ]
Dominating the Mobile UI width: parent.width height: parent.height source: "image://pictures/" + index + "/full" } } } }
We first define two properties, pictureName and pictureIndex. The current pictureName property is displayed in the toolbarTitle and pictureIndex is used to initialize the correct currentIndex in ListView, currentIndex: pictureIndex. To be able to swipe through the pictures, we again use a ListView. Here, each item (a simple Image element) will take the full size of its parent. When the component is loaded, even if currentIndex is correctly set, the view has to be updated to be positioned at the correct index. This is what we do in pictureListView with this: Component.onCompleted: { positionViewAtIndex(currentIndex, ListView.SnapPosition) }
This will update the position of the current visible item to currentIndex. So far so good. Nonetheless, when a ListView is created, the first thing it does is to initialize its delegate. A ListView has a view property, which is filled with the delegate content. That implies that the ListView.view (yes, it hurts) does not have any width in Component.onCompleted(). As a consequence, the positionViewAtIndex() function does… absolutely nothing. To prevent this behavior, we have to provide a default initial width to the delegate with the ternary expression ListView.view.width == 0 ? parent.width : ListView.view.width. The view will then have a default width on the first load and the positionViewAtIndex() function can happily move until ListView.view is properly loaded. To swipe through each picture, we set the snapMode value of the ListView to ListView.SnapOneItem. Each fling will snap to the next or previous picture without continuing the motion. The Image item of the delegate looks very much like the thumbnail version. The sole difference is the source property, where we request PictureImageProvider class with the full resolution.
[ 192 ]
Dominating the Mobile UI
When PicturePage opens, the correct pictureName property is displayed in the header. However, when the user flings to another picture, the name is not updated. To handle this, we have to detect the motion state. Add the following callbacks in pictureListView: onMovementEnded: { currentIndex = itemAt(contentX, contentY).itemIndex } onCurrentItemChanged: { toolbarTitleLabel.text = currentItem.itemName }
The onMovementEnded() class is triggered when the motion started by the swipe has ended. In this function, we update the ListViewcurrentIndex with the itemIndex of the visible item at the contentX and contentY coordinates. The second function, onCurrentItemChanged(), is called upon the currentIndex update. It will simply update the toolbarTitleLabel.text with the picture name of the current item. To display PicturePage.qml, the same MouseArea pattern is used in the thumbnailList delegate of AlbumPage.qml: MouseArea { anchors.fill: parent onClicked: { thumbnailList.currentIndex = index pageStack.push("qrc:/qml/PicturePage.qml", { pictureName: name, pictureIndex: index }) } }
Again, the PicturePage.qml file is pushed on the pageStack and the needed parameters (pictureName and pictureIndex) are provided in the same manner.
[ 193 ]
Dominating the Mobile UI
Summary
This chapter brings closure to the development of the gallery application. We built a strong foundation with gallery-core, created a widget UI with gallery-desktop, and finally crafted a QML UI with gallery-mobile. QML enables a very fast approach to UI development. Unfortunately, the technology is still young and rapidly changing. The integration with mobile OSes (Android, iOS) is under heavy development and we hope that it will lead to great mobile applications with Qt. For now, the inherent limits of a mobile cross-platform toolkit are still hard to overcome. The next chapter will take QML technology to new shores: the development of a snake game running on a Raspberry Pi.
[ 194 ]
6
Even Qt Deserves a Slice of Raspberry Pi In the previous chapter, we created a QML UI targeted at Android and iOS. We will continue our journey in the embedded world by discovering how we can deploy a Qt application on a Raspberry Pi. The example project to illustrate this topic will be a snake game using the Qt3D modules. The player will control a snake trying to eat apples to get as big as possible. In this chapter, you will learn: The architecture of the Qt3D modules The basic principles of cross-compilation How to build your own Qt Creator kit to compile and deploy your game on a Raspberry Pi How to handle the differences and limitations of various platforms (desktop, Raspberry Pi) The Factory design pattern How to write a complete game engine using JavaScript and QML The usage of the QML Profiler
Even Qt Deserves a Slice of Raspberry Pi
Discovering Qt3D
The example project of this chapter will rely on 3D rendering. For this, we will use Qt3D. This part of the framework is divided into various Qt modules that enable the application to have a near-real time simulation of a 3D environment. Built on OpenGL, Qt3D offers a highlevel API to describe complex scenes without having to resort to writing low-level OpenGL instructions. Qt3D supports the following basic features: 2D and 3D rendering for C++ and Qt Quick Meshes Materials GLSL shaders Shadow mapping Deferred rendering Instance rendering Uniform Buffer Object All these features are implemented in the ECS (entity component system) architecture. Each mesh, material, or shader that you define is a component. The aggregation of these components makes an entity. If you wanted to draw a 3D red apple, you would need the following components: A mesh component, holding the vertices of your apple A material component, applying a texture on the mesh or coloring it These two components will then be regrouped to define the entity Apple. You see here the two parts of the ECS: entities and components. The overall architecture looks like this:
[ 196 ]
Even Qt Deserves a Slice of Raspberry Pi
Each of these components can be regrouped in aspects. An aspect is a “slice” of multiple components working on the same part (rendering, audio, logic, and physics). When the graph of all your entities is processed by the Qt3D engine, each layer of aspects is processed sequentially. The underlying approach is to favor composition over inheritance. In a game, an entity (an apple, a player, an enemy) can have various states during its life cycle: spawning, animating for a given state, dying animation, and so on. Using inheritance to describe these states will lead to a nerve-wracking tree: AppleSpawn, AppleAnimationShiny, AppleDeath, and so on. It would become quickly unmaintainable. Any modification to a class could have huge impact on many other classes and the number of possible combinations of states would get out of hand. Saying that a state is simply a component for a given entity, gives the flexibility to easily swap components and still keep the entity abstraction; an apple Entity element is still an apple, even though it is using the AnimationShinyComponent instead of the AnimationSpawnComponent. Let's see how to define a basic Entity element in QML. Imagine that this is the apple we have been talking about. The Apple.qml file would look like this: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0 Entity {
[ 197 ]
Even Qt Deserves a Slice of Raspberry Pi property alias position: transform.translation PhongMaterial { id: material diffuse: "red" } SphereMesh { id: mesh } Transform { id: transform } components: [material, mesh, transform] }
In a very few lines, you describe every aspect of the Entity element: Entity: This is the root object of the file; it follows the same QML pattern we studied in Chapter 5, Dominating the Mobile UI. PhongMaterial: This defines how the surface will be rendered. Here, it uses the Phong shading technique to achieve smooth surfaces. It inherits QMaterial, which is the base class for all the material classes. CuboidMesh: This defines what type of mesh will be used. It inherits QGeometryRenderer, which also gives the ability to load custom models (exported from 3D modeling software). Transform: This defines the transformation matrix of the component. It can customize the translation, scale, and position of the Entity element. Position: This is a property to expose transform.translation for a given caller/parent. This might quickly become handy if we want to move the apple around. Components: This is the array containing all the IDs of all the components for the Entity element.
If we want to make this Apple a child of another Entity, it is simply a matter of defining the Apple inside this new Entity element. Let's call it World.qml: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0 Entity { id: sceneRoot
[ 198 ]
Even Qt Deserves a Slice of Raspberry Pi RenderSettings { id: frameFraph activeFrameGraph: ForwardRenderer { clearColor: Qt.rgba(0, 0, 0, 1) } } Apple { id: apple position: Qt.vector3d(3.0, 0.0, 2.0) } components: [frameGraph] }
Here, the World Entity has no visual representation; we want it to be the root of our 3D scene. It only contains the Apple we defined earlier. The x, y, z coordinates of the apple are relative to the parent. When the parent makes a translation, the same translation will be applied to the apple. This is how the hierarchy of entities/components is defined. If you write your Qt3D code in C++, the same logic applies to the equivalent C++ classes (QEntity, QComponent, and so on). Because we decided to use the World.qml file as the root of our scene, it has to define how the scene will be rendered. The Qt3D rendering algorithm is data-driven. In other words, there is a clear separation between what should be rendered (the tree of entities and components) and how it should be rendered. The how relies on a similar tree structure using framegraph. In Qt Quick, a single method of rendering is used and it covers the 2D drawing. On the other hand, in 3D, the need for flexible rendering makes it necessary to decouple the rendering techniques. Consider this example: you play a game where you control your avatar and you encounter a mirror. The same 3D scene must be rendered from multiple viewports. If the rendering technique is fixed, this poses multiple problems: which viewport should be drawn first? Is it possible to parallelize the rendering of the viewports in the GPU? What if we need to make multiple passes for the rendering? In this code snippet, we use the traditional OpenGL rendering technique with the ForwardRenderer tree, where each object is rendered directly on the back buffer, one at a time. Qt3D offers the possibility to choose the renderer (ForwardRenderer, DeferredRenderer, and so on) and configure how the scene should be rendered. [ 199 ]
Even Qt Deserves a Slice of Raspberry Pi
OpenGL typically uses the double-buffering technique to render its content. The frontbuffer is what is displayed on the screen and the back-buffer is where the scene is being rendered. When the back-buffer is ready, the two buffers are swapped. One last thing to notice at the top of each Entity element is that we specified the following: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0
There are only Qt3D modules in the import section. Qt3D classes do not inherit Item so cannot be directly mixed with QML components. This inheritance tree of the basic Qt3D building blocks is:
The QNode class is the base class of all Qt3D node classes. It relies on the QObject class to define the parenting relationship. Each QNode class instance also adds a unique id variable, which allows it to be recognized from other instances. Even though QNode cannot be mixed with Qt Quick types, they can be added to a Q3DScene element (or Scene3D in QML), which serves as the canvas for Qt3D content and can be added to a Qt Quick Item. Adding World.qml to a scene is as simple as this: Rectangle { Scene3D { id: scene anchors.fill: parent focus: true World { } } }
The Scene3D element includes a World instance and defines common Qt Quick properties (anchors, focus).
[ 200 ]
Even Qt Deserves a Slice of Raspberry Pi
Configuring Qt for your Raspberry Pi
This project targets a new embedded platform: the Raspberry Pi. Qt officially supports the Raspberry Pi 2, but we got the project running without any trouble on a Raspberry Pi 3. If you do not have one of these devices, it might be nonetheless interesting to read this section to know how the cross-compilation works and how to configure your own kit in Qt Creator. The rest of the chapter will work on a Desktop platform anyway. Before diving into the Raspberry Pi configuration, let's take a step back to understand our aim. Your computer is probably running on an x86 CPU architecture. This means that every program you run will be executed with the x86 instructions set of your CPU. In Qt Creator, this translates to your available kits. A kit must match your target platform. On startup, Qt Creator searches for available kits in your computer and loads them for you. In Chapter 5, Dominating the Mobile UI, we targeted different platforms: Android and iOS. These platforms are running on a different CPU instruction set: ARM. Luckily, the people behind Qt automatically configured for us the necessary nuts and bolts to make it work. The Raspberry Pi also runs on ARM but it is not ready for Qt by default. We have to prepare it before playing with it in Qt Creator. Note that the following commands are run from a Linux box, but you should be able to run them from Mac or Windows with Cygwin. Please follow the complete guide to prepare your Raspberry Pi for Qt at ht
tps://wiki.qt.io/RaspberryPi2EGLFS, or simply download a precompiled bundle from http://www.qtrpi.com.
The complete Raspberry Pi installation guide is outside the scope of the book. It is interesting nonetheless to sum up the main steps: 1. Add development packages to the Raspberry Pi. 2. Retrieve the complete toolchain, including the cross-compiler that will be executed from your machine. 3. Create a sysroot folder on your machine that will mirror the necessary directories from the Raspberry Pi. 4. Compile Qt with the cross-compiler in the sysroot folder. 5. Synchronize this sysroot with the Raspberry Pi.
[ 201 ]
Even Qt Deserves a Slice of Raspberry Pi
A sysroot is simply a directory containing a minimal filesystem for a given platform. It typically contains the /usr/lib and /usr/include directories. Having this directory on your machine enables the cross-compiler to properly compile and link the output binary without being executed from the Raspberry Pi. All these steps are done to avoid compiling anything directly on the Raspberry Pi. Being a low-powered device, the execution of any compilation would take a very, very long time. Compiling Qt on a Raspberry Pi would easily take more than 40 hours. Knowing this, the time spent on configuring the cross-compiler seems much easier to bear. The qopenglwidget example mentioned in the wiki should be properly running before proceeding. Once this has been done, we have to cross-compile a few more Qt modules to have our project running: Qtdeclarative: This model is used to access Qt Quick qt3d: This model is used to construct a 3D world qtquickcontrols: This model is used to include interesting controls (Label) qtquickcontrols2: This model is used to make some new layouts available
For each of these modules, execute the following commands (from your ~/raspi directory): git clone git://code.qt.io/qt/.git -b 5.7 cd ~/raspi/qt5/bin/qmake -r make make install
You can speed up the compilation by adding the parameter -j (or -jobs) to make. The make command will try to parallelize the compilations jobs over your CPU cores, if you have four cores, use make -j 4, eight cores, make -j 8, and so on. When everything has been compiled, synchronize your sysroot directory again with: rsync -avz qt5pi pi@IP:/usr/local
In the previous command, you must replace the IP with the real Raspberry Pi address.
[ 202 ]
Even Qt Deserves a Slice of Raspberry Pi
The Raspberry Pi is ready to execute our Qt code. However, we have to create our own kit in Qt Creator to be able to compile and deploy our program on it. A kit is composed of the following parts: A compiler that will compile your code using the CPU instruction set of the target platform A debugger that will know the instruction set of the target platform to properly break and read the memory content A Qt version compiled for the targeted platform to compile and link your binary to the target platform's shared objects A device to which Qt Creator can connect to deploy and execute your program We will start with the compiler. In Qt Creator: 1. Go to Tools | Options | Build & Run | Compilers. 2. Click on Add |GCC. 3. Browse to ~/raspi/tools/arm-bcm2708/gcc-linaro-arm-linuxgnueabihf-raspbian/bin/arm-linux-gnueabihf-g++. 4. Rename the compiler to Rpi GCC. This strange binary name makes it easier for Qt to parse the ABI (application binary interface) to find out the platform architecture, file format, and so on. It should look like this:
[ 203 ]
Even Qt Deserves a Slice of Raspberry Pi
Now for the debugger. As we said earlier, we are building this project from a Linux box (Ubuntu). Cross-compilation and embedded development tend to be easier on Linux but you should be able to do the same on a Windows or Mac with a few additional steps. On Ubuntu Linux, just install a multi-architecture gdb with the command sudo apt-get install gdb-multiarch. In Qt Creator, add this new debugger in the Debuggers tab:
Next, add the cross-compiled Qt explained on the wiki page in the Qt Versions tab. Click on Add and browse to ~/raspi/qt5/bin/qmake. This is the resulting Qt Version:
[ 204 ]
Even Qt Deserves a Slice of Raspberry Pi
We are almost there! Before building the kit, we simply have to configure Raspberry Pi device access. In Options | Devices, follow this procedure: 1. Click on Add.. | Generic Linux Device | Start Wizard. 2. The name will be Rpi 2 (or 3 if you have one). 3. Enter the IP address of your device (indeed, you have to be connected to your local network!). 4. The default username is pi. 5. The default password is “raspberry”. 6. Click on Next to test the connection to the device. If everything went well, this is your new device:
[ 205 ]
Even Qt Deserves a Slice of Raspberry Pi
Finally, the kit will compose all these parts into a valid Qt Creator platform. Go back to Build & Run | Kits. From here you simply have to point to each of the parts we built previously. Here is the resulting kit:
Note that the Sysroot filed should point to the sysroot folder we previously created at ~/raspi/sysroot. If you click on the button to the right of Name, you can choose a custom picture for a kit, such as the Raspberry Pi logo.
Everything is now ready to make an awesome snake game.
Creating an entry point for your Qt3D code
For those who did not play the snake game in their youth, here is a quick reminder of the gameplay: You control a snake moving in an empty area [ 206 ]
Even Qt Deserves a Slice of Raspberry Pi
This area is surrounded by walls An apple spawns randomly in the game area If the snake eats the apple, it grows and you gain a point. Right after, another apple spawns in the game area If the snake touches a wall or a part of its own body, you lose The goal is to eat as many apples as possible to have the highest score. The longer the snake, the harder it will become to avoid the wall and its own tail. Oh, and the snake goes faster and faster each time it eats an apple. The architecture of the game will be the following: All the game items will be defined using Qt3D in QML All the game logic will be done in JavaScript, which will communicate with the QML elements We will keep the 2D feel of the original snake game by placing the camera above the game area but we will spice things up with 3D models and some shaders. Alright, we spent an awful lot of pages preparing for this moment. It is now time to begin the snake project. Create a new Qt Quick Controls Application named ch06-snake. In the project details: 1. 2. 3. 4.
Select Qt 5.6 for the minimal required Qt version field. Uncheck With ui.qml file. Uncheck Enable native styling. Click on Next and select the following kits: RaspberryPi 2 Desktop
5. Click on Next | Finish. We have to add the Qt3D modules. Modify ch06-snake.pro like this: TEMPLATE = app QT += qml quick 3dcore 3drender 3dquick 3dinput 3dextras CONFIG += c++11 SOURCES += main.cpp RESOURCES += \ snake.qrc HEADERS +=
[ 207 ]
Even Qt Deserves a Slice of Raspberry Pi target.files = ch06-snake target.path = /home/pi INSTALLS += target
We have to prepare the entry point of the application to have a proper OpenGL context with which Qt3D can work. Open and update main.cpp like so: #include #include #include #include
int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); qputenv("QT3D_GLSL100_WORKAROUND", ""); QSurfaceFormat format; if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { format.setVersion(3, 2); format.setProfile(QSurfaceFormat::CoreProfile); } format.setDepthBufferSize(24); format.setStencilBufferSize(8); QQuickView view; view.setFormat(format); view.setResizeMode(QQuickView::SizeRootObjectToView); QObject::connect(view.engine(), &QQmlEngine::quit, &app, &QGuiApplication::quit); view.setSource(QUrl("qrc:/main.qml")); view.show(); return app.exec(); }
The idea is to configure a QSurfaceFormat to properly handle OpenGL and to give it to a custom QQuickView view. This view will use this format to paint itself. The qputenv("QT3D_GLSL100_WORKAROUND", "") instruction is a workaround related to Qt3D shaders on some embedded Linux devices, such as the Raspberry Pi. It will enable a separate GLSL 1.00 snippet for the lights required by some embedded devices. If you do not use this workaround, you will get a black screen and will not be able to properly run the project on Raspberry Pi. [ 208 ]
Even Qt Deserves a Slice of Raspberry Pi
The details of the Qt3d lights workaround are here:
https://codereview.qt-project.org/#/c/143136/.
We chose to handle the view using Qt Quick. Another approach would be to create a C++ class that inherits QMainWindow and make it the parent of the QML content. This approach can be found in many Qt3D example projects. Both are valid and work. You tend to write more code with the QMainWindow approach, but it allows you to create 3D scenes with C++ only. Note that view from the main.cpp file tries to load a main.qml file. You can see it coming; here is the main.qml: import QtQuick 2.6 import QtQuick.Controls 1.4 Item { id: mainView property int score: 0 readonly property alias window: mainView width: 1280; height: 768 visible: true Keys.onEscapePressed: { Qt.quit() } Rectangle { id: hud color: "#31363b" anchors.left: parent.left anchors.right: parent.right anchors.top : parent.top height: 60 Label { id: snakeSizeText anchors.centerIn: parent font.pointSize: 25 color: "white" text: "Score: " + score }
[ 209 ]
Even Qt Deserves a Slice of Raspberry Pi } }
Here we define the HUD (heads up display) at the top of the screen, where the score (the number of apples eaten) will be displayed. Note that we bound the Escape key to the Qt.quit() signal. This signal is connected in main.cpp to the QGuiApplication::quit() signal to quit the application. The QML context is now ready to welcome Qt3D content. Modify main.qml like so: import QtQuick 2.6 import QtQuick.Controls 1.4 import QtQuick.Scene3D 2.0 Item { ... Rectangle { id: hud ... } Scene3D { id: scene anchors.top: hud.bottom anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right focus: true aspects: "input" } }
The Scene3D element takes all the available space below the hud object. It takes the focus of the window to be able to intercept keyboard events. It also enables the input aspect to let the Qt3D engine process keyboard events in its graph traversal.
Setting up the scene
We can now start writing Qt3D code. The first step is to define the root of the scene. Create a new file named GameArea.qml: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0
[ 210 ]
Even Qt Deserves a Slice of Raspberry Pi Entity { id: root property alias gameRoot: root Camera { id: camera property real x: 24.5 property real y: 14.0 projectionType: CameraLens.PerspectiveProjection fieldOfView: 45 aspectRatio: 16/9 nearPlane : 0.1 farPlane : 1000.0 position: Qt.vector3d( x, y, 33.0 ) upVector: Qt.vector3d( 0.0, 1.0, 0.0 ) viewCenter: Qt.vector3d( x, y, 0.0 ) }
RenderSettings { id: frameFraph activeFrameGraph: ForwardRenderer { clearColor: Qt.rgba(0, 0, 0, 1) camera: camera } } components: [frameFraph] }
The first thing we do is create a camera and position it. Remember that, in OpenGL, the coordinates follow the thumb on your right hand points left!:
[ 211 ]
Even Qt Deserves a Slice of Raspberry Pi
By placing the camera at Qt.vector3d(x, y, 33), we make it come “out of the screen” to be able to express our yet-to-be-created entitiy's coordinates with the simple x, y axis. The upVector: Qt.vector3d(0.0, 1.0, 0.0) specifies the up vector of the camera, in our case it is the Y axis. Finally, we point at Qt.vector(x, y, 0), meaning the center of the screen. The overall goal is to simplify coordinate expression. By positioning the camera this way, placing an object at the coordinate 0, 0 will put it in the bottom-left part of the window, whereas the coordinates 50, 28 mean the top-right part of the window. We also configure RenderSettings with a ForwardRendered that defines two properties: clearColor: This property Qt.rgba(0, 0, 0, 1) means that the background will be pitch-black camera: This property is used to determine the viewport to be rendered
The scene is ready to be rendered, but we need to handle user input, namely the keyboard. To capture keyboard events, modify GameArea.qml to look like this: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Input 2.0 Entity { ... RenderSettings { ... } KeyboardDevice { id: keyboardController } InputSettings { id: inputSettings } KeyboardHandler { id: input sourceDevice: keyboardController focus: true onPressed: { } } components: [frameFraph, input] }
[ 212 ]
Even Qt Deserves a Slice of Raspberry Pi
The KeyboardDevice element is in charge of dispatching key events to the active KeyboardHandler, namely input. The KeyboardHandler component is attached to the controller and the onPressed() function will be called each time a key is pressed. The KeyboardHandler is a component; therefore it needs to be added to the list of components for GameArea. The last missing part of GameArea is preapring the engine execution (initialization and update): import import import import
Qt3D.Core 2.0 Qt3D.Render 2.0 Qt3D.Input 2.0 QtQuick 2.6 as QQ2
Entity { id: root property property property property property ...
alias gameRoot: root alias timerInterval: timer.interval int initialTimeInterval: 80 int initialSnakeSize: 5 string state: ""
KeyboardDevice { id: keyboardController } QQ2.Component.onCompleted: { console.log("Start game..."); timer.start() } QQ2.Timer { id: timer interval: initialTimeInterval repeat: true onTriggered: {} } components: [frameFraph, input] }
[ 213 ]
Even Qt Deserves a Slice of Raspberry Pi
Here we mix Qt Quick elements with Qt3D. Due to possible name collisions, we have to import the module using the alias QQ2. We already met Component.onCompleted in Chapter 5, Dominating the Mobile UI. Its job will be to start the game engine and start the timer defined right after. This timer variable will repeat every 80 milliseconds (as defined in the initialTimeInterval property) and call the engine's update() function. This function will be covered when we build the engine code, later in this chapter. The goal is to emulate the original snake game as closely as possible. The whole game logic will be updated at this interval and not at the normal frame refresh interval. After each call to update(), the snake will advance. As a result, the snake's movement will not be smooth but rather jerky. This is clearly a design choice we made to have a retro-gaming feeling. Each time the snake eats an apple, two things happen: The interval of the timer will be reduced by the engine (accessed by the timerInterval property). The snake will grow. Its initial size is defined in the intialSnakeSize property. Reducing the timer interval will make the snake advance faster until it becomes very hard to manage its direction.
Assembling your Qt3D entities
We will now proceed to create the building blocks of the game, each in the form of an Entity element: Wall: This represents the limit of where the snake cannot go SnakePart: This represents a part of the snake's body Apple: This represents the apple (no way!) spawned at a random location Background: This represents a good-looking background behind the snake and
the apple
Each entity will be placed on a grid handled by the engine and will have a type identifier to make it easier to find. To factorize these properties, let's create a parent QML file named GameEntity.qml: import Qt3D.Core 2.0 Entity { property int type: 0
[ 214 ]
Even Qt Deserves a Slice of Raspberry Pi property vector2d gridPosition: Qt.vector2d(0, 0) }
This Entity element only defines a type property and a gridPosition property , which will be used by the engine to lay out the content on the grid. The first item we will build is the Wall.qml file: import Qt3D.Core 2.0 GameEntity { id: root property alias position: transform.translation Transform { id: transform } components: [transform] }
As you can see, the Wall type does not have any visual representation. Because we target a Raspberry Pi device, we have to be very careful with the CPU/GPU consumption. The game area will be a grid where each cell contains an instance of one of our entities. The snake will be surrounded by Wall instances. The Raspberry Pi is much slower than your average computer, to the extent that the game would become unbearably slow if we displayed all the walls. To address this issue, the walls are invisible. They will be placed outside the visible viewport and the borders of the window will act as the visual limit of the snake. Of course, if you do not target the Raspberry Pi, but rather your computer, it is fine to display the walls and make them look fancier than just nothing. The next Entity element we will implement is SnakePart.qml: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0 GameEntity { id: root property alias position: transform.translation PhongMaterial { id: material
[ 215 ]
Even Qt Deserves a Slice of Raspberry Pi diffuse: "green" } CuboidMesh { id: mesh } Transform { id: transform } components: [material, mesh, transform] }
If added to the GameArea scene, the SnakePart block will display a single green cube. The SnakePart block is not the complete snake, rather a part of its body. Remember that the snake grows each time it eats an apple. Growing means adding a new instance of SnakePart to a list of SnakePart. Let's proceed with the Apple.qml: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0 GameEntity { id: root property alias position: transform.translation property alias color: material.diffuse Transform { id: transform scale: 0.5 } Mesh { id: mesh source: "models/apple.obj" } DiffuseMapMaterial { id: material diffuse: "qrc:/models/apple-texture.png" } components: [material, mesh, transform]
[ 216 ]
Even Qt Deserves a Slice of Raspberry Pi }
This snippet starts with introducing more complex yet easy-to-use features of Qt3D, namely a custom mesh and a texture applied to it. Qt3D supports the Wavefront obj format to load custom meshes. Here we added a home-cooked apple to the .qrc file of the application and we just have to provide the path to this resource to load it. The same principle is applied for the DiffuseMapMaterial element. We added a custom texture and added it as a source of the component. As you can see, the Entity definition and its components look very much the same. Yet we effortlessly traded a Qt3D CuboidMesh with a custom model. We will push things even further with Background.qml: import Qt3D.Core 2.0 import Qt3D.Render 2.0 import Qt3D.Extras 2.0 Entity { id: root property alias position: transform.translation property alias scale3D: transform.scale3D MaterialBackground { id: material } CuboidMesh { id: mesh } Transform { id: transform } components: [material, mesh, transform] }
[ 217 ]
Even Qt Deserves a Slice of Raspberry Pi
The Background block will be displayed behind the snake and the apple. At first sight, this entity looks very much like SnakePart. However, Material is not a Qt3D class. It is a custom defined Material that relies on shaders. Let's see MaterialBackground.qml: import Qt3D.Core 2.0 import Qt3D.Render 2.0 Material { id: material effect: Effect { techniques: [ Technique { graphicsApiFilter { api: GraphicsApiFilter.OpenGL majorVersion: 3 minorVersion: 2 } renderPasses: RenderPass { shaderProgram: ShaderProgram { vertexShaderCode: loadSource("qrc:/shaders/gl3/grass.vert") fragmentShaderCode: loadSource("qrc:/shaders/gl3/grass.frag") } } } ] } }
If you are not familiar with shaders, we can summarize them in the following statement: shaders are computer programs written in a C-style syntax that are executed by the GPU. Data from your logic will be fed by the CPU and made available to the GPU memory where your shaders will run. Here we manipulate two types of shader: Vertex shader, which is executed on each vertex of the source of your mesh Fragment, which is executed on each pixel to produce the final rendering By being executed on the GPU, these shaders utilize the huge parallelization power of the GPU (which is orders-of-magnitude higher than your CPU). It enables modern games to have such stunning visual rendering. Covering shaders and the OpenGL pipeline is beyond the scope of this book (you can fill several bookshelves on this subject alone). We will limit ourselves to showing you how you can use shaders in Qt3D.
[ 218 ]
Even Qt Deserves a Slice of Raspberry Pi
If you want to delve into OpenGL or sharpen your skills with shaders, we recommend the OpenGL SuperBible, by Graham Sellers, Richard S Wright Jr., and Nicholas Haemel. Qt3D supports shaders in a very convenient way. Simply add your shader file to the .qrc resource file and load it in the effect property of a given Material. In this snippet, we specify that this shader Technique should be run only on OpenGL 3.2. This is indicated in the graphicsApiFilter block. This version of OpenGL targets your desktop machine. Because the performance gap between your desktop and your Raspberry Pi is very marked, we have the ability to execute different shaders depending on the platform. Thus, here is the Raspberry Pi-compatible technique: Technique { graphicsApiFilter { api: GraphicsApiFilter.OpenGLES majorVersion: 2 minorVersion: 0 } renderPasses: RenderPass { shaderProgram: ShaderProgram { vertexShaderCode: loadSource("qrc:/shaders/es2/grass.vert") fragmentShaderCode: loadSource("qrc:/shaders/es2/grass.frag") } } }
You just have to add it to the techniques property of the Material. Note that the targeted OpenGL version is OpenGLES 2.0, which will run fine on your Raspberry Pi and even your iOS/Android phone. A last thing to cover is how parameters can be passed to shaders. Here is an example: Material { id: material parameters: [ Parameter { name: "score"; value: score }
[ 219 ]
Even Qt Deserves a Slice of Raspberry Pi ] ... }
The score variable will be accessible in the shader with this simple section. Please take a look at the source code for the chapter to see the complete content of this Material element. We had the fun of writing a shader displaying a moving and glowing wave over a grass texture. The only fixed element of the game is the background. We can directly add it to GameArea.qml: Entity { id: root ... Background { position: Qt.vector3d(camera.x, camera.y, 0) scale3D: Qt.vector3d(camera.x * 2, camera.y * 2, 0) } components: [frameFraph, input] }
The Background element is positioned to cover the whole visible area behind the snake and the apple. Being defined inside GameArea, it will be automatically added to the entity/component tree and will be drawn right away.
Preparing the board game
Even if our game has a 3D representation, we will implement 2D gameplay like the original snake game. Our game items are born, will live, and die in a 2D area. Like chess, this board will be composed of rows and columns. But in our snake game, each square can be: An apple A snake A wall Empty
[ 220 ]
Even Qt Deserves a Slice of Raspberry Pi
Here is an example of a board representation from the point of view of the engine:
This is a small 10×8 board; even if the size does not matter, you will be able to define a bigger one. Your game, your rules! We have walls (W) surrounding the game area. An apple (A) is spawned at 7×2. Finally, we have a snake (S) beginning at 3×4 and ending at 5×5. It is time to create our board class. Please create a JS file called board.js: function Board(columnCount, rowCount, blockSize) { this.columnCount = columnCount; this.rowCount = rowCount; this.blockSize = blockSize; this.maxIndex = columnCount * rowCount; this.data = new Array(this.maxIndex); }
This object constructor function required three parameters. The columnCount and rowCount parameters will help you to choose the board dimension. The last parameter, blockSize, is the size of a board square in the OpenGL world. For example, we can set blockSize to 10. In this case, the apple in 7×2 on the board we be displayed with x = 70 and y = 20 in the OpenGL world. In this chapter, we will use a blockSize of 1, so the board coordinates match OpenGL coordinates. Let's add some utility functions to board.js: Board.prototype.init = function() { for (var i = 0; i < this.data.length; i++) { this.data[i] = null; } } Board.prototype.index = function(column, row) { return column + (row * this.columnCount); }
[ 221 ]
Even Qt Deserves a Slice of Raspberry Pi Board.prototype.setData = function(data, column, row) { this.data[this.index(column, row)] = data; } Board.prototype.at = function(column, row) { return this.data[this.index(column, row)]; }
Defining a class in JavaScript can be disturbing for a C++ developer. Every JavaScript object has a prototype object to which you can add functions. We are using it to add class methods to Board. Here is a summary of the purpose of each function of the Board class: init(): This function initializes all array values to the null value index(): This function returns the array index from column/row coordinates setData(): This function assigns the data value on the board from column/row
coordinates at(): This function retrieves the data value in an array from column/row coordinates Please note that, in our case, a null square means an empty square.
Crafting entities from the factory
Now that we have a board to receive items, we will create the game items factory. The factory is a design pattern that allows us to create an object without exposing the creation logic to the caller. This factory can be seen as a helper class that will handle all the dirty tasks required when you want to create a new game item from JavaScript. Do you remember GameEntity.qml? It is the parent class of Apple.qml, Snake.qml, and Wall.qml. The factory will be able to create a specific entity for a given a type and coordinates. We will use the property type to identify an entity kind. Here is the factory pattern schema used in our snake game:
[ 222 ]
Even Qt Deserves a Slice of Raspberry Pi
We can now create the factory.js file, which begins like this: var SNAKE_TYPE = 1; var WALL_TYPE = 2; var APPLE_TYPE = 3; var snakeComponent = Qt.createComponent("Snake.qml"); var wallComponent = Qt.createComponent("Wall.qml"); var appleComponent = Qt.createComponent("Apple.qml");
First of all, we define all the game entity types. In our case we have apple, snake, and wall types. Then, we create game item components from QML files. These components will be use by the factory to dynamically create new game entities. We can now add the constructor and a removeAllEntities() utility function to remove all instantiated entities: function GameFactory() { this.board = null; this.parentEntity = null; this.entities = []; } GameFactory.prototype.removeAllEntities = function() { for(var i = 0; i < this.entities.length; i++) { this.entities[i].setParent(null); }
[ 223 ]
Even Qt Deserves a Slice of Raspberry Pi
This factory has three member variables: A reference to the game board described in the previous section A reference to the parentEntity variable, that is, the game area An entities array that keeps a reference to created items The removeAllEntities() function will remove the items from their parent (that is, the game area) and create a new empty entities array. This ensures that old entities are deleted by the garbage collector. Let's add the core function createGameEnity() in the factory: GameFactory.prototype.createGameEntity = function(type, column, row) { var component; switch(type) { case SNAKE_TYPE: component = snakeComponent; break; case WALL_TYPE: component = wallComponent; break; case APPLE_TYPE: component = appleComponent; break; } var gameEntity = component.createObject(this.parentEntity); gameEntity.setParent(this.parentEntity); this.board.setData(gameEntity, column, row); gameEntity.gridPosition = Qt.vector2d(column, row); gameEntity.position.x = column * this.board.blockSize; gameEntity.position.y = row * this.board.blockSize; this.entities.push(gameEntity); return gameEntity; }
As you can see, the caller provides an entity type and board coordinates (column and row). The first part is a switch to select the correct QML component. Once we have the component, we can call component.createObject() to create an instance of this component. The parent of this new component will be this.parentEntity, in our case, GameArea. Then, we can update the board, update the entity position, and add this new entity in the entities array. [ 224 ]
Even Qt Deserves a Slice of Raspberry Pi
The last thing to do is to update our QML game entities with the proper factory type. Please open Apple.qml and update the file like this: import "factory.js" as Factory GameEntity { id: root type: Factory.APPLE_TYPE ... }
You can now update Snake.qml with the Factory.SNAKE_TYPE type and Wall.qml with the Factory.WALL_TYPE type.
Building a snake engine in JavaScript
It is time to get your hands dirty. Let's see how to create an engine in JavaScript to manage a snake game using our board, our factory, and the power of QML. Please create a new engine.js file with the following snippet: .import "factory.js" as Factory .import "board.js" as Board var COLUMN_COUNT = 50; var ROW_COUNT = 29; var BLOCK_SIZE = 1; var factory = new Factory.GameFactory(); var board = new Board.Board(COLUMN_COUNT, ROW_COUNT, BLOCK_SIZE); var snake = []; var direction;
The first lines are the Qt way to import a JavaScript file from another JavaScript file. Then, we can easily instantiate a factory variable and a 50×29 board variable. The snake array contains all the snake game items instantiated. This array will be useful to move our snake. Finally, the direction variable is a 2d vector handling the current snake direction.
[ 225 ]
Even Qt Deserves a Slice of Raspberry Pi
This is the first function of our engine: function start() { initEngine(); createSnake(); createWalls(); spawnApple(); gameRoot.state = "PLAY"; }
This gives you a summary of what is done when we start the engine: 1. 2. 3. 4. 5.
Initialize the engine. Create the initial snake. Create walls surrounding the game area. Spawn the first apple. Switch the GameArea state to PLAY.
Let's begin with the initEngine() function: function initEngine() { timer.interval = initialTimeInterval; score = 0; factory.board = board; factory.parentEntity = gameRoot; factory.removeAllEntities(); board.init(); direction = Qt.vector2d(-1, 0); }
This function initializes and resets all the variables. The first task is to set the GameArea timer interval to its initial value. Each time the snake eats an apple, this interval is reduced, increasing the game speed and thus the snake's movement speed. Logically, we reset the score of the player to 0. Then we initialize the factory, giving the board and gameRoot references. The gameRoot refers to the GameArea; this entity will be the parent of all items instantiated by the factory. Then, we remove all the existing entities from the factory and call the board's init() function to clear the board. Finally, we set a default direction for the snake. The vector -1,0 means that the snake will begin moving to the left. If you want the snake to start moving up, you can set the vector to 0, 1.
[ 226 ]
Even Qt Deserves a Slice of Raspberry Pi
The next function is creating the snake: function createSnake() { snake = []; var initialPosition = Qt.vector2d(25, 12); for (var i = 0; i < initialSnakeSize; i++) { snake.push(factory.createGameEntity(Factory.SNAKE_TYPE, initialPosition.x + i, initialPosition.y)); } }
No big deal here, we reset and initialize the snake array. The first snake item will be created at 25×12. We then proceed to create as many snake items as we need to spawn a snake with the correct initial size. Please note that other snake items will be created to the right of the first item (26×12, 27×12, and so on). You can see how easy it is to call our factory and request a new snake item instance. Let's add the createWalls() function to engine.js: function createWalls() { for (var x = 0; x < board.columnCount; x++) { factory.createGameEntity(Factory.WALL_TYPE, factory.createGameEntity(Factory.WALL_TYPE, } for (var y = 1; y < board.rowCount - 1; y++) { factory.createGameEntity(Factory.WALL_TYPE, factory.createGameEntity(Factory.WALL_TYPE, y); } }
x, 0); x, board.rowCount - 1);
0, y); board.columnCount - 1,
The first loop creates the top and bottom walls. The second loop creates the left and right walls. The indexes of the second loop are different from the first one to avoid creating the corners twice. Let's see now how to implement the spawnApple() function in engine.js: function spawnApple() { var isFound = false; var position; while (!isFound) { position = Qt.vector2d(Math.floor(Math.random() * board.columnCount), Math.floor(Math.random() * board.rowCount)); if (board.at(position.x, position.y) == null) {
[ 227 ]
Even Qt Deserves a Slice of Raspberry Pi isFound = true; } } factory.createGameEntity(Factory.APPLE_TYPE, position.x, position.y); if (timerInterval > 10) { timerInterval -= 2; } }
The first step is to find an empty square. The while loop will generate a random board position and check whether a square is empty. As soon as an empty square is found, we request the factory to create an apple entity at this position. Finally, we reduce the timerInverval value of GameArea to speed up the game. We will now add some utility functions related to the snake position in engine.js: function setPosition(item, column, row) { board.setData(item, column, row); item.gridPosition = Qt.vector2d(column, row); item.position.x = column * board.blockSize; item.position.y = row * board.blockSize; } function moveSnake(column, row) { var last = snake.pop(); board.setData(null, last.gridPosition.x, last.gridPosition.y); setPosition(last, column, row); snake.unshift(last); }
The setPosition() function handles all the necessary tasks when we want to move a game item. We first assign the game item to the correct board square, then we update the gridPosition property (from GameEntity) but also the OpenGL position.x and position.y. The second function, moveSnake(), moves the snake to an adjacent square. Let's dissect all the steps performed by this function: 1. The snake is our global array containing all the snake items. The pop() method removes and returns the last element that we store in the last variable. 2. The last variable contains the snake's tail's grid position. We set this board square to null; that means an empty square for us. 3. The last variable is now put on the adjacent square requested by the caller. 4. The last variable is finally inserted at the beginning of the snake array. [ 228 ]
Even Qt Deserves a Slice of Raspberry Pi
The next schema illustrates the moveSnake() process when a snake is moving on the left. We also name snake items with a letter to visualize how the tail becomes the head, simulating a moving snake:
Now that we can move our snake, we must handle key events to move the snake in the correct direction. Please add this new function to engine.js: function handleKeyEvent(event) { switch(event.key) { // restart game case Qt.Key_R: start(); break; // direction UP case Qt.Key_I: if (direction != Qt.vector2d(0, -1)) { direction = Qt.vector2d(0, 1); } break; // direction RIGHT case Qt.Key_L: if (direction != Qt.vector2d(-1, 0)) { direction = Qt.vector2d(1, 0); } break; // direction DOWN case Qt.Key_K: if (direction != Qt.vector2d(0, 1)) { direction = Qt.vector2d(0, -1);
[ 229 ]
Even Qt Deserves a Slice of Raspberry Pi } break; // direction LEFT case Qt.Key_J: if (direction != Qt.vector2d(1, 0)) { direction = Qt.vector2d(-1, 0); } break; } }
In this game, we use the I-J-K-L keys to update the snake direction vector. Like the original snake game, you can't reverse your direction. A check is performed to avoid this behavior. Please notice that pressing the R key will call start() and so restart the game. We will see soon how to bind this function with the QML keyboard controller. Here we are, the last (but not least) function, the update() function of engine.js: function update() { if (gameRoot.state == "GAMEOVER") { return; } var headPosition = snake[0].gridPosition; var newPosition = Qt.vector2d(headPosition.x + direction.x, headPosition.y + direction.y); var itemOnNewPosition = board.at(newPosition.x, newPosition.y); ... }
This function will be called at regular intervals by QML. As you can see, if the gameRoot (that is GameArea) state variable equals GAMEOVER, this function does nothing and returns immediately. Then, three important steps are performed: 1. Retrieve the grid position of the snake's head in headPosition. 2. Process where the snake goes using the direction vector in newPosition. 3. Put the item where the snake is going in itemOnNewPosition.
[ 230 ]
Even Qt Deserves a Slice of Raspberry Pi
The second part of the update() function is the following snippet: function update() { ... if(itemOnNewPosition == null) { moveSnake(newPosition.x, newPosition.y); return; } switch(itemOnNewPosition.type) { case Factory.SNAKE_TYPE: gameRoot.state = "GAMEOVER"; break; case Factory.WALL_TYPE: gameRoot.state = "GAMEOVER"; break; case Factory.APPLE_TYPE: itemOnNewPosition.setParent(null); board.setData(null, newPosition.x, newPosition.y); snake.unshift(factory.createGameEntity( Factory.SNAKE_TYPE, newPosition.x, newPosition.y)); spawnApple(); score++; break; } }
If the snake is going to an empty square (itemOnNewPosition is null), it is alright and we only move the snake to newPosition. If the square is not empty, we must apply the correct rule depending on the item type. If the next square is a snake part or a wall, we update the state to GAMEOVER. On the other hand, if the next square is an apple, several steps are performed: 1. 2. 3. 4. 5.
Detach the apple item from GameArea, setting its parent to null. Remove the apple from the board, setting the board square to null. Grow the snake, creating a snake part at the beginning of the snake array. Spawn a new apple in a random empty square. Increment the score.
[ 231 ]
Even Qt Deserves a Slice of Raspberry Pi
Our snake engine is now complete. The last step is to call some engine functions from QML. Please update GameArea.qml: ... import "engine.js" as Engine Entity { ... QQ2.Component.onCompleted: { console.log("Start game..."); Engine.start(); timer.start() } QQ2.Timer { id: timer interval: initialTimeInterval repeat: true onTriggered: Engine.update() } KeyboardInput { id: input controller: keyboardController focus: true onPressed: Engine.handleKeyEvent(event) } ... }
You can already play the game. If you eat an apple, the snake grows and you get one point. When you hit yourself or a wall, the game state switches to GAMEOVER and the game stops. Finally, if you press the R key, the game restarts. The game looks like the next screenshot on to null Raspberry Pi:
[ 232 ]
Even Qt Deserves a Slice of Raspberry Pi
Varying the HUD with QML states
We will now create a “Game Over” HUD, displayed when you lose the game. Create a new file GameOverItem.qml: Item { id: root anchors.fill: parent onVisibleChanged: { scoreLabel.text = "Your score: " + score } Rectangle { anchors.fill: parent color: "black" opacity: 0.75 } Label { id: gameOverLabel anchors.centerIn: parent color: "white" font.pointSize: 50 text: "Game Over" }
[ 233 ]
Even Qt Deserves a Slice of Raspberry Pi Label { id: scoreLabel width: parent.width anchors.top: gameOverLabel.bottom horizontalAlignment: "AlignHCenter" color: "white" font.pointSize: 20 } Label { width: parent.width anchors.bottom: parent.bottom anchors.bottomMargin: 50 horizontalAlignment: "AlignHCenter" color: "white" font.pointSize: 30 text:"Press R to restart the game" } }
Let's examine the items of this Game Over screen: A black rectangle filling the entire screen with an opacity value of 75%. As a consequence, the game area will still be visible at 25% behind the game over screen. A gameOverLabel label displaying the text “Game Over”. This is a traditional video game message but you can edit this label with text such as “Loser!” or “Too bad!”. A dynamic scoreLabel label that will display the final score. A label explaining to the player how he can restart the game. Please notice that, when the visibility of the root item changes, the scoreLabel text is updated with the current score variable from main.qml. Qt Quick provides an interesting feature related to UI states. You can define several states for an item and describe the behaviors for each state. We will now use this feature and our GameOverItem in a new file called OverlayItem.qml: Item { id: root states: [ State { name: "PLAY" PropertyChanges { target: root; visible: false }
[ 234 ]
Even Qt Deserves a Slice of Raspberry Pi }, State { name: "GAMEOVER" PropertyChanges { target: root; visible: true } PropertyChanges { target: gameOver; visible: true } } ] GameOverItem { id: gameOver } }
You can see that the states element is an Item property. By default, the states element contains an empty string state. Here we are defining two State items named PLAY and GAMEOVER. We are using the same naming convention as in engine.js. Afterwards we can bind property values to a state. In our case, when the state is GAMEOVER, we set the visibility to true for this OverlayItem and its GameOverItem. Otherwise, for the state PLAY, we hide it. The overlay HUD and its “Game Over” screen are ready to be used. Please update your mail.qml with the following snippet: Item { id: mainView property int score: 0 readonly property alias window: mainView ... OverlayItem { id: overlayItem anchors.fill: mainView visible: false Connections { target: gameArea onStateChanged: { overlayItem.state = gameArea.state; } } } }
[ 235 ]
Even Qt Deserves a Slice of Raspberry Pi
Our OverlayItem element fits the screen and is not visible by default. Like a C++ Qt Widgets signal/slot connection, you can perform a QML connection. The target property contains the item that will send the signal. Then you can use the QML slot syntax: onChanged
In our case, the target is gameArea. This item contains the state variable, so we can be notified when the state variable is updated using onStateChanged. Then, we switch the state of OverlayItem. This assignation will trigger all ProperyChanged defined in OverlayItem element and display or hide our GameOverItem. You can now lose the game and enjoy your Game Over overlay:
[ 236 ]
Even Qt Deserves a Slice of Raspberry Pi
Profiling your QML application
Qt Creator provides a QML profiler to collect useful data on your application during the runtime. You can use it on a desktop and also on a remote target such as our Raspberry Pi. Let's check that your debug build configuration allows QML debugging and profiling. Click on Projects | Rpi 2 | Build. Then you can click on Details of qmake from Build Steps. You should also check it for your desktop kit:
By default, data is only sent from target to host when you stop profiling. You can flush data periodically: Tools | Options | Analyser | QML Profiler. Keep in mind that flushing data while profiling frees memory on the target device but takes time. Thus, it can affect your profiling result and analysis. While we are using Qt Creator kits, we can start the QML profiler in the same way for desktops or remote devices. Switch to a kit and click on Analyze | QML Profiler to start the QML profiling. If you are profiling an application running on your desktop, Qt Creator starts your software with an argument such as this: -qmljsdebugger=file:/tmp/QtCreator.OU7985
If you're profiling an application on a remote device (such as a Raspberry Pi), Qt Creator uses a TCP socket to retrieve data, adding an argument such as this: -qmljsdebugger=port:10000
For both targets, the QML profiler will afterwards try to connect to your application. Another way to start the QML profiler on a remote device is to start the application yourself with the -qmljsdebugger argument, for example: ./ch06-snake -qmljsdebugger=port:3768
[ 237 ]
Even Qt Deserves a Slice of Raspberry Pi
Then, you can click on Analyze | QML Profiler (External). Select your remote kit (such as Rpi 2), set the port to 3768, and click on OK. Great, the QML profiler is started, a new toolbar appears. You can play the game for a few seconds and click on the Stop button from the QML Profiler toolbar. Then the QML profiler processes data and displays something like this:
Let's begin analyzing the top buttons from left to right: 1. 2. 3. 4. 5. 6. 7. 8.
Start QML profiler. Stop the application and the QML profiler. Enable/disable profiling. You can also select an event to capture. Discard data to clean your profiling session. Search timeline event notes. Hide or show event categories. Elapsed indicates the session duration. Views hides or shows the Timeline, Statistics, and Flamegraph tabs. [ 238 ]
Even Qt Deserves a Slice of Raspberry Pi
To learn to use the QML profiler, we will take a real case. Restarting the game is a little slow on the Raspberry Pi. Let's find with the QML profiler what requires several seconds to restart the game! Please follow this operational mode to gather data from the QML profiler: 1. 2. 3. 4. 5. 6.
Select the Raspberry Pi kit. Start the QML profiler. Wait for the snake to hit a wall. Press the R key to restart the game. Wait for the game to restart and the snake to move again. Stop the QML profiler.
Let's begin our investigation using the timeline tab. This view displays a chronological view of events, grouped by event type. The JavaScript row dissects your code and displays useful information. You can click on an item to get some details. Identify in the timeline when you restart the game. The JavaScript row can be read as a call stack, from top to bottom:
[ 239 ]
Even Qt Deserves a Slice of Raspberry Pi
In our case, we restarted the game around 3.5 seconds after the application started. Here is the stack with durations provided by the QML profiler. Here is the stack with durations provided by the QML profiler. Let's track all functions called when we restart the game pressing the R key: The onPressed() function from GameArea.qml The handleKetEvent() function from engine.js The start() function from engine.js at 4.2 seconds initEngine() at 80 ms createSnake() at 120 ms createWalls() at 4.025 seconds! Here we are, createWalls() takes ~4 seconds on the Raspberry Pi when we restart the game. Let's switch to the Statistics view:
[ 240 ]
Even Qt Deserves a Slice of Raspberry Pi
The Statistics view displays numbers concerning the call count of an event. An event can be a QML binding, creation, signal triggered, or a JavaScript function. The bottom part shows QML callers and callees. A caller is the source of a change in a binding. For example, the JS function createWalls() is a caller. A callee is the affected item that a binding triggers. For example, the QML item Wall.qml is a callee. Once again, createWalls() requesting many factory item creation seems responsible for the slow restart of the game on Raspberry Pi. Take a look at the last view of the QML profiler, the Flamegraph:
The Flamegraph view is a compact summary of your QML and JavaScript code while running the game. You can see the call count and the amount of time relative to the total duration. Like the Timeline view, you can see the call stack but from bottom to top!
[ 241 ]
Even Qt Deserves a Slice of Raspberry Pi
Again, the profiler indicates createWalls() is a heavy function. On a profiling session of 10 seconds with one game restart, 77% of the time is spent in engine.createWalls(). You will now be able to profile a QML application. You can try to edit the code to speed up the restart. Here are some hints: Create the walls only once at application startup; do not delete and recreate them on each restart. Implement a common design pattern in video games: an object pool of preloaded items. Request a wall when needed, and return the wall to the pool when you do not use it.
Summary
In this chapter, we discovered how to use the Qt3D module. You also learned how to configure Qt Creator to create a new kit for an embedded Linux device. Your Raspberry Pi can now run your Qt applications. We created a snake game using QML views and an engine in JavaScript. We also covered the Factory design pattern to easily create new game items from the engine. Finally, you are now able to investigate the bad behavior of QML software using the powerful QML profiler. Even if Qt is a powerful framework, sometimes you need to use a third-party library. In the next chapter, we will see how to integrate the OpenCV library into your Qt application.
[ 242 ]
7
Third-Party Libraries Without a Headache In previous chapters, we have used our own libraries or the ones provided by Qt. In this chapter, we will learn how to integrate the third-party library OpenCV with a Qt project. This library will give you an impressive image processing toolbox. For each platform, you will learn to use a different specific compiler link configuration. Qt Designer is a powerful WYSIWYG editor. This is why this chapter will also teach you to build a Qt Designer plugin that can be dragged and dropped from the Widget Box to the Form Editor, and then configured directly from Qt Creator. In the example project, the user can load a picture, select a filter from thumbnail previews, and save the result. This application will rely on OpenCV functions for image processing. This chapter will cover the following topics: Prepare a cross-platform project to host a third-party library Link with a third party library Build a custom QWidget class using Qt Designer plugins How the OpenCV API can work with Qt Create a Qt application that relies on a custom QWidget class
Third-Party Libraries Without a Headache
Creating your Qt Designer plugin
In Chapter 4, Conquering the Desktop UI, we created a custom Qt widget in Qt Designer using the promoting technique. It is now time to learn how to create a custom Qt widget by building a plugin for Qt Designer. Your widget will be available from the Design mode in the Widget Box alongside other regular Qt widgets. For this project example, we will create a FilterWidget class that processes an input image to apply a filter. The widget will also display the filter name and a dynamic thumbnail of the filtered picture. This project is composed of two sub-projects: filter-plugin-designer: This is a Qt Designer plugin containing FilterWidget class and the image processing code. This plugin is a
dynamic library that will be used by the Qt Creator to offer our new FilterWidget in the Form Editor. image-filter: This is a Qt Widget application using multiple FilterWidget. The user can open an image from their hard disk, select a filter (grayscale, blur, and so on), and save the filtered image. Our filter-plugin-designer will use the third-party library OpenCV (Open Source Computer Vision). It is a powerful, cross-platform open source library to manipulate images. Here is an overview schema:
[ 244 ]
Third-Party Libraries Without a Headache
You can see a plugin as a kind of module, which can be easily added to an existing software. A plugin must respect a specific interface to be automatically called by the application. In our case, the Qt Designer is the application that loads Qt plugins. So creating a plugin allows us to enhance the application without the need to modify the Qt Designer source code and recompile it. A plugin is a generally dynamic library (.dll/.so), so it will be loaded at runtime by the application. Now that you have a clear mind about the Qt Designer plugins, let's build one! First, create a Subdirs project called ch07-image-filter. Then, you can add a subproject, filterplugin-designer. You can use the Empty qmake Project template because we start this project from scratch. Here is the filter-plugin-designer.pro file: QT += widgets uiplugin CONFIG += plugin CONFIG += c++14 TEMPLATE = lib DEFINES += FILTERPLUGINDESIGNER_LIBRARY TARGET = $$qtLibraryTarget($$TARGET) INSTALLS += target
Please note the uiplugin and plugin keywords for QT and CONFIG. They are required to create a Qt Designer plugin. We set the TEMPLATE keyword to lib because we are creating a dynamic library. The define, FILTERPLUGINDESIGNER_LIBRARY, will be used by the import/export mechanism of the library. We already covered this topic in Chapter 3, Dividing Your Project and Ruling Your Code. By default, our TARGET is filter-plugindesigner; the $$qtLibraryTarget() function will update it according to your platform. For example, the suffix “d” (standing for debug) will be appended on Windows. Finally, we append target to INSTALLS. Right now, this line does nothing, but we will describe a destination path for each platform soon; this way, executing the make install command will copy our target library file (.dll/.so) into the correct folder. To automatically perform this task on each compilation, you can add a new build step. The deploy path is configured, but it will not be done automatically. Open the Projects tab and do the following: 1. Open the Build Settings | Build Steps. 2. Click on Add Build Step | Make. 3. In the Make arguments field, type install.
[ 245 ]
Third-Party Libraries Without a Headache
You should get something like this:
Each time you build the project, the make install command will be called and it will deploy the library in Qt Creator.
Configuring the project for Windows
Before preparing this project on Windows, let's talk about the available choices when you develop a Qt application on a Windows host. The official Qt website provides multiple binary packages. We are mainly interested in the following: Qt for Windows 32-bit (MinGW) Qt for Windows 32-bit (VS 2013) You may already be using one of these versions. The first one comes with a MinGW GCC compiler and the Qt framework. The second only provides the Qt framework and relies on the Microsoft Visual C++ compiler that will be installed with Visual Studio. Both versions are fine when you want to create a common Qt application for Windows. However, for this chapter, we want to link our filter-plugin-designer project with OpenCV libraries. Qt Designer must also be able to dynamically load filter-plugindesigner, so we must use a consistent compiler version at all stages. Please note that Qt Creator on Windows is always based on MSVC, even in the MinGW binary package! So if you create a Qt Designer plugin using a MinGW compiler, your Qt Creator will not be able to load it. OpenCV for Windows provides only MSVC libraries, compile for MSVC11 (which is VS 2012), and MSVC12 (VS 2013).
[ 246 ]
Third-Party Libraries Without a Headache
Here is a summary of the different solutions for building our project example in Windows:
Keep in mind that for open-source software such as Qt Creator and OpenCV you can always try to compile them from a source with a different compiler. So, if you absolutely want to use a MinGW compiler, you must recompile OpenCV and Qt Creator from sources. Otherwise, we suggest that you use Qt for Windows 32-bit (VS 2013) as explained shortly. Here are the steps to prepare your development environment: 1. 2. 3. 4.
Download and install Visual Studio Community Edition. Download and install Qt for Windows 32-bit (VS 2013). Download and extract OpenCV for Windows (for example: C:\lib\opencv). Create a new OPENCV_HOME: C:\lib\opencv\build\x86\vc12 environment variable. 5. Append to your system Path: C:\lib\opencv\build\x86\vc12\bin environment variable.
The OPENCV_HOME directory will be used in our .pro file. We also add an OpenCV libraries folder to the Path directory to easily resolve the dependencies at runtime. You can now add the following snippet to the filter-plugin-designer.pro file: windows { target.path = $$(QTDIR)/../../Tools/QtCreator/bin/plugins/designer debug:target_lib.files = $$OUT_PWD/debug/$${TARGET}.lib release:target_lib.files = $$OUT_PWD/release/$${TARGET}.lib target_lib.path = $$(QTDIR)/../../Tools/QtCreator/bin/plugins/designer INSTALLS += target_lib INCLUDEPATH += $$(OPENCV_HOME)/../../include LIBS += -L$$(OPENCV_HOME)/lib -lopencv_core2413
[ 247 ]
Third-Party Libraries Without a Headache -lopencv_imgproc2413 }
The target path is set to the Qt Creator plugin folder. We also create a target_lib library to copy the .lib file generated by MSVC when we make a dynamic library (.dll). We add the OpenCV headers folder to the INCLUDEPATH to easily include them in our code. Finally, we update LIBS variable to link our plugin with the OpenCV modules (core and imgproc) from the OpenCV lib folder. Please note that the standalone Qt Designer application and the Qt Creator are different software. Both programs use a different plugin path. In our case, we only used the form editor from the Qt Creator, so we are targeting the Qt Creator plugin path. Just as we appended target and target_lib to INSTALLS, both .dll and .lib files will be copied in the Qt Creator plugin path on a make install command. Qt Creator only requires the .dll file to load the plugin at runtime. The .lib file is only used to resolve the links with filter-plugin-designer when building our image-filter application. For simplicity, we are using the same directory.
Configuring the project for Linux
OpenCV binaries are certainly available in official software repositories. Depending on your distribution and your package manager, you can install it with commands such as the following: apt-get install libopencv yum install opencv
When OpenCV is installed on your Linux, you can add this snippet to the filter-plugindesigner.pro file: linux { target.path = $$(QTDIR)/../../Tools/QtCreator/lib/Qt/plugins/designer/ CONFIG += link_pkgconfig PKGCONFIG += opencv }
[ 248 ]
Third-Party Libraries Without a Headache
This time we do not use the LIBS variable but PKGCONFIG, which relies on pkg-config. It is a helper tool that will insert the correct options into the compile command line. In our case, we will request pkg-config to link our project with OpenCV. You can list all the libs managed by pkg-config with the pkg-config -list-all command.
Configuring the project for Mac
The first step in making the project work on Mac OS is to install OpenCV. Fortunately, this is very easy using the brew command. If you develop on Mac OS and do not use it already, you should download it right now. In a nutshell, brew is an alternate package manager that gives you access to many packages (for developers and non-developers) that are not available on the Mac App Store. You can download and install brew from http://brew.sh/.
In a terminal, simply type the following command: brew install opencv
This will download, compile, and install OpenCV on your machine. At the time of writing, the latest OpenCV version available on brew was version 2.4.13. Once this is done, open filter-plugin-designer.pro and add the following block: macx { target.path = "$$(QTDIR)/../../QtCreator.app/Contents/PlugIns/designer/" target_lib.files = $$OUT_PWD/lib$${TARGET}.dylib target_lib.path = "$$(QTDIR)/../../QtCreator.app/Contents/PlugIns/designer/" INSTALLS += target_lib INCLUDEPATH += /usr/local/Cellar/opencv/2.4.13/include/ LIBS += -L/usr/local/lib \ -lopencv_core \ -lopencv_imgproc }
[ 249 ]
Third-Party Libraries Without a Headache
We add OpenCV headers and link the path with INCLUDEPATH and LIBS variables. The target definition and INSTALLS are used to automatically deploy the output shared object to the Qt Creator application plugins directory. The last thing we have to do is to add an environment variable to let the Qt Creator know where it will find the library that will link it to the final application. In the Projects tab, go through the following steps: 1. 2. 3. 4.
Open the Details window in Build Environment. Click on the Add Button. Type DYLD_LIBRARY_PATH in the field. Type the path of the build directory in (you can copy and paste it from the section General | Build directory).
Implementing your OpenCV filters
Now that your development environment is ready, we can begin the fun part! We will implement three filters using OpenCV: FilterOriginal: This filter does nothing and returns the same picture (lazy!) FilterGrayscale: This filter converts a picture from color to grayscale FilterBlur: This filter smoothes the picture
The parent class of all these filters is Filter. Here is this abstract class: //Filter.h class Filter { public: Filter(); virtual ~Filter(); virtual QImage process(const QImage& image) = 0; }; //Filter.cpp Filter::Filter() {} Filter::~Filter() {}
[ 250 ]
Third-Party Libraries Without a Headache
As you can see, process() is a pure abstract method. All filters will implement a specific behavior with this function. Let's begin with the simple FilterOriginal class. Here is FilterOriginal.h: class FilterOriginal : public Filter { public: FilterOriginal(); ~FilterOriginal(); QImage process(const QImage& image) override; };
This class inherits Filter and we override the process() function. The implementation is also really simple. Fill FilterOriginal.cpp with the following: FilterOriginal::FilterOriginal() : Filter() { } FilterOriginal::~FilterOriginal() { } QImage FilterOriginal::process(const QImage& image) { return image; }
No modification is performed; we return the same picture. Now that the filter structure is clear, we can create FilterGrayscale. The .h/.cpp files are close to FilterOriginalFilter, so let's jump to the process() function of FilterGrayscale.cpp: QImage FilterGrayscale::process(const QImage& image) { // QImage => cv::mat cv::Mat tmp(image.height(), image.width(), CV_8UC4, (uchar*)image.bits(), image.bytesPerLine()); cv::Mat resultMat; cv::cvtColor(tmp, resultMat, CV_BGR2GRAY); // cv::mat => QImage
[ 251 ]
Third-Party Libraries Without a Headache QImage resultImage((const uchar *) resultMat.data, resultMat.cols, resultMat.rows, resultMat.step, QImage::Format_Grayscale8); return resultImage.copy(); }
In the Qt framework, we use the QImage class to manipulate pictures. In the OpenCV world, we use the Mat class, so the first step is to create a correct Mat object from the QImage source. OpenCV and Qt both handle many image formats. An image format describes the data bytes organization with information such as the following: Channel count: A grayscale picture only needs one channel (white intensity),
while a color picture requires three channels (red, green, and blue). You will even need four channels to handle the opacity (alpha) pixel information. Bit depth: The number of bits used to store a pixel color. Channel order: The most common orders are RGB and BGR. Alpha can be placed before or after the color information.
For example, the OpenCV image format, CV_8UC4, means four channels of unsigned 8-bit, which is the perfect fit for an alpha color picture. In our case, we are using a compatible Qt and OpenCV image format to convert our QImage in Mat. Here is a little summary:
Please note that some QImage class formats also depend on your platform endianness. The preceding table is for a little endian system. For OpenCV, the order is always the same: BGRA. It is not required in our project example, but you can swap blue and red channels as follows: // with OpenCV cv::cvtColor(mat, mat, CV_BGR2RGB); // with Qt
[ 252 ]
Third-Party Libraries Without a Headache QImage swapped = image.rgbSwapped();
OpenCV Mat and Qt QImage classes perform shallow construction/copy by default. This means that only metadata is really copied; the pixel data is shared. To create a deep copy of a picture, you must call the copy() function: // with OpenCV mat.clone(); // with Qt image.copy();
We created a Mat class called tmp from the QImage class. Note that tmp is not a deep copy of image; they share the same data pointer. Then, we can call the OpenCV function to convert the picture from color to grayscale using cv::cvtColor(). Finally, we create a QImage class from the grayscale resultMat element. In that case too, resultMat and resultImage share the same data pointer. Once we're done, we return a deep copy of resultImage. It is now time to implement the last filter. Here is the process() function of FilterBlur.cpp: QImage FilterBlur::process(const QImage& image) { // QImage => cv::mat cv::Mat tmp(image.height(), image.width(), CV_8UC4, (uchar*)image.bits(), image.bytesPerLine()); int blur = 17; cv::Mat resultMat; cv::GaussianBlur(tmp, resultMat, cv::Size(blur, blur), 0.0, 0.0); // cv::mat => QImage QImage resultImage((const uchar *) resultMat.data, resultMat.cols, resultMat.rows, resultMat.step, QImage::Format_RGB32); return resultImage.copy(); }
[ 253 ]
Third-Party Libraries Without a Headache
The conversion from QImage to Mat is the same. The processing differs because we use the cv::GaussianBlur() OpenCV function to smooth the picture. The blur is the kernel size used by the Gaussian blur. You can increase this value to get a softer picture, but only use an odd and positive number. Finally, we convert the Mat to QImage and return a deep copy to the caller.
Designing the UI with FilterWidget
Fine. Our filter classes are implemented, and we can now create our custom widget. This widget will take in input, a source, and a thumbnail picture. Then the thumbnail is immediately processed to display a preview of the filter. If the user clicks on the widget, it will process the source picture and trigger a signal with the filtered picture. Keep in mind that this widget will later be dragged and dropped in the Form Editor of Qt Creator. That's why we will provide properties with getters and setters to select a filter from Qt Creator. Please create a new widget called FilterWidget using the Qt Designer Form Class template. The FilterWidget.ui is really simple:
[ 254 ]
Third-Party Libraries Without a Headache
The titleLabel is a QLabel on top of the QWidget. Below, thumbnailLabel will display the filtered picture thumbnail. Let's switch to FilterWidget.h: class FILTERPLUGINDESIGNERSHARED_EXPORT FilterWidget : public QWidget { Q_OBJECT Q_ENUMS(FilterType) Q_PROPERTY(QString title READ title WRITE setTitle) Q_PROPERTY(FilterType filterType READ filterType WRITE setFilterType) public: enum FilterType { Original, Blur, Grayscale }; explicit FilterWidget(QWidget *parent = 0); ~FilterWidget(); void process(); void setSourcePicture(const QImage& sourcePicture); void updateThumbnail(const QImage& sourceThumbnail); QString title() const; FilterType filterType() const; public slots: void setTitle(const QString& tile); void setFilterType(FilterType filterType); signals: void pictureProcessed(const QImage& picture); protected: void mousePressEvent(QMouseEvent*) override; private: Ui::FilterWidget *ui; std::unique_ptr mFilter; FilterType mFilterType; QImage mDefaultSourcePicture; QImage mSourcePicture; QImage mSourceThumbnail; QImage mFilteredPicture; QImage mFilteredThumbnail; };
[ 255 ]
Third-Party Libraries Without a Headache
The top part defines all the available filter types with the enumFilterType. We also use the Qtproperty system to expose the widget title and the current filter type to the Property Editor of Qt Creator. The syntax is like this: Q_PROPERTY( READ WRITE )
Please note that exposing an enumeration requires it to be registered using the Q_ENUM() macro, so the Property Editor will display a combo box that allows you to choose the filter type from Qt Creator. The middle part lists all functions, slots, and signals. The most notable is the process() function that will use the current filter to modify the source picture. The pictureProcessed() signal will notify the application with the filtered picture. The bottom part lists the picture and thumbnail QImage variables used in this class. In both cases, we handle both source and filtered pictures. The default source picture is an embedded picture in the plugin. This allows you to display a default preview when no thumbnail has been provided. The mFilter variable is a smart pointer to the current Filter class. Let's switch to the implementation with FilterWidget.cpp: FilterWidget::FilterWidget(QWidget *parent) : QWidget(parent), ui(new Ui::FilterWidget), mFilterType(Original), mDefaultSourcePicture(":/lenna.jpg"), mSourcePicture(), mSourceThumbnail(mDefaultSourcePicture.scaled(QSize(256, 256), Qt::KeepAspectRatio, Qt::SmoothTransformation)), mFilteredPicture(), mFilteredThumbnail() { ui->setupUi(this); setFilterType(Original); } FilterWidget::~FilterWidget() { delete ui; }
[ 256 ]
Third-Party Libraries Without a Headache
Here are the constructor and the destructor. Please note that the default source picture loads an embedded picture of the gorgeous Lenna often used in image processing literature. The picture is in the resource file, filter-plugin-designer.qrc. The mSourceThumbnail function is initialized with a scaled picture of Lenna. The constructor calls the setFilterType()function to initialize an Original filter by default. Here is the core process() function: voidFilterWidget::process() { mFilteredPicture = mFilter->process(mSourcePicture); emitpictureProcessed(mFilteredPicture); }
The process() function is powerful, but really simple. We call process() of the current filter to update our filtered picture from the current source picture. Then we trigger the pictureProcessed() signal with the filtered picture. We can now add our QImage setters: voidFilterWidget::setSourcePicture(constQImage&sourcePicture) { mSourcePicture = sourcePicture; } voidFilterWidget::updateThumbnail(constQImage&sourceThumbnail) { mSourceThumbnail = sourceThumbnail; mFilteredThumbnail = mFilter->process(mSourceThumbnail); QPixmappixmap = QPixmap::fromImage(mFilteredThumbnail); ui->thumbnailLabel->setPixmap(pixmap); }
The setSourcePicture() function is a simple setter called by the application with a new source picture. The updateThumbnail() method will filter the new source thumbnail and display it. Let's add the setters used by Q_PROPERTY: voidFilterWidget::setTitle(constQString& tile) { ui->titleLabel->setText(tile); } voidFilterWidget::setFilterType(FilterWidget::FilterTypefilterType) { if (filterType == mFilterType&&mFilter) { return; } mFilterType = filterType;
[ 257 ]
Third-Party Libraries Without a Headache switch (filterType) { case Original: mFilter = make_unique(); break; case Blur: mFilter = make_unique(); break; case Grayscale: mFilter = make_unique(); break; default: break; } updateThumbnail(mSourceThumbnail); }
The setTitle()function is a simple setter used to customize the widget title. Let's talk about the setFilterType() function. As you can see, this function does not just update the current filter type, mFilterType. Depending on the type, the corresponding filter will be created. Do you remember the smart pointer from Chapter 3, Dividing Your Project and Ruling Your Code? Here we are using a unique_ptr pointer for the mFilter variable, so we can use make_unique instead of a new raw. The FilterWidget class takes the ownership of the Filter class, and we do not need to worry about the memory management. Upon the make_unique instruction, the old owned pointer (if there is any) will be automatically deleted. Finally, we call the updateThumbnail() function to display a filtered thumbnail corresponding to the selected filter type. Here are the getters and the mouse event handler: QStringFilterWidget::title() const { returnui->titleLabel->text(); } FilterWidget::FilterTypeFilterWidget::filterType() const { returnmFilterType; } voidFilterWidget::mousePressEvent(QMouseEvent*) { process();
[ 258 ]
Third-Party Libraries Without a Headache }
The title() and filterType() functions are getters used by the Qt Property System. We override the mousePressEvent() function to call our process() function each time the user clicks on the widget.
Exposing your plugin to Qt Designer
The FilterWidget class is completed and ready to be used. We now have to register FilterWidget with the Qt Designer plugin system. This glue code is made using a child class of QDesignerCustomWidgetInterface. Create a new C++ class named FilterPluginDesigner and update FilterPluginDesigner.h like so: #include class FilterPluginDesigner : public QObject, public QDesignerCustomWidgetInterface { Q_OBJECT Q_PLUGIN_METADATA(IID "org.masteringqt.imagefilter.FilterWidgetPluginInterface") Q_INTERFACES(QDesignerCustomWidgetInterface) public: FilterPluginDesigner(QObject* parent = 0); };
The FilterPlugin class inherits from two classes: The QObject class, to rely on the Qt parenting system The QDesignerCustomWidgetInterface class to properly expose the FilterWidget information to the plugin system The QDesignerCustomWidgetInterface class brings two new macros: The Q_PLUGIN_METADATA() macro annotates the class to indicate a unique name for our filter to the meta-object system The Q_INTERFACES() macro tells the meta-object system which interface the current class has implemented
[ 259 ]
Third-Party Libraries Without a Headache
Qt Designer is now able to detect our plugin. We now have to provide information about the plugin itself. Update FilterPluginDesigner.h: class FilterPluginDesigner : public QObject, public QDesignerCustomWidgetInterface { ... FilterPluginDesigner(QObject* parent = 0); QStringname() const override; QStringgroup() const override; QStringtoolTip() const override; QStringwhatsThis() const override; QStringincludeFile() const override; QIconicon() const override; boolisContainer() const override; QWidget* createWidget(QWidget* parent) override; boolisInitialized() const override; void initialize(QDesignerFormEditorInterface* core) override; private: boolmInitialized; };
This is much less overwhelming than it looks. The body of each one of these functions usually takes a single line. Here is the implementation of the most straightforward functions: QStringFilterPluginDesigner::name() const { return "FilterWidget"; } QStringFilterPluginDesigner::group() const { return "Mastering Qt5"; } QStringFilterPluginDesigner::toolTip() const { return "A filtered picture"; } QStringFilterPluginDesigner::whatsThis() const { return "The filter widget applies an image effect"; }
[ 260 ]
Third-Party Libraries Without a Headache QIconFilterPluginDesigner::icon() const { returnQIcon(":/icon.jpg"); } boolFilterPluginDesigner::isContainer() const { return false; }
As you can see, there isn't much to say about these functions. Most of them will simply return a QString value that will be displayed on the appropriate spot in the Qt Designer UI. We will focus only on the most interesting ones. Let's start with includeFile(): QStringFilterPluginDesigner::includeFile() const { return "FilterWidget.h"; }
This function will be called by uic (User Interface Compiler) to generate the header corresponding to a .ui file. Continuing with createWidget(): QWidget* FilterPluginDesigner::createWidget(QWidget* parent) { return new FilterWidget(parent); }
This function makes the bridge between Qt Designer and FilterWidget. When you add the FilterWidget class in a .ui file, Qt Designer will call the createWidget() function to have an instance of the FilterWidget class and display its content. It also provides the parent element to which FilterWidget will be attached. Let's finish with initialize(): voidFilterPluginDesigner::initialize(QDesignerFormEditorInterface*) { if (mInitialized) return; mInitialized = true; }
[ 261 ]
Third-Party Libraries Without a Headache
Nothing much is done in this function. However, the QDesignerFormEditorInterface* parameter is worth some explanation. This pointer, provided by Qt Designer, gives access to a few of Qt Designer's components via functions: actionEditor(): This function is the action editor (bottom panel of the designer) formWindowManager(): This function is the interface that enables you to create a new form window objectInspector(): This function is the hierarchical representation of your layout (top right panel of the designer) propertyEditor(): This function is the list of all the editable properties of the currently selected widget (bottom right panel of the designer) topLevel(): This function is the top-level widget of the designer
We covered each of these panels in Chapter 1, Get Your Qt Feet Wet. If your widget plugin needs to intervene in any of these areas, this function is the entry point to customize the behavior of Qt Designer.
Using your Qt Designer plugin
Our custom plugin is now finished. Because we added a custom Build command to automatically deploy the filter-widget library, it should be visible in Qt Designer. The deploy path we specified is inside the Qt Creator directory. Qt Creator integrates Qt Designer via a plugin that displays the UI inside Qt Creator. When Qt Creator starts, it will try to load every library available in its specific paths. This means that you have to restart Qt Creator each time you modify the plugin (if you want to see the result of your modifications in the designer). To see the plugin in action, we now have to create the application project of the chapter. Create a Qt Widgets Application sub-project in the ch07-image-filter project named image-filter. In the wizard, let it generate the form, MainWindow.ui.
[ 262 ]
Third-Party Libraries Without a Headache
To properly use the plugin, just link the filter-plugin-designer library in image-
filter.pro like so: QT
+= core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = image-filter TEMPLATE = app INCLUDEPATH += ../filter-plugin-designer win32 { LIBS += -L$$(QTDIR)/../../Tools/QtCreator/bin/plugins/designer lfilter-plugin-designer } macx { LIBS += -L$$(QTDIR)/../../"QtCreator.app"/Contents/PlugIns/designer/ lfilter-plugin-designer } linux { LIBS += -L$$(QTDIR)/../../Tools/QtCreator/lib/Qt/plugins/designer/ lfilter-plugin-designer } SOURCES += main.cpp\ MainWindow.cpp HEADERS
+= MainWindow.h
FORMS
+= MainWindow.ui
To have access to the headers of filter-plugin-designer, we simply add it to the INCLUDEPATH directory. Finally, the linker is instructed to link to the library we deployed in Qt Creator. This ensures that the same library is used by Qt Designer and by our application.
[ 263 ]
Third-Party Libraries Without a Headache
Open the MainWindow.ui file and scroll to the bottom of the Widget box. Lo and behold, you should see this:
The FilterWidget plugin appears under the Mastering Qt5 section. It even displays the famous Lenna as a preview icon. If you do not see the FilterWidget plugin, then restart Qt Creator and make sure that the plugin is properly loaded. To check this (in the Design tab), go to Tools | Form Editor | About Qt Designer Plugins. This is what it should display:
If the FilterWidget plugin does not appear in this list, you should check the Qt Creator plugin directory content (the path is stated in image-filter.pro).
[ 264 ]
Third-Party Libraries Without a Headache
Building the image-filter application
We can proceed to build the UI of the application. The idea is to open a picture from the filesystem and apply to it the various filters we developed in the filter-designerplugin project. If you want to keep the result, you can save the resulting image. We will start by designing the UI. Modify MainWindow.ui to look like this:
[ 265 ]
Third-Party Libraries Without a Headache
Here is the Object Inspector content to help you build this layout:
There are three elements of this UI: The menuFile element, which contains three possible actions: actionOpenPicture, actionExit, and actionSaveAs. You can see the details of these actions in the Action Editor window. The pictureLabel element, which will display the loaded picture in the empty top part. The filtersLayout element, which contains the three instances of our FilterWidget class in the bottom part. As you add a FilterWidget class in filtersLayout, you can see that you can customize the title and the filterType in the Property Editor window. The preview will be automatically updated with the selected filter applied to our default picture. A dynamic preview like this is simply awesome, and you can foresee that your custom Qt Designer widgets can become quite powerful. Let's implement the logic of our application. Update MainWindow.h like so: #include #include #include namespaceUi { classMainWindow; } classFilterWidget; classMainWindow : public QMainWindow {
[ 266 ]
Third-Party Libraries Without a Headache Q_OBJECT public: explicitMainWindow(QWidget *parent = 0); ~MainWindow(); voidloadPicture(); private slots: voiddisplayPicture(constQImage& picture); private: voidinitFilters(); voidupdatePicturePixmap(); private: Ui::MainWindow *ui; QImagemSourcePicture; QImagemSourceThumbnail; QPixmapmCurrentPixmap; FilterWidget* mCurrentFilter; QVectormFilters; };
Here are some elements that we have to explain: mSourcePicture: This element is the loaded picture. mSourceThumbnail: This element is the generated thumbnail from mSourcePicture. To avoid wasting CPU cycles, mSourcePicture will be resized only once, and each of the FilterWidget instances will process this
thumbnail rather than the full-resolution picture. mCurrentPixmap: This element is the currently displayed QPixmap in the pictureLabel widget. mCurrentFilter: This element is the currently applied filter. Each time the user clicks on a different FilterWidget, this pointer will be updated. mFilters: This element is a QVector of the FilterWidget class that we added to MainWindow.ui. It is only a helper, introduced to easily apply the same instructions to each FilterWidget class.
[ 267 ]
Third-Party Libraries Without a Headache
Now for the functions, we will limit ourselves to a broad overview. The details will be covered when we look at the implementation of each function: loadPicture(): This function triggers the whole pipeline. It will be called when the user clicks on actionOpenPicture. initFilters(): This function is in charge of initializing mFilters. displayPicture(): This function is the slot called by mCurrentWidget::pictureProcessed() to display the filtered picture. updatePicturePixmap(): This function handles the display of mCurrentPixmap inside pictureLabel.
Let's look at the MainWindow class's constructor implementation in MainWindow.cpp: #include #include #include #include "FilterWidget.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), mSourcePicture(), mSourceThumbnail(), mCurrentPixmap(), mCurrentFilter(nullptr), mFilters() { ui->setupUi(this); ui->pictureLabel->setMinimumSize(1, 1); connect(ui->actionOpenPicture, &QAction::triggered, this, &MainWindow::loadPicture); connect(ui->actionExit, &QAction::triggered, this, &QMainWindow::close); initFilters(); }
We connect the actionOpenPicture::triggered() signal to our yet-to-beimplemented loadPicture() function. The actionExit is straightforward; it is simply connected to the QMainWindow::close() slot. Finally, initFilter() is called. Let's see its body: voidMainWindow::initFilters() {
[ 268 ]
Third-Party Libraries Without a Headache mFilters.push_back(ui->filterWidgetOriginal); mFilters.push_back(ui->filterWidgetBlur); mFilters.push_back(ui->filterWidgetGrayscale); for (inti = 0; iupdateThumbnail(mSourceThumbnail); } mCurrentFilter->process(); }
The mSourcePicture method is loaded using a QFileDialog, and mSourceThumbnail is generated from this input. Every FilterWidget class is updated with this new data and the mCurrentFilter element is triggered by calling its process() function.
[ 269 ]
Third-Party Libraries Without a Headache
When FilterWidget::process() is finished, it emits the pictureProcessed() signal, which is connected to our displayPicture() slot. Let's switch to this function: voidMainWindow::displayPicture(constQImage& picture) { mCurrentPixmap = QPixmap::fromImage(picture); updatePicturePixmap(); }
Nothing very fancy here: mCurrentPixmap is updated from the given picture and the updatePicturePixmap() function is in charge of updating the pictureLabel element. Here is the implementation of updatePicturePixmap(): voidMainWindow::updatePicturePixmap() { if (mCurrentPixmap.isNull()) { return; } ui->pictureLabel->setPixmap( mCurrentPixmap.scaled(ui->pictureLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); }
This function simply creates a scaled version of mCurrentPixmap that fits inside pictureLabel. The whole picture loading/filter processing is completed. If you run the application you should be able to load and modify your pictures. However, if you resize the window, you will see that the pictureLabel element does not scale very well. To address this issue, we have to regenerate the scaled version of mCurrentPixmap each time the window is resized. Update MainWindow like so: // In MainWindow.h classMainWindow : public QMainWindow { ... voidloadPicture(); protected: voidresizeEvent(QResizeEvent* event) override; ... }; // In MainWindow.cpp voidMainWindow::resizeEvent(QResizeEvent* /*event*/)
[ 270 ]
Third-Party Libraries Without a Headache { updatePicturePixmap(); }
Here, the separation of mCurrentPixmap and the pictureLabel element's pixmap makes sense. Because we always generate the scaled version from the fullresolution mCurrentPixmap, we are sure that the resulting pixmap will look good. The image-filter application would not be complete without the ability to save your filtered picture. This will not take much effort. Here is the updated version of MainWindow.h: classMainWindow : public QMainWindow { ... private slots: voiddisplayPicture(constQImage& picture); voidsaveAsPicture(); ... private: Ui::MainWindow *ui; QImagemSourcePicture; QImagemSourceThumbnail; QImage&mFilteredPicture; ... };
Here, we simply added a saveAsPicture() function that will take the mFilteredPicture element and save it to a file. The implementation in MainWindow.cpp should not blow your mind: // In MainWindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), mSourcePicture(), mSourceThumbnail(), mFilteredPicture(mSourcePicture), ... { ui->setupUi(this); ui->actionSaveAs->setEnabled(false); ui->pictureLabel->setMinimumSize(1, 1); connect(ui->actionOpenPicture, &QAction::triggered, this, &MainWindow::loadPicture);
[ 271 ]
Third-Party Libraries Without a Headache connect(ui->actionSaveAs, &QAction::triggered, this, &MainWindow::saveAsPicture); ... } voidMainWindow::loadPicture() { ... if (filename.isEmpty()) { return; } ui->actionSaveAs->setEnabled(true); ... } voidMainWindow::displayPicture(constQImage& picture) { mFilteredPicture = picture; mCurrentPixmap = QPixmap::fromImage(picture); updatePicturePixmap(); } voidMainWindow::saveAsPicture() { QString filename = QFileDialog::getSaveFileName(this, "Save Picture", QDir::homePath(), tr("Images (*.png *.jpg)")); if (filename.isEmpty()) { return; } mFilteredPicture.save(filename); }
The code snippet is long, but not very complex. The actionSaveAs function is enabled only when a picture is loaded. When the picture has been processed, mFilteredPicture is updated with the given picture. Because it is a reference, it costs absolutely nothing to store this filtered picture. Finally, the saveAsPicture() function asks the user for a path and saves it using the QImage API, which tries to deduce the picture type based on the file extension.
[ 272 ]
Third-Party Libraries Without a Headache
Summary
In this chapter, you learned how to integrate a third-party library with each desktop OS (Windows, Linux, and Mac). We chose the OpenCV library, which has been included in a custom Qt Designer plugin, and which can display a live preview of your image processing result in Qt Designer. We created an image filtering application that can open pictures, apply filters to them, and save the result on your machine. We had a good look at how you can integrate third-party libraries and how to make a Qt Designer plugin. In the next chapter, we will push things forward by making the imagefilter application ready to load filter plugins that could be implemented by third-party developers. To make things even cooler, we will cover the Qt animation framework to make the image-filter more spectacular.
[ 273 ]
8
Animations - It's Alive, Alive! In the previous chapter, you learned how to create a custom Qt Designer plugin. This chapter will push things further and teach you how to create a distributable Software Development Kit (SDK) to third-party developers, how the plugin system works with Qt, and how to make your application more attractive using fancy animations. The example project will be a reimplementation of the project from Chapter 7, Third-Party Libraries Without a Headache. You will build the same image processing application, but with the ability to import the filters from plugins. This chapter will teach you how to do the following: Create an SDK using the Qt Plugin system Implement custom plugins using the SDK Factorize build tasks using .pri Dynamically load plugins in your final application Understand the Qt Animation framework Use simple, sequential, and parallel animations Apply custom effects using QPropertyAnimation and QGraphics effects
Preparing an SDK
Before diving into the code, we have to take a moment to reflect on how we are going to structure it. This chapter has two goals: Cover the Qt Plugin system in more depth Study and integrate the Qt Animation Framework
Animations - It's Alive, Alive!
The first part of the chapter will focus on the plugin system. What we aim to do is provide a way to build plugins that can be integrated in our application to third-party developers. These plugins should be dynamically loaded. The application will be a direct offspring of the example project from Chapter 7, Third-Party Libraries Without a Headache. The features will be exactly the same, except it will be using this new plugin system and will have fancy animations. The structure of the project will be as follows:
The parent project is ch08-image-animation, which is composed of the following: filter-plugin-original: A library project, which is the implementation of the original filter filter-plugin-grayscale: A library project, which is the implementation of the grayscale filter filter-plugin-blur: A library project, which is the implementation of the blur filter image-animation: A Qt Widgets application, which will load the plugins needed to display them and make it possible to apply each one to a loaded picture
[ 275 ]
Animations - It's Alive, Alive!
We will develop each one of these plugins, but keep in mind that they might have been created by a third-party developer. To achieve this openness, an SDK will be available for each plugin. This SDK relies on the Qt Plugin system. It is crucial to think about what should be handled by the plugin. Our application is an image processing piece of software. We chose to limit the responsibility of the plugin to the picture processing part, but this is definitely a design choice. Another approach could have been to let the plugin developer provide its own UI to configure the plugin (for example, to vary the intensity of the blur). In this chapter, we have kept it simple by focusing only on the plugin development itself. It is really up to you and how you want to design your application. By opening up the range of what the plugin can do, you also increase the burden for the plugin developer. There is always a trade-off; giving more choice tends to increase the complexity. It is a well-known fact that we developers are a bunch of lazy people. At least, we want to be lazy while the computer is working for us. We will start by building the SDK that will be deployed in each plugin. Execute the following steps: 1. Create a Subdirs project named ch08-image-animation (do not add a subproject at the end of the wizard). 2. In your filesystem explorer, open the ch08-image-animation directory and create an sdk directory. 3. Inside sdk, create an empty Filter.h file. Our SDK will consist of a single file, Filter.h, the interface (or header) that should be implemented with each plugin. Each plugin is responsible for returning the modified picture according to its desired features. Because this SDK is not linked to any particular project, we will simply display it in Qt Creator under the special folder Other files. To do so, update ch08-image-animation.pro: TEMPLATE = subdirs CONFIG += c++14 OTHER_FILES += \ sdk/Filter.h
[ 276 ]
Animations - It's Alive, Alive!
After ch08-image-animation.pro has been parsed by Qt Creator, you should see the following in the Projects tab:
The Filter.h file is available at the parent project level. As a result, it will be easier to factorize the SDK plumbing code between our various plugins. Let's implement Filter.h: #include class Filter { public: virtual ~Filter() {} virtual QString name() const = 0; virtual QImage process(const QImage& image) = 0; }; #define Filter_iid "org.masteringqt.imageanimation.filters.Filter" Q_DECLARE_INTERFACE(Filter, Filter_iid)
Let's break down this interface: a Filter subclass must provide a name by implementing name() and returning a processed image when implementing process(). As you can see, Filter.h is indeed very close to the version seen in Chapter 7, Third-Party Libraries Without a Headache. However, the really new stuff comes right after the class definition: #define Filter_iid "org.masteringqt.imageanimation.filters.Filter" Q_DECLARE_INTERFACE(Filter, Filter_iid)
The Filter_iid is a unique identifier to let Qt know the interface name. This will be enforced on the implementer side, which will also have to state this identifier. For a real-world use case, you should add a version number to this unique identifier. This will let you properly handle the versioning of your SDK and the attached plugins.
[ 277 ]
Animations - It's Alive, Alive!
The Q_DECLARE_INTERFACE macro associates the class to the given identifier. It will give Qt the ability to check that the loaded plugin can be safely casted to the Filter type. In production code, it is safer to declare your interfaces inside a namespace. You never know the code environment in which your SDK will be deployed. This, way, you avoid potential name collision. If you do declare in a namespace, make sure that the Q_DECLARE_INTERFACE macro is outside the namespace scope.
Creating your plugins
The SDK was painless to create. We can now proceed to create our first plugin. We already know that all our plugins will include the SDK we just completed. Fortunately, this can be easily factorized in a .pri file (PRoject Include). A .pri file behaves exactly like a .pro file; the only difference is that it is intended to be included inside .pro files. In the ch08-image-animation directory, create a file named plugins-common.pri that contains the following code: INCLUDEPATH += $$PWD/sdk DEPENDPATH += $$PWD/sdk
This file will be included in each .pro plugin. It aims to tell the compiler where it can find the headers of the SDK and where to look to resolve dependencies between headers and sources. This will enhance the modification detection and properly compile the sources when needed. To see this file in the project, we have to add it to the OTHER_FILES macro in ch08-imageanimation.pro: OTHER_FILES += \ sdk/Filter.h \ plugins-common.pri
The most straightforward plugin to build is filter-plugin-original as it does not perform any specific processing on the image. Let's create this plugin with the following steps: 1. Create a new Subproject in ch08-image-animation. 2. Select Library | C++ Library | Choose…. 3. Choose a Shared Library, name it filter-plugin-original, and then click on Next. [ 278 ]
Animations - It's Alive, Alive!
4. Select QtCore and go to QtWidgets | Next. 5. Name the created class FilterOriginal and click on Next. 6. Add it as a subproject to ch08-image-animation then click on Finish. Qt Creator creates a lot of boilerplate code for us, but in this case, we do not need it. Update filter-plugin-original.pro like so: QT
+= core widgets
TARGET = $$qtLibraryTarget(filter-plugin-original) TEMPLATE = lib CONFIG += plugin SOURCES += \ FilterOriginal.cpp HEADERS += \ FilterOriginal.h include(../plugins-common.pri)
We start by specifying that the TARGET should be properly named according to the OS convention with $$qtLibraryTarget(). The CONFIG property adds the plugin directive, which tells the generated Makefile to include the necessary instructions to compile a dll/so/dylib (pick your OS). We removed the unnecessary DEFINES and FilterOriginal_global.h. Nothing specific to the plugin should be exposed to the caller, and therefore, there is no need to handle the symbol export. We can now proceed to FilterOriginal.h: #include #include class FilterOriginal : public QObject, Filter { Q_OBJECT Q_PLUGIN_METADATA(IID "org.masteringqt.imageanimation.filters.Filter") Q_INTERFACES(Filter) public: FilterOriginal(QObject* parent = 0); ~FilterOriginal();
[ 279 ]
Animations - It's Alive, Alive! QString name() const override; QImage process(const QImage& image) override; };
The FilterOriginal class must first inherit QObject; when the plugin will be loaded, it will first be a QObject class before being casted to the real type, Filter. The Q_PLUGIN_METADATA macro is stated to export the proper implemented interface identifier to Qt. It annotates the class to let the Qt metasystem know about it. We meet the unique identifier we defined in Filter.h again. The Q_INTERFACES macro tells the Qt metaobject system which interface the class implements. Finally, the FilterOriginal.cpp barely deserves to be printed: FilterOriginal::FilterOriginal(QObject* parent) : QObject(parent) { } FilterOriginal::~FilterOriginal() { } QString FilterOriginal::name() const { return "Original"; } QImage FilterOriginal::process(const QImage& image) { return image; }
As you can see, its implementation is a no-op. The only thing we added to the version from Chapter 7, Third-Party Libraries Without a Headache, is the name() function, which returns Original. We will now implement the grayscale filter. As we did in Chapter 7, Third-Party Libraries Without a Headache, we will rely on the OpenCV library to process the picture. The same can be said for the following plugin, the blur.
[ 280 ]
Animations - It's Alive, Alive!
Since these two projects have their own .pro file, you can already foresee that the OpenCV linking will be the same. This is a perfect use-case for a .pri file. Inside the ch08-image-animation directory, create a new file called plugins-commonopencv.pri. Do not forget to add it to OTHER_FILES in ch08-image-animation.pro: OTHER_FILES += \ sdk/Filter.h \ plugins-common.pri \ plugins-common-opencv.pri
Here is the content of plugins-common-opencv.pri: windows { INCLUDEPATH += $$(OPENCV_HOME)/../../include LIBS += -L$$(OPENCV_HOME)/lib \ -lopencv_core2413 \ -lopencv_imgproc2413 } linux { CONFIG += link_pkgconfig PKGCONFIG += opencv } macx { INCLUDEPATH += /usr/local/Cellar/opencv/2.4.13/include/ LIBS += -L/usr/local/lib \ -lopencv_core \ -lopencv_imgproc }
The content of plugins-common-opencv.pri is a direct copy of what we made in Chapter 7, Third-Party Libraries Without a Headache. All the plumbing is now ready; we can now go ahead with the filter-plugingrayscale project. As with filter-plugin-original, we will build it the following way: 1. Create a C++ Library Subproject of ch08-image-animation with the Shared Library type. 2. Create a class named FilterGrayscale. 3. In the Required Modules, select QtCore and QWidgets.
[ 281 ]
Animations - It's Alive, Alive!
Here is the updated version of filter-plugin-grayscale.pro: QT
+= core widgets
TARGET = $$qtLibraryTarget(filter-plugin-grayscale) TEMPLATE = lib CONFIG += plugin SOURCES += \ FilterGrayscale.cpp HEADERS += \ FilterGrayscale.h include(../plugins-common.pri) include(../plugins-common-opencv.pri)
The content is very much like filter-plugin-original.pro. We only added pluginscommon-opencv.pri to let our plugin link with OpenCV. As for FilterGrayscale, the header is exactly like FilterOriginal.h. Here are the relevant pieces on FilterGrayscale.cpp: #include // Constructor & Destructor here ... QString FilterOriginal::name() const { return "Grayscale"; } QImage FilterOriginal::process(const QImage& image) { // QImage => cv::mat cv::Mat tmp(image.height(), image.width(), CV_8UC4, (uchar*)image.bits(), image.bytesPerLine()); cv::Mat resultMat; cv::cvtColor(tmp, resultMat, CV_BGR2GRAY); // cv::mat => QImage QImage resultImage((const uchar *) resultMat.data, resultMat.cols,
[ 282 ]
Animations - It's Alive, Alive! resultMat.rows, resultMat.step, QImage::Format_Grayscale8); return resultImage.copy(); }
The inclusion of plugins-common-opencv.pri lets us properly include the cv.h header. The last plugin we will implement is the blur plugin. Once again, create a C++ Library Subproject and create the FilterBlur class. The project structure and the content of the .pro file are the same. Here is FilterBlur.cpp: QString FilterOriginal::name() const { return "Blur"; } QImage FilterOriginal::process(const QImage& image) { // QImage => cv::mat cv::Mat tmp(image.height(), image.width(), CV_8UC4, (uchar*)image.bits(), image.bytesPerLine()); int blur = 17; cv::Mat resultMat; cv::GaussianBlur(tmp, resultMat, cv::Size(blur, blur), 0.0, 0.0); // cv::mat => QImage QImage resultImage((const uchar *) resultMat.data, resultMat.cols, resultMat.rows, resultMat.step, QImage::Format_RGB32); return resultImage.copy(); }
[ 283 ]
Animations - It's Alive, Alive!
The amount of blur is hard-coded at 17. In a production application, it could have been compelling to make this amount variable from the application. If you want to push the project further, try to include a layout in the SDK that contains a way to configure the plugin properties.
Loading your plugins dynamically We will now deal with the application loading these plugins:
1. Create a new Subproject inside ch08-image-animation. 2. Select the type Qt Widgets Application. 3. Name it image-animation and accept the default Class Information settings. We have a few last things to do in the .pro files. First, image-animation will try to load the plugins from somewhere in its output directory. Because each filter plugin project is independent, its output directory is separated from image-animation. Thus, each time you modify a plugin, you will have to copy yourself the compiled shared library inside the proper image-animation directory. This works to make it available to the imageanimation application, but we are lazy developers, right? We can automate this by updating plugins-common-pri like so: INCLUDEPATH += $$PWD/sdk DEPENDPATH += $$PWD/sdk windows { CONFIG(debug, debug|release) { target_install_path = $$OUT_PWD/../image-animation/debug/plugins/ } else { target_install_path = $$OUT_PWD/../image-animation/release/plugins/ } } else { target_install_path = $$OUT_PWD/../image-animation/plugins/ } # Check Qt file 'spec_post.prf' for more information about '$$QMAKE_MKDIR_CMD' createPluginsDir.path = $$target_install_path createPluginsDir.commands = $$QMAKE_MKDIR_CMD $$createPluginsDir.path
[ 284 ]
Animations - It's Alive, Alive! INSTALLS += createPluginsDir target.path = $$target_install_path INSTALLS += target
In a nutshell, the output library is deployed in the output image-animation/plugins directory. Windows has a different output project structure; that is why we have to have a platform-specific section. Even better, the plugins directory is automatically created with the instruction createPluginsDir.commands = $$QMAKE_MKDIR_CMD $$createPluginsDir.path. Instead of using a system command (mkdir), we have to use the special $$QMAKE_MKDIR_CMD command. Qt will then replace it with the correct shell command (depending on your OS) to create the directory only if it does not already exist. Do not forget to add the make install build step to execute this task! The last thing to do in the .pro files concerns image-animation. The application will manipulate Filter instances. As a consequence, it needs to access the SDK. Add the following to image-animation.pro: INCLUDEPATH += $$PWD/../sdk DEPENDPATH += $$PWD/../sdk
Fasten your seatbelt. We will now load our freshly baked plugins. In image-animation, create a new class named FilterLoader. Here is the FilterLoader.h content: #include #include #include class FilterLoader { public: FilterLoader(); void loadFilters(); const std::vector& filters() const; private: std::vector mFilters; };
[ 285 ]
Animations - It's Alive, Alive!
This class is responsible for loading the plugins. Once again, we rely on C++11 smart pointers with unique_ptr to explicate the ownership of the Filter instances. The FilterLoader class will be the owner with mFilters and provides a getter to the vector with filters(). Note that filter() returns a const& to the vector. This semantic brings two benefits: The reference makes sure that the vector is not copied. Without it, the compiler would have barked something like “FilterLoader is not the owner anymore of mFilters content!” at us. Of course, because it deals with C++ templates, the compiler error would have looked rather like an astounding insult to the English language. The const keyword makes sure that the vector type cannot be modified by callers. Now we can create the FilterLoader.cpp: file: #include "FilterLoader.h" #include #include #include FilterLoader::FilterLoader() : mFilters() { } void FilterLoader::loadFilters() { QDir pluginsDir(QApplication::applicationDirPath()); #ifdef Q_OS_MAC pluginsDir.cdUp(); pluginsDir.cdUp(); pluginsDir.cdUp(); #endif pluginsDir.cd("plugins"); for(QString fileName: pluginsDir.entryList(QDir::Files)) { QPluginLoader pluginLoader( pluginsDir.absoluteFilePath(fileName)); QObject* plugin = pluginLoader.instance(); if (plugin) { mFilters.push_back(std::unique_ptr( qobject_cast(plugin) ));
[ 286 ]
Animations - It's Alive, Alive! } } } const std::vector& FilterLoader::filters() const { return mFilters; }
The meat of the class lies in loadFilter(). We start by moving in the plugins directory with pluginsDir, located in the output directory of image-animation. A special case is handled for the Mac platform: QApplication::applicationDirPath() returns a path inside the bundle of the generated application. The only way to get out is to climb our way up three times with the cdUp() instruction. For each fileName in this directory, we try to load a QPluginLoader loader. A QPluginLoader provides access to a Qt plugin. It is the cross-platform way to load a shared library. Moreover, the QPluginLoader loader has the following benefits: It checks that the plugin is linked with the same version of Qt as the host application It simplifies the loading of the plugin by providing direct access to the plugin via instance() rather than relying on C functions We then proceed to try to load the plugin using pluginLoader.instance(). This will try to load the root component of the plugin. In our case, the root component is either FilerOriginal, FilterGrayscale, or FilterBlur. This function always returns a QObject*; if the plugin could not be loaded, it returns 0. This is the reason why we inherited the QObject class in our custom plugins. The call to instance() implicitly tries to load the plugin. Once this has been done, the QPluginLoader does not handle the memory of the plugin. From here, we cast the plugin to Filter* using qobject_cast(). The qobject_cast() function behaves similarly to the standard C++ dynamic_cast(); the difference is that it does not require RTTI (runtime type information). Last but not least, the Filter* casted plugin is wrapped inside a unique_ptr and added to mFilters vector.
[ 287 ]
Animations - It's Alive, Alive!
Using the plugins inside the application
Now that the plugins are properly loaded, they have to be reachable from the UI of the application. To do so, we are going to take some inspiration (shameless stealing) from the FilterWidget class of Chapter 7, Third-Party Libraries Without a Headache. Create a new Qt Designer Form Class using the Widget template named FilterWidget. The FilterWidget.ui file is exactly the same as the one completed in Chapter 7, ThirdParty Libraries Without a Headache. Create the FilterWidget.h file like this: #include #include namespace Ui { class FilterWidget; } class Filter; class FilterWidget : public QWidget { Q_OBJECT public: explicit FilterWidget(Filter& filter, QWidget *parent = 0); ~FilterWidget(); void process(); void setSourcePicture(const QImage& sourcePicture); void setSourceThumbnail(const QImage& sourceThumbnail); void updateThumbnail(); QString title() const; signals: void pictureProcessed(const QImage& picture); protected: void mousePressEvent(QMouseEvent*) override; private: Ui::FilterWidget *ui; Filter& mFilter;
[ 288 ]
Animations - It's Alive, Alive! QImage mDefaultSourcePicture; QImage mSourcePicture; QImage mSourceThumbnail; QImage mFilteredPicture; QImage mFilteredThumbnail; };
Overall, we trimmed everything concerning the Qt Designer plugin and simply passed the mFilter value by reference to the constructor. The FilterWidget class is not the owner of the Filter anymore; it is rather the client that calls it. Remember that the owner of Filter (aka the plugin) is FilterLoader. The other modification is the new setThumbnail() function. It should be called in place of the old updateThumbnail(). The new updateThumbnail() now only performs the thumbnail processing and does not touch the source thumbnail. This division is done to prepare the work for the coming animation section. The thumbnail update will be done only once the animation has been finished. Please refer to the source code of the chapter to see FilterWidget.cpp.
All the low layers have been completed. The next step is to fill MainWindow. Once again, it follows the same pattern we covered in Chapter 7, Third-Party Libraries Without a Headache. The sole difference with MainWindow.ui is that filtersLayout is empty. Obviously, the plugin is loaded dynamically, so we have nothing to put inside it at compile time. Let's cover MainWindow.h: #include #include #include #include "FilterLoader.h" namespace Ui { class MainWindow; } class FilterWidget; class MainWindow : public QMainWindow { Q_OBJECT
[ 289 ]
Animations - It's Alive, Alive! public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); void loadPicture(); protected: void resizeEvent(QResizeEvent* event) override; private slots: void displayPicture(const QImage& picture); void saveAsPicture(); private: void initFilters(); void updatePicturePixmap(); private: Ui::MainWindow *ui; QImage mSourcePicture; QImage mSourceThumbnail; QImage& mFilteredPicture; QPixmap mCurrentPixmap; FilterLoader mFilterLoader; FilterWidget* mCurrentFilter; QVector mFilters; };
The only notable thing is the addition of mFilterLoader as a member variable. In MainWindow.cpp, we will focus on the changes only: void MainWindow::initFilters() { mFilterLoader.loadFilters(); auto& filters = mFilterLoader.filters(); for(auto& filter : filters) { FilterWidget* filterWidget = new FilterWidget(*filter); ui->filtersLayout->addWidget(filterWidget); connect(filterWidget, &FilterWidget::pictureProcessed, this, &MainWindow::displayPicture); mFilters.append(filterWidget); } if (mFilters.length() > 0) { mCurrentFilter = mFilters[0]; }
[ 290 ]
Animations - It's Alive, Alive! }
The initFilters() function does not load the filters from the ui content. Rather, it starts by calling the mFilterLoader.loadFilters() function to dynamically load the plugins from the plugins directory. After that, an auto& filter is assigned with mFilterLoader.filters(). Note that it is much more readable to use auto keyword. The real type is std::vector&, which looks more like a cryptic incantation than a simple object type. For each of these filters, we create a FilterWidget* and pass it the reference of the filter. Here, filter is effectively a unique_ptr. The people behind C++11 wisely modified the dereferencing operator, making it transparent to the new FilterWidget(*filter). The combination of the auto keyword and the overload of the -> operator, or the dereference operator, makes the use of new C++ features much more enjoyable. Look at the for loop. For each filter we do the following tasks: 1. Create a FilterWidget template. 2. Add the FilterWidget template to the filtersLayout children. 3. Connect the FilterWidget::pictureProcessed signal to the MainWindow::displayPicture slot. 4. Add the new FilterWidget template to the QVectormFilters. In the end, the first FilterWidget is selected. The only other modification to MainWindow.cpp is the implementation of loadPicture(): void MainWindow::loadPicture() { ... for (int i = 0; i setSourcePicture(mSourcePicture); mFilters[i]->setSourceThumbnail(mSourceThumbnail); mFilters[i]->updateThumbnail(); } mCurrentFilter->process(); }
[ 291 ]
Animations - It's Alive, Alive!
The updateThumbnail() function has been split into two functions, and here is where it is used. The application can now be tested. You should be able to execute it and see the dynamic plugins loaded and displaying the processed default Lenna picture.
Discovering the Animation Framework
Your application works like a charm. It is now time to look at how we can make it jump and move, or, in a word, live. The Qt Animation Framework can be used to create and start animations of Qt properties. The property value will be smoothly interpolated by an internal global timer handle by Qt. You can animate anything as long as it is a Qt property. You can even create a property for your own object using Q_PROPERTY. If you forgot about Q_PROPERTY, please refer to Chapter 7, Third-Party Libraries Without a Headache. Three main classes are provided to build animations: QPropertyAnimation: This class animates one Qt property animation QParallelAnimationGroup: This class animates multiple animations in parallel
(all the animations start together) QSequentialAnimationGroup: This class animates multiple animations in sequence (the animations run one by one in a defined order) All those classes inherit QAbstractAnimation. Here is a diagram from the official Qt documentation:
[ 292 ]
Animations - It's Alive, Alive!
Please notice that QAbstractAnimation, QVariantAnimation, and QAnimationGroup are abstract classes. Here is a simple example of a Qt animation: QLabel label; QPropertyAnimation animation; animation.setTargetObject(&label); animation.setPropertyName("geometry"); animation.setDuration(4000); animation.setStartValue(QRect(0, 0, 150, 50)); animation.setEndValue(QRect(300, 200, 150, 50)); animation.start();
The preceding snippet moves a QLabel label from the 0 x 0 position to 300 x 200 in four seconds. The first thing to do is to define the target object and its property. In our case, the target object is label and we want to animate the property called geometry. Then, we set the animation duration in milliseconds: 4000 milliseconds for four seconds. Finally, we can decide the start and end values of the geometry property, which is a QRect, defined like this: QRect(x, y, width, height)
The label object starts with the 0 x 0 position and ends with 300 x 200. In this example, the size is fixed (150 x 50), but you can also animate the width and the height if you want. Finally, we call the start() function to begin the animation. In four seconds, the animation smoothly moves the label from the 0 x 0 position to 300 x 200. By default, the animation uses a linear interpolation to provide intermediate values, so, after two seconds, the label will be at the 150 x 100 position. The linear interpolation of the value looks like the following schema:
[ 293 ]
Animations - It's Alive, Alive!
In our case, the label object will move with a constant speed from the start to the end position. An easing function is a mathematical function that describes the evolution of a value over time. The easing curve is the visual representation of the mathematical function. The default linear interpolation is a good start, but Qt provides plenty of easing curves to control the speed behavior of your animation. Here is the updated example: QLabel label; QPropertyAnimation animation(&label, "geometry"); animation.setDuration(4000); animation.setStartValue(QRect(0, 0, 150, 50)); animation.setEndValue(QRect(300, 200, 150, 50)); animation.setEasingCurve(QEasingCurve::InCirc); animation.start();
You can set the target object and the property name directly using the QPropertyAnimation constructor. As a result, we removed the setTargetObject() and setPropertyName() functions. After that, we use setEasingCurve() to specify a curve for this animation. The InCirc looks like the following:
With this easing curve, the label starts to move really slowly but accelerates progressively during the animation. Another way is to define the intermediate key steps yourself, using the setKeyValueAt() function. Let's update our example: QLabel label; QPropertyAnimation animation(&label, "geometry"); animation.setDuration(4000); animation.setKeyValueAt(0, QRect(0, 0, 150, 50)); animation.setKeyValueAt(0.25, QRect(225, 112.5, 150, 50)); animation.setKeyValueAt(1, QRect(300, 200, 150, 50)); animation.start();
[ 294 ]
Animations - It's Alive, Alive!
We are now setting key frames using setKeyValueAt(). The first argument is the time step in the range 0 to 1. In our case, step 1 means four seconds. The key frames at step 0 and step 1 provide the same positions as the start/end positions of the first example. As you can see, we also add a key frame at step 0.25 (that's one second for us) with the position 225 x 112.5. The next schema illustrates this:
You can clearly distinguish the three key frames created with setKeyValueAt(). In our example, our label will quickly reach the 225 x 112.5 position in one second. Then the label will slowly move to the 300 x 200 position during the remaining three seconds. If you have more than one QPropertyAnimation object, you can use groups to create more complex sequences. Let's see an example: QPropertyAnimation animation1(&label1, "geometry"); QPropertyAnimation animation2(&label2, "geometry"); ... QSequentialAnimationGroup animationGroup; animationGroup.addAnimation(&anim1); animationGroup.addAnimation(&anim2); animationGroup.start();
In this example, we are using a QSequentialAnimationGroup to run animations one by one. The first thing to do is to add animations to animationGroup. Then, when we call start() on our animation group, animation1 is launched. When animation1 is finished, animationGroup runs animation2. A QSequentialAnimationGroup is finished when the last animation of the list ends. The next schema depicts this behavior:
[ 295 ]
Animations - It's Alive, Alive!
The second animation group, QParallelAnimationGroup, is initialized and started in the same way as QSequentialAnimationGroup. But the behavior is different: it starts all the animations in parallel, waiting for the longest animation to end. Here is an illustration of this:
Keep in mind that an animation group is itself an animation (it inherits QAbstractAnimation). As a consequence, you can add animation groups to other animation groups to create a very complex animation sequence!
Making your thumbnails jump
Let's apply what we learned about the Qt Animation Framework to our project. Each time the user clicks on a filter thumbnail, we want to poke it. All modifications will be done on the FilterWidget class. Let's start with FilterWidget.h: #include class FilterWidget : public QWidget { Q_OBJECT public: explicit FilterWidget(Filter& filter, QWidget *parent = 0); ~FilterWidget(); ... private: void initAnimations(); void startSelectionAnimation(); private: ... QPropertyAnimation mSelectionAnimation; };
[ 296 ]
Animations - It's Alive, Alive!
The first function, initAnimations(), initializes the animations used by FilterWidget. The second function, startSelectionAnimation(), performs tasks required to start this animation correctly. As you can see, we are also declaring a QPropertyAnimation class, as covered in the previous section. We can now update FilterWidget.cpp. Let's update the constructor: FilterWidget::FilterWidget(Filter& filter, QWidget *parent) : QWidget(parent), ... mSelectionAnimation() { ... initAnimations(); updateThumbnail(); }
We initialize our QPropertyAnimation called mSelectionAnimation. The constructor also calls initAnimations(). Here is its implementation: void FilterWidget::initAnimations() { mSelectionAnimation.setTargetObject(ui->thumbnailLabel); mSelectionAnimation.setPropertyName("geometry"); mSelectionAnimation.setDuration(200); }
You should be familiar with these animation initialization steps now. The target object is the thumbnailLabel displaying the filter plugin preview. The property name to animate is geometry, because we want to update the position of this QLabel. Finally, we set the animation duration to 200 ms. Like jokes, keep it short and sweet. Update the existing mouse event handler like this: void FilterWidget::mousePressEvent(QMouseEvent*) { process(); startSelectionAnimation(); }
Each time the user clicks on the thumbnail, the selection animation moving the thumbnail will be called. We can now add this most important function like this: void FilterWidget::startSelectionAnimation() { if (mSelectionAnimation.state() == QAbstractAnimation::Stopped) {
[ 297 ]
Animations - It's Alive, Alive! QRect currentGeometry = ui->thumbnailLabel->geometry(); QRect targetGeometry = ui->thumbnailLabel->geometry(); targetGeometry.setY(targetGeometry.y() - 50.0); mSelectionAnimation.setKeyValueAt(0, currentGeometry); mSelectionAnimation.setKeyValueAt(0.3, targetGeometry); mSelectionAnimation.setKeyValueAt(1, currentGeometry); mSelectionAnimation.start(); } }
The first thing to do is to retrieve the current geometry of thumbnailLabel called currentGeometry. Then, we create a targetGeometry object with the same x, width, and height values. We only reduce the y position by 50, so the target position is always above the current position. After that, we define our key frames: At step 0, the value is the current position. At step 0.3 (60 ms, because the total duration is 200 ms), the value is the target position. At step 1 (the end of the animation), we bring it to back the original position. The thumbnail will quickly reach the target position, then slowly fall down to its original position. These key frames must be initialized before each animation starts. Because the layout is dynamic, the position (and so the geometry) could have been updated when the user resizes the main window. Please note that we are preventing the animation from starting again if the current state is not stopped. Without this precaution, the thumbnail could move to the top again and again if the user clicks like a mad man on the widget. You can now test your application and click on a filter effect. The filter thumbnail will jump to respond to your click!
Fading the picture in
When the user opens a picture, we want to fade in the image by playing with its opacity. The classes QLabel or QWidget do not provide an opacity property. However, we can add a visual effect to any QWidget using a QGraphicsEffect. For this animation, we will use QGraphicsOpacityEffect to provide an opacity property. [ 298 ]
Animations - It's Alive, Alive!
Here is a schema to describe the role of each one:
In our case, the QWidget class is our QLabel and the QGraphicsEffect class is QGraphicsOpacityEffect. Qt provides the Graphics Effect system to alter the rendering of a QWidget class. The abstract class QGraphicsEffect has a pure virtual method draw() that is implemented by each graphics effect. We can now update the MainWindow.h according to the next snippet: #include #include class MainWindow : public QMainWindow { ... private: ... void initAnimations(); private: ... QPropertyAnimation mLoadPictureAnimation; QGraphicsOpacityEffect mPictureOpacityEffect; };
The initAnimations() private function is in charge of all the animation initializations. The mLoadPictureAnimation member variable performs the fade-in animation on the loaded picture. Finally, we declare mPictureOpacityEffect, the mandatory QGraphicsOpacityEffect. Let's switch to the implementation part with the MainWindow.cpp constructor: MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ... mLoadPictureAnimation(), mPictureOpacityEffect() { ... initFilters(); initAnimations(); }
[ 299 ]
Animations - It's Alive, Alive!
No surprises here. We use the initializer list to construct our two new member variables. The MainWindow constructor also calls initAnimations(). Let's look at how this animation is initialized: void MainWindow::initAnimations() { ui->pictureLabel->setGraphicsEffect(&mPictureOpacityEffect); mLoadPictureAnimation.setTargetObject(&mPictureOpacityEffect); mLoadPictureAnimation.setPropertyName("opacity"); mLoadPictureAnimation.setDuration(500); mLoadPictureAnimation.setStartValue(0); mLoadPictureAnimation.setEndValue(1); mLoadPictureAnimation.setEasingCurve(QEasingCurve::InCubic); }
The first thing to do is to link our QGraphicsOpacityEffect with our QLabel. This can be easily done by calling the setGraphicsEffect() function on pictureLabel. Now we can set our animation up. In this case, mLoadPictureAnimation targets mPictureOpacityEffect and will affect its property named opacity. The animation duration is 500 milliseconds. Next, we set the opacity value when the animation starts and ends: At the beginning, the picture is completely transparent (opacity value is 0) At the end, the picture is fully visible (opacity value is 1) For this animation, we use the easing curve InCubic. This curve looks like this:
Feel free to try other curves to find the one that looks the best for you.
[ 300 ]
Animations - It's Alive, Alive!
You can get the list of all easing curves with a visual preview here: http://doc.qt.io/qt-5/qeasingcurve.html
The last step is to start the animation at the right place: void MainWindow::loadPicture() { ... mCurrentFilter->process(); mLoadPictureAnimation.start(); }
You can now start your application and load a picture. You should see your picture fade in over 500 milliseconds!
Flashing the thumbnail in a sequence
For this last animation, we want to display a blue flash on each filter preview when the thumbnail is updated. We do not want to flash all previews at the same time, but in a sequence, one by one. This feature will be achieved in two parts: Create a color animation in FilterWidget to display a blue flash Build a sequential animation group in MainWindow containing all FilterWidget color animations Let's start to add the color animation. Update FilterWidget.h as shown in the following snippet: #include class FilterWidget : public QWidget { Q_OBJECT public: explicit FilterWidget(Filter& filter, QWidget *parent = 0); ~FilterWidget(); ... QPropertyAnimation* colorAnimation(); private: ...
[ 301 ]
Animations - It's Alive, Alive! QPropertyAnimation mSelectionAnimation; QPropertyAnimation* mColorAnimation; QGraphicsColorizeEffect mColorEffect; };
This time we do not want to affect the opacity, but rather colorize the thumbnail in blue. Thus, we use another Qt standard effect: QGraphicsColorizeEffect. We also declare a new QPropertyAnimation named mColorAnimation and its corresponding getter, colorAnimation(). We declare mColorAnimation as a pointer because the ownership will be taken by the animation group of MainWindow. This topic will be covered soon. Let's update the constructor in FilterWidget.cpp: FilterWidget::FilterWidget(Filter& filter, QWidget *parent) : QWidget(parent), ... mColorAnimation(new QPropertyAnimation()), mColorEffect() { ... }
We just have to construct our two new member variables, mColorAnimation and mColorEffect. Let's look at the amazing complexity of the getter: QPropertyAnimation* FilterWidget::colorAnimation() { return mColorAnimation; }
It was a lie: we always try to write comprehensive code! Now that the preliminaries are done, we can initialize the color animation by updating the initAnimations() function like this: void FilterWidget::initAnimations() { ... mColorEffect.setColor(QColor(0, 150, 150)); mColorEffect.setStrength(0.0); ui->thumbnailLabel->setGraphicsEffect(&mColorEffect); mColorAnimation->setTargetObject(&mColorEffect); mColorAnimation->setPropertyName("strength"); mColorAnimation->setDuration(200); mColorAnimation->setStartValue(1.0);
[ 302 ]
Animations - It's Alive, Alive! mColorAnimation->setEndValue(0.0); }
The first part sets the color filter up. Here, we chose a kind of turquoise color for the flash effect. The colorize effect is handled by its strength property. By default, the value is 1.0, so, we set it to 0.0 to keep it from affecting our default thumbnail of Lenna. Finally, we link the thumbnailLabel with this mColorEffect calling setGraphicsEffect(). The second part is the color animation preparation. This animation targets the color effect and its property, named strength. This is a short flash; 200 milliseconds is enough: We want to start with a full strength effect, so we put the start value at 1.0 During the animation, the colorize effect will decrease until it reaches 0.0 The default linear interpolation is fine here, so we do not use any easing curve. Here we are. The color effect/animation is initialized and we provided a colorAnimation() getter. We can now begin the second part of this feature, Updating MainWindow.h: #include class MainWindow : public QMainWindow { Q_OBJECT ... private: ... QSequentialAnimationGroup mFiltersGroupAnimation; };
We declare a QSequentialAnimationGroup class to trigger, one by one, all FilterWidget color animations displaying the blue flash. Let's update the constructor in MainWindow.cpp: MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ... mFiltersGroupAnimation() { ... }
[ 303 ]
Animations - It's Alive, Alive!
A new member variable means a new construction in the initializer list: that is the rule! We can now update initAnimations() to prepare our animation group: void MainWindow::initAnimations() { ... for (FilterWidget* filterWidget : mFilters) { mFiltersGroupAnimation.addAnimation( filterWidget->colorAnimation()); } }
Do you remember that an animation group is only an animation container? As a consequence, we iterate on every FilterWidget to get its color animation and fill our mFiltersGroupAnimation calling addAnimation(). Thanks to C++11's range-based for loop, it is really readable. Keep in mind that when you add an animation to an animation group, the group takes ownership of this animation. Our animation group is ready. We can now start it: void MainWindow::loadPicture() { ... mCurrentFilter->process(); mLoadPictureAnimation.start(); mFiltersGroupAnimation.start(); }
Start your application and open a picture. You can see that all filter thumbnails will flash one by one from left to right. This is what we intended, but it's still not perfect because all the thumbnails are already updated before the flashes. We have this behavior because the loadPicture() function actually sets and updates all thumbnails, and then finally starts the sequential animation group. Here is a schema illustrating the current behavior:
[ 304 ]
Animations - It's Alive, Alive!
The schema only describes the behavior for two thumbnails, but the principle is the same with three thumbnails. Here is the targeted behavior:
We must only update the thumbnail when the flash animation is over. Fortunately, QPropertyAnimation emits the finished signal when the animation is over, so we only have to make a few changes. Update loadPicture() function from MainWindow.cpp: void MainWindow::loadPicture() { ... for (int i = 0; i setSourcePicture(mSourcePicture); mFilters[i]->setSourceThumbnail(mSourceThumbnail); //mFilters[i]->updateThumbnail(); } ... }
As you can see, we kept the set and only removed the update thumbnail when a new picture is opened by the user. At this stage, all FilterWidget instances have the correct thumbnail, but they don't display it. Let's fix this by updating FilterWidget.cpp: void FilterWidget::initAnimations() { ... mColorAnimation->setTargetObject(&mColorEffect); mColorAnimation->setPropertyName("strength"); mColorAnimation->setDuration(200); mColorAnimation->setStartValue(1.0); mColorAnimation->setEndValue(0.0); connect(mColorAnimation, &QPropertyAnimation::finished, [this] { updateThumbnail(); }); }
[ 305 ]
Animations - It's Alive, Alive!
We connect a lambda function to the finished signal of the color animation. This lambda simply updates the thumbnail. You can now start your application again and load a picture. You should see that we not only animate the sequential blue flash, but also the thumbnail update.
Summary
In this chapter, you defined a Filter interface in your own SDK. Your filters are now plugins. You know how to create and load a new plugin, so your application is now modular and can be easily extended. We have also enhanced the application with the Qt Animation Framework. You know how to animate the position, the color, and the opacity of any QWidget, using QGraphicsEffect if necessary. We created a sequential animation that starts three animations one by one with QSequentialAnimationGroup. In the next chapter, we will talk about a big subject: threading. The Qt framework can help you build a robust and reliable multithreading application. To illustrate the chapter, we will create a Mandelbrot fractal generator using threadpools.
[ 306 ]
9
Keeping Your Sanity with Multithreading In previous chapters, we managed to always write code without ever relying on threads. It is time to face the beast and truly understand how threading works in Qt. In this chapter, you will develop a multithreaded application that displays a Mandelbrot fractal. It is a heavy computational process that will bring tears to your CPU cores. In the example project, the user can see the Mandelbrot fractal, zoom in the picture, and pan around to discover the magic of fractals. The chapter covers the following topics: Deep understanding of the QThread framework Overview of all the available threading technologies in Qt Using a QThreadPool class to dispatch jobs and aggregate the results How to synchronize threads and minimize sharing states Low-level drawing to optimize the performances Common threading pitfalls and challenges
Discovering QThread
Qt provides a sophisticated threading system. We assume you already know threading basics and the associated issues (deadlocks, threads synchronization, resource sharing, and so on) and we will focus on how Qt implements it.
Keeping Your Sanity with Multithreading
The QThread is the central class of the Qt threading system. A QThread instance manages one thread of execution within the program. You can subclass QThread to override the run() function, which will be executed in the QThread framework. Here is how you can create and start a QThread: QThread thread; thread.start();
The start() function calling will automatically call the run() function of the thread and emit the started() signal. Only at this point will the new thread of execution be created. When run() is completed, the thread object will emit the finished() signal. This brings us to a fundamental aspect of QThread: it works seamlessly with the signal/slot mechanism. Qt is an event-driven framework, where a main event loop (or the GUI loop) processes events (user input, graphical, and so on) to refresh the UI. Each QThread comes with its own event loop that can process events outside the main loop. If not overridden, run() calls the QThread::exec() function, which starts the thread object's event loop. You can also override QThread and call yourself exec(), like so: class Thread : public QThread { Q_OBJECT protected: void run() { Object* myObject = new Object(); connect(myObject, &Object::started, this, &Thread::doWork); exec(); } private slots: void doWork(); };
The started()signal will be processed by the Thread event loop only upon the exec() call. It will block and wait until QThread::exit() is called. A crucial thing to note is that a thread event loop delivers events for all QObjects that are living in that thread. This includes all objects created in that thread or moved to that thread. This is referred to as the thread affinity of an object. Let's see an example: class Thread : public QThread {
[ 308 ]
Keeping Your Sanity with Multithreading Thread() : mObject(new QObject()) { } private : QObject* myObject; }; // Somewhere in MainWindow Thread thread; thread.start();
In this snippet, myObject is constructed in Thread class's constructor, which is created in turn in MainWindow. At this point, thread is living in the GUI thread. Hence, myObject is also living in the GUI thread. An object created before a QCoreApplication object has no thread affinity. As a consequence, no event will be dispatched to it.
It is great to be able to handle signals and slots in our own QThread, but how can we control signals across multiple threads? A classic example is a long-running process that is executed in a separate thread that has to notify the UI to update some state: class Thread : public QThread { Q_OBJECT void run() { // long running operation emit result("I