下拉列表与可滚动面板中的组合框分开

Posted

技术标签:

【中文标题】下拉列表与可滚动面板中的组合框分开【英文标题】:DropDown List separated from the ComboBox in a scrollable Panel 【发布时间】:2021-09-01 14:43:21 【问题描述】:

如果我将 ComboBox 放在 Panel 内并将 Panel 的 AutoScroll 属性设置为 True,则滚动面板时不会重绘 ComboBox 列表。这导致 DropDown 浮动在其初始位置并覆盖其他控件。可以通过以下方式轻松重现此问题:

    创建一个新的 Windows 窗体项目 在表单中放置一个面板(使其足够大以容纳几个 ComboBox) 垂直堆叠放置一些组合框(添加了一些项目),以便它们放置在面板边界之外(这将使滚动条出现在设计器中) 运行项目并单击其中一个组合框以显示下拉菜单 将鼠标移动到面板上的某个位置并滚动鼠标滚轮

当您滚动时,您会注意到 ComboBox 会上下移动,但 ComboBox 下拉列表会保留在之前绘制的位置。

我认为这是 Windows 窗体中的一个错误,但我在任何地方都找不到它的文档。

是否有针对此问题的解决方法,因此我不会出现此行为?

【问题讨论】:

【参考方案1】:

在 Windows 10 中,当您的窗体使用其主题时,可滚动 UI 元素的行为体现了这一功能:滚动操作被调度到鼠标指针下的任何控件,完全忽略许多控件的功能都依赖于鼠标捕获。

在这种情况下,不会通知持有列表控件句柄的 NativeWindow ComboBox.Location 已更改。 请注意,ScrollableControl Container 也不会引发 Scroll 事件。


您可以测试从标准 ComboBox 派生的自定义控件。 它根据其 CloseDropDownOnScroll 属性的值修改其 Location 属性更改时的默认行为:

CloseDropDownOnScroll = true: 当控件的位置发生变化并显示下拉列表时,它会隐藏列表控件。

CloseDropDownOnScroll = false: 在同样的情况下,如果 ComboBox 和 List Control 可以放入 Parent Container 的客户区,则 DropDown List 被移动,跟随其 ComboBox,否则隐藏。

控件使用GetComboBoxInfo() 函数来检索下拉列表控件的句柄和GetWindowRect() 来获取它的大小。

然后在必要时使用 SetWindowPos() 函数移动 ListControl NativeWindow。 在需要时调整或扩展其功能应该很简单。

using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

[DesignerCategory("code")]
public class ComboBoxExt : ComboBox

    IntPtr listHandle = IntPtr.Zero;
    private Size listSize = Size.Empty;

    public ComboBoxExt()  

    public bool CloseDropDownOnScroll  get; set;  = true;

    protected override void OnHandleCreated(EventArgs e)
    
        base.OnHandleCreated(e);
        listHandle = GetComboBoxListInternal(this.Handle);
        if (GetWindowRect(listHandle, out Rectangle rect)) 
            listSize = rect.Size;
        
    

    protected override void OnLocationChanged(EventArgs e)
    
        base.OnLocationChanged(e);

        if (!DesignMode && DroppedDown) 
            if (CloseDropDownOnScroll) 
                DroppedDown = false;
            
            else 
                var rect = RectangleToScreen(ClientRectangle);
                if (Bottom > 0 && (Bottom + listSize.Height) < this.Parent.ClientSize.Height) 
                    SetWindowPos(listHandle, IntPtr.Zero, rect.Left, rect.Bottom, 0, 0, swpflags);
                
                else 
                    DroppedDown = false;
                
            
        
    

    private const uint SWP_NOSIZE = 0x0001;
    private const uint SWP_NOZORDER = 0x0004;
    private const uint SWP_ASYNCWINDOWPOS = 0x4000;
    uint swpflags = SWP_NOSIZE | SWP_NOZORDER | SWP_ASYNCWINDOWPOS;

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    internal static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

    [DllImport("user32.dll", SetLastError = true)]
    internal static extern bool GetWindowRect(IntPtr hwnd, out Rectangle lpRect);

    [StructLayout(LayoutKind.Sequential)]
    internal struct COMBOBOXINFO
    
        public int cbSize;
        public Rectangle rcItem;
        public Rectangle rcButton;
        public int buttonState;
        public IntPtr hwndCombo;
        public IntPtr hwndEdit;
        public IntPtr hwndList;
        public void Init() => this.cbSize = Marshal.SizeOf<COMBOBOXINFO>();
    

    internal static IntPtr GetComboBoxListInternal(IntPtr cboHandle)
    
        var cbInfo = new COMBOBOXINFO();
        cbInfo.Init();
        GetComboBoxInfo(cboHandle, ref cbInfo);
        return cbInfo.hwndList;
    

▶ 不要从this.Handlethis.Parent 中删除this

【讨论】:

【参考方案2】:

它也发生在Windows 8.1。一种方法是检测Location 何时更改,然后重新显示下拉菜单。注意:LocationChanged 可能不会被触发,具体取决于正在滚动的父控件。

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Form f5 = new Form();
    ComboBox combo5 = new ComboBox();
    combo5.Margin = new Padding(50, 100, 0, 0);
    combo5.Items.AddRange(new Object[]  "A", "B", "C" );
    FlowLayoutPanel panel = new FlowLayoutPanel();
    panel.Controls.Add(combo5);
    f5.Controls.Add(panel);
    combo5.LocationChanged += delegate 
        if (combo5.DroppedDown) 
            // this could be replaced with better code that grabs the handle of the list box window
            // and then sets the location of the list box
            combo5.DroppedDown = false;
            combo5.DroppedDown = true;
        
    ;
    panel.AutoScroll = true;
    panel.AutoScrollMinSize = new Size(500, 500);
    panel.Dock = DockStyle.Fill;
    Application.Run(f5);

【讨论】:

以上是关于下拉列表与可滚动面板中的组合框分开的主要内容,如果未能解决你的问题,请参考以下文章

win32day10-组合框/列表框/滚动条/控件的自绘制

MFC 组合框下拉/下拉列表:仅显示一项

JAVA 下拉列表和滚动条

如何隐藏组合框下拉列表中的列?

表格中的组合框下拉列表

可空类型的 datagridview 组合框下拉列表中的空值