DBTreeView своими рукамиИсточник: delphikingdom Елена Филиппова
В статье речь пойдет об отображении данных, хранящихся в БД и имеющих иерархическую (древовидную) структуру. Визуальное представление таких данных требует соответствующего инструмента. Существует немало компонент, которые позволяют представлять данные в виде дерева - для краткости будем называть их все DB TreeView. Компоненты эти довольно удобны, но, как правило, "заточены" под определенные задачи и каждый "шаг в сторону" в структуре данных заставляет многих пускаться в поиски. И на Круглом Столе появляются вопросы: "помогите найти компонент DB TreeView, который позволяет делать еще и ..." и так далее. А ведь в Delphi существует стандартный компонент для представления древовидных данных, это знакомый всем TTreeView, его возможностей хватает с лихвой практически для всех задач по отображению деревьев. Сделать из TreeView самый настоящий DB TreeView, да еще полностью контролировать его развитие, более перспективный путь, нежели каждый раз искать новый чужой компонент. Дерево подразделений Данные представляют собой классическую древовидную структуру. Глубина вложенности дерева не ограничена, заранее не известна и не одинакова для разных ветвей. Дерево может не иметь общего корневого узла, то есть распадаться на несколько деревьев.; Дерево аналитических признаков Реально данные не являются иерархическими, но могут быть представлены в таком виде. Глубина вложенности дерева одинакова для всех его веток и фиксирована в данном примере. Правило построения иерархии может меняться в режиме run-time. Отличаются эти примеры стуктурой данных и, соответственно, способом формирования дерева. Примечание: Пусть у нас существует таблица подразделений, каждое из которых может иметь свои внутренние подразделения. Необходимо отображать эти данные в виде дерева.
Для тех подразделений, которые не имеют головных над собой, поле ParentID равно 0. Формировать уровни дерева мы будем с помощью запроса к таблице (компонент qTreeCompanies: TQuery). Select * From COMPANY Where ParentID=:ParentID Параметр ParentID будет определять, какую ветку мы сейчас достраиваем. То есть, к какой ищем дочерние подразделения. 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 каждой ветки ее идентификационный номер? Вот его то мы сейчас и используем. 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 правую кнопку мышки и достраивайте наше дерево, как Вам угодно! "Куст - это пучок веток, растущих из одного места" Пусть у нас есть таблица документов, каждый документ, например, описывает некоторую операцию по покупке( или продаже ) товара. В этой операции участвуют: определенный товар, клиент, у которого куплен (или которому продан) этот товар, и город, в котором данная операция совершена. Таким образом, мы имеем таблицу документов, каждая запись в которой наделена тремя аналитическими признаками: Город, Клиент и Товар. Описание примера, реализующего данную задачу Используемые таблицы:
Таблицы аналитики, соответственно CITIES.DB, CLIENTS.DB и GOODS.DB, содержат поля названия Name и номера (CityID, ClientID, GoodID). Так как порядок следования аналитики произвольный, зараннее невозможно написать текст SQL-запроса, который будет возвращать данные для очередного уровня дерева. Этот текст придется формировать в run-time, когда все данные будут известны.
В нашем случае эта таблица будет выглядеть так :
В примере я использую список ListEntities (TCollection), каждый элемент которого содержит поля TableName, KeyColumn и ImageIndex. Элементы в этом списке расположены в том порядке, в каком будет строиться дерево. Заполняется этот список только той аналитикой, которая требуется для конкретного дерева. Например, только города и клиенты или товары и клиенты, или сразу все вместе. Следовательно этот список (ListEntities) и содержит полную информацию для построения дерева в каждый конкретный момент. 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 содержит метаданные о структуре базы. Правда с большой натяжкой, так как в данном примере структура просто элементарна, а связи слишком просты и не поддерживают никакой глубины вложения (как, например, в таком случае, когда город не указан явно в документе, но может быть вытянут из таблицы клиентов и так далее). Итак... Итак, были рассмотрены две принципиально разные задачи, а реализация DBTreeView оказалась практически идентична. Собственно, этот факт и является важным результатом статьи - воспользуйтесь примерами, добавьте собственный функционал и создайте для себя несложный компонент для отображения древовидной структуры. Это не значит, что не стоит пользоваться сторонними компонентами, ни в коем случае. Просто если существующие вас не устраивают полностью, вы будете знать, как это исправить. Для иллюстрации материала статьи подготовлен проект TreeDB. Проект откомпилирован в Delphi 5, использует BDE и настроен на алиас TreeDB. |