Change color of the border of a ComboBox DropDown List

Viewed 339

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

1 Answers

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
Related