Проект Simple Tracker. Часть 4: сервер. Модели данных и GUI основного окна приложения

Iscander Che, Simple Tracker, Qt, C++

Теперь подробно рассмотрим внутреннее устройство самого трекера и его графического окружения.

Эта часть, с одной стороны, важная, поскольку модели данных служат для перехода от базы данных к использованию данных. С другой стороны, она немного скучная, в части формирования GUI.

Когда я начал работу над проектом, я ещё не знал, как правильно с помощью Qt Desinger установить QSplitter (вот статья, где подробно описано, как это всё-таки можно сделать: https://evileg.com/post/73/ ). Поэтому дизайн окна писал через код. Пришлось потратить вечер, чтобы немного разобраться. И на этот проект я решил писать все окна, в том числе и диалоги, через код. Вначале это было утомительно, но на диалогах стало удобнее: большинство диалогов имеют кнопки ОК и Cancel, и части кода, описывающие сами кнопки, их размещение, сигнально-слотовые соединения и сами слоты можно было спокойно копировать. Конечно, на совсем простеньких диалогах-вопросах можно использовать QMessageBox, но меня огорчил внешний вид итоговых окон. Возможно, надо повозиться с размерами, чтобы фразы отображались красиво, но я не захотел. Зато автоматическое размещение компонентов прямо из кода меня вполне устроило.

# ICTrackerServer.pro

include(../common.pri)

# Подключаем модуль network для реализации QLocalServer
# и модель sql для реализации моделей SQL-таблиц
QT       += core gui network sql

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = ICTrackerServer
TEMPLATE = app

DEFINES += QT_DEPRECATED_WARNINGS

SOURCES += \
        ...

HEADERS += \
        ...

# В файле ресурсов хранятся изображения для пунктов контекстного меню трея
RESOURCES += \
    ictrackerserver.qrc

В файлах main.h и main.cpp введены некоторые переменные, которые будут использоваться во многих файлах. По итогам разработки базы данных списки свойств задач были удалены оттуда и перенесены в обычные списки строк.

// main.h

#ifndef MAIN_H
#define MAIN_H

#include "database.h"
#include <QtCore>

extern DataBase* database;

extern QStringList statusList;
extern QStringList colorList;
extern QStringList typeList;

#endif // MAIN_H
// main.cpp

#include "main.h"
#include "trackerserver.h"
#include <QApplication>

DataBase* database;

QStringList statusList;
QStringList colorList;
QStringList typeList;

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);

    // Инициализируем базу данных
    database = new DataBase;

    // Инициализируем списки состояний, цветов и типов задач
    statusList << "не активна" << "в работе" << "закрыта";
    colorList << "#ffffff" << "#fffe04" << "#8fd244";
    typeList << "bug" << "feature" << "issue" << "milestone";

    QCoreApplication::setApplicationName("ICTracker");

    TrackerServer trackerServer;

    // После закрытия главного окна программа не завершается, остаётся иконка в трее
    QApplication::setQuitOnLastWindowClosed(false);

    return app.exec();
}

Теперь перейдём непосредственно к серверу.

// trackerserver.h

#ifndef TRACKERSERVER_H
#define TRACKERSERVER_H

#include "customsortfilterproxymodel.h"
#include "comboboxdelegate.h"

#include <QWidget>
#include <QSqlTableModel>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QLocalServer>
#include <QLocalSocket>
#include <QGroupBox>
...
#include <QAction>
#include <QSystemTrayIcon>
#include <QMenu>

class TrackerServer : public QWidget
{
    Q_OBJECT

public:
    TrackerServer(QWidget* parent = 0);
    ~TrackerServer();

private:

    // Объявляем модели данных
    QSqlTableModel* projectTableModel;
    QSqlTableModel* taskSqlTableModel;
    CustomSortFilterProxyModel* taskFilterModel;
    QStandardItemModel* projectListModel;

    // Идентификатор текущего проекта
    int projectCurrentId;

    // Объявляем сервер и необходимые переменные
    QLocalServer* localServer;
    quint16 nextBlockSize;
    QString nameServer;
    QString clientProjectName;
    int clientTaskNumber;
    int clientRevision;

    // Объявляем виджеты проектов
    ...

    // Объявляем виджеты задач
    ...

