Search code examples
vb.netwinformsmenustrip

Positioning of shortcut key of ToolStripMenu item properly


In WinForms, i have listed toolstripmenu items as below:

ToolStripMenuItem of my program

We can see that the list of shortcut key is not properly indented.

I have searched for the solution and found to use spaces but I have tried that and it didn't work properly.

So, is it possible to position the shortcut keys as below figure at a certain position for all menu items?

ToolStripMenuItem of Visual Studio 2013


Solution

  • I searched and read a bit about that topic but cound'nt find a working example. So I thought I try to create one. It's not perfect but a base to get started. I didnt try items which have additional subitems for example.

    To render menuitems you do not have to override the Items' OnPaint Method. We better use the ToolStripProfessionalRenderer. The renderer will manage everything required to show the menuitems. Therefore we have to create our own class MyToolStripProfessionalRenderer and set the Toolstrip.Renderer Property.

    Public Class Form1
    
      Public Sub New()
    
        InitializeComponent()
        MenuStrip1.Renderer = New MyToolStripProfessionalRenderer()
    
      End Sub
    End Class
    

    Within our class we have to override the OnRenderItemText Method. This method draws the strings for item name and shortcut. The base method is simple and draws the Name with left align and Shortcut with right align. Our custom method should draw the name with left align and the shortcut with left align. therefore we need to find out the proper place to draw the shortcut. I created a loop checking all items shortcut texts to find the item with the highest width. Create a rectangle from it and draw the string in this rectangle.

    Note: When using this example, you have to set the ShortcutKeyDisplayString Property in the designer manually because somehow its always null otherwise.

    /major edit:

    We also have to change the autosize algorithm to set the with of each dropdownmenu.

    New Autowidth: Imagewidth + some space + largest Itemtext + some space + largest ShortCutText + some space

    enter image description here

    Therefore I override the Initialize(toolStrip As System.Windows.Forms.ToolStrip) Method. First I added a few constants to set the spaces. To calculate the width I fetch through all items and find the largest text of the subitems and then set the new width for the items.

    Note: If your dropdownmenu has another dropdownmenu then you have to add recursion.

        Imports System.Windows.Forms
    
    
    Public Class MyToolStripProfessionalRenderer
      Inherits ToolStripProfessionalRenderer
    
    
    
      Protected iconwidth As Integer = 22 ' the width of image icons
      Protected paddingIconToText As Integer = 3
      Protected paddingTextToShortCut As Integer = 20
      Protected paddingShortCutToBoarder As Integer = 20
    
    
    
      Private Enum TextType
        Text = 0
        Shortcut = 1
      End Enum
    
    
      Protected Overrides Sub OnRenderItemText(e As System.Windows.Forms.ToolStripItemTextRenderEventArgs)
    
        ' render only ToolStripMenuItems
        If e.Item.IsOnDropDown And TypeOf e.Item Is ToolStripMenuItem Then
    
          Dim MenuItem As ToolStripMenuItem = e.Item
          Dim Name As String = MenuItem.Text
          Dim Shortcut As String = MenuItem.ShortcutKeyDisplayString
    
    
          'avoid double draw. The method is called twice for each item, check what should be drawn, Text or Shortcut? 
          Dim Mode As TextType
          If e.Text = Name Then
            Mode = TextType.Text
          Else
            Mode = TextType.Shortcut
          End If
    
    
          If Mode = TextType.Text Then
    
            ' this is our column for the menuitem text
            Dim FirstColumn As Rectangle = New Rectangle(MenuItem.ContentRectangle.Left + iconwidth + paddingIconToText,
                                    MenuItem.ContentRectangle.Top + 1,
                                    MenuItem.Width - iconwidth - paddingIconToText,
                                    MenuItem.Height)
            ' drawing the menu item
            e.Graphics.DrawString(Name, MenuItem.Font, New SolidBrush(MenuItem.ForeColor), FirstColumn)
            ' this is the Shortcut to display, be sure to have set it manually
    
          Else
    
            ' to align the text on the wanted position, we need to know the width for the shortcuts, this depends also on the other menu items
            Dim CurStrip As ToolStrip = MenuItem.GetCurrentParent()
            Dim fShortCutWidth As Single = 0
            ' lets find the other menuitems for this group
            For Each item As Object In CurStrip.Items
              ' lets look for the ToolStripMenuItem only
              If TypeOf item Is ToolStripMenuItem Then
                Dim ChildItem As ToolStripMenuItem = item
                Dim sCurShortcut As String = ChildItem.ShortcutKeyDisplayString
                ' how many pixels are needed to draw the current shortcut?
                Dim size As SizeF = e.Graphics.MeasureString(sCurShortcut, ChildItem.Font)
                If size.Width > fShortCutWidth Then
                  fShortCutWidth = size.Width ' save it for later
                End If
              End If
            Next
    
            ' avoid to lose 1 pixel by casting to integer
            Dim ShortCutWidth As Integer = Convert.ToInt32(fShortCutWidth) + 1
    
            If fShortCutWidth > 0 Then
              ' this is our second column for the shortcut text
              Dim SecondColumn As Rectangle = New Rectangle(MenuItem.Width - ShortCutWidth - paddingShortCutToBoarder,
                                   MenuItem.ContentRectangle.Top + 1,
                                   ShortCutWidth,
                                   MenuItem.Height)
              ' drawing the shortcut
              e.Graphics.DrawString(Shortcut, MenuItem.Font, New SolidBrush(MenuItem.ForeColor), SecondColumn)
            End If
    
          End If
        Else ' there might be other items, use the default method
    
    
          MyBase.OnRenderItemText(e)
        End If
    
    
      End Sub
    
    
    
      Protected Overrides Sub Initialize(toolStrip As System.Windows.Forms.ToolStrip)
        MyBase.Initialize(toolStrip)
    
    
        ' custom autosize algorithm
        ' 1: Find all dropdownbuttons  
        ' 2: Get all Menuitems within dropdown
        ' 3: find the largest string of the dropdownitems text
        ' 4: find the latgest string of the dropdownitems shortcuttext
        ' 5: set the width for all items = picture width + padding + longest_itemtext + padding + longest_shortcuttext + padding
    
        For Each item As ToolStripItem In toolStrip.Items  ' get all dropdownbuttons
          If TypeOf item Is ToolStripDropDownButton Then
            Dim btn As ToolStripDropDownButton = item
            If btn.HasDropDownItems Then ' dropdownitems
              Dim MaxSizeOfItemName As Single = 0
              Dim MaxSizeOfShortCutName As Single = 0
              Dim CurSizeOfItemName As Single = 0
              Dim CurSizeOfShortCutName As Single = 0
    
              For Each child As ToolStripItem In btn.DropDownItems ' menu items within dropdown menu
                ' find the largest strings of dropdownitems
                If TypeOf child Is ToolStripMenuItem Then
                  Dim CurrentMenuItem As ToolStripMenuItem = child
                  CurSizeOfItemName = TextRenderer.MeasureText(CurrentMenuItem.Text, child.Font).Width
                  CurSizeOfShortCutName = TextRenderer.MeasureText(CurrentMenuItem.ShortcutKeyDisplayString, child.Font).Width
                  MaxSizeOfItemName = Math.Max(MaxSizeOfItemName, CurSizeOfItemName)
                  MaxSizeOfShortCutName = Math.Max(MaxSizeOfShortCutName, CurSizeOfShortCutName)
                End If
              Next
              If MaxSizeOfItemName > 0 Then
                Dim autowidth As Integer = iconwidth + paddingIconToText + Convert.ToInt32(MaxSizeOfItemName) + 1 + paddingTextToShortCut + Convert.ToInt32(MaxSizeOfShortCutName) + 1 + paddingShortCutToBoarder
                ' it's not enough to set only the dropdownitems' width, also have to change the ToolStripDropDownMenu width
                Dim menu As ToolStripDropDownMenu = btn.DropDownItems.Item(0).GetCurrentParent() ' maybe there is a better way to get the menuobject?!
                menu.AutoSize = False
                menu.Width = autowidth
                For Each child As ToolStripItem In btn.DropDownItems
                  child.AutoSize = False
                  child.Width = autowidth
                Next
              End If ' MaxSizeOfItemName
    
              ' CAUTION: this works only for the first level of menuitems, if your dropdownmenu has another dropdownmenu, move the code above into a method and add recursion for each dropdownbutton with subitems
            End If ' btn.HasDropDownItems
          End If ' TypeOf item Is ToolStripDropDownButton
        Next 'For Each item As ToolStripItem
    
    
      End Sub
    End Class