增强 TreeView:自定义 LabelEdit






4.41/5 (19投票s)
2005年10月12日
8分钟阅读

151152

3597
本文介绍如何为TreeView控件的LabelEdit功能添加类似VS解决方案资源管理器的功能,包括标签编辑的前后处理和输入验证。
引言
许多人将Visual Studio IDE视为应用程序GUI的典范。大家都喜欢它整洁的界面,可以随意停靠、滑动和(哇!)选项卡式的布局。大家都渴望在自己的应用程序中重现这种美感。但是,(意外吧!)标准的.NET控件根本无法提供Visual Studio本身所展现功能的一半。好消息是,与它们的ActiveX前身相比,.NET控件提供了更多用于增强和适应的挂钩和处理程序。ActiveX不具备而.NET控件具备的主要功能是继承。控件的对象模型也设计得很巧妙,为适应提供了很多可能性。
在本文中,我们将讨论如何增强TreeView
控件的LabelEdit
功能,使其更像VS环境的解决方案资源管理器中使用的树控件。本文也旨在启动一系列文章,解释如何扩展TreeView
的功能并为其提供一些急需的功能。
问题
与.NET库自带的TreeView
控件相比,它是一个显示分层数据结构和对象的非常方便的方式。然而,与Visual Studio IDE、Windows XP资源管理器和其他顶级GUI应用程序中使用的其他类似控件相比,它的功能受到了很大的限制。一个最新的树控件所期望的主要功能是
- 多选(一次选择多个节点的能力)
- 更丰富的拖放支持,包括放置位置高亮、放置验证、全彩拖动图像
- 节点标签编辑自定义和验证(本文讨论的内容)
- 可插入的外部节点标签编辑器(如
ComboBox
、Calendar
、SpinBox
等) - 节点图像叠加(在常规节点图像上方显示小型属性图像的能力,如感叹号、星号等)
- 内置的节点剪切/复制/粘贴和撤销/重做堆栈支持,但这有争议,因为这些功能非常依赖于具体任务
展开时填充行为,这也被频繁提及为一种理想的树功能,在标准的TreeView
中很容易用很少的代码重现,所以我真的认为我们不应该在这里讨论它。
例如,让我们看看VS树控件(在解决方案资源管理器和类视图中使用)。首先映入眼帘的是在解决方案资源管理器中为根节点实现的自定义标签编辑。在节点文本中,我们可以看到类似这样的内容
Solution 'Solution1' (1 project)
然而,当我们点击它开始标签编辑时,它会变为Solution1
。如果我们将其更改为Solution2
并完成编辑,我们将看到标签变为
Solution 'Solution2' (1 project)
我们看到标签文本在编辑前经过预处理,在编辑后经过后处理。这种行为被称为LabelEdit
自定义,在许多情况下都非常有用。例如,如果我们想允许用户只编辑文件名而不保留扩展名。或者,如果我们想让我们的节点更有自描述性,比如VS中的解决方案节点,例如,方法“GetItems
”或属性“IsCreated
”,但只公开其命名部分供编辑。
乍一看,似乎我们可以使用TreeView
事件BeforeLabelEdit
和AfterLabelEdit
轻松模仿这种行为。这似乎很自然,并且像这样的事件名称只能用于标签文本的预处理和后处理,也许还用于编辑验证(我们将在本文后面讨论)。但事实并非如此!这些事件不允许进行任何这些操作。你很快就会发现,由于BeforeLabelEdit
事件发生在LabelEdit
编辑框显示之后,因此无法从BeforeLabelEdit
事件中更改LabelEdit
框的内容。这真的很奇怪,因为它使这个事件几乎没有意义。很难设想这个事件可以用于任何可能的应用程序。更糟糕的是,你还会发现AfterLabelEdit
事件也完全没有用,因为它不允许你对编辑后的标签文本进行任何后处理。哦,你可以在e.Label
中看到这个文本,虽然它是只读的,并且你可以按你希望的方式更改e.Node.Text
,比如
private void treeView1_AfterLabelEdit(object sender,
System.Windows.Forms.NodeLabelEditEventArgs e) {
e.Node.Text = e.Label + "hahaha";
}
但这一切都是徒劳的,因为更改后的e.Node.Text
只在事件处理程序结束前有效。在没有进一步的事件或通知的情况下,它很快就会被e.Label
的值覆盖。是的,这令人震惊,超乎逻辑,完全无法理解,但这是真的。BeforeLabelEdit
和AfterLabelEdit
事件完全没有用,只是为了嘲弄诚实的.NET程序员。:)
但永远不要放弃。另一种可以快速想到的是通过其他事件(MouseDown
、KeyDown
或菜单点击(MouseUp
和Click
发生得太晚,无法改变任何内容))来拦截用户开始标签编辑的意图。在理解用户确实要开始标签编辑后,我们可以更改TreeView.SelectedNode.Text
,以便LabelEdit
框获得自定义格式的string
进行编辑。这种方法似乎还可以,但在编写测试程序后,你会很快发现节点文本内容在编辑开始前大约半秒钟可见地更改,导致视觉上不舒服的行为。当然,我们在Visual Studio解决方案资源管理器树中看不到这种行为的任何迹象。
因此,在研究了不同的方法并深入研究了TreeView
类的可重写成员之后,我们得出了一个悲伤的结论:没有简单的方法可以实现我们想要的东西。当然,总有可能抑制内置的标签编辑,并在TreeView
控件之上编写自己的编辑功能。这个选项似乎是可行的,尽管很复杂。但恰好还有一个更简单的方法。
解决方案
在.NET基类Control
中有一个非常有用的方法叫做OnNotifyMessage
,它可以作为增强.NET控件的一个漏洞。如果通过在其构造函数中添加以下行来配置控件
this.SetStyle(ControlStyles.EnableNotifyMessage, true);
然后,我们可以通过在继承类中重写此方法来拦截WM_Messages
。编写一个简单的基于ListBox
的监视器来研究不同情况下的wm_messages
序列非常有帮助。如果我们创建一个测试程序来监视TreeView
标签编辑过程中发生的事件和wm_message
序列,我们会很快注意到,在LabelEdit
框出现之前,会有一个特定的WM_TIMER
事件。而那个时刻正是替换节点文本、为其定制编辑的最佳时机。很可能,这个消息是用来区分双击和单击的,毕竟,双击不应该开始标签编辑,对吧?而这一定是用户点击和实际开始标签编辑之间恼人的延迟的原因,这让我们之前如此头疼。
因此,想法是:通过在重写的OnNotifyMessage
方法中拦截WM_TIMER
消息来检测用户开始标签编辑的意图,用自定义版本替换节点文本(例如,将Solution 'Solution1'替换为Solution1,或将filename.ext替换为filename),在重写的OnAfterLabelEdit
方法中拦截AfterLabelEdit
事件,并将编辑后的标签转换回原始格式(例如,将newfilename转换为newfilename.ext)。
private const int WM_TIMER = 0x0113;
private bool TriggerLabelEdit = false;
private string viewedLabel;
private string editedLabel;
protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e) {
// put node label to initial state
// to ensure that in case of label editing cancelled
// the initial state of label is preserved
this.SelectedNode.Text = viewedLabel;
// base.OnBeforeLabelEdit is not called here
// it is called only from StartLabelEdit
}
protected override void OnAfterLabelEdit(NodeLabelEditEventArgs e) {
this.LabelEdit = false;
e.CancelEdit = true;
if(e.Label==null) return;
ValidateLabelEditEventArgs ea =
new ValidateLabelEditEventArgs(e.Label);
OnValidateLabelEdit(ea);
if(ea.Cancel==true) {
e.Node.Text = editedLabel;
this.LabelEdit = true;
e.Node.BeginEdit();
}
else
base.OnAfterLabelEdit(e);
}
public void BeginEdit() {
StartLabelEdit();
}
protected override void OnNotifyMessage(Message m) {
if(TriggerLabelEdit)
if(m.Msg==WM_TIMER) {
TriggerLabelEdit = false;
StartLabelEdit();
}
base.OnNotifyMessage(m);
}
public void StartLabelEdit() {
TreeNode tn = this.SelectedNode;
viewedLabel = tn.Text;
NodeLabelEditEventArgs e =
new NodeLabelEditEventArgs(tn);
base.OnBeforeLabelEdit(e);
editedLabel = tn.Text;
this.LabelEdit = true;
tn.BeginEdit();
}
在OnMouseDown
、OnMouseUp
、OnClick
和OnDoubleClick
方法中应添加以下代码,以涵盖所有可能的LabelEdit
情况。
protected override void OnMouseDown(MouseEventArgs e) {
if(e.Button==MouseButtons.Right) {
TreeNode tn = this.GetNodeAt(e.X, e.Y);
if(tn!=null)
this.SelectedNode = tn;
}
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e) {
TreeNode tn;
if(e.Button==MouseButtons.Left) {
tn = this.SelectedNode;
if(tn==this.GetNodeAt(e.X, e.Y)) {
if(wasDoubleClick)
wasDoubleClick = false;
else {
TriggerLabelEdit = true;
}
}
}
base.OnMouseUp(e);
}
protected override void OnClick(EventArgs e) {
TriggerLabelEdit = false;
base.OnClick(e);
}
private bool wasDoubleClick = false;
protected override void OnDoubleClick(EventArgs e) {
wasDoubleClick = true;
base.OnDoubleClick(e);
}
如何使用它
这段代码几乎涵盖了所有可能的标签编辑情况,并且以与Visual Studio解决方案资源管理器树行为完全相同的方式解决了它们。它还赋予了修改后的BeforeLabelEdit
新的含义,更接近其真实目的。该事件在标签编辑开始之前立即发生,因此您实际上可以使用它将树节点文本替换为为编辑定制的新值。
private void Tree1_BeforeLabelEdit(object sender,
NodeLabelEditEventArgs e) {
// --- Here we can customize label for editing ---
TreeNode tn = Tree1.SelectedNode;
switch(tn.ImageIndex) {
case 0:
// strip filename from extension for editing
tn.Text =
System.IO.Path.GetFileNameWithoutExtension(tn.Text);
break;
case 1:
// extract quoted item name for editing
tn.Text = GetQuotedName(tn.Text);
break;
}
}
private string GetQuotedName(string label) {
int pos1 = label.IndexOf("\"") + 1;
int pos2 = label.LastIndexOf("\"");
if((pos2-pos1)>0)
return label.Substring(pos1, pos2 - pos1);
else
return "";
}
private void Tree1_AfterLabelEdit(object sender,
System.Windows.Forms.NodeLabelEditEventArgs e) {
// --- Here we can transform edited label
// --- back to its original format ---
TreeNode tn = Tree1.SelectedNode;
switch(tn.ImageIndex) {
case 0:
// paste extension back to edited filename
tn.Text = e.Label +
System.IO.Path.GetExtension(tn.Text);
break;
case 1:
// restore full label
// formatText = "Item \"" + e.Label + "\"";
break;
}
}
输入验证
TreeView
控件另一个令人满意的功能是LabelEdit
输入数据验证。正如您在Visual Studio IDE中看到的,如果标签编辑中的输入无效,则会弹出一个错误消息,例如“您必须输入一个名称”,如果您试图将空string
作为解决方案名称输入。在点击错误消息标签上的“确定”后,编辑将从初始值(即编辑开始时拥有的值)继续。为了让我们的增强型TreeView
控件的用户可以使用此功能,我们为控件提供了一个附加事件。
ValidateLabelEdit(object sender, ValidateLabelEditEventArgs e)
该事件使用新的ValidateLabelEditEventArgs
类,该类通过继承CancelEventArgs
并添加Label
属性来创建。
以下是一个如何实现ValidateLabelEdit
事件处理程序的示例
private void Tree1_ValidateLabelEdit(object sender,
ValidateLabelEditEventArgs e) {
if(e.Label.Trim()=="") {
MessageBox.Show("The tree node label cannot be empty",
"Label Edit Error", MessageBoxButtons.OK,
MessageBoxIcon.Error);
e.Cancel = true;
return;
}
if (e.Label.IndexOfAny(new char[]{'\\',
'/', ':', '*', '?', '"', '<', '>', '|'})!=-1) {
MessageBox.Show("Invalid tree node label.\n" +
"The tree node label must not contain " +
"following characters:\n \\ / : * ? \" < > |",
"Label Edit Error", MessageBoxButtons.OK,
MessageBoxIcon.Error);
e.Cancel = true;
return;
}
}
摘要
我们发现,TreeView
的BeforeLabelEdit
和AfterLabelEdit
事件除了可以取消之外,并没有给您控制LabelEdit
过程的任何其他能力。
另一方面,**确实**有控制此过程的需要,例如
- 当您只想编辑
TreeNode
名称的一部分而保留其余部分不变时(如VS解决方案资源管理器中) - 当您想验证用户输入并阻止
LabelEdit
以不期望的结果文本结束时(如Windows资源管理器或VS解决方案资源管理器中)。
经过仔细研究,我们发现最简单且无干扰的方法如下
- 继承自
TreeView
。 - 重写
OnNotifyMessage
方法以拦截wm_messages
。 - 在
LabelEdit
操作的上下文中捕获WM_TIMER
消息。 - 重写
OnBeforeLabelEdit
和OnAfterLabelEdit
,以及OnMouseDown
、OnMouseUp
、OnClick
和OnDoubleClick
,使LabelEdit
过程具有一致且逻辑的行为。 - 添加一个新事件
ValidateLabelEdit
。
结果是,我们获得了一个增强的TreeView
控件,它允许使用熟悉的BeforeLabelEdit
和AfterLabelEdit
事件来自定义LabelEdit
,并使用新的ValidateLabelEdit
事件来检查编辑结果并在输入不期望时阻止LabelEdit
完成。
历史
- 2005年10月12日 - 文章提交
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。