Search code examples
.netvb.netwinformsgraphicscombobox

Change color of the border of a ComboBox DropDown List


My code:

Private Sub ComboBox2_DrawItem(sender As Object, e As DrawItemEventArgs) Handles ComboBox2.DrawItem
    If e.Index < 0 Then
        Return
    End If
    e.Graphics.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
    Dim CB As ComboBox = TryCast(sender, ComboBox)
    If (e.State And DrawItemState.Selected) = DrawItemState.Selected Then
        e.Graphics.FillRectangle(New SolidBrush(Color.DarkRed), e.Bounds)
    Else
        e.Graphics.FillRectangle(New SolidBrush(CB.BackColor), e.Bounds)
    End If
    e.Graphics.DrawString(CB.Items(e.Index).ToString(), e.Font, New SolidBrush(CB.ForeColor), New Point(e.Bounds.X, e.Bounds.Y))
End Sub

Result (notice the blue edges, what I want to change):

Printscreen


Solution

  • To change the Theme border color of the DropDown List of a ComboBox, you need to handle the WM_NCPAINT message of the List Control, which is sent to the handle of the Window when the non-client area needs to be painted: usually, when the DropDown is shown.

    To get the handle of the List Control of a ComboBox, you can use the GetComboBoxInfo() function: the handle of its List Control and Edit Control are returned in a COMBOBOXINFO structure.

    You can then assign the List Control handle to a NativeWindow, so you can override its WndProc and trap WM_NCPAINT.
    When the message is received, get the handle to the Device Context (HDC) of the List Control, using the GetWindowDc() function and pass it to the Graphics.FromHdc() method, to create a Graphics object that can be used to draw on this surface.

    ▶ Reading the documentation about the WM_NCPAINT message, you may notice that WPARAM should reference the update Region Handle: but it's usually IntPtr.Zero, that's why we need GetWindowDc().

    Release the handle to the Device Context calling ReleaseDC() after (important).

    That's pretty much it.
    The custom ComboBox Control exposes a public ListBorderColor property that is used to set the Color of the List Control border at Design-Time and Run-Time.

    Imports System.ComponentModel
    Imports System.Drawing
    Imports System.Runtime.InteropServices
    Imports System.Windows.Forms
    
    <DesignerCategory("code")>
    Public Class ComboBoxExt
        Inherits ComboBox
    
        Private listControl As ListNativeWindow = Nothing
        Private m_ListBorderColor As Color = Color.Transparent
    
        Public Sub New()
        End Sub
    
        <DefaultValue(GetType(Color), "Transparent")>
        Public Property ListBorderColor As Color
            Get
                Return m_ListBorderColor
            End Get
            Set
                m_ListBorderColor = Value
                If listControl IsNot Nothing Then
                    listControl.BorderColor = m_ListBorderColor
                End If
            End Set
        End Property
    
        Protected Overrides Sub OnHandleCreated(e As EventArgs)
            MyBase.OnHandleCreated(e)
            listControl = New ListNativeWindow(GetComboBoxListInternal(Me.Handle))
            listControl.BorderColor = ListBorderColor
        End Sub
    
        Protected Overrides Sub OnHandleDestroyed(e As EventArgs)
            listControl.ReleaseHandle()
            MyBase.OnHandleDestroyed(e)
        End Sub
    
        Public Class ListNativeWindow
            Inherits NativeWindow
    
            Public Sub New()
                Me.New(IntPtr.Zero)
            End Sub
    
            Public Sub New(hWnd As IntPtr)
                If hWnd <> IntPtr.Zero Then AssignHandle(hWnd)
            End Sub
    
            Public Property BorderColor As Color = Color.Transparent
    
            Protected Overrides Sub WndProc(ByRef m As Message)
                MyBase.WndProc(m)
                Select Case m.Msg
                    Case WM_NCPAINT
                        Dim hDC As IntPtr = GetWindowDC(Me.Handle)
                        Try
                            Using g = Graphics.FromHdc(hDC),
                                pen = New Pen(BorderColor)
                                Dim rect = g.VisibleClipBounds
                                g.DrawRectangle(pen, 0, 0, rect.Width - 1, rect.Height - 1)
                            End Using
                        Finally
                            ReleaseDC(Me.Handle, hDC)
                        End Try
                    m.Result = IntPtr.Zero
                End Select
            End Sub
        End Class
    
    
        <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
        Friend Shared Function GetComboBoxInfo(hWnd As IntPtr, ByRef pcbi As COMBOBOXINFO) As Boolean
        End Function
    
        <DllImport("user32.dll", SetLastError:=True)>
        Friend Shared Function GetWindowDC(hWnd As IntPtr) As IntPtr
        End Function
    
        <DllImport("user32.dll", SetLastError:=True)>
        Friend Shared Function ReleaseDC(hWnd As IntPtr, hDc As IntPtr) As Boolean
        End Function
    
        Friend Const WM_NCPAINT As Integer = &H85
    
        <StructLayout(LayoutKind.Sequential)>
        Friend Structure COMBOBOXINFO
            Public cbSize As Integer
            Public rcItem As Rectangle
            Public rcButton As Rectangle
            Public buttonState As Integer
            Public hwndCombo As IntPtr
            Public hwndEdit As IntPtr
            Public hwndList As IntPtr
            Public Sub Init()
                cbSize = Marshal.SizeOf(Of COMBOBOXINFO)()
            End Sub
        End Structure
    
        Friend Function GetComboBoxListInternal(cboHandle As IntPtr) As IntPtr
            Dim cbInfo = New COMBOBOXINFO()
            cbInfo.Init()
            GetComboBoxInfo(cboHandle, cbInfo)
            Return cbInfo.hwndList
        End Function
    End Class
    

    C# Version:

    using System.ComponentModel;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;
    
    [DesignerCategory("code")]
    public partial class ComboBoxExt : ComboBox {
    
        private ListNativeWindow listControl = null;
        private Color m_ListBorderColor = Color.Transparent;
    
        public ComboBoxExt() { }
    
        [DefaultValue(typeof(Color), "Transparent")]
        public Color ListBorderColor {
            get {
                return m_ListBorderColor;
            }
            set {
                m_ListBorderColor = value;
                if (listControl is not null) {
                    listControl.BorderColor = m_ListBorderColor;
                }
            }
        }
    
        protected override void OnHandleCreated(EventArgs e) {
            base.OnHandleCreated(e);
            listControl = new ListNativeWindow(GetComboBoxListInternal(Handle));
            listControl.BorderColor = ListBorderColor;
        }
    
        protected override void OnHandleDestroyed(EventArgs e) {
            listControl.ReleaseHandle();
            base.OnHandleDestroyed(e);
        }
    
        public partial class ListNativeWindow : NativeWindow {
            public ListNativeWindow() : this(IntPtr.Zero) { }
    
            public ListNativeWindow(IntPtr hWnd) {
                if (hWnd != IntPtr.Zero) AssignHandle(hWnd);
            }
    
            public Color BorderColor { get; set; } = Color.Transparent;
    
            protected override void WndProc(ref Message m) {
                base.WndProc(ref m);
                switch (m.Msg) {
                    case WM_NCPAINT: {
                        var hDC = GetWindowDC(Handle);
                        try {
                            using (var g = Graphics.FromHdc(hDC))
                            using (var pen = new Pen(BorderColor)) {
                                var rect = g.VisibleClipBounds;
                                g.DrawRectangle(pen, 0f, 0f, rect.Width - 1f, rect.Height - 1f);
                            }
                        }
                        finally {
                            ReleaseDC(Handle, hDC);
                        }
                        m.Result = IntPtr.Zero;
                        break;
                    }
                }
            }
        }
    
    
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        internal static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);
    
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern IntPtr GetWindowDC(IntPtr hWnd);
    
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);
    
        internal const int WM_NCPAINT = 0x85;
    
        [StructLayout(LayoutKind.Sequential)]
        internal partial 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() {
                cbSize = Marshal.SizeOf<COMBOBOXINFO>();
            }
        }
    
        internal IntPtr GetComboBoxListInternal(IntPtr cboHandle) {
            var cbInfo = new COMBOBOXINFO();
            cbInfo.Init();
            GetComboBoxInfo(cboHandle, ref cbInfo);
            return cbInfo.hwndList;
        }
    }