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

Finding clients

We've just successfully saved our first clients to the database, so let's now look at how we can find and view that data. We’ll encapsulate our searching functionality in a dedicated class in cm-lib, so go ahead and create a new class named ClientSearch in cm-lib/source/models.

client-search.h:

#ifndef CLIENTSEARCH_H
#define CLIENTSEARCH_H
#include <QScopedPointer>
#include <cm-lib_global.h> #include <controllers/i-database-controller.h> #include <data/string-decorator.h> #include <data/entity.h> #include <data/entity-collection.h> #include <models/client.h>
namespace cm { namespace models {
class CMLIBSHARED_EXPORT ClientSearch : public data::Entity { Q_OBJECT Q_PROPERTY( cm::data::StringDecorator* ui_searchText READ
searchText CONSTANT )
Q_PROPERTY( QQmlListProperty<cm::models::Client> ui_searchResults
READ ui_searchResults NOTIFY searchResultsChanged )
public: ClientSearch(QObject* parent = nullptr,
controllers::IDatabaseController* databaseController = nullptr);
~ClientSearch();
data::StringDecorator* searchText(); QQmlListProperty<Client> ui_searchResults(); void search();
signals: void searchResultsChanged(); private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif

client-search.cpp:

#include "client-search.h"
#include <QDebug>
using namespace cm::controllers; using namespace cm::data;
namespace cm { namespace models {
class ClientSearch::Implementation { public: Implementation(ClientSearch* _clientSearch, IDatabaseController*
_databaseController)
: clientSearch(_clientSearch) , databaseController(_databaseController) { }
ClientSearch* clientSearch{nullptr}; IDatabaseController* databaseController{nullptr}; data::StringDecorator* searchText{nullptr}; data::EntityCollection<Client>* searchResults{nullptr}; };
ClientSearch::ClientSearch(QObject* parent, IDatabaseController* databaseController) : Entity(parent, "ClientSearch") { implementation.reset(new Implementation(this, databaseController)); implementation->searchText = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "searchText", "Search Text"))); implementation->searchResults = static_cast<EntityCollection<Client>*>(addChildCollection(new EntityCollection<Client>(this, "searchResults"))); connect(implementation->searchResults, &EntityCollection<Client>::collectionChanged, this, &ClientSearch::searchResultsChanged); }
ClientSearch::~ClientSearch() { }
StringDecorator* ClientSearch::searchText() { return implementation->searchText; }
QQmlListProperty<Client> ClientSearch::ui_searchResults() { return QQmlListProperty<Client>(this, implementation->searchResults->derivedEntities()); } void ClientSearch::search() { qDebug() << "Searching for " << implementation->searchText->value() << "..."; }
}}

We need to capture some text from the user, search the database using that text, and display the results as a list of matching clients. We accommodate the text using a StringDecorator, implement a search() method to perform the search for us, and finally, add an EntitityCollection<Client> to store the results. One additional point of interest here is that we need to signal to the UI when the search results have changed so that it knows that it needs to rebind the list. To do this, we notify using the signal searchResultsChanged()and we connect this signal directly to the collectionChanged() signal built into EntityCollection. Now, whenever the list that is hidden away in EntityCollection is updated, the UI will be automatically notified of the change and will redraw itself as needed.

Next, add an instance of ClientSearch to MasterController, just as we did for the new client model. Add a private member variable of the ClientSearch* type named clientSearch, and initialize it in the Implementation constructor. Remember to pass the databaseController dependency to the constructor. Now that we are passing more and more dependencies, we need to be careful about the initialization order. ClientSearch has a dependency on DatabaseController, and when we come to implement our search commands in CommandController, that will have a dependency on ClientSearch. So ensure that you initialize DatabaseController before ClientSearch and that CommandController comes after both of them. To finish off the changes to MasterController, add a clientSearch() accessor method and a Q_PROPERTY named ui_clientSearch.

As usual, we need to register the new class in the QML subsystem before we can use it in the UI. In main.cpp, #include <models/client-search.h> and register the new type:

qmlRegisterType<cm::models::ClientSearch>("CM", 1, 0, "ClientSearch");

With all that in place, we can wire up our FindClientView:

import QtQuick 2.9
import assets 1.0
import CM 1.0
import components 1.0
Item { property ClientSearch clientSearch: masterController.ui_clientSearch
Rectangle { anchors.fill: parent color: Style.colourBackground
Panel { id: searchPanel anchors { left: parent.left right: parent.right top: parent.top margins: Style.sizeScreenMargin } headerText: "Find Clients" contentComponent: StringEditorSingleLine { stringDecorator: clientSearch.ui_searchText anchors { left: parent.left right: parent.right } } } } }

We access the ClientSearch instance via MasterController and create a shortcut to it with a property. We also utilize our new Panel component again, which gives us a nice consistent look and feel across views with very little work:

The next step is to add a command button for us to be able to instigate a search. We do this back over in CommandController. Before we get into the commands, we have an additional dependency on the ClientSearch instance, so add a parameter to the constructor:

CommandController::CommandController(QObject* parent, IDatabaseController* databaseController, Client* newClient, ClientSearch* clientSearch)
 : QObject(parent)
{
 implementation.reset(new Implementation(this, databaseController, newClient, clientSearch));
}

Pass the parameter through to the Implementation class and store it in a private member variable, just as we did with newClient. Hop back to MasterController briefly and add the clientSearch instance into the CommandController initialization:

commandController = new CommandController(masterController, databaseController, newClient, clientSearch);

Next, in CommandController, duplicate and rename the private member variable, accessor, and Q_PROPERTY that we added for the create client view so that you end up with a ui_findClientViewContextCommands property for the UI to use.

Create an additional public slot, onFindClientSearchExecuted()which will be called when we hit the search button:

void CommandController::onFindClientSearchExecuted()
{
 qDebug() << "You executed the Search command!";

implementation->clientSearch->search(); }

Now we have an empty command list for our find view and a delegate to be called when we click on the button; all we need to do now is add a search button to the Implementation constructor:

Command* findClientSearchCommand = new Command( commandController, QChar( 0xf002 ), "Search" );
QObject::connect( findClientSearchCommand, &Command::executed, commandController, &CommandController::onFindClientSearchExecuted );
findClientViewContextCommands.append( findClientSearchCommand );

That’s it for the command plumbing; we can now easily add a command bar to FindClientView. Insert the following as the last element within the root item:

CommandBar {
 commandList: masterController.ui_commandController.ui_findClientViewContextCommands
} 

Enter some search text and click on the button, and you will see in the Application Output console that everything triggers as expected:

You executed the Search command!
Searching for "Testing"...

Great, now what we need to do is take the search text, query the SQLite database for a list of results, and display those results on screen. Fortunately, we’ve already done the groundwork for querying the database, so we can easily implement that:

void ClientSearch::search()
{
 qDebug() << "Searching for " << implementation->searchText->value() 
<< "...";
auto resultsArray = implementation->databaseController-
>find("client", implementation->searchText->value());
implementation->searchResults->update(resultsArray);
qDebug() << "Found " << implementation->searchResults-
>baseEntities().size() << " matches";
}

There is a bit more work to do on the UI side to display the results. We need to bind to the ui_searchResults property and dynamically display some sort of QML subtree for each of the clients in the list. We will use a new QML component, ListViewto do the heavy lifting for us. Let’s start simple to demonstrate the principle and then build out from there. In FindClientView, immediately after the Panel element, add the following:

ListView {
 id: itemsView
 anchors {
 top: searchPanel.bottom
 left: parent.left
 right: parent.right
 bottom: parent.bottom
 margins: Style.sizeScreenMargin
 }
 clip: true
 model: clientSearch.ui_searchResults
 delegate:
 Text {
 text: modelData.ui_reference.ui_label + ": " + 
modelData.ui_reference.ui_value
font.pixelSize: Style.pixelSizeDataControls color: Style.colourPanelFont } }

The two key properties of a ListView are as listed:

  • The model, which is the list of items that you want to display
  • The delegatewhich is how you want to visually represent each item

In our case, we bind the model to our ui_searchResults and represent each item with a simple Text element displaying the client reference number. Of particular importance here is the modelData property, which is magically injected into the delegate for us and exposes the underlying item (which is a client object, in this case).

Build, run, and perform a search for a piece of text you know exists in the JSON for one of the test clients you have created so far, and you will see that the reference number is displayed for each of the results. If you get more than one result and they lay out incorrectly, don’t worry, as we will replace the delegate anyway:

To keep things neat and tidy, we’ll write a new custom component to use as the delegate. Create SearchResultDelegate in cm-ui/components, and update components.qrc and qmldir as usual:

import QtQuick 2.9
import assets 1.0
import CM 1.0
Item { property Client client
implicitWidth: parent.width implicitHeight: Math.max(clientColumn.implicitHeight,
textAddress.implicitHeight) + (Style.heightDataControls / 2)
Rectangle { id: background width: parent.width height: parent.height color: Style.colourPanelBackground
Column { id: clientColumn width: parent / 2 anchors { left: parent.left top: parent.top margins: Style.heightDataControls / 4 } spacing: Style.heightDataControls / 2
Text { id: textReference anchors.left: parent.left text: client.ui_reference.ui_label + ": " +
client.ui_reference.ui_value
font.pixelSize: Style.pixelSizeDataControls color: Style.colourPanelFont } Text { id: textName anchors.left: parent.left text: client.ui_name.ui_label + ": " +
client.ui_name.ui_value
font.pixelSize: Style.pixelSizeDataControls color: Style.colourPanelFont } }
Text { id: textAddress anchors { top: parent.top right: parent.right margins: Style.heightDataControls / 4 } text: client.ui_supplyAddress.ui_fullAddress font.pixelSize: Style.pixelSizeDataControls color: Style.colourPanelFont horizontalAlignment: Text.AlignRight }
Rectangle { id: borderBottom anchors { bottom: parent.bottom left: parent.left right: parent.right } height: 1 color: Style.colourPanelFont }
MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: background.state = "hover" onExited: background.state = "" onClicked: masterController.selectClient(client) }
states: [ State { name: "hover" PropertyChanges { target: background color: Style.colourPanelBackgroundHover } } ] } }

There isn’t really anything new here, we’ve just combined techniques covered in other components. Note that the MouseArea element will trigger a method on masterController that we haven’t implemented yet, so don’t worry if you run this and get an error when you click on one of the clients.

Replace the old Text delegate in FindClientView with our new component using the modelData property to set the client:

ListView {
 id: itemsView
 ...
 delegate:
 SearchResultDelegate {
 client: modelData
 }
}

Now, let’s implement the selectClient() method on MasterController:

We can just emit the goEditClientView() signal directly from the SearchResultDelegate and bypass MasterController entirely. This is a perfectly valid approach and is indeed simpler; however, I prefer to route all the interactions through the business logic layer, even if all the business logic does is to emit the navigation signal. This means that if you need to add any further logic later on, everything is already wired up and you don’t need to change any of the plumbing. It’s also much easier to debug C++ than QML.

In master-controller.h, we need to add our new method as a public slot as it will be called directly from the UI, which won’t have visibility of a regular public method:

public slots:
 void selectClient(cm::models::Client* client);

Provide the implementation in master-controller.cpp, simply calling the relevant signal on the navigation coordinator and passing through the client:

void MasterController::selectClient(Client* client)
{
 implementation->navigationController->goEditClientView(client);
}

With the searching and selection in place, we can now turn our attention to editing clients.