Link to home
Create AccountLog in
Avatar of blueskybooking
blueskybookingFlag for Canada

asked on

VB.NET : Adding UseMnemonic to a custom SplitButton class

The custom User Control I have added, SplitButton, does handle the events related to the UseMnemonic key, but it does not show the underlined letter in the form itself when holding down the [Alt] key.

1. Is there a way to have the Mnemonic key show up in the form when the [Alt] key is pressed?

2. Is there a specific event called when a Mnemonic key is called?  It appears to be the Click event and you can't differentiate it from the Mnemonic, perhaps with the arguments?
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.VisualStyles

'Get the latest version of SplitButton at: http://wyday.com/splitbutton/

Namespace wyDay.Controls
  Public Class SplitButton
    Inherits Button
    Private _state As PushButtonState


    Const SplitSectionWidth As Integer = 18

    Shared BorderSize As Integer = SystemInformation.Border3DSize.Width * 2
    Private skipNextOpen As Boolean
    Private dropDownRectangle As Rectangle
    Private m_showSplit As Boolean

    Private isSplitMenuVisible As Boolean


    Private m_SplitMenuStrip As ContextMenuStrip
    Private m_SplitMenu As ContextMenu

    Private textFormatFlags As TextFormatFlags = TextFormatFlags.[Default]

    Public Sub New()
      AutoSize = True
    End Sub

#Region "Properties"

    <Browsable(False)> _
    Public Overrides Property ContextMenuStrip() As ContextMenuStrip
      Get
        Return SplitMenuStrip
      End Get
      Set(ByVal value As ContextMenuStrip)
        SplitMenuStrip = value
      End Set
    End Property

    <DefaultValue(CStr(Nothing))> _
    Public Property SplitMenu() As ContextMenu
      Get
        Return m_SplitMenu
      End Get
      Set(ByVal value As ContextMenu)
        'remove the event handlers for the old SplitMenu
        If m_SplitMenu IsNot Nothing Then
          RemoveHandler m_SplitMenu.Popup, AddressOf SplitMenu_Popup
        End If

        'add the event handlers for the new SplitMenu
        If value IsNot Nothing Then
          ShowSplit = True
          AddHandler value.Popup, AddressOf SplitMenu_Popup
        Else
          ShowSplit = False
        End If

        m_SplitMenu = value
      End Set
    End Property

    <DefaultValue(CStr(Nothing))> _
    Public Property SplitMenuStrip() As ContextMenuStrip
      Get
        Return m_SplitMenuStrip
      End Get
      Set(ByVal value As ContextMenuStrip)
        'remove the event handlers for the old SplitMenuStrip
        If m_SplitMenuStrip IsNot Nothing Then
          RemoveHandler m_SplitMenuStrip.Closing, AddressOf SplitMenuStrip_Closing
          RemoveHandler m_SplitMenuStrip.Opening, AddressOf SplitMenuStrip_Opening
        End If

        'add the event handlers for the new SplitMenuStrip
        If value IsNot Nothing Then
          ShowSplit = True
          AddHandler value.Closing, AddressOf SplitMenuStrip_Closing
          AddHandler value.Opening, AddressOf SplitMenuStrip_Opening
        Else
          ShowSplit = False
        End If


        m_SplitMenuStrip = value
      End Set
    End Property

    <DefaultValue(False)> _
    Public WriteOnly Property ShowSplit() As Boolean
      Set(ByVal value As Boolean)
        If value <> m_showSplit Then
          m_showSplit = value
          Invalidate()

          If Parent IsNot Nothing Then
            Parent.PerformLayout()
          End If
        End If
      End Set
    End Property

    Private Property State() As PushButtonState
      Get
        Return _state
      End Get
      Set(ByVal value As PushButtonState)
        If Not _state.Equals(value) Then
          _state = value
          Invalidate()
        End If
      End Set
    End Property

