4
votes

I am working on creating Treeview with checkboxes. I figured it out how to toggle checkboxes to nodes (procedure ToggleTreeViewCheckBoxes). I have added TImageList component with checkbox bitmaps and change StateIndex in OnClick treeview event. It works fine, but I would like to add additional behaviour to that.

I created treeview structure as an example:

  • Root 1

    • Parent 1 (checkbox)

      • Child 1 (checkbox)
      • Child 2 (checkbox)
    • Parent 2 (checkbox)

      • Child 1 (checkbox)
      • Child 2 (checkbox)
  • Root 2

    • Parent 1 (checkbox)

      • Child 1 (checkbox)
      • Child 2 (checkbox)
    • Parent 2 (checkbox)

      • Child 1 (checkbox)
      • Child 2 (checkbox)

Below I am attaching you sample code which I have prepared with creating treeview and adding nodes, checkboxes.

unit TreeViewCheckboxes;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.ImageList, Vcl.ImgList,
  Vcl.ComCtrls;

type
  TForm5 = class(TForm)
    ImageList1: TImageList;
    TreeView1: TTreeView;
    procedure FormCreate(Sender: TObject);
    procedure TreeView1Click(Sender: TObject);
  private
    { Private declarations }
    procedure ToggleTreeViewCheckBoxes(Node:TTreeNode; cUnChecked, cChecked: Integer);
  public
    { Public declarations }
  end;

var
  Form5: TForm5;

const
  cStateUnCheck = 1;
  cStateChecked = 2;

   aRootList: Array[1..2] of String =
   (
      'Root 1',
      'Root 2'
   );

implementation

{$R *.dfm}

{ TForm5 }

procedure TForm5.FormCreate(Sender: TObject);
var
   RootNode: TTreeNode;
   ParentNode: TTreeNode;
   ChildNode: TTreeNode;
   i: Integer;
begin
   for i := 1 to High(aRootList) do
   begin
      RootNode := TreeView1.Items.Add(nil, aRootList[i]);

      ParentNode := TreeView1.Items.AddChild(RootNode, 'Parent 1');
      ParentNode.StateIndex := 1;
      ChildNode := TreeView1.Items.AddChild(ParentNode, 'Child 1');
      ChildNode.StateIndex := 1;
      ChildNode := TreeView1.Items.AddChild(ParentNode, 'Child 2');
      ChildNode.StateIndex := 1;    

      ParentNode := TreeView1.Items.AddChild(RootNode, 'Parent 2');
      ParentNode.StateIndex := 1;
      ChildNode := TreeView1.Items.AddChild(ParentNode, 'Child 1');
      ChildNode.StateIndex := 1;
      ChildNode := TreeView1.Items.AddChild(ParentNode, 'Child 2');
      ChildNode.StateIndex := 1;
   end;
end;

procedure TForm5.ToggleTreeViewCheckBoxes(Node: TTreeNode; cUnChecked,
  cChecked: Integer);
begin
   if Assigned(Node) then
   begin
      if Node.StateIndex = cUnChecked then
         Node.StateIndex := cChecked
      else if Node.StateIndex = cChecked then
         Node.StateIndex := cUnChecked;
   end;
end;

procedure TForm5.TreeView1Click(Sender: TObject);
var
   P: TPoint;
begin
   GetCursorPos(P);
   P := TreeView1.ScreenToClient(P);
   if (htOnStateIcon in TreeView1.GetHitTestInfoAt(P.X, P.Y)) then
      ToggleTreeViewCheckBoxes(TreeView1.Selected, cStateUnCheck, cStateChecked);
end;    
end.

Questions:

1) How can I do something like that: If I click on any Parent checkbox node, all child nodes are unchecked?

2) Do you know any better way to dynamically add nodes and set StateIndex for all childs? I mean no every time use line ChildNode.StateIndex := 1;

