申明:
- 本文適用於WinForm開發
- 文中的“控件”一詞是廣義上的說法,泛指包括ToolStripItem、MenuItem在內單個界面元素,並不特指繼承自Control類的狹義控件
用過ToolTip這個組件的童鞋都知道這樣一個現象:在VS中拖入一個ToolTip,然后點擊窗體中的各種控件,在其屬性窗格中就會多出一個叫ToolTip的屬性出來,如圖:
本文要說的就是如何像ToolTip這樣,為控件“擴展”出一個屬性來(之所以用引號,是因為並不是真的為控件增加了一個屬性,而是在VS中看起來像那么回事)。下面通過一個實際案例來說明。
需求是這樣的:當用戶把鼠標指向菜單項(ToolStripMenuItem)或工具欄項(ToolStripButton、ToolStripLabel之類)的時候,在狀態欄標簽(ToolStripStatusLabel)中顯示該項的功能說明——很多軟件都這樣做,比如著名的Beyond Compare,如圖:
對於這個效果,很容易想到的做法是分別為各個菜單項和工具欄項(下稱item)注冊MouseEnter和MouseLeave事件,在enter事件中設置狀態欄標簽(下稱viewer)的Text="item的功能描述",在leave事件中viewer.Text=string.Empty,即將Text清空;又或者把所有的item的這倆事件分別綁定到兩個總的enter和leave事件處理方法中,然后在方法中用switch區分處理;再或者,把item的功能描述填在各自的Tag屬性里,然后在enter事件中只需一句viewer.Text=(sender as ToolStripItem).Tag as string即可~總之方法多多。題外,對於菜單項和工具欄項這樣的ToolStripItem,它們天生就有ToolTipText屬性可以設置氣泡提示,但本文並不探討氣泡方式好還是狀態欄方式好。
那么有沒有一種方式,寫一個像ToolTip這樣的組件,比如叫ToolDescribe,在VS中拖入后,就能在item的屬性窗格中多出一個叫Describe的屬性來,直接在里面填寫item的功能描述文本就完了,要能這樣該有多好:
這不廢話么,圖都上了,能不沒有么。上ToolDescribe的代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows.Forms; namespace AhDung.WinForm.Controls { /// <summary>
/// 為菜單項提供【描述】這一擴展屬性 /// </summary>
[Description("為菜單項或控件提供描述擴展屬性")] [ProvideProperty("Describe", typeof(ToolStripItem))] public class ToolDescribe : Component, IExtenderProvider { /// <summary>
/// 存儲所服務的ToolStripItem及其描述 /// </summary>
readonly Dictionary<ToolStripItem, string> dic; /// <summary>
/// 獲取或設置用於顯示菜單項描述的控件 /// </summary>
[DefaultValue(null), Description("獲取或設置用於顯示菜單項描述的控件")] public Component Viewer { get; set; } /// <summary>
/// 創建一個ToolDescribe類 /// </summary>
public ToolDescribe() { dic = new Dictionary<ToolStripItem, string>(); } /// <summary>
/// 獲取菜單項描述 /// </summary>
[Description("設置菜單項描述")] //雖然方法為Get,但在VS中顯示為“設置”才符合理解
[DefaultValue(null)] public string GetDescribe(ToolStripItem item) { //從集合中取出該item的描述
string value; dic.TryGetValue(item, out value); return value; } /// <summary>
/// 設置菜單項描述 /// </summary>
public void SetDescribe(ToolStripItem item, string value) { if (item == null) { return; } //如果賦值為null或string.Empty,視為不再擴展該ToolStripItem
if (string.IsNullOrEmpty(value) || value.Trim().Length == 0) { //從集合中移除該item,並取消其相關事件綁定
dic.Remove(item); item.MouseEnter -= item_MouseEnter; item.MouseLeave -= item_MouseLeave; } else { if (!dic.ContainsKey(item))//若是新添加的item,注冊其相關事件
{ item.MouseEnter += item_MouseEnter; item.MouseLeave += item_MouseLeave; } //添加或更改該item的描述
dic[item] = value;//這種寫法對於dic中不存在的Key,會自動添加
} } //鼠標指向事件
private void item_MouseEnter(object sender, EventArgs e) { //讓Viewer.Text顯示item的描述
if (Viewer is ToolStripItem) { (Viewer as ToolStripItem).Text = dic[sender as ToolStripItem]; } else if (Viewer is StatusBarPanel) { (Viewer as StatusBarPanel).Text = dic[sender as ToolStripItem]; } else if (Viewer is Control) { (Viewer as Control).Text = dic[sender as ToolStripItem]; } } //鼠標移開事件
private void item_MouseLeave(object sender, EventArgs e) { //清空Viewer.Text
if (Viewer is ToolStripItem) { (Viewer as ToolStripItem).Text = string.Empty; } else if (Viewer is StatusBarPanel) { (Viewer as StatusBarPanel).Text = string.Empty; } else if (Viewer is Control) { (Viewer as Control).Text = string.Empty; } } /// <summary>
/// 是否可為某對象擴展屬性 /// </summary>
public bool CanExtend(object extendee) { return true; } } }
實現說明:
1、讓ToolDescribe類實現System.ComponentModel.IExtenderProvider接口,並繼承System.ComponentModel.Component類,同時用ProvidePropertyAttribute特性描述該類。實現IExtenderProvider接口就表明本類是一個【擴展程序提供程序】,MSDN有相關的示例:http://msdn.microsoft.com/zh-cn/library/ms229066(v=vs.80).aspx。那么到底是要給什么類擴展出什么屬性呢,這是由ProvideProperty特性定義的,本類的目的是為【ToolStripItem】類擴展出一個叫【Describe】的屬性,所以這樣描述[ProvideProperty("Describe", typeof(ToolStripItem))]。繼承Component則是為了讓ToolDescribe像ToolTip那樣能拖入到VS組件欄中,這樣item的屬性窗格中才會多出一個Describe屬性來;
2、在ToolDescribe類中定義一個集合類容器,用於存儲設置了功能描述的item及其描述文本。本例采用的是Dictionary<ToolStripItem, string>,顯然Key代表item,Value代表item的描述文本;
3、定義一個屬性,類型為Component,用來呈現item功能描述的控件,本例是Viewer。類型之所以為Component而不是Control,是考慮到Viewer要允許設置為狀態欄標簽(ToolStripStatusLabel)的,而ToolStripStatusLabel並不是Control,所以得把類型定得再“基類”一點,以加大Viewer的設置靈活性;
4、實現一個public string GetDescribe(ToolStripItem item)方法,作用是獲取指定item的描述文本,這也是第2步中定義容器的原因,沒有容器記錄下各個item及其描述文本的話,這個方法將難以實現。注意該方法的命名必須是Get+ProvideProperty中定義的擴展屬性名,即Describe,合起來就是GetDescribe。另外,對該方法加DefaultValue特性是必要的,不然當拖入ToolDescribe時,VS會對所有item進行擴展,不管有沒有設置某個item的Describe,這點可以從InitializeComponent方法中看出來;
5、實現一個public void SetDescribe(ToolStripItem item, string value)方法,命名要求同上。該方法的作用顯然是用來設置item的描述文本的。具體實現邏輯上,它主要要做兩件事:①把item及其value存入集合;②注冊item的相關事件。即當item發生了什么時要做什么事,本例當然是當item發生MouseEnter和MouseLeave時,要做一些事,所以得注冊item的這倆事件。說到這里,其實可以理解顯示item功能描述的核心實現仍然是基於對相關事件的注冊,也就是說本質上,與前面提到的分別為各個item注冊事件這種看起來原始且笨的方式是一樣一樣的,用了ToolDescribe也沒有什么高大上的地方,只是沒那么麻煩了而已。當然這里說的是應用層面,底層VS對IExtenderProvider程序做了些什么那自然是高大上的;
6、實現上述事件的處理方法,本例就是item_MouseEnter和item_MouseLeave,實現上沒什么好說的。只是上面的代碼重點在演示實現套路,所以沒有做額外的性能優化處理,如果代碼要應用在生產環境,則需對if (Viewer is ToolStripItem)這樣的語句進行處理,例如可以在Viewer屬性的setter中就記錄下Viewer屬於何種類型,然后就不必在每次事件觸發時判斷Viewer類型了;
7、最后是實現IExtenderProvider接口的唯一成員:public bool CanExtend(object extendee)方法。這方法純粹是供VS用的,方法的邏輯是,當你在VS中點擊某個控件時,extendee就是該控件,返回true則在該控件的屬性窗格中添加擴展屬性,否則不添加。本例是直接返回true,那會不會造成點擊任意控件都會多出Describe屬性呢,答案是不會,因為ProvideProperty特性已經首先限定了只擴展ToolStripItem類。那該方法在什么情況下需要加邏輯呢,舉例,要為Button和TextBox擴展屬性,自然ProvideProperty限定為Control較合適,但這樣一來,不屬於Button和TextBox的其它控件也被擴展了,為了不擴展它們,就需要在CanExtend方法中加邏輯return extendee is Button || extendee is TextBox,等於一個控件是否會被擴展,取決於兩個地方,一是ProvideProperty,二是CanExtend,前者是第一關,后者是第二關;
OK,套路就是這樣,看看成果:
1、拖入一個ToolDescribe組件,並設置其Viewer為狀態欄標簽:
2、設置item的Describe屬性,見圖3;
3、跑起來看看:
話說回來,對於這種效果,路過高手如果有比添加擴展屬性更好的方案還望不吝賜教。
下面附贈一枚正式的ToolDescribe,這個比上述Demo強在,可以為ToolStripItem、Control、MenuItem添加擴展屬性,並對性能優化做了處理,可用於生產環境。同時可以看出ProvideProperty特性可以疊加使用,達到為不同控件添加不同擴展屬性的目的,話說之所以不寫成為Component擴展Describe屬性,是因為MenuItem只有鼠標移進事件(Select),沒有移出事件,要想達到指向沒有設置Describe的MenuItem時,Viewer.Text清空,只有為所有MenuItem擴展,這也是沒有為GetDescribeOfMenuItem加DefaultValue特性的原因~不知道看官能不能明白我在說什么,呵呵。上代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows.Forms; namespace AhDung.WinForm.Controls { /// <summary> /// 為菜單項或控件提供功能描述擴展屬性 /// </summary> /// <remarks> /// Author:AhDung /// Update:201412161356,初版 /// </remarks> [Description("為菜單項或控件提供功能描述擴展屬性")] [ProvideProperty("DescribeOfToolStripItem", typeof(ToolStripItem))] [ProvideProperty("DescribeOfControl", typeof(Control))] [ProvideProperty("DescribeOfMenuItem", typeof(MenuItem))] public class ToolDescribe : Component, IExtenderProvider { /// <summary> /// 存儲所服務的組件及其描述 /// </summary> readonly Dictionary<Component, string> dic; Component viewer; bool viewerIsToolStripItem, viewerIsStatusBarPanel, viewerIsControl; ToolStripItem viewerAsToolStripItem; StatusBarPanel viewerAsStatusBarPanel; Control viewerAsControl; /// <summary> /// 獲取或設置用於顯示功能描述的控件 /// </summary> [DefaultValue(null), Description("設置用於顯示功能描述的控件")] public Component Viewer { get { return viewer; } set { if (viewer == value) { return; } viewer = value; viewerIsToolStripItem = false; viewerIsStatusBarPanel = false; viewerIsControl = false; if (viewer is ToolStripItem) { viewerIsToolStripItem = true; viewerAsToolStripItem = viewer as ToolStripItem; } else if (viewer is StatusBarPanel) { viewerIsStatusBarPanel = true; viewerAsStatusBarPanel = viewer as StatusBarPanel; } else if (viewer is Control) { viewerIsControl = true; viewerAsControl = viewer as Control; } } } /// <summary> /// 創建一個ToolDescribe類 /// </summary> public ToolDescribe() { dic = new Dictionary<Component, string>(); } /// <summary> /// 獲取ToolStripItem描述 /// </summary> [DefaultValue(null), Description("設置菜單項的功能描述")] public string GetDescribeOfToolStripItem(ToolStripItem item) { return GetDescribe(item); } /// <summary> /// 獲取Control描述 /// </summary> [DefaultValue(null), Description("設置控件的功能描述")] public string GetDescribeOfControl(Control item) { return GetDescribe(item); } /// <summary> /// 獲取MenuItem描述 /// </summary> [Description("設置菜單項的功能描述")] public string GetDescribeOfMenuItem(MenuItem item) { return GetDescribe(item); } //獲取組件描述(私有) private string GetDescribe(Component item) { string value; dic.TryGetValue(item, out value); return value; } /// <summary> /// 設置ToolStripItem描述 /// </summary> public void SetDescribeOfToolStripItem(ToolStripItem item, string value) { if (item == null) { return; } if (string.IsNullOrEmpty(value) || value.Trim().Length == 0) { dic.Remove(item); item.MouseEnter -= item_MouseEnter; item.MouseLeave -= item_MouseLeave; } else { if (!dic.ContainsKey(item)) { item.MouseEnter += item_MouseEnter; item.MouseLeave += item_MouseLeave; } dic[item] = value; } } /// <summary> /// 設置Control描述 /// </summary> public void SetDescribeOfControl(Control item, string value) { if (item == null) { return; } if (string.IsNullOrEmpty(value) || value.Trim().Length == 0) { dic.Remove(item); item.MouseEnter -= item_MouseEnter; item.MouseLeave -= item_MouseLeave; } else { if (!dic.ContainsKey(item)) { item.MouseEnter += item_MouseEnter; item.MouseLeave += item_MouseLeave; } dic[item] = value; } } /// <summary> /// 設置MenuItem描述 /// </summary> public void SetDescribeOfMenuItem(MenuItem item, string value) { if (item == null) { return; } if (!dic.ContainsKey(item)) { item.Select += item_MouseEnter; } dic[item] = value; } //組件鼠標指向事件 private void item_MouseEnter(object sender, EventArgs e) { this.SetViewerText(dic[sender as Component]); } //組件鼠標移開事件 private void item_MouseLeave(object sender, EventArgs e) { this.SetViewerText(string.Empty); } /// <summary> /// 設置Viewer的Text值 /// </summary> private void SetViewerText(string s) { if (viewerIsToolStripItem) { viewerAsToolStripItem.Text = s; } else if (viewerIsStatusBarPanel) { viewerAsStatusBarPanel.Text = s; } else if (viewerIsControl) { viewerAsControl.Text = s; } } //實現IExtenderProvider成員 [EditorBrowsable(EditorBrowsableState.Never)] public bool CanExtend(object extendee) { return !(extendee is Form); } } }
- 文畢-