三态树视图






4.92/5 (26投票s)
为目录浏览和安装程序设计的 Tri-State Tree View。


引言
Tri-State Tree Views 是带有复选框的 Tree View 控件,允许三种状态 - Checked(选中)、UnChecked(未选中)和 Mixed(混合)。Checked 和 UnChecked 不言而喻,与往常一样工作,但 Mixed 状态是新的,用于指示并非所有子节点都具有相同状态。这些控件现在被更频繁地使用,特别是在安装程序或选择目录执行某些操作时,但标准的 .NET Tree View 控件仍然只允许带有标准两种状态的复选框。此处提供的控件允许使用所有三种状态。
背景
已经有一些 Tri-State Tree View 控件可用,但似乎没有一个适合处理目录结构。
在此实现中,我试图保留现有商业 Tri-State Tree Views 的功能 - 特别是每个节点都记住它是否被勾选(“Checked”),并且更改子节点的勾选状态不会改变父节点的勾选状态。此外,该控件可以通过键盘完全操作,无需使用鼠标即可更改节点的状态。为了性能,当节点的状态更改时,只重新计算受影响的节点 - 这即使对于大型树也能保持快速的性能。
除非您在树创建后或节点展开时添加新节点(稍后将详细介绍),否则您的应用程序无需进行特殊编码来处理新功能。
这些状态是通过设置每个节点的 StateImageIndex
(System.Windows.Forms.TreeNode
类的一个标准成员)来处理的,该成员引用了在类构造期间创建的 ImageList
中的图像。勾选状态像往常一样存储在节点的 Checked
成员变量中。
该控件可以两种模式运行 - Standard(标准)和 Installer(安装程序)。
Standard 模式从不更改父节点的 Checked 状态,仅根据子节点的 Checked 状态更改显示的图像。
Installer 模式会自动将父节点设置为 Checked(如果所有子节点都 Checked),或设置为 UnChecked(如果至少有一个子节点 UnChecked)。这种行为对用户是不可见的,仅在以编程方式访问节点状态时才会体现。
Using the Code
将 TriStateTreeView.cs 文件添加到您的项目中并编译。一个新的控件 TriStateTreeView
将出现在 Toolbox
的顶部,可以像其他控件一样添加到您的窗体中。设置控件的 CheckBoxes
属性不是必需的,但如果您愿意,可以进行设置。该控件默认为 Standard
行为,要在安装程序中使用,请在 Properties 中将 Style
设置为 Installer
。
该控件可以像普通的 TreeView
一样使用,无需额外的代码即可使用新功能。在树创建之前添加的节点将自动显示一个 UnChecked 的框。
作为父节点展开时添加的节点将显示与其父节点相同的勾选状态(如果您考虑在磁盘上选择文件夹,选择父文件夹将自动包含所有子文件夹)。
勾选子节点不会导致父节点被勾选(如果您考虑在磁盘上选择文件夹,选择子文件夹不应自动选择父文件夹)。
要显示您自己选择的状态,或在任何其他时间添加节点,有必要编辑节点的 StateImageIndex
属性。
// Not usually required, only use to override default functionality
// Or when adding nodes at unusual times
System.Windows.Forms.TreeNode tn;
tn.StateImageIndex = (int)RikTheVeggie.TriStateTreeView.CheckedState.UnChecked;
tn.StateImageIndex = (int)RikTheVeggie.TriStateTreeView.CheckedState.Checked;
tn.StateImageIndex = (int)RikTheVeggie.TriStateTreeView.CheckedState.Mixed;
使用示例
包含的示例项目在窗口显示之前以编程方式填充树。其他所有节点也是可展开的(它包含一个“dummy”节点,该节点在父节点展开时被替换),并演示了通过 BeforeExpand
事件以编程方式添加新节点。
关于代码
该类派生自标准的 System.Windows.Forms.TreeView
类。使用了一个 enum CheckedState
,以便我们可以通过易于理解的语义名称而不是数字来引用这些状态。创建了一个 IgnoreClickAction
变量,以便我们可以忽略在以编程方式更改节点状态时引发的事件。
enum TriStateStyles
存储树允许的样式,选定的样式存储在 TriStateStyle
变量中。
为了允许从 Properties 窗口选择样式,请在 getter/setter 之前设置 System.ComponentModel.Category
等。
public class TriStateTreeView : System.Windows.Forms.TreeView
{
// <remarks>
// CheckedState is an enum of all allowable nodes states
// </remarks>
public enum CheckedState : int { UnInitialised = -1, UnChecked, Checked, Mixed };
// <remarks>
// IgnoreClickAction is used to ignore messages generated by setting the node.
// Checked flag in code
// Do not set <c>e.Cancel = true</c> in <c>OnBeforeCheck</c>
// otherwise the Checked state will be lost
// </remarks>
int IgnoreClickAction = 0;
// <remarks>
// TriStateStyles is an enum of all allowable tree styles
// All styles check children when parent is checked
// Installer automatically checks parent if all children are checked,
// and unchecks parent if at least one child is unchecked
// Standard never changes the checked status of a parent
// </remarks>
public enum TriStateStyles : int { Standard = 0, Installer };
// Create a private member for the tree style, and allow it to be
// set on the property sheer
private TriStateStyles TriStateStyle = TriStateStyles.Standard;
[System.ComponentModel.Category("Tri-State Tree View")]
[System.ComponentModel.DisplayName("Style")]
[System.ComponentModel.Description("Style of the Tri-State Tree View")]
public TriStateStyles TriStateStyleProperty
{
get { return TriStateStyle; }
set { TriStateStyle = value; }
}
};
构造函数派生了一个新类,该类继承自标准的 System.Windows.Forms.TreeView
类。
public TriStateTreeView() : base()
{
StateImageList = new System.Windows.Forms.ImageList();
// populate the image list, using images from the
// System.Windows.Forms.CheckBoxRenderer class
for (int i = 0; i < 3; i++)
{
// Create a bitmap which holds the relevant check box style
// see http://msdn.microsoft.com/en-us/library/ms404307.aspx and
// http://msdn.microsoft.com/en-us/library/
// system.windows.forms.checkboxrenderer.aspx
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(16, 16);
System.Drawing.Graphics chkGraphics =
System.Drawing.Graphics.FromImage(bmp);
switch ( i )
{
// 0,1 - offset the checkbox slightly so it
// positions in the correct place
case 0:
System.Windows.Forms.CheckBoxRenderer.DrawCheckBox
(chkGraphics, new System.Drawing.Point(0, 1),
System.Windows.Forms.VisualStyles.
CheckBoxState.UncheckedNormal);
break;
case 1:
System.Windows.Forms.CheckBoxRenderer.DrawCheckBox
(chkGraphics, new System.Drawing.Point(0, 1),
System.Windows.Forms.VisualStyles.CheckBoxState.
CheckedNormal);
break;
case 2:
System.Windows.Forms.CheckBoxRenderer.DrawCheckBox
(chkGraphics, new System.Drawing.Point(0, 1),
System.Windows.Forms.VisualStyles.
CheckBoxState.MixedNormal);
break;
}
StateImageList.Images.Add(bmp);
}
}
OnCreateControl
在树首次显示之前由系统调用。默认的 CheckBoxes
功能被禁用,因为我们提供了自己的实现,并增加了额外状态。辅助函数 UpdateChildState()
用于设置每个节点显示一个空的复选框。
protected override void OnCreateControl()
{
base.OnCreateControl();
CheckBoxes = false; // Disable default CheckBox functionality if
// it's been enabled
// Give every node an initial 'unchecked' image
IgnoreClickAction++; // we're making changes to the tree,
// ignore any other change requests
UpdateChildState(this.Nodes, (int)CheckedState.UnChecked, false, true);
IgnoreClickAction--;
}
OnAfterCheck
在复选框的状态被更改时由系统调用,无论是通过用户还是代码。在进行任何工作之前,我们检查 IgnoreClickAction
变量以确保它是安全的(我们不希望在以代码明确更改状态时执行任何操作)。
在 StateImageIndex
中设置新状态后,任何子节点都被设置为相同状态(例如,如果父节点被选中,它会自动选择所有子节点)。最后,任何父节点都会被告知新状态,因为它们可能需要将其自身状态更改为 Mixed
。
protected override void OnAfterCheck(System.Windows.Forms.TreeViewEventArgs e)
{
base.OnAfterCheck(e);
if (IgnoreClickAction > 0)
{
return;
}
IgnoreClickAction++; // we're making changes to the tree,
// ignore any other change requests
// the checked state has already been changed,
// we just need to update the state index
// node is either ticked or unticked. Ignore mixed state,
// as the node is still only ticked or unticked regardless of state of children
System.Windows.Forms.TreeNode tn = e.Node;
tn.StateImageIndex = tn.Checked ? (int)CheckedState.Checked :
(int)CheckedState.UnChecked;
// force all children to inherit the same state as the current node
UpdateChildState(e.Node.Nodes, e.Node.StateImageIndex, e.Node.Checked, false);
// populate state up the tree, possibly resulting in parents with mixed state
UpdateParentState(e.Node.Parent);
IgnoreClickAction--;
}
OnAfterExpand
在节点展开时由系统调用。在此处调用 UpdateChildState
,以防展开节点作为副作用添加了新的子节点(请参阅附加示例中的 OnBeforeExpand
)。
protected override void OnAfterExpand(System.Windows.Forms.TreeViewEventArgs e)
{
// If any child node is new, give it the same check state as the current node
// So if current node is ticked, child nodes will also be ticked
base.OnAfterExpand(e);
IgnoreClickAction++; // we're making changes to the tree,
// ignore any other change requests
UpdateChildState(e.Node.Nodes, e.Node.StateImageIndex, e.Node.Checked, true);
IgnoreClickAction--;
}
辅助函数 UpdateChildState
用于用父节点的状态替换子节点的状态。通常用 ChangeUninitialisedNodesOnly = false
调用,以便节点始终被更改,但也可以用 ChangeUninitialisedNodesOnly = true
调用,以便只更改未初始化的节点(例如,这样我们就不会覆盖用户在创建树时设置的任何显式状态)。
protected void UpdateChildState(System.Windows.Forms.TreeNodeCollection Nodes,
int StateImageIndex, bool Checked, bool ChangeUninitialisedNodesOnly)
{
foreach (System.Windows.Forms.TreeNode tnChild in Nodes)
{
if (!ChangeUninitialisedNodesOnly || tnChild.StateImageIndex == -1)
{
tnChild.StateImageIndex = StateImageIndex;
tnChild.Checked = Checked; // override 'checked' state
// of child with that of parent
if (tnChild.Nodes.Count > 0)
{
UpdateChildState(tnChild.Nodes, StateImageIndex,
Checked, ChangeUninitialisedNodesOnly);
}
}
}
}
辅助函数 UpdateParentState
用于通知父节点其子节点已更改,并且它们可能需要因此更改自身状态。在此处,我们确定是否将节点设为 Mixed。
在 Installer 模式下,父节点的 Checked
状态会根据当前子节点自动更新。
这里的代码可以压缩,但我喜欢显式的 if
语句,因为它使逻辑更容易理解,特别是对于语言新手来说。
protected void UpdateParentState(System.Windows.Forms.TreeNode tn)
{
// Node needs to check all of it's children to see if any of them
// are ticked or mixed
if (tn == null)
return;
int OrigStateImageIndex = tn.StateImageIndex;
int UnCheckedNodes = 0, CheckedNodes = 0, MixedNodes = 0;
// The parent needs to know how many of it's children are Checked or Mixed
foreach (System.Windows.Forms.TreeNode tnChild in tn.Nodes)
{
if (tnChild.StateImageIndex == (int)CheckedState.Checked)
CheckedNodes++;
else if (tnChild.StateImageIndex == (int)CheckedState.Mixed)
{
MixedNodes++;
break;
}
else
UnCheckedNodes++;
}
if (TriStateStyle == TriStateStyles.Installer)
{
// In Installer mode, if all child nodes are checked
// then parent is checked
// If at least one child is unchecked, then parent is unchecked
if (MixedNodes == 0)
{
if (UnCheckedNodes == 0)
{
// all children are checked,
// so parent must be checked
tn.Checked = true;
}
else
{
// at least one child is unchecked,
// so parent must be unchecked
tn.Checked = false;
}
}
}
// Determine the parent's new Image State
if (MixedNodes > 0)
{
// at least one child is mixed, so parent must be mixed
tn.StateImageIndex = (int)CheckedState.Mixed;
}
else if (CheckedNodes > 0 && UnCheckedNodes == 0)
{
// all children are checked
if (tn.Checked)
tn.StateImageIndex = (int)CheckedState.Checked;
else
tn.StateImageIndex = (int)CheckedState.Mixed;
}
else if (CheckedNodes > 0)
{
// some children are checked, the rest are unchecked
tn.StateImageIndex = (int)CheckedState.Mixed;
}
else
{
// all children are unchecked
if (tn.Checked)
tn.StateImageIndex = (int)CheckedState.Mixed;
else
tn.StateImageIndex = (int)CheckedState.UnChecked;
}
if (OrigStateImageIndex != tn.StateImageIndex && tn.Parent != null)
{
// Parent's state has changed, notify the parent's parent
UpdateParentState(tn.Parent);
}
}
可以通过键盘光标键导航该控件,但默认情况下空格键不起作用。通过处理 OnKeyDown
并检查 Space,我们可以通过键盘启用完整功能。
简单地切换节点的 Checked
状态就会导致系统调用 OnAfterCheck
。
protected override void OnKeyDown(System.Windows.Forms.KeyEventArgs e)
{
base.OnKeyDown(e);
// is the keypress a space? If not, discard it
if (e.KeyCode == System.Windows.Forms.Keys.Space)
{
// toggle the node's checked status.
// This will then fire OnAfterCheck
SelectedNode.Checked = !SelectedNode.Checked;
}
}
开发过程中遇到的一个问题是,点击节点上的任何位置(复选框、标签等)都会导致节点 Checked
状态发生变化。通过重写 OnNodeMouseClick
,我能够测试鼠标的当前位置并忽略任何未点击 StateImage
(即 CheckBox
)的点击。
与 OnKeyDown
类似,简单地切换节点的 Checked
状态就会导致系统调用 OnAfterCheck
。
protected override void OnNodeMouseClick
(System.Windows.Forms.TreeNodeMouseClickEventArgs e)
{
base.OnNodeMouseClick(e);
// is the click on the checkbox? If not, discard it
System.Windows.Forms.TreeViewHitTestInfo info = HitTest(e.X, e.Y);
if (info == null || info.Location !=
System.Windows.Forms.TreeViewHitTestLocations.StateImage)
{
return;
}
// toggle the node's checked status. This will then fire OnAfterCheck
System.Windows.Forms.TreeNode tn = e.Node;
tn.Checked = !tn.Checked;
}
关于示例
随附的示例代码演示了在树完全创建之前(即在显示给用户之前)以编程方式添加节点,以及在父节点展开时以编程方式添加节点。
示例是通过启动一个新的 C# Windows Forms Application,将 TriStateTreeView.cs 文件添加到项目中,编译,然后将 TriStateTreeView
控件拖放到窗体(Form1
)上创建的。函数 triStateTreeView1_BeforeExpand
与树的 BeforeExpand
事件相关联。
Form
的构造函数中没有做什么特别的事情,只需调用辅助函数 PopulateTree
并传入空树。
public Form1()
{
UseWaitCursor = true;
InitializeComponent();
PopulateTree(triStateTreeView1.Nodes, "");
UseWaitCursor = false;
}
此辅助函数向指定的子树(ParentNodes
)添加 5 个节点。PreText
放在每个节点标签之前,只是为了让它看起来更漂亮。
为了演示子节点,每隔一个节点会创建一个“dummy”子节点,该节点将在节点展开时被替换。
private void PopulateTree(TreeNodeCollection ParentNodes, string PreText)
{
// Add 5 nodes to the current node. Every other node will have a child
for (int i = 0; i < 5; i++)
{
TreeNode tn = new TreeNode(PreText + (i + 1).ToString());
if (i % 2 == 0)
{
// add a 'dummy' child node which will be replaced
// at runtime when the parent is expanded
tn.Nodes.Add("");
}
// There is no need to set special properties on the node
// if adding it at form creation or when expanding a parent node.
// Otherwise, set
// tn.StateImageIndex =
// (int)RikTheVeggie.TriStateTreeView.CheckedState.UnChecked;
ParentNodes.Add(tn);
}
}
通过处理 BeforeExpand
事件,我们可以检查需要替换为实际数据的“dummy”节点。检测到此类节点时,我们只需将其删除并调用 PopulateTree
,传入父节点的子树和文本。
private void triStateTreeView1_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
// A node in the tree has been selected
TreeView tv = sender as TreeView;
tv.UseWaitCursor = true;
if ((e.Node.Nodes.Count == 1) && (e.Node.Nodes[0].Text == ""))
{
// This is a 'dummy' node. Replace it with actual data
e.Node.Nodes.RemoveAt(0);
PopulateTree(e.Node.Nodes, e.Node.Text);
}
tv.UseWaitCursor = false;
}
关注点
这是在 VS2010 中为 .NET 4 开发和编译的,但很可能也适用于其他版本。如果您在不同条件下成功运行,请发表评论!如果有需求,我将添加一个选项,使其表现得像一个“installer” - 即,当所有 Child
节点都被勾选时,Parent
节点将自动被勾选。
致谢
历史
- 2011 年 5 月 26 日 - 首次上传
- 2011 年 5 月 30 日 - 添加了 Installer 模式