Search code examples
c#.netwinformscomboboxuser-controls

Adding a button to a ComboBox and resizing the inner TextBox editing control


I am attempting to make a custom combo box. I want to have a button inside the combobox so the user can add items to its data source. I know I need to use WndProc in order to get the inner textbox but I am not sure how to get it and then edit it's length to not cover the button when selected.

right now when I hover my mouse over the combobox or enter it to edit test the button is covered up by the inner textbox editing control.

this is what the combobox looks like when I enter

Button Covered

this is what I want the combobox to look like

How I want the button to look

my code is an absolute mess from trying to figure this out I have not gotten anywhere really. I will say that I have added the button control to the combobox since I want to access built in functionality.

public ComboBoxButton() : base()
{
    
    _button1.Size = buttonSize;
    this.DrawMode = DrawMode.OwnerDrawVariable;
    _button1.BackColor = SystemColors.Control;
    _button1.Text = "?";
    _button1.Location = buttonLocation;
    this.Controls.Add(_button1);
    SetStyle(ControlStyles.OptimizedDoubleBuffer, true);            
}

I have tried several different ways to get the editing control via

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

I also found this code and was hoping I could edit it to modify the text box size but was not able to

private class TextWindow : NativeWindow
{
    [StructLayout(LayoutKind.Sequential)]
    private struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    private struct COMBOBOXINFO
    {
        public Int32 cbSize;
        public RECT rcItem;
        public RECT rcButton;
        public int buttonState;
        public IntPtr hwndCombo;
        public IntPtr hwndEdit;
        public IntPtr hwndList;
    }

    [DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
    private static extern IntPtr SendMessageCb(IntPtr hWnd, int msg, IntPtr wp, out COMBOBOXINFO lp);

    public TextWindow(ComboBox cb)
    {
        COMBOBOXINFO info = new COMBOBOXINFO();
        info.cbSize = Marshal.SizeOf(info);
        SendMessageCb(cb.Handle, 0x164, IntPtr.Zero, out info);
        this.AssignHandle(info.hwndEdit);
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == (0x0302))
        {
            MessageBox.Show("No pasting allowed!");
            return;
        }
        base.WndProc(ref m);
    }
}

private TextWindow textWindow;

protected override void OnHandleCreated(EventArgs e)
{
    textWindow = new TextWindow(this);
    base.OnHandleCreated(e);
    listBoxHandle = GetComboBoxListInternal(this.Handle, out COMBOBOXINFO info);
}

protected override void OnHandleDestroyed(EventArgs e)
{
    textWindow.ReleaseHandle();            
    base.OnHandleDestroyed(e);
}

