link7242 link7243 link7244 link7245 link7246 link7247 link7248 link7249 link7250 link7251 link7252 link7253 link7254 link7255 link7256 link7257 link7258 link7259 link7260 link7261 link7262 link7263 link7264 link7265 link7266 link7267 link7268 link7269 link7270 link7271 link7272 link7273 link7274 link7275 link7276 link7277 link7278 link7279 link7280 link7281 link7282 link7283 link7284 link7285 link7286 link7287 link7288 link7289 link7290 link7291 link7292 link7293 link7294 link7295 link7296 link7297 link7298 link7299 link7300 link7301 link7302 link7303 link7304 link7305 link7306 link7307 link7308 link7309 link7310 link7311 link7312 link7313 link7314 link7315 link7316 link7317 link7318 link7319 link7320 link7321 link7322 link7323 link7324 link7325 link7326 link7327 link7328 link7329 link7330 link7331 link7332 link7333 link7334 link7335 link7336 link7337 link7338 link7339 link7340 link7341 link7342 link7343 link7344 link7345 link7346 link7347 link7348 link7349 link7350 link7351 link7352 link7353 link7354 link7355 link7356 link7357 link7358 link7359 link7360 link7361 link7362 link7363 link7364 link7365 link7366 link7367 link7368 link7369 link7370 link7371 link7372 link7373 link7374 link7375 link7376 link7377 link7378 link7379 link7380 link7381 link7382 link7383
http://rxlib.ru  

   | Уроки |



Иерархические структуры в Базе Данных FireBird и работа с ними из C++Builder.

 
В Интернете нашел ряд статей, посвященных этой теме, попытался реализовать то, что в них написано, понял что эти примеры очень не полные, дополнил, переработал их, и вот что из этого получилось.
Для написания этого примера, исходил из того, что полностью формировать и показывать дерево целиком вряд-ли когда может понадобиться, особенно если в таблице, содержащей иерархические данные несколько тысяч или десятков тысяч данных. Хотя предусмотрел и полное рисование дерева. Предусмотрены следующие возможности — формирование дерева на основании таблицы базы данных, редактирование,
добавление, удаление узлов дерева, быстрый поиск и перерисовка нужной части дерева, перенос мышкой узлов дерева.

Итак, исходные данные — база данных FireBird, отображать дерево будем в стандартном компоненте TTreeView,
работать с базой через компоненты доступа FIBPLus, хотя разумеется можно организовать работу и через стандартные компоненты доступа.

Проектирование базы данных.

База данных — FireBird, то есть поддерживает механизм триггеров и хранимых процедур, которые мы и будем использовать. Структура таблицы, содержащей иерархические данные стандартная, и у нас в примере будет следующая — таблица "Изделия и детали":

CREATE TABLE DETAIL (
ID     INTEGER NOT NULL,
PARENT INTEGER,
CCOUNT INTEGER DEFAULT 0,
NAME   VARCHAR(60) COLLATE PXW_CYRL
);
ALTER TABLE DETAIL ADD CONSTRAINT
PK_DETAIL PRIMARY KEY (ID);
ALTER TABLE DETAIL ADD CONSTRAINT
FK_DETAIL FOREIGN KEY (PARENT)
              REFERENCES DETAIL (
ID) ON DELETE CASCADE ON UPDATE CASCADE;

Итак, с полями ID и PARENT все понятно — поле ID — первичный ключ таблицы, поле PARENT показывает родителя для данной детали и является внешним ключем этой же таблицы на поле ID. Кроме того, обратите внимание, что поле PARENT не задано как NOT NULL, то есть может быть NULL — таковым у нас и будет самая верхняя запись таблицы — "Изделия и детали".
Также обратите внимание на не очень очевидное поле CCOUNT — количество прямых потомков у данной записи, которое очень облегчит нам жизнь в дальнейшем. Постараемся переложить максимально большее количество правил базы данных, проверок и ограничений на саму базу данных — меньше придется писать в клиентском приложении и меньше ошибок при этом мы сможем допустить.
Итак — в поле CCOUNT у нас будет количество прямых потомков у нашей записи, и следить за этим полем мы поручим самой базе данных с помощью 3 триггеров(см. ниже).

Поскольку поле ID — ключ нашей таблицы, это поле будет AUTOINC,
создадим в базе данных для него генератор

CREATE GENERATOR GEN_DETAIL_ID;
SET GENERATOR GEN_DETAIL_ID TO 0;

Cоздадим исключение для вершины дерева.

CREATE EXCEPTION E_DET 'Эту запись удалить нельзя !'; 

Создадим 3 триггера для таблицы DETAIL:

CREATE TRIGGER DETAIL_BI0 FOR DETAIL
ACTIVE BEFORE INSERT POSITION 0
AS
begin
UPDATE DETAIL
D SET D.CCOUNT = D.CCOUNT + 1 WHERE D.ID = new.PARENT;
end


CREATE TRIGGER DETAIL_BD0 FOR DETAIL
ACTIVE BEFORE DELETE POSITION 0
AS
begin
if(OLD.
ID = 1) then exception "E_DET";
UPDATE DETAIL
D SET D.CCOUNT = D.CCOUNT — 1 WHERE D.ID = old.PARENT;
end


CREATE TRIGGER DETAIL_BU0 FOR DETAIL
ACTIVE BEFORE UPDATE POSITION 0
AS
begin
if (old.
PARENT <> new.PARENT) then
begin
UPDATE DETAIL
D SET D.CCOUNT = D.CCOUNT — 1 WHERE D.ID = old.PARENT;
UPDATE DETAIL
D SET D.CCOUNT = D.CCOUNT +1 WHERE D.ID = new.PARENT;
end
end

Вставим в таблицу первую запись — вершину нашего дерева:

Insert into DETAIL (ID, NAME) values(1, 'Изделия и детали')

эта запись будет единственной вечной вершиной, удалить ее мы не дадим, для этого
и было создано исключение и внесена соответствующая проверка в триггер перед удалением записи.

Реализуем несколько хранимых процедур, для работы с иерархическими стркутурами, которые и будем использовать в работе:

1 Хранимая процедура GETCHILDS выберет из таблицы всех потомков указанной записи.

CREATE PROCEDURE GETCHILDS (ID_P INTEGER)
RETURNS (
ID_CHILD INTEGER)
AS
begin
       FOR SELECT
D.ID FROM DETAIL D
       WHERE
D.PARENT = :ID_P INTO :ID_CHILD
DO
begin
     suspend;
     IF(NOT EXISTS (SELECT * FROM DETAIL WHERE DETAIL.
ID = :ID_CHILD)) THEN
           BEGIN SUSPEND; END
     ELSE
           BEGIN
                FOR SELECT
ID_CHILD FROM GETCHILDS(:ID_CHILD) INTO :ID_CHILD
           DO BEGIN SUSPEND; END
     END
end
end

В качестве входного параметра она получает ID интересующей записи, и возвращает для нее список ID всех ее детей.

2. Хранимая процедура GETPARENTS выберет из таблицы всех родителей указанной записи вплоть до самой вершины (у нас это первая запись таблицы "Изделия и детали"), со всеми их данными.

ALTER PROCEDURE GETPARENTS (ID INTEGER)
RETURNS (
DID INTEGER,OID INTEGER,NAME VARCHAR(60),CCOUNT INTEGER)
AS
BEGIN
        WHILE (:
ID > 0) DO
            BEGIN
                 SELECT
O.ID, O.PARENT, O.NAME, O.CCOUNT
                 FROM DETAIL
O
                 WHERE
O.ID = :ID
                 INTO
:DID, :OID, :NAME , :CCOUNT;
                  ID = :OID;
                  SUSPEND;
            END
END

3. Хранимая процедура CHECK1_TREE проверяет, является-ли данная запись родителем некоторой другой записи, эта процедура нам понадобиться для проверки возможности перетаскивания записей в дереве при помощи технологии Drag and Drop

CREATE PROCEDURE CHECK1_TREE (S_ID INTEGER,D_ID INTEGER)
RETURNS (
IS_CHILD INTEGER)
AS
DECLARE VARIABLE
DID INTEGER;
begin
For Select
ID_CHILD from GETCHILDS(:D_ID) into :DID
do begin
if(
DID = S_ID) then
begin
IS_CHILD = 1;
suspend;
exit;
end
end
IS_CHILD = 0;
suspend;
end

То есть, если запись D_ID является родителем для записи S_ID, то в результате выполнения этой хранимой процедуры будет возвращено 1, если не является — будет возвращен 0.

Проектирование приложений.

Ну, вроде с базой данных разобрались. Теперь перейдем непосредственно к приложению на C++Builder
Разместите на форме компоненты TpFIBDatabase, TpFIBTransaction, 2 компонента типа
TpFIBDataSet. Свяжите их между собой и подключитесь к базе.
Компонент типа TpFIBDataSet работающий с таблицей DETAIL назовем Detail, назовем второй компонент типа TpFIBDataSet FTree.

Для вершины дерева, которое не имеет ничего над собой, поле PARENT равно NULL.
Формировать уровни дерева мы будем с помощью запроса к таблице (компонент FTree)

Разместите на форме компоненты TTreeView и TImageList, компонент для работы с деревом типа TTreeView назовем Tree, компонент типа TImageList назовите IList. В свойстве Images у Tree задайте IList
Поместите в IList 5 разных иконок, иконка с индексом 0 — будет иметь вид папки, с индексами 4 и 5 — для листьев дерева 5 — обычная иконка, 4 — при выделении данного листа.

Заполним свойства компонента Detail:
SelectSQL: Select * from DETAIL order by ID

и сгенерим остальные: UpdateSQL , InsertSQL, DeleteSQL, RefreshSQL
Заполним свойства AutoUpdateOptions компонента Detail

GeneratorName = GEN_DETAIL_ID
KeyFields = ID
Update Table = Detail
WhenGetGenID = wgOnNewRecord


// Откроем таблицу, нарисуем вершину дерева
void __fastcall
TForm1::FormShow(TObject *Sender)
{
   Detail->Open();
   ExpandLevel(NULL, -1); // рисуем дерево — верхний уровень
}

//---------------------------------------------------------------------------


Итак, мы решили, что считывать и формировать все дерево сразу нет смысла, значит будем делать это частями по мере необходимости — по запросу пользователя, то есть по счелчку на [+] у нужного узла дерева. Для этого напишем функцию раскрытия узла дерева
ExpandLevel

// раскрыть указанный уровень
TTreeNode* __fastcall TForm1::ExpandLevel(TTreeNode* Node, int searchID)
{
TTreeNode* TreeNode, *searchNode=NULL;
int ID = (Node == NULL)? 0 : (int)Node->Data;

AnsiString sql = "Select * from DETAIL where PARENT ";
if(ID) sql = sql + "= "+IntToStr(ID); else sql = sql + "is NULL";
FTree->Close();
FTree->SelectSQL->Clear(); FTree->SelectSQL->Add(sql);
FTree->Open();
Tree->Items->BeginUpdate();
while(!FTree->Eof){
      // Запомним в поле Data ветки ее идентификационный номер (ID) в таблице
      TreeNode = Tree->Items->AddChildObject(Node , FTreeNAME->AsString, (void*)FTreeID->AsInteger);
      TreeNode->ImageIndex = 0; TreeNode->SelectedIndex = 0;

      // если задан также режим поиска, вернем искомый Node

      if(searchID != -1 && FTreeID->AsInteger == searchID) searchNode = TreeNode;
      // Добавим фиктивную (пустую) дочернюю ветвь только для того,
      // чтобы был отрисован [+] на ветке и ее можно было бы раскрыть,

      // у узла у которогое дети реально есть != 0 (вот оно — пригодилось поле CCOUNT)
      if(FTree->FieldByName("CCOUNT")->AsInteger)
          Tree->Items->AddChildObject(TreeNode , "" , NULL);
      else { // это лист — устанавливаем другую иконку для листа
            TreeNode->ImageIndex = 5; TreeNode->SelectedIndex = 4;
           }
      FTree->Next();
      }
Tree->Items->EndUpdate();
FTree->Close();
return searchNode;
}
//---------------------------------------------------------------------------

//На событие OnExpanding сформируем реальную ветку, предварительно удалив фиктивную.
void __fastcall TForm1::TreeExpanding(TObject *Sender, TTreeNode *Node,
bool &AllowExpansion)
{
     Node->DeleteChildren(); ExpandLevel(Node, -1);
}
//---------------------------------------------------------------------------



В общем, это немного переработанный и дополненный пример функции из статьи.
А дополнен этот пример вот чем: у моего примера нет [+] у каждого узла, в отличие от примера из этой статьи,
с помощью поля CCOUNT мы всегда ! знаем, есть ли реально дочерние ветви или нет, и нужно ли нам рисовать [+] или нет.
Также эта функция стала выполнять и другую работу — если задан режим поиска
(SearchID != -1), то фунция также вернет узел TTreeNode для искомого searchID -
идентификационного номера детали, если он содержиться в прямых потомках данного узла.

Напишем и другие необходимые функции.

Движение по дереву.

// двигаемся по дереву (например стрелками, мышкой и т.д.)
void __fastcall TForm1::TreeChange(TObject *Sender, TTreeNode *Node)
{
   if(Tree->Selected && Node){
        int ID = (int)Node->Data;
        Detail->Locate("ID",ID,TLocateOptions());
       }
}
//---------------------------------------------------------------------------

Редактирование текста узла дерева.

// редактирование текста узла дерева
void __fastcall
TForm1::TreeEdited(TObject *Sender, TTreeNode *Node,
AnsiString &S)
{
   Detail->Locate("ID", (int)Node->Data, TLocateOptions());
   Detail->Edit();
      Detail->FieldByName("NAME")->Value = S;
   Detail->Post();
}
//---------------------------------------------------------------------------

Быстрая перерисовка дерева по ID.

// перерисовка дерева по известному ID номеру
// воспользуемся хранимой процедурой GETPARENTS для получения пути вверх,
// для этого разместим на форме еще один компонент DSet типа TpFIBDataSet
// в его свойстве SelectSQL напишем: Select * from GETPARENTS(:ID)
void __fastcall TForm1::PaintTree(int ID)
{
TTreeNode* Node = NULL;

DSet->ParamByName("ID")->Value = ID;
DSet->Prepare();
DSet->Open(); // получим список всех PARENT до самой вершины
int CCount = DSet->FieldByName("CCOUNT")->AsInteger; // кол-во детей
DSet->Last(); // начинаем с последнего, то есть с вершины
Tree->Items->Clear();           // очищаем дерево
Tree->OnExpanding = NULL; // отключаем обработчик
Tree->Items->BeginUpdate();

while
(!DSet->Bof){
   Node = ExpandLevel(Node, DSet->FieldByName("DID")->AsInteger);
   if(Node){
        if(Node->HasChildren) Node->DeleteChildren();
        Node->Expanded = true;
        Node->Selected = true;
        }
   DSet->Prior();
   }
DSet->Close();
Tree->Items->EndUpdate();
Tree->OnExpanding = TreeExpanding; // включаем обработчик
// есть дети? — рисуем у узла[+]
if(CCount) ExpandLevel(Node, DSet-FieldByName("DID")->AsInteger);
}
//--------------------------------------------------------------------------

Поиск по базе с быстрой перерисовкой дерева.

// вышеуказанная функция теперь позволит нам организовать быстрый поиск
// по дереву, с перерисовкой лишь неободимой части дерева
// бросьте на форму компонент eSearch типа TEdit,
// в его обработчик OnChange
напишем:

void __fastcall TForm1::eSearchChange(TObject *Sender)
{
if(eSearch->Text == "") return;
if(Detail->Locate("NAME", eSearch->Text, TLocateOptions()<<loPartialKey<<loCaseInsensitive))
   {
    PaintTree(Detail->FieldByName("ID")->AsInteger);
    eSearch->SetFocus();                                                          // вернуться в окно поиска
    eSearch->SelStart = eSearch->Text.Length(); // в конец слова
   }
}
//---------------------------------------------------------------------------

При наборе первых символов, курсор в базе и в дереве будет становиться на ближайшую похожую запись, максимально быстро перерисовывая лишь путь до искомой части дерева.
Если Вы в таблице DETAIL также создадите и другие поля — номер детали, описание, цена, и т.д. то быстрый поиск по этим полям организовать будет теперь также просто !
Создадим эти поля:
Alter table DETAIL add NUM integer
Alter table DETAIL add ARTICUL varchar(20)
Alter table DETAIL add PRICE double precision
...
Поместите на форму компонент CBox типа TComboBox, впишите в его свойство Items русские названия этих полей, типа "Название детали", "Номер", "Артикул","Цена" и т.д., создайте массив с именами реально созданных полей таблицы DETAIL:
AnsiString sField[]={"NAME", "NUM", "ARTICUL", "PRICE"};
Тогда текст нашего обработчика eSearchChange изменится незначительно:

...
if(Detail->Locate(sField[CBox->ItemIndex], Variant(eSearch->Text),
TLocateOptions()<<loPartialKey<<loCaseInsensitive))
   {

...

Осталось рассмотреть еще 2 очень важных вопроса -
1. Полное рисование дерева — мало-ли что нужно клиенту ???
2. Возможность перемещения узлов в дереве с помощью технологии Drag&&Drop

Полное рисование дерева.

// -------------------------------------------------------------------------
// полное рисование и раскрытие дерева по нажатию кнопки
// b_TreeFullExpand типа
TButton
// бросьте на форму кнопку, назовите ее b_TreeFullExpand в ее обработчик
// OnClick впишем:

void __fastcall TForm1::b_TreeFullExpandClick(TObject *Sender)
{
int p;
Tree->Items->Clear();                  // очищаем дерево
Tree->Items->BeginUpdate(); // запрещаем перерисовку

Detail->First();
while(!Detail->Eof){              // формируем дерево
p = Detail->FieldByName("PARENT")->AsInteger;
Tree->Items->AddChildObject(FindData(p),
Detail->FieldByName("NAME")->AsString,
(void*)(Detail->FieldByName("ID")->AsInteger));
Detail->Next();
}
Tree->FullExpand();                 // полностью раскрываем нарисованное дерево
Tree->Items->EndUpdate();  // включаем перерисовку
Detail->First();
Tree->SetFocus();
Tree->Items->Item[0]->Selected = true;
}
//---------------------------------------------------------------------------
TTreeNode* TForm1::FindData(int aData)
{
TTreeNode* Res = NULL;
if(Tree->Items->Count == 0) return NULL;
Res = Tree->Items->Item[0];
while(Res){
if((int)(Res->Data) == aData) return Res;
else Res = Res->GetNext();
}
return Res;
}
//---------------------------------------------------------------------------

 

Перемещения узлов в дереве с помощью технологии Drag&&Drop

Перемещать узлы будем мышкой, нажав предварительно клавишу <Ctrl>

TTreeNode* ddNode;
//---------------------------------------------------------------------------
// перемещение узлов дерева по CTRL + mbLeft
void __fastcall TForm1::TreeMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
  if(Button == mbLeft && Shift.Contains(ssCtrl)){
      ddNode = Tree->GetNodeAt(X,Y);                              // запоминаем перемещаемый узел
      if(ddNode == Tree->Items->Item[0]) return// вершину никогда не трогаем
      Tree->BeginDrag(true, 1);                                         // включаем режим переноса
     }
}
//---------------------------------------------------------------------------

// бросьте на форму компонент Ds типа TpFIBDataSet
// проверяем  — разрешено ли перемещение в данный узел
// 1. перенесен откуда-то еще — нельзя 2. сам в себя — нельзя
// 3. сверху вниз в себя — нельзя

void __fastcall TForm1::TreeDragOver(TObject *Sender, TObject *Source,
int X, int Y, TDragState State, bool &Accept)
{
if(Sender != Source) { Accept = false; return; } // 1. принесли извне — отказать
TTreeNode* SourceNode = Tree->GetNodeAt(X,Y);
if(SourceNode == NULL) { Accept = false; return; } // вне Node — отказать
if(SourceNode == ddNode){ Accept = false; return; } // 2. сам в себя — отказать

// 3. сверху вниз в себя — нельзя
Ds->SelectSQL->Clear();
AnsiString sql = "Select IS_CHILD from CHECK1_TREE(:S_ID,:D_ID)";
Ds->SelectSQL->Add(sql);
Ds->ParamByName("S_ID")->Value = (int)SourceNode->Data;
Ds->ParamByName("D_ID")->Value = (int)ddNode->Data;
Ds->Prepare();
Ds->Open();
int ddCheck = Ds->FieldByName("IS_CHILD")->Value;
Ds->Close();
if(ddCheck == 1) Accept = false; else Accept = true;
}
//---------------------------------------------------------------------------

// собственно сам перенос (Qr — компонент типа TpFIBQuery)
void __fastcall TForm1::TreeDragDrop(TObject *Sender, TObject *Source,
int X, int Y)
{
TTreeNode* SourceNode = Tree->GetNodeAt(X,Y);
if(SourceNode == ddNode) return; // сам в себя
int
ddID = (int)ddNode->Data;         // ID переносимого узла
int sID = (int)SourceNode->Data; // ID нового родителя
Qr->SQL->Clear();
AnsiString sql = "Update DETAIL set PARENT = :sID where ID = :ddID";
Qr->SQL->Add(sql);
Qr->ParamByName("sID")->Value = sID;
Qr->ParamByName("ddID")->Value = ddID;
Qr->Prepare();
Qr->ExecQuery();
Qr->Close(); Detail->CloseOpen(false);
Detail->Locate("ID", ddID, TLocateOptions());
PaintTree(ddID);
}
//---------------------------------------------------------------------------
 


Вот собственно и все.

Код работающего примера к С++Builder 6 и базу данных можно скачать здесь (50 Кб).
Учтите, что в C++Builder 6 у Вас должны быть установлены компоненты FIBPlus
и в системе должен быть установлен сервер FireBird 1.5
Если у Вас есть какие-либо замечания, пожелания, уточнения — пишите.
Статьи, на основе которых был создан данный пример — вот они:

http://www.ibase.ru/devinfo/treedb.htm
http://www.ibase.ru/devinfo/treedb2.htm
http://dynamic.nm.ru/Articles/extreeview.htm
http://www.delphikingdom.com/asp/viewitem.asp?catalogid=488