树形复选框 - 添加复选框行为

4
我正在创建带有复选框的 Treeview。我已经弄清如何将复选框切换到节点(procedure ToggleTreeViewCheckBoxes)。我添加了一个带有复选框位图的 TImageList 组件,并在 OnClick 树形视图事件中更改了 StateIndex。它运行得很好,但我想添加额外的行为。
我创建了一个树形结构示例:
  • Root 1

    • Parent 1 (复选框)

      • Child 1 (复选框)
      • Child 2 (复选框)
    • Parent 2 (复选框)

      • Child 1 (复选框)
      • Child 2 (复选框)
  • Root 2

    • Parent 1 (复选框)

      • Child 1 (复选框)
      • Child 2 (复选框)
    • Parent 2 (复选框)

      • Child 1 (复选框)
      • Child 2 (复选框)
以下是我准备好的带有树形视图、节点和复选框的示例代码。
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.

问题:

1) 如何实现:如果我点击任何一个父复选框节点,所有子节点都取消勾选?

2) 你知道有更好的方法动态添加节点并为所有子节点设置StateIndex吗?我的意思是不要每次都使用ChildNode.StateIndex := 1;这一行。


标准 Win32 TreeView 控件本身支持复选框(请参见 TVS_CHECKBOXES 窗口样式),但是 TTreeView VCL 组件至今仍不支持该功能(RSP-15270)。 - Remy Lebeau
没错,但我找到了这个方法:“您可以通过重写TTreeView的CreateParams过程并为控件指定TVS_CHECKBOXES样式来向树形视图添加复选框。结果是树形视图中的所有节点都将附带复选框。此外,无法再使用StateImages属性,因为WC_TREEVIEW在内部使用此图像列表来实现复选框。” 我不会在每个节点上添加复选框。 - astack
1个回答

9

1) 我该如何实现这样的功能:如果我点击任意父复选框节点,所有子节点都将取消选中?

您需要手动迭代所有的子节点,例如:

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) 你知道有没有更好的方法来动态添加节点并设置所有子节点的StateIndex吗?我的意思是不用每次都写 ChildNode.StateIndex := 1;

很抱歉,这可能是唯一的方法。但是你可以将其封装在一个函数中:

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;

或者,您可以创建一个类助手(您可以将其用于切换逻辑):

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;    

如果您使用的是不支持类助手的早期版本的Delphi,则可以从 TTreeNode 派生一个类,然后将其与 TreeView 的 OnCreateNodeClass 事件一起使用。例如:
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;

首先,感谢您详尽而有价值的回复。我发现第一个问题还有一个问题(在单击父节点时取消选中子节点)。例如:如果我们进入子节点并将其选中,然后选中父节点,所有子节点都将取消选中。您知道如何解决这个问题吗? - astack
@astack:嗯,这就是你要求的:“如果我点击任何父复选框节点,则所有子节点都将取消选择。” 你想要它做什么?如果父节点被选中,则所有子节点应该被选中,如果父节点未被选中,则所有子节点应该被取消选择?那样会有意义。只需相应地更改SetCheckBoxes()的最后一个参数(或完全删除该参数)。或者,即使父节点被选中,您是否希望所有子节点都不被选中,但跳过先前已选中的任何子节点?那就变得更加复杂了。 - Remy Lebeau
我的意思是:“如果我在_cStateChecked_模式下点击父节点,则所有子节点都将取消选中”。问题在于,如果我选中了一些子节点并且我在_cStateUnchecked_中点击父节点,则所有子节点都将取消选中。在这种情况下,子节点应该独立于父节点,并且我应该能够保持它们的选中状态。你知道我的意思吗? - astack
如果一个父节点当前被选中并且单击取消选中它,那么它的所有子节点都应该取消选中,对吗?所示代码正是如此。如果一个父节点当前未被选中并且单击选中它,那么已经选中的子节点应该保持选中状态,对吗?那么当前未被选中的子节点呢?它们应该切换到选中状态还是保持未选中状态?如果是前者,则在将父节点切换为选中状态时,将SetCheckBoxes()的第二个参数设置为cStateChecked。如果是后者,则只切换父节点本身,完全忽略其子节点。 - Remy Lebeau
如果父节点当前被选中,点击后应取消所有子节点的选中状态(这个功能正常工作),但是如果父节点当前未被选中,我们点击选中时,不应该选中所有子节点,而应该保持它们的状态(这个功能也正常工作)。唯一的问题只出现在一个情况下,例如:父节点未被选中,但其中一个子节点被选中,然后我们点击未被选中的父节点以选中它——在这种情况下,所有的子节点都会被取消选中,但是我们应该保持它们的状态。希望现在你已经明白了。 - astack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接