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 checked(bool checked) slot that will be connected to the QCheckBox::toggled signal. In slot checked(), we strike out the checkbox text according to the bool checked value. This is done by using the QFont class. We create a copied font from checkbox->font(), modify it, and assign it back to ui->checkbox. Event if the original font was in bold or with a special size, its appearance would still be guaranteed to stay the same.
The last instruction notifies MainWindow that the Task status has changed. The signal name is statusChanged, rather than checkboxChecked, in order to hide the implementation details of the task. Now 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 called taskStatusChanged, which is connected once a task is created. The single instruction of this slot is to call updateStatus(). This function iterates through the tasks and updates 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:
// without the 'auto' keyword
std::vector<Task*>::const_iterator iterator = mTasks.toStdVector().begin();
// with the 'auto' keyword, how many neurones did you save? auto autoIter = mTasks.toStdVector().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.
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.