4

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

Community
  • 1
  • 1
Topman
  • 306
  • 1
  • 4
  • 14
  • The way to go should be: Setting the `ShortcutKeys` property, this way the `ShortcutKeyDisplayString` is set automatically. Setting `ShowShortcutKeys` to true should show the shortcut next to the items properly. – Koryu May 22 '15 at 08:07
  • I just have tried it for you. The items have right align then. Looks like you have to extend the class and overwrite the painting. :/ – Koryu May 22 '15 at 08:25
  • @Koryu, sorry, i don't know about overwriting the painting. Can you provide some further help? – Topman May 22 '15 at 10:45
  • Sorry it's not that easy. If i were you, i would go for the `ShowShortcutKeys` Property and live with the right align, it's way too much work for such a little benefit. Or if you want a really fancy menu, check out some Ribbon Toolbars. If you want to use the `toolstripmenuitem` maybe anybody else can help you with [overriding the OnPaint Method](http://msdn.microsoft.com/en-us/library/cksxshce.aspx). – Koryu May 22 '15 at 12:09
  • There's a good reason it works this way, do be careful. You can't compare with the VS menus, Microsoft spends a lot of money on localizing their apps. Hard to match, you are not going to enjoy discovering that the string gets clipped on, say, the notoriously verbose German. – Hans Passant May 22 '15 at 14:14

2 Answers2

1

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
Koryu
  • 1,371
  • 1
  • 11
  • 21
  • Thank you for your great effort. I will try it n get back to you. – Topman May 23 '15 at 02:38
  • Hello @Koryu, I have tried using MenuStrip but i got a little problem. Problem: the design time (text+shortcutdisplaystring) and run time (text+shorcutdisplaystring) is both visible in RunTime. Both design and runtime text are overlapping. If solved, it will work awesome. Can you edit and help? Thanks. – Topman May 25 '15 at 05:24
  • @KnockKnock could you post the designer generated code for your menu? So i can copy your menu into my project. – Koryu May 25 '15 at 10:17
  • Aded an custom autosize algorithm to avoid overlapping. Removed the C# code, anybody please fix the syntax highlighting :) – Koryu May 25 '15 at 12:38
  • You are very helpful and thank you very much. It works. Can't we be in touch somehow 'cause you seem helpful and i need a mentor too :). – Topman May 26 '15 at 11:49
  • you're welcome :) it's just the entry point to get started. Why did you remove the method for autosizing `Initialize(toolStrip As System.Windows.Forms.ToolStrip)` in the answer? Without the changes in autosizing this wont work anymore, i've put it back in the answer. – Koryu May 26 '15 at 14:18
  • :Because, I was not able to see any change on the menu item. I thought, the above code was enough for me :) and it is working. But only little problem right now is: The color of MenuItem with shortcut is darker and MenuItem without shorcut is lighter. Plus the font is not same as in design. – Topman May 26 '15 at 14:31
  • Yes, seems like the `OnRenderItemText` Method is called twice for each item, first time for the name, second time for the shortcut. The lighter item prolly has `ShowShortCutKeys = false` thats why it gets drawn one time and the other items two times so the other ones look darker. To fix that, check if `e.Text == Menuitem.Text or e.Text == MenuItem.ShortcutKeyDisplayString` and only draw them one time. – Koryu May 26 '15 at 15:05
  • I am not a pro in this way of coding and i don't know know much what's going :D . But it has helped me perfectly :) .Thank you very much. – Topman May 27 '15 at 03:22
1

Here's another solution in C#

private class MenuRenderer : ToolStripProfessionalRenderer {

    Hashtable ht = new Hashtable();
    int shortcutTextMargin = 5;
    Font cachedFont = null;

    protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) {
        ToolStrip ts = e.Item.Owner;
        if (ts.Font != cachedFont) {
            cachedFont = ts.Font; // assumes all menu items use the same font
            ht.Clear();
        }

        var mi = e.Item as ToolStripMenuItem;

        if (mi != null && mi.ShortcutKeys != (Keys) 0) {
            if (e.Text != mi.Text) { // shortcut text
                ToolStripMenuItem owner = (ToolStripMenuItem) e.Item.OwnerItem;

                e.TextFormat = TextFormatFlags.VerticalCenter;
                Size sz = TextRenderer.MeasureText(e.Graphics, e.Text, e.TextFont);

                int w = owner.DropDown.Width;
                int x = w - (sz.Width + shortcutTextMargin);
                int? xShortcut = (int?) ht[owner];
                if (!xShortcut.HasValue || x < xShortcut.Value) {
                    xShortcut = x;
                    ht[owner] = xShortcut;
                    owner.DropDown.Invalidate();
                }

                Rectangle r = e.TextRectangle;
                r.X = xShortcut.Value;
                e.TextRectangle = r;
            }
        }

        base.OnRenderItemText(e);
    }
}
Loathing
  • 5,109
  • 3
  • 24
  • 35