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 gray 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 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
-It inherits all functionality of
ToolStripMenuItem
and additionally provides the following functional enhancements:Ability to display
RadioButton
image in place ofCheckBox
.Ability 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 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 |
CheckMarkDisplayStyle CheckMarkDisplayStyle |
Default value is
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 |
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 |
public enum MarqueeScrollDirection
{
RightToLeft,
LeftToRight
}
|
int MinimumTextWidth |
By default, width on menu item is determined by the width of the |
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 |
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.