The C# class MenuOwnerDraw
transforms at runtime menus to owner drawn menus, with the possibility to
display images, which symbolize the menu items functions. MenuOwnerDraw is a
class, not a component, so no additional DLL has to be deployed. Look at a tiny
example and the source code:
|
using System;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Runtime.InteropServices;
namespace SI.Controls
{
public class MenuOwnerDraw
{
#region Description
//
//####################################################################################
//
// class MenuOwnerDraw {
// Author Jan Schröder
//
Schröder Informatik
//
www.SchroederInformatik.de
// Version 1.0.2
// Date 05/05/23
//
// This class transforms menus to owner drawn menus,
with the possibility to display
// images, which symbolize the menu items functions.
//
// Use this class as follows:
// 1. Add this Visual Basic source to your project.
// 2. Create the form or context menu as usually. It's
important to leave the
// properties "OwnerDraw" as
"false".
// 3. Add an image list to the form or use one, which
is already linked to the
// toolbox.
// 4. Append the indexes of the images, which should be
displayed, left to the text
// of the appropriated menu
items. for ( example: if ( you want to display the
// image with index 0 left to
the menu item with the text "Save", append "_i_0"
// to the text. The text has to
be "Save_i_0".
// 5. Create instances of this class in the forms load
event. for ( example:
// MenuOwnerDraw1 = new
MenuOwnerDraw(this, ImageList1);
// MenuOwnerDraw2 = new
MenuOwnerDraw(ContextMenu1, ImageList2);
//
// That's all.
//
// MenuOwnerDraw has only one public member: the new
constructor with two
// alternatives, one for the form menu and the other
for context menus.
//
// The private methodes "MeasureItem" and "DrawItem"
are doing the work. Both are
// callback functions, used to handle the events
"MeasureItem" and "DrawItem".
//
//
// Versions:
// 1.0.0 04/11/26 First Version
// 1.0.1 05/01/29 DrawMenuCheck;
new design (more XP-stylish);
//
better support of system colors with very much contrast;
//
new outline of the source code
// 1.0.2 05/05/23 Translation
from VB into C#
//
//####################################################################################
//
#endregion
#region Constructors
public MenuOwnerDraw(Form FormX, ImageList
imgMenuImages)
{
InitializeFormMenu(ref FormX, ref
imgMenuImages);
}
public MenuOwnerDraw(ContextMenu ContextMenu, ImageList
imgMenuImages)
{
InitializeContextMenu(ref ContextMenu, ref
imgMenuImages);
}
#endregion
#region Data definition
private ImageList imgMenuImagesForModule;
private int nXimgLeft, nXimgRectRight,
nXtextLeft; // metrics
[ StructLayout( LayoutKind.Sequential )]
private class StringSize
{
public int cx, cy;
public StringSize(){cx = 0; cy = 0;}
}
#endregion
#region Event handling
private void DrawSubMenuItem(object sender,
DrawItemEventArgs e)
{
//
// A sub menu item can be a seperator or
text. So, a seperator can be detected
// as an item, which text is empty.
//
MenuItem customItem = (MenuItem) sender;
string sText = new string(' ',256);
string sShortcut = new string(' ',256);
int nImageIndex = 0;
GetMenuTextImageShortcut(customItem.Text,
ref sText, ref nImageIndex,
ref sShortcut);
if ( sText == "" )
{
DrawMenuSep(e);
}
else
{
DrawMenuTextImageShortcut(e,
ref sText, ref nImageIndex, ref sShortcut);
}
//
}
private void MeasureItem(object sender,
MeasureItemEventArgs e)
{
//
// for ( each menu item, the height an the
width of the drawing area have to be
// evaluated.
//
MenuItem customItem = (MenuItem) sender;
int nImageIndex = 0;
Graphics grfx =
Graphics.FromImage(imgMenuImagesForModule.Images[0]);
string sText = new string(' ',256);
string sShortcut = new string(' ',256);
GetMenuTextImageShortcut(customItem.Text,
ref sText, ref nImageIndex,
ref sShortcut);
SizeF stringSize = grfx.MeasureString(sText
+ sShortcut,
SystemInformation.MenuFont);
//
// The width is determined by the width of
the image area, given by nXtextLeft
// (look at sub SetMetric above) and the
width of the menu item string, which
// is a combination of text and shortcut.
//
e.ItemWidth = (int)stringSize.Width +
nXtextLeft + 7;
//
// The height is determined by the height
of the image or the height of the
// menu item string. for ( menu separators,
the string is empty.
//
e.ItemHeight = Max((int)stringSize.Height +
7,
imgMenuImagesForModule.Images[0].Height);
if ( sText == "" ) {e.ItemHeight =
(int)(e.ItemHeight / 2);}
//
}
#endregion
#region API stuff
//
private const int MF_BYPOSITION = 0x400;
//
private const int DST_ICON = 0x3;
private const int DST_BITMAP = 0x4;
private const int DSS_NORMAL = 0x0;
private const int DSS_DISABLED = 0x20;
//
private const int COLOR_SCROLLBAR = 0;
private const int COLOR_BACKGROUND = 1;
private const int COLOR_ACTIVECAPTION = 2;
private const int COLOR_INACTIVECAPTION = 3;
private const int COLOR_MENU = 4;
private const int COLOR_WINDOW = 5;
private const int COLOR_WINDOWFRAME = 6;
private const int COLOR_MENUTEXT = 7;
private const int COLOR_WINDOWTEXT = 8;
private const int COLOR_CAPTIONTEXT = 9;
private const int COLOR_ACTIVEBORDER = 10;
private const int COLOR_INACTIVEBORDER = 11;
private const int COLOR_APPWORKSPACE = 12;
private const int COLOR_HIGHLIGHT = 13;
private const int COLOR_HIGHLIGHTTEXT = 14;
private const int COLOR_BTNFACE = 15;
private const int COLOR_BTNSHADOW = 16;
private const int COLOR_GRAYTEXT = 17;
private const int COLOR_BTNTEXT = 18;
private const int COLOR_INACTIVECAPTIONTEXT = 19;
private const int COLOR_BTNHIGHLIGHT = 20;
//
private const int TRANSPARENT = 1;
//
[DllImport("user32")] private static extern int
DestroyIcon(int hIcon);
[DllImport("user32", CharSet=CharSet.Auto)] private
static extern int
DrawState(int hdc, int hBrush, int
lpDrawStateProc, int lParam, int wParam,
int n1, int n2, int n3, int n4, int un);
[DllImport("user32", CharSet=CharSet.Auto)] private
static extern int
GetMenuString(int hMenu, int wIDItem,
StringBuilder lpString,
int nMaxCount, int wFlag);
[DllImport("gdi32", CharSet=CharSet.Auto)] private
static extern int
GetTextExtentPoint32(int hDC, string lpsz,
int cbString,
[ In, Out ] StringSize lpSize);
[DllImport("user32")] private static extern int
GetSysColor(int nIndex);
[DllImport("COMCTL32")] private static extern int
ImageList_GetIcon(int HIMAGELIST,
int ImgIndex, int hbmMask);
[DllImport("gdi32")] private static extern int
SetBkMode(int hDC, int nBkMode);
[DllImport("gdi32")] private static extern int
SetTextColor(int hDC, int crColor);
[DllImport("gdi32", CharSet=CharSet.Auto)] private
static extern int
TextOut(int hDC, int x, int y, string
lpString, int nCount);
//
#endregion
#region Subroutines and functions
private int Max(int nA, int nB)
{
//
// Gives back the maximum value of both.
//
if ( nA > = nB ) {return nA;}
else {return nB;}
//
}
private void DrawMenuCheck(DrawItemEventArgs e)
{
//
// Draws a check mark, for checked menu
items. The check mark is build of some
// lines. The menu item could be disabled.
In this case, the check mark has to
// be drawn with a grayed pen.
//
int nOffsetX, nOffsetY, nQuarter1X, nHalfX,
nQuarter3X, nQuarter1Y,
nHalfY, nQuarter3Y;
Pen penCheck, penRect;
Color ColorRect = Color.FromArgb(28, 81,
128);
Color ColorCheck = Color.FromArgb(33, 161,
33);
int nWidth = nXimgRectRight - 9;
if ( (e.State & DrawItemState.Grayed)
== DrawItemState.Grayed )
{
penCheck = SystemPens.GrayText;
penRect = SystemPens.GrayText;
}
else
{
penCheck = new Pen(ColorCheck);
penRect = new Pen(ColorRect);
}
nOffsetX = e.Bounds.X + 4;
nOffsetY = e.Bounds.Y + (e.Bounds.Height -
nWidth) / 2 + 1;
nQuarter1X = nOffsetX + nWidth / 4;
nHalfX = nOffsetX + nWidth / 2;
nQuarter3X = nOffsetX + (3 * nWidth) / 4;
nQuarter1Y = nOffsetY + nWidth / 4;
nHalfY = nOffsetY + nWidth / 2 + 1;
nQuarter3Y = nOffsetY + (3 * nWidth) / 4;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.HighQuality;
//
// Rectangle with white background and
shadow
//
e.Graphics.FillRectangle(Brushes.White,
nOffsetX, nOffsetY, nWidth,
nWidth);
e.Graphics.DrawRectangle(SystemPens.Control,
nOffsetX + 1, nOffsetY + 1,
nWidth - 1, nWidth - 1);
e.Graphics.DrawRectangle(SystemPens.ControlLight,
nOffsetX + 2, nOffsetY + 2,
nWidth - 4, nWidth - 4);
e.Graphics.DrawRectangle(SystemPens.ControlLightLight,
nOffsetX + 3, nOffsetY + 3,
nWidth - 7, nWidth - 7);
e.Graphics.DrawRectangle(penRect, nOffsetX,
nOffsetY, nWidth, nWidth);
//
// Checkmark
//
e.Graphics.DrawLine(penCheck, nQuarter1X,
nHalfY,
nHalfX, nQuarter3Y);
e.Graphics.DrawLine(penCheck, nQuarter1X,
nHalfY - 1,
nHalfX, nQuarter3Y - 1);
e.Graphics.DrawLine(penCheck, nHalfX - 1,
nQuarter3Y - 1,
nQuarter3X, nQuarter1Y);
e.Graphics.DrawLine(penCheck, nHalfX - 2,
nQuarter3Y - 2,
nQuarter3X, nQuarter1Y + 1);
e.Graphics.DrawLine(penCheck, nHalfX - 2,
nQuarter3Y - 2,
nQuarter3X, nQuarter1Y + 2);
//
if ( (e.State & DrawItemState.Grayed)
== DrawItemState.Grayed )
{
}
else
{
penCheck.Dispose();
penRect.Dispose();
}
}
private void DrawMenuImage(DrawItemEventArgs e,
int nImageIndex, int x, int y)
{
//
// Draw the desired image. if ( the menu
item is disabled, this state has to be
// shown. Therefore the API function
"DrawState" is used for drawing.
//
int hIcon, nResult, fuFlags;
System.IntPtr hDc;
if ( (e.State & DrawItemState.Grayed)
== DrawItemState.Grayed )
{
fuFlags = DST_ICON +
DSS_DISABLED;
}
else
{
fuFlags = DST_ICON +
DSS_NORMAL;
}
hIcon =
ImageList_GetIcon(imgMenuImagesForModule.Handle.ToInt32(),
nImageIndex, 0);
hDc = e.Graphics.GetHdc();
nResult = DrawState(hDc.ToInt32(), 0, 0,
hIcon, 0, x, y, 0, 0, fuFlags);
DestroyIcon(hIcon);
e.Graphics.ReleaseHdc(hDc);
//
}
private void DrawMenuSep(DrawItemEventArgs e)
{
//
// Draws a line to represent a menu
separator.
//
int nYMiddle;
nYMiddle = (int)(e.Bounds.Y +
e.Bounds.Height / 2);
e.Graphics.FillRectangle(SystemBrushes.Menu,
e.Bounds);
e.Graphics.FillRectangle(SystemBrushes.Control,
e.Bounds.X, e.Bounds.Y,
nXimgRectRight,
e.Bounds.Height);
e.Graphics.DrawLine(SystemPens.ControlDark,
e.Bounds.X + nXtextLeft, nYMiddle,
e.Bounds.Right, nYMiddle);
//
}
private void
DrawMenuTextImageShortcut(DrawItemEventArgs e,
ref string sText, ref int
nImageIndex, ref string sShortcut)
{
//
// Draw all the stuff by using some
subroutines.
//
// First of all, draw Text and Shortcut.
//
DrawStringUnderline(e, ref sText, ref
sShortcut);
//
// Fill the background of the image area.
//
e.Graphics.FillRectangle(SystemBrushes.Control,
e.Bounds.X, e.Bounds.Y,
nXimgRectRight,
e.Bounds.Height);
//
// Draw the image, if desired.
//
if ( nImageIndex > -1 )
{
DrawMenuImage(e, nImageIndex,
e.Bounds.X + nXimgLeft,
e.Bounds.Y +
e.Bounds.Height / 2 -
imgMenuImagesForModule.Images[nImageIndex].Height
/ 2);
}
//
// Draw a check mark for checked items.
//
if ( (e.State & DrawItemState.Checked)
== DrawItemState.Checked )
{
DrawMenuCheck(e);
}
//
}
private void DrawStringUnderline(DrawItemEventArgs e,
ref string sText, ref string sShortcut)
{
//
// Most of the work will be done here:
// - The Background off the text area is to
be drawn
// - The text and the shortcut are to be
drawn
// - The accelerator is to be drawn
//
// The API functions "TextOut" and
"GetTextExtentPoint32" are used, because
// Graphics.DrawString in combination with
Graphics.MeasureString is not exact
// enough for drawing the accelerator by
underlining the appropriate character.
// Especially if ClearType is active, the
result would not be acceptable.
//
bool bAccelerator, bNoAccelerator;
string sWithoutAmpersand, sBeforAmpersand,
sUnderlineChar;
int nTextHeight, nOffsetY, nPos,
nUnderlineWidth, nBeforAmpersandWidth;
int nTextColor;
Brush brushRect;
Pen penMenuText;
int x1, y1, x2, y2;
int hDc; System.IntPtr hDcIntPtr;
StringSize szTest = new StringSize();
//
// Evaluate the background brush and the
text color
//
nOffsetY = e.Bounds.Y + e.Bounds.Height /
2;
if ( (e.State & DrawItemState.Grayed)
== DrawItemState.Grayed )
{
brushRect = SystemBrushes.Menu;
nTextColor =
GetSysColor(COLOR_GRAYTEXT);
}
else
{
if ( (e.State &
DrawItemState.Selected) == DrawItemState.Selected )
{
brushRect =
SystemBrushes.Highlight;
nTextColor =
GetSysColor(COLOR_HIGHLIGHTTEXT);
}
else
{
brushRect =
SystemBrushes.Menu;
nTextColor =
GetSysColor(COLOR_MENUTEXT);
}
}
//
// Draw the Background off the text area
//
e.Graphics.FillRectangle(brushRect,
nXimgRectRight, e.Bounds.Y,
e.Bounds.Width,
e.Bounds.Height);
//
// The menus device context is needed for
the use of some GDI functions.
// Background mode "Transparent" and
TextColor has to be set, the menu font
// is already selected.
//
hDcIntPtr = e.Graphics.GetHdc();
hDc = hDcIntPtr.ToInt32();
SetBkMode(hDc, TRANSPARENT);
SetTextColor(hDc, nTextColor);
//
// Evaluate the text height.
//
GetTextExtentPoint32(hDc, "A", 1, szTest);
nTextHeight = szTest.cy;
//
// The accelerator is defined by the
character in the menu text, which follows the
// ampersand character. First draw the text
without an ampersand.
//
sWithoutAmpersand = sText.Replace("&",
"");
if ( sWithoutAmpersand != null )
{
TextOut(hDc, e.Bounds.X +
nXtextLeft, nOffsetY - nTextHeight / 2,
sWithoutAmpersand,
sWithoutAmpersand.Length);
}
//
// if a shortcut is defined, draw it right
aligned.
//
if ( sShortcut != null )
{
GetTextExtentPoint32(hDc,
sShortcut, sShortcut.Length, szTest);
TextOut(hDc, e.Bounds.Width -
szTest.cx - 4,
nOffsetY -
nTextHeight / 2, sShortcut, sShortcut.Length);
}
//
// In menus, drawn by Windows 2000 or
later, accelerators wont be shown by
// default. Only if the user is poping up a
menu by keyboard, the accelerators
// will be shown.
//
bNoAccelerator = (bool)
((e.State &
DrawItemState.NoAccelerator) == DrawItemState.NoAccelerator);
bAccelerator = ! bNoAccelerator;
if ( bAccelerator )
{
//
// Evaluate the character,
which shall be underlined.
//
nPos = sText.IndexOf("&");
if ( nPos > 0 )
{
sBeforAmpersand =
sText.Substring(0, nPos - 1);
sUnderlineChar =
sText.Substring(nPos, 1);
}
else
{
sBeforAmpersand =
sText;
sUnderlineChar =
null;
}
//
// if ( there is a character to
underline, make is so.
//
if ( sUnderlineChar != null )
{
//
// get { the width
of the text before the ampersand and the width of the
// character, which
has to be underlined.
//
GetTextExtentPoint32(hDc,
sBeforAmpersand,
sBeforAmpersand.Length,
szTest);
nBeforAmpersandWidth
= szTest.cx;
GetTextExtentPoint32(hDc,
sUnderlineChar, 1, szTest);
nUnderlineWidth =
szTest.cx - 1;
e.Graphics.ReleaseHdc(hDcIntPtr);
//
// Setting the
coordinates for drawing the underline
//
x1 = e.Bounds.X +
nXtextLeft + nBeforAmpersandWidth;
x2 = x1 +
nUnderlineWidth;
y1 = e.Bounds.Y +
e.Bounds.Height / 2 + nTextHeight / 2 - 1;
y2 = y1;
//
// The menu item
could be disabled. In this case, the underline has to
// be drawn with a
grayed pen. if ( it is selected, a highlighted pen is
// to be used.
//
if ( (e.State &
DrawItemState.Grayed) == DrawItemState.Grayed )
{
penMenuText
= SystemPens.GrayText;
}
else
{
if (
(e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
penMenuText
= SystemPens.HighlightText;
}
else
{
penMenuText
= SystemPens.MenuText;
}
}
e.Graphics.DrawLine(penMenuText,
x1, y1, x2, y2);
}
else
{
e.Graphics.ReleaseHdc(hDcIntPtr);
}
}
else
{
e.Graphics.ReleaseHdc(hDcIntPtr);
}
//
}
private void GetMenuTextImageShortcut( string
sMenuText, ref string sText,
ref int nImage, ref
string sShortcut)
{
//
// Separates the originally text, an image
index and the "cultured" shortcut
// from the given string.
//
string sImageIndex;
int nIdx;
//
// The shortcut is separated by a tab
character.
//
nIdx = sMenuText.IndexOf("\t");
if ( nIdx > -1 )
{
sText = sMenuText.Substring(0,
nIdx);
sShortcut =
sMenuText.Substring(nIdx + 1, sMenuText.Length - nIdx -1);
}
else
{
sText = sMenuText;
sShortcut = null;
}
//
// The index of the image is separated by
the string "_i_".
//
nIdx = sText.IndexOf("_i_");
if ( nIdx > -1 )
{
sImageIndex =
sText.Substring(nIdx + 3, sText.Length - nIdx - 3);
sText = sText.Substring(0,
nIdx);
nImage =
Convert.ToInt32(sImageIndex);
}
else
{
nImage = -1;
}
//
}
private void InitializeContextMenu(ref ContextMenu
ContextMenu,
ref ImageList imgMenuImages)
{
//
// Initializing each menu item of a context
menu
//
imgMenuImagesForModule = imgMenuImages;
foreach (MenuItem SubMenuItem in
ContextMenu.MenuItems )
{
InitializeSubMenu(SubMenuItem);
} //
SetMetric(ref imgMenuImages);
//
}
private void InitializeFormMenu(ref Form FormX, ref
ImageList imgMenuImages)
{
//
// Initializing each menu item of the main
menu
//
imgMenuImagesForModule = imgMenuImages;
foreach (MenuItem FormMenuItem in
FormX.Menu.MenuItems)
{
InitializeMainMenu(FormMenuItem);
} //
SetMetric(ref imgMenuImages);
//
}
private void InitializeMainMenu(MenuItem FormMenuItem)
{
//
// Initializing each sub menu item of a
menu item
//
foreach (MenuItem SubMenuItem in
FormMenuItem.MenuItems)
{
InitializeSubMenu(SubMenuItem);
} //
//
}
private void InitializeSubMenu(MenuItem SubMenuItem)
{
//
// Initializing a sub menu item
//
long nResult;
StringBuilder sBuffer = new
StringBuilder(256);
//
// The menu string is read before the
property "OwnerDraw" is set to "true",
// because it contains the short cut
corresponding to the users keyboard. for
// (example: in Germany "Strg" is the name
of the Key "Ctrl". The Text of the
// menu item is used to store the original
text, an image index and the
// "cultured" shortcut. The shortcut is
separated by a tab character.
//
nResult =
GetMenuString(SubMenuItem.Parent.Handle.ToInt32(), SubMenuItem.Index,
sBuffer, sBuffer.Capacity,
MF_BYPOSITION);
SubMenuItem.Text =
sBuffer.ToString().Substring(0, (int)nResult);
SubMenuItem.OwnerDraw = true;
//
// Add handlers to the events "MeasureItem"
and "DrawItem". The work has to
// be done there.
//
SubMenuItem.MeasureItem += new
MeasureItemEventHandler(MeasureItem);
SubMenuItem.DrawItem += new
DrawItemEventHandler(DrawSubMenuItem);
//
// if ( the sub menu has one or more sub
menus, they have to be initialized too.
//
foreach (MenuItem SubSubMenuItem in
SubMenuItem.MenuItems)
{
InitializeSubMenu(SubSubMenuItem);
} //
//
}
private void SetMetric(ref ImageList imgMenuImages)
{
//
// Calculation of some metrics, so it has
to be done only one time.
//
nXimgLeft =
(int)(imgMenuImages.Images[0].Width / 4 + 0.5);
nXimgRectRight = (int)(3 *
imgMenuImages.Images[0].Width / 2 + 0.5);
nXtextLeft = imgMenuImages.Images[0].Width
* 2;
//
}
#endregion
}
}
|
|
|
|
|
|