End to End GUI Development with Qt5
上QQ阅读APP看书,第一时间看更新

Commands

The next thing on our to-do list is to implement a context-sensitive command bar. While our navigation bar is a constant presence with the same buttons regardless of what the user is doing, the command bar will come and go and will contain different buttons depending on the context. For example, if the user is adding or editing a client, we will need a Save button to commit any changes to the database. However, if we are searching for a client, then saving makes no sense and a Find button is more relevant. While the techniques for creating our command bar are broadly similar to the navigation bar, the additional flexibility required poses more of a challenge.

To help us overcome these obstacles, we will implement commands. An additional benefit of this approach is that we get to move the logic out of the UI layer and into the business logic layer. I like the UI to be as dumb and as generic as possible. This makes your application more flexible, and bugs in C++ code are easier to identify and resolve than those in QML.

A command object will encapsulate an icon, descriptive text, a function to determine whether the button is enabled or not, and finally, an executed() signal that will be emitted when the related button is pressed. Each button in our command bar will then be bound to a command object.

Each of our child view may have a list of commands and an associated command bar. For the views that do, we will present the list of commands to the UI via a command controller.

Create two new C++ classes in the cm-lib project, both of which should inherit from QObject:

  • Command in a new folder cm-lib/source/framework
  • Command Controller in the existing folder cm-lib/source/controllers

command.h:

#ifndef COMMAND_H
#define COMMAND_H
#include <functional>
#include <QObject> #include <QScopedPointer> #include <QString>
#include <cm-lib_global.h>
namespace cm { namespace framework {
class CMLIBSHARED_EXPORT Command : public QObject { Q_OBJECT Q_PROPERTY( QString ui_iconCharacter READ iconCharacter CONSTANT ) Q_PROPERTY( QString ui_description READ description CONSTANT ) Q_PROPERTY( bool ui_canExecute READ canExecute NOTIFY canExecuteChanged )
public: explicit Command(QObject* parent = nullptr, const QString& iconCharacter = "", const QString& description = "", std::function<bool()> canExecute = [](){ return
true; });
~Command();
const QString& iconCharacter() const; const QString& description() const; bool canExecute() const;
signals: void canExecuteChanged(); void executed();
private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif

command.cpp:

#include "command.h"
namespace cm { namespace framework {
class Command::Implementation { public: Implementation(const QString& _iconCharacter, const QString&
_description, std::function<bool()> _canExecute)
: iconCharacter(_iconCharacter) , description(_description) , canExecute(_canExecute) { }
QString iconCharacter; QString description; std::function<bool()> canExecute; };
Command::Command(QObject* parent, const QString& iconCharacter, const QString& description, std::function<bool()> canExecute) : QObject(parent) { implementation.reset(new Implementation(iconCharacter, description, canExecute)); }
Command::~Command() { }
const QString& Command::iconCharacter() const { return implementation->iconCharacter; }
const QString& Command::description() const { return implementation->description; }
bool Command::canExecute() const { return implementation->canExecute(); }
} }

The QObject, namespaces, and dll export code should be familiar by now. We represent the icon character and description values we want to display on the UI buttons as strings. We hide the member variables away in the private implementation and provide accessor methods for them. We could have represented the canExecute member as a simple bool member that calling code could set to true or false as required; however, a much more elegant solution is to pass in a method that calculates the value for us on the fly. By default, we set it to a lambda that returns true, which means that the button will be enabled. We provide a canExecuteChanged() signal to go along with this, which we can fire whenever we want the UI to reassess whether the button is enabled or not. The last element is the executed() signal that will be fired by the UI when the corresponding button is pressed.

command-controller.h:

#ifndef COMMANDCONTROLLER_H
#define COMMANDCONTROLLER_H
#include <QObject> #include <QtQml/QQmlListProperty> #include <cm-lib_global.h> #include <framework/command.h>
namespace cm { namespace controllers {
class CMLIBSHARED_EXPORT CommandController : public QObject { Q_OBJECT Q_PROPERTY(QQmlListProperty<cm::framework::Command>
ui_createClientViewContextCommands READ
ui_createClientViewContextCommands CONSTANT)
public: explicit CommandController(QObject* _parent = nullptr); ~CommandController();
QQmlListProperty<framework::Command>
ui_createClientViewContextCommands();
public slots: void onCreateClientSaveExecuted();
private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif

command-controller.cpp:

#include "command-controller.h"
#include <QList> #include <QDebug>
using namespace cm::framework;
namespace cm { namespace controllers {
class CommandController::Implementation { public: Implementation(CommandController* _commandController) : commandController(_commandController) { Command* createClientSaveCommand = new Command(
commandController, QChar( 0xf0c7 ), "Save" );
QObject::connect( createClientSaveCommand, &Command::executed,
commandController, &CommandController::onCreateClientSaveExecuted );
createClientViewContextCommands.append( createClientSaveCommand ); }
CommandController* commandController{nullptr}; QList<Command*> createClientViewContextCommands{}; };
CommandController::CommandController(QObject* parent) : QObject(parent) { implementation.reset(new Implementation(this)); }
CommandController::~CommandController() { }
QQmlListProperty<Command> CommandController::ui_createClientViewContextCommands() { return QQmlListProperty<Command>(this, implementation->createClientViewContextCommands); }
void CommandController::onCreateClientSaveExecuted() { qDebug() << "You executed the Save command!"; }
}}

Here, we introduce a new type—QQmlListProperty. It is essentially a wrapper that enables QML to interact with a list of custom objects. Remember that we need to fully qualify the templated type in the Q_PROPERTY statements. The private member that actually holds the data is a QList, and we have implemented an accessor method that takes the QList and converts it into a QQmlListProperty of the same templated type.

As per the documentation for QQmlListProperty, this method of object construction should not be used in production code, but we’ll use it to keep things simple.

We have created a single command list for our CreateClientView. We’ll add command lists for other views later. Again, we’ll keep things simple for now; we just create a single command to save a newly created client. When creating the command, we parent it to the command coordinator so that we don’t have to worry about memory management. We assign it a floppy disk icon (unicode f0c7) and the Save label. We leave the canExecute function as the default for now so it will always be enabled. Next, we connect the executed() signal of the command to the onCreateClientSaveExecuted() slot of the CommandController. With the wiring done, we then add the command to the list.

The intention is that we present the user with a command button bound to a Command object. When the user presses the button, we will fire the executed() signal from the UI. The connection we’ve set up will cause the slot on the command controller to be called, and we will execute our business logic. For now, we’ll simply print out a line to the console when the button is pressed.

Next, let’s register both of our new types in main.cpp (remember the #includes):

qmlRegisterType<cm::controllers::CommandController>("CM", 1, 0, "CommandController");
qmlRegisterType<cm::framework::Command>("CM", 1, 0, "Command");

Finally, we need to add the CommandCoordinator property to MasterController:

Q_PROPERTY( cm::controllers::CommandController* ui_commandController READ commandController CONSTANT )

Then, we add an accessor method:

CommandController* commandController();

Finally, in master-controller.cpp, instantiate the object in the private implementation and implement the accessor method in exactly the same way as we did for NavigationController.

We now have a (very short!) list of commands ready for our CreateClientView to consume.