#End Region

    Protected Overrides Function IsInputKey(ByVal keyData As Keys) As Boolean
      If keyData.Equals(Keys.Down) AndAlso m_showSplit Then
        Return True
      End If

      Return MyBase.IsInputKey(keyData)
    End Function

    Protected Overrides Sub OnGotFocus(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnGotFocus(e)
        Return
      End If

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = PushButtonState.[Default]
      End If
    End Sub

    Protected Overrides Sub OnKeyDown(ByVal kevent As KeyEventArgs)
      If m_showSplit Then
        If kevent.KeyCode.Equals(Keys.Down) AndAlso Not isSplitMenuVisible Then
          ShowContextMenuStrip()

        ElseIf kevent.KeyCode.Equals(Keys.Space) AndAlso kevent.Modifiers = Keys.None Then
          State = PushButtonState.Pressed
        End If
      End If

      MyBase.OnKeyDown(kevent)
    End Sub

    Protected Overrides Sub OnKeyUp(ByVal kevent As KeyEventArgs)
      If kevent.KeyCode.Equals(Keys.Space) Then
        If MouseButtons = MouseButtons.None Then
          State = PushButtonState.Normal
        End If
      ElseIf kevent.KeyCode.Equals(Keys.Apps) Then
        If MouseButtons = MouseButtons.None AndAlso Not isSplitMenuVisible Then
          ShowContextMenuStrip()
        End If
      End If

      MyBase.OnKeyUp(kevent)
    End Sub

    Protected Overrides Sub OnEnabledChanged(ByVal e As EventArgs)
      State = If(Enabled, PushButtonState.Normal, PushButtonState.Disabled)

      MyBase.OnEnabledChanged(e)
    End Sub

    Protected Overrides Sub OnLostFocus(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnLostFocus(e)
        Return
      End If

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = PushButtonState.Normal
      End If
    End Sub

    Private isMouseEntered As Boolean

    Protected Overrides Sub OnMouseEnter(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseEnter(e)
        Return
      End If

      isMouseEntered = True

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = PushButtonState.Hot
      End If

    End Sub

    Protected Overrides Sub OnMouseLeave(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseLeave(e)
        Return
      End If

      isMouseEntered = False

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = If(Focused, PushButtonState.[Default], PushButtonState.Normal)
      End If
    End Sub

    Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseDown(e)
        Return
      End If

      'handle ContextMenu re-clicking the drop-down region to close the menu
      If m_SplitMenu IsNot Nothing AndAlso e.Button = MouseButtons.Left AndAlso Not isMouseEntered Then
        skipNextOpen = True
      End If

      If dropDownRectangle.Contains(e.Location) AndAlso Not isSplitMenuVisible AndAlso e.Button = MouseButtons.Left Then
        ShowContextMenuStrip()
      Else
        State = PushButtonState.Pressed
      End If
    End Sub

    Protected Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseUp(mevent)
        Return
      End If

      ' if the right button was released inside the button
      If mevent.Button = MouseButtons.Right AndAlso ClientRectangle.Contains(mevent.Location) AndAlso Not isSplitMenuVisible Then
        ShowContextMenuStrip()
      ElseIf m_SplitMenuStrip Is Nothing AndAlso m_SplitMenu Is Nothing OrElse Not isSplitMenuVisible Then
        SetButtonDrawState()

        If ClientRectangle.Contains(mevent.Location) AndAlso Not dropDownRectangle.Contains(mevent.Location) Then
          OnClick(New EventArgs())
        End If
      End If
    End Sub

    Protected Overrides Sub OnPaint(ByVal pevent As PaintEventArgs)
      MyBase.OnPaint(pevent)

      If Not m_showSplit Then
        Return
      End If

      Dim g As Graphics = pevent.Graphics
      Dim bounds As Rectangle = ClientRectangle

      ' draw the button background as according to the current state.
      If State <> PushButtonState.Pressed AndAlso IsDefault AndAlso Not Application.RenderWithVisualStyles Then
        Dim backgroundBounds As Rectangle = bounds
        backgroundBounds.Inflate(-1, -1)
        ButtonRenderer.DrawButton(g, backgroundBounds, State)

        ' button renderer doesnt draw the black frame when themes are off
        g.DrawRectangle(SystemPens.WindowFrame, 0, 0, bounds.Width - 1, bounds.Height - 1)
      Else
        ButtonRenderer.DrawButton(g, bounds, State)
      End If

      'PAINT THE BACKGROUND OF THE BUTTON ACCORDING TO THE SELECTED BACKCOLOR PROPERTY
      Dim backBrush As New SolidBrush(Me.BackColor)
      g.FillRectangle(backBrush, 4, 4, bounds.Width - 8, bounds.Height - 8)

      ' calculate the current dropdown rectangle.
      dropDownRectangle = New Rectangle(bounds.Right - SplitSectionWidth, 0, SplitSectionWidth, bounds.Height)

      Dim internalBorder As Integer = BorderSize
      Dim focusRect As New Rectangle(internalBorder - 1, internalBorder - 1, bounds.Width - dropDownRectangle.Width - internalBorder, bounds.Height - (internalBorder * 2) + 2)

      Dim drawSplitLine As Boolean = (State = PushButtonState.Hot OrElse State = PushButtonState.Pressed OrElse Not Application.RenderWithVisualStyles)

      If RightToLeft = RightToLeft.Yes Then
        dropDownRectangle.X = bounds.Left + 1
        focusRect.X = dropDownRectangle.Right

        If drawSplitLine Then
          ' draw two lines at the edge of the dropdown button
          g.DrawLine(SystemPens.ButtonShadow, bounds.Left + SplitSectionWidth, BorderSize, bounds.Left + SplitSectionWidth, bounds.Bottom - BorderSize - 1)
          g.DrawLine(SystemPens.ButtonFace, bounds.Left + SplitSectionWidth + 1, BorderSize, bounds.Left + SplitSectionWidth + 1, bounds.Bottom - BorderSize - 1)
        End If
      Else
        If drawSplitLine Then
          ' draw two lines at the edge of the dropdown button
          g.DrawLine(SystemPens.ButtonShadow, bounds.Right - SplitSectionWidth, BorderSize, bounds.Right - SplitSectionWidth, bounds.Bottom - BorderSize - 1)
          g.DrawLine(SystemPens.ButtonFace, bounds.Right - SplitSectionWidth - 1, BorderSize, bounds.Right - SplitSectionWidth - 1, bounds.Bottom - BorderSize - 1)
        End If
      End If

      ' Draw an arrow in the correct location
      PaintArrow(g, dropDownRectangle)

      'paint the image and text in the "button" part of the splitButton
      PaintTextandImage(g, New Rectangle(0, 0, ClientRectangle.Width - SplitSectionWidth, ClientRectangle.Height))

      ' draw the focus rectangle.
      If State <> PushButtonState.Pressed AndAlso Focused AndAlso ShowFocusCues Then
        ControlPaint.DrawFocusRectangle(g, focusRect)
      End If
    End Sub

    'Protected Overrides Sub OnPaint(ByVal pevent As PaintEventArgs)
    '  MyBase.OnPaint(pevent)

    '  If Not m_showSplit Then
    '    Return
    '  End If

    '  Dim g As Graphics = pevent.Graphics
    '  Dim bounds As Rectangle = ClientRectangle

    '  ' draw the button background as according to the current state.
    '  If State <> PushButtonState.Pressed AndAlso IsDefault AndAlso Not Application.RenderWithVisualStyles Then
    '    Dim backgroundBounds As Rectangle = bounds
    '    backgroundBounds.Inflate(-1, -1)
    '    ButtonRenderer.DrawButton(g, backgroundBounds, State)

    '    ' button renderer doesnt draw the black frame when themes are off
    '    g.DrawRectangle(SystemPens.WindowFrame, 0, 0, bounds.Width - 1, bounds.Height - 1)
    '  Else
    '    ButtonRenderer.DrawButton(g, bounds, State)
    '  End If

    '  ' calculate the current dropdown rectangle.
    '  dropDownRectangle = New Rectangle(bounds.Right - SplitSectionWidth, 0, SplitSectionWidth, bounds.Height)

    '  Dim internalBorder As Integer = BorderSize
    '  Dim focusRect As New Rectangle(internalBorder - 1, internalBorder - 1, bounds.Width - dropDownRectangle.Width - internalBorder, bounds.Height - (internalBorder * 2) + 2)

    '  Dim drawSplitLine As Boolean = (State = PushButtonState.Hot OrElse State = PushButtonState.Pressed OrElse Not Application.RenderWithVisualStyles)

    '  If RightToLeft = RightToLeft.Yes Then
    '    dropDownRectangle.X = bounds.Left + 1
    '    focusRect.X = dropDownRectangle.Right

    '    If drawSplitLine Then
    '      ' draw two lines at the edge of the dropdown button
    '      g.DrawLine(SystemPens.ButtonShadow, bounds.Left + SplitSectionWidth, BorderSize, bounds.Left + SplitSectionWidth, bounds.Bottom - BorderSize)
    '      g.DrawLine(SystemPens.ButtonFace, bounds.Left + SplitSectionWidth + 1, BorderSize, bounds.Left + SplitSectionWidth + 1, bounds.Bottom - BorderSize)
    '    End If
    '  Else
    '    If drawSplitLine Then
    '      ' draw two lines at the edge of the dropdown button
    '      g.DrawLine(SystemPens.ButtonShadow, bounds.Right - SplitSectionWidth, BorderSize, bounds.Right - SplitSectionWidth, bounds.Bottom - BorderSize)
    '      g.DrawLine(SystemPens.ButtonFace, bounds.Right - SplitSectionWidth - 1, BorderSize, bounds.Right - SplitSectionWidth - 1, bounds.Bottom - BorderSize)
    '    End If
    '  End If

    '  ' Draw an arrow in the correct location
    '  PaintArrow(g, dropDownRectangle)

    '  'paint the image and text in the "button" part of the splitButton
    '  PaintTextandImage(g, New Rectangle(0, 0, ClientRectangle.Width - SplitSectionWidth, ClientRectangle.Height))

    '  ' draw the focus rectangle.
    '  If State <> PushButtonState.Pressed AndAlso Focused AndAlso ShowFocusCues Then
    '    ControlPaint.DrawFocusRectangle(g, focusRect)
    '  End If
    'End Sub

    Private Sub PaintTextandImage(ByVal g As Graphics, ByVal bounds As Rectangle)
      ' Figure out where our text and image should go
      Dim text_rectangle As Rectangle
      Dim image_rectangle As Rectangle

      CalculateButtonTextAndImageLayout(bounds, text_rectangle, image_rectangle)

      'draw the image
      If Image IsNot Nothing Then
        If Enabled Then
          g.DrawImage(Image, image_rectangle.X, image_rectangle.Y, Image.Width, Image.Height)
        Else
          ControlPaint.DrawImageDisabled(g, Image, image_rectangle.X, image_rectangle.Y, BackColor)
        End If
      End If

      ' If we dont' use mnemonic, set formatFlag to NoPrefix as this will show ampersand.
      If Not UseMnemonic Then
        textFormatFlags = textFormatFlags Or TextFormatFlags.NoPrefix
      ElseIf Not ShowKeyboardCues Then
        textFormatFlags = textFormatFlags Or TextFormatFlags.HidePrefix
      End If

      'draw the text
      If Not String.IsNullOrEmpty(Text) Then
        If Enabled Then
          TextRenderer.DrawText(g, Text, Font, text_rectangle, ForeColor, textFormatFlags)
        Else
          ControlPaint.DrawStringDisabled(g, Text, Font, BackColor, text_rectangle, textFormatFlags)
        End If
      End If
    End Sub

    Private Sub PaintArrow(ByVal g As Graphics, ByVal dropDownRect As Rectangle)
      Dim middle As New Point(Convert.ToInt32(dropDownRect.Left + dropDownRect.Width \ 2), Convert.ToInt32(dropDownRect.Top + dropDownRect.Height \ 2))

      'if the width is odd - favor pushing it over one pixel right.
      middle.X += (dropDownRect.Width Mod 2)

      Dim arrow As Point() = New Point() {New Point(middle.X - 2, middle.Y - 1), New Point(middle.X + 3, middle.Y - 1), New Point(middle.X, middle.Y + 2)}

      If Enabled Then
        g.FillPolygon(SystemBrushes.ControlText, arrow)
      Else
        g.FillPolygon(SystemBrushes.ButtonShadow, arrow)
      End If
    End Sub

    Public Overrides Function GetPreferredSize(ByVal proposedSize As Size) As Size
      Dim preferredSize As Size = MyBase.GetPreferredSize(proposedSize)

      'autosize correctly for splitbuttons
      If m_showSplit Then
        If AutoSize Then
          Return CalculateButtonAutoSize()
        End If

        If Not String.IsNullOrEmpty(Text) AndAlso TextRenderer.MeasureText(Text, Font).Width + SplitSectionWidth > preferredSize.Width Then
          Return preferredSize + New Size(SplitSectionWidth + BorderSize * 2, 0)
        End If
      End If

      Return preferredSize
    End Function

    Private Function CalculateButtonAutoSize() As Size
      Dim ret_size As Size = Size.Empty
      Dim text_size As Size = TextRenderer.MeasureText(Text, Font)
      Dim image_size As Size
      If Image Is Nothing Then
        image_size = Size.Empty
      Else
        image_size = Image.Size
      End If

      ' Pad the text size
      If Text.Length <> 0 Then
        text_size.Height += 4
        text_size.Width += 4
      End If

      Select Case TextImageRelation
        Case TextImageRelation.Overlay
          ret_size.Height = Math.Max(If(Text.Length = 0, 0, text_size.Height), image_size.Height)
          ret_size.Width = Math.Max(text_size.Width, image_size.Width)
          Exit Select
        Case TextImageRelation.ImageAboveText, TextImageRelation.TextAboveImage
          ret_size.Height = text_size.Height + image_size.Height
          ret_size.Width = Math.Max(text_size.Width, image_size.Width)
          Exit Select
        Case TextImageRelation.ImageBeforeText, TextImageRelation.TextBeforeImage
          ret_size.Height = Math.Max(text_size.Height, image_size.Height)
          ret_size.Width = text_size.Width + image_size.Width
          Exit Select
      End Select

      ' Pad the result
      ret_size.Height += (Padding.Vertical + 6)
      ret_size.Width += (Padding.Horizontal + 6)

      'pad the splitButton arrow region
      If m_showSplit Then
        ret_size.Width += SplitSectionWidth
      End If

      Return ret_size
    End Function

#Region "Button Layout Calculations"

    'The following layout functions were taken from Mono's Windows.Forms 
    'implementation, specifically "ThemeWin32Classic.cs", 
    'then modified to fit the context of this splitButton

    Private Sub CalculateButtonTextAndImageLayout(ByRef content_rect As Rectangle, ByRef textRectangle As Rectangle, ByRef imageRectangle As Rectangle)
      Dim text_size As Size = TextRenderer.MeasureText(Text, Font, content_rect.Size, textFormatFlags)
      'Dim image_size As Size = If(Image Is Nothing, Size.Empty, Image.Size)
      Dim image_size As Size
      If Image Is Nothing Then
        image_size = Size.Empty
      Else
        image_size = Image.Size
      End If

      textRectangle = Rectangle.Empty
      imageRectangle = Rectangle.Empty

      Select Case TextImageRelation
        Case TextImageRelation.Overlay
          ' Overlay is easy, text always goes here
          textRectangle = OverlayObjectRect(content_rect, text_size, TextAlign)
          ' Rectangle.Inflate(content_rect, -4, -4);
          'Offset on Windows 98 style when button is pressed
          If _state = PushButtonState.Pressed AndAlso Not Application.RenderWithVisualStyles Then
            textRectangle.Offset(1, 1)
          End If

          ' Image is dependent on ImageAlign
          If Image IsNot Nothing Then
            imageRectangle = OverlayObjectRect(content_rect, image_size, ImageAlign)
          End If

          Exit Select
        Case TextImageRelation.ImageAboveText
          content_rect.Inflate(-4, -4)
          LayoutTextAboveOrBelowImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
        Case TextImageRelation.TextAboveImage
          content_rect.Inflate(-4, -4)
          LayoutTextAboveOrBelowImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
        Case TextImageRelation.ImageBeforeText
          content_rect.Inflate(-4, -4)
          LayoutTextBeforeOrAfterImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
        Case TextImageRelation.TextBeforeImage
          content_rect.Inflate(-4, -4)
          LayoutTextBeforeOrAfterImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
      End Select
    End Sub

    Private Shared Function OverlayObjectRect(ByRef container As Rectangle, ByRef sizeOfObject As Size, ByVal alignment As System.Drawing.ContentAlignment) As Rectangle
      Dim x As Integer, y As Integer

      Select Case alignment
        Case System.Drawing.ContentAlignment.TopLeft
          x = 4
          y = 4
          Exit Select
        Case System.Drawing.ContentAlignment.TopCenter
          x = (container.Width - sizeOfObject.Width) \ 2
          y = 4
          Exit Select
        Case System.Drawing.ContentAlignment.TopRight
          x = container.Width - sizeOfObject.Width - 4
          y = 4
          Exit Select
        Case System.Drawing.ContentAlignment.MiddleLeft
          x = 4
          y = (container.Height - sizeOfObject.Height) \ 2
          Exit Select
        Case System.Drawing.ContentAlignment.MiddleCenter
          x = (container.Width - sizeOfObject.Width) \ 2
          y = (container.Height - sizeOfObject.Height) \ 2
          Exit Select
        Case System.Drawing.ContentAlignment.MiddleRight
          x = container.Width - sizeOfObject.Width - 4
          y = (container.Height - sizeOfObject.Height) \ 2
          Exit Select
        Case System.Drawing.ContentAlignment.BottomLeft
          x = 4
          y = container.Height - sizeOfObject.Height - 4
          Exit Select
        Case System.Drawing.ContentAlignment.BottomCenter
          x = (container.Width - sizeOfObject.Width) \ 2
          y = container.Height - sizeOfObject.Height - 4
          Exit Select
        Case System.Drawing.ContentAlignment.BottomRight
          x = container.Width - sizeOfObject.Width - 4
          y = container.Height - sizeOfObject.Height - 4
          Exit Select
        Case Else
          x = 4
          y = 4
          Exit Select
      End Select

      Return New Rectangle(x, y, sizeOfObject.Width, sizeOfObject.Height)
    End Function

    Private Sub LayoutTextBeforeOrAfterImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
      Dim element_spacing As Integer = 0
      ' Spacing between the Text and the Image
      Dim total_width As Integer = textSize.Width + element_spacing + imageSize.Width

      If Not textFirst Then
        element_spacing += 2
      End If

      ' If the text is too big, chop it down to the size we have available to it
      If total_width > totalArea.Width Then
        textSize.Width = totalArea.Width - element_spacing - imageSize.Width
        total_width = totalArea.Width
      End If

      Dim excess_width As Integer = totalArea.Width - total_width
      Dim offset As Integer = 0

      Dim final_text_rect As Rectangle
      Dim final_image_rect As Rectangle

      Dim h_text As HorizontalAlignment = GetHorizontalAlignment(TextAlign)
      Dim h_image As HorizontalAlignment = GetHorizontalAlignment(ImageAlign)

      If h_image = HorizontalAlignment.Left Then
        offset = 0
      ElseIf h_image = HorizontalAlignment.Right AndAlso h_text = HorizontalAlignment.Right Then
        offset = excess_width
      ElseIf h_image = HorizontalAlignment.Center AndAlso (h_text = HorizontalAlignment.Left OrElse h_text = HorizontalAlignment.Center) Then
        offset += excess_width \ 3
      Else
        offset += 2 * (excess_width \ 3)
      End If

      If textFirst Then
        final_text_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
        final_image_rect = New Rectangle(final_text_rect.Right + element_spacing, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
      Else
        final_image_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
        final_text_rect = New Rectangle(final_image_rect.Right + element_spacing, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
      End If

      textRect = final_text_rect
      imageRect = final_image_rect
    End Sub

    Private Sub LayoutTextAboveOrBelowImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
      Dim element_spacing As Integer = 0
      ' Spacing between the Text and the Image
      Dim total_height As Integer = textSize.Height + element_spacing + imageSize.Height

      If textFirst Then
        element_spacing += 2
      End If

      If textSize.Width > totalArea.Width Then
        textSize.Width = totalArea.Width
      End If

      ' If the there isn't enough room and we're text first, cut out the image
      If total_height > totalArea.Height AndAlso textFirst Then
        imageSize = Size.Empty
        total_height = totalArea.Height
      End If

      Dim excess_height As Integer = totalArea.Height - total_height
      Dim offset As Integer = 0

      Dim final_text_rect As Rectangle
      Dim final_image_rect As Rectangle

      Dim v_text As VerticalAlignment = GetVerticalAlignment(TextAlign)
      Dim v_image As VerticalAlignment = GetVerticalAlignment(ImageAlign)

      If v_image = VerticalAlignment.Top Then
        offset = 0
      ElseIf v_image = VerticalAlignment.Bottom AndAlso v_text = VerticalAlignment.Bottom Then
        offset = excess_height
      ElseIf v_image = VerticalAlignment.Center AndAlso (v_text = VerticalAlignment.Top OrElse v_text = VerticalAlignment.Center) Then
        offset += excess_height \ 3
      Else
        offset += 2 * (excess_height \ 3)
      End If

      If textFirst Then
        final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, totalArea.Top + offset, textSize.Width, textSize.Height)
        final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, final_text_rect.Bottom + element_spacing, imageSize.Width, imageSize.Height)
      Else
        final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, totalArea.Top + offset, imageSize.Width, imageSize.Height)
        final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, final_image_rect.Bottom + element_spacing, textSize.Width, textSize.Height)

        If final_text_rect.Bottom > totalArea.Bottom Then
          final_text_rect.Y = totalArea.Top
        End If
      End If

      textRect = final_text_rect
      imageRect = final_image_rect
    End Sub

    Private Shared Function GetHorizontalAlignment(ByVal align As System.Drawing.ContentAlignment) As HorizontalAlignment
      Select Case align
        Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.TopLeft
          Return HorizontalAlignment.Left
        Case System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.TopCenter
          Return HorizontalAlignment.Center
        Case System.Drawing.ContentAlignment.BottomRight, System.Drawing.ContentAlignment.MiddleRight, System.Drawing.ContentAlignment.TopRight
          Return HorizontalAlignment.Right
      End Select

      Return HorizontalAlignment.Left
    End Function

    Private Shared Function GetVerticalAlignment(ByVal align As System.Drawing.ContentAlignment) As VerticalAlignment
      Select Case align
        Case System.Drawing.ContentAlignment.TopLeft, System.Drawing.ContentAlignment.TopCenter, System.Drawing.ContentAlignment.TopRight
          Return VerticalAlignment.Top
        Case System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.MiddleRight
          Return VerticalAlignment.Center
        Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.BottomRight
          Return VerticalAlignment.Bottom
      End Select

      Return VerticalAlignment.Top
    End Function

    Friend Shared Function AlignInRectangle(ByVal outer As Rectangle, ByVal inner As Size, ByVal align As System.Drawing.ContentAlignment) As Rectangle
      Dim x As Integer = 0
      Dim y As Integer = 0

      If align = System.Drawing.ContentAlignment.BottomLeft OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.TopLeft Then
        x = outer.X
      ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.TopCenter Then
        x = Math.Max(outer.X + ((outer.Width - inner.Width) \ 2), outer.Left)
      ElseIf align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.MiddleRight OrElse align = System.Drawing.ContentAlignment.TopRight Then
        x = outer.Right - inner.Width
      End If
      If align = System.Drawing.ContentAlignment.TopCenter OrElse align = System.Drawing.ContentAlignment.TopLeft OrElse align = System.Drawing.ContentAlignment.TopRight Then
        y = outer.Y
      ElseIf align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.MiddleRight Then
        y = outer.Y + (outer.Height - inner.Height) \ 2
      ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.BottomLeft Then
        y = outer.Bottom - inner.Height
      End If

      Return New Rectangle(x, y, Math.Min(inner.Width, outer.Width), Math.Min(inner.Height, outer.Height))
    End Function

#End Region


    Private Sub ShowContextMenuStrip()
      If skipNextOpen Then
        ' we were called because we're closing the context menu strip
        ' when clicking the dropdown button.
        skipNextOpen = False
        Return
      End If

      State = PushButtonState.Pressed

      If m_SplitMenu IsNot Nothing Then
        m_SplitMenu.Show(Me, New Point(0, Height))
      ElseIf m_SplitMenuStrip IsNot Nothing Then
        m_SplitMenuStrip.Show(Me, New Point(0, Height), ToolStripDropDownDirection.BelowRight)
      End If
    End Sub

    Private Sub SplitMenuStrip_Opening(ByVal sender As Object, ByVal e As CancelEventArgs)
      isSplitMenuVisible = True
    End Sub

    Private Sub SplitMenuStrip_Closing(ByVal sender As Object, ByVal e As ToolStripDropDownClosingEventArgs)
      isSplitMenuVisible = False

      SetButtonDrawState()

      If e.CloseReason = ToolStripDropDownCloseReason.AppClicked Then
        skipNextOpen = (dropDownRectangle.Contains(PointToClient(Cursor.Position))) AndAlso MouseButtons = MouseButtons.Left
      End If
    End Sub


    Private Sub SplitMenu_Popup(ByVal sender As Object, ByVal e As EventArgs)
      isSplitMenuVisible = True
    End Sub

    Protected Overrides Sub WndProc(ByRef m As Message)
      '0x0212 == WM_EXITMENULOOP
      If m.Msg = &H212 Then
        'this message is only sent when a ContextMenu is closed (not a ContextMenuStrip)
        isSplitMenuVisible = False
        SetButtonDrawState()
      End If

      MyBase.WndProc(m)
    End Sub

    Private Sub SetButtonDrawState()
      If Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
        State = PushButtonState.Hot
      ElseIf Focused Then
        State = PushButtonState.[Default]
      ElseIf Not Enabled Then
        State = PushButtonState.Disabled
      Else
        State = PushButtonState.Normal
      End If
    End Sub
  End Class
End Namespace

Open in new window

SplitButton-Designer.png
SplitButton-RunTime.png
Avatar of samml
samml
Flag of Germany image

This is the code code doing it for the Windows Forms Label:
Public Property UseMnemonic As Boolean
    Get
        Return (Me.labelState.Item(Label.StateUseMnemonic) <> 0)
    End Get
    Set(ByVal value As Boolean)
        If (Me.UseMnemonic <> value) Then
            Me.labelState.Item(Label.StateUseMnemonic) = If(value, 1, 0)
            Me.MeasureTextCache.InvalidateCache
            Using LayoutTransaction.CreateTransactionIf(Me.AutoSize, Me.ParentInternal, Me, PropertyNames.Text)
                Me.AdjustSize
                MyBase.Invalidate
            End Using
            If MyBase.IsHandleCreated Then
                Dim windowStyle As Integer = MyBase.WindowStyle
                If Not Me.UseMnemonic Then
                    windowStyle = (windowStyle Or &H80)
                Else
                    windowStyle = (windowStyle And -129)
                End If
                MyBase.WindowStyle = windowStyle
            End If
        End If
    End Set
End Property

Open in new window

Especially the part after the
If MyBase.IsHandleCreated Then

Open in new window

should be interesting.
Avatar of blueskybooking

ASKER

My apologies on the delay.

I tried to massage the Label code intoUseMnemonic, but I was unable to convert it.  Perhaps someone could shed a light on how to fix this.
SplitButton-UseMnemonic.png
For assistance with debugging, here is the latest version of the full class.

Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.VisualStyles

'Get the latest version of SplitButton at: http://wyday.com/splitbutton/

Namespace wyDay.Controls
  Public Class SplitButton
    Inherits Button
    Private _state As PushButtonState


    Const SplitSectionWidth As Integer = 18

    Shared BorderSize As Integer = SystemInformation.Border3DSize.Width * 2
    Private skipNextOpen As Boolean
    Private dropDownRectangle As Rectangle
    Private m_showSplit As Boolean

    Private isSplitMenuVisible As Boolean

    Private m_SplitMenuStrip As ContextMenuStrip
    Private m_SplitMenu As ContextMenu

    Private textFormatFlags As TextFormatFlags = textFormatFlags.[Default]

    Private m_isMnemonic As Boolean

    Public Sub New()
      AutoSize = True
    End Sub

#Region "Properties"

    <DefaultValue(False)> _
    Public Overrides Property UseMnemonic() As Boolean
      Get
        Return m_isMnemonic
      End Get
      Set(ByVal value As Boolean)
        m_isMnemonic = value
        If (Me.UseMnemonic <> value) Then
          'Me.MeasureTextCache.InvalidateCache()
          'Using LayoutTransaction.CreateTransactionIf(Me.AutoSize, Me.ParentInternal, Me, PropertyNames.Text)
          '  Me.AdjustSize()
          '  MyBase.Invalidate()
          'End Using
          If MyBase.IsHandleCreated Then
            Dim windowStyle As Integer = MyBase.WindowStyle
            If Not Me.UseMnemonic Then
              windowStyle = (windowStyle Or &H80)
            Else
              windowStyle = (windowStyle And -129)
            End If
            MyBase.WindowStyle = windowStyle
          End If
        End If
      End Set
    End Property

    <Browsable(False)> _
    Public Overrides Property ContextMenuStrip() As ContextMenuStrip
      Get
        Return SplitMenuStrip
      End Get
      Set(ByVal value As ContextMenuStrip)
        SplitMenuStrip = value
      End Set
    End Property

    <DefaultValue(CStr(Nothing))> _
    Public Property SplitMenu() As ContextMenu
      Get
        Return m_SplitMenu
      End Get
      Set(ByVal value As ContextMenu)
        'remove the event handlers for the old SplitMenu
        If m_SplitMenu IsNot Nothing Then
          RemoveHandler m_SplitMenu.Popup, AddressOf SplitMenu_Popup
        End If

        'add the event handlers for the new SplitMenu
        If value IsNot Nothing Then
          ShowSplit = True
          AddHandler value.Popup, AddressOf SplitMenu_Popup
        Else
          ShowSplit = False
        End If

        m_SplitMenu = value
      End Set
    End Property

    <DefaultValue(CStr(Nothing))> _
    Public Property SplitMenuStrip() As ContextMenuStrip
      Get
        Return m_SplitMenuStrip
      End Get
      Set(ByVal value As ContextMenuStrip)
        'remove the event handlers for the old SplitMenuStrip
        If m_SplitMenuStrip IsNot Nothing Then
          RemoveHandler m_SplitMenuStrip.Closing, AddressOf SplitMenuStrip_Closing
          RemoveHandler m_SplitMenuStrip.Opening, AddressOf SplitMenuStrip_Opening
        End If

        'add the event handlers for the new SplitMenuStrip
        If value IsNot Nothing Then
          ShowSplit = True
          AddHandler value.Closing, AddressOf SplitMenuStrip_Closing
          AddHandler value.Opening, AddressOf SplitMenuStrip_Opening
        Else
          ShowSplit = False
        End If


        m_SplitMenuStrip = value
      End Set
    End Property

    <DefaultValue(False)> _
    Public WriteOnly Property ShowSplit() As Boolean
      Set(ByVal value As Boolean)
        If value <> m_showSplit Then
          m_showSplit = value
          Invalidate()

          If Parent IsNot Nothing Then
            Parent.PerformLayout()
          End If
        End If
      End Set
    End Property

    Private Property State() As PushButtonState
      Get
        Return _state
      End Get
      Set(ByVal value As PushButtonState)
        If Not _state.Equals(value) Then
          _state = value
          Invalidate()
        End If
      End Set
    End Property

#End Region

    Protected Overrides Function IsInputKey(ByVal keyData As Keys) As Boolean
      If keyData.Equals(Keys.Down) AndAlso m_showSplit Then
        Return True
      End If

      Return MyBase.IsInputKey(keyData)
    End Function

    Protected Overrides Sub OnGotFocus(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnGotFocus(e)
        Return
      End If

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = PushButtonState.[Default]
      End If
    End Sub

    Protected Overrides Sub OnKeyDown(ByVal kevent As KeyEventArgs)
      If m_showSplit Then
        If kevent.KeyCode.Equals(Keys.Down) AndAlso Not isSplitMenuVisible Then
          ShowContextMenuStrip()

        ElseIf kevent.KeyCode.Equals(Keys.Space) AndAlso kevent.Modifiers = Keys.None Then
          State = PushButtonState.Pressed
        End If
      End If

      MyBase.OnKeyDown(kevent)
    End Sub

    Protected Overrides Sub OnKeyUp(ByVal kevent As KeyEventArgs)
      If kevent.KeyCode.Equals(Keys.Space) Then
        If MouseButtons = MouseButtons.None Then
          State = PushButtonState.Normal
        End If
      ElseIf kevent.KeyCode.Equals(Keys.Apps) Then
        If MouseButtons = MouseButtons.None AndAlso Not isSplitMenuVisible Then
          ShowContextMenuStrip()
        End If
      End If

      MyBase.OnKeyUp(kevent)
    End Sub

    Protected Overrides Sub OnEnabledChanged(ByVal e As EventArgs)
      State = If(Enabled, PushButtonState.Normal, PushButtonState.Disabled)

      MyBase.OnEnabledChanged(e)
    End Sub

    Protected Overrides Sub OnLostFocus(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnLostFocus(e)
        Return
      End If

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = PushButtonState.Normal
      End If
    End Sub

    Private isMouseEntered As Boolean

    Protected Overrides Sub OnMouseEnter(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseEnter(e)
        Return
      End If

      isMouseEntered = True

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = PushButtonState.Hot
      End If

    End Sub

    Protected Overrides Sub OnMouseLeave(ByVal e As EventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseLeave(e)
        Return
      End If

      isMouseEntered = False

      If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
        State = If(Focused, PushButtonState.[Default], PushButtonState.Normal)
      End If
    End Sub

    Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseDown(e)
        Return
      End If

      'handle ContextMenu re-clicking the drop-down region to close the menu
      If m_SplitMenu IsNot Nothing AndAlso e.Button = MouseButtons.Left AndAlso Not isMouseEntered Then
        skipNextOpen = True
      End If

      If dropDownRectangle.Contains(e.Location) AndAlso Not isSplitMenuVisible AndAlso e.Button = MouseButtons.Left Then
        ShowContextMenuStrip()
      Else
        State = PushButtonState.Pressed
      End If
    End Sub

    Protected Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
      If Not m_showSplit Then
        MyBase.OnMouseUp(mevent)
        Return
      End If

      ' if the right button was released inside the button
      If mevent.Button = MouseButtons.Right AndAlso ClientRectangle.Contains(mevent.Location) AndAlso Not isSplitMenuVisible Then
        ShowContextMenuStrip()
      ElseIf m_SplitMenuStrip Is Nothing AndAlso m_SplitMenu Is Nothing OrElse Not isSplitMenuVisible Then
        SetButtonDrawState()

        If ClientRectangle.Contains(mevent.Location) AndAlso Not dropDownRectangle.Contains(mevent.Location) Then
          OnClick(New EventArgs())
        End If
      End If
    End Sub

    Protected Overrides Sub OnPaint(ByVal pevent As PaintEventArgs)
      MyBase.OnPaint(pevent)

      If Not m_showSplit Then
        Return
      End If

      Dim g As Graphics = pevent.Graphics
      Dim bounds As Rectangle = ClientRectangle

      ' draw the button background as according to the current state.
      If State <> PushButtonState.Pressed AndAlso IsDefault AndAlso Not Application.RenderWithVisualStyles Then
        Dim backgroundBounds As Rectangle = bounds
        backgroundBounds.Inflate(-1, -1)
        ButtonRenderer.DrawButton(g, backgroundBounds, State)

        ' button renderer doesnt draw the black frame when themes are off
        g.DrawRectangle(SystemPens.WindowFrame, 0, 0, bounds.Width - 1, bounds.Height - 1)
      Else
        ButtonRenderer.DrawButton(g, bounds, State)
      End If

      'PAINT THE BACKGROUND OF THE BUTTON ACCORDING TO THE SELECTED BACKCOLOR PROPERTY
      Dim backBrush As New SolidBrush(Me.BackColor)
      g.FillRectangle(backBrush, 4, 4, bounds.Width - 8, bounds.Height - 8)

      ' calculate the current dropdown rectangle.
      dropDownRectangle = New Rectangle(bounds.Right - SplitSectionWidth, 0, SplitSectionWidth, bounds.Height)

      Dim internalBorder As Integer = BorderSize
      Dim focusRect As New Rectangle(internalBorder - 1, internalBorder - 1, bounds.Width - dropDownRectangle.Width - internalBorder, bounds.Height - (internalBorder * 2) + 2)

      Dim drawSplitLine As Boolean = (State = PushButtonState.Hot OrElse State = PushButtonState.Pressed OrElse Not Application.RenderWithVisualStyles)

      If RightToLeft = RightToLeft.Yes Then
        dropDownRectangle.X = bounds.Left + 1
        focusRect.X = dropDownRectangle.Right

        If drawSplitLine Then
          ' draw two lines at the edge of the dropdown button
          g.DrawLine(SystemPens.ButtonShadow, bounds.Left + SplitSectionWidth, BorderSize, bounds.Left + SplitSectionWidth, bounds.Bottom - BorderSize - 1)
          g.DrawLine(SystemPens.ButtonFace, bounds.Left + SplitSectionWidth + 1, BorderSize, bounds.Left + SplitSectionWidth + 1, bounds.Bottom - BorderSize - 1)
        End If
      Else
        If drawSplitLine Then
          ' draw two lines at the edge of the dropdown button
          g.DrawLine(SystemPens.ButtonShadow, bounds.Right - SplitSectionWidth, BorderSize, bounds.Right - SplitSectionWidth, bounds.Bottom - BorderSize - 1)
          g.DrawLine(SystemPens.ButtonFace, bounds.Right - SplitSectionWidth - 1, BorderSize, bounds.Right - SplitSectionWidth - 1, bounds.Bottom - BorderSize - 1)
        End If
      End If

      ' Draw an arrow in the correct location
      PaintArrow(g, dropDownRectangle)

      'paint the image and text in the "button" part of the splitButton
      PaintTextandImage(g, New Rectangle(0, 0, ClientRectangle.Width - SplitSectionWidth, ClientRectangle.Height))

      ' draw the focus rectangle.
      If State <> PushButtonState.Pressed AndAlso Focused AndAlso ShowFocusCues Then
        ControlPaint.DrawFocusRectangle(g, focusRect)
      End If
    End Sub

    'Protected Overrides Sub OnPaint(ByVal pevent As PaintEventArgs)
    '  MyBase.OnPaint(pevent)

    '  If Not m_showSplit Then
    '    Return
    '  End If

    '  Dim g As Graphics = pevent.Graphics
    '  Dim bounds As Rectangle = ClientRectangle

    '  ' draw the button background as according to the current state.
    '  If State <> PushButtonState.Pressed AndAlso IsDefault AndAlso Not Application.RenderWithVisualStyles Then
    '    Dim backgroundBounds As Rectangle = bounds
    '    backgroundBounds.Inflate(-1, -1)
    '    ButtonRenderer.DrawButton(g, backgroundBounds, State)

    '    ' button renderer doesnt draw the black frame when themes are off
    '    g.DrawRectangle(SystemPens.WindowFrame, 0, 0, bounds.Width - 1, bounds.Height - 1)
    '  Else
    '    ButtonRenderer.DrawButton(g, bounds, State)
    '  End If

    '  ' calculate the current dropdown rectangle.
    '  dropDownRectangle = New Rectangle(bounds.Right - SplitSectionWidth, 0, SplitSectionWidth, bounds.Height)

    '  Dim internalBorder As Integer = BorderSize
    '  Dim focusRect As New Rectangle(internalBorder - 1, internalBorder - 1, bounds.Width - dropDownRectangle.Width - internalBorder, bounds.Height - (internalBorder * 2) + 2)

    '  Dim drawSplitLine As Boolean = (State = PushButtonState.Hot OrElse State = PushButtonState.Pressed OrElse Not Application.RenderWithVisualStyles)

    '  If RightToLeft = RightToLeft.Yes Then
    '    dropDownRectangle.X = bounds.Left + 1
    '    focusRect.X = dropDownRectangle.Right

    '    If drawSplitLine Then
    '      ' draw two lines at the edge of the dropdown button
    '      g.DrawLine(SystemPens.ButtonShadow, bounds.Left + SplitSectionWidth, BorderSize, bounds.Left + SplitSectionWidth, bounds.Bottom - BorderSize)
    '      g.DrawLine(SystemPens.ButtonFace, bounds.Left + SplitSectionWidth + 1, BorderSize, bounds.Left + SplitSectionWidth + 1, bounds.Bottom - BorderSize)
    '    End If
    '  Else
    '    If drawSplitLine Then
    '      ' draw two lines at the edge of the dropdown button
    '      g.DrawLine(SystemPens.ButtonShadow, bounds.Right - SplitSectionWidth, BorderSize, bounds.Right - SplitSectionWidth, bounds.Bottom - BorderSize)
    '      g.DrawLine(SystemPens.ButtonFace, bounds.Right - SplitSectionWidth - 1, BorderSize, bounds.Right - SplitSectionWidth - 1, bounds.Bottom - BorderSize)
    '    End If
    '  End If

    '  ' Draw an arrow in the correct location
    '  PaintArrow(g, dropDownRectangle)

    '  'paint the image and text in the "button" part of the splitButton
    '  PaintTextandImage(g, New Rectangle(0, 0, ClientRectangle.Width - SplitSectionWidth, ClientRectangle.Height))

    '  ' draw the focus rectangle.
    '  If State <> PushButtonState.Pressed AndAlso Focused AndAlso ShowFocusCues Then
    '    ControlPaint.DrawFocusRectangle(g, focusRect)
    '  End If
    'End Sub

    Private Sub PaintTextandImage(ByVal g As Graphics, ByVal bounds As Rectangle)
      ' Figure out where our text and image should go
      Dim text_rectangle As Rectangle
      Dim image_rectangle As Rectangle

      CalculateButtonTextAndImageLayout(bounds, text_rectangle, image_rectangle)

      'draw the image
      If Image IsNot Nothing Then
        If Enabled Then
          g.DrawImage(Image, image_rectangle.X, image_rectangle.Y, Image.Width, Image.Height)
        Else
          ControlPaint.DrawImageDisabled(g, Image, image_rectangle.X, image_rectangle.Y, BackColor)
        End If
      End If

      ' If we dont' use mnemonic, set formatFlag to NoPrefix as this will show ampersand.
      If Not UseMnemonic Then
        textFormatFlags = textFormatFlags Or textFormatFlags.NoPrefix
      ElseIf Not ShowKeyboardCues Then
        textFormatFlags = textFormatFlags Or textFormatFlags.HidePrefix
      End If

      'draw the text
      If Not String.IsNullOrEmpty(Text) Then
        If Enabled Then
          TextRenderer.DrawText(g, Text, Font, text_rectangle, ForeColor, textFormatFlags)
        Else
          ControlPaint.DrawStringDisabled(g, Text, Font, BackColor, text_rectangle, textFormatFlags)
        End If
      End If
    End Sub

    Private Sub PaintArrow(ByVal g As Graphics, ByVal dropDownRect As Rectangle)
      Dim middle As New Point(Convert.ToInt32(dropDownRect.Left + dropDownRect.Width \ 2), Convert.ToInt32(dropDownRect.Top + dropDownRect.Height \ 2))

      'if the width is odd - favor pushing it over one pixel right.
      middle.X += (dropDownRect.Width Mod 2)

      Dim arrow As Point() = New Point() {New Point(middle.X - 2, middle.Y - 1), New Point(middle.X + 3, middle.Y - 1), New Point(middle.X, middle.Y + 2)}

      If Enabled Then
        g.FillPolygon(SystemBrushes.ControlText, arrow)
      Else
        g.FillPolygon(SystemBrushes.ButtonShadow, arrow)
      End If
    End Sub

    Public Overrides Function GetPreferredSize(ByVal proposedSize As Size) As Size
      Dim preferredSize As Size = MyBase.GetPreferredSize(proposedSize)

      'autosize correctly for splitbuttons
      If m_showSplit Then
        If AutoSize Then
          Return CalculateButtonAutoSize()
        End If

        If Not String.IsNullOrEmpty(Text) AndAlso TextRenderer.MeasureText(Text, Font).Width + SplitSectionWidth > preferredSize.Width Then
          Return preferredSize + New Size(SplitSectionWidth + BorderSize * 2, 0)
        End If
      End If

      Return preferredSize
    End Function

    Private Function CalculateButtonAutoSize() As Size
      Dim ret_size As Size = Size.Empty
      Dim text_size As Size = TextRenderer.MeasureText(Text, Font)
      Dim image_size As Size
      If Image Is Nothing Then
        image_size = Size.Empty
      Else
        image_size = Image.Size
      End If

      ' Pad the text size
      If Text.Length <> 0 Then
        text_size.Height += 4
        text_size.Width += 4
      End If

      Select Case TextImageRelation
        Case TextImageRelation.Overlay
          ret_size.Height = Math.Max(If(Text.Length = 0, 0, text_size.Height), image_size.Height)
          ret_size.Width = Math.Max(text_size.Width, image_size.Width)
          Exit Select
        Case TextImageRelation.ImageAboveText, TextImageRelation.TextAboveImage
          ret_size.Height = text_size.Height + image_size.Height
          ret_size.Width = Math.Max(text_size.Width, image_size.Width)
          Exit Select
        Case TextImageRelation.ImageBeforeText, TextImageRelation.TextBeforeImage
          ret_size.Height = Math.Max(text_size.Height, image_size.Height)
          ret_size.Width = text_size.Width + image_size.Width
          Exit Select
      End Select

      ' Pad the result
      ret_size.Height += (Padding.Vertical + 6)
      ret_size.Width += (Padding.Horizontal + 6)

      'pad the splitButton arrow region
      If m_showSplit Then
        ret_size.Width += SplitSectionWidth
      End If

      Return ret_size
    End Function

#Region "Button Layout Calculations"

    'The following layout functions were taken from Mono's Windows.Forms 
    'implementation, specifically "ThemeWin32Classic.cs", 
    'then modified to fit the context of this splitButton

    Private Sub CalculateButtonTextAndImageLayout(ByRef content_rect As Rectangle, ByRef textRectangle As Rectangle, ByRef imageRectangle As Rectangle)
      Dim text_size As Size = TextRenderer.MeasureText(Text, Font, content_rect.Size, textFormatFlags)
      'Dim image_size As Size = If(Image Is Nothing, Size.Empty, Image.Size)
      Dim image_size As Size
      If Image Is Nothing Then
        image_size = Size.Empty
      Else
        image_size = Image.Size
      End If

      textRectangle = Rectangle.Empty
      imageRectangle = Rectangle.Empty

      Select Case TextImageRelation
        Case TextImageRelation.Overlay
          ' Overlay is easy, text always goes here
          textRectangle = OverlayObjectRect(content_rect, text_size, TextAlign)
          ' Rectangle.Inflate(content_rect, -4, -4);
          'Offset on Windows 98 style when button is pressed
          If _state = PushButtonState.Pressed AndAlso Not Application.RenderWithVisualStyles Then
            textRectangle.Offset(1, 1)
          End If

          ' Image is dependent on ImageAlign
          If Image IsNot Nothing Then
            imageRectangle = OverlayObjectRect(content_rect, image_size, ImageAlign)
          End If

          Exit Select
        Case TextImageRelation.ImageAboveText
          content_rect.Inflate(-4, -4)
          LayoutTextAboveOrBelowImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
        Case TextImageRelation.TextAboveImage
          content_rect.Inflate(-4, -4)
          LayoutTextAboveOrBelowImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
        Case TextImageRelation.ImageBeforeText
          content_rect.Inflate(-4, -4)
          LayoutTextBeforeOrAfterImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
        Case TextImageRelation.TextBeforeImage
          content_rect.Inflate(-4, -4)
          LayoutTextBeforeOrAfterImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
          Exit Select
      End Select
    End Sub

    Private Shared Function OverlayObjectRect(ByRef container As Rectangle, ByRef sizeOfObject As Size, ByVal alignment As System.Drawing.ContentAlignment) As Rectangle
      Dim x As Integer, y As Integer

      Select Case alignment
        Case System.Drawing.ContentAlignment.TopLeft
          x = 4
          y = 4
          Exit Select
        Case System.Drawing.ContentAlignment.TopCenter
          x = (container.Width - sizeOfObject.Width) \ 2
          y = 4
          Exit Select
        Case System.Drawing.ContentAlignment.TopRight
          x = container.Width - sizeOfObject.Width - 4
          y = 4
          Exit Select
        Case System.Drawing.ContentAlignment.MiddleLeft
          x = 4
          y = (container.Height - sizeOfObject.Height) \ 2
          Exit Select
        Case System.Drawing.ContentAlignment.MiddleCenter
          x = (container.Width - sizeOfObject.Width) \ 2
          y = (container.Height - sizeOfObject.Height) \ 2
          Exit Select
        Case System.Drawing.ContentAlignment.MiddleRight
          x = container.Width - sizeOfObject.Width - 4
          y = (container.Height - sizeOfObject.Height) \ 2
          Exit Select
        Case System.Drawing.ContentAlignment.BottomLeft
          x = 4
          y = container.Height - sizeOfObject.Height - 4
          Exit Select
        Case System.Drawing.ContentAlignment.BottomCenter
          x = (container.Width - sizeOfObject.Width) \ 2
          y = container.Height - sizeOfObject.Height - 4
          Exit Select
        Case System.Drawing.ContentAlignment.BottomRight
          x = container.Width - sizeOfObject.Width - 4
          y = container.Height - sizeOfObject.Height - 4
          Exit Select
        Case Else
          x = 4
          y = 4
          Exit Select
      End Select

      Return New Rectangle(x, y, sizeOfObject.Width, sizeOfObject.Height)
    End Function

    Private Sub LayoutTextBeforeOrAfterImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
      Dim element_spacing As Integer = 0
      ' Spacing between the Text and the Image
      Dim total_width As Integer = textSize.Width + element_spacing + imageSize.Width

      If Not textFirst Then
        element_spacing += 2
      End If

      ' If the text is too big, chop it down to the size we have available to it
      If total_width > totalArea.Width Then
        textSize.Width = totalArea.Width - element_spacing - imageSize.Width
        total_width = totalArea.Width
      End If

      Dim excess_width As Integer = totalArea.Width - total_width
      Dim offset As Integer = 0

      Dim final_text_rect As Rectangle
      Dim final_image_rect As Rectangle

      Dim h_text As HorizontalAlignment = GetHorizontalAlignment(TextAlign)
      Dim h_image As HorizontalAlignment = GetHorizontalAlignment(ImageAlign)

      If h_image = HorizontalAlignment.Left Then
        offset = 0
      ElseIf h_image = HorizontalAlignment.Right AndAlso h_text = HorizontalAlignment.Right Then
        offset = excess_width
      ElseIf h_image = HorizontalAlignment.Center AndAlso (h_text = HorizontalAlignment.Left OrElse h_text = HorizontalAlignment.Center) Then
        offset += excess_width \ 3
      Else
        offset += 2 * (excess_width \ 3)
      End If

      If textFirst Then
        final_text_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
        final_image_rect = New Rectangle(final_text_rect.Right + element_spacing, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
      Else
        final_image_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
        final_text_rect = New Rectangle(final_image_rect.Right + element_spacing, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
      End If

      textRect = final_text_rect
      imageRect = final_image_rect
    End Sub

    Private Sub LayoutTextAboveOrBelowImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
      Dim element_spacing As Integer = 0
      ' Spacing between the Text and the Image
      Dim total_height As Integer = textSize.Height + element_spacing + imageSize.Height

      If textFirst Then
        element_spacing += 2
      End If

      If textSize.Width > totalArea.Width Then
        textSize.Width = totalArea.Width
      End If

      ' If the there isn't enough room and we're text first, cut out the image
      If total_height > totalArea.Height AndAlso textFirst Then
        imageSize = Size.Empty
        total_height = totalArea.Height
      End If

      Dim excess_height As Integer = totalArea.Height - total_height
      Dim offset As Integer = 0

      Dim final_text_rect As Rectangle
      Dim final_image_rect As Rectangle

      Dim v_text As VerticalAlignment = GetVerticalAlignment(TextAlign)
      Dim v_image As VerticalAlignment = GetVerticalAlignment(ImageAlign)

      If v_image = VerticalAlignment.Top Then
        offset = 0
      ElseIf v_image = VerticalAlignment.Bottom AndAlso v_text = VerticalAlignment.Bottom Then
        offset = excess_height
      ElseIf v_image = VerticalAlignment.Center AndAlso (v_text = VerticalAlignment.Top OrElse v_text = VerticalAlignment.Center) Then
        offset += excess_height \ 3
      Else
        offset += 2 * (excess_height \ 3)
      End If

      If textFirst Then
        final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, totalArea.Top + offset, textSize.Width, textSize.Height)
        final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, final_text_rect.Bottom + element_spacing, imageSize.Width, imageSize.Height)
      Else
        final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, totalArea.Top + offset, imageSize.Width, imageSize.Height)
        final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, final_image_rect.Bottom + element_spacing, textSize.Width, textSize.Height)

        If final_text_rect.Bottom > totalArea.Bottom Then
          final_text_rect.Y = totalArea.Top
        End If
      End If

      textRect = final_text_rect
      imageRect = final_image_rect
    End Sub

    Private Shared Function GetHorizontalAlignment(ByVal align As System.Drawing.ContentAlignment) As HorizontalAlignment
      Select Case align
        Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.TopLeft
          Return HorizontalAlignment.Left
        Case System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.TopCenter
          Return HorizontalAlignment.Center
        Case System.Drawing.ContentAlignment.BottomRight, System.Drawing.ContentAlignment.MiddleRight, System.Drawing.ContentAlignment.TopRight
          Return HorizontalAlignment.Right
      End Select

      Return HorizontalAlignment.Left
    End Function

    Private Shared Function GetVerticalAlignment(ByVal align As System.Drawing.ContentAlignment) As VerticalAlignment
      Select Case align
        Case System.Drawing.ContentAlignment.TopLeft, System.Drawing.ContentAlignment.TopCenter, System.Drawing.ContentAlignment.TopRight
          Return VerticalAlignment.Top
        Case System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.MiddleRight
          Return VerticalAlignment.Center
        Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.BottomRight
          Return VerticalAlignment.Bottom
      End Select

      Return VerticalAlignment.Top
    End Function

    Friend Shared Function AlignInRectangle(ByVal outer As Rectangle, ByVal inner As Size, ByVal align As System.Drawing.ContentAlignment) As Rectangle
      Dim x As Integer = 0
      Dim y As Integer = 0

      If align = System.Drawing.ContentAlignment.BottomLeft OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.TopLeft Then
        x = outer.X
      ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.TopCenter Then
        x = Math.Max(outer.X + ((outer.Width - inner.Width) \ 2), outer.Left)
      ElseIf align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.MiddleRight OrElse align = System.Drawing.ContentAlignment.TopRight Then
        x = outer.Right - inner.Width
      End If
      If align = System.Drawing.ContentAlignment.TopCenter OrElse align = System.Drawing.ContentAlignment.TopLeft OrElse align = System.Drawing.ContentAlignment.TopRight Then
        y = outer.Y
      ElseIf align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.MiddleRight Then
        y = outer.Y + (outer.Height - inner.Height) \ 2
      ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.BottomLeft Then
        y = outer.Bottom - inner.Height
      End If

      Return New Rectangle(x, y, Math.Min(inner.Width, outer.Width), Math.Min(inner.Height, outer.Height))
    End Function

#End Region


    Private Sub ShowContextMenuStrip()
      If skipNextOpen Then
        ' we were called because we're closing the context menu strip
        ' when clicking the dropdown button.
        skipNextOpen = False
        Return
      End If

      State = PushButtonState.Pressed

      If m_SplitMenu IsNot Nothing Then
        m_SplitMenu.Show(Me, New Point(0, Height))
      ElseIf m_SplitMenuStrip IsNot Nothing Then
        m_SplitMenuStrip.Show(Me, New Point(0, Height), ToolStripDropDownDirection.BelowRight)
      End If
    End Sub

    Private Sub SplitMenuStrip_Opening(ByVal sender As Object, ByVal e As CancelEventArgs)
      isSplitMenuVisible = True
    End Sub

    Private Sub SplitMenuStrip_Closing(ByVal sender As Object, ByVal e As ToolStripDropDownClosingEventArgs)
      isSplitMenuVisible = False

      SetButtonDrawState()

      If e.CloseReason = ToolStripDropDownCloseReason.AppClicked Then
        skipNextOpen = (dropDownRectangle.Contains(PointToClient(Cursor.Position))) AndAlso MouseButtons = MouseButtons.Left
      End If
    End Sub


    Private Sub SplitMenu_Popup(ByVal sender As Object, ByVal e As EventArgs)
      isSplitMenuVisible = True
    End Sub

    Protected Overrides Sub WndProc(ByRef m As Message)
      '0x0212 == WM_EXITMENULOOP
      If m.Msg = &H212 Then
        'this message is only sent when a ContextMenu is closed (not a ContextMenuStrip)
        isSplitMenuVisible = False
        SetButtonDrawState()
      End If

      MyBase.WndProc(m)
    End Sub

    Private Sub SetButtonDrawState()
      If Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
        State = PushButtonState.Hot
      ElseIf Focused Then
        State = PushButtonState.[Default]
      ElseIf Not Enabled Then
        State = PushButtonState.Disabled
      Else
        State = PushButtonState.Normal
      End If
    End Sub
  End Class
End Namespace

Open in new window

Avatar of Nasir Razzaq
I am not sure if I understand your question. Are you saying that despite setting the text to "Re&ceipt", it does not show the underline?
My apologies for the delay in responding.

Yes, the event for the "hot key" works properly when I press it.

However, I am having trouble displaying the underlined letter when the [Alt] key is held down and then obviously I need it to disappear when the [Alt] key is released.

Am I missing a reference for MyBase
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Windows.Forms.VisualStyles

Namespace wyDay.Controls
  Public Class SplitButton
    Inherits Button

    ...

    Private m_isMnemonic As Boolean

#Region "Properties"

    <DefaultValue(False)> _
    Public Overloads Property UseMnemonic As Boolean
      Get
        Return m_isMnemonic
      End Get
      Set(ByVal value As Boolean)
        If (Me.UseMnemonic <> value) Then
          m_isMnemonic = value

          If MyBase.IsHandleCreated Then
            Dim windowStyle As Integer ' = MyBase.WindowStyle ' Error
            If Not Me.UseMnemonic Then
              windowStyle = (windowStyle Or &H80)
            Else
              windowStyle = (windowStyle And -129)
            End If
           'MyBase.WindowStyle = windowStyle ' Error
          End If
        End If
      End Set
    End Property

    ...

Open in new window

ASKER CERTIFIED SOLUTION
Avatar of Nasir Razzaq
Nasir Razzaq
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
Create a free account to see this answer
Signing up is free and takes 30 seconds. No credit card required.
See answer
SOLUTION
Link to home
membership
Create a free account to see this answer
Signing up is free and takes 30 seconds. No credit card required.
SOLUTION
Link to home
membership
Create a free account to see this answer
Signing up is free and takes 30 seconds. No credit card required.
My solution has the corrected code and reasons for the original issue.