Программирование на Lazarus

Деревья

Показывать лекцию целиком

Цель лекции

Применение компонента TTreeView для работы с древовидными иерархическими данными.

Дерево TTreeView

Компонент TTreeView расположен на вкладке Common Controls Палитры компонентов, и предназначен для отображения различных древовидных иерархических структур:

(рис 19.1) Компонент TTreeView

Мы часто сталкиваемся с иерархической информацией. Это может быть т.н. Дерево каталогов в стандартном Проводнике Windows, которое отображает в виде ветвей дерева внешние и вложенные папки, и файлы, которые там хранятся. Это может быть структура предприятия, библиотечный каталогизатор, структура книги, реферата, курсовой… В общем, самая разная информация, в которой присутствуют родительские и дочерние (вложенные) объекты. Все подобные объекты можно отобразить с помощью компонента TTreeView.

Объекты, которые содержаться в данном компоненте называются узлами (англ. node), а сам компонент представляет собой список узлов. Подобно спискам TListBox и TComboBox, с которыми мы уже знакомы, TTreeView имеет свойство Items - индексированный список узлов. Каждый узел - это объект, который имеет тип TTreeNode. И родительские, и вложенные в них дочерние элементы - всё это узлы (объекты) типа TTreeNode. Причем дочерний элемент одновременно может быть родителем по отношению к другому узлу, уровень вложенности неограничен.

Но давайте-ка всё по порядку. Рассмотрим работу компонента на примере библиотечного каталога. Откройте Lazarus с новым проектом. Как обычно, форму назовите fMain, проект сохраните под именем MyLibrary в папку 19-01, модулю формы дайте имя Main. В свойстве Caption формы напишите Библиотечный каталог. Саму форму немного растяните, пусть у нас будет высота 350, а ширина 500 пикселей.

Далее, установим на форму простую панель TPanel, из нее мы сделаем своеобразную Панель инструментов программы. У панели очистите свойство Caption, в свойстве Align установите alLeft, в свойстве Width 127 пикселей.

Далее, на полученную Панель инструментов одну за другой установите 7 простых кнопок TButton. У всех кнопок в свойстве Left установите значение 1, а в свойстве Width - значение 125. Верхнюю кнопку расположите повыше (Top = 1), остальные - чуть ниже, чтобы между кнопками было совсем небольшое расстояние. Нам нужно переименовать кнопки и сделать на них соответствующие надписи. Сделайте следующие настройки кнопок:

Настройки свойств кнопок
Name Caption
bNewNode Новый раздел
bNewChildNode Новый подраздел
bDelete Удалить
bEdit Переименовать
bSort Сортировать
bCollapse Свернуть список
bExpand Развернуть список

Теперь, правее панели, с вкладки Common Controls установите компонент TTreeView. Поскольку дерево у нас одно, переименовывать его мы не будем. В свойстве Align дерева также установите значение alLeft, в свойстве Width установите 360 пикселей. Поверх TTreeView с этой же вкладки установите список изображений TImageList, его тоже переименовывать не будем. В результате у нас должна получиться вот такая форма:

(рис 19.2) Полученная форма

Как видите, в правой части формы после TreeView1 осталось немного места. Сейчас мы научимся только производить различные действия с компонентом TTreeView, а позже, в другой лекции, мы вернемся к этому проекту, и в правой части сделаем отображение книг, которые относятся к данному разделу библиотеки.

Итак, выделите дерево TreeView1 и обратите внимание на его свойства. Мы рассмотрим только основные, с которыми еще не сталкивались в других компонентах.

Свойства TTreeView

AutoExpand - если True, то дерево автоматически распахивается, когда программа начинает работать, иначе список остается свернутым. Нам желательно, чтобы дерево было распахнуто, поэтому установите тут значение True.
HideSelection - если True, то когда компонент теряет фокус (активным становится другой компонент), выделение текущего узла будет скрыто. Ну, это дело вкуса, можно оставить значение по умолчанию - True.
Images - тут нужно выбрать список изображений, если хотите, чтобы у родительских и дочерних узлов были разные пиктограммы. Выберем тут ImageList1, а подходящие картинки подберем позже.
Indent - расстояние в пикселях от левого узла до его подузлов. По умолчанию равно 15 пикселей, и я рекомендую это расстояние не уменьшать. Иначе узлы дерева будут расположены близко друг от друга, как бы слипнуться, что затруднит чтение списка.
Items - сам список, основное свойство компонента. При нажатии на кнопку "" выходит Редактор элементов дерева:

(рис 19.3) Редактор элементов компонента TTreeView

Как видите, кнопки практически идентичны тем, что мы установили на форму, и работают примерно также. Так что мы не будем рассматривать работу с этим редактором, поскольку почти всегда дерево заполняют не вручную, а программно. Обратиться к отдельному узлу дерева можно через свойство Items, указав индекс элемента; индексация начинается с нуля. Например, к первому узлу в списке обращаются так: TreeView1.Items[0].

MultiSelect - если True, то разрешает одновременное выделение нескольких узлов; если False - запрещает.
MultiSelectStyle - переключатели, которые определяют способ выделения нескольких узлов. Переключателей четыре, если они имеют значение True, то данный способ работает, иначе не работает:
  • msControlSelect - с нажатой и удерживаемой <CTRL>, когда щелкают по элементам списка в произвольном порядке.
  • msShiftSelect - с нажатой и удерживаемой <SHIFT>, когда выбирают сразу диапазон элементов, щелкая сперва по первому, затем по последнему элементу.
  • msSiblingOnly - как msShiftSelect, но в диапазон включаются только узлы одного уровня.
  • msVisibleOnly - как msShiftSelect, но в диапазон не включаются нераскрытые дочерние узлы.
  • RightClickSelect - при True узлы выделяются как левой, так и правой кнопкой мыши; при False - только левой.
    RowSelect - при True выделяется вся строка элемента, при False - только его имя. Бесполезное свойство, надо сказать.
    ScrollBars - полосы прокрутки, с ними вы знакомы по компоненту TMemo. Рекомендую устанавливать в этом свойстве значение ssAutoBoth.
    ShowButtons - разрешает (при True) или запрещает показ кнопок возле узлов. Обычно разрешает, тогда слева от закрытого узла будет кнопка с плюсом, от открытого - с минусом.
    ShowLines - разрешает показ линий ветвей дерева. Желательно оставлять True.
    ShowRoot - разрешает показ линии, идущей от корня дерева. Если ShowLines = False, то никаких линий не будет показано в любом случае.
    SortType - способ сортировки списка. К этому свойству мы еще вернемся в программе. Может быть:
  • stNone - нет сортировки.
  • stText - сортировка по тексту.
  • stData - сортировка по данным.
  • stBoth - сортировка и по тексту, и по данным.
  • TreeLineColor и TreeLinePenStyle отвечают за цвет и тип линий ветвей дерева.

    Методы TTreeView

    AlphaSort - еще один способ отсортировать список. Вызывается так:
    TreeView1.AlphaSort;
            
    Метод возвращает True, если сортировка прошла успешно, и False в другом случае.
    FullCollapse - сворачивает дерево, скрывая все его дочерние узлы.
    FullExpand - наоборот, разворачивает дерево.
    LoadFromFile - метод считывает информацию об узлах из текстового файла. Файл сохраняется в формате UTF-8. Дочерние узлы находятся ниже родительских, и сдвинуты вправо символом табулятора. Перед использованием желательно проверять реальное наличие файла.
    SaveToFile - наоборот, сохраняет структуру дерева в текстовый файл.

    События TTreeView

    Компонент имеет несколько специфичных событий, которые могут оказаться полезными.

    OnChange - возникает после перемещения на другой узел.
    OnChanging - возникает перед перемещением на другой узел. При желании, можно сгенерировать это событие, и запретить пользователю этот переход, например, если не выполнено какое-то условие.
    OnCollapsed - возникает после свертывания узла. Пользователь может вызвать это событие, щелкнув по кнопке "-" рядом с узлом.
    OnCollapsing - возникает после свертывания узла.
    OnCompare - возникает, когда обработчик при сортировке списка сравнивает два узла и решает, который должен быть выше.
    OnDeletion - возникает при удалении узла.
    OnEdited - возникает после редактирования пользователем надписи узла.
    OnEditing - возникает во время редактирования надписи узла.
    OnExpanded - возникает после разворачивания узла.
    OnExpanding - возникает перед разворачивания узла.
    OnGetImageIndex - возникает при определении индекса изображения у узла из связанного TImageList.
    OnGetSelectedIndex - возникает при определении индекса текущего узла.

    Свойства и методы TTreeView.Items

    Как уже упоминалось, при программной обработке дерева (а чаще всего, такая обработка и используется) приходится пользоваться свойством Items, которое имеет тип TTreeNodes и само является объектом, а потому имеет собственный набор свойств и методов. Разберем основные из них:

    Свойства

    Count - Количество узлов, входящих в дерево, включая и вложенные.
    Item[Index:Integer] - Индексированный доступ к узлам. Как всегда, индексация начинается с нуля, поэтому, к примеру, изменить текст первого узла дерева можно так:
    TreeView1.Item[0].Text:= 'Новый текст';
                

    Нередко возникает необходимость обойти всё дерево, от первого до последнего узла. Например, изменить текст всех узлов можно так (пример выполнять не нужно):

    var
      i: integer;
    begin
      for i:= 0 to TreeView1.Items.Count-1 do
        TreeView1.Item[i].Text:= 'Узел №' + IntToStr(i+1);
            

    В этом случае мы получим дерево с текстом

    Узел №1
    Узел №2
            

    и так далее.

    Методы

    Add(SiblingNode:TTreeNode; const S):TTreeNode - добавляет новый узел в конец списка SiblingNode, с текстом S. Если добавляется корневой (не имеющий родителя) узел, то SiblingNode = NIL.
    AddChild(ParentNode:TTreeNode; const S):TTreeNode - добавляет новый дочерний (вложенный) узел в конец родительского списка ParentNode, с текстом S.
    AddFirst(SiblingNode:TTreeNode; const S):TTreeNode - добавляет новый узел в начало списка SiblingNode, с текстом S. Если добавляется корневой (не имеющий родителя) узел, то SiblingNode = NIL.
    AddChildFirst(ParentNode:TTreeNode; const S):TTreeNode - добавляет новый дочерний (вложенный) узел в начало родительского списка ParentNode, с текстом S.
    Clear - очищает список дерева от всех узлов.
    Delete(Node:TTreeNode) - удаляет узел Node.

    Основные свойства узла TTreeNode

    К отдельному узлу можно получить доступ через свойство Item, например, TreeView1.Item[0].

    ImageIndex - содержит индекс пиктограммы из связанного TImageList, которая будет отображаться в данном узле.
    Text - содержит текст, отображаемый в ветке данного узла.

    Пожалуйста, не путайте - свойство компонента Items имеет тип TTreeNodes, и представляет собой индексированный список узлов. А отдельный узел имеет тип TTreeNode - это не одно и то же!

    Вернемся к программе. Найдем две подходящих пиктограммы для узлов дерева. Щелкните дважды по ImageList1, чтобы открыть его редактор. Нажмите кнопку "Добавить". Если вы устанавливали Lazarus в папку по умолчанию, то различные стандартные картинки будут расположены по адресу

    C:\Lazarus\Images

    Не будем копаться по вложенным папкам - прямо тут находятся две подходящих пиктограммы. Первым добавьте изображение folder.png, это будет пиктограмма для родительских узлов. Пиктограмма получила индекс 0. Вторым добавьте изображение template.png, это будет изображение вложенных подразделов. Картинка встала под индексом 1. Можно закрыть редактор ImageList кнопкой "ОК".

    Теперь приступим к программированию кнопок. Сгенерируйте событие OnClick для кнопки "Новый раздел". Её код будет следующим:

    procedure TfMain.bNewNodeClick(Sender: TObject);
    var
      NodeCaption: string; //для получения заголовка нового узла
      NewNode: TTreeNode; //для создания нового узла
    begin
      //сначала очистим заголовок:
      NodeCaption:= '';
      //теперь, если пользователь не ввел заголовок нового узла, выходим:
     if not InputQuery('Ввод заголовка', 'Введите заголовок раздела',
                         NodeCaption) then exit;
      //если мы здесь, то заголовок есть. создаем родительский узел:
      NewNode:= TreeView1.Items.Add(nil, NodeCaption);
      //присваиваем ему картинку под индексом 0:
      NewNode.ImageIndex:=0;
    end;
            

    Как видно из кода и комментариев, эта кнопка добавляет в конец списка узлов новый родительский узел. Функцией-запросом InputQuery() мы получаем у пользователя заголовок для будущего узла. Если пользователь закрыл диалог, не введя этого заголовка, то мы просто выходим из события, ничего не предпринимая. Но если он что-то туда ввел, то этот заголовок попадает в переменную NodeCaption, и мы приступаем к созданию родительского узла, что и делает код:

      NewNode:= TreeView1.Items.Add(nil, NodeCaption);
            

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

    В заключение мы присваиваем этому узлу картинку под индексом 0, если помните, там изображение папки.

    Код для кнопки "Новый подраздел" очень похож на предыдущий:

    procedure TfMain.bNewChildNodeClick(Sender: TObject);
    var
      NodeCaption: string;
      NewNode: TTreeNode;
    begin
      NodeCaption:= '';
      if not InputQuery('Ввод заголовка', 'Введите заголовок подраздела',
                         NodeCaption) then exit;
      NewNode:= TreeView1.Items.AddChild(TreeView1.Selected, NodeCaption);
      if NewNode.Parent = nil then NewNode.ImageIndex:=0
      else NewNode.ImageIndex:=1;
    end;
            

    Разницы тут две. Во-первых, в этот раз мы используем метод Items.AddChild, который добавляет именно дочерний узел. В параметре вместо nil вы видите уже TreeView1.Selected, что означает ссылку на выделенный в данный момент раздел. Именно для этого раздела будет создаваться подраздел.

    Во-вторых, мы указываем не конкретный индекс картинки, которая будет тут отображаться, а делаем проверку:

      if NewNode.Parent = nil then NewNode.ImageIndex:=0
      else NewNode.ImageIndex:=1;
            

    Если у нового узла все-таки нет родителя (NewNode.Parent = nil), то присваиваем узлу изображение 0, иначе это будет изображение 1.

    Для кнопки "Удалить" код будет следующим:

    procedure TfMain.bDeleteClick(Sender: TObject);
    begin
      if TreeView1.Selected <> nil then
        TreeView1.Items.Delete(TreeView1.Selected);
    end;
            

    Код очень простенький. Если выделенный узел не равен nil (то есть, если вообще какой-то узел выделен), то мы удаляем из списка этот выделенный узел.

    Код для кнопки "Переименовать":

    procedure TfMain.bEditClick(Sender: TObject);
    var
      NodeCaption: string;
    begin
      NodeCaption:= '';
      if not InputQuery('Ввод заголовка', 'Введите новый заголовок',
                         NodeCaption) then exit;
      TreeView1.Selected.Text:= NodeCaption;
    end;
            

    Здесь мы точно также пытаемся получить у пользователя новый заголовок. Если он его ввел, то этот новый заголовок мы присваиваем свойству Text выделенного в данный момент узла.

    Теперь напишем код для кнопки "Сортировать". Её код совсем простой:

    procedure TfMain.bSortClick(Sender: TObject);
    begin
      TreeView1.AlphaSort;
    end;
            

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

    Далее на очереди у нас кнопка "Свернуть список". Её код не сложнее:

    procedure TfMain.bCollapseClick(Sender: TObject);
    begin
      TreeView1.FullCollapse;
    end;
            

    Для "Развернуть список":

    procedure TfMain.bExpandClick(Sender: TObject);
    begin
      TreeView1.FullExpand;
    end;
            

    Методы FullCollapse и FullExpand мы изучали выше.

    Кнопки мы запрограммировали. Однако пока толку от нашей программы - ноль. Пользователь потратит время, заполняя список разделов и подразделов библиотеки, но стоит ему только выйти из программы, и вся эта работа потеряется. Нам нужно научить программу этот список сохранять в файл, и загружать его из файла. Файл списка назовем MyLibrary.dat. MyLibrary - потому, что так называется наша программа, dat - такое расширение традиционно имеют файлы с данными. Где лучше всего загружать этот список? Конечно, в событии OnCreate главной формы! Это событие возникает однажды, когда загружается программа, но перед ее отображением на экране. Если вам требуется сделать какую то подготовительную работу перед открытием вашей программы, то OnCreate для этого - самое место. Выделите форму fMain - это можно сделать, щелкнув по маленькому свободному участку правее компонента TreeView1, или выбрав fMain в верхней части Инспектора объектов. Затем перейдите на вкладку "События" Инспектора объектов, найдите и сгенерируйте событие OnCreate. Код будет следующим:

    procedure TfMain.FormCreate(Sender: TObject);
    var
      i: integer;
    begin
      //если файл существует, загрузим его:
      if FileExists('MyLibrary.dat') then
        TreeView1.LoadFromFile('MyLibrary.dat');
      //теперь пройдемся по списку, и каждому узлу присвоим
      //нужную пиктограмму:
      for i:= 0 to TreeView1.Items.Count-1 do
        if TreeView1.Items[i].Parent=nil then TreeView1.Items[i].ImageIndex:=0
        else TreeView1.Items[i].ImageIndex:=1;
    end;
            

    Здесь мы сначала с помощью функции FileExists() проверяем, есть ли вообще в текущей папке файл MyLibrary.dat? Функция вернет истину, если такой файл есть. В этом случае мы его загружаем в дерево TreeView1. Но дерево выйдет без пиктограмм, их еще нужно загрузить. Это мы делаем в цикле for, обходя все узлы дерева. Если узел родительский (TreeView1.Items[i].Parent=nil), мы присваиваем ему картинку с индексом 0, иначе - картинку с индексом 1.

    Осталось научить программу сохранять список. Делать это лучше всего при выходе из программы, в событии OnClose главной формы. В этом событии обычно делают все завершающие действия - закрывают открытые ресурсы, сохраняют параметры программы, и т.п. Код события следующий:

    procedure TfMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
    begin
      TreeView1.SaveToFile('MyLibrary.dat');
    end;
            

    Тут мы просто сохраняем список в файл. Сохраните проект, запустите его и попробуйте заполнить разделы и подразделы библиотеки. У меня получилось примерно так:

    (рис 19.4) Работающая программа

    Поэкспериментируйте с работой всех кнопок. При выходе из программы список должен сохраняться, а при входе - считываться. Не удаляйте проект - мы к нему еще вернемся!

    Вернуться к учебному плану