IscanderChe
IscanderChe24 июля 2019 г. 8:56

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

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

Эта часть, с одной стороны, важная, поскольку модели данных служат для перехода от базы данных к использованию данных. С другой стороны, она немного скучная, в части формирования 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);
}
Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

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

Evgenii Legotckoi
  • 24 июля 2019 г. 9:58

А если так?

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/ Просто код тимлида на работе скучно ревировать, поэтому столько комментариев и мыслей.

IscanderChe
  • 24 июля 2019 г. 12:00
extern QStringList statusList = {"не активна", "в работе", "закрыта"};

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

CONFIG += c++11

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

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

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

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

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

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

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

IscanderChe
  • 24 июля 2019 г. 12:19

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

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

IscanderChe
  • 24 июля 2019 г. 13:20

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

IscanderChe
  • 24 июля 2019 г. 14:55

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

IscanderChe
  • 24 июля 2019 г. 16:52

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

Evgenii Legotckoi
  • 25 июля 2019 г. 6:06

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
d
  • dsfs
  • 26 апреля 2024 г. 14:56

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

  • Результат:80баллов,
  • Очки рейтинга4
d
  • dsfs
  • 26 апреля 2024 г. 14:45

C++ - Тест 002. Константы

  • Результат:50баллов,
  • Очки рейтинга-4
d
  • dsfs
  • 26 апреля 2024 г. 14:35

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

  • Результат:73баллов,
  • Очки рейтинга1
Последние комментарии
k
kmssr9 февраля 2024 г. 5:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 12:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 21:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 19:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 8:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
G
Gar22 апреля 2024 г. 15:46
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil Academics20 апреля 2024 г. 17:45
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasov14 апреля 2024 г. 16:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 12:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 14:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

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