link7038 link7039 link7040 link7041 link7042 link7043 link7044 link7045 link7046 link7047 link7048 link7049 link7050 link7051 link7052 link7053 link7054 link7055 link7056 link7057 link7058 link7059 link7060 link7061 link7062 link7063 link7064 link7065 link7066 link7067 link7068 link7069 link7070 link7071 link7072 link7073 link7074 link7075 link7076 link7077 link7078 link7079 link7080 link7081 link7082 link7083 link7084 link7085 link7086 link7087 link7088 link7089 link7090 link7091 link7092 link7093 link7094 link7095 link7096 link7097 link7098 link7099 link7100 link7101 link7102 link7103 link7104 link7105 link7106 link7107 link7108 link7109 link7110 link7111 link7112 link7113 link7114 link7115 link7116 link7117 link7118 link7119 link7120 link7121 link7122 link7123 link7124 link7125 link7126 link7127 link7128 link7129 link7130 link7131 link7132 link7133 link7134 link7135 link7136 link7137 link7138 link7139 link7140 link7141 link7142 link7143 link7144 link7145 link7146 link7147 link7148 link7149 link7150 link7151 link7152 link7153 link7154 link7155 link7156 link7157 link7158 link7159 link7160 link7161 link7162 link7163 link7164 link7165 link7166 link7167 link7168 link7169 link7170 link7171 link7172 link7173 link7174 link7175
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