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); }
А если так?
Хотелось бы посоветовать ещё одно улучшение в том случае, если будет делаться поддержка переводов.
Поскольку здесь имеется недостаток в том случае, если подключать переводы, то есть использование 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. Моэете там посмотреть.