Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change combobox drop down list border color in C#

Is it possible to change the border color of a combobox dropdown list in c#?

enter image description here

I want to change the white border to a darker shade to match the dark scheme. I searched through the .net documentation and found the DropDownList.BorderStyle property. However, I'm not sure if this will work. I am using WinForms.

Here is the class I'm using for the combobox:

public class FlattenCombo : ComboBox
{
    private Brush BorderBrush = new SolidBrush(SystemColors.WindowFrame);
    private Brush ArrowBrush = new SolidBrush(SystemColors.ControlText);
    private Brush DropButtonBrush = new SolidBrush(SystemColors.Control);

    private Color _borderColor = Color.Black;
    private ButtonBorderStyle _borderStyle = ButtonBorderStyle.Solid;
    private static int WM_PAINT = 0x000F; 

    private Color _ButtonColor = SystemColors.Control;

    public Color ButtonColor
    {
        get { return _ButtonColor; }
        set
        {
            _ButtonColor = value;
            DropButtonBrush = new SolidBrush(this.ButtonColor);
            this.Invalidate();
        }
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        switch (m.Msg)
        {
            case 0xf:
                Graphics g = this.CreateGraphics();
                Pen p = new Pen(Color.Black);
                g.FillRectangle(BorderBrush, this.ClientRectangle);

                //Draw the background of the dropdown button
                Rectangle rect = new Rectangle(this.Width - 17, 0, 17, this.Height);
                g.FillRectangle(DropButtonBrush, rect);

                //Create the path for the arrow
                System.Drawing.Drawing2D.GraphicsPath pth = new System.Drawing.Drawing2D.GraphicsPath();
                PointF TopLeft = new PointF(this.Width - 13, (this.Height - 5) / 2);
                PointF TopRight = new PointF(this.Width - 6, (this.Height - 5) / 2);
                PointF Bottom = new PointF(this.Width - 9, (this.Height + 2) / 2);
                pth.AddLine(TopLeft, TopRight);
                pth.AddLine(TopRight, Bottom);

                g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

                //Determine the arrow's color.
                if (this.DroppedDown)
                {
                    ArrowBrush = new SolidBrush(SystemColors.HighlightText);
                }
                else
                {
                    ArrowBrush = new SolidBrush(SystemColors.ControlText);
                }

                //Draw the arrow
                g.FillPath(ArrowBrush, pth);

                break;
            default:
                break;
        }
    }

    [Category("Appearance")]
    public Color BorderColor
    {
        get { return _borderColor; }
        set
        {
            _borderColor = value;
            Invalidate(); // causes control to be redrawn
        }
    }

    [Category("Appearance")]
    public ButtonBorderStyle BorderStyle
    {
        get { return _borderStyle; }
        set
        {
            _borderStyle = value;
            Invalidate();
        }
    }

    protected override void OnLostFocus(System.EventArgs e)
    {
        base.OnLostFocus(e);
        this.Invalidate();
    }

    protected override void OnGotFocus(System.EventArgs e)
    {
        base.OnGotFocus(e);
        this.Invalidate();
    }
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        this.Invalidate();
    }
}
like image 388
Spencer Avatar asked Dec 30 '13 01:12

Spencer


1 Answers

