使用通用 Abstract PopUp 类实现的多选下拉列表






4.92/5 (29投票s)
一个提供真正弹出控件的通用抽象类,在一个多选下拉列表中实现。
引言
在我当前的项目中,屏幕空间对于列表过滤器来说有点紧张,所以我希望能够让用户从列表中选择多个项目,而不会占用太多初始屏幕空间。显而易见的方法是使用样式为 DropDownList 的 ComboBox,但这只允许单选。所以我上网寻找合适的控件……
背景
我找到了这个: 复选框组合框 扩展组合框类及其项[^]
但它有一些问题:在更改 checkbox.Checked 状态之前需要点击几次,复选框本身的间距太大了(容易解决),坦率地说,它看起来有点笨拙。代码本身也很难理解,而且似乎比它需要的要复杂得多。
所以我从这里获得灵感,并查看了 MSDN 上的 ToolStripDropDown: ToolStripDropDown 类[^]。这提供了一个更简单、更干净的示例,但仍然有一些奇怪的地方。
所以,我构建了自己的。这非常容易。然后我看到同样的方法可以用来为任何控件提供弹出功能——所以我重新开始,并创建了一个抽象通用类,为任何控件提供弹出功能。
使用代码
在这次讨论中,我需要讨论两个可以显示的独立控件。第一个我称之为 Base 控件——它位于您的表单上,并在用户与其交互时触发弹出窗口。在弹出窗口中显示的控件我称之为 Target 控件。PopUpControl
从 PopUpControl 派生您的 Base 控件,指定您要“弹出”的控件(Target 控件)。您派生的 Base 控件将是您添加到表单的控件,您将提供打开弹出窗口和显示 Target 控件的方法。
这比必需的要复杂一些。谢谢微软!
- 以正常方式创建一个新的 UserControl。
- 编辑代码文件。
- 停止!现在不要从
PopUpControl
派生您的控件!PopUpControl
是抽象的,所以您不能意外使用它。如果您直接派生它,您将遇到设计器问题,因为它拒绝显示派生自抽象类的控件(至少自 VS2008 以来一直是这样)。但它会显示派生自派生自抽象类的控件的控件。
所以……一个临时解决方案! - 转到文件末尾,在命名空间内,但在您的 usercontrol 之外,添加行
/// <summary> /// This class exists solely to provide a non-abstract layer /// </summary> [ToolboxItem(false)] public class MyClassMyControl : PopUpControl<MyControl> { }
将MyClass
替换为您的UserControl
名称,将MyControl
替换为您要弹出的控件。 - 现在,回到顶部,让您的类派生自您在上一步中创建的新类。
您可以使用#if DEBUG
来选择派生自哪个类,但这表示您测试的代码与您发布的代码不同,所以我更喜欢不这样做。 - 您现在要做的就是设计您的 Base 控件,并在您希望弹出 Target 时调用
Open
方法。
就是这样!您选择的 Target 控件将在您指示时弹出。
它不公开任何公共属性、方法或事件,并且不需要实现任何方法。它唯一的需要是 Target 控件,以及在您希望 Target 弹出时调用 Open
方法。
MultiSelectDropList
这是一个 Base 控件,看起来像一个设置为使用 DropDownList 样式的 ComboBox,其中包含一个多选 ListBox 作为其 Target。只需将其拖到您的表单上!它非常简单,没有多少属性/方法/事件。
属性:
Items
(ListBox.ObjectCollection
) 打开时显示在下拉列表中的项目集合。可以是任何对象,前提是它重写 ToString
以提供人类可读的内容。仅 getter,未提供 setter。
SelectedItems
(ListBox.ObjectCollection
) Items 属性中由用户选中的项目集合。仅 getter,未提供 setter。
事件:
SelectionChanged
当用户选择或取消选择一个或多个项目时触发。
方法:
无。
您不能在设计器中添加项目到下拉列表中,Items 集合必须在运行时设置。
MultiSelectDownDownList 包含一个控件 - 一个停靠以填充控件的 Button,其 TextAlign 设置为 Left,并带有下拉三角形的图像设置在右侧对齐。单击事件调用 Open 方法以显示 Target ListBox。
演示应用程序
演示应用程序展示了 MultiSelectDropList 的实际效果。它由两个项目组成 - 演示本身,以及一个包含 PopUpControl 和 MultiSelectDropList 的 UtilityControls 类库。
演示应用程序本身很简单:一个演示类,它只接受一个字符串和一个数字,并重写 ToString 以提供人类可读的字符串。
namespace Demo
{
/// <summary>
/// Demo of a simple class for use in MultiSelectDropList
/// </summary>
public class DemoClass
{
#region Properties
/// <summary>
/// A numeric value
/// </summary>
public int Number { get; set; }
/// <summary>
/// A string value
/// </summary>
public string Text { get; set; }
#endregion
#region Constructors
/// <summary>
/// Default constructor
/// </summary>
public DemoClass(string text, int number)
{
Text = text;
Number = number;
}
#endregion
#region Overrides
/// <summary>
/// Provide a human readable version
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("{0}: {1}", Number, Text);
}
#endregion
}
}
以及一个包含三个控件的演示表单:一个标签,一个 MultiSelectDropList 和一个 ListBox 来显示当前选择。Form 在构造函数中加载 MultiSelectDropList 项目,并处理 MultiSelectDropList.SelectionChanged 事件来更新 Listbox:
using System;
using System.Windows.Forms;
namespace Demo
{
/// <summary>
/// Demonstrate the MultiSelectDropList
/// </summary>
public partial class frmDemo : Form
{
/// <summary>
/// Default constructor
/// </summary>
public frmDemo()
{
InitializeComponent();
DemoClass[] demo = new DemoClass[] {
new DemoClass("The first string", 1),
new DemoClass("Select me!",76),
new DemoClass("No, me!", 14),
new DemoClass("PICK ME", 42),
new DemoClass("You try thinking up",3),
new DemoClass("Seven strings for this.",-423),
new DemoClass("The last string", 666)};
dropList.Items.AddRange(demo);
}
/// <summary>
/// Selection changed, update the display list.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void dropList_SelectionChanged(object sender, EventArgs e)
{
showSelected.Items.Clear();
foreach (object o in dropList.SelectedItems)
{
showSelected.Items.Add(o);
}
}
}
}
结果很容易看到。
下拉列表位于右上角,显示没有选择任何项目。
单击下拉列表……
列表弹出。选择一个项目……
列表保持打开状态,但列表框显示选中的项目 - 下拉列表本身也显示。
再选择一些……
列表框已更新,但下拉列表没有足够的空间,因此它会自动插入省略号。当列表关闭时,如果将鼠标悬停在控件上,工具提示将显示完整列表。
再选择一些……
请注意,下拉列表文本已更改以反映项目数量,而不是显示任何文本。这只是一种说法:“这里有很多东西——不要被一个简短的字符串欺骗!”
当组合文本长度超过可用空间的两倍时,这会自动发生。
工作原理,第一部分
首先,我们来看一下 MultiSelectDropList
类。令人惊讶的是,这里与直接下拉列表相关的代码非常少:一个包含一行代码的单个事件处理程序。
/// <summary>
/// Clicked - open the drop down
/// It will close when the user clicks away from it.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void theButton_Click(object sender, EventArgs e)
{
Open();
}
就是这样。所有其他代码都与支持列表框及其选择有关。如果您的 Target 控件不需要支持,那么您就不需要更多代码。
属性
/// <summary>
/// The items for the list
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public ListBox.ObjectCollection Items
{
get { return content.Items; }
}
/// <summary>
/// The selected items
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public ListBox.SelectedObjectCollection SelectedItems
{
get { return content.SelectedItems; }
}
每个属性的第一行确保它不会出现在设计器属性表中,getter 只是将属性传递下去。
事件
SelectionChanged
事件以正常方式构建(我使用 Visual Studio 片段自动创建它们:添加事件的简单代码片段[^]
唯一的另一个事件处理程序是 Popup ListBox 的 SelectedIndexChanged
事件处理程序。
/// <summary>
/// Combo selection changed
/// Update drop down text, and pass event on up.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void content_SelectedIndexChanged(object sender, EventArgs e)
{
if (content.SelectedItems.Count == 0)
{
SetDropDownText("--None--");
}
else
{
StringBuilder sb = new StringBuilder();
string sep = "";
foreach (object o in content.SelectedItems)
{
sb.AppendFormat("{0}{1}", sep, o.ToString());
sep = "; ";
}
SetDropDownText(sb.ToString());
}
OnSelectionChanged(null);
}
这很明显:它构建一个字符串并使用私有方法将其输出到按钮的文本中,该按钮是控件的可见表面。然后,它将事件向上通知到表单处理程序。 /// <summary>
/// Set the control text, fitting it as needed.
/// </summary>
/// <param name="text" />
private void SetDropDownText(string text)
{
// Tooltip: the list itself
toolTip.SetToolTip(theButton, text.Replace("; ", "\n"));
// The button gets an abbreviated version.
text = StaticMethods.FitTextToSpace(text,
Width - 25,
2,
string.Format("++ {0} Items ++", content.SelectedItems.Count),
Font);
theButton.Text = text;
}
同样,很明显,它只是使用库方法来适应文本到空间。 /// <summary>
/// Fit the text in the space
/// If there is not enough space, first truncate and add an elipsis,
/// Then replace with the substitution string when it exceeds the limit
/// </summary>
/// <param name="text" />
/// <param name="availableSpace" />
/// <param name="limitMultiplier" />
/// <param name="substituteText" />
/// <param name="font" />
/// <returns />
public static string FitTextToSpace(string text, int availableSpace, int limitMultiplier, string substituteText, Font font)
{
Size s = TextRenderer.MeasureText(text, font);
if (s.Width > availableSpace)
{
if (s.Width > availableSpace * limitMultiplier && !string.IsNullOrWhiteSpace(substituteText))
{
text = substituteText;
}
else
{
while (s.Width > availableSpace)
{
text = text.Substring(0, text.Length - 1);
s = TextRenderer.MeasureText(text + "...", font);
}
text += "...";
}
}
return text;
}
同样,不复杂。
构造函数
剩下的就是构造函数,它设置 ListBox 参数和 ToolTip。
/// <summary>
/// Default constructor
/// </summary>
public MultiSelectDropList()
{
InitializeComponent();
content.SelectionMode = SelectionMode.MultiSimple;
content.SelectedIndexChanged += new EventHandler(content_SelectedIndexChanged);
// Set up the delays for the ToolTip.
toolTip.AutoPopDelay = 5000;
toolTip.InitialDelay = 1000;
toolTip.ReshowDelay = 500;
// Force the ToolTip text to be displayed whether or not the form is active.
toolTip.ShowAlways = true;
// Set up the ToolTip text for the Button and Checkbox.
SetDropDownText("--None--");
}
工作原理,第二部分
现在是最复杂的部分: PopUpControl
。
你什么意思,这里几乎没有代码?
它使用 ToolStripDropDown
控件,它为您完成了所有工作……
public partial class PopUpControl<T> : UserControl where T: Control, new()
声明控件派生自 Usercontrol
,并指定它需要一个类型参数,该参数必须派生自 Control
,并且必须有一个不带参数的构造函数。 常量
/// <summary>
/// Milliseonds before reopen of drop down
/// </summary>
/// <seealso cref="ignoreOpenUntil"/>
const int reopenDelay = 200;
私有字段
/// <summary>
/// The actual drop down that shows the ListBox
/// </summary>
protected ToolStripDropDown dropDown = new ToolStripDropDown();
这是进行工作的控件 - 它处理下拉列表及其显示、定位、关闭以及所有其他事情。它需要通过 ToolStripControlHost
连接到 Target 控件。 /// <summary>
/// The Toolstrip Host
/// (This can't be set until the control is ready)
/// </summary>
protected ToolStripControlHost host;
这支持 ToolStripDropDown
控件并形成到 Target 控件的桥梁。 /// <summary>
/// The content itself
/// </summary>
protected T content = new T();
Target 控件本身。 /// <summary>
/// Used to prevent re-open of dropdown if the old one has just closed.
/// For example, if this is a drop down list, then clicking on the
/// list control (button with a drop triangle) will open the list.
/// Clicking on it again should close the drop list, not re-open
/// it again.
/// </summary>
private DateTime ignoreOpenUntil = DateTime.MinValue;
描述说明了一切。 构造函数
/// <summary>
/// Default constructor
/// </summary>
public PopUpControl()
{
InitializeComponent();
// Prepare the dropdown for action
host = new ToolStripControlHost(content);
dropDown.Items.Add(host);
// Add event to prevent re-open when expecting close.
dropDown.VisibleChanged += new EventHandler(DropDown_VisibleChanged);
}
只需将这三个控件连接起来,并设置 VisibleChanged
事件的处理程序——我们使用它来防止在用户单击 Base 控件关闭弹出窗口时再次打开它。 事件
/// <summary>
/// Dropdown Visible changed.
/// If it is now hidden, prevent immediate re-open
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DropDown_VisibleChanged(object sender, EventArgs e)
{
Control c = sender as Control;
if (c != null)
{
if (!c.Visible)
{
ignoreOpenUntil = DateTime.Now.AddMilliseconds(reopenDelay);
}
}
}
只需设置一个时间,这样我们就不会立即再次打开它(以防他单击 Base 控件关闭下拉列表)——目前设置为 200 毫秒——从人类的角度来看,零点几秒是察觉不到的。 方法
/// <summary>
/// Open the Pop up
/// </summary>
protected void Open()
{
// Prevent re-open of dropdown if the old one has just closed.
// For example, if this is a drop down list, then clicking on the
// list control (button with a drop triangle) will open the list.
// Clicking on it again should close the drop list, not re-open
// it again.
if (DateTime.Now > ignoreOpenUntil)
{
host.Width = content.Width;
host.Height = content.Height;
dropDown.Show(this, 0, this.Height);
}
}
就是这样。您所要做的就是调用此方法,一切都在后台进行!