IscanderChe
IscanderCheJuly 24, 2019, 6:56 p.m.

Simple Tracker project. Part 4: server. Data Models and GUI of the Main Application Window

Now let's take a closer look at the internal structure of the tracker itself and its graphical environment.

This part, on the one hand, is important, since data models serve to move from the database to the use of data. On the other hand, it is a bit boring in terms of GUI generation.

When I started working on the project, I still did not know how to properly install QSplitter using Qt Desinger (here is an article that describes in detail how this can still be done: https://evileg.com/post /73/ ). Therefore, the design of the window was written through code. It took me an evening to figure it out. And for this project, I decided to write all windows, including dialogs, through code. At first it was tedious, but it became more convenient on dialogs: most dialogs have OK and Cancel buttons, and parts of the code describing the buttons themselves, their placement, signal-slot connections and the slots themselves could be easily copied. Of course, on very simple question dialogs, you can use QMessageBox, but I was upset by the appearance of the final windows. It may be necessary to tinker with the sizes so that the phrases are displayed beautifully, but I did not want to. But the automatic placement of components directly from the code suited me perfectly.


# 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

The main.h and main.cpp files contain some variables that will be used in many files. As a result of the development of the database, the lists of task properties were removed from there and moved to regular lists of strings.

// 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();
}

Now let's go directly to the server.

// 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

Consider in detail the creation of data models.

// 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);
    }
}

Now let's move on to creating the 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();
}

As a result, we get the following appearance of the window.

An application icon appears in the tray and there is a context menu.

Finally, consider one slot, void TrackerServer::slotOpenArchive() , and the OpenArchiveDialog dialog box class. In the dialog box, unlike the main window, QListWidget is responsible for displaying the list of projects, since the implementation of multiple selection of elements was required, and it is quite simple to set checkboxes in 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);
}
We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

Evgenii Legotckoi
  • July 24, 2019, 7:58 p.m.

А если так?

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
  • July 24, 2019, 10 p.m.
extern QStringList statusList = {"не активна", "в работе", "закрыта"};

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

CONFIG += c++11

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

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

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

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

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

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

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

IscanderChe
  • July 24, 2019, 10:19 p.m.

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

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

IscanderChe
  • July 24, 2019, 11:20 p.m.

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

IscanderChe
  • July 25, 2019, 12:55 a.m.

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

IscanderChe
  • July 25, 2019, 2:52 a.m.

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

Evgenii Legotckoi
  • July 25, 2019, 4:06 p.m.

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

Comments

Only authorized users can post comments.
Please, Log in or Sign up
B

C++ - Test 002. Constants

  • Result:16points,
  • Rating points-10
B

C++ - Test 001. The first program and data types

  • Result:46points,
  • Rating points-6
FL

C++ - Test 006. Enumerations

  • Result:80points,
  • Rating points4
Last comments
k
kmssrFeb. 9, 2024, 5:43 a.m.
Qt Linux - Lesson 001. Autorun Qt application under Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lesson 007. Working with ICMP Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVADec. 25, 2023, 9:30 p.m.
Boost - static linking in CMake project under Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJoDec. 25, 2023, 7:38 p.m.
Boost - static linking in CMake project under Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
GvozdikDec. 19, 2023, 8:01 a.m.
Qt/C++ - Lesson 056. Connecting the Boost library in Qt for MinGW and MSVC compilers Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Now discuss on the forum
AC
Alexandru CodreanuJan. 19, 2024, 10:57 p.m.
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…
BlinCT
BlinCTDec. 27, 2023, 7:57 p.m.
Растягивать Image на парент по высоте Ну и само собою дял включения scrollbar надо чтобы был Flickable. Так что выходит как то так Flickable{ id: root anchors.fill: parent clip: true property url linkFile p…
Дмитрий
ДмитрийJan. 10, 2024, 3:18 p.m.
Qt Creator загружает всю оперативную память Проблема решена. Удалось разобраться с помощью утилиты strace. Запустил ее: strace ./qtcreator Начал выводиться весь лог работы креатора. В один момент он начал считывать фай…
Evgenii Legotckoi
Evgenii LegotckoiDec. 12, 2023, 5:48 p.m.
Побуквенное сравнение двух строк Добрый день. Там случайно не высылается этот сигнал textChanged ещё и при форматировани текста? Если решиать в лоб, то можно просто отключать сигнал/слотовое соединение внутри слота и …

Follow us in social networks