65.9K
CodeProject 正在变化。 阅读更多。
Home

三态树视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (26投票s)

2011 年 5 月 26 日

CPOL

7分钟阅读

viewsIcon

180152

downloadIcon

13477

为目录浏览和安装程序设计的 Tri-State Tree View。

TriStateTreeView.png Property.png

引言

Tri-State Tree Views 是带有复选框的 Tree View 控件,允许三种状态 - Checked(选中)、UnChecked(未选中)和 Mixed(混合)。Checked 和 UnChecked 不言而喻,与往常一样工作,但 Mixed 状态是新的,用于指示并非所有子节点都具有相同状态。这些控件现在被更频繁地使用,特别是在安装程序或选择目录执行某些操作时,但标准的 .NET Tree View 控件仍然只允许带有标准两种状态的复选框。此处提供的控件允许使用所有三种状态。

背景

已经有一些 Tri-State Tree View 控件可用,但似乎没有一个适合处理目录结构。

在此实现中,我试图保留现有商业 Tri-State Tree Views 的功能 - 特别是每个节点都记住它是否被勾选(“Checked”),并且更改子节点的勾选状态不会改变父节点的勾选状态。此外,该控件可以通过键盘完全操作,无需使用鼠标即可更改节点的状态。为了性能,当节点的状态更改时,只重新计算受影响的节点 - 这即使对于大型树也能保持快速的性能。

除非您在树创建后或节点展开时添加新节点(稍后将详细介绍),否则您的应用程序无需进行特殊编码来处理新功能。

这些状态是通过设置每个节点的 StateImageIndexSystem.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 模式
© . All rights reserved.