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

增强 TreeView:自定义 LabelEdit

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.41/5 (19投票s)

2005年10月12日

8分钟阅读

viewsIcon

151152

downloadIcon

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应用程序中使用的其他类似控件相比,它的功能受到了很大的限制。一个最新的树控件所期望的主要功能是

  • 多选(一次选择多个节点的能力)
  • 更丰富的拖放支持,包括放置位置高亮、放置验证、全彩拖动图像
  • 节点标签编辑自定义和验证(本文讨论的内容)
  • 可插入的外部节点标签编辑器(如ComboBoxCalendarSpinBox等)
  • 节点图像叠加(在常规节点图像上方显示小型属性图像的能力,如感叹号、星号等)
  • 内置的节点剪切/复制/粘贴和撤销/重做堆栈支持,但这有争议,因为这些功能非常依赖于具体任务

展开时填充行为,这也被频繁提及为一种理想的树功能,在标准的TreeView中很容易用很少的代码重现,所以我真的认为我们不应该在这里讨论它。

例如,让我们看看VS树控件(在解决方案资源管理器和类视图中使用)。首先映入眼帘的是在解决方案资源管理器中为根节点实现的自定义标签编辑。在节点文本中,我们可以看到类似这样的内容

Solution 'Solution1' (1 project)

然而,当我们点击它开始标签编辑时,它会变为Solution1。如果我们将其更改为Solution2并完成编辑,我们将看到标签变为

Solution 'Solution2' (1 project)

我们看到标签文本在编辑前经过预处理,在编辑后经过后处理。这种行为被称为LabelEdit自定义,在许多情况下都非常有用。例如,如果我们想允许用户只编辑文件名而不保留扩展名。或者,如果我们想让我们的节点更有自描述性,比如VS中的解决方案节点,例如,方法“GetItems”或属性“IsCreated”,但只公开其命名部分供编辑。

乍一看,似乎我们可以使用TreeView事件BeforeLabelEditAfterLabelEdit轻松模仿这种行为。这似乎很自然,并且像这样的事件名称只能用于标签文本的预处理和后处理,也许还用于编辑验证(我们将在本文后面讨论)。但事实并非如此!这些事件不允许进行任何这些操作。你很快就会发现,由于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的值覆盖。是的,这令人震惊,超乎逻辑,完全无法理解,但这是真的。BeforeLabelEditAfterLabelEdit事件完全没有用,只是为了嘲弄诚实的.NET程序员。:)

但永远不要放弃。另一种可以快速想到的是通过其他事件(MouseDownKeyDown或菜单点击(MouseUpClick发生得太晚,无法改变任何内容))来拦截用户开始标签编辑的意图。在理解用户确实要开始标签编辑后,我们可以更改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();
}

OnMouseDownOnMouseUpOnClickOnDoubleClick方法中应添加以下代码,以涵盖所有可能的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;
  }
}

摘要

我们发现,TreeViewBeforeLabelEditAfterLabelEdit事件除了可以取消之外,并没有给您控制LabelEdit过程的任何其他能力。

另一方面,**确实**有控制此过程的需要,例如

  • 当您只想编辑TreeNode名称的一部分而保留其余部分不变时(如VS解决方案资源管理器中)
  • 当您想验证用户输入并阻止LabelEdit以不期望的结果文本结束时(如Windows资源管理器VS解决方案资源管理器中)。

经过仔细研究,我们发现最简单且无干扰的方法如下

  1. 继承自TreeView
  2. 重写OnNotifyMessage方法以拦截wm_messages
  3. LabelEdit操作的上下文中捕获WM_TIMER消息。
  4. 重写OnBeforeLabelEditOnAfterLabelEdit,以及OnMouseDownOnMouseUpOnClickOnDoubleClick,使LabelEdit过程具有一致且逻辑的行为。
  5. 添加一个新事件ValidateLabelEdit

结果是,我们获得了一个增强的TreeView控件,它允许使用熟悉的BeforeLabelEditAfterLabelEdit事件来自定义LabelEdit,并使用新的ValidateLabelEdit事件来检查编辑结果并在输入不期望时阻止LabelEdit完成。

历史

  • 2005年10月12日 - 文章提交

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

增强TreeView:自定义LabelEdit - CodeProject - 代码之家
© . All rights reserved.