[翻译]具有TreeView下拉控件的ComboBox
具有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);}
在此实现中,数据模型和视图完全分离,节点可以单独于控件/下拉框被定义和操纵。
模型——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;}
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,文章有深度
[解决办法]
大家都很勤奋,刚吃完2只螃蟹,上来学习学习
[解决办法]
这个有两种简单的实现方法:
1 隐藏一个显示所有节点的大小的Treeview,在Combobox_DrawItem时遍历节点项,或者直接TreeViewer.DrawToBitmap
2 纯代码绘制出一个TreeView
我个人的实现方法(使用自定义控件,这个自定义控件不会接收焦点,但会响应鼠标事件,不会导致焦点抢占,内容绘制,使得整个自定义控件的显示内容是画上去的,只是通过实现鼠标交互来达到交互效果。其次,在展开自定义控件时,通过设置父窗体矩形,但不影响父窗体显示内容及背景来实现)。
[解决办法]
这个控件太给力了..
[解决办法]
哎,一直都是选择的替代方案
[解决办法]
感谢楼主分享!
[解决办法]
MARK一下,等假期过来再好好研究研究!
[解决办法]
OK
,Mark 了,慢慢学习。
[解决办法]
以前遇到过,不过我遇到的是网页版本的
[解决办法]
关于Combobox的下拉问题我也找过很多例子,但没有一个让我100%满意,相对比较好的一个是http://www.codeproject.com/Articles/17502/Simple-Popup-Control,但仍旧存在文中所提到的诡异焦点问题。
[解决办法]
谢谢翻译分享,非常不错.