I see my ImageComboBox and ToolStripImageComboBox was broken.

So I fixed it. Not sure when it broke. Also I don’t think I shared it before… Anyway, here it is now, and it’s fixed in my full source code archive too. The issue was the native struct pinvoke declaration for ComboboxInfo was wrong. Bizarrely, that caused the ComboBox to display its image correctly in the designer but it failed to get the control’s handle at runtime, and I never noticed because I don’t really use it for anything.

Similarly to my recently shared Slider/ToolStripSlider control, I have a custom ComboBox control, called an ImageComboBox that displays an image, kind of like the search box in Windows explorer, and a corresponding control that I use on a ToolStrip. It doesn’t display images for items in the ComboBox because that’s really fucking annoying and unnecessary, but does display an image for the top level of the control. Typically I give it a 16×16 glyph of some sort. Here’s the working version…

It looks like this – the search ComboBox displayed on the top right of my main form:

SNAGHTML49086cf9

First the native methods and structs… I’ve only copied the one method in the NativeMethods class used by this control so hopefully I didn’t forget anything.

We need these because we use a native window to get to the edit window inside the ComboBox, then draw an image on there. Actually that’s where this code went wrong… My ComboBoxInfo declaration was missing the button states. Maybe that was changed since I last used this.

using System.Runtime.InteropServices;
using System.Security;

namespace Romy.Controls
{
    [SuppressUnmanagedCodeSecurity]
    internal static class NativeMethods
    {
        [DllImport("user32.dll")]
        public static extern bool GetComboBoxInfo(System.IntPtr hwndCombo, ref ComboBoxInfo info);
    }
}
using System;
using System.Runtime.InteropServices;

namespace Romy.Controls
{
    [StructLayout(LayoutKind.Sequential)]
    internal struct RECT
    {
        public int left;

        public int top;

        public int right;

        public int bottom;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct ComboBoxInfo
    {
        public Int32 cbSize;

        public RECT rcItem;

        public RECT rcButton;

        public ComboBoxButtonState buttonState;

        public IntPtr hwndCombo;

        public IntPtr hwndEdit;

        public IntPtr hwndList;
    }

    internal enum ComboBoxButtonState
    {
        STATE_SYSTEM_NONE = 0,
        STATE_SYSTEM_INVISIBLE = 0x00008000,
        STATE_SYSTEM_PRESSED = 0x00000008
    }
}

Then the control and its NativeWindow class… Not much code here really. We extend the control and intercept some Windows messages sent to its edit window and change the way it gets painted, shoving our image in there on the right. I also added a call to SelectAll when the control is clicked, because typically I use it for a search ComboBox, and when I click it, I want to overwrite anything already typed there. Feel free to remove that if you don’t like it. And lastly I added some code to allow pasting into the control. It’s been a while, but as far as I can remember, pasting into the control didn’t work out of the box.

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

namespace Romy.Controls
{
    /// <summary>A ComboBox that supports an image in its TextBox part. The image is
    /// drawn on the right of the edit box.</summary>
    /// <remarks>Originally this was based on a component with the same name I found
    /// on CodeProject. However, since I was only interested in an image in the edit
    /// box part (to create a standard-looking search ComboBox), I stripped out all
    /// other functionality.</remarks>
    public class ImageComboBox : ComboBox
    {
        private readonly ComboEditWindow editBox;

        public ImageComboBox()
        {
            editBox = new ComboEditWindow(this);
            DoubleBuffered = true;
        }

        [Category("Appearance"), Description("The Image to display for the editBox part of the ComboBox."),
        DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public Image Image
        {
            get => editBox.Image;
            set => editBox.Image = value;
        }

        /// <summary>Suport Ctrl+V pasting text into the ComboBox.</summary>
        public override bool PreProcessMessage(ref Message msg)
        {
            Keys keys = (Keys)msg.WParam.ToInt32();

            if (keys == Keys.V && ModifierKeys == Keys.Control)
            {
                Text = Clipboard.GetDataObject().GetData(typeof(string)) as string;
                return string.IsNullOrEmpty(Text);
            }

            return base.PreProcessMessage(ref msg);
        }

        protected override void OnClick(EventArgs e)
        {
            SelectAll();
            base.OnClick(e);
        }
    }
}
using System.Drawing;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Windows.Forms;

namespace Romy.Controls
{
    /// <summary>A helper class that accesses the windows message stream directed towards the Edit
    /// portion of the ComboBox. Gets assigned the handle of the TextBox of the ComboBox.</summary>
    public sealed class ComboEditWindow : NativeWindow
    {
        private ComboBoxInfo cbxinfo;