Solution

  • To get the handle and position of the Edit Control of your custom ComboBox, you can call GetComboBoxInfo, which returns a COMBOBOXINFO struct that references this information (and also the Arrow Button's, of course).

    You're sending a CB_GETCOMBOBOXINFO message now; same thing, you get the same information back.

    When you add your custom Button to the ComboBox, you then consider its Width, the Arrow Button's Width, and subtract these measures from the current Width of the Edit Control.

    In the sample code, this is done when the ComboBox is first created and when the Control receives a WM_WINDOWPOSCHANGED message, which tells you that the Control has been resized. You then reposition your custom Button and also resize the Edit Control accordingly.

    To do that, you can call SetWindowPos, to set the new size of the Edit Control (note that this size cannot represent an arbitrary measure; the child Controls must cover the entire surface of the ComboBox. Its background is not actually painted).

    The custom Button, when clicked, raises a public event (CustomButtonClicked). You can subscribe to this event in parent Form as usual. See the events of your ComboBox.

    The custom Button is disposed, and its event is unsubscribed, when the ComboBox receives a WM_DESTROY message.


    If you cannot use nullables, just remove the ?, as in Button? customButton = null;

    using System.ComponentModel;
    using System.Runtime.InteropServices;
    
    public class ComboBoxWithButton : ComboBox {
        Button? customButton = null;
        public event EventHandler<EventArgs>? CustomButtonClicked;
    
        public ComboBoxWithButton() {
            customButton = new Button() { Text = "?", FlatStyle = FlatStyle.Flat, Parent = this };
            customButton.FlatAppearance.BorderSize = 0;
            customButton.Click += CustomButton_Click;
        }
    
        [DefaultValue(4)]
        protected internal int EditControlOffset { get; set; } = 4;
    
        protected override void OnHandleCreated(EventArgs e) {
            base.OnHandleCreated(e);
            UpdateControlsPosition();
        }
    
        protected override void WndProc(ref Message m) {
            base.WndProc(ref m);
            switch (m.Msg) {
                case WM_WINDOWPOSCHANGED:
                    UpdateControlsPosition();
                    break;
                case WM_DESTROY:
                    if (customButton != null) {
                        customButton.Click -= CustomButton_Click;
                        customButton.Dispose();
                    }
                    break;
                default:
                    break;
            }
        }
    
        private void UpdateControlsPosition() {
            if (customButton != null) {
                SuspendLayout();
                Rectangle arrowRect = GetComboBoxInfoInternal(Handle, out Rectangle editRect, out IntPtr editHwnd);
                if (arrowRect.IsEmpty || editRect.IsEmpty) return;
                // Same size as the Arrow Button: change as required
                customButton.Size = arrowRect.Size;
                customButton.Location = new Point(arrowRect.Left - customButton.Width - 1, 1);
                editRect.Width = ClientSize.Width - customButton.Width * 2 - EditControlOffset;
                SetWindowPos(editHwnd, IntPtr.Zero, 0, 0, editRect.Width, editRect.Height, SWP_Flags);
                ResumeLayout(false);
            }
        }
    
        private void CustomButton_Click(object? sender, EventArgs e) => OnEditButtonClicked(e);
    
        protected virtual void OnEditButtonClicked(EventArgs e) {
            CustomButtonClicked?.Invoke(this, e);
        }
    
        const int SWP_NOMOVE = 0x0002;
        const int SWP_NOZORDER = 0x0004;
        const int SWP_SHOWWINDOW = 0x0040;
        const int SWP_Flags = SWP_NOMOVE | SWP_NOZORDER;
        const int WM_DESTROY = 0x0002;
        const int WM_WINDOWPOSCHANGED = 0x0047;
    
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        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, int uFlags);
    
        [StructLayout(LayoutKind.Sequential)]
        internal struct RECT {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
    
            public RECT(int left, int top, int right, int bottom) {
                Left = left; Top = top; Right = right; Bottom = bottom;
            }
    
            public static RECT FromRectangle(Rectangle rect) => new RECT(rect.X, rect.Y, rect.Right, rect.Bottom);
            public readonly Rectangle ToRectangle() => Rectangle.FromLTRB(Left, Top, Right, Bottom);
        }
    
        [StructLayout(LayoutKind.Sequential)]
        internal struct COMBOBOXINFO {
            public int cbSize;
            public RECT rcItem;
            public RECT rcButton;
            public int buttonState;
            public IntPtr hwndCombo;
            public IntPtr hwndEdit;
            public IntPtr hwndList;
            //public COMBOBOXINFO() => cbSize = Marshal.SizeOf<COMBOBOXINFO>();
        }
    
        internal static Rectangle GetComboBoxInfoInternal(IntPtr cboHandle, out Rectangle editRect, out IntPtr editHandle) {
            var cbInfo = new COMBOBOXINFO() { cbSize = Marshal.SizeOf<COMBOBOXINFO>() };
            GetComboBoxInfo(cboHandle, ref cbInfo);
            editHandle = cbInfo.hwndEdit;
            editRect = cbInfo.rcItem.ToRectangle();
            return cbInfo.rcButton.ToRectangle();
        }
    }