1
Standard Win32 TreeView controls natively support checkboxes (see the TVS_CHECKBOXES window style), but the TTreeView VCL component still to this day does not support that feature (RSP-15270). - Remy Lebeau
That's true, but I found it: "You can add checkboxes to the treeview by overriding the CreateParams procedure of the TTreeView, specifying the TVS_CHECKBOXES style for the control.The result is that all nodes in the treeview will have checkboxes attached to them. In addition, the StateImages property can't be used anymore because the WC_TREEVIEW uses this imagelist internally to implement checkboxes". I am not going add checkboxes in every node. - astack

1 Answers

8
votes

1) How can I do something like that: If I click on any Parent checkbox node, all child nodes are unchecked?

You have to manually iterate through all of the children, eg:

procedure TForm5.SetTreeViewCheckState(Node: TTreeNode; StateIndex: Integer; Recursive: Boolean);
begin
  Node.StateIndex := StateIndex;
  if not Recursive then Exit;
  for I := 0 to Node.Count-1 do
    SetTreeViewCheckState(Node.Item[I], StateIndex, True);
end;

procedure TForm5.ToggleTreeViewCheckBoxes(Node: TTreeNode);
var
  I: Integer;
begin
  if Assigned(Node) then
  begin
    if Node.StateIndex = cStateUnCheck then
      SetTreeViewCheckState(Node, cStateChecked, False);
    else if Node.StateIndex = cStateChecked then
      SetTreeViewCheckState(Node, cStateUnCheck, True);
  end;
end;

2) Do you know any better way to dynamically add nodes and set StateIndex for all childs? I mean no every time use line ChildNode.StateIndex := 1;

Sorry, but that is the only way to do it. But you can wrap it in a function:

procedure TForm5.FormCreate(Sender: TObject);
var
  RootNode: TTreeNode;
  ParentNode: TTreeNode;
  i: Integer;
begin
  for i := Low(aRootList) to High(aRootList) do
  begin
    RootNode := TreeView1.Items.Add(nil, aRootList[i]);

    ParentNode := AddChildNodeWithState(RootNode, 'Parent 1');
    AddChildNodeWithState(ParentNode, 'Child 1');
    AddChildNodeWithState(ParentNode, 'Child 2');

    ParentNode := AddChildNodeWithState(RootNode, 'Parent 2');
    AddChildNodeWithState(ParentNode, 'Child 1');
    AddChildNodeWithState(ParentNode, 'Child 2');
  end;
end;

function TForm5.AddChildNodeWithState(AParentNode: TTreeNode, const ACaption: String; AStateIndex: Integer = 1): TTreeNode;
begin
  Result := TreeView1.Items.AddChild(AParentNode, ACaption);
  Result.StateIndex := AStateIndex;
end;

Or, you can create a class helper (which you can use for the toggling logic as well):

type
  TTreeNodeHelper = class helper for TTreeNode
  public
    function AddChildWithState(const ACaption: string; AStateIndex: Integer = 1): TTreeNode;
    procedure SetCheckState(StateIndex: Integer; Recursive: Boolean);
    procedure ToggleCheckState;
  end;

function TTreeNodeHelper.AddChildWithState(const ACaption: string; AStateIndex: Integer = 1): TTreeNode;
begin
  Result := Self.TreeView.Items.AddChild(Self, ACaption);
  Result.StateIndex := AStateIndex;
end;

procedure TTreeNodeHelper.SetCheckState(StateIndex: Integer; Recursive: Boolean);
begin
  Self.StateIndex := StateIndex;
  if not Recursive then Exit;
  for I := 0 to Self.Count-1 do
    Self.Item[I].SetCheckState(StateIndex, True);
end;

procedure TTreeNodeHelper.ToggleCheckState;
var
  I: Integer;
begin
  if Self.StateIndex = cStateUnCheck then
    SetCheckState(cStateChecked, False);
  else if Self.StateIndex = cStateChecked then
    SetCheckState(cStateUnCheck, True);
  end;
