Проект 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);
}

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

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

По-моему, это всё-таки уже часть 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 так

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
Как стать автором?

Внесите вклад в развитие сообщества EVILEG.

Узнайте, как стать автором сайта.

Изучить
Donate

Добрый день, Дорогие Пользователи !!!

Я Евгений Легоцкой, разработчик EVILEG. И это мой хобби-проект, который помогает учиться программированию другим программистам и разработчикам

Если сайт помог вам, и вы хотите также поддержать развитие сайта, то вы можете сделать пожертвование следующими способами

PayPalYandex.Money
Timeweb

Позвольте мне порекомендовать вам отличный хостинг, на котором расположен EVILEG.

В течение многих лет Timeweb доказывает свою стабильность.

Для проектов на Django рекомендую VDS хостинг

Посмотреть Хостинг Timeweb
5 июня 2020 г. 23:20
Алексей

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:60баллов,
  • Очки рейтинга-1
5 июня 2020 г. 23:15
Алексей

C++ - Тест 001. Первая программа и типы данных

  • Результат:53баллов,
  • Очки рейтинга-4
V
5 июня 2020 г. 16:47
Vladzo

C++ - Тест 005. Структуры и Классы

  • Результат:83баллов,
  • Очки рейтинга4
Последние комментарии
6 июня 2020 г. 15:13
Владислав Меленчук

Как установить и настроить Django JET

Можно использовать six и оттуда импортировать unicode. Я так решил проблему) Но всё равно откатился обратно на админку джанги.
6 июня 2020 г. 11:20
BL4CK R4BBIT

Как установить и настроить Django JET

на Django >3+ работать не будет. Либо придется лезть внутрь и переопределять метод unicode . Либо писать декоратор
5 июня 2020 г. 10:52
progammist

Распознавание изображений на Python с помощью TensorFlow и Keras

Огромное спасибо за метериал, по-больше бы подобных статей (с подробным описанием работы и примерами применения) на тему современных технологий. Вопрос поразмышлять. На текущий момент реал…
5 июня 2020 г. 1:39
Евгений Легоцкой

Qt/C++ - Урок 091. Как написать кастомный делегат управляющий подсветкой строки в таблице

По-моему, смысла в этом нет особого. Если делегат будет игнорировать настройки таблицы, то это приведёт ещё к большему непониманию, что вообще происходит, для программиста, который после вас буд…
5 июня 2020 г. 1:34
IscanderChe

Qt/C++ - Урок 091. Как написать кастомный делегат управляющий подсветкой строки в таблице

Сижу, размышляю: можно ли переписать делегата так, чтобы независимо от настроек строк выделялись строки?
Сейчас обсуждают на форуме
u
6 июня 2020 г. 7:26
ubomj

Галерея изображений

delete
s
6 июня 2020 г. 1:54
shuric

Qt/C++ Определение положения курсора над действие(кнопкой) в QToolBar

Доброго дня. Возник вопрос - как можно определить что курсор находится над определенным действием(кнопкой) в qtoolbar ? mainwindow.cpp MainWindow::MainWi…
s
6 июня 2020 г. 0:45
shuric

Qt/C++ особенности QProxyStyle

Да, Вы правы. Код был скопирован с сайта (уже не помню с какого), но решил пойти по пути более легком. Пришлось переписать - кому интересно: использовал stackedWidget для пе…
5 июня 2020 г. 23:08
Алексей

Посоветуйте новичку (базы данных и Qt, что учить)

Блин, а я недавно купил Шлее Qt 5.10 :( С детства хотел стать программистом, баловался Паскалем, писал простенькие программки на Delphi, создавал движок на php, изучал C (забросил и перешел на п…
5 июня 2020 г. 13:09
IscanderChe

QPlainTextEdit настройка цвета фона

Вечер добрый. Пытаюсь настроить цвет фона QPlainTextEdit следующим образом: CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent){ ... QPalette::ColorRole role = bac…
О нас
Услуги
© EVILEG 2015-2020
Рекомендует хостинг TIMEWEB