вторник, 1 июля 2014 г.

Qt: Дерево в Qt

Возникла необходимость построить дерево структурных подразделений в приложении Qt. Решение задачи решил реализовать с помощью виджета QTreeWidget, так как дерево не очень большое и редко (практически никогда) не изменяется. Вообще то создать дерево не представляет особого труда, но ввиду того того что к узлам дерева необходимо привязывать пользовательские объекты, приходится учитывать кое какие моменты. Ниже опишу как решается такого рода проблема имея Qt и СУБД FireBird 2.5.

Структура базы данных

Чтобы читатель имел понятие о том с какими данными приходится работать, кратко опишу структуру моей БД.

Имеется 3 таблицы следующего содержания:

Таблица: SEC_DEPARTMENTS
Поле Описание
DEP_ID Ид. подразделения
DEP_NAME Полное название
DESCRIPTION Описание
DEP_KSORT Сокращенное название
DEP_SKNAME Порядок сортировки
Таблица: SEC_SERVICES
Поле Описание
SERV_ID Ид. службы
DEP_ID Ид. подразделения
SERV_NAME Полное название
DESCRIPTION Описание
SERV_SKNAME Сокращенное название
Таблица: SEC_SECTORS
Поле Описание
SECT_ID Ид. сектора
SERV_ID Ид. службы
DEP_ID Ид. подразделения
SECT_NAME Полное название
DESCRIPTION Описание
SECT_SKNAME Сокращенное описание

В принципе с таблицами должно быть все понятно. Подразделение содержит службы, а служба содержит сектора.

Сборка драйвера QIBASE для Qt

Прежде чем приступать к работе с базой, необходимо собрать драйвер QIBASE. Как собрать можно почитать здесь. От себя только добавлю, что в моем случае (система windows 64 разряда) команды сборки были такими:

c:\Qt\qt-4.8.4\src\plugins\sqldrivers\ibase>qmake "INCLUDEPATH+=c:/Firebird_2_5/include" 
                             "LIBS+=c:/Firebird_2_5/WOW64/lib/fbclient_ms.lib" ibase.pro

mingw32-make -f Makefile.Debug

mingw32-make -f Makefile.Release

После успешной сборки я поместил два файла ...\debug\qsqlibased4.dll и ...\release\qsqlibase4.dll в папку c:\Qt\qt-4.8.4\plugins\sqldrivers\.

Извлечение набора данных для дерева

Чтобы упростить себе жизнь с формированием дерева, я решил извлекать данные вместе с уровнями узлов в дереве. При этом большая часть логики ложится на сервер БД, а это плюс. Сам запрос в виде хранимой процедуры выглядит таким образом:

CREATE OR ALTER PROCEDURE ENT_TREE
RETURNS (
    ENT_ID INTEGER,
    ENT_PARENT_ID INTEGER,
    ENT_NAME VARCHAR(300),
    ENT_SMALL_NAME VARCHAR(50),
    ENT_LEVEL VARCHAR(12)
)
AS
    DECLARE VARIABLE DEP_ID INTEGER;
    DECLARE VARIABLE SERV_ID INTEGER;
BEGIN
    ENT_ID = NULL;
    ENT_PARENT_ID = NULL;
    ENT_NAME = NULL;
    ENT_SMALL_NAME = NULL;
    ENT_LEVEL = NULL;
    FOR SELECT DEP_ID, 0, DEP_NAME, DEP_SKNAME, 0
        FROM SEC_DEPARTMENTS DEP
        WHERE DEP.DEP_KSORT < 9
        ORDER BY DEP.DEP_KSORT ASC
        INTO :ENT_ID, :ENT_PARENT_ID, :ENT_NAME, :ENT_SMALL_NAME, :ENT_LEVEL
        DO BEGIN
            DEP_ID = ENT_ID;
            SUSPEND;
            FOR SELECT SERV_ID, DEP_ID, SERV_NAME, SERV_SKNAME, 1
                FROM SEC_SERVICES SERV
                WHERE SERV.DEP_ID = :DEP_ID
                ORDER BY SERV.SERV_NAME COLLATE WIN1251_UA
                INTO :ENT_ID, :ENT_PARENT_ID, :ENT_NAME, :ENT_SMALL_NAME, :ENT_LEVEL
                DO BEGIN
                    SERV_ID = ENT_ID;
                    SUSPEND;
                    FOR SELECT SECT_ID, SERV_ID, SECT_NAME, SECT_SKNAME, 2
                        FROM SEC_SECTORS SECT
                        WHERE (SECT.DEP_ID = :DEP_ID) AND (SECT.SERV_ID = :SERV_ID)
                        ORDER BY SECT.SECT_NAME COLLATE WIN1251_UA
                        INTO :ENT_ID, :ENT_PARENT_ID, :ENT_NAME, :ENT_SMALL_NAME, :ENT_LEVEL
                        DO BEGIN
                            SUSPEND;
                        END
                END
        END
END

В FireBird 2.5 есть возможность выполнять хранимые процедуры как простые запросы к базе, используя EXECUTE BLOCK. Этим я и решил воспользоватся.

Формирование дерева

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

#ifndef TREEDATA_H
#define TREEDATA_H

#include <qvariant.h>

class TreeData
{

public:
    TreeData();
    TreeData(unsigned int level, unsigned int entId);
    TreeData(TreeData*);
    ~TreeData();
    unsigned int level();
    unsigned int entId();
    void setLevel(unsigned int level);
    void setEntId(unsigned int entId);

private:
    unsigned int _level;
    unsigned int _entId;

};

Q_DECLARE_METATYPE(TreeData*)

#endif // TREEDATA_H

Реализация геттеров и сеттеров так же как и конструкторов тривиальна и не нуждается в описании.

Теперь сам код для формирования дерева

#include <treedata.h>

//...

void MainWindow::setTreeDepartments()
{
    ui->treeDepartments->setColumnCount(1);
    QSqlQuery query("EXECUTE BLOCK "
                "RETURNS ( "
                    "ENT_ID INTEGER, "
                    "ENT_PARENT_ID INTEGER, "
                    "ENT_NAME VARCHAR(300), "
                    "ENT_SMALL_NAME VARCHAR(50), "
                    "ENT_LEVEL VARCHAR(12)) "
                    "AS "
                "DECLARE VARIABLE DEP_ID INTEGER; "
                "DECLARE VARIABLE SERV_ID INTEGER; "
                "BEGIN "
                    "ENT_ID = NULL; "
                    "ENT_PARENT_ID = NULL; "
                    "ENT_NAME = NULL; "
                    "ENT_SMALL_NAME = NULL; "
                    "ENT_LEVEL = NULL; "
                    " "
                    "FOR SELECT DEP_ID, 0, DEP_NAME, DEP_SKNAME, 0 "
                    "FROM SEC_DEPARTMENTS DEP "
                    "WHERE DEP.DEP_KSORT > 9 "
                    "ORDER BY DEP.DEP_KSORT ASC "
                    "INTO :ENT_ID, :ENT_PARENT_ID, :ENT_NAME, :ENT_SMALL_NAME, :ENT_LEVEL "
                    "DO BEGIN "
                        "DEP_ID = ENT_ID; "
                        "SUSPEND; "
                        " "
                        "FOR SELECT SERV_ID, DEP_ID, SERV_NAME, SERV_SKNAME, 1 "
                        "FROM SEC_SERVICES SERV "
                        "WHERE SERV.DEP_ID = :DEP_ID "
                        "ORDER BY SERV.SERV_NAME COLLATE WIN1251_UA "
                        "INTO :ENT_ID, :ENT_PARENT_ID, :ENT_NAME, :ENT_SMALL_NAME, :ENT_LEVEL "
                        "DO BEGIN "
                            "SERV_ID = ENT_ID; "
                            "SUSPEND; "
                            " "
                            "FOR SELECT SECT_ID, SERV_ID, SECT_NAME, SECT_SKNAME, 2 "
                            "FROM SEC_SECTORS SECT "
                            "WHERE (SECT.DEP_ID = :DEP_ID) AND (SECT.SERV_ID = :SERV_ID) "
                            "ORDER BY SECT.SECT_NAME COLLATE WIN1251_UA "
                            "INTO :ENT_ID, :ENT_PARENT_ID, :ENT_NAME, :ENT_SMALL_NAME, :ENT_LEVEL "
                            "DO BEGIN "
                                "SUSPEND; "
                            "END "
                        "END "
                    "END "
                "END ");

    QTreeWidgetItem *level0, *level1, *level2;
    while ( query.next() ){
        TreeData *td = new TreeData(query.value(4).toUInt(), query.value(0).toUInt());
        switch (query.value(4).toInt()) {
        case 0:{
            level0 = new QTreeWidgetItem;
            level0->setText(0, query.value(2).toString());
            level0->setData(0, Qt::UserRole, qVariantFromValue(td));
            ui->treeDepartments->addTopLevelItem(level0);
            break;
        }
        case 1:{
            level1 = new QTreeWidgetItem;
            level1->setText(0, query.value(2).toString());
            level1->setData(0, Qt::UserRole, qVariantFromValue(td));
            level0->addChild(level1);
            break;
        }
        case 2:{
            level2 = new QTreeWidgetItem;
            level2->setText(0, query.value(2).toString());
            level2->setData(0, Qt::UserRole, qVariantFromValue(td));
            level1->addChild(level2);
            break;
        }
        default:
            break;
        }
    }
}

Ниже описан слот для кнопки, по нажатию на которую я извлекаю данные и вывожу их в QMessageBox:

void MainWindow::btnTestClick()
{
    QList<QTreeWidgetItem*> selected = ui->treeDepartments->selectedItems();
    QVariant tdVariant = selected.at(0)->data(0, Qt::UserRole);
    TreeData *td = tdVariant.value<TreeData*>();
    QString res = QString("Level: %1; EntId: %2").arg(td->level()).arg(td->entId());
    QMessageBox::information(this, "Information", res);
}

Удачи!

Комментариев нет:

Отправить комментарий