I'm trying to paint my own border (user selectable size and paint color) on a .net winform control. In this case, an inherited OwnerDraw ListBox. I am so close - but stumped by a painting issue that is highlighted in the gif below.
As you can see, I am successfully drawing the border in the NonClient area. The control is successfully appending the scrollbars. But there is an area in the bottom right (the spacer area between the vertical and horizontal scrollbar) where havoc occurs. Although not shown in the the animation, the red border in that bottom right area also disappears sometimes.
My steps are (relevant source below gif):
- Set relevant base control properties (such as DrawMode = OwnerDraw.Fixed and HorizontalScrollbar = True)
- Override CreateParams, switching off WS_BORDER and WS_EX_CLIENTEDGE
- Intercept WM_NCCALCSIZE, setting the shrunken client area.
- Intercept WM_NCPAINT, and painting the borders.
- Override OnResize and repaint the NonClient areas.
It seems that after I resize the client area (WM_NCCALCSIZE), the underlying control then resizes it again to accommodate the scrollbars...but for some reason doesn't consistently paint the 'End Spacer' (the square area at the end of the horz and vert scrollbar). Or possibly is copying pixels from the wrong place, which is why I sometimes lose the bottom right edge of the border too.
I hope the above makes some sense! The code below has a couple of calls to wrapper functions that just then call the relevant winapi function. Also, you will note that this class inherits from ExListBoxBase, which in turn inherits from ListBox. In ExListBoxBase, I add support for Checkboxes and Icons and make it owner draw.
Finally, please note a silly hack that deals with most of the issue:
A call to OnNonClientPreFill
at the start of handling WM_NCCALCSIZE.
OnNonClientPrefill
merely paints the entire control area. This should be completely unnecessary - but is the only hack that seems to partially work...and even then, the missing bottom right border shown in the pic above, still pops up from time to time.
So the question is how to clean up those artifacts, the right way, and avoid using my silly hack which forces a totally unnecessary repaint...and isn't very robust.
Public Class ExListBox
Inherits ExListBoxBase
Private _ClientRect As Rectangle
'// Turn off Windows drawn borders
Protected Overrides ReadOnly Property CreateParams As CreateParams
Get
Dim cp As CreateParams = MyBase.CreateParams
cp.ExStyle = cp.ExStyle And (Not WS_EX_CLIENTEDGE)
cp.Style = cp.Style And Not WS_BORDER
'cp.Style = cp.Style Or WS_BORDER
Return cp
End Get
End Property
Protected Overrides Sub WndProc(ByRef m As Message)
Select Case m.Msg
'// resize the Client Area (excluding Scrollbar area)
Case WM_NCCALCSIZE
If m.WParam.ToInt32() = 0 Then
Dim rc As RECT = DirectCast(m.GetLParam(GetType(RECT)), RECT)
ResizeClientArea(rc)
Marshal.StructureToPtr(rc, m.LParam, True)
m.Result = IntPtr.Zero
Else
Dim ncc As NCCALCSIZE_PARAMS = DirectCast(m.GetLParam(GetType(NCCALCSIZE_PARAMS)), NCCALCSIZE_PARAMS)
'// HACK: fill the entire control Area before resizing it
'OnNonClientPreFill(m.HWnd)
ResizeClientArea(ncc.rgrc0)
Marshal.StructureToPtr(ncc, m.LParam, True)
'// have tried all combos!
m.Result = WVR_VALIDRECTS
End If
'// needed after the above, for windows to create room for the scrollbars
MyBase.WndProc(m)
Case WM_NCPAINT
DebugMessage(m)
OnNonClientPaint(m.HWnd)
m.Result = IntPtr.Zero
'// needed after the above, for windows to reliably draw the scrollbars
MyBase.WndProc(m)
Return
Case Else
'// note: MyBase has its own WndProc that handles OwnerDraw painting amongst other things
MyBase.WndProc(m)
End Select
End Sub
'// shrink the client area for the border width.
Public Overridable Sub ResizeClientArea(ByRef rect As RECT)
Dim thickness = Me.Style.Border.Width
rect.Left = rect.Left + thickness
rect.Top = rect.Top + thickness
rect.Right = rect.Right - thickness
rect.Bottom = rect.Bottom - thickness
'// helper rectangle
_ClientRect = New Rectangle(thickness, thickness, rect.Right - rect.Left, rect.Bottom - rect.Top)
End Sub
'// paint the border
Private Sub OnNonClientPaint(handle As IntPtr)
Dim hDC As IntPtr = GetWindowDC(handle)
Using g As Graphics = Graphics.FromHdc(hDC)
g.ExcludeClip(_ClientRect)
DrawBorder(g, New Rectangle(0, 0, Me.Width, Me.Height), Color.Red, ButtonBorderStyle.Solid, Me.Style.Border.Width)
End Using
ReleaseDC(handle, hDC)
End Sub
'// silly hack to prepaint the entire area (not sure if that excludeclip works as written)
Private Sub OnNonClientPreFill(handle As IntPtr)
Dim hDC As IntPtr = GetWindowDC(handle)
Dim rect As New Rectangle(0, 0, Me.Width, Me.Height)
Using g As Graphics = Graphics.FromHdc(hDC)
g.ExcludeClip(_ClientRect)
FillRectangle(g, rect, Color.YellowGreen)
End Using
ReleaseDC(handle, hDC)
End Sub
'// deal with resizing. RedrawWindow didn't work properly
Protected Overrides Sub OnResize(e As EventArgs)
MyBase.OnResize(e)
'RedrawWindow(Me.Handle, IntPtr.Zero, IntPtr.Zero, RedrawWindowFlags.Frame Or RedrawWindowFlags.UpdateNow Or RedrawWindowFlags.Invalidate)
OnNonClientPaint(Me.Handle)
End Sub
'// force a recalc of the non client areas. Not used.
Public Sub RecalcNCArea()
SetWindowPos(Me.Handle, 0, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE Or SWP_NOZORDER Or SWP_FRAMECHANGED)
End Sub
'// force a repaint of the non client areas. Not used.
Public Sub RedrawNCAreas()
SendMessageW(Me.Handle, WM_NCPAINT, 0&, 0&)
End Sub
Private Sub DebugMessage(m As Message)
Debug.Print("MSG: {0} : &H{1}", GetConstantName(m.Msg), Hex(m.Msg))
End Sub
End Class
Finally I should note a bunch of articles that helped get me to where I am...but none deals with the Scrollbar phenomenon. Here are a couple: