Introduction

In my professional life, I have to do a lot of GUI development and MenuStrip, especially ContextMenuStrip is one of WinForms controls I use probably most often. As designed by Microsoft, number of menu items you can chose from is short. You can have:

  1. MenuItem -

    Basic menu element: text you can click on to trigger desired action; it can have the following optional elements: image, shortcut and checkbox.

  2. Separator -

    Simple, usually gray line to separate distinct groups of menu items.

  3. ComboBox -

    It allows to pick item from drop-down combo box.

  4. TextBox -

    It allows to enter any text.

I found the above list rather limiting. Very often, I needed to add into MenuStrip controls not present on the list.

For example, I wanted to display radio button instead of check box, have descriptive separators and scrollable (marquee) menu items (see animated image below).

Background

Fortunately, there are ways to enrich set of objects that can be used as items in the ToolStrip.

One way is to use ToolStripControlHost object to wrap any WinForms control as ToolStrip insertable control. There are plenty of Internet articles and examples showing how to do it. I used this method few times and found it temperamental and buggy, but I was able to achieve my goals.

This article is presenting another approach: Components inherited from ToolStripMenuItem class. My reasoning for this solution was the following: base ToolStripMenuItem class contains most functionality I needed. Only real difference is how enhanced ToolStripMenuItem should present itself in GUI (extra text for separator, radio button as check mark or scrolling text). That means that the only thing I needed to change was how enhanced ToolStripMenuItem paints itself, what didn't look like a lot of work.

Code Anatomy

EnhancedContextMenu library consists of three components:

ToolStripEnhancedMenuItem - Menu item with the RadioButton as the check mark

ToolStripEnhancedMenuItem defines the following additional properties:

Property

Description

string RadioButtonGroupName

This property provides means to show several menu items as menu items group (in similar way as GroupBox groups RadioButton controls). All menu items with identical RadioButtonGroupName belong to the same group. Default RadioButtonGroupName is an empty string. If you want to have multiple groups in your menu, you should set this property accordingly.

CheckMarkDisplayStyle CheckMarkDisplayStyle

Default value is CheckMarkDisplayStyle.RadioButton. If set to CheckMarkDisplayStyle.Checkbox, TollStripEnhancedMenuItem behaves identically as "regular" ToolStripMenuItem. Otherwise item check mark is replaced with RadioButton image.

CheckMarkDisplayStyle is defined as follows:

public enum CheckMarkDisplayStyle
{
   CheckBox=0,
   RadioButton=1
} 

Below are presented essential pieces of code responsible for enhanced functionality. For details, please refer to the attached zip file with the source code.

The following override code ensures display of RadioButton image:

/// <summary>
/// if CheckMarkDisplayStyle is equal RadioButton OnPaint override paints radio button images. 
/// </summary>
/// <param name="e">Standard event arguments for OnPaint method.</param>
protected override void OnPaint(PaintEventArgs e)
{
    //base.OnPaint will render menu item.
    base.OnPaint(e);
 
    //if CheckMarkDisplayStyle is equal RadioButton additional paining or radioButton is needed
    if ( (CheckMarkDisplayStyle == CheckMarkDisplayStyle.RadioButton))
    {
        //Find location of radio button
        Size radioButtonSize = RadioButtonRenderer.GetGlyphSize
        		(e.Graphics, RadioButtonState.CheckedNormal);
        int radioButtonX = ContentRectangle.X+3; 
        int radioButtonY = ContentRectangle.Y + 
        	(ContentRectangle.Height - radioButtonSize.Height) / 2;
 
        //Find state of radio button
        RadioButtonState state = RadioButtonState.CheckedNormal;
        if (this.Checked)
        {
            if (Pressed)
                state = RadioButtonState.CheckedPressed;
            else if (Selected)
                state = RadioButtonState.CheckedHot;
        }
        else
        {
            if (Pressed)
               state = RadioButtonState.UncheckedPressed;
            else if (Selected)
                state = RadioButtonState.UncheckedHot;
            else
                state = RadioButtonState.UncheckedNormal;
        }
                
        //Draw RadioButton in proper state (Checked/Unchecked; Hot/Normal/Pressed)
        RadioButtonRenderer.DrawRadioButton
        	(e.Graphics, new Point(radioButtonX, radioButtonY), state);
   }
}

