Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_5845.html
 

Первое использование VirtualTreeView (TVirtualStringTree)

Опубликовано 30.01.2013 г. 14:10

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

Описание задачи

Имеется редатор исходных данных. В редакторе данные представлены в виде таблиц. Таблицы сгруппированы в блоки (блоки вида Экономика, Образование, Демография и т.п.). Каждое значение исходных данных привязывается к источнику данных. Источники могут быть разные - справочники, формы отчетности и т.п. Моя задача - организовать управление списком источников. Цель - нет смысла при редактировании исходных данных из блока экономики показывать источники, которые относятся, например, к статистическим формам по образованию. В связи с этим:
Необходимо реализовать функционал управления источниками данных. Каждый источник относится к какой-либо категории по его типу (справочник, стат.форма и т.д.). Для каждого источника необходимо настраивать блоки редактора, в которых он отображается. Дополнительно - доабвление/удаление/редактирование и слияние источников, а также перемещение в категориях (желательно мышью).

Инструментарий

На уровне БД источники и  их типы (назовем их категорями) хранятся в БД в виде двух таблиц - sources и sourceCategories. Таблицы связаны через поле sources.category_id. Для хранения выбора блоков отображения для источника используется битовая маска - поле sources.deCategoryMask (число блоков отображения не привязано к БД, и жестко (почти жестко (: ) указано в программном коде). Доступ к даннм в БД - dbGo - ADO. Пользовательский интерфейс представление данных  - TVirtualStringTree. Первый уровень узлов - типы источников. Второй уровень - сами источники.

Для наглядности ниже приведет скриншот того, что получилось в итоге:

Лирическое отступление

В набор VirtualTreeView входят два основных компонента: VirtualDrawTree и VirtualStringTree. Компоненты предназначены для отображения в первую очередь древовидного представления данных. Исходя из названия компонентов, в DrawTree вы в полной мере ответственны за отображение информации, в StringTree более ориентирован на представление текстовых узлов. Компоненты имеют в названии слово Virtual, которое означает, что данные, которые представляются с точки зрения самого компонента - виртуальны. Компонент не знает ничего об их типе. Таким образом, компонент ответственен за иерархическую структуру данных и действия с узлами. Вы же должны манипулировать теми струтурами данных, которые стоят за этими узлами. Какие проблемы могут возникнуть: документация по компоненту далеко не самая актуальная - в колонтитулах руководства стоит 2005-й год. Само руководство занимает 810 страниц. Установка проблем не вызывает - только один bpl-пакет. Исходный код содержит множество комментариев. Демо проекты могут вызывать ошибки компиляции из за разных версий компонента (встречалось несколько ошибко связанных с разной сигнатурой событий). Компонент, что называется pure-pascal, не привязан к системным компонентам Windows. Изначальная цель компонента - обеспечить работу с большим количеством данных (тысячи и миллионы узлов).

Наполнение дерева

Заполнение дерева узлами может быть выполнена двумя способами. Способ первый это ручное создание узлов и добавление их в дерево с помощью методов AddChild(). Использование данного метода оправдано, когда вам необходимо сразу же загрузить все дерево и отобразить его. В моем случае у меня всего порядка 40-50 узлов, и этот способ был мне вполне удобен. Второй способ - заполнение через события. Данный способ является первоочередным, но более сложным в реализации. Первоочередность определяется тем, что узлы загружаются по мере отображения. Если в вашем дереве максимум может быть например, миллион узлов, а на экране вы показываете в большинстве случаев только 20, то смысла загружать остальные 999980 записей нет. Для работы второго способа вам необходимо указать количество узлов первого уровня. Далее компонент через события OnInitNode запросит вас необходимые данные для инициализации узла. Раскроете верхний узел - и компонент инициализирует вложенные узлы.

Перед началом работы, необходимо определить вашу логическую структуру, которая связывается с узлом дерева. Компонент оперирует узлами TVirtualNode, каждый узел при инициализации связывается с вашей структурой данных. На самом деле не обязательно использовать какие-либо структуры, вы можете хранить указатель на объект и т.п. В начале работы следует указать размер ваших данных, и при иницилизации узла TreeView выделит нужное количество памяти.

 В моем случае я использовал структуру TSourceNode. Описание получилось немного громоздким, т.к у структуры есть и методы и свойства. Но полей всего несколько:

  • Тип объекта (категория, источник) FNodeType : TNodeType = (ntCategory, ntSource)
  • ID объекта (значение ключа соответствующей записи в БД) - FId
  • Caption  - FCaption
  • Категория источника  FCatID
  • Маска блоков отображения - FDEMask
  • Событие изменения узла FOnNodeChanged

в общем и целом, необходимые пользовательские типы данных выглядели следующим образом:

    TNodeType = (ntCategory, ntSource);

    PSourceNode = ^TSourceNode;

    TNodeChangedEvent = procedure (sender : PSourceNode) of object;

    TSourceNode = record
      strict private
        FNodeType : TNodeType;
        FId : integer;
        FCaption : string;
        FCatId : integer;
        FMask : integer;
        FOnNodeChanged : TNodeChangedEvent;
        procedure SetMask(newMask : integer);
        procedure SetCatId(newCat : integer);
        procedure SetCaption(newCaption : string);
      public
        procedure InitCategoryNode(id : integer; caption : string);
        procedure InitSourceNode(id : integer; caption : string; catId : integer; aMask : integer);

        property NodeType : TNodeType read FNodeType;
        property ID : integer read FId write FId;
        property Caption : string read FCaption write SetCaption;
        property CategoryId : integer read FCatId write SetCatId;
        property DEMask : integer read FMask write setMask;

        property OnNodeChanged : TNodeChangedEvent read FOnNodeChanged write FOnNodeChanged;
    end;

С точки зрения редактирования узла, у источника может изменятся категория, название и маска отображения. Поэтому запись этих свойств происходит через соответствующие set-методы, которые в свою очередь вызывают обработчик события OnNodeChanged. За обработку события будет отвечать форма, которая, зная ID измененного источника (через параметр события) произведет необходимые изменения с набором данных ADO. Наборов данных испоьзуется два - CategoriesQuery и SourcesQuery. В целом, при инициализации дерева необходимо указать размер пользовательских данных узла, и заполнить их. Например, так:

    SourcesTree.NodeDataSize  := SizeOf(TSourceNode);
...
        with CategoriesQuery do begin
            while not eof do begin
                pvnode := SourcesTree.AddChild(SourcesTree.RootNode);  //возврат созданного узла PVirtualTree

                sn := sourcesTree.GetNodeData(pvnode);  //пользовательские данны узла PSourceData
                id := FieldByName('id').AsInteger;
                title := FieldByName('title').AsString;
                sn.InitCategoryNode(id, title);         //инициализациия категории источника 

                catNodes.Add(sn.ID, pvnode);            //вспомогательный словарик для заполнения источников
                Next();
            end;
        end;

 

 Далее аналогичным образом заполняются источники. Как видно в данном случае, дерево отвечает за выделение памяти пользовательских узлов, и возвращает указательна буфер памяти. Как вариант, мы можем сами сначала выделить память, и передать указатель на нее при добавлении узла через второй параметр AddChild. Очистка памяти для инициализированных узлов проводится в событии OnFreeNode. В простейшем случае, вам потребуется вызвать Finalize/Dispose для указателя на вашу структуру. Теперь дерево готово к отображению.

Вывод данных

 Для отображения текста в дереве используется событие OnGextText. Данное событие вызывается для каждого узла (строки) и  ячейки. На входе имеем узел и номер столбца, на выходе должны вернуть текст для ячейки. Само собой, событие вызывается лишь тогда, когда текст требуется отобразить на экране. В моем случае, текст содержится в главной (нулевой) и последней колонке. Последняя колонка содержит количество записей в таблицах данных, связанных с этим источником. Поэтому название в первом столбце следует выводить для всех узлов дерева, а последний столбец заполнять только для узлов-источников данных. В данном случае словарь FUsage содержит статистику использования источников, статистика эта собирается в отдельном потоке, поэтому следует проверять, получена ли она, прежде чем использовать FUsage.

procedure TManageSourcesForm.SourcesTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
    TextType: TVSTTextType;  var CellText: string);