end;

procedure TForm5.FormCreate(Sender: TObject);
var
  RootNode: TTreeNode;
  ParentNode: TTreeNode;
  i: Integer;
begin
  for i := Low(aRootList) to High(aRootList) do
  begin
    RootNode := TreeView1.Items.Add(nil, aRootList[i]);

    ParentNode := RootNode.AddChildWithState('Parent 1');
    ParentNode.AddChildWithState('Child 1');
    ParentNode.AddChildWithState('Child 2');

    ParentNode := RootNode.AddChildWithState('Parent 2');
    ParentNode.AddChildWithState('Child 1');
    ParentNode.AddChildWithState('Child 2');
  end;
end;

procedure TForm5.TreeView1Click(Sender: TObject);
var
  P: TPoint;
begin
  GetCursorPos(P);
  P := TreeView1.ScreenToClient(P);
  if (htOnStateIcon in TreeView1.GetHitTestInfoAt(P.X, P.Y)) then
    TreeView1.GetNodeAt(P.X, P.Y).ToggleCheckState;
end;    

If you are using an older version of Delphi that does not support class helpers, you can derive a class from TTreeNode instead and use it with the TreeView's OnCreateNodeClass event, eg:

type
  TMyTreeNode = class(TTreeNode)
  public
    function AddChildWithState(const ACaption: string; AStateIndex: Integer = 1): TTreeNode;
    procedure SetCheckState(StateIndex: Integer; Recursive: Boolean);
    procedure ToggleCheckState;
  end;

function TMyTreeNode.AddChildWithState(const ACaption: string; AStateIndex: Integer = 1): TTreeNode;
begin
  Result := Self.TreeView.Items.AddChild(Self, ACaption);
  Result.StateIndex := AStateIndex;
end;

procedure TMyTreeNode.SetCheckState(StateIndex: Integer; Recursive: Boolean);
begin
  Self.StateIndex := StateIndex;
  if not Recursive then Exit;
  for I := 0 to Self.Count-1 do
    TMyTreeNode(Self.Item[I]).SetCheckState(StateIndex, True);
end;

procedure TMyTreeNode.ToggleCheckBoxes;
var
  I: Integer;
begin
  if Self.StateIndex = cStateUnCheck then
    SetCheckBoxes(cStateChecked, cStateUnChecked);
  else if Self.StateIndex = cStateChecked then
    SetCheckBoxes(cStateUnCheck, cStateUnChecked);
  end;
end;

procedure TForm5.FormCreate(Sender: TObject);
var
  RootNode: TTreeNode;
  ParentNode: TTreeNode;
  i: Integer;
begin
  for i := Low(aRootList) to High(aRootList) do
  begin
    RootNode := TreeView1.Items.Add(nil, aRootList[i]);

    ParentNode := TMyTreeNode(RootNode).AddChildWithState('Parent 1');
    TMyTreeNode(ParentNode).AddChildWithState('Child 1');
    TMyTreeNode(ParentNode).AddChildWithState('Child 2');

    ParentNode := TMyTreeNode(RootNode).AddChildWithState('Parent 2');
    TMyTreeNode(ParentNode).AddChildWithState('Child 1');
    TMyTreeNode(ParentNode).AddChildWithState('Child 2');
  end;
end;

procedure TForm5.TreeView1Click(Sender: TObject);
var
  P: TPoint;
begin
  GetCursorPos(P);
  P := TreeView1.ScreenToClient(P);
  if (htOnStateIcon in TreeView1.GetHitTestInfoAt(P.X, P.Y)) then
    TMyTreeNode(TreeView1.GetNodeAt(P.X, P.Y)).ToggleCheckState;
end;    

procedure TForm5.TreeView1CreateNodeClass(Sender: TCustomTreeView; var NodeClass: TTreeNodeClass)
begin
  NodeClass := TMyTreeNode;
end;