The following override of OnClick method reinforces the rule that only one item within the same radio button group can be checked out:

/// <summary>
/// If menu item belongs to the radio group, this override ensures proper functionality 
/// (select clicked item and de-select all others from the same group).
/// </summary>
/// <param name="e"></param>
protected override void OnClick(EventArgs e)
{
   if ((CheckMarkDisplayStyle == WinForms.CheckMarkDisplayStyle.RadioButton) && (CheckOnClick))
   {
      //Un-click all radio buttons different than the clicked one
      ToolStrip toolStrip = this.GetCurrentParent();
                 
      //Iterate all siblings of clicked item and make them unchecked
      foreach (ToolStripItem toolStripItem in toolStrip.Items)
      {
         if (toolStripItem is ToolStripEnhancedMenuItem)
         {
             ToolStripEnhancedMenuItem toolStripEnhancedItem = (ToolStripEnhancedMenuItem)toolStripItem;
             if ((toolStripEnhancedItem.CheckMarkDisplayStyle == 
             	WinForms.CheckMarkDisplayStyle.RadioButton) && 
             			(toolStripEnhancedItem.CheckOnClick) &&
                (toolStripEnhancedItem.RadioButtonGroupName == RadioButtonGroupName))
                            toolStripEnhancedItem.Checked = false;
         }
      }
   }
   //If CheckOnClick is 'true', base.OnClick will make clicked item selected.
   base.OnClick(e);
}

ToolStripEnhancedSeparator - Separator with Text property

ToolStripEnhancedSeparator defines the following additional property:

Property

Description

bool ShowSeparatorLine

If set to true, ToolStripEnhancedSeparator displays separator line in areas not occupied by the separator text. Otherwise only Text is displayed.

All enhanced behavior (drawing separator text and drawing separator line) is coded in OnPaint override (For full details, please refer to the attached zip file).

protected override void OnPaint(PaintEventArgs e)
{
    ToolStrip ts = this.Owner ?? this.GetCurrentParent();
    int textLeft = ts.Padding.Horizontal;
 
    if (ts.BackColor != this.BackColor)
    {
        using (SolidBrush sb = new SolidBrush(BackColor))
        {
            e.Graphics.FillRectangle(sb, e.ClipRectangle);
        }
    }
  
    Size textSize = TextRenderer.MeasureText(Text, Font);
 
    //Find horizontal text position offset
    switch (TextAlign)
    {
        case ContentAlignment.BottomCenter:
        case ContentAlignment.MiddleCenter:
        case ContentAlignment.TopCenter:
            textLeft = (ContentRectangle.Width + textLeft - textSize.Width) / 2;
            break;
        case ContentAlignment.BottomRight:
        case ContentAlignment.MiddleRight:
        case ContentAlignment.TopRight:
            textLeft = ContentRectangle.Right - textSize.Width;
            break;
 
    }
 
    int yLinePosition = (ContentRectangle.Bottom - ContentRectangle.Top) / 2;
    int yTextPosition = (ContentRectangle.Bottom - 
    			textSize.Height - ContentRectangle.Top) / 2;
 
    switch (TextAlign)
    {
        case ContentAlignment.BottomCenter:
        case ContentAlignment.BottomLeft:
        case ContentAlignment.BottomRight:
            yLinePosition = yTextPosition;
            break;
        case ContentAlignment.TopCenter:
        case ContentAlignment.TopLeft:
        case ContentAlignment.TopRight:
            yLinePosition = yTextPosition + textSize.Height;
            break;
    }
 
    using (Pen pen = new Pen(ForeColor))
    {
        if (ShowSeparatorLine)
            e.Graphics.DrawLine(pen, ts.Padding.Horizontal, 
            		yLinePosition, textLeft, yLinePosition);
 
        TextRenderer.DrawText(e.Graphics, Text, Font, 
        	new Point(textLeft, yTextPosition), ForeColor);
 
        if (ShowSeparatorLine)
            e.Graphics.DrawLine(pen, textLeft + textSize.Width, 
            	yLinePosition, ContentRectangle.Right, yLinePosition);
    }
}

