Search code examples
delphidelphi-7virtualtreeviewtvirtualstringtree

Is it possible to use VirtualStringTree for a master detail grid view?


Alright I got something really tricky here... I would like to DRAW/USE Headers to a ChildNode. I think the idea is reasonable because it would look amazing to have headers in subnodes so the childnodes can be specified in a table. Is there a feature that VST has or is it not possible at all?

Thanks for your help.


Solution

  • 1. Is there a way to use VirtualTreeView for a master / detail grid view ?

    No, there is no such feature available at this time and IMHO won't be, since that would involve a very big intervention to an existing code.

    2. How to create fully functional header for a child node detail grid view ?

    Considering few ways, how to simulate header look and behavior for child nodes I've found useful to use nested tree views for a detail grid view. This brings you the separateness for your detail data and allows you to minimize the whole simulation to positioning of the nested tree view into a child node's rectangle.

    2.1. Startup project

    In the following project I'm trying to show how complicated could be implement such an easy task like the positioning of a control inside of a child node could be (without involving the original VirtualTree code). Take it just as a startup project, not as a final solution.

    2.2. Known issues & limitations:

    • this project was written and tested to use only one child per root node, so don't be surprised with a behavior when you exceed this limit, because this was not designed nor even tested for
    • when a double click column resize of a main tree animates the column resize, the nested tree views are overdrawn with lines when the canvas is being scrolled by the ScrollDC function
    • to keep the VirtualTree code without changing I've overrided the method for scroll bars updating. It is used to update nested tree views bounds whenever the scrollbars needs to be updated
    • current OnExpanded implementation fires the event before the range and scroll positions are fixed, what makes the code more complicated and with a big weakness - the bounds of a detail tree view are updated after the tree is shown, what can be sometimes visible

    2.3. Project code

    It was written and tested in Delphi 2009 with respect to use in Delphi 7. For commented version of a next code follow this link:

    Unit1.pas

    unit Unit1;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, StdCtrls, VirtualTrees;
    
    type
      TVTScrollBarsUpdateEvent = procedure(Sender: TBaseVirtualTree; DoRepaint: Boolean) of object;
      TVirtualStringTree = class(VirtualTrees.TVirtualStringTree)
      private
        FOnUpdateScrollBars: TVTScrollBarsUpdateEvent;
      public
        procedure UpdateScrollBars(DoRepaint: Boolean); override;
      published
        property OnUpdateScrollBars: TVTScrollBarsUpdateEvent read FOnUpdateScrollBars write FOnUpdateScrollBars;
      end;
    
    type
      PNodeSubTree = ^TNodeSubTree;
      TNodeSubTree = class
        FChildTree: TVirtualStringTree;
      end;
    
    type
      TForm1 = class(TForm)
        Button1: TButton;
        VirtualStringTree1: TVirtualStringTree;
        procedure FormCreate(Sender: TObject);
        procedure Button1Click(Sender: TObject);
        procedure VirtualStringTree1AfterAutoFitColumns(Sender: TVTHeader);
        procedure VirtualStringTree1BeforeDrawTreeLine(Sender: TBaseVirtualTree;
          Node: PVirtualNode; Level: Integer; var PosX: Integer);
        procedure VirtualStringTree1Collapsed(Sender: TBaseVirtualTree;
          Node: PVirtualNode);
        procedure VirtualStringTree1ColumnResize(Sender: TVTHeader;
          Column: TColumnIndex);
        procedure VirtualStringTree1Expanded(Sender: TBaseVirtualTree;
          Node: PVirtualNode);
        procedure VirtualStringTree1FocusChanging(Sender: TBaseVirtualTree; OldNode,
          NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex;
          var Allowed: Boolean);
        procedure VirtualStringTree1FreeNode(Sender: TBaseVirtualTree;
          Node: PVirtualNode);
        procedure VirtualStringTree1MeasureItem(Sender: TBaseVirtualTree;
          TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
      private
        procedure InvalidateSubTrees(Tree: TBaseVirtualTree);
        procedure ResizeSubTrees(Tree: TBaseVirtualTree);
        procedure UpdateSubTreeBounds(Tree: TBaseVirtualTree; Node: PVirtualNode);
        procedure OnUpdateScrollBars(Sender: TBaseVirtualTree; DoRepaint: Boolean);
      public
        { Public declarations }
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    { TVirtualStringTree }
    
    procedure TVirtualStringTree.UpdateScrollBars(DoRepaint: Boolean);
    begin
      inherited;
      if HandleAllocated and Assigned(FOnUpdateScrollBars) then
        FOnUpdateScrollBars(Self, DoRepaint);
    end;
    
    { TForm1 }
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      ReportMemoryLeaksOnShutdown := True;
      VirtualStringTree1.NodeDataSize := SizeOf(TNodeSubTree);
      VirtualStringTree1.OnUpdateScrollBars := OnUpdateScrollBars;
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      Data: PNodeSubTree;
      Node: PVirtualNode;
    begin
      Node := VirtualStringTree1.AddChild(nil);
      Node := VirtualStringTree1.AddChild(Node);
      VirtualStringTree1.InitNode(Node);
      Data := VirtualStringTree1.GetNodeData(Node);
      Data^ := TNodeSubTree.Create;
      Data^.FChildTree := TVirtualStringTree.Create(nil);
      with Data.FChildTree do
      begin
        Visible := False;
        Parent := VirtualStringTree1;
        Height := 150;
        DefaultNodeHeight := 21;
        Header.AutoSizeIndex := 0;
        Header.Font.Charset := DEFAULT_CHARSET;
        Header.Font.Color := clWindowText;
        Header.Font.Height := -11;
        Header.Font.Name := 'Tahoma';
        Header.Font.Style := [];
        Header.Height := 21;
        Header.Options := [hoColumnResize, hoDrag, hoShowSortGlyphs, hoVisible];
        TabStop := False;
        with Header.Columns.Add do
        begin
          Width := 100;
          Text := 'Header item 1';
        end;
        with Header.Columns.Add do
        begin
          Width := 100;
          Text := 'Header item 2';
        end;
      end;
    end;
    
    procedure TForm1.VirtualStringTree1AfterAutoFitColumns(Sender: TVTHeader);
    begin
      InvalidateSubTrees(Sender.Treeview);
    end;
    
    procedure TForm1.VirtualStringTree1BeforeDrawTreeLine(Sender: TBaseVirtualTree;
      Node: PVirtualNode; Level: Integer; var PosX: Integer);
    begin
      if Level = 1 then
        PosX := 0;
    end;
    
    procedure TForm1.VirtualStringTree1Collapsed(Sender: TBaseVirtualTree;
      Node: PVirtualNode);
    var
      Data: PNodeSubTree;
    begin
      Data := VirtualStringTree1.GetNodeData(Node.FirstChild);
      if Assigned(Data^) and Assigned(Data^.FChildTree) then
        Data^.FChildTree.Visible := False;
    end;
    
    procedure TForm1.VirtualStringTree1ColumnResize(Sender: TVTHeader;
      Column: TColumnIndex);
    begin
      ResizeSubTrees(Sender.Treeview);
    end;
    
    procedure TForm1.VirtualStringTree1Expanded(Sender: TBaseVirtualTree;
      Node: PVirtualNode);
    var
      Data: PNodeSubTree;
    begin
      Data := VirtualStringTree1.GetNodeData(Node.FirstChild);
      if Assigned(Data^) and Assigned(Data^.FChildTree) then
        Data^.FChildTree.Visible := True;
    end;
    
    procedure TForm1.VirtualStringTree1FocusChanging(Sender: TBaseVirtualTree;
      OldNode, NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex;
      var Allowed: Boolean);
    begin
      if Sender.GetNodeLevel(NewNode) = 1 then
      begin
        Allowed := False;
        if Sender.AbsoluteIndex(OldNode) > Sender.AbsoluteIndex(NewNode) then
          Sender.FocusedNode := Sender.GetPreviousSibling(OldNode)
        else
        if OldNode <> Sender.GetLastChild(nil) then
          Sender.FocusedNode := Sender.GetNextSibling(OldNode)
        else
          Sender.FocusedNode := OldNode;
      end;
    end;
    
    procedure TForm1.VirtualStringTree1FreeNode(Sender: TBaseVirtualTree;
      Node: PVirtualNode);
    var
      Data: PNodeSubTree;
    begin
      Data := VirtualStringTree1.GetNodeData(Node);
      if Assigned(Data^) then
      begin
        if Assigned(Data^.FChildTree) then
          Data^.FChildTree.Free;
        Data^.Free;
      end;
    end;
    
    procedure TForm1.VirtualStringTree1MeasureItem(Sender: TBaseVirtualTree;
      TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
    var
      Data: PNodeSubTree;
    begin
      if VirtualStringTree1.GetNodeLevel(Node) = 1 then
      begin
        Data := VirtualStringTree1.GetNodeData(Node);
        if Assigned(Data^) and Assigned(Data^.FChildTree) then
          NodeHeight := Data^.FChildTree.Height + 8;
      end;
    end;
    
    procedure TForm1.InvalidateSubTrees(Tree: TBaseVirtualTree);
    var
      Data: PNodeSubTree;
      Node: PVirtualNode;
    begin
      Node := Tree.GetFirst;
      while Assigned(Node) do
      begin
        if Tree.HasChildren[Node] then
        begin
          Data := Tree.GetNodeData(Node.FirstChild);
          if Assigned(Data^) and Assigned(Data^.FChildTree) then
          begin
            Data^.FChildTree.Header.Invalidate(nil);
            Data^.FChildTree.Invalidate;
          end;
        end;
        Node := Tree.GetNextSibling(Node);
      end;
    end;
    
    procedure TForm1.ResizeSubTrees(Tree: TBaseVirtualTree);
    var
      Node: PVirtualNode;
    begin
      Node := Tree.GetFirst;
      while Assigned(Node) do
      begin
        if Tree.HasChildren[Node] then
          UpdateSubTreeBounds(Tree, Node.FirstChild);
        Node := Tree.GetNextSibling(Node);
      end;
    end;
    
    procedure TForm1.UpdateSubTreeBounds(Tree: TBaseVirtualTree; Node: PVirtualNode);
    var
      R: TRect;
      Data: PNodeSubTree;
    begin
      if Assigned(Node) then
      begin
        Data := Tree.GetNodeData(Node);
        if Assigned(Data^) and Assigned(Data^.FChildTree) and
          Data^.FChildTree.Visible then
        begin
          R := Tree.GetDisplayRect(Node, -1, False, True);
          R.Left := R.Left + (Tree as TVirtualStringTree).Indent;
          R.Top := R.Top + 4;
          R.Right := R.Right - 8;
          R.Bottom := R.Bottom - 4;
          Data^.FChildTree.BoundsRect := R;
        end;
      end;
    end;
    
    procedure TForm1.OnUpdateScrollBars(Sender: TBaseVirtualTree; DoRepaint: Boolean);
    begin
      ResizeSubTrees(Sender);
    end;
    
    end.
    

    Unit1.dfm

    object Form1: TForm1
      Left = 0
      Top = 0
      Caption = 'Form1'
      ClientHeight = 282
      ClientWidth = 468
      Color = clBtnFace
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'Tahoma'
      Font.Style = []
      OldCreateOrder = False
      OnCreate = FormCreate
      DesignSize = (
        468
        282)
      PixelsPerInch = 96
      TextHeight = 13
      object VirtualStringTree1: TVirtualStringTree
        Left = 8
        Top = 8
        Width = 371
        Height = 266
        Anchors = [akLeft, akTop, akRight, akBottom]
        Header.AutoSizeIndex = 0
        Header.Font.Charset = DEFAULT_CHARSET
        Header.Font.Color = clWindowText
        Header.Font.Height = -11
        Header.Font.Name = 'Tahoma'
        Header.Font.Style = []
        Header.Height = 21
        Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoShowSortGlyphs, hoVisible]
        TabOrder = 0
        TreeOptions.MiscOptions = [toVariableNodeHeight]
        OnAfterAutoFitColumns = VirtualStringTree1AfterAutoFitColumns
        OnBeforeDrawTreeLine = VirtualStringTree1BeforeDrawTreeLine
        OnCollapsed = VirtualStringTree1Collapsed
        OnColumnResize = VirtualStringTree1ColumnResize
        OnExpanded = VirtualStringTree1Expanded
        OnFocusChanging = VirtualStringTree1FocusChanging
        OnFreeNode = VirtualStringTree1FreeNode
        OnMeasureItem = VirtualStringTree1MeasureItem
        ExplicitWidth = 581
        ExplicitHeight = 326
        Columns = <
          item
            Position = 0
            Width = 75
            WideText = 'Column 1'
          end
          item
            Position = 1
            Width = 75
            WideText = 'Column 2'
          end
          item
            Position = 2
            Width = 75
            WideText = 'Column 3'
          end>
      end
      object Button1: TButton
        Left = 385
        Top = 8
        Width = 75
        Height = 25
        Anchors = [akTop, akRight]
        Caption = 'Button1'
        TabOrder = 1
        OnClick = Button1Click
        ExplicitLeft = 595
      end
    end
    

    2.4. Screenshot

    enter image description here