IscanderChe
IscanderChe24 липня 2019 р. 08: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 р. 09: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 р. 06:06

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

Коментарі

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

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

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

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

  • Результат:80бали,
  • Рейтинг балів4
m
  • molni99
  • 26 жовтня 2024 р. 01:29

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

  • Результат:20бали,
  • Рейтинг балів-10
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Evgenii Legotckoi
Evgenii Legotckoi24 червня 2024 р. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 листопада 2024 р. 06:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject04 червня 2022 р. 03:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 жовтня 2024 р. 09:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Слідкуйте за нами в соціальних мережах