Теперь подробно рассмотрим внутреннее устройство самого трекера и его графического окружения.
Эта часть, с одной стороны, важная, поскольку модели данных служат для перехода от базы данных к использованию данных. С другой стороны, она немного скучная, в части формирования 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); }
А если так?
Хотелось бы посоветовать ещё одно улучшение в том случае, если будет делаться поддержка переводов.
Поскольку здесь имеется недостаток в том случае, если подключать переводы, то есть использование QObject::tr().
В случае добавления поддержки переводов придётся делать статический метод у класса с поддержкой переводов в Qt.
Выглядит так
Header
Cpp
Ну или так...
А не думали ли наследоваться от QAbstractItemModel вместо QStandardItemModel например для проектов. А там внутри просто управлять вектором из например структур описания проекта. Что-то типо такого?
Последнее время Qt возвращается к std функционалу, вместо своих велосипедов, например QList вполне можно заменить на std::vector. Это так, размышления...
P/S/ Просто код тимлида на работе скучно ревировать, поэтому столько комментариев и мыслей.
Для этого, насколько я помню, требуется использовать такую конструкцию в pro-файле:
У меня её там нет.
То, что вы написали в Header и Cpp, требует построчных комментариев. :)) Я не совсем улавливаю, что там написано.
А вот за эту строчку спасибо!
Возможно, что наследоваться от QAbstractItemModel - более удобная идея. Мне просто боязно пока туда соваться... Так же, как было в первый раз боязно с делегатами работать.
"Это так, размышления..." Ну вот мне как раз предпочтительнее работать с их типами, чем с std. И итераторы в стиле Java выглядят более читабельными. (Тоже - в порядке растекания мыслию по древу. :) )
"поэтому столько комментариев и мыслей" Наоборот, спасибо за возможность посмотреть на свой код чужими глазами. Это всегда полезно.
Почитал про наследование как таковое. Это я ещё понять могу. А вот как туда структуру запихнуть, ума не приложу.
Кажется, придумал. Попробую - отпишусь.
Не, не получилось сходу проблему решить. Думал, можно вписаться с этой структурой в заявляемый QVariant. Не вышло.
Всё, разобрался, как QVariant связывается со struct. Через Q_DECLARE_METATYPE. А дальше через QVariant::fromValue и qvariant_cast.
В Qt Creator есть пример Animals что ли. Там есть таблица с животными и там как раз используется вектор или структур или обхектов класса Animal. Моэете там посмотреть.