[翻译]具有TreeView下拉控件的ComboBox
没错,如标题所说的那样,在下拉框中是一个TreeView,但是,为什么我们需要这样的控件?事实上这样的需求我已经遇到很多次了,比如适用于:
?当遇到层次结构的数据
?让用户选择树上的一个节点
?需要TreeView但是界面上缺少足够的空间
?用于不经常修改的选项
?当一个对话框看起来太笨重和突兀
在这些情况下,一个普通的ComboBox不符合要求,无论你喜欢不喜欢,它不会同时显示每一个数据条目以及它们的结构。
我会大篇幅探讨过在Windows窗体中写一个这样的控件面临的挑战以及分享围绕ToolStripDropDown控件的常见的陷阱以及最终的解决方案。
告诉你下拉框的真相
当考虑这个问题时,大多数人会立即告诉你下拉框是一个窗体。毕竟,它表现出了很多窗体具有的行为:
?它的位置可以超出父窗体客户区的范围
?能够独立处理鼠标和键盘输入
?出现在最顶层,其他窗口不能掩盖它
然而,这种观点是根本错误的。下拉框有一些非常不同于窗体的东西:
?它们打开时,父窗体的焦点不会被转移过来
?当它们捕捉鼠标/键盘输入,父窗体的焦点不会被转移过来
?同它们的子控件/项交互的时候,不切换焦点
上述的这几点代表它和窗体、控件的工作方式迥然不同,那么是如何做到能拥有这些特性呢?
错在哪里
考虑到这一点,在Windows窗体中用如下“伟大的”方式实现下拉框时会遭遇失败:
使用一个窗体(懵懂无知期)
于是,你决定用一个Form实现下拉框。你将这个窗体设置为无边框的并且在任务栏上不显示图标,也许它被设置了TopMost属性。你甚至可能会设置它的父窗体,不管其中细节的差异,都是这样:用户点击下拉按钮然后你的窗体被显示。当焦点切换到下拉框窗体时,会有一个轻微的闪烁。父窗体的标题栏会变色,窗体阴影变淡(在Aero下)。然后用户通过下拉列表控件所在的窗体做出选择,然后窗体被关闭。直到再次单击父窗体焦点改变,标题栏改变了颜色,并且窗体的阴影又变深。又会出现一次尴尬的闪烁。最后结果如何?需要额外的点击,非常差的用户体验。
还是使用一个窗体,尝试变得更聪明
那些已经尝试了以上办法(或对WinForms/Win32的问题有更深入的理解)的人会尽力解决最初的问题,那就是,下拉窗体会从父窗体抢到焦点。一个鲜有人知的秘密是,Form类有一个受保护的属性,称为ShowWithoutActivation,(在大多数情况下),它将在显示窗体的时候跳过窗体的激活:
protected override bool ShowWithoutActivation {
get { return true; }
}
protected override CreateParams CreateParams {
get {
const int WS_EX_NOACTIVATE = 0x08000000;
CreateParams p = base.CreateParams;
p.ExStyle |= WS_EX_NOACTIVATE;
return p;
}
}
protected override void DefWndProc(ref Message m) {
const int WM_MOUSEACTIVATE = 0x21;
const int MA_NOACTIVATE = 0x0003;
if (m.Msg == WM_MOUSEACTIVATE)
m.Result = (IntPtr)MA_NOACTIVATE;
else
base.DefWndProc(ref m);
}
任何ToolStripItem的派生类(标签,按钮等)可以被添加到下拉列表框中,包括了ToolStripControlHost(虽然我不是选择简单地将一个TreeView控件放入下拉框中,这样仍然会有一些诡异的焦点问题,没有提及的间接开销)。下拉列表框自身必须包含至少一个项,即使它不会占用空间。作为一个控件类的派生类,它可以通过重写OnPaint方法重绘,并且同时响应鼠标和键盘事件。结合了无焦点行为,它提供了符合我们创建下拉框所预期的所有行为所需的工具。
实现
全功能的TreeView在下拉控件中并不需要,所以我们基于一个类似(但是更简单)的数据模型来实现。
DropDownBase类包含可编辑部分的基本功能,绘制,事件处理和设计时支持,以及DroppedDown状态的管理。数据模型围绕类似于TreeNode的ComboTreeNode类。它和它的集合类型ComboTreeNodeCollection构成创建一棵树的循环关系。ComboTreeBox类是DropDownBase类主要的实现类。它拥有和控制数据模型并且可视化选定的节点。它还拥有ComboTreeDropDown——实际的下拉列表(ToolStripDropDown类的派生类)。
在此实现中,数据模型和视图完全分离,节点可以单独于控件/下拉框被定义和操纵。
模型——ComboTreeNode类和ComboTreeNodeCollection集合
ComboTreeNode类是一个简单用来保存节点的名称,文本,状态以及和模型树中其它节点之间关系的原子类。ComboTreeNodeCollection集合表示子树,并因此关联一个父节点。此规则的唯一例外是对于尚未被添加到控件的子树,以及根集合属于控件。无论这个类和ComboTreeBox类关系如何,当添加到ComboTreeBox控件中时都会带上附加的属性和行为。
ComboTreeNodeCollection集合实现了IList<ComboTreeNode>,因为节点的顺序是很重要的。一个内部对象List<ComboTreeNode>用来作为后备存储字段。集合负责分配每个节点的父节点,以确保树的CollectionChanged事件(来自INotifyCollectionChanged接口)被递归触发:
public void Add(ComboTreeNode item) {
innerList.Add(item);
item.Parent = node;
// changes in the subtree will fire the event on the parent
item.Nodes.CollectionChanged += this.CollectionChanged;
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)
);
}
internal bool IsNodeVisible(ComboTreeNode node) {
bool displayed = true;
ComboTreeNode parent = node;
while ((parent = parent.Parent) != null) {
if (!parent.Expanded) {
displayed = false;
break;
}
}
return displayed;
}
?为每个节点分配一个名称,这个名称将被用来通过集合访问节点。
?使用ImageIndex/ImageKey为每个节点指定一个图标。
?保存每个节点的展开状态。
?SelectedNode属性用来获取/设置用户在树中的选择。
?PathSeparator和Path属性来表示选定节点的路径字符串。
?BeginUpdate()/EndUpdate()方法实现批量添加。
?ExpandAll()/CollapseAll()方法管理树的节点全部收起/全部展开。
?对树的排序(执行递归排序,使用默认或自定义的比较器)。
提供以下附加功能:
?递归的枚举器(AllNodes)遍历整棵树。
?根据节点名称或文本判断路径是否构成。
?使用Path属性设置选定的节点。
?ExpandedImageIndex/ExpandedImageKey:以允许展开的节点显示一个不同的图标。
TreeView控件的以下功能没有在ComboTreeBox中实现:
?水平滚动(在下拉框中不引人注目)。
?节点旁的复选框(多选)。
?自定义元素的外观或使用事件重绘控件。
?SelectedImageIndex/SelectedImageKey属性(其实,个人而言,我觉得没什么用)。
?节点上的工具提示,拖放功能等超过下拉列表框之外的其他功能。
?一个Sorted属性,用来创建自动排序的树,取而代之的是,它可以按需排序。
写在最后
ComboTreeBox既是一个自定义下拉列表框的例子,同时也是创建一个Windows窗体的解决方案。ToolStripDropDown组件使这一切成为可能,即使(在本例中)需要手工实现下拉框中的内容。对于编写自定义控件,保持原有的外观,行为和处理输入,这也是一个非常有益的练习。我特别满意的是优雅的滚动机制和使用位图缓存,这些使得我能够成功填充几十万个节点到下拉框中而不会导致性能损失(即使这样的规模被认为是不适合下拉框的)。
通过演示如何实现一个自定义下拉框,我希望能起到抛砖引玉的作用,发掘它更多的用处。
下载
源代码
(基于.NET 3.5, 引用System.Design和WindowsBase)
注:本文翻译自http://www.brad-smith.info/blog/archives/193,作者是Bradley Smith。
[解决办法]
其实自从WPF出现后,自定义控件就应该全交给WPF来完成了,这种功能的控件在WPF里面就是小儿科的实现,基础中的基础,虽然WPF的控件没有WinForm那么强大,但是WPF允许自定义控件,很容易将各个简单的控件拼凑成一个非常强大的复杂控件,而且做好的控件还可以被直接调用到WinForm中使用。
[解决办法]
哗,中秋国庆都不休息啊,还发原创帖啊
不过,说真的,这个功能真的很少用到,甚至没见过
WinXP的Explorer.exe的地址栏也不是Combobox+TreeView,虽然有点像
[解决办法]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
ComboTree MyComboTree = new ComboTree();
this.Controls.Add(MyComboTree);
}
}
public class ComboTree : UserControl
{
private System.Windows.Forms.TreeView treeView1;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Panel panel1;
/// <summary>
/// 设计器支持所需的方法 - 不要
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
System.Windows.Forms.TreeNode treeNode1 = new System.Windows.Forms.TreeNode("节点0");
System.Windows.Forms.TreeNode treeNode2 = new System.Windows.Forms.TreeNode("节点3");
System.Windows.Forms.TreeNode treeNode3 = new System.Windows.Forms.TreeNode("节点4");
System.Windows.Forms.TreeNode treeNode4 = new System.Windows.Forms.TreeNode("节点7");
System.Windows.Forms.TreeNode treeNode5 = new System.Windows.Forms.TreeNode("节点8");
System.Windows.Forms.TreeNode treeNode6 = new System.Windows.Forms.TreeNode("节点9");
System.Windows.Forms.TreeNode treeNode7 = new System.Windows.Forms.TreeNode("节点5", new System.Windows.Forms.TreeNode[] {
treeNode4,
treeNode5,
treeNode6});
System.Windows.Forms.TreeNode treeNode8 = new System.Windows.Forms.TreeNode("节点6");
System.Windows.Forms.TreeNode treeNode9 = new System.Windows.Forms.TreeNode("节点1", new System.Windows.Forms.TreeNode[] {
treeNode2,
treeNode3,
treeNode7,
treeNode8});
System.Windows.Forms.TreeNode treeNode10 = new System.Windows.Forms.TreeNode("节点2");
this.treeView1 = new System.Windows.Forms.TreeView();
this.textBox1 = new System.Windows.Forms.TextBox();
this.button1 = new System.Windows.Forms.Button();
this.panel1 = new System.Windows.Forms.Panel();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
// treeView1
//
this.treeView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.treeView1.Location = new System.Drawing.Point(0, 23);
this.treeView1.Name = "treeView1";
treeNode1.Name = "节点0";
treeNode1.Text = "节点0";
treeNode2.Name = "节点3";
treeNode2.Text = "节点3";
treeNode3.Name = "节点4";
treeNode3.Text = "节点4";
treeNode4.Name = "节点7";
treeNode4.Text = "节点7";
treeNode5.Name = "节点8";
treeNode5.Text = "节点8";
treeNode6.Name = "节点9";
treeNode6.Text = "节点9";
treeNode7.Name = "节点5";
treeNode7.Text = "节点5";
treeNode8.Name = "节点6";
treeNode8.Text = "节点6";
treeNode9.Name = "节点1";
treeNode9.Text = "节点1";
treeNode10.Name = "节点2";
treeNode10.Text = "节点2";
this.treeView1.Nodes.AddRange(new System.Windows.Forms.TreeNode[] {
treeNode1,
treeNode9,
treeNode10});
this.treeView1.Size = new System.Drawing.Size(380, 273);
this.treeView1.TabIndex = 1;
this.treeView1.Visible = false;
this.treeView1.VisibleChanged += new System.EventHandler(this.treeView1_VisibleChanged);
this.treeView1.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeView1_NodeMouseClick);
//
// textBox1
//
this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top
[解决办法]
System.Windows.Forms.AnchorStyles.Left)
[解决办法]
System.Windows.Forms.AnchorStyles.Right)));
this.textBox1.BackColor = System.Drawing.Color.White;
this.textBox1.Location = new System.Drawing.Point(0, 2);
this.textBox1.Name = "textBox1";
this.textBox1.ReadOnly = true;
this.textBox1.Size = new System.Drawing.Size(347, 21);
this.textBox1.TabIndex = 2;
//
// button1
//
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top
[解决办法]
System.Windows.Forms.AnchorStyles.Right)));
this.button1.Location = new System.Drawing.Point(350, 3);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(30, 20);
this.button1.TabIndex = 3;
this.button1.Text = "↓";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// panel1
//
this.panel1.Controls.Add(this.textBox1);
this.panel1.Controls.Add(this.button1);
this.panel1.Dock = System.Windows.Forms.DockStyle.Top;
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(380, 23);
this.panel1.TabIndex = 4;
//
// ComboTree
//
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.Controls.Add(this.treeView1);
this.Controls.Add(this.panel1);
this.Name = "ComboTree";
this.Size = new System.Drawing.Size(380, 296);
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
this.ResumeLayout(false);
}
public ComboTree()
{
InitializeComponent();
this.DoubleBuffered = true;
treeView1.Visible = false;
}
private void button1_Click(object sender, EventArgs e)
{
treeView1.Visible = !treeView1.Visible;
}
private void treeView1_VisibleChanged(object sender, EventArgs e)
{
if (treeView1.Visible)
{
this.Height = panel1.Height + treeView1.Height;
}
else
{
this.Height = panel1.Height;
}
}
private void treeView1_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
if (e.Node.Bounds.Contains(e.Location))
{
textBox1.Text = e.Node.Text;
treeView1.Visible = false;
}
}
}
}
[解决办法]
这个控件太给力了..
[解决办法]
哎,一直都是选择的替代方案
[解决办法]
感谢楼主分享!
[解决办法]
MARK一下,等假期过来再好好研究研究!
[解决办法]
OK
,Mark 了,慢慢学习。
[解决办法]
以前遇到过,不过我遇到的是网页版本的
[解决办法]
关于Combobox的下拉问题我也找过很多例子,但没有一个让我100%满意,相对比较好的一个是http://www.codeproject.com/Articles/17502/Simple-Popup-Control,但仍旧存在文中所提到的诡异焦点问题。
[解决办法]
谢谢翻译分享,非常不错.