    // Объявляем виджеты окна
    ...

    // Объявляем действия
    QAction* actionQuit;
    QAction* actionSettings;
    QAction* actionOpenWindow;

    // Объявляем иконку трея и контекстное меню
    QSystemTrayIcon* trayIcon;
    QMenu* contextMenu;

    // Метод создания моделей данных
    void createModels();

    // Метод обновления модели данных для виджета проектов
    void updateProjectListModel();

    // Метод создания визуального интерфейса
    void createGUI();

    // Метод обновления виджета проектов
    void updateProjectView();

    // Метод обновления виджета задач
    void updateTaskView();

    // Метод создания и запуска сервера
    void createServer();

    // Метод создания подключений
    void createConnections();

private slots:
    // Большинство слотов мы рассмотрим позднее
    ...
    // Слот открытия архива
    void slotOpenArchive();

    ...
};

#endif // TRACKERSERVER_H

Рассмотрим подробно создание моделей данных.

// trackerserver.cpp

#include "trackerserver.h"
#include "newedittaskdialog.h"
#include "deletetaskdialog.h"
#include "newprojectdialog.h"
#include "openarchivedialog.h"
#include "main.h"

#include <QDebug>
#include <QStandardItem>
#include <QSqlRecord>
#include <QMessageBox>
#include <QDataStream>
#include <QByteArray>
#include <QIcon>
#include <QHeaderView>
#include <QSharedPointer>

TrackerServer::TrackerServer(QWidget* parent)
    : QWidget(parent)
{
    // Задаём имя сервера
    nameServer = "TrackerServer";

    // Устанавливаем начальное значение идентификатора текущего проекта
    projectCurrentId = 0;

    // Создаём модели
    createModels();
    // Создаём сервер
    createServer();
    // Создаём визуальный интерфейс
    createGUI();
    // Создаём подключения
    createConnections();
}

TrackerServer::~TrackerServer()
{
    localServer->close();
}

// Метод создания моделей данных
void TrackerServer::createModels()
{
    // Инициализируем таблицу задач
    taskSqlTableModel = new QSqlTableModel;
    taskSqlTableModel->setTable(taskTable);
    taskSqlTableModel->select();

    taskFilterModel = new CustomSortFilterProxyModel;
    taskFilterModel->setSourceModel(taskSqlTableModel);
    taskFilterModel->setFilterFixedString(QString("%1").arg(projectCurrentId));
    taskFilterModel->setFilterKeyColumn(1);

    taskFilterModel->setHeaderData(0, Qt::Horizontal, tr("№"));
    taskFilterModel->setHeaderData(1, Qt::Horizontal, tr("Идентификатор проекта"));
    taskFilterModel->setHeaderData(2, Qt::Horizontal, tr("Дата"));
    taskFilterModel->setHeaderData(3, Qt::Horizontal, tr("Тип"));
    taskFilterModel->setHeaderData(4, Qt::Horizontal, tr("Описание"));
    taskFilterModel->setHeaderData(5, Qt::Horizontal, tr("Состояние"));
    taskFilterModel->setHeaderData(6, Qt::Horizontal, tr("Дата закрытия"));
    taskFilterModel->setHeaderData(7, Qt::Horizontal, tr("Ревизия"));

    // Инициализируем таблицу проектов
    projectTableModel = new QSqlTableModel;
    projectTableModel->setTable(projectTable);

    updateProjectListModel();
}

// Метод обновления модели данных для виджета проектов
void TrackerServer::updateProjectListModel()
{
    // Обновляем данные SQL-модели
    projectTableModel->select();

    int numberRowsProject = projectTableModel->rowCount();
    int numberColumnsProject = 1;

    // Создаём модель для загрузки таблицы проектов в QListView
    projectListModel = new QStandardItemModel(numberRowsProject, numberColumnsProject);
    for(int row = 0; row < numberRowsProject; ++row)
    {
        QStandardItem* item = new QStandardItem();
        // Извлекаем данные из QSqlTableModel
        QString projectName =
            projectTableModel->record(row).value(visibleNameProjectCol).toString();
        QString visibleProjectName = projectName;
        int projectId =
            projectTableModel->record(row).value(idProjectCol).toInt();
        int projectManual =
            projectTableModel->record(row).value(manualPropertyCol).toInt();
        int projectArch =
            projectTableModel->record(row).value(archivePropertyCol).toInt();

        // Формируем видимое имя проекта
        if(projectManual == ProjectVCS::WithoutVCS)
            visibleProjectName = "(M) " + projectName;

        // Загружаем данные в элемент
        item->setData(visibleProjectName, Qt::DisplayRole);
        item->setData(projectName, Qt::UserRole + 1);
        item->setData(projectId, Qt::UserRole + 2);
        item->setData(projectManual, Qt::UserRole + 3);
        item->setData(projectArch, Qt::UserRole + 4);

        // Загружаем элемент в модель
        projectListModel->setItem(row, item);
    }
}