        private ImageComboBox Owner;

        private const int WM_CHAR = 0x102;

        private const int WM_GETTEXT = 0xd;

        private const int WM_GETTEXTLENGTH = 0xe;

        private const int WM_KEYDOWN = 0x100;

        private const int WM_KEYUP = 0x101;

        private const int WM_LBUTTONDOWN = 0x201;

        private const int WM_PAINT = 0xF;

        private const int WM_SETCURSOR = 0x20;

        private readonly Control parent;

        public ComboEditWindow(Control parent)
        {
            cbxinfo = new ComboBoxInfo();

            this.parent = parent;
            parent.HandleCreated += (sender, e) => AssignTextBoxHandle(sender as ImageComboBox);
            parent.HandleDestroyed += (sender, e) => ReleaseHandle();
        }

        public Image Image { get; set; }

        /// <summary>The native window's original handle is released
        /// and the handle of the TextBox is assigned to it.</summary>
        public void AssignTextBoxHandle(ImageComboBox owner)
        {
            Owner = owner;
            cbxinfo.cbSize = Marshal.SizeOf(cbxinfo);
            NativeMethods.GetComboBoxInfo(Owner.Handle, ref cbxinfo);
            AssignHandle(cbxinfo.hwndEdit);
        }

        /// <summary>Whenever the textbox is repainted, draw the image.</summary>
        public void DrawImage()
        {
            if (Image != null)
            {
                new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();

                using (Graphics graphics = Graphics.FromHwnd(Handle))
                {
                    if (Owner != null)
                    {
                        using (SolidBrush solidBrush = new SolidBrush(Owner.BackColor))
                        {
                            graphics.FillRectangle(solidBrush, new Rectangle((int)(graphics.VisibleClipBounds.Width - Image.Width), 0, Image.Width, (int)graphics.VisibleClipBounds.Height));
                        }
                    }
                    graphics.DrawImage(Image, graphics.VisibleClipBounds.Width - Image.Width, 0);
                }
            }
        }

        /// <summary>Override the WndProc method to redraw the
        /// TextBox when the textbox is repainted.</summary>
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_PAINT:
                case WM_LBUTTONDOWN:
                case WM_KEYDOWN:
                case WM_KEYUP:
                case WM_CHAR:
                case WM_GETTEXTLENGTH:
                case WM_GETTEXT:
                    base.WndProc(ref m);
                    DrawImage();
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
    }
}

Originally I started the above control with one I’d downloaded from CodeProject. (This was years ago and I didn’t save the URL.) The control I used did a lot more and painted images for the items too… We tend to forget this but a ComboBox is actually a composite control – a text box with a drop down list view. So it has separate Windows handles for its different parts… One for the edit window, one for the list view, maybe a button on the right, etc. I don’t really know the details. I stripped out the functionality I didn’t want, but also ended up rewriting the control almost completely, taking the design for the NativeWindow in terms of when to assign its handle, almost as is from the MSDN sample. As for which Windows messages to intercept, I changed that too. All I did was debug and look at the messages received, to choose which ones need the image to be painted.

And to host it on a ToolStrip, it uses this ToolStripControlHost-derived class…

This is very similar to my last example that hosted my custom Slider. The pattern to host a control on a ToolStrip is quite straightforward. Derive a class from ToolStripControlHost; create the control in a method called by your constructor – mine is always called CreateControlInstance; pick the properties in the hosted control you need to expose; and lastly expose any events of the hosted control (as many as you like) and hook/unhook them in your overridden OnSubscribeControlEvents and OnUnsubscribeControlEvents. Once you’ve done it once or twice, it only takes a couple of minutes to write the code to host whatever control you want.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace Romy.Controls
{
    /// <summary>ToolStripControlHost-derived control to support hosting a ComboBox with an
    /// image in its editBox part on the ToolStrip family of container controls.</summary>
    [ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.None | ToolStripItemDesignerAvailability.ToolStrip | ToolStripItemDesignerAvailability.StatusStrip)]
    public class ToolStripImageComboBox : ToolStripControlHost
    {
        public ToolStripImageComboBox()
            : base(CreateControlInstance())
        { }

