Проектирование базы данных.
База данных — 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
|