Drawing Borders and Edges using the API DrawEdge Function

Simplify standard drawing tasks using the System's built-in functionality

DrawEdge Tester Project

The DrawEdge function is a useful function provided by the Win32 API. It can draw a variety of the edge styles used to draw buttons, frames and borders around controls and forms. The source code for this article shows you how to:

  • Use the DrawEdge to achieve a variety of edge styles.
  • Evaluate the client area within an edge and prevent it being overdrawn.

DrawEdge is declared as follows:

Private Declare Function DrawEdge Lib "user32" ( _
    ByVal hDC As Long, _
    qrc As RECT, _
    ByVal edge As Long, _
    ByVal grfFlags As Long) As Long 

The parameters to it are the hDC to draw onto, the rectangle in which to draw the edge, then two parameters to determine how the edge is drawn:

  • edge determines what type of edge is drawn. This can be set to show outer and inner border parts, either sunken or raised.
  • grfFlags sets which parts (left, top, right, bottom) of the edge are going to be drawn, and has some additional options which modify how the edge is drawn (although these don't seem to do anything for me!). See the code below for a list of constants you can use in these parameters (EDEDBorderStyle in the sample code enumerates settings for edge, EDEDBorderParts the settings for grfFlags).

Once you have drawn an edge, you then normally want to draw any other parts of the control within the edge rather than overdrawing it. The Inner and Outer parts of the border occupy 1 pixel (or TwipsPerPixel twips) each, so it isn't too difficult to work out what the client area within the border is. The EvaluateSize() function below shows how this is done. This function also demonstrates how to make drawing easier by preventing anything from being drawn over the border.

This is done by selecting a 'clipping region' which excludes the border into the device you are drawing into. When the clipping region is in place, Windows will only draw within the clipping region, all other drawing simply disappears. To select a clipping region, first create it with a region creating API function. The sample below uses CreateRectRgn, however you can create regions of any shape you like (see my article "A window that's star-shaped, circular or tank-shaped for details on creating more arbitrary shaped regions"). Once the region is created, you can make it the clipping region by using SelectClipRgn. To remove the region again, simply select a region handle of zero into the control.

The code below provides a simple UserControl which allows all the DrawEdge options to be exercised, and also demonstrates clipping to prevent drawing over the edge which has been drawn. The code in the download expands on the details below by drawing some text into the control to prove that the edge isn't overwritten. It also demonstrates a couple of other techniques:

  • Providing a neat interface for choosing different edge options using a hierarchical tree (a simple implementation of the Hierarchy Selector Control.
  • Drawing text at any angle - in this case text is drawn at 30 degrees across the control (see also Text at Any Angle for more details).
' ================================================================== 
' EDGE Drawing: 
' ================================================================== 
Public Enum EDEDBorderStyle 
    BDR_RAISEDOUTER = 1 
    BDR_SUNKENOUTER = 2 
    BDR_RAISEDINNER = 4 
    BDR_SUNKENINNER = 8 
    
    BDR_BUTTON = BDR_RAISEDINNER Or BDR_RAISEDOUTER 
    BDR_CONTROL = BDR_SUNKENINNER Or BDR_SUNKENOUTER 
    BDR_THINBUTTON = BDR_RAISEDOUTER 
    BDR_THINCONTROL = BDR_SUNKENOUTER 
    
    BDR_ETCHRAISE = BDR_RAISEDOUTER Or BDR_SUNKENINNER 
    BDR_ETCHINSET = BDR_SUNKENOUTER Or BDR_RAISEDINNER 
    
    BDR_ALL = BDR_BUTTON Or BDR_CONTROL 
End Enum 
Public Enum EDEDBorderParts 
    Bf_left = 1 
    Bf_Top = 2 
    Bf_right = 4 
    Bf_bottom = 8 
    BF_TOPLEFT = Bf_left Or Bf_Top 
    Bf_BOTTOMRIGHT = Bf_right Or Bf_bottom 
    BF_RECT = Bf_left Or Bf_Top Or Bf_right Or Bf_bottom 
    BF_MIDDLE = &H800 
    BF_SOFT = &H1000 
    BF_ADJUST = &H2000 
    BF_FLAT = &H4000 
    BF_MONO = &H8000&
    BF_ALL = BF_RECT Or BF_MIDDLE Or BF_SOFT Or BF_ADJUST Or BF_FLAT Or BF_MONO 
End Enum 
Private Type RECT 
    Left As Long 
    Top As Long 
    Right As Long 
    Bottom As Long 
End Type 
Private Declare Function DrawEdge Lib "user32" ( _
    ByVal hDC As Long, qrc As RECT, _
    ByVal edge As Long, ByVal grfFlags As Long) As Long 

' ================================================================== 
' Clipping functions: 
' ================================================================== 
Private Declare Function SelectClipRgn Lib "gdi32" ( _
    ByVal hDC As Long, ByVal hRgn As Long) As Long 
Private Declare Function CreateRectRgn Lib "gdi32" ( _
    ByVal x1 As Long, ByVal y1 As Long, _
    ByVal x2 As Long, ByVal y2 As Long) As Long 
Private Declare Function DeleteObject Lib "gdi32" ( _
    ByVal hObject As Long) As Long 


' ================================================================== 
' Client Region: 
' ================================================================== 
Private m_lLeft As Long 
Private m_lTop As Long 
Private m_lWidth As Long 
Private m_lHeight As Long 
Private m_hRgn As Long 

' ================================================================== 
' Border styles: 
' ================================================================== 
Private m_lBorderStyle As Long 
Private m_lFlags As Long 

Public Property Get BackColor() As OLE_COLOR 
    BackColor = UserControl.BackColor 
End Property 

Public Property Let BackColor(ByVal eColor As OLE_COLOR) 
    UserControl.BackColor = eColor 
    PropertyChanged "BackColor" 
    Draw 
End Property 

Public Property Let BorderPart(ByVal ePart As EDEDBorderParts, _
    ByVal bState As Boolean) 
    If (bState) Then 
        m_lFlags = m_lFlags Or ePart 
    Else 
        m_lFlags = m_lFlags And Not ePart 
    End If 
    Draw 
    PropertyChanged "BorderPart" 
End Property 

Public Property Get BorderPart(ByVal ePart As EDEDBorderParts) As Boolean 
    BorderPart = BitSet(m_lFlags, ePart) 
End Property 

Public Property Let BorderStyle(ByVal eStyle As EDEDBorderStyle, _
    ByVal bState As Boolean) 
    If (bState) Then 
        m_lBorderStyle = m_lBorderStyle Or eStyle 
    Else 
        m_lBorderStyle = m_lBorderStyle And Not eStyle 
    End If 
    Draw 
    PropertyChanged "BorderStyle" 
End Property 

Public Property Get BorderStyle(ByVal eStyle As EDEDBorderStyle) As Boolean 
    BorderStyle = BitSet(m_lBorderStyle, eStyle) 
End Property 

Private Function BitSet(ByVal lIn As Long, ByVal lBits As Long) As Boolean 
    BitSet = ((lIn And lBits) = lBits) 
End Function 

Private Sub EvaluateSize() 
Dim lBSize As Long 
    m_lWidth = UserControl.ScaleWidth \ Screen.TwipsPerPixelX 
    m_lHeight = UserControl.ScaleHeight \ Screen.TwipsPerPixelY 
    m_lLeft = 0 
    m_lTop = 0 
    If (m_lBorderStyle >= 4) Then 
        lBSize = 2 
    ElseIf (m_lBorderStyle >= 1) Then 
        lBSize = 1 
    End If 
    If (BitSet(m_lFlags, Bf_left)) Then 
        m_lLeft = lBSize 
        m_lWidth = m_lWidth - lBSize 
    End If 
    If (BitSet(m_lFlags, Bf_Top)) Then 
        m_lTop = lBSize 
        m_lHeight = m_lHeight - lBSize 
    End If 
    If (BitSet(m_lFlags, Bf_right)) Then 
        m_lWidth = m_lWidth - lBSize 
    End If 
    If (BitSet(m_lFlags, Bf_bottom)) Then 
        m_lHeight = m_lHeight - lBSize 
    End If 
    DeleteObject m_hRgn 
    m_hRgn = CreateRectRgn(m_lLeft, m_lTop, m_lWidth + m_lLeft, _
                m_lHeight + m_lTop) 
    SelectClipRgn UserControl.hDC, m_hRgn 
End Sub 

Public Sub Draw() 
Dim tR As RECT 

    ' Clear control: 
    If (m_hRgn <> 0) Then 
        SelectClipRgn UserControl.hDC, 0 
    End If 
    UserControl.Cls 
    
    ' Draw the edge: 
    tR.Right = UserControl.ScaleWidth \ Screen.TwipsPerPixelX 
    tR.Bottom = UserControl.ScaleHeight \ Screen.TwipsPerPixelY 
    DrawEdge UserControl.hDC, tR, m_lBorderStyle, m_lFlags 
    
    ' Draw in the 'client area' 
    EvaluateSize 
        
    UserControl.Refresh 

End Sub 

Private Sub UserControl_Initialize() 
    m_lBorderStyle = BDR_CONTROL 
    m_lFlags = BF_RECT 
End Sub 

Private Sub UserControl_Paint() 
    Draw 
End Sub 

Private Sub UserControl_ReadProperties(PropBag As PropertyBag) 
    m_lBorderStyle = PropBag.ReadProperty("BorderStyle", BDR_CONTROL) 
    m_lFlags = PropBag.ReadProperty("BorderPart", BF_RECT) 
    BackColor = PropBag.ReadProperty("BackColor", vbButtonFace) 
End Sub 

Private Sub UserControl_Resize() 
    Draw 
End Sub 

Private Sub UserControl_Terminate() 
    If (m_hRgn <> 0) Then 
        SelectClipRgn UserControl.hDC, 0 
        DeleteObject m_hRgn 
    End If 
End Sub 

Private Sub UserControl_WriteProperties(PropBag As PropertyBag) 
    PropBag.WriteProperty "BorderStyle", m_lBorderStyle, BDR_CONTROL 
    PropBag.WriteProperty "BorderPart", m_lFlags, BF_RECT 
    PropBag.WriteProperty "BackColor", BackColor, vbButtonFace 
End Sub