var data : PSourceNode;
begin
    Data := Sender.GetNodeData(node);
    CellText := '';

    if column = 0 then begin
        CellText := data.Caption;
    end
    else if column = FUsageCol then begin
        if data.NodeType <> ntSource then exit;

        if assigned(FUsage) and FUsage.ContainsKey(data.id) then
             CellText := IntToStr(FUsage[data.ID])
        else CellText := '0';
    end;
end;

Далее, чтобы сделать несколько акцентов на узлах, изменим шрифт. Названия категорий, а также статистика использования будут выводится жирным шрифтом. Кроме того, для статистики, если значение нулевое (отсутствует) будем использовать красный цвет. Установка шарифта ячеек выполняется с помощью события OnPaintText.

procedure TManageSourcesForm.SourcesTreePaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode;
  Column: TColumnIndex; TextType: TVSTTextType);
var level : integer;
    d : PSourceNode;
begin
    if column in FDEColRange then exit;

    d := sender.GetNodeData(node);
    if column = 0 then begin
        if d.NodeType = ntCategory then
             targetCanvas.Font.Style := targetCanvas.Font.Style + [fsBold];
    end
    else if (column = FUsageCol) and Assigned(FUsage) then begin
        if d.NodeType = ntSource then begin
            if not (FUsage.ContainsKey(d.Id) and (FUsage[d.Id] > 0)) then begin
                TargetCanvas.Font.Style := TargetCanvas.Font.Style + [fsBold];
                TargetCanvas.Font.Color := clRed;
            end;
        end;
    end;
