创建自定义下拉控件






4.93/5 (68投票s)
解释了如何有效地创建几乎任何类型的下拉控件

引言
本文演示了如何有效地创建自己的下拉控件,其灵活性远超通过捕获 Windows 标准 ComboBox
的 Paint
事件来实现。我见过的大多数免费自定义下拉控件都名不副实。其中许多控件会冻结在屏幕上,无法正常关闭,有些代码臃肿不堪。
我终于下定决心,决定从头开始创建自己的控件。由于日程安排的限制,我没有花很多时间开发设计时支持,但目前的方法效果很好。我们将通过一个创建 QuickColor
控件的例子来进行讲解,该控件就是您在上面看到的。在本文的范围内,我将假设您熟悉继承的概念。
Using the Code
我设计了一个基类用于继承控件,名为 DropDownControl
。此类处理所有与下拉相关的函数,因此我们可以专注于自定义控件的外观。
创建新控件的第一步是向项目中添加一个新的 UserControl
,切换到代码视图,并将其设置为继承 DropDownControl
。
public partial class QuickColor : UserControl //<-- replace with DropDownControl { ... }
切换回设计视图后,您首先会注意到该控件顶部已绘制了一个组合框。下面的剩余区域将用于设计下拉内容。
让我们深入了解一下它的工作原理。为了使下拉控件在运行时正常工作,我们必须调用 InitializeDropDown()
方法。这应该在 InitializeComponent()
方法之后立即完成。InitializeDropDown
方法接受一个 Control
类型的参数,该参数决定下拉窗口的外观和大小。例如,如果您传递一个宽度为 150 像素、高度为 50 像素的 Label
控件,DropDownControl
的窗口大小将设置为 150 x 50,然后 Label
控件将被加载到下拉窗口中,如图 2 所示。
public partial class QuickColor : DropDownControl
{
public QuickColor()
{
InitializeComponent();
InitializeDropDown(label1);
}
}
显然,这个简单的例子在实际场景中作用不大,但它应该能清楚地演示如何初始化下拉窗口。对于 QuickColor
的例子,我们需要将三个控件加载到下拉窗口中:
- “无颜色”按钮
- 颜色网格
- “更多纯色”按钮
多个下拉项
如果下拉窗口只接受一个参数,我们如何才能有多个项呢?对于多个控件,您应该使用 Panel
控件作为容器项。这就是用于容纳 QuickColor
下拉窗口中的三个控件的内容。如您所知,Panel
类扩展了 Control
类,因此它可以作为 InitializeDropDown()
方法中的参数。
绘制锚点
让用户知道下拉窗口中选定了什么始终是一个好主意。对于 QuickColor
,用户需要知道选择了什么颜色。有一个名为 AnchorClientBounds
的 DropDownControl
属性,其中包含锚点内绘制内容的坐标。如果使用标准的 ClientRectangle
属性,组合框的渲染将被覆盖。使用 AnchorClientBounds
可确保锚点内容直接在组合框内渲染。对于 QuickColor
,我设计了一个名为 ColorPanel
的控件,用于锚点区域。ColorPanel
会检查颜色的 alpha 分量(透明度),并在必要时绘制类似 Photoshop 的网格作为背景,使 alpha 分量更加明显。ColorPanel
的边界由 AnchorClientBounds
属性设置,该属性正如您在第一个图像中看到的,完美地适合锚点内。
执行下拉
当用户单击锚点上的任何位置时,窗口会自动下拉。但是,如果您有一个覆盖锚点的控件,例如 ColorPanel
,则必须捕获其鼠标事件并以编程方式强制窗口下拉。在 QuickColor
的情况下,我们需要重写 ColorPanel
的 Click
事件来启动下拉。
private void colorPanel1_Click(object sender, EventArgs e)
{
OpenDropDown();
}
关闭下拉窗口
如果用户单击下拉窗口边界之外的任何位置,窗口会自动关闭。但是,如果鼠标单击发生在下拉窗口边界内,您必须确定关闭窗口的操作,并调用 CloseDropDown()
方法。在 QuickColor
的情况下,从颜色网格中选择一种颜色会关闭下拉窗口。
private void colorGrid1_SelectedIndexChange(object sender, EventArgs e)
{
this.Color = colorGrid1.Color;
this.CloseDropDown();
...
}
冻结下拉窗口
有时需要将下拉窗口冻结在其下拉状态。还记得 QuickColor
下拉窗口中的“更多纯色”按钮吗?该按钮会启动一个新的对话框,允许用户以更多选项创建自定义颜色。让我们来看一下代码。
private void btnMoreSolidColors_Click(object sender, EventArgs e)
{
this.FreezeDropDown(false); // false=drop visibility
ColorChooser frm = new ColorChooser(this.Color);
if (frm.ShowDialog() != DialogResult.Cancel && !frm.Color.Equals(this.Color))
{
this.Color = frm.Color;
this.CloseDropDown();
...
}
this.UnFreezeDropDown();
}
注意 FreezeDropDown()
方法。这是怎么回事?如果在从下拉窗口打开新窗体时未调用 FreezeDropDown()
,下拉窗口将关闭并终止从中启动的任何窗体。如果您是技术爱好者,想了解为什么必须调用此方法,请阅读下面的部分。其余人可以跳过(无聊的)部分。
下拉窗口实现了 IMessageFilter
,并监听下拉期间发送的所有应用程序消息。当创建消息时,下拉窗口使用静态 Form
方法 Form.ActiveForm.Equals()
来确保它是活动窗体。当另一个窗体变为活动窗体时,下拉窗口将关闭并将其自身作为 Windows 消息侦听器移除。调用 FreezeDropDown()
方法会导致下拉窗口暂停活动窗体检查,并强制其保持打开状态。(请注意,我不建议在生命周期比下拉窗口长的控件中使用 IMessageFilter
。)
DropState
属性
在设计复杂的下拉控件时,您可能会遇到需要了解下拉窗口状态的情况。有一个名为 DropState
的属性,您可以随时检查它来确定下拉窗口的状态。注意:DropState
是只读的,因此您不能使用此属性更改下拉窗口的状态。
下面是演示项目中包含的其他自定义下拉控件的一些缩略图。DropState
属性在关闭这两个下拉控件时非常有用。EmployeePicker
(图 3)在 ListView
的 SelectedIndexChange
事件触发时关闭。但是,当下拉窗口正在关闭并将其从控件列表中删除时,会出现一个问题。SelectedIndexChange
事件再次触发!这反过来会导致 CloseDropDown()
方法被递归调用,从而导致抛出异常。为了避免这种情况,我们在 SelectedIndexChange
处理程序中简单地检查 DropState
属性。如果窗口处于 Dropping
或 Closing
状态,我们就会退出处理程序。
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
if (this.DropState == eDropState.Dropping || this.DropState == eDropState.Closing)
return;
this.CloseDropDown();
...
}
上述控件是快速创建的演示。它们唯一的目的是激发您对可用可能性的想象。
创建属性更改事件
Windows ComboBox
控件会触发 SelectedIndexChanged
事件来通知侦听器选择了已更改。我们也需要触发某种事件来通知侦听器选择已更改。DropDownControl
提供了一个通用事件 PropertyChanged
,但我强烈建议您为自定义控件创建更具意义的事件。对于 QuickColor
,我创建了一个 ColorChanged
事件。如果其他开发人员在查看 QuickColor
的事件以进行捕获时,他们会比 PropertyChanged
事件更快地发现 ColorChanged
事件。但是,如果您决定坚持使用 PropertyChanged
事件,可以通过受保护的 protected
方法 OnPropertyChanged()
来触发它。
最后的 remarks
我相信本文将为您创建自己的下拉控件提供一个良好的开端。如果您发现任何可以改进的功能,请告诉我。我也很乐意听到您从本文创建的任何控件。
类似 Photoshop 的颜色对话框的概念来自 Danny Blanchard。我对原始代码进行了几处修改,使其更紧凑、更高效。Danny,做得好!
历史
- 2010/7/19:将受保护的
protected
方法“GetDropDownLocation()
”更改为“GetDropDownBounds()
”。该方法现在通过检查其待处理的屏幕边界来验证下拉窗口的位置。特别感谢 Marco Mastropaolo 的建议。
现在,如果ParentForm
被移动,下拉窗口会自动更新其位置。感谢 Dan Randolph 的绝佳建议! - 2009/11/6:修复了当操作系统不支持 VisualStyles 时出现的组合框渲染问题。感谢 Mathiyazhagan 引起了我们的注意。