Проект Simple Tracker. Часть 5: сервер. Модель данных задач и представление

Simple Tracker, Iscander Che, C++, Qt

Рассмотрим подробно таблицу задач.

По требованиям, указанным вначале, таблица должна была выглядеть так.

На момент разработки стало понятно, что логика работы отдельно стоящего выпадающего списка становится сложной, и было принято решение реализовать его через делегата. Поэтому таблица теперь выглядит так.

Вначале реализуем делегата, потом займёмся заливкой цветом и запретом редактирования ячеек.

Для этого надо унаследовать QStyledItemDelegate и перезаписать несколько его методов.

// comboboxdelegate.h

#ifndef COMBOBOXDELEGATE_H
#define COMBOBOXDELEGATE_H

// Подключаем main.h, откуда нам понадобится и база данных, и список состояний задач
#include "main.h"
#include <QStyledItemDelegate>

class ComboBoxDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    ComboBoxDelegate();

    QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
                          const QModelIndex& index) const override;

    void setEditorData(QWidget* editor, const QModelIndex& index) const override;

    void setModelData(QWidget* editor, QAbstractItemModel* model,
                      const QModelIndex& index) const override;

    void updateEditorGeometry(QWidget* editor,
        const QStyleOptionViewItem& option, const QModelIndex& index) const override;

    void paint(QPainter* painter,
        const QStyleOptionViewItem& option, const QModelIndex& index) const override;

private slots:
    void changedComboBox(int index);
};

#endif // COMBOBOXDELEGATE_H

Реализуем указанные методы.

// comboboxdelegate.cpp

#include "comboboxdelegate.h"
#include "customsortfilterproxymodel.h"
#include <QComboBox>
#include <QApplication>
#include <QMessageBox>

ComboBoxDelegate::ComboBoxDelegate()
{
}

// Создаём комбобокс
QWidget* ComboBoxDelegate::createEditor(QWidget* parent,
                                        const QStyleOptionViewItem& /*option*/,
                                        const QModelIndex& /*index*/) const
{
    QComboBox* comboBox = new QComboBox(parent);

    // Загружаем список состояний задач
    foreach(QString status, statusList)
        comboBox->addItem(status);

    comboBox->setCurrentIndex(0);

    // Создаём соединение между сигналом изменения индекса комбобокса и его обработчиком
    connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &ComboBoxDelegate::changedComboBox);

    return comboBox;
}

// Устанавливаем данные для комбобокса
void ComboBoxDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
    // Если поле 5 (колонка "Состояние")
    if(index.column() == 5)
    {
        // читаем данные из модели
        QString value = index.model()->data(index, Qt::EditRole).toString();
        QComboBox* comboBox = static_cast<QComboBox*>(editor);

        for(int i = 0; i < statusList.size(); ++i)
        {
            if(value == statusList.at(i))
                // и устанавливаем соответствующий индекс
                comboBox->setCurrentIndex(i);
        }
    }
}

// Записываем данные в модель (через базу данных)
void ComboBoxDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
                                    const QModelIndex& index) const
{
    // Если поле 5 (колонка "Состояние")
    if(index.column() == 5)
    {
        QComboBox* edit = static_cast<QComboBox*>(editor);
        QString status = edit->currentText();
        // Получаем исходную модель
        CustomSortFilterProxyModel* filterModel =
            static_cast<CustomSortFilterProxyModel*>(model);
        int row = index.row();
        // Получаем из модели поле с номером задачи
        QModelIndex idIndex = filterModel->index(row, 0);
        // Получаем номер задачи
        int id = filterModel->data(idIndex, Qt::DisplayRole).toInt();
        // Если состояние задачи переведено в "закрыта"
        if(status == "закрыта")
            // закрыть задачу
            database->closeTask(id, "manual");
        else
            // иначе обновить состояние задачи
            database->updateStatusTask(id, status);
    }
}

// Переустанавливаем геометрию комбобокса
void ComboBoxDelegate::updateEditorGeometry(QWidget* editor,
    const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const
{
    editor->setGeometry(option.rect);
}

// Перерисовываем комбобокс
void ComboBoxDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
                             const QModelIndex& index) const
{
    if(index.column() == 5)
    {
        QStyleOptionComboBox comboBoxStyleOption;
        comboBoxStyleOption.state = option.state;
        comboBoxStyleOption.rect = option.rect;
        comboBoxStyleOption.currentText = index.data(Qt::EditRole).toString();

        QApplication::style()->drawComplexControl(QStyle::CC_ComboBox, &comboBoxStyleOption, painter, 0);
        QApplication::style()->drawControl(QStyle::CE_ComboBoxLabel, &comboBoxStyleOption, painter, 0);

        return;
    }

    QStyledItemDelegate::paint(painter,option, index);
}

// Отправка сигнала модели, что данные должны быть записаны после изменения
// состояния комбобокса
void ComboBoxDelegate::changedComboBox(int /*index*/)
{
    emit commitData(qobject_cast<QComboBox*>(sender()));
}

Реализовалось не совсем гладко. Осталась беда с доступностью выпадающего списка: он начинает нормально работать только после двойного щелчка на нём. И всё бы ничего, но на двойной щелчок у меня подвешено редактирование задачи, диалоговое окно которого и появляется. После закрытия окна можно спокойно менять состояние задачи.

Теперь займёмся цветом и ячейками. Здесь необходимо унаследовать соответствующую модель данных.

// customsortfilterproxymodel.h

#ifndef CUSTOMSORTFILTERPROXYMODEL_H
#define CUSTOMSORTFILTERPROXYMODEL_H

// Подключаем main.h, из которого нам понадобятся список состояний и соответствующих им цветов
#include "main.h"
#include <QSortFilterProxyModel>

class CustomSortFilterProxyModel : public QSortFilterProxyModel
{
public:
    CustomSortFilterProxyModel();

    Qt::ItemFlags flags(const QModelIndex& index) const override;

    QVariant data(const QModelIndex& idx, int role) const override;
};

#endif // CUSTOMSORTFILTERPROXYMODEL_H
// customsortfilterproxymodel.cpp

#include "customsortfilterproxymodel.h"
#include <QBrush>
#include <QColor>

CustomSortFilterProxyModel::CustomSortFilterProxyModel()
{
}

// Устанавливаем для всех ячеек, кроме столбца "Состояние", свойства
// доступности ячеек и возможности их выбора, редактирование ячеек невозможно
Qt::ItemFlags CustomSortFilterProxyModel::flags(const QModelIndex& index) const
{
    if(index.isValid())
    {
        if(index.column() != 5)
        {
            Qt::ItemFlags flags = QSortFilterProxyModel::flags(index);
            flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
            return flags;
        }
    }

    return QSortFilterProxyModel::flags(index);
}

// Устанавливаем для всех ячеек в строке цвет в зависимости от состояния задачи
QVariant CustomSortFilterProxyModel::data(const QModelIndex& idx, int role) const
{
    if(idx.isValid())
    {
        if(role == Qt::BackgroundRole)
        {
            for(int i = 0; i < statusList.size(); ++i)
            {
                if(QSortFilterProxyModel::data(this->index(idx.row(), 5)).toString() == statusList.at(i))
                    return QBrush(QColor(colorList.at(i)));
            }
        }
        else if(role == Qt::DisplayRole)
            return QSortFilterProxyModel::data(idx);
    }

    return QSortFilterProxyModel::data(idx, role);
}

Теперь вид таблицы соответствует обновлённым требованиям.

10% refund of hotel reservation amount on Booking
10% refund of hotel reservation amount on Booking
We offer a link with a 10% return on the amount of the order when booking a hotel through Booking

По-моему, это всё-таки уже часть 5.

Вот здесь у вас такой код

            Qt::ItemFlags flags = QSortFilterProxyModel::flags(index);
            flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
            return flags;

В данном случае вызывать метод базового класс не имеет смысла, поскольку вы всё равно полностью перезаписываете флаги, а не добавляете новые к тем, которые вернул метод базового класса. То есть можно просто так написать.

return Qt::ItemIsEnabled | Qt::ItemIsSelectable;

Вы придерживаетесь какого-то код стайла с ограничение количества колонок на строку?

        CustomSortFilterProxyModel* filterModel =
            static_cast<CustomSortFilterProxyModel*>(model);