end;

 

 Для получения индекса иконки узла применяется событие OnGetImageIndex. Логика работы в событиях в целом всегда схожа. В качестве входного параметра мы получаем узел дерева PVirtualNode, используя который извлекаем соответствующие пользовательские данные. На основании этих данных, в данном случае свойства NodeType мы возвращаем нужный индекс рисунка. Текст всплывающей подсказки возвращается аналогично через событие OnGetHint.

Флажки выбора блоков отображения (checkbox)

После того как текстовые метки в дереве мы получили, необходимо добавить чекбоксы для выбора блоков отображения. По умолчанию дерево может показывать чекбоксы (радио-кнпки и т.п.) только в главном столбце. Таким образом, вы можете получить лишь один чекбокс для узла. В нашей задаче флажки необходимо разместить в нескольких дополнительных колонках. Дерево может функционировать в двух режимах - режим просмотра и режим редактирования. Режим редактирования (в который узел переводится автоматиечски, или с помощью вызова метода EditNode()) позволяет отображать произвольные редакторы для ячеек. Реализация редакторов полностью в ваших руках. Экземпляр редактора должен поддерживать интерфейс IVTEditLink и создается при обработке события OnCreateEditor. Редакторы отображаются пока узел находится в режиме редактирования. Как только режим снимается, редактор исчезает. Такой подход нам не подходит, т.к. для наглядности и удобства лучше наблюдать чекбоксы постоянно. Чтобы получить постоянно видимые чекбоксы потребуется нарисовать их вручную. Сложности это не представляет, особенно в последних версиях Delphi, используя TStyleServices. Конечно, у чекбоксов достаточно много различных визуальных состояний. Помимо checked/unchecked чекбокс выглядит по разному при наведении мыши и т.п., но для простоты я использовал только эти два.

