DBTreeView своїми руками, Інші СУБД, Бази даних, статті

 





Введення 


У статті мова піде про відображення даних, що зберігаються в БД і мають ієрархічну (деревоподібну) структуру. Візуальне подання таких даних вимагає відповідного інструменту. Існує чимало компонент, які дозволяють представляти дані у вигляді дерева – для стислості будемо називати їх все DB TreeView. Компоненти ці досить зручні, але, як правило, “заточені” під певні завдання і кожен “крок в сторону” в структурі даних змушує багатьох пускатися в пошуки. І на Круглому Столі з’являються питання: “Допоможіть знайти компонент DB TreeView, який дозволяє робити ще й …” і так далі. А адже в Delphi існує стандартний компонент для подання деревовидних даних, це знайомий всім TTreeView, його можливостей вистачає з лишком практично для всіх завдань по відображенню дерев. Зробити з TreeView справжнісінький DB TreeView, та ще повністю контролювати його розвиток, більш перспективний шлях, ніж щоразу шукати новий чужий компонент.
Весь цей матеріал грунтується на моєму особистому досвіді і, природно, не обов’язково є найоптимальнішим варіантом. Це приватна думка, яким я просто хочу поділитися. Розглянемо два принципово різних випадки:


Дерево підрозділів


Дані являють собою класичну деревоподібну структуру. Глибина вкладеності дерева не обмежена, заздалегідь не відома і не однакова для різних гілок. Дерево може не мати спільного кореневого вузла, тобто розпадатися на декілька дерев.;


Дерево аналітичних ознак


Реально дані не є ієрархічними, але можуть бути представлені в такому вигляді. Глибина вкладеності дерева однакова для всіх його гілок і фіксована в даному прикладі. Правило побудови ієрархії може змінюватися в режимі run-time.


Відрізняються ці приклади стуктурой даних і, відповідно, способом формування дерева.
Тим не менш сущест загальні правила для генерації дерева. По-перше, я виходжу з припущення, що все дерево відразу з великим ступенем імовірності не знадобиться користувачеві. Більше того, сам процес формування TTreeView досить тривалий і займає відчутний час, особливо якщо працювати з великим об’ємом БД.
Набагато правильніше будувати окремі гілки дерева динамічно, тільки в той момент, коли вони напевно знадобляться.
Тобто, у початковий момент необхідно сформувати тільки самий верхній рівень дерева. Потім, у міру вимоги, можна добудовувати чергову гілку.
Який же момент можна вважати моментом вимоги? В принципі є два рівнозначних варіанту і відрізняються вони лише зовнішнім виглядом.
Перший: по подвійному кліку на поточному гілки дерева перевіряти, чи була вона добудована, якщо ні то формувати її і генерувати подія OnExpanding, розкриваючи нову гілку.
Другий: при спробі “розкрити” гілку проводити аналогічні дії. Основна відмінність цих підходів: у першому випадку користувачеві необхідно зробити явно зайве і не дуже природне для дерева рух, то є подвійний клік. А в другому випадку ми повинні забезпечити можливість розкриття для кожної гілки дерева, адже якщо у гілки немає значка [+], то і спробувати розкрити її буде неможливо. А значить для всіх нових, свіжо сформованих гілок, примусово потрібно забезпечити існування фіктивної дочірньої гілки.
Можете самі вибрати, що Вам зручніше, я використовую другий варіант. Кожен спосіб має свої переваги і недоліки, але мені здається дещо невиправданим такий підхід, коли структура таблиць залежить від способу їх отображаенія. У прикладах, які ілюструють матеріал цієї статті застосовується використання фіктивних дочірніх гілок для забезпечення появи у кожної гілки значка [+].


Примітка:
Таке питання, як способи подання ієрархічних даних в БД, не є предметом даної статті.
Приклад побудований на локальних Paradox-таблицях. Отже для перенесення його на SQL-серверні БД варто враховувати їх особливості.






Дерево подразделееній 


Нехай у нас існує таблиця підрозділів, кожне з яких може мати свої внутрішні підрозділи. Необхідно відображати ці дані у вигляді дерева.
Використовувана в прикладі таблиця – COMPANY.DB
Структура даних реалізована класичним деревом: кожна запис про підрозділ представлена ​​полями



Для тих підрозділів, які не мають головних над собою, поле ParentID дорівнює 0. Формувати рівні дерева ми будемо за допомогою запиту до таблиці (компонент qTreeCompanies: TQuery).


Select * From COMPANY


Where ParentID=:ParentID


Параметр ParentID буде визначати, яку гілку ми зараз добудовуємо. Тобто, до якої шукаємо дочірні підрозділи.
Процедура, що формує черговий рівень (дочірній для гілки Node) реалізована наступним чином:


Procedure TFormTree.ExpandLevel( Node : TTreeNode);


Var ID , i   : Integer;


    TreeNode : TTreeNode;


Begin


/ / Для самого верхнього рівня вибрати лише тих,


/ / Хто не має батьків.


    IF Node = nil Then ID:=0


    Else ID:=Integer(Node.Data);


    qTreeCompanies.Close;


    qTreeCompanies.ParamByName(“ParentID”).AsInteger:=ID;


    qTreeCompanies.Open;


                                                                                                             


    TreeCompanies.Items.BeginUpdate;


              


/ / Для кожного рядка з отриманого набору даних


/ / Формуємо гілку в TreeView, як дочірні гілки до тієї,


/ / Яку ми тільки що “розкрили”


    For i:=1 To qTreeCompanies.RecordCount Do


    Begin


/ / Запишемо в поле Data гілки її ідентифікаційний номер (ID) в таблиці


       TreeNode:=TreeCompanies.Items.AddChildObject(Node ,


                                  qTreeCompanies.FieldByName(“Name”).AsString ,


                                  Pointer(qTreeCompanies.FieldByName(“ID”).AsInteger));


       TreeNode.ImageIndex:=1;


       TreeNode.SelectedIndex:=2;


/ / Додамо фіктивну (порожню) дочірню гілку тільки для того,


/ / Щоб був відмалювали [+] на гілці і її можна було б розкрити


       TreeCompanies.Items.AddChildObject(TreeNode , “” , nil);


       qTreeCompanies.Next;


    End;                                  


              


    TreeCompanies.Items.EndUpdate;


End;


Тепер подбаємо про те, щоб вона викликалася в потрібний нам момент часу. На подію OnExpanding перевіримо, чи є у поточній гілки фіктивна дочірня гілка і, якщо вона є, сформуємо реальну гілку, попередньо видаливши фіктивну.


  IF Node.getFirstChild.Data = nil


  Then Begin


          Node.DeleteChildren;


          ExpandLevel(Node);


       End;


На формі в проекті окрім дерева розташована ще й сітка (Grid), в якій відображаються записи поточного рівня підрозділів. Це, по суті, список дочірніх гілок для поточної гілки дерева. Для того, щоб синхронізувати TreeView і DBGrid використовуємо нехитрий прийом – на подію TTreeView.OnChange (крок по гілці) додамо наступний код:


   


  IF TreeCompanies.Selected <> nil Then


   Begin


/ / ID батьківського гілки, для неї і шукаємо всі дочірні


       ID:=Integer(TreeCompanies.Selected.Data);


       qCompanies.Close;


       qCompanies.ParamByName(“ParentID”).AsInteger:=ID;


       qCompanies.Open;


   End;


Пам’ятайте, у процедурі ExpandLevel ми записували в полі Data кожної гілки її ідентифікаційний номер? Ось його то ми зараз і використовуємо.
Для повного відчуття пересування по Grid “у, як по дереву можна додати ефект” провалювання “на більш глибокий рівень. За подвійному кліку на записи в Grid” е користувач провалюється на один рівень вниз по ієрархії, якщо такий рівень, звичайно ж ще є.
На подію OnDblClick для гріду:


          ID:=qCompanies.FieldByName(“ID”).AsInteger;


/ / Примусове “невидиме” розкриття тієї гілки, на якій стоїмо


          TreeCompanies.OnExpanding(TreeCompanies ,TreeCompanies.Selected , Allow);


/ / Перебираємо все вийшли дочірні гілки і шукаємо ту, ID якої


/ / Збігається з ID рядки в правій таблиці. Тобто шукаємо гілку в дереві,


/ / Яка відповідає тій записи в таблиці, на якій ми стоїмо


/ / Як тільки знайшли, візуально розкриваємо гілку і робимо її виділеної,


/ / Тобто візуально “встаємо” на неї в дереві


          FOR i:=0 To TreeCompanies.Selected.Count-1 Do


          IF Integer(TreeCompanies.Selected.Item[i].Data) = ID


          Then Begin


                TreeCompanies.Selected.Item[i].Expand(False);


                TreeCompanies.Selected.Item[i].Selected:=True;


                TreeCompanies.Repaint;


                Exit;


               End;


Паралельно з цим розкривається відповідна гілка самого дерева. Дуже ефектно. : О) В проекті реалізована можливість додавання нових гілок, тобто нових підрозділів. Нажімте на TreeView праву кнопку миші і добудовувати наше дерево, як Вам завгодно!






Дерево аналітичних ознак 


“Кущ – це пучок гілок, що ростуть з одного місця”
з військових афоризмів


Нехай у нас є таблиця документів, кожен документ, наприклад, описує деяку операцію з купівлі (або продажу) товару. У цій операції беруть участь: певний товар, клієнт, у якого куплений (або якому проданий) цей товар, і місто, в якому дана операція здійснена. Таким чином, ми маємо таблицю документів, кожен запис в якою наділена трьома аналітичними ознаками: Місто, Клієнт і Товар.
По суті своїй ці дані не є ієрархічними, і ніякої явної залежності між документами не простежується. Але тим не менше цей зв’язок існує – однакові аналітичні ознаки. Наприклад, в одну групу можна об’єднати всі документи, що мають відношення до певного товару або клієнта.
Якщо для аналізу даних користувачеві необхідно працювати з документами по зафіксованим аналітичним ознаками, то виникає завдання візуального представлення таких даних. Наприклад, замовник вимагає показувати йому всі документи для певного міста, а потім, для певного клієнта і товару, але в тому ж місті. Проте бажано бачити інформацію рівно у зворотному порядку: спочатку вибрати документи з потрібним товаром, потім побачити всі міста, де працювали з цим товаром, вибирати потрібний місто і так далі.
Явно простежується деревоподібна структура, де кожен рівень дерева суть аналітичний ознака. Саме тому, як говорилося вище, в даному дереві фіксована глибина вкладеності. Вона однозначно визначається кількістю аналітики для документа.


Опис прикладу, реалізовує дану задачу


Використовувані таблиці:
Таблиця документів DOCUMENTS.DB, де кожен документ визначений полями:



Таблиці аналітики, відповідно CITIES.DB, CLIENTS.DB і GOODS.DB, містять поля назви Name і номера (CityID, ClientID, GoodID).


Так як порядок проходження аналітики довільний, зараннее неможливо написати текст SQL-запиту, який буде повертати дані для чергового рівня дерева. Цей текст доведеться формувати в run-time, коли всі дані будуть відомі.
Щоб не зашивати саме на такий список аналітики, що наведений у прикладі, варто витратити трохи більше сил і забезпечити собі деяку універсальність. Для цього додамо ще одну таблицю (таблицю сутностей) Entities, Що містить опис використовуваної аналітики.
Поля таблиці ENTITIES.DB



В нашому випадку ця таблиця буде виглядати так:

 


























EntityID 


Name 


TableName 


KeyColumn 



1


Місто


CITIES


CityID



2


Товар


GOODS


GoodID



3


Клієнт


CLIENTS


ClientID



У прикладі я використовую список ListEntities (TCollection), кожен елемент якого містить поля TableName, KeyColumn і ImageIndex. Елементи в цьому списку розташовані в тому порядку, в якому буде будуватися дерево. Заповнюється цей список тільки тієї аналітикою, яка потрібна для конкретного дерева. Наприклад, тільки міста і клієнти або товари і клієнти, або відразу всі разом. Отже цей список (ListEntities) і містить повну інформацію для побудови дерева в кожен конкретний момент.
Заповнення списку аналітики проводиться в модулі setupEntities.pas.
Процедура GetEntities (var List: TEntityLists); зчитує аналітику з таблиці,
а функція SetEntities (var List: TEntityLists): Boolean; викликає діалог для настойки аналітики користувачем. Формування дерева в цьому випадку повністю аналогічно попередньому прикладу, з тією лише різницею, яка стосується формування тексту запиту для кожного рівня дерева.
Так само в дерево додається єдиний для всіх гілок самий верхній кореневий вузол. З точки зору аналітики це “фіктивний” вузол, так як він буде відображати ВСІ документи, без зазначення конкретного значення аналітичного ознаки. Крім того, добудовується ще один, самий нижній рівень – список документів по зафіксованій для поточної гілки аналітики.
Наявність цих “фіктивних” вузлів зовсім не обов’язково, але, на мій погляд, дуже логічно.
У підсумку, глибина вкладеності дерева буде дорівнює “кількість аналітики” + 2. Отже, процедура ExpandLevel буде модифікована таким чином:


Procedure TFormTree.ExpandLevelAnalytic(Node : TTreeNode );


Var NewItem    : TListsItem;


    ImageIndex ,


    Level , i  : Integer;


    TreeNode   : TTreeNode;


    Sql,Name   : String;


Begin


     IF Node = nil Then Exit;


     TreeAnalytic.Items.BeginUpdate;


Level: = Node.Level + 1; / / рівень, який буде розкриватися


/ / Найпершого аналітичному ознакою в списку ListEntities


/ / Соответсвует _второй_ фізичний рівень гілок дерева.


/ / Так як самий верхній рівень дерева фіктивний – “всі документи”


/ / Звідси і гра з (+ / -) 1 при зверненні до списку


     qTreeAnalytic.Close;


/ / Визначимо, на якому типі рівня ми зараз знаходимося


     //


     IF Level > ListEntities.Count


Then Begin / / Рівень документів, Аналітки закінчилася


            Sql:=”SELECT * FROM Documents Where “+ GetSqlPath(Node);


            Name:= “DocumentID”;


            ImageIndex:=3;


          End


Else Begin / / Черговий рівень аналітики


            Sql:=”SELECT DISTINCT “+ ListEntities[Level-1].AsString + “.* ” +


                 ” FROM Documents , ” +  ListEntities[Level-1].AsString + ” WHERE ” +


                 ListEntities[Level-1].AsString + “.” +ListEntities[Level-1].Name + “=” +


                 “Documents.”+ListEntities[Level-1].Name + ” AND ” + GetSqlPath(Node) ;


            Name:=ListEntities[Level-1].Name;


            ImageIndex:=ListEntities[Level-1].ImageIndex;


          End;


     qTreeAnalytic.Sql.Clear;


     qTreeAnalytic.Sql.Add(Sql);


     qTreeAnalytic.Open;


                                                                             


/ / Одержаний черговий рівень гілок дерева


     For i:=1 To qTreeAnalytic.RecordCount Do


     Begin


         NewItem:=List.AddItem(qTreeAnalytic.FieldByName(Name).AsInteger , Name);


         TreeNode:=TreeAnalytic.Items.AddChildObject( Node ,


                              qTreeAnalytic.FieldByName(“Name”).AsString, NewItem );


         TreeNode.ImageIndex:=ImageIndex;


         TreeNode.SelectedIndex:=TreeNode.ImageIndex;


/ / Фіктивна дочірня гілка ТІЛЬКИ для рівнів аналітики,


/ / Так як документи – останній рівень, за яким нічого і не може бути


         IF Level <= ListEntities.Count


         Then TreeAnalytic.Items.AddChild(TreeNode , “” );


         qTreeAnalytic.Next;


     End;


     TreeAnalytic.Items.EndUpdate;


End;


У попередньому прикладі ми запам’ятовували ID рядки з таблиці в поле Data кожної гілки дерева. Зараз нам не годиться такий варіант, так як аналітичний ознака визначається не одним ідентифікатором, а цілим елементом списку ListEntities, От його то і треба запам’ятовувати. Тому в поле Data зберігається посилання на конкретний елемент цього списку. Благо це Pointer і записати туди можна все, що завгодно. У процедурі використовується функція GetSqlPath, Яка повертає повний шлях від кореня до зазначеної гілки дерева. Повний шлях це є зафіксовані значення для кожного рівня аналітики. Ці значення необхідні для того, щоб вірно побудувати запит. Тобто ми фактично формуємо додатковий фільтр для наступних вибірок, напрмер – отримуємо всіх клієнтів для конкретного міста та зазначеного товару.


Function TFormTree.GetSqlPath( Node : TTreeNode ) : String;


Begin


   Result:=” 0=0 ” ;


/ / Беруть участь всі гілки дерева, крім самого верхнього фіктивного рівня


   While Node.Level > 0   Do


   Begin


      Result:= Result + ” AND ” +


               “Documents.” + TListsItem(Node.Data).Name + “=” +


                              TListsItem(Node.Data).AsString ;


/ / Робимо крок назад по гілці дерева


      Node:=Node.Parent;


   End;


End;


Приклад текстів SQL запиту, який буде сформований при русі по дереву:


Рівень “товари” – Усі товари, які зустрічаються в документах, створених у місті номер 6 і для клієнта номер 3


SELECT DISTINCT Goods.*  FROM Documents , Goods


WHERE Goods.GoodsID=Documents.GoodsID AND  0=0 


AND Documents.CityID=6 AND Documents.ClientID=3


Останній рівень “документи” – Усі документи, створені в місті номер 6 і для клієнта номер 3, по товару номер 1


SELECT * FROM Documents Where  0=0  AND


Documents.GoodsID=1 AND Documents.CityID=6 AND Documents.ClientID=3


Такий підхід дозволяє легко розширювати набір аналітичних ознак, які повинні використовуватися в програмі, практично без зміни коду клієнтського додатку. Достатньо змінити структуру таблиці DOCUMENTS і доповнити таблицю ENTITIES. В деякій мірі можна сказати, що таблиця ENTITIES містить метадані про структуру бази. Правда з великою натяжкою, оскільки в даному прикладі структура просто елементарна, а зв’язки надто прості і не підтримують жодної глибини вкладення (як, наприклад, в такому випадку, коли місто не вказаний явно в документі, але може бути витягнуть з таблиці клієнтів і так далі).
Для отримання набору даних не обов’язково використовувати саме запити, точно так само можна використовувати збережені процедури для SQL-серверних СУБД. Зміниться структура метаданих, але не зміниться принцип формування дерева.
На мій погляд, як приклад, варто уважно розглянути такий підхід, щоб ви могли в своїх конкретних завданнях на його основі конструювати реальні метадані і без проблем модифіковані дерево аналітичних ознак.


Отже …


Отже, були розглянуті дві принципово різні завдання, а реалізація DBTreeView виявилася практично ідентична. Власне, цей факт і є важливим результатом статті – скористайтеся прикладами, додайте власний функціонал і створіть для себе нескладний компонент для відображення деревоподібної структури. Це не означає, що не варто користуватися сторонніми компонентами, ні в якому разі. Просто якщо існуючі вас не влаштовують повністю, ви будете знати, як це виправити.


Для ілюстрації матеріалу статті підготовлений проект TreeDB. Проект откомпилирован в Delphi 5, Використовує BDE і налаштований на аліас TreeDB.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*