        public ComboBox.ObjectCollection Items => IsDisposed ? null : ImageComboBoxControl.Items;

        public ComboBoxStyle DropDownStyle
        {
            get => ImageComboBoxControl.DropDownStyle;
            set => ImageComboBoxControl.DropDownStyle = value;
        }

        public DrawMode DrawMode
        {
            get => ImageComboBoxControl.DrawMode;
            set => ImageComboBoxControl.DrawMode = value;
        }

        [Description("This property is not applicable for this type.")]
        public override Image BackgroundImage
        {
            get => null;
            set => base.BackgroundImage = null;
        }

        [Category("Appearance")]
        [Browsable(true)]
        [Description("The Image to display for the EditBox part of the ComboBox.")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public new Image Image
        {
            get => ImageComboBoxControl.Image;
            set => ImageComboBoxControl.Image = value;
        }

        public ImageComboBox ImageComboBoxControl => Control as ImageComboBox;

        [Description("This property is not applicable for this type.")]
        public override ImageLayout BackgroundImageLayout
        {
            get => base.BackgroundImageLayout;
            set { }
        }

        public new int Height
        {
            get => ImageComboBoxControl.Height;
            set => ImageComboBoxControl.Height = value;
        }

        public int MaxDropDownItems
        {
            get => ImageComboBoxControl.MaxDropDownItems;
            set => ImageComboBoxControl.MaxDropDownItems = value;
        }

        public int MaxLength
        {
            get => ImageComboBoxControl.MaxLength;
            set => ImageComboBoxControl.MaxLength = value;
        }

        public int SelectedIndex
        {
            get => ImageComboBoxControl.SelectedIndex;
            set => ImageComboBoxControl.SelectedIndex = value;
        }

        public new int Width
        {
            get => ImageComboBoxControl.Width;
            set => ImageComboBoxControl.Width = value;
        }

        public object SelectedItem
        {
            get => ImageComboBoxControl.SelectedItem;
            set => ImageComboBoxControl.SelectedItem = value;
        }

        protected override Size DefaultSize => new Size(100, 22);

        public override string Text
        {
            get => ImageComboBoxControl.IsHandleCreated ? base.Text : string.Empty;
            set => base.Text = value;
        }

        public new event KeyPressEventHandler KeyPress;

        public event EventHandler SelectedIndexChanged;

        public new event EventHandler TextChanged;

        public void BeginUpdate() => ImageComboBoxControl.BeginUpdate();

        public void EndUpdate() => ImageComboBoxControl.EndUpdate();

        protected override void OnSubscribeControlEvents(Control control)
        {
            base.OnSubscribeControlEvents(control);

            ImageComboBoxControl.SelectedIndexChanged += new EventHandler(ImageComboBoxControl_SelectedIndexChanged);
            ImageComboBoxControl.KeyPress += new KeyPressEventHandler(ImageComboBoxControl_KeyPress);
            ImageComboBoxControl.TextChanged += new EventHandler(ImageComboBoxControl_TextChanged);
        }

        protected override void OnUnsubscribeControlEvents(Control control)
        {
            base.OnUnsubscribeControlEvents(control);

            ImageComboBoxControl.SelectedIndexChanged -= new EventHandler(ImageComboBoxControl_SelectedIndexChanged);
            ImageComboBoxControl.KeyPress -= new KeyPressEventHandler(ImageComboBoxControl_KeyPress);
            ImageComboBoxControl.TextChanged -= new EventHandler(ImageComboBoxControl_TextChanged);
        }

        private static Control CreateControlInstance() => new ImageComboBox { Size = new Size(100, 22) };

        private void ImageComboBoxControl_KeyPress(object sender, KeyPressEventArgs e) => KeyPress?.Invoke(this, e);

        private void ImageComboBoxControl_SelectedIndexChanged(object sender, EventArgs e) => SelectedIndexChanged?.Invoke(this, e);

        private void ImageComboBoxControl_TextChanged(object sender, EventArgs e) => TextChanged?.Invoke(this, e);
    }
}

One cool thing about creating your own hosted ToolStrip controls is that they show up in the editor just like standard ones. For example, in my own solution the controls are all in a class library project that’s part of the solution, referenced by the application project, so when clicking the button to add more controls on my form in the designer, I can pick standard controls or my own ones, and it looks like this:

image

Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s