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:
MenuItem
- Basic menu element: text you can click on to trigger desired action; it can have the following optional elements: image, shortcut and checkbox. Separator
- Simple, usually grey line to separate distinct groups of menu items. ComboBox
- It allows to pick item from drop-down combo box. 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 checkmark 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
- It inherits all functionality of ToolStripMenuItem
and additionally provides the following functional enhancements:
- Ability to display
RadioButton
image in place of CheckBox
. - Abitilty to group items into
RadiButtonGroup
to ensure that only one item within the group can be selected.
ToolStripenhancedSeparator
- It acts as MenuItemSeparator
with additional ability to display static
text.
ToolStripMarqueeMenuItem
- This component inherits from ToolStripEnhancedMenuItem
. Additionally to parent class functionality it provides options for text scrolling.
ToolStripEnhancedMenuItem - Menu item with the RadioButton as the checkmark
ToolStripEnhancedMenuItem
defines the following additional properties:
public 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.
public 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:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if ( (CheckMarkDisplayStyle == CheckMarkDisplayStyle.RadioButton))
{
Size radioButtonSize = RadioButtonRenderer.GetGlyphSize
(e.Graphics, RadioButtonState.CheckedNormal);
int radioButtonX = ContentRectangle.X+3;
int radioButtonY = ContentRectangle.Y +
(ContentRectangle.Height - radioButtonSize.Height) / 2;
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;
}
RadioButtonRenderer.DrawRadioButton
(e.Graphics, new Point(radioButtonX, radioButtonY), state);
}
}
The following override reinforces the rule that only one item within the same radio button group can be checked out:
protected override void OnClick(EventArgs e)
{
if ((CheckMarkDisplayStyle == WinForms.CheckMarkDisplayStyle.RadioButton) && (CheckOnClick))
{
ToolStrip toolStrip = this.GetCurrentParent();
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;
}
}
}
base.OnClick(e);
}
ToolStripEnhancedSeparator - Separator with Text property
ToolStripEnhancedSeparator
defines the following additional property:
public 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 behaviour (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);
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:
public MarqueeScrollDirection MarqueeScrollDirection
ToolStripMarqueeMenuItem
allows scrolling text Right-To-Left or Left-To-Right. Default value is MarqueeScrollDirection.RightToLeft
.
public enum MarqueeScrollDirection
{
RightToLeft,
LeftToRight
}
public 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.
public 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.
This is another means to control scrolling speed. It defines how many pixels text should be shifted every time new position is recalculated.
public 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.
void m_Timer_Tick(object sender, EventArgs e)
{
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:
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
base.OnPaint(e);
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.
History
- November 30, 2013 - Initial article release