IscanderChe
IscanderChe26 июля 2019 г. 3:52

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

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

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

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

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


Для этого надо унаследовать 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 хостинг.

Вам это нравится? Поделитесь в социальных сетях!

Evgenii Legotckoi
  • 26 июля 2019 г. 5:37
  • (ред.)

По-моему, это всё-таки уже часть 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);
}
IscanderChe
  • 26 июля 2019 г. 6:38

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

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

Согласен.

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

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

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

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

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

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

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

Согласен.

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

Evgenii Legotckoi
  • 26 июля 2019 г. 6:53
  • (ред.)

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

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

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

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

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

CustomSortFilterProxyModel::E_COLUMN_DATE_OF_CLOSING

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

IscanderChe
  • 26 июля 2019 г. 7:16

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

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

Комментарии

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

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

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 25 октября 2024 г. 22:37

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

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 25 октября 2024 г. 22:29

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

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
i
innorwall14 ноября 2024 г. 8:42
Как Копировать Файлы в Linux If only females relatives with DZ offspring were considered these percentages were 23 order priligy online uk
i
innorwall14 ноября 2024 г. 6:09
Qt/C++ - Урок 068. Hello World с использованием системы сборки CMAKE в CLion ditropan pristiq dosing With the Yankees leading, 4 3, Rivera jogged in from the bullpen to a standing ovation as he prepared for his final appearance in Chicago buy priligy pakistan
i
innorwall14 ноября 2024 г. 1:05
EVILEG-CORE. Использование Google reCAPTCHA 2001; 98 29 34 priligy buy
i
innorwall14 ноября 2024 г. 1:00
PyQt5 - Урок 007. Работаем с QML QtQuick (Сигналы и слоты) priligy 30mg Am J Obstet Gynecol 171 1488 505
Сейчас обсуждают на форуме
i
innorwall14 ноября 2024 г. 0:39
добавить qlineseries в функции priligy amazon canada 93 GREB1 protein GREB1 AB011147 6
i
innorwall11 ноября 2024 г. 7:55
Всё ещё разбираюсь с кешем. priligy walgreens levitra dulcolax carbs The third ring was found to be made up of ultra relativistic electrons, which are also present in both the outer and inner rings
9
9Anonim25 октября 2024 г. 6:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…
ИМ
Игорь Максимов3 октября 2024 г. 1:05
Реализация навигации по разделам Спасибо Евгений!

Следите за нами в социальных сетях