Теперь перейдём к созданию GUI.

// trackerserver.cpp

// Метод создания визуального интерфейса
void TrackerServer::createGUI()
{
    // Создаём и настраиваем список проектов
    ...

    // Создаём кнопки управления проектами
    ...

    // Формируем внешний вид блока проектов
    ...

    // Создаём и настраиваем таблицу задач
    ...

    // Создаём кнопки управления задачами
    ...

    // Формируем внешний вид блока задач
    ...

    // Создаём и настраиваем группы проектов и задач
    ...

    // Создаём и настраиваем сплиттер
    ...

    // Формируем главный вид окна приложения
    ...

    // Создаём иконку и действие для выхода из приложения
    const QIcon iconQuit = QIcon(":/images/exit.png");
    actionQuit = new QAction(tr("Выход"), this);
    actionQuit->setIcon(iconQuit);

    // Создаём иконку и действие для открытия окна настроек приложения
    const QIcon iconSettings = QIcon(":/images/settings.png");
    actionSettings = new QAction(tr("Настройки"), this);
    actionSettings->setIcon(iconSettings);

    // Создаём иконку и действие для открытия главного окна приложения
    const QIcon iconOpenWindow = QIcon(":/images/openwindow.png");
    actionOpenWindow = new QAction(tr("Открыть трекер"), this);
    actionOpenWindow->setIcon(iconOpenWindow);

    // Создаём и настраиваем контекстное меню
    contextMenu = new QMenu(this);
    contextMenu->addAction(actionOpenWindow);
    contextMenu->addAction(actionSettings);
    contextMenu->addSeparator();
    contextMenu->addAction(actionQuit);

    // Создаём и настраиваем иконку трея
    trayIcon = new QSystemTrayIcon(this);
    trayIcon->setContextMenu(contextMenu);
    trayIcon->setToolTip(tr("ICTracker"));
    trayIcon->setIcon(QPixmap(":/images/tracker.png"));

    trayIcon->show();
}

В итоге получаем следующий внешний вид окна.

В трее появляется иконка приложения и имеется контекстное меню.

И в заключение рассмотрим один слот, void TrackerServer::slotOpenArchive() и класс диалогового окна OpenArchiveDialog . В диалоговом окне, в отличие от основного окна, отображением списка проектов занимается QListWidget , поскольку требовалась реализация множественного выбора элементов, а в QListWidget установить чекбоксы довольно просто.

// trackerserver.cpp

// Слот открытия архива
void TrackerServer::slotOpenArchive()
{
    QSharedPointer<OpenArchiveDialog> dialog(new OpenArchiveDialog(projectListModel));
    // Если подтверждено извлечение из архива проекта/проектов
    if(dialog->exec() == QDialog::Accepted)
    {
        // Получаем список id извлекаемых из архива проектов
        QList<int> projectIdList = dialog->getListProjectId();
        int lastId = projectIdList.last();
        // Извлекаем проекты из архива
        foreach(int id, projectIdList)
            database->extractProject(id);

        // Обновляем модель списка проектов
        updateProjectListModel();
        // Обновляем виджет списка проектов
        updateProjectView();

        int numberRows = projectListModel->rowCount();

        // Определяем индес последнего извлечённого проекта
        QModelIndex lastIndex;
        for(int row = 0; row < numberRows; ++row)
        {
            QStandardItem* item = projectListModel->item(row);
            int projectId = item->data(Qt::UserRole + 2).toInt();
            if(projectId == lastId)
                lastIndex = projectListModel->indexFromItem(item);
        }

        // Устанавливаем текущим последний извлечённый проект
        slotSetCurrentProject(lastIndex);
    }
}
// openarchivedialog.h

// Слот открытия архива
void TrackerServer::slotOpenArchive()
{
    QSharedPointer<OpenArchiveDialog> dialog(new OpenArchiveDialog(projectListModel));
    // Если подтверждено извлечение из архива проекта/проектов
    if(dialog->exec() == QDialog::Accepted)
    {
        // Получаем список id извлекаемых из архива проектов
        QList<int> projectIdList = dialog->getListProjectId();
        int lastId = projectIdList.last();
        // Извлекаем проекты из архива
        foreach(int id, projectIdList)
            database->extractProject(id);

        // Обновляем модель списка проектов
        updateProjectListModel();
        // Обновляем виджет списка проектов
        updateProjectView();

        int numberRows = projectListModel->rowCount();

        // Определяем индес последнего извлечённого проекта
        QModelIndex lastIndex;
        for(int row = 0; row < numberRows; ++row)
        {
            QStandardItem* item = projectListModel->item(row);
            int projectId = item->data(Qt::UserRole + 2).toInt();
            if(projectId == lastId)
                lastIndex = projectListModel->indexFromItem(item);
        }

        // Устанавливаем текущим последний извлечённый проект
        slotSetCurrentProject(lastIndex);
    }
}
// openarchivedialog.cpp

#include "openarchivedialog.h"
#include <QStandardItem>

OpenArchiveDialog::OpenArchiveDialog(QStandardItemModel* model, QWidget* parent) :
    QDialog(parent)
{
    // Устанавливаем дизайн диалогового окна
    setWindowTitle(tr("Разархивировать проект(ы)"));

    label = new QLabel(tr("Выберите проект(ы) для разархивации:"), this);

    projectListWidget = new QListWidget(this);

    OKButton = new QPushButton(tr("OK"), this);
    OKButton->setDisabled(true);
    cancelButton = new QPushButton(tr("Отмена"), this);

    buttonsLayout = new QHBoxLayout;
    buttonsLayout->addWidget(OKButton);
    buttonsLayout->addWidget(cancelButton);

    mainLayout = new QVBoxLayout;
    mainLayout->addWidget(label);
    mainLayout->addWidget(projectListWidget);
    mainLayout->addLayout(buttonsLayout);

    setLayout(mainLayout);

    int numberRows = model->rowCount();
    for(int row = 0; row < numberRows; ++row)
    {
        // Извлекаем данные из переданной модели
        QStandardItem* modelItem = new QStandardItem();
        modelItem = model->item(row);
        QString visibleProjectName =
            modelItem->data(Qt::DisplayRole).toString();
        int projectId =
            modelItem->data(Qt::UserRole + 2).toInt();
        int projectArch =
            modelItem->data(Qt::UserRole + 4).toInt();

        if(projectArch == ProjectArchive::Archive)
        {
            // Записываем данные в виджет списка
            QListWidgetItem* widgetItem = new QListWidgetItem();
            widgetItem->setText(visibleProjectName);
            widgetItem->setCheckState(Qt::Unchecked);
            projectListWidget->addItem(widgetItem);
            listProjectId << projectId;
        }
    }

    connect(OKButton, &QPushButton::clicked,
            this, &OpenArchiveDialog::clickedOKButton);
    connect(cancelButton, &QPushButton::clicked,
            this, &OpenArchiveDialog::clickedCancelButton);

    // По изменению элемента списка проверяем его состояние
    connect(projectListWidget, &QListWidget::itemChanged,
            this, &OpenArchiveDialog::checkItemState);
}

OpenArchiveDialog::~OpenArchiveDialog()
{
}

// Возвращаем список id выбранных проектов
QList<int> OpenArchiveDialog::getListProjectId()
{
    return listCheckedProjectId;
}

// Формируем список выбранных проектов
void OpenArchiveDialog::clickedOKButton()
{
    int numberRows = projectListWidget->count();

    for(int row = 0; row < numberRows; ++row)
    {
        QListWidgetItem* item = new QListWidgetItem();
        item = projectListWidget->item(row);
        if(item->checkState() == Qt::Checked)
            listCheckedProjectId << listProjectId.at(row);
    }

    emit accept();
}

