2

If I put a ComboBox inside of a Panel and set the Panel's AutoScroll property to True, the ComboBox List is not redrawn when the panel is scrolled. This results in the DropDown floating in its initial spot and covering other controls. This issue can be easily reproduced by:

  1. Create a new Windows Forms Project
  2. Place a Panel into the form (make it big enough to fit a few ComboBoxes)
  3. Place some ComboBoxes (with some Items added) stacked vertically so they are placed outside of the Panels bounds (this will make a scrollbar appear in Designer)
  4. Run the Project and click one of the ComboBoxes to show the drop down
  5. Move your mouse to somewhere on the Panel and scroll your mouse wheel

You'll notice that the ComboBoxes move up and down as you scroll, but the ComboBox drop down list stays wherever it was previously drawn.

I think this is a bug in Windows Forms but I can't find it documented anywhere.

Is there a workaround for this issue so I won't get this behavior?

Picture of ComboBox dropdown behavior in Panel with AutoScroll = True

Jimi
  • 29,621
  • 8
  • 43
  • 61

2 Answers2

1

In Windows 10, as it appears your Form uses its Theme, the behavior of scrollable UI elements sports this feature: the scroll action is dispatched to whatever control is under the Mouse Pointer, ignoring completely the Mouse Capture many Controls rely upon for their functionality.

The NativeWindow that holds the List Control's handle is not notified that the ComboBox.Location has changed, in this scenario.
Note that the ScrollableControl Container doesn't raise Scroll events either.


You can test this Custom Control derived from the standard ComboBox.
It modifies the default behavior when its Location property is changed, base on the value of its CloseDropDownOnScroll property:

  • CloseDropDownOnScroll = true: when the Control's Location changes and the DropDown List is shown, it hides the List Control.

  • CloseDropDownOnScroll = false: in the same situation, if the ComboBox and the List Control can fit in the Client Area of the Parent Container, the DropDown List is moved, to follow its ComboBox, otherwise it's hidden.

The Control uses the GetComboBoxInfo() function ti retrieve the Handle of the DropDown List Control and GetWindowRect() to get it's size.

The ListControl NativeWindow is then moved, when necessary, using the SetWindowPos() function.
It should be simple to adapt or extend its functionality, when required.

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;
    }
}

▶ Don't remove this from this.Handle and this.Parent

Jimi
  • 29,621
  • 8
  • 43
  • 61
0

It also happens on Windows 8.1. One way is to detect when the Location changed and then reshow the drop down menu. Note: LocationChanged might not be fired depending on which parent control is doing the scrolling.

    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);
Loathing
  • 5,109
  • 3
  • 24
  • 35