While the FlatComboBoxAdapter is indeed not available. It is still possible to capture the WM_CTLCOLORLISTBOX windows message and apply a native GDI rectangle to the drop down border. It is a bit of work, but it does the job. (There's a link to an example at the bottom if you wish to skip this)

First off, if you're not familiar with the WM_CTLCOLORLISTBOX windows message, it is described as such:

"Sent to the parent window of a list box before the system draws the list box. By responding to this message, the parent window can set the text and background colors of the list box by using the specified display device context handle."

The message constant would be define like so:

const int WM_CTLCOLORLISTBOX = 0x0134;

Once the message constant is defined, condition for it within your custom ComboBox's overridden WndProc() event:

protected override void WndProc(ref Message m)
{
    // Filter window messages
    switch (m.Msg)
    {
        // Draw a custom color border around the drop down pop-up
        case WM_CTLCOLORLISTBOX:
            base.WndProc(ref m);
            DrawNativeBorder(m.LParam);
            break;

        default: base.WndProc(ref m); break;
    }
}

The DrawNativeBorder() method is where we are going to use the Win API to draw our rectangle. It accepts the handle to the drop down as an argument. Before we can do that however, we need to define the native methods, enumerations, and structs that are going to be used:

public enum PenStyles
{
    PS_SOLID = 0,
    PS_DASH = 1,
    PS_DOT = 2,
    PS_DASHDOT = 3,
    PS_DASHDOTDOT = 4
}

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

[StructLayout(LayoutKind.Sequential)]
public struct COMBOBOXINFO
{
    public Int32 cbSize;
    public RECT rcItem;
    public RECT rcButton;
    public ComboBoxButtonState buttonState;
    public IntPtr hwndCombo;
    public IntPtr hwndEdit;
    public IntPtr hwndList;
}

[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

[DllImport("user32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);

[DllImport("user32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

[DllImport("user32.dll")]
public static extern IntPtr SetFocus(IntPtr hWnd);

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

[DllImport("gdi32.dll")]
public static extern int ExcludeClipRect(IntPtr hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

[DllImport("gdi32.dll")]
public static extern IntPtr CreatePen(PenStyles enPenStyle, int nWidth, int crColor);

[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);

[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);

[DllImport("gdi32.dll")]
public static extern void Rectangle(IntPtr hdc, int X1, int Y1, int X2, int Y2);

public static int RGB(int R, int G, int B)
{
    return (R | (G << 8) | (B << 16));
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public 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 override bool Equals(object obj)
    {
        if (obj == null || !(obj is RECT))
        {
            return false;
        }
        return this.Equals((RECT)obj);
    }

    public bool Equals(RECT value)
    {
        return this.Left == value.Left &&
        this.Top == value.Top &&
        this.Right == value.Right &&
        this.Bottom == value.Bottom;
    }

    public int Height
    {
        get
        {
             return Bottom - Top + 1;
        }
    }

    public int Width
    {
        get
        {
            return Right - Left + 1;
        }
    }

    public Size Size { get { return new Size(Width, Height); } }
    public Point Location { get { return new Point(Left, Top); } }
    // Handy method for converting to a System.Drawing.Rectangle
    public System.Drawing.Rectangle ToRectangle()
    {
        return System.Drawing.Rectangle.FromLTRB(Left, Top, Right, Bottom);
    }

    public static RECT FromRectangle(Rectangle rectangle)
    {
        return new RECT(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
    }

    public void Inflate(int width, int height)
    {
        this.Left -= width;
        this.Top -= height;
        this.Right += width;
        this.Bottom += height;
    }

    public override int GetHashCode()
    {
        return Left ^ ((Top << 13) | (Top >> 0x13))
                          ^ ((Width << 0x1a) | (Width >> 6))
                          ^ ((Height << 7) | (Height >> 0x19));
    }

    public static implicit operator Rectangle(RECT rect)
    {
        return System.Drawing.Rectangle.FromLTRB(rect.Left, rect.Top, rect.Right, rect.Bottom);
    }

    public static implicit operator RECT(Rectangle rect)
    {
        return new RECT(rect.Left, rect.Top, rect.Right, rect.Bottom);
    }
}

The DrawNativeBorder() method is defined as:

/// <summary>
/// Non client area border drawing
/// </summary>
/// <param name="handle">The handle to the control</param>
public static void DrawNativeBorder(IntPtr handle)
{
    // Define the windows frame rectangle of the control
    RECT controlRect;
    GetWindowRect(handle, out controlRect);
    controlRect.Right -= controlRect.Left; controlRect.Bottom -= controlRect.Top;
    controlRect.Top = controlRect.Left = 0;

    // Get the device context of the control
    IntPtr dc = GetWindowDC(handle);

    // Define the client area inside the control rect so it won't be filled when drawing the border
    RECT clientRect = controlRect;
    clientRect.Left += 1;
    clientRect.Top += 1;
    clientRect.Right -= 1;
    clientRect.Bottom -= 1;
    ExcludeClipRect(dc, clientRect.Left, clientRect.Top, clientRect.Right, clientRect.Bottom);

    // Create a pen and select it
    Color borderColor = Color.Magenta;
    IntPtr border = WinAPI.CreatePen(WinAPI.PenStyles.PS_SOLID, 1, RGB(borderColor.R,
        borderColor.G, borderColor.B));

    // Draw the border rectangle
    IntPtr borderPen = SelectObject(dc, border);
    Rectangle(dc, controlRect.Left, controlRect.Top, controlRect.Right, controlRect.Bottom);
    SelectObject(dc, borderPen);
    DeleteObject(border);

    // Release the device context
    ReleaseDC(handle, dc);
    SetFocus(handle);
}

I used the color Magenta to clearly show the painting. That will do it for the border painting. There is however, one more problem. When the drop down displays and the mouse hasn't moved over a drop down item, the default border still shows. To handle that issue, you'd have to determine when the drop down is fully open. Then send a WM_CTLCOLORLISTBOX message of our own to update the border.

I crudely check for the drop down display moment using a timer. I tried various other options but they didn't pan out. Honestly, if someone has a better solution that works, that would be great.

You'll need a timer to check when the drop down actually drops fully:

private Timer _dropDownCheck = new Timer();

The timer is a field in your custom combobox. Set it up in your custom combobox constructor after InitializeComponent():

_dropDownCheck.Interval = 100;
_dropDownCheck.Tick += new EventHandler(dropDownCheck_Tick);

Override the custom combobox's OnDropDown() event, and set up the timer tick event:

/// <summary>
/// On drop down
/// </summary>
protected override void OnDropDown(EventArgs e)
{
    base.OnDropDown(e);

    // Start checking for the dropdown visibility
    _dropDownCheck.Start();
}

/// <summary>
/// Checks when the drop down is fully visible
/// </summary>
private void dropDownCheck_Tick(object sender, EventArgs e)
{
    // If the drop down has been fully dropped
    if (DroppedDown)
    {
        // Stop the time, send a listbox update
        _dropDownCheck.Stop();
        Message m = GetControlListBoxMessage(this.Handle);
        WndProc(ref m);
    }
}

Lastly, create the following methods to get the drop down handle and to create a WM_CTLCOLORLISTBOX message to send to the control:

/// <summary>
/// Creates a default WM_CTLCOLORLISTBOX message
/// </summary>
/// <param name="handle">The drop down handle</param>
/// <returns>A WM_CTLCOLORLISTBOX message</returns>
public Message GetControlListBoxMessage(IntPtr handle)
{
    // Force non-client redraw for focus border
    Message m = new Message();
    m.HWnd = handle;
    m.LParam = GetListHandle(handle);
    m.WParam = IntPtr.Zero;
    m.Msg = WM_CTLCOLORLISTBOX;
    m.Result = IntPtr.Zero;
    return m;
}

/// <summary>
/// Gets the list control of a combo box
/// </summary>
/// <param name="handle">Handle of the combo box itself</param>
/// <returns>A handle to the list</returns>
public static IntPtr GetListHandle(IntPtr handle)
{
    COMBOBOXINFO info;
    info = new COMBOBOXINFO();
    info.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(info);
    return GetComboBoxInfo(handle, ref info) ? info.hwndList : IntPtr.Zero;
}

That about does that, if you're still confused, it's probably just easier to take a look at the control in this example VS 2010 custom combobox project I have provided:

http://www.pyxosoft.com/downloads/CustomComboBoxBorderColor.zip

like image 153
user3756575 Avatar answered Sep 27 '22 21:09

user3756575