У нас в проекте принято именовать через enum колонки в модели данных, чтобы не было вот таких магических чисел

if(index.column() != 5)

Такое именование может выглядеть следующим образом.

class CustomSortFilterProxyModel : public QSortFilterProxyModel
{
public:
    enum EColumn 
    {
        E_COLUMN__BEGIN = 0,
        E_COLUMN_NUMBER = E_COLUMN__BEGIN,
        E_COLUMN_DATE,
        E_COLUMN_TYPE,
        E_COLUMN_DESCRIPTION,
        E_COLUMN_STATE,
        E_COLUMN_DATE_OF_CLOSING,
        E_COLUMN_REVISION,
        E_COLUMN__END
    }
    CustomSortFilterProxyModel();

    Qt::ItemFlags flags(const QModelIndex& index) const override;

    QVariant data(const QModelIndex& idx, int role) const override;
};

Тогда такой код будет выглядеть так, это лучше, чем магическое число.

if(index.column() != E_COLUMN_DATE_OF_CLOSING)

Плюс я бы объединил проверки

 if (index.isValid() && index.column() != 5)

Вообще я думаю, что весь этот метод можно переписать тогда так

Qt::ItemFlags CustomSortFilterProxyModel::flags(const QModelIndex& index) const
{
    return (index.isValid() && index.column() != E_COLUMN_DATE_OF_CLOSING) ? (Qt::ItemIsEnabled | Qt::ItemIsSelectable) : QSortFilterProxyModel::flags(index);
}

Если не будет компилироваться, то тогда так

Qt::ItemFlags CustomSortFilterProxyModel::flags(const QModelIndex& index) const
{
    if (index.isValid() && index.column() != E_COLUMN_DATE_OF_CLOSING) 
    {
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    }
    return QSortFilterProxyModel::flags(index);
}

Вот этот метод стоит переписать так

QVariant CustomSortFilterProxyModel::data(const QModelIndex& idx, int role) const
{
    // Если индекс невалидный, то вы всё равно ничего хорошего дальше не получите даже из базового метода.
    if (!idx.isValid())
    {
        // Поэтому можно смело возвращать невалидный QVariant и по большей части не беспокоиться
        return QVariant();
    }

    if (role == Qt::BackgroundRole)
    {
        for (int i = 0; i < statusList.size(); ++i)
        {
            if (QSortFilterProxyModel::data(this->index(idx.row(), 5)).toString() == statusList.at(i))
            {
                return QBrush(QColor(colorList.at(i)));
            }
        }
    }

    // В вашем коде условие на Qt::DisplayRole бесполезное, поскольку role имеет аргумент по умолчанию Qt::DisplayRole,
    // то есть если вы даже не будете передавать ту роль в метод, то она всё равно туда попадёт, то есть просто лишняя проверка в коде
    return QSortFilterProxyModel::data(idx, role);
}

Да, часть 5, поправил.

В данном случае вызывать метод базового класс не имеет смысла, поскольку вы всё равно полностью перезаписываете флаги, а не добавляете новые к тем, которые вернул метод базового класса. То есть можно просто так написать.

Согласен.

Вы придерживаетесь какого-то код стайла с ограничение количества колонок на строку?

Да, стараюсь вписаться в 80 символов. Если разделить окно просмотра кода на два экрана по вертикали, это актуально.

У нас в проекте принято именовать через enum колонки в модели данных, чтобы не было вот таких магических чисел

Согласен, что нужны enumы. Только я думаю, что будет лучше вынести их глобально куда-нибудь, т.к. у меня эти же магические числа используются и в теле основного класса.

Вообще я думаю, что весь этот метод можно переписать тогда так
return (index.isValid() && index.column() != E_COLUMN_DATE_OF_CLOSING) ? (Qt::ItemIsEnabled | Qt::ItemIsSelectable) : QSortFilterProxyModel::flags(index);

От такого однострочника у меня голова лопается.)) Лучше второй вариант.

Вот этот метод стоит переписать так

Согласен.

Спасибо за подробный разбор кода!

Да, стараюсь вписаться в 80 символов. Если разделить окно просмотра кода на два экрана по вертикали, это актуально.

Понимаю, к сожалению иногда названия методов или классов становятся достаточно большими, чтобы проект комфортно поддерживался, то есть сокращения неприемлемы, что актуально в крупном проекте. В тоже время код-стайл может не приветствовать подобный перенос того, что справа от оператора = на следующую строку.

Согласен, что нужны enumы. Только я думаю, что будет лучше вынести их глобально куда-нибудь, т.к. у меня эти же магические числа используются и в теле основного класса.

Это пока у вас одна модель данных на весь проект, а когда в проекте около 1000 моделей, то тогда лучше, чтобы каждая модель данных имела свой набор наименований колонок, которые бы подчинялись определённым правилам. Тем более, что наименования колонок имеют прямое отношение к модели данных, поэтому скорее другой программист ожидал бы видеть такой enum в области модели, чем где-то глобально.

Вы можете просто подключать заголовочный файл модели и использовать enum так

CustomSortFilterProxyModel::E_COLUMN_DATE_OF_CLOSING

Таким образом у вас будет информация, к какой области кода непосредственно относится колонка.

Вы можете просто подключать заголовочный файл модели и использовать enum так

Да, так даже лучше. Спасибо.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
D
Aug. 16, 2019, 11:58 a.m.
Damir

C++ - Тест 003. Условия и циклы

  • Result:92points,
  • Rating points8
D
Aug. 16, 2019, 11:46 a.m.
Damir

C++ - Test 005. Structures and Classes

  • Result:75points,
  • Rating points2
u
Aug. 14, 2019, 1:55 p.m.
unrealproro

C++ - Test 005. Structures and Classes

  • Result:83points,
  • Rating points4
Last comments
Aug. 19, 2019, 6:41 a.m.
Andrej Jankovich

это проблема дистрибутива, попробуйте установить через пакетный менеджер snap Суть проблемы: libQt5Core которая лежит в дистрибутиве требует версию glibc >= 2.25 у вас видимо …
b
Aug. 18, 2019, 5:09 a.m.
bbb116

cqtdeployer /home/aleks/CQtDeployer/bin/cqtdeployer: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25' not found (required by /home/aleks/CQtDeployer/lib/libQt5Core.so.5) linux mint …
D
Aug. 17, 2019, 8:04 a.m.
Damir

github ChekableTView Правой групповая смена значения при перетаскивании левой как обычно.
Aug. 16, 2019, 12:03 p.m.
Evgenij Legotskoj

Потому, что в минуте 60 секунд
Aug. 16, 2019, 11:16 a.m.
Dmitrij

а почему делитель 60000, а не 1000?
Now discuss on the forum
Aug. 20, 2019, 12:17 p.m.
Evgenij Legotskoj

Добрый день. Вы делаете некорректную попытку создать исключение. Исключения генерируются кодом, то есть любое исключение, которое вы перехватываете, всегда генерируется оператором th…
Aug. 20, 2019, 8:04 a.m.
IscanderChe

Ещё раз здравствуйте. Собираю Qt-проект с помощью CMake. Применяю к полученному exe-файлу windeployqt. В результате подцепляются почему-то dll-ки, оканчивающиеся в наименованиях на "d": Qt…
Aug. 20, 2019, 7:46 a.m.
IscanderChe

Да, с таргетом тоже работает. Спасибо!
Aug. 20, 2019, 7:25 a.m.
Evgenij Legotskoj

вы можете испльзовать QList, просто помещайте туда QPair, будет примерно тоже самое. Просто QMap автоматически сортируется по ключу. QList<QPair<QString, QString>> list_with_pair;…
Looking for a Job?
14,000.00 руб. - 40,000.00 руб.
Разработчик Qt
Annino, Moscow Oblast, Russia
5,000.00 руб. - 15,000.00 руб.
Дизайнер
Moskovskiy, Moscow, Russia
25,000.00 руб. - 30,000.00 руб.
Разработчик Qt/C++
Barnaul, Altai Krai, Russia

For registered users on the site there is a minimum amount of advertising

EVILEG
About
Services
© EVILEG 2015-2019
Recommend hosting TIMEWEB