簡介
僅僅使用一行簡單的程序,你就能夠使你的Windows窗體的所有菜單和上下文菜單具有office2003的菜單外觀。同樣地,你也可以只用一行程序,就能為你的菜單加上漂亮的圖標。本文實現的是一個具有該功能的組件。如果你想讓你的菜單恢復原來的外觀,也只須調用End方法即可。
組件的使用
要正確使用組件,必須先將你的組件加入到工具箱中。然後將該組件從工具箱中拖放放到form窗體中。這時會看到你的form的設計頁中多出了一個名為OfficeMenus1的圖標,說明已經將菜單組件加入到了form中。緊接著調用如下方法:
//開始顯示office 2003菜單
同樣,你也可以通過調用如下方法終止菜單的office2003風格,使之回到原始外觀:
OfficeMenus1.Start( FormName ); 注:FormName為要改變菜單風格的窗口名稱。// 改變菜單的外觀風格到原始狀態
為菜單頂添加圖標也很簡單,只須為工程添加一個ImageList(圖像列表控件),然後將OfficeMenu組件的ImageList屬性更改為你添加的ImageList,使用如下代碼實現:
OfficeMenus1.End();// 為菜單添加圖像
// OfficeMenus.AddPicture( MenuItem MenuItemToAddPictureTo, int ImageIndex );
OfficeMenus1.ImageList = imageList1;
OfficeMenus1.AddPicture(menuItem2, 1);
可以看出,只須如此幾行代碼就能輕松讓你的菜單實現office2003風格。
組件的實現方法及原理
組件由三個類實現,這三個類分別為OfficeMenus,MainMenuItemDrawing和MenuItemDrawing,都位於命名空間Dev4Arabs中。由於實現代碼較長,所以在此只給出了組件實現的思路。
組件實現的第一步是從System.ComponentModel.Component類派生類OfficeMenus。定義如下: public class OfficeMenus : System.ComponentModel.Component
然後在類中定義兩靜態變量:
//圖像列表用來存儲菜單中用到的圖標 static ImageList _imageList;
// 存儲圖片細節的一個名稱集合,NameValueCollection的詳細說明請查閱MSDN,該類主要用來使每個菜單的句柄與每個圖標形成一一對應的關系,以便後面繪制菜單頂的圖標時快速地找到某個菜單所對應的圖標。 static NameValueCollection picDetails = new NameValueCollection();
接下來定義公共接口方法start和End。
公共接口方法Start的實現原理如下: public void Start(Form form)
{
先從Start傳入的參數中獲得該窗口的主菜單。接下來為每一個主菜單下的MenuItem添加MeasureItem事件
處理mainMenuItem_MeasureItem和DrawItem事件處理mainMenuItem_DrawItem,
將MenuItem的OwnerDraw屬性設置為true,並使用InitMenuItem(Menu mi)方法對每個MenuItem應用改變。
然後再從form參數中獲取該窗口的上下文菜單對象,對其調用InitMenuItem(Menu mi)應用改變。
最後循環查找窗口中包含的每個子控件的上下文菜單,對其應用改變。
System.Windows.Forms.MainMenu menu = form.Menu;
foreach ( MenuItem mi in menu.MenuItems )
{
mi.MeasureItem += new System.Windows.Forms.MeasureItemEventHandler(mainMenuItem_MeasureItem);
mi.DrawItem += new System.Windows.Forms.DrawItemEventHandler(mainMenuItem_DrawItem);
mi.OwnerDraw = true;
InitMenuItem(mi);
}
ContextMenu cmenu = form.ContextMenu;
if ( cmenu != null ) {InitMenuItem(cmenu);}
foreach ( Control c in form.Controls ) {
if ( c.ContextMenu != null )
InitMenuItem(c.ContextMenu);}
catch {}
}
End方法與這類似,只須將InitMenuItem換為UninitMenuItem,這裡就不再舉出了。具體實現代碼請到http://www.codeproject.com/cs/menu/MhOffice2003Menus.asp下載。
為每個菜單應用改變的InitMenuItem方法的實現如下: private void InitMenuItem(Menu mi)
UninitMenuItem的定義與此類似。
{
循環查找mi中的每個MenuItem,為其添加MeasureItem事件處理menuItem_MeasureItem和
DrawItem事件處理menuItem_DrawItem(注意,此處的事件處理方法名稱與Start中對
主菜單的事件處理方法名稱不同),並將OwnerDraw屬性設置為true。使用遞歸調用對每個查找
到的MenuItem調用InitMenuItem方法,這樣便可以對菜單項下的所有級別的子菜單項都應用到改變。
}
menuItem_MeasureItem事件處理方法定義如下: private void menuItem_MeasureItem(object sender, System.Windows.Forms.MeasureItemEventArgs e)
menuItem_DrawItem的實現如下:
{
先取得要進行消息處理的MenuItem對象(MenuItem item = (MenuItem) sender;)。
if (item為seperator ) { e.ItemHeight = 7;}
else {
獲取item的文字寬度,如果有快捷鍵,還要獲取item中的快捷鍵所占用的寬度。
設置item的邊界:e.ItemHeight = 文字高度+7; e.ItemWidth = 文字寬度 + 快捷鍵的寬度 + 圖標寬度*2;
}
}private void menuItem_DrawItem(object sender, System.Windows.Forms.DrawItemEventArgs e)
該函數只是簡單地調用了類MenuItemDrawing中的靜態方法DrawMenuItem。
{
MenuItemDrawing.DrawMenuItem(e, (MenuItem) sender);
}
Start中為主菜單的子菜單添加事件處理的mainMenuItem_MeasureItem和mainMenuItem_DrawItem的定義如下: private void mainMenuItem_MeasureItem(object sender, System.Windows.Forms.MeasureItemEventArgs e)
該方法只是調用了類MainMenuItemDrawing中的靜態方法DrawMenuItem進行菜單繪制。
{
MenuItem mi = (MenuItem) sender;//獲得菜單項對象
SizeF miSize = e.Graphics.MeasureString(mi.Text, Globals.menuFont);
//由於頂級菜單(如文件菜單)無快捷鍵和圖標,所以繪制的寬度為文字的寬度。
e.ItemWidth = Convert.ToInt32(miSize.Width);
}
private void mainMenuItem_DrawItem(object sender, System.Windows.Forms.DrawItemEventArgs e)
{
MainMenuItemDrawing.DrawMenuItem(e, (MenuItem) sender);
}
其它方法如AddPicture和GetItemPicture的定義如下:public void AddPicture(MenuItem mi, int index)
{
//將菜單項的句柄轉化為字符串與圖標的索引一一對應添加到picDetails集合中。
picDetails.Add(mi.Handle.ToString(), index.ToString());
}
public static Bitmap GetItemPicture(MenuItem mi)
{
if ( _imageList == null )
return null;
//將菜單項的句柄作為鍵查找該鍵對應的值,返回值為圖標索引
string [] picIndex = picDetails.GetValues(mi.Handle.ToString());
if ( picIndex == null )
return null;
else
//根據索引取出位圖對象並返回
return (Bitmap)_imageList.Images[Convert.ToInt32(picIndex[0])];
}
類MainMenuItemDrawing主要負責主菜單的一級子菜單(如常見的文件菜單)的繪制。
實現如下:public class MainMenuItemDrawing
類MenuItemDrawing負責主菜單的子菜單和上下文菜單的繪制。
{
//靜態方法,實現菜單項的繪制
public static void DrawMenuItem(System.Windows.Forms.DrawItemEventArgs e, MenuItem mi)
{
首先檢查menuItem的狀態,如為鼠標懸浮在其上的狀態,則調用DrawHoverRect繪制並填充懸浮矩形;
如為選定態,則調用DrawSelectionRect繪制並填充相應的選定態時的矩形;
如兩者都不是,則用控件的填充色繪制並填充矩形。
最後利用e.Graphics.DrawString方法繪制菜單文字。
}
……
}public class MenuItemDrawing
{
//靜態方法,實現菜單項的繪制
public static void DrawMenuItem(System.Windows.Forms.DrawItemEventArgs e, MenuItem mi)
{
檢查菜單頂是否被選中,如被選中,則調用DrawSelectionRect繪制並填充選中後的矩形,
否則只用背景色繪制空白區域並調用DrawPictureArea繪制圖片區域。
調用DrawCheckBox繪制復合框如果該菜單項被選中。
調用DrawMenuText繪制菜單項文字。最後調用DrawItemPicture繪制圖標。
}
……
}
由於繪制的代碼比較長,不宜在文中全部給出,所以具體的繪制代碼將省去。文中只是給出了實現該組件的思路。
結束語:
由於.net中使用了GDI+,所以組件的繪制工作比以前在MFC或者Win32API模式下繪制要容易地多。組件的開發最重要的一點就是當菜單項被置為自繪方式後,用戶需要激活兩個事件來定制菜單的顯示。第一個事件對應Win32的WM_MEASUREITEM消息。窗口收到這個消息時,它就會觸發一個 MeasureItem 事件給所有的自繪 MenuItem 對象。這個事件代理(Delegate)是一個名為MeasureItemEventHandler的類,與此事件相關的信息都被存儲在一個MeasureItemEventArgs 對象中並被傳遞到事件處理函數(文中為mainMenuItem_MeasureItem或者menuItem_MeasureItem)。第二個事件與 Win32 的WM_DRAWITEM消息對應,並給每個注冊了的事件處理函數傳遞一個 DrawItemEventArgs 對象。這個事件代理是一個名為DrawItemEventHandler的類。個人認為組件實現的難點和重點就是在兩事件處理函數中根據菜單的不同狀態所要進行的不同繪制工作。