ToolStripMarqueeMenuItem - ToolStripItem with scrolling Text

ToolstripMarqueeMenuItem has the following properties defined:

Property

Description

MarqueeScrollDirection MarqueeScrollDirection

ToolStripMarqueeMenuItem allows scrolling text Right-To-Left or Left-To-Right. Default value is MarqueeScrollDirection.RightToLeft.

public enum MarqueeScrollDirection
{
   RightToLeft,
   LeftToRight
} 

int MinimumTextWidth

By default, width on menu item is determined by the width of the Text property. This can be a problem when scrollable text is very long. Using this property, you can limit minimum width of menu item to value smaller than dictated by size of Text property.

int RefreshInterval

This is one of two properties used to control speed text is scrolled with. It defines in milliseconds how often new scrolling position is recalculated and how often text is refreshed.

int ScrollStep

This is another method to control scrolling speed. It defines how many pixels text should be shifted every time new position is recalculated.

bool StopScrollOnMouseOver

If set to true, when mouse is hovering over menu item, scrolling stops.

Essential piece of code in this component is Timer event handler. In predefined intervals (defined in RefreshInterval property), it recalculates new text horizontal position of scrolling text. After calculation is done, it invalidates itself in order to allow test displaying in newly calculated position. Pease note, that ScrollStep property is also used in the below formula.

 
/// <summary>
/// Recalculate new text position and calls Invalidate to repaint.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void m_Timer_Tick(object sender, EventArgs e)
{
    //Change offset only when menu item is visible, 
    //mouse is not hovering over or StopScrollOnMouseOver is not set to 'false'
    if ((Visible) && ((!Selected) ||(!StopScrollOnMouseOver)))
    {
            m_PixelOffest = (m_PixelOffest + ScrollStep + 
            	m_TextSize.Width) % (2 * m_TextSize.Width + 1) - m_TextSize.Width;
            Invalidate();
    }
}

And the final chunk of code used to display positioned text:

 
/// <summary>
/// Method responsible for painting text every time new text offset is recalculated.
/// </summary>
/// <param name="e"></param>
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
    base.OnPaint(e);  //Paint text (blank) and check box/radio button (if required)

    //Paint scrolling text
    ToolStrip parent = GetCurrentParent();
    Rectangle displayRect = parent.DisplayRectangle;
    int horizPadding = parent.Padding.Horizontal;
 
    Rectangle clipRectangle = new Rectangle(displayRect.X , 
    displayRect.Y, displayRect.Width - horizPadding, displayRect.Height);
 
        e.Graphics.FillRectangle(Brushes.Transparent, e.ClipRectangle);
 
    int textYPosition = (this.Size.Height - m_TextSize.Height) / 2;
 
    Region savedClip = e.Graphics.Clip;
    Region clipRegion =new Region(clipRectangle);
    e.Graphics.Clip = clipRegion;
    if (MarqueeScrollDirection== WinForms.MarqueeScrollDirection.RightToLeft)
        e.Graphics.DrawString(m_Text, Font, 
        Brushes.Black, -m_PixelOffest + horizPadding, textYPosition);
    else
        e.Graphics.DrawString(m_Text, Font, 
        Brushes.Black, + m_PixelOffest + horizPadding, textYPosition);
 
    clipRegion.Dispose();
    e.Graphics.Clip = savedClip; 
}

Using the Code

In your program, add reference to EnhancedContxtMenu.dll. And this is all. Now, whenever in development mode, you need to add item to the context menu, your selection will show three more items as shown in the screenshot below:

Demo Program

Full source code and demo program of described library can be found in the zip file attached to this article.


Home  bullet Products  bullet Downloads  bullet About Us  bullet Contact Us

Credit Card Logo