void OpenArchiveDialog::clickedCancelButton()
{
    emit reject();
}

// Проверяем, выбраны ли элементы списка
void OpenArchiveDialog::checkItemState(QListWidgetItem* /* item */)
{
    int numberRows = projectListWidget->count();

    int checkedItems = 0;

    for(int row = 0; row < numberRows; ++row)
    {
        QListWidgetItem* item = new QListWidgetItem();
        item = projectListWidget->item(row);
        if(item->checkState() == Qt::Checked)
            ++checkedItems;
    }

    // Если выбран хотя бы один, кнопка ОК доступна.
    if(checkedItems > 0)
        OKButton->setEnabled(true);
    else
        OKButton->setDisabled(true);
}
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

А если так?

extern QStringList statusList = {"не активна", "в работе", "закрыта"};

Хотелось бы посоветовать ещё одно улучшение в том случае, если будет делаться поддержка переводов.
Поскольку здесь имеется недостаток в том случае, если подключать переводы, то есть использование QObject::tr().
В случае добавления поддержки переводов придётся делать статический метод у класса с поддержкой переводов в Qt.

Выглядит так

Header

class StandardMessages final
{
    Q_DECLARE_TR_FUNCTIONS(authorization::StandardMessages)

public:
    StandardMessages() = delete;
    StandardMessages(const StandardMessages&) = delete;
    StandardMessages& operator= (const StandardMessages&) = delete;

    static QString getDemoMessage();
};

Cpp

QString StandardMessages::getDemoMessage()
{
    return tr("Demo version."); 
}

Ну или так...

extern QStringList statusList = {QObject::tr("не активна"), QObject::tr("в работе"), QObject::tr("закрыта")};

А не думали ли наследоваться от QAbstractItemModel вместо QStandardItemModel например для проектов. А там внутри просто управлять вектором из например структур описания проекта. Что-то типо такого?

struct Project
{
    QString name;
    int status;
}

Последнее время Qt возвращается к std функционалу, вместо своих велосипедов, например QList вполне можно заменить на std::vector. Это так, размышления...

P/S/ Просто код тимлида на работе скучно ревировать, поэтому столько комментариев и мыслей.

extern QStringList statusList = {"не активна", "в работе", "закрыта"};

Для этого, насколько я помню, требуется использовать такую конструкцию в pro-файле:

CONFIG += c++11

У меня её там нет.

То, что вы написали в Header и Cpp, требует построчных комментариев. :)) Я не совсем улавливаю, что там написано.

А вот за эту строчку спасибо!

extern QStringList statusList = {QObject::tr("не активна"), QObject::tr("в работе"), QObject::tr("закрыта")};

Возможно, что наследоваться от QAbstractItemModel - более удобная идея. Мне просто боязно пока туда соваться... Так же, как было в первый раз боязно с делегатами работать.

"Это так, размышления..." Ну вот мне как раз предпочтительнее работать с их типами, чем с std. И итераторы в стиле Java выглядят более читабельными. (Тоже - в порядке растекания мыслию по древу. :) )

"поэтому столько комментариев и мыслей" Наоборот, спасибо за возможность посмотреть на свой код чужими глазами. Это всегда полезно.

А не думали ли наследоваться от QAbstractItemModel вместо QStandardItemModel например для проектов. А там внутри просто управлять вектором из например структур описания проекта. Что-то типо такого?

Почитал про наследование как таковое. Это я ещё понять могу. А вот как туда структуру запихнуть, ума не приложу.

Кажется, придумал. Попробую - отпишусь.

Не, не получилось сходу проблему решить. Думал, можно вписаться с этой структурой в заявляемый QVariant. Не вышло.

Всё, разобрался, как QVariant связывается со struct. Через Q_DECLARE_METATYPE. А дальше через QVariant::fromValue и qvariant_cast.

В Qt Creator есть пример Animals что ли. Там есть таблица с животными и там как раз используется вектор или структур или обхектов класса Animal. Моэете там посмотреть.

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, 11:44 a.m.
Evgenij Legotskoj

Ну вообще это я вам не решение вашей задачи кинул, а просто как пример... Регулярку вам надо было бы самому придумать.. Ну вот так будет работать TextField { validator: RegExpValida…
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