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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (29投票s)

2012 年 6 月 11 日

CPOL

7分钟阅读

viewsIcon

34498

downloadIcon

793

一个提供真正弹出控件的通用抽象类,在一个多选下拉列表中实现。

引言

在我当前的项目中,屏幕空间对于列表过滤器来说有点紧张,所以我希望能够让用户从列表中选择多个项目,而不会占用太多初始屏幕空间。显而易见的方法是使用样式为 DropDownList 的 ComboBox,但这只允许单选。所以我上网寻找合适的控件……

背景

我找到了这个: 复选框组合框 扩展组合框类及其项[^]

但它有一些问题:在更改 checkbox.Checked 状态之前需要点击几次,复选框本身的间距太大了(容易解决),坦率地说,它看起来有点笨拙。代码本身也很难理解,而且似乎比它需要的要复杂得多。

所以我从这里获得灵感,并查看了 MSDN 上的 ToolStripDropDown: ToolStripDropDown 类[^]。这提供了一个更简单、更干净的示例,但仍然有一些奇怪的地方。

所以,我构建了自己的。这非常容易。然后我看到同样的方法可以用来为任何控件提供弹出功能——所以我重新开始,并创建了一个抽象通用类,为任何控件提供弹出功能。 

使用代码 

在这次讨论中,我需要讨论两个可以显示的独立控件。第一个我称之为 Base 控件——它位于您的表单上,并在用户与其交互时触发弹出窗口。在弹出窗口中显示的控件我称之为 Target 控件。

PopUpControl 

从 PopUpControl 派生您的 Base 控件,指定您要“弹出”的控件(Target 控件)。您派生的 Base 控件将是您添加到表单的控件,您将提供打开弹出窗口和显示 Target 控件的方法。

这比必需的要复杂一些。谢谢微软!

  1. 以正常方式创建一个新的 UserControl。
  2. 编辑代码文件。
  3. 停止!现在不要从 PopUpControl 派生您的控件!
    PopUpControl 是抽象的,所以您不能意外使用它。如果您直接派生它,您将遇到设计器问题,因为它拒绝显示派生自抽象类的控件(至少自 VS2008 以来一直是这样)。但它会显示派生自派生自抽象类的控件的控件。
    所以……一个临时解决方案!
  4. 转到文件末尾,在命名空间内,但在您的 usercontrol 之外,添加行
           /// <summary>
           /// This class exists solely to provide a non-abstract layer
           /// </summary>
           [ToolboxItem(false)] public class MyClassMyControl : PopUpControl<MyControl> { }
    MyClass 替换为您的 UserControl 名称,将 MyControl 替换为您要弹出的控件。
  5. 现在,回到顶部,让您的类派生自您在上一步中创建的新类。
    您可以使用 #if DEBUG 来选择派生自哪个类,但这表示您测试的代码与您发布的代码不同,所以我更喜欢不这样做。
  6. 您现在要做的就是设计您的 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(&quot;; &quot;, &quot;\n&quot;));
            // The button gets an abbreviated version.
            text = StaticMethods.FitTextToSpace(text,
                                                Width - 25,
                                                2,
                                                string.Format(&quot;++ {0} Items ++&quot;, 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 &gt; availableSpace)
                {
                if (s.Width &gt; availableSpace * limitMultiplier && !string.IsNullOrWhiteSpace(substituteText))
                    {
                    text = substituteText;
                    }
                else
                    {
                    while (s.Width &gt; availableSpace)
                        {
                        text = text.Substring(0, text.Length - 1);
                        s = TextRenderer.MeasureText(text + &quot;...&quot;, font);
                        }
                    text += &quot;...&quot;;
                    }
                }
            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);
                }
            }
就是这样。您所要做的就是调用此方法,一切都在后台进行!

历史

第一版
© . All rights reserved.