Schauen wir uns nun die interne Struktur des Trackers selbst und seine grafische Umgebung genauer an.
Dieser Teil ist einerseits wichtig, da Datenmodelle dazu dienen, von der Datenbank zur Nutzung von Daten zu gelangen. Andererseits ist es in Bezug auf die GUI-Generierung etwas langweilig.
Als ich anfing, an dem Projekt zu arbeiten, wusste ich noch nicht, wie man QSplitter mit Qt Desinger richtig installiert (hier ist ein Artikel, der ausführlich beschreibt, wie das trotzdem gemacht werden kann: https://evileg.com/ post /73/ ). Daher wurde das Design des Fensters durch Code geschrieben. Ich habe einen Abend gebraucht, um es herauszufinden. Und für dieses Projekt habe ich mich entschieden, alle Fenster, einschließlich Dialoge, durch Code zu schreiben. Anfangs war es mühsam, aber bei Dialogen wurde es bequemer: Die meisten Dialoge haben OK- und Abbrechen-Schaltflächen, und Teile des Codes, die die Schaltflächen selbst, ihre Platzierung, Signal-Slot-Verbindungen und die Slots selbst beschreiben, konnten einfach kopiert werden. Natürlich können Sie bei sehr einfachen Fragedialogen QMessageBox verwenden, aber ich war verärgert über das Erscheinen der letzten Fenster. Es kann notwendig sein, an den Größen herumzubasteln, damit die Phrasen schön angezeigt werden, aber ich wollte nicht. Aber das automatische Platzieren von Bauteilen direkt aus dem Code hat mir perfekt gepasst.
# 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
Die Dateien main.h und main.cpp enthalten einige Variablen, die in vielen Dateien verwendet werden. Als Ergebnis der Entwicklung der Datenbank wurden die Listen der Aufgabeneigenschaften von dort entfernt und in reguläre Listen von Zeichenfolgen verschoben.
// 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(); }
Gehen wir nun direkt zum 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
Betrachten Sie im Detail die Erstellung von Datenmodellen.
// 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); } }
Lassen Sie uns nun mit der Erstellung einer GUI fortfahren.
// 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(); }
Als Ergebnis erhalten wir das folgende Aussehen des Fensters.
In der Taskleiste erscheint ein Anwendungssymbol und es gibt ein Kontextmenü.
Betrachten Sie abschließend einen Slot, void TrackerServer::slotOpenArchive() , und die Dialogfeldklasse OpenArchiveDialog . In der Dialogbox ist im Gegensatz zum Hauptfenster QListWidget für die Anzeige der Projektliste zuständig, da die Implementierung einer Mehrfachauswahl von Elementen erforderlich war, und es ist recht einfach, Checkboxen in QListWidget zu setzen.
// 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. Моэете там посмотреть.