IscanderChe
IscanderCheJuly 24, 2019, 8:56 a.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, 9:58 a.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, noon
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, 12:19 p.m.

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

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

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

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

IscanderChe
  • July 24, 2019, 2:55 p.m.

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

IscanderChe
  • July 24, 2019, 4:52 p.m.

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

Evgenii Legotckoi
  • July 25, 2019, 6:06 a.m.

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

Comments

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

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

  • Result:40points,
  • Rating points-8
AD

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 8:51 a.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiOct. 31, 2024, 11:37 a.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 5:19 a.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 4:51 a.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 8:02 a.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
Evgenii Legotckoi
Evgenii LegotckoiJune 24, 2024, 12:11 p.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 3:04 a.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 12:49 a.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimOct. 25, 2024, 6:10 a.m.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Follow us in social networks