Итак, в для двух состояний чебокса нам необходимо иметь детали их отрисовки, и размеры чекбоса. Чтобы не извлекать их при каждой отрисовке, извлечем их при запуске приложения.

    FCBSize : TSize;
    FCBDetails : array[boolean] of TThemedElementDetails;
...
procedure TManageSourcesForm.InitCheckBoxDrawing();
begin
    with TStyleManager.ActiveStyle do begin
        FCBDetails[true]  := GetElementDetails(tbCheckBoxCheckedNormal);
        FCBDetails[false] := GetElementDetails(tbCheckBoxUncheckedNormal);

        GetElementSize(canvas.Handle, FCBdetails[true], esActual, FCBSize);
    end;
end;

Теперь, используя событие OnAfterCellPaint, можем нарисовать чекбоксы в нужных ячейках (FDEColRange - диапазон ячеек для чекбоксов). Ячейки следует рисовать только для источников, поэтому узлы категорий пропускаем. Для каждого чекбокса необходимо определить его состояние, которое задается битовой маской. Также необходимо центрировать прямоугольник отрисовки в ячейке в зависимости от разницы между высотой ячейки и размерами самого чекбокса. Сама отрисовка чекбокса делается весьма просто. Необходимо лишь вызвать метод TSyleManager.ActiveStyle.DrawElement() с нужными параметрами - на чем рисовать, что рисовать, и в каком месте.

procedure TManageSourcesForm.SourcesTreeAfterCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; Node: PVirtualNode;
  Column: TColumnIndex; CellRect: TRect);
var data : PSourceNode;
    dy : integer;
    mflag : integer;
    checked : boolean;
begin
    if not (column in FDEColRange) then exit;

    data := sender.GetNodeData(node);
    if data.NodeType <> ntSource then exit;

    mflag := column - 1;
    checked := mflag in TIntegerSet(data.DEMask);

    dy := (CellRect.Height - FCBSize.Height) div 2;
    inc(CellRect.Top,    dy);
    CellRect.Bottom := Cellrect.Top + FCBSize.cy;

    TStyleManager.ActiveStyle.DrawElement(TargetCanvas.Handle, FCBDetails[checked], CellRect);
end;

 После того как флажки нарисованы, требуется обработать клик мыши по флажку. Делается это весьма просто. Необходимо написать обработчик события клика в узел. Зная узел и номер столбца, в которые был совершен клик (информация из HitInfo), мы можем получить прямоугольник этой ячейки (GetDisplayRect). Зная механизм расположения чекбокса в этом прямоугольнике мы можем определить, был ли совершен клик в чекбокс, или вне его:.

procedure TManageSourcesForm.SourcesTreeNodeClick(Sender: TBaseVirtualTree; const HitInfo: THitInfo);
var data : PSourceNode;
    im : TIntegerSet;
    mPos : integer;

    MousePos : TPoint;
    cbRect, cellRect : TRect;
    dy,dx : integer;
begin
    data := sender.GetNodeData(hitinfo.HitNode);

    if not (hiOnItemRight in HitInfo.HitPositions) then exit;

    if hitINfo.HitColumn in FDEColRange then begin
        mousePos := SourcesTree.ScreenToClient(mouse.CursorPos);
        CellRect := SourcesTree.GetDisplayRect(hitInfo.HitNode, hitInfo.HitColumn, false);
        cbRect   := CellRect;

        dy := (CellRect.Height - FCBSize.Height ) div 2;
        inc(cbRect.Top, dy);
        cbRect.Bottom := cbRect.Top + FCBSize.cy;

        dx := (CellRect.Width - FCBSize.Width) div 2;
        inc(cbRect.Left, dx);
        cbRect.Right := cbRect.Left + FCBSize.cx;

        if not cbRect.Contains(mousePos) then
            exit;

        mPos := HitInfo.HitColumn - 1;

        if data.NodeType <> ntSource then
            exit;

        data.DEMask := data.DEMask xor (1 shl mPos);

        sender.InvalidateNode(hitinfo.HitNode);
    end;
end;

Дле перерисовки чекбокса следует перерисовать узел целиком, вызвав метод InvalidateNode().

Перемещение узлов

Для данной конкретной задачи при перемещении узлов есть несколько ограничений. Во первых, мы можем перемещать только источники, но не категории. Во вторых - источники мы можем перемещать только в категорию отличную от текущей категории источника. На уровне БД это означает изменение ID категории в таблице источников. Для учета таких ограничений, нам следует выбрать ручной режим Drag&Drop. Снала следует определить, можно ли вообще перемещать узел (категория или источник). Для этих целей используем событие OnDragAllowed. Параметр Allowed - выходной, определяющий разрешено ли перемещение, будет зависеть от типа нашего узла.

Далее - сам процесс перемещения. Когда мы тащим узел, следует определить можем ли мы его бросить в данный момент. Перетскаивая источник, мы можем его бросить только на "не свою" категорию. За процесс перетаскивание отвеачает событие OnDragOver:

var dropNode, dragNode : PSourceNode;
begin
    if not Assigned(sender.DropTargetNode) then exit;

    dropNode := sender.GetNodeData(sender.DropTargetNode);
    dragNode := sender.GetNodeData(sender.GetSortedSelection(true)[0]);

    Accept := (dropNode.NodeType = ntCategory) and (dragNode.CategoryId <> dropNode.ID);
end;

И последние - отпускание узла и его перемещение. Когда DragOver вернул Accept = true и мы отпустили мышь возникает событие OnDragDrop. Здесь нам требуется определить эффект перетаскавания (перемещение, копирование и  т.п.), переместить узел, изменить ID категории в пользовательской структуре данных:

var nodes : TNodeArray;
    d, cat : PSourceNode;
    i : integer;
begin
    if not (Source is TVirtualStringTree) then exit;
    effect := DROPEFFECT_MOVE;

    nodes := sender.GetSortedSelection(true);
    cat := SourcesTree.GetNodeData(SourcesTree.DropTargetNode);
    for i := 0 to high(nodes) do begin
        sender.MoveTo(nodes[i], SourcesTree.DropTargetNode, amAddChildFirst, false);
        d := sender.GetNodeData(nodes[i]);
        d.CategoryId := cat.ID;
    end;
end;

 Добавление  узла

 Панель инструментов позволяет создавать, редактировать, удалять, перемещать и объединять узлы. Эти операции сами по себе достаточно просты. Для добавления узла нам необходимо взять текущий выделенный узел, определить в какой категории он находится, и создать в ней новый узел, по аналогии с изначальным заполнением дерева. ID источника при этом установим в -1, что для обработчика события OnNodeChanged будет обозначать, что узел добавлен, а не изменен:

procedure TManageSourcesForm.AddSourceButtonClick(Sender: TObject);
var d, nps : PSourceNode;
    catId : integer;
    targetNode, newNode : PVirtualNode;
begin
    targetNode := SourcesTree.FocusedNode;
    if not Assigned(targetNode) then exit;

    d := sourcesTree.GetNodeData(targetNode);
    if d.NodeType = ntSource then begin
        targetNode := targetNode.Parent;
        d := sourcesTree.GetNodeData(targetNode);
    end;
    catId := d.ID;

    newNode := SourcesTree.AddChild(TargetNode);
    nps := SourcesTree.GetNodeData(newNode);
    nps.InitSourceNode(-1, 'Новый источник', catId, DEFAULT_CATEGORY_MASK);
    nps.OnNodeChanged := SourceNodeChanged;
    SourcesTree.FocusedNode := newNode;
    SourcesTree.EditNode(newNode, 0);
end;

После добавления узла переводим фокус на него, и включаем режим редактирования. Редактирование осуществляется еще проще. Если узел - источник, то просто вызываем EditNode(). Новоое значение загловка из главного столбца можно получить с помощью события OnNewText. В случае удаления необходимо удалить узел из дерева - DeleteNode(), и удалить его из БД. Причем провести эти действия в обратном порядке, чтобы не удалить узел из дерева раньше времени, в случае если удаление записи из БД вызовет исключение.

Объединение узлов операция специфическая, ибо будет зависеть скорее от БД. С точки зрения интерфейса в моем случае это реализовано слеудющим образом: пользователь выбирает какой-нибудь узел, жмет кнопку объединения. При этом свойство/поле формы JoinMode/FJoinMode устанавливается в значение true. Кнопка фиксируется в "нажатом" состоянии. Цвет выбранной строки изменяется (фон, шрифт). Выбранный узел запоминается в FJoinTarget (это узел, которому будет присоединен другой источник). Далее пользователь должен выбрать второй источник (который будет в дальнешем удален). Обработчик клика в узел OnNodeClick получает дополнительный функционал: если FJoinMode установлен в true, и выбранный узел - источник, а не категория, то выполняется слияние. JoinMode устанавливается в false, цвета возвращаются в исходные. Выглядит это как то так:

 

Обновление данных в БД

Как я отмечал в начале статьи, при измении любого из свойств Caption, CategoryId, Mask пользовательско структуры TSourceNode вызывается обработчик события OnNodeChagned. Обработчик этот имеет весьма простой вид. Все что от него требуется - перевести набор данных SourcesQuery  в нужное состояние - Insert или Edit. Далее изменить значения полей и выполнить Post() для обновления данных в БД. В случе если узел создавался, то вернуть созданный ID:

procedure TManageSourcesForm.SourceNodeChanged(Sender: PSourceNode);
var isNew : boolean;
begin
    with SourcesQuery do begin

        isNew := (sender.ID = -1);

        if  isNew then
            Insert()
        else begin
            if not Locate('id', sender.ID, []) then exit;
            Edit();
        end;

        try
            FieldByName('name').AsString := sender.Caption;
            FieldByName('deCategoryMask').AsInteger := sender.DEMask;
            FieldByName('category_id').AsInteger    := sender.CategoryId;

            Post();

            if isNew then begin
                sender.ID := FieldByName('id').AsInteger;
            end;
        except
            on e : Exception do begin
                Cancel();
                MessageDlg('Произошла ошибка при изменении узла'#13 +
                            e.ClassName + ' ' + e.Message, mtError, [mbOk],0);
            end;
        end;
    end;
end;

Заключение

История развития этого компонента в 3 раза дольше чем мой опыт программирования в Delphi. Из-за существующих пробелов в документации может быть сложновато осваивать работу с компонентом, тем не менее основные принципы описаны. Google обычно может направить вас в верном направлении, например, мне было не совсем очеивден способ, как создать чекбоксы в дополнительных колонках. Немножко поискав, понмимаешь, что приедтся рисовать их самому. Хотелось бы конечно, чтобы дерево могло строиться самостоятельно, используя набор данных. Но здесь компонент сохраняет свой нейтралитет и истинную виртуальность по отношению к данным, оставляя вам просто для творчества. В целом, поняв идеологию работы, использование компонента становится весьма простым.

Метки:  VirualTreeView 

Комментарии

Анатолий Краснов
30.01.2013 в 18:23
Если внимательно почитать документацию, то на OnFreeNode необходимо еще проделать ряд действий. Во всяком случае в Вашем случае необходимо сделать присвоение строкам пустых значений. Например:
title :='';
Иначе получите утечку памяти. Маленькую, но утечку :)
teran
30.01.2013 в 18:19

честно сказать посмотрел документацию, ничего кроме


You should however finalize the data in such a case if it contains references to external memory objects (e.g. variants, strings, interfaces).

там не нашел. следующий код, в ХЕ2 об утечках не рапортует, а я так понимаю в VST механизм примерно такой же.


program Project4;
{$APPTYPE CONSOLE}
type
    TTest = record
       value : string;
    end;
    PTest = ^TTest;
var p : PTest;
begin
    ReportMemoryLeaksOnShutdown := true;
    p := AllocMem(sizeof(TTest));
    p.value := 'qwe';
    Dispose(p);
end.
Smike
30.01.2013 в 21:45
Строки освобождаются автоматически. Но... при завершении программы :) Поэтому ReportMemoryLeaksOnShutdown не работает. Но лик есть, я тоже помню, что при работе с VirtualTreeView строки нужно было принудительно освобождать.
teran
30.01.2013 в 23:31
так-то при завершении программы освобождается вообще все что менеджер памяти навыделял (: так что в таком случае ReportMemoryLeaks вообще никогда не рапортовал бы об утечках. В данном случае если освободить с FreeMem то утечка будет. Если же перед вызовом FreeMem сделать строку пустой, то утечки нет, о чем речь и шла.
Так что выходит либо Dispose(p) либо p.value := ''; FreeMem(p);
teran
30.01.2013 в 18:34
Вообще говоря в паре с Alloc следовало бы использовать FreeMem, но как пишут тут http://stackoverflow.com/a/5530577/1216425 как раз таки и следует использовать dispose для избежания утечек в т.ч. со строками.
Анатолий Краснов
31.01.2013 в 10:32
>>честно сказать посмотрел документацию, ничего кроме
Место в документации я конечно не помню, но там есть пример как раз со строками.
Правда здесь в комментариях все уже обсудили :) Действительно утечка будет на FreeMem. На Dispose не проверял, но скорее всего не будет. У меня еще утечка появляется, если использовать запись (record). Например, так:
PNodeDataRecord = ^TNodeDataRecord;
TNodeDataRecord = record
FullName: string;
ImageIndex: integer;
SelectedIndex: integer;
StateIndex: integer;
end;
Если не сделать Fullname:='' - потечет :)
Кузан Дмитрий
01.02.2013 в 16:37
Хорошая статья по Virtual Treeview
http://delphigears.blogspot.ru/2011/08/virtual-treeview.html
Alex Zaslav
14.02.2013 в 00:29
Статья понравилась. Некоторые моменты не ясны. Можете ли выложить исходник или как-то иначе, чтобы понять до конца применение VT с БД?
teran
14.02.2013 в 11:42
Исходник полностью выкладывать не буду, так как проект с работы, а не личный.
Что касается работы с БД, то пример начального заполнения дерева из датасетов был приведен (в данном случае, дерево заполняется полностью и сразу, т.к. количество узлов мало). Приведен правда только для категорий, но датасета всего два - CatagegoriesQuery & SourcesQuery. Каждый пользовательский узел хранит соответствующий ID из БД. У узла есть событие onNodeChanged, которое вызывается при смене имени, клике в чекбоксы, или перемещении узла в другую категорию (смене категории). Обработчик события имеет следующий код:
procedure TManageSourcesForm.SourceNodeChanged(Sender: PSourceNode);
var isNew : boolean;
begin
    if not FCanManageSources then exit;

    with SourcesQuery do begin

        isNew := (sender.ID = -1);

        if  isNew then
            Insert()
        else begin
            if not Locate('id', sender.ID, []) then exit;
            Edit();
        end;

        try
            FieldByName('name').AsString := sender.Caption;
            FieldByName('deCategoryMask').AsInteger := sender.DEMask;
            FieldByName('category_id').AsInteger    := sender.CategoryId;

            Post();

            if isNew then begin
                sender.ID := FieldByName('id').AsInteger;
            end;
        except
            on e : Exception do begin
                Cancel();
                MessageDlg('....', mtError, [mbOk],0);
            end;
        end;
    end;
end;

т.е. если узел новый то ID = -1, переводим датасет в режим вставки и записываем поля. Если было редактирование, то делаем Edit, и опять таки записываем поля.
Удаление делается в отдельном методе, но там тоже ничего сложного - locate по ID и Delete().

В общем в данном случае структура таблиц (всего две) и число записей в них делают код достаточно простым. В более сложных случаях, механизмы работы будут другими, и дерево не должно заполняться целиком сразу, а только по необходимости отобразить тот или иной узел.
Владимир
28.02.2013 в 10:24

Все перерыл, но процедуры OnNodeChanged не нашел: procedure TManageSourcesForm.SourceNodeChanged(Sender: PSourceNode); Есть только OnChange: procedure TfmPermissions.RoleObjectTreeChange(Sender: TBaseVirtualTree; Node: PVirtualNode);

Владимир
28.02.2013 в 11:18

А, вижу, она у Вас в описании записи: TSourceNode = record Но, видимо, опять у меня пробел в знаниях - в теле записи (record) можно писать методы??

teran
28.02.2013 в 11:44
OnNodeChanged это не процедура в данном случае а событие (т.е. событие, которое мы сами генерируем при изменении названия, маски или категории), а в от форма уже это событие обрабатывает с помощью метода SourceNodeChanged.
В целом да, записи уже достаточно давно позволяют включать процедуры и функции. В данном случае имеются 3 процедуры SetMask, SetCaption, SetCatID и еще 2 для Init*.
Вообще записи могут содержать почти все те же элементы что и классы, за исключением деструктора, конечно исключая часть с наследованием и поддержкой интерфейсов.
Владимир
01.03.2013 в 10:35
В delphi 2010 следующие элементы не опознаются:
TThemedElementDetails
TStyleManager
GetElementDetails
GetElementSize

Можно ли реализовать чекбоксы в столбцах в Delphi 2010?
teran
01.03.2013 в 10:18
TStyleManager и все остальное появилось начиная с XE2, когда ввели визуальные стили для приложения.

конечно, в D2010 и более ранних версиях все это тоже можно сделать. Вот, например, вариант рисования чекбоксов в TStringGrid - http://stackoverflow.com/questions/5306037/how-to-set-a-checkbox-tstringgrid-in-delphi .
Владимир
01.03.2013 в 11:30
Принципы отрисовки в TStringGrid и в TVirtualStringTree те же самые? (у меня VirtualStringTree)
teran
01.03.2013 в 16:52
да, принцип отрисовки в любом месте будет одинаков.
Компонент предоставит участок канвы для рисования, а вы в свою очередь запросите у системы изображение чекбокса в разных состояниях, после чего отрисовываете это изображение в нужном месте на предоставленной канве.
В случае со StringGrid примере по ссылке рисование на канве грида с помощью метода DrawThemeBackground где первым параметром является дескриптор канвы (StringGrid1.canvas.handle).
Вот собственно для VirtualStringTree канва придет в качестве параметра TargetCanvas метода AfterCellPaint
Павел
16.07.2013 в 20:57
Будет ещё один "подход к снаряду"? Компонент хороший, а толковых статей по нему мало.
teran
06.08.2013 в 16:02
Может и будет :) с тех пор просто компонент не использовал в задачах :)
Дмитрий
10.11.2013 в 16:38
при добавлении узлов нет скролла
хотя все настройки установлены
почему так?
что сделать чтобы появился?
PAULO
08.05.2014 в 02:25
Boa Noite.
Sou do brasil.
Gostaria de saber se tem como vc disponibilizar para mim os fontes do projeto exemplo de
Primeiro uso VirtualTreeview (TVirtualStringTree)
Postado em 2013/01/30, às 14:10
Quero começar a trabalhar com ele e gostei do seu post.
Obrigado.
Андрей
21.09.2015 в 14:50
Какие настройки надо включить, чтобы добиться максимальной скорости работы TVirtualStringTree?
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно