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); }
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.
Tip
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); 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) { ... }
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.
Tip
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() << "Trying to remove" << prettyName(name); this->emit removed(this); });
Now that's something beautiful. Combining auto
with lambda
makes very readable code and opens up a world of possibilities.
The last item to study is the QString
API. We used it in updateStatus()
:
ui->statusLabel->setText( QString("Status: %1 todo / %2 completed") .arg(todoCount) .arg(completedCount));
The people behind Qt put a lot of work into making string manipulation bearable in C++. This is a perfect example, where we replace the classic C sprintf
with a more modern and robust API. Arguments are position-based only, no need to specify the type (less error-prone), and the arg(...)
function accepts all kinds of types.
Tip
Take some time to skim through the QString
documentation at http://doc.qt.io/qt-5/qstring.html. It shows how much you can do with this class and you'll see yourself using fewer and fewer examples of std string
or even cstring
.