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

只读组合框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.46/5 (28投票s)

2005年9月8日

CPOL

8分钟阅读

viewsIcon

308445

downloadIcon

1824

一个带有只读属性的组合框,允许复制文本和查看下拉列表

引言

和许多其他人一样,我们的开发人员被TextBoxReadOnly属性的外观和感觉所吸引。与禁用的TextBox相比,它是可读的,但又传达了控件状态的变化。您无法更改其值,但如果需要,可以将其中的文本复制出来。由于ComboBoxTextBox的增强版,因此自然会寻找ReadOnly属性,但正如大家所知,它并不存在。哦,它确实存在。它并未在Framework中公开,但它就在底层,并且可以通过一行代码访问。好吧……算是吧。

赋予ReadOnly属性外观和感觉的本质确实需要一行代码,但堵住周围的漏洞则需要更多代码。在Code Project上,Dan Anatoli和Claudio Grazioli有几篇关于创建ReadOnly状态外观及其感觉的文章,但他们的解决方案未能提供我期望的只读ComboBox。我期望的是,它仍然应该显示下拉列表,以防用户想查看其他选择。它应该允许通过Ctrl+C按键或使用右键上下文菜单从框中复制文本。当然,在ReadOnly模式下,它不应允许更改。

一行代码

搜索Google已成为一项工作要求。我开始寻找那个神奇的关键词组合,它将引导我找到一篇帖子,其中有人会给我一个鲜为人知但能解决我问题的属性或方法。希望它是一行代码。在这种情况下,起初看起来可能就是这么简单。

SendMessage(GetWindow(Me.Handle, GW_CHILD), _
                      EM_SETREADONLY, Value, 0)

您可以通过发送消息与Windows控件进行交互。这些消息记录在MSDN上。对于ComboBox,您可以发送EM_SETREADONLY消息。一旦发送,控件的TextBox部分就会像您期望的那样外观和行为;颜色会改变,您不能再在框中键入,不能粘贴,也不能使用任何编辑键,但是……(这是一个很大的但是)控件的ListBox部分对此一无所知。它仍然允许您选择新值,更改框中的文本和列表的选定索引。这就是您必须开始堵漏洞的地方。

堵住漏洞

在组合框中的TextBox被置于ReadOnly状态后,还剩下四个漏洞。它们是

  • 下拉列表鼠标选择
  • 右键菜单中的撤销
  • 使用光标键从列表中选择
  • 反复按F4键打开列表

下拉列表鼠标选择

我希望用户能够查看列表,即使他们无法更改选择。当您单击列表中的某个项目时,会发生两件事。它会更改文本框中的文本,并更改SelectedIndex。为了阻止文本更改,我发现如果在下拉状态下拦截Windows消息273,就可以阻止文本更改。消息273还会拉起列表。所以我必须向组合框发送一条消息,告诉它打开列表。

Protected Overrides Sub WndProc(_
              ByRef m As System.Windows.Forms.Message)
    If _ReadOnly AndAlso _DroppedDown Then
        If m.Msg = 273 Then
            _DroppedDown = False
            SendMessage(Me.Handle, CB_SHOWDROPDOWN, _
                       System.Convert.ToInt32(False), 0)
            Exit Sub
        End If
    End If
    MyBase.WndProc(m)
End Sub

这阻止了文本更改,但没有阻止SelectedIndex。为了处理这个问题,我必须控制SelectedIndex属性。当在WndProc重写中吸收消息273时,不会调用OnSelectedIndexChanged方法。通过将SelectedIndex的值本地保存,我可以获得最后一个选定的值,以便在被隐藏的SelectedIndex属性中传递回来。我必须隐藏该属性,否则OnSelectedIndexChanged方法将不会被调用。

Protected Overrides Sub OnSelectedIndexChanged(_
                        ByVal e As System.EventArgs)
    _SelectedIndex = MyBase.SelectedIndex
    MyBase.OnSelectedIndexChanged(e)
End Sub

Public Shadows Property SelectedIndex() As Integer
    Get
        Return _SelectedIndex
    End Get
    Set(ByVal Value As Integer)
        _SelectedIndex = Value
        MyBase.SelectedIndex = Value
    End Set
End Property

右键菜单中的撤销

TextBox的右键上下文菜单有一个撤销选项。如果用户粘贴值或手动键入更改,则撤销选项将在上下文菜单中启用。如果在控件被置于ReadOnly状态时正在进行撤销,则可以绕过ReadOnly并恢复到先前的值。但幸运的是,这是那些一行奇迹之一。

SendMessage(GetWindow(Me.Handle, _
          GW_CHILD), EM_EMPTYUNDOBUFFER, Value, 0)

SendMessage函数会导致控件中的文本框清空其撤销缓冲区并禁用撤销上下文菜单选项。接下来是光标键。

使用光标键从列表中选择

无论列表是否下拉,向上和向下的光标键以及Page Up和Page Down键都允许您更改选定的值。这可以通过重写OnKeyDown方法来处理。请注意,Alt + 向下光标键是允许的,因为它可用于下拉列表框。

Protected Overrides Sub OnKeyDown(_
        ByVal e As System.Windows.Forms.KeyEventArgs)
    If _ReadOnly Then
        If e.KeyCode = Keys.Up OrElse _
             e.KeyCode = Keys.PageUp OrElse _
             e.KeyCode = Keys.PageDown OrElse _
             (e.KeyCode = Keys.Down And _
               ((Me.ModifierKeys And Keys.Alt) <> Keys.Alt)) Then
           e.Handled = True
        End If
    End If
    MyBase.OnKeyDown(e)
End Sub

反复按F4键打开列表

最后是ComboBox本身的一个奇怪的bug。如果您按F4,列表将下拉。如果您再次按下,它将打开列表并触发OnSelectedChangeCommitted事件,即使没有任何更改。将以下代码放入其中可以在控件处于ReadOnly状态时停止事件。这对于在控件可写时事件不恰当地触发没有任何作用。

Protected Overrides Sub OnSelectionChangeCommitted(_
                           ByVal e As System.EventArgs)
    If _ReadOnly Then
    Else
        MyBase.OnSelectionChangeCommitted(e)
    End If
End Sub

使用代码 

该代码是以一个类编写的,可以添加到类库中,编译后添加到工具箱中。当我第一次在我工作的地方向其他开发人员试用这个类时,我将其命名为ReadOnlyComboBox。我立即受到了评论的攻击,比如“为什么要使用一个只能读取的ComboBox?”和“提供列表让他们永远无法更改值的目的是什么?”好吧,它不是一个ReadOnlyComboBox。它是一个具有ReadOnly属性的ComboBox。我 resorting 到了一种古老的技术,在继承的类名前面加上一个“X”。在示例项目中,它被命名为xboXComboBox。我们所有的控件都以三个字符的前缀开头,然后是后面的一个重要名称。

顺便说一句,ZIP文件包含源代码和测试项目。您可能需要先构建解决方案,然后才能在设计器中查看测试窗体。

2012年更新

随着我们在团队中使用该控件以及Code Project用户提交评论,已经进行了各种修复和增强。在撰写本文时,.Net Framework的当前版本尚未包含ComboBox的自动完成功能。AutoCompleteMode意外地打开了一个漏洞,使得在ReadOnly时可以更改控件。修复很简单,只需在切换到ReadOnly时将AutoCompleteMode设置为None,然后在返回时恢复以前的值。

有几个与清除选择相关的问题。最复杂的情况是当控件绑定到类中的数字属性时。清除控件会将-1放在SelectedIndex中。控件将要求用户在允许退出之前从列表中选择一个项目。修复方法是将-1替换为零,以便选择列表中的第一个项目。这意味着在绑定到数字属性时,无法清除选择。另一个选择是使用Nullable(Of)数字属性。这将允许ComboBox正确处理清除选择。

摘要

在构建这个类时,我认为我得到了特别的东西。它不大也不复杂,但我确实定义了一个调用user32.dll的函数,并且确实拦截并处理了Windows消息。我对Windows消息的理解来自于观察它们以及看看如果不传递它们会发生什么。这会使代码易受攻击吗?我不这么认为。我期待您的想法。到目前为止,我们的实现一直很成功,我们非常满意。很高兴能找到一种与底层控件交互并从中提取更多功能的方法。对我而言,这是一个更完整的解决方案,处理了我期望的ComboBoxReadOnly属性的所有外观和行为。

历史 

  • 09/08/05
    • 首次发布
  • 08/14/06
    • 从Visual Studio 2003移植到Visual Studio 2005
    • 增加了对DropDownStyle = DropDownList的支持
    • 增加了在数据绑定时将SelectedIndex设置为-1的解决方法,请参见KB327244
  • 01/23/08
    • 修复了当ReadOnly连续两次设置为true时丢失DropDownStyle的bug
  • 06/15/12
    • ReadOnly时禁用AutoCompleteMode,以防止通过键盘更新
    • 强制设置FormattingEnabled以防止在清除后选择列表中的第一个项目
    • 修复了控件绑定到数字属性后清除时的“蟑螂旅馆”行为
    • 将注释转换为XML摘要和备注
© . All rights reserved.