申明:

- 本文适用于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的代码:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Windows.Forms;
  5.  
  6. namespace AhDung.WinForm.Controls
  7. {
  8. /// <summary>
  9. /// 为菜单项提供【描述】这一扩展属性
  10. /// </summary>
  11. [Description("为菜单项或控件提供描述扩展属性")]
  12. [ProvideProperty("Describe", typeof(ToolStripItem))]
  13. public class ToolDescribe : Component, IExtenderProvider
  14. {
  15. /// <summary>
  16. /// 存储所服务的ToolStripItem及其描述
  17. /// </summary>
  18. readonly Dictionary<ToolStripItem, string> dic;
  19.  
  20. /// <summary>
  21. /// 获取或设置用于显示菜单项描述的控件
  22. /// </summary>
  23. [DefaultValue(null), Description("获取或设置用于显示菜单项描述的控件")]
  24. public Component Viewer { get; set; }
  25.  
  26. /// <summary>
  27. /// 创建一个ToolDescribe类
  28. /// </summary>
  29. public ToolDescribe()
  30. {
  31. dic = new Dictionary<ToolStripItem, string>();
  32. }
  33.  
  34. /// <summary>
  35. /// 获取菜单项描述
  36. /// </summary>
  37. [Description("设置菜单项描述")] //虽然方法为Get,但在VS中显示为“设置”才符合理解
  38. [DefaultValue(null)]
  39. public string GetDescribe(ToolStripItem item)
  40. {
  41. //从集合中取出该item的描述
  42. string value;
  43. dic.TryGetValue(item, out value);
  44. return value;
  45. }
  46.  
  47. /// <summary>
  48. /// 设置菜单项描述
  49. /// </summary>
  50. public void SetDescribe(ToolStripItem item, string value)
  51. {
  52. if (item == null) { return; }
  53.  
  54. //如果赋值为null或string.Empty,视为不再扩展该ToolStripItem
  55. if (string.IsNullOrEmpty(value) || value.Trim().Length == )
  56. {
  57. //从集合中移除该item,并取消其相关事件绑定
  58. dic.Remove(item);
  59. item.MouseEnter -= item_MouseEnter;
  60. item.MouseLeave -= item_MouseLeave;
  61. }
  62. else
  63. {
  64. if (!dic.ContainsKey(item))//若是新添加的item,注册其相关事件
  65. {
  66. item.MouseEnter += item_MouseEnter;
  67. item.MouseLeave += item_MouseLeave;
  68. }
  69.  
  70. //添加或更改该item的描述
  71. dic[item] = value;//这种写法对于dic中不存在的Key,会自动添加
  72. }
  73. }
  74.  
  75. //鼠标指向事件
  76. private void item_MouseEnter(object sender, EventArgs e)
  77. {
  78. //让Viewer.Text显示item的描述
  79. if (Viewer is ToolStripItem) { (Viewer as ToolStripItem).Text = dic[sender as ToolStripItem]; }
  80. else if (Viewer is StatusBarPanel) { (Viewer as StatusBarPanel).Text = dic[sender as ToolStripItem]; }
  81. else if (Viewer is Control) { (Viewer as Control).Text = dic[sender as ToolStripItem]; }
  82. }
  83.  
  84. //鼠标移开事件
  85. private void item_MouseLeave(object sender, EventArgs e)
  86. {
  87. //清空Viewer.Text
  88. if (Viewer is ToolStripItem) { (Viewer as ToolStripItem).Text = string.Empty; }
  89. else if (Viewer is StatusBarPanel) { (Viewer as StatusBarPanel).Text = string.Empty; }
  90. else if (Viewer is Control) { (Viewer as Control).Text = string.Empty; }
  91. }
  92.  
  93. /// <summary>
  94. /// 是否可为某对象扩展属性
  95. /// </summary>
  96. public bool CanExtend(object extendee)
  97. {
  98. return true;
  99. }
  100. }
  101. }

实现说明:

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特性的原因~不知道看官能不能明白我在说什么,呵呵。上代码:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Windows.Forms;
  5.  
  6. namespace AhDung.WinForm.Controls
  7. {
  8. /// <summary>
  9. /// 为菜单项或控件提供功能描述扩展属性
  10. /// </summary>
  11. /// <remarks>
  12. /// Author:AhDung
  13. /// Update:201412161356,初版
  14. /// </remarks>
  15. [Description("为菜单项或控件提供功能描述扩展属性")]
  16. [ProvideProperty("DescribeOfToolStripItem", typeof(ToolStripItem))]
  17. [ProvideProperty("DescribeOfControl", typeof(Control))]
  18. [ProvideProperty("DescribeOfMenuItem", typeof(MenuItem))]
  19. public class ToolDescribe : Component, IExtenderProvider
  20. {
  21. /// <summary>
  22. /// 存储所服务的组件及其描述
  23. /// </summary>
  24. readonly Dictionary<Component, string> dic;
  25.  
  26. Component viewer;
  27. bool viewerIsToolStripItem, viewerIsStatusBarPanel, viewerIsControl;
  28. ToolStripItem viewerAsToolStripItem;
  29. StatusBarPanel viewerAsStatusBarPanel;
  30. Control viewerAsControl;
  31.  
  32. /// <summary>
  33. /// 获取或设置用于显示功能描述的控件
  34. /// </summary>
  35. [DefaultValue(null), Description("设置用于显示功能描述的控件")]
  36. public Component Viewer
  37. {
  38. get { return viewer; }
  39. set
  40. {
  41. if (viewer == value) { return; }
  42.  
  43. viewer = value;
  44.  
  45. viewerIsToolStripItem = false;
  46. viewerIsStatusBarPanel = false;
  47. viewerIsControl = false;
  48.  
  49. if (viewer is ToolStripItem) { viewerIsToolStripItem = true; viewerAsToolStripItem = viewer as ToolStripItem; }
  50. else if (viewer is StatusBarPanel) { viewerIsStatusBarPanel = true; viewerAsStatusBarPanel = viewer as StatusBarPanel; }
  51. else if (viewer is Control) { viewerIsControl = true; viewerAsControl = viewer as Control; }
  52. }
  53. }
  54.  
  55. /// <summary>
  56. /// 创建一个ToolDescribe类
  57. /// </summary>
  58. public ToolDescribe()
  59. {
  60. dic = new Dictionary<Component, string>();
  61. }
  62.  
  63. /// <summary>
  64. /// 获取ToolStripItem描述
  65. /// </summary>
  66. [DefaultValue(null), Description("设置菜单项的功能描述")]
  67. public string GetDescribeOfToolStripItem(ToolStripItem item)
  68. {
  69. return GetDescribe(item);
  70. }
  71.  
  72. /// <summary>
  73. /// 获取Control描述
  74. /// </summary>
  75. [DefaultValue(null), Description("设置控件的功能描述")]
  76. public string GetDescribeOfControl(Control item)
  77. {
  78. return GetDescribe(item);
  79. }
  80.  
  81. /// <summary>
  82. /// 获取MenuItem描述
  83. /// </summary>
  84. [Description("设置菜单项的功能描述")]
  85. public string GetDescribeOfMenuItem(MenuItem item)
  86. {
  87. return GetDescribe(item);
  88. }
  89.  
  90. //获取组件描述(私有)
  91. private string GetDescribe(Component item)
  92. {
  93. string value;
  94. dic.TryGetValue(item, out value);
  95. return value;
  96. }
  97.  
  98. /// <summary>
  99. /// 设置ToolStripItem描述
  100. /// </summary>
  101. public void SetDescribeOfToolStripItem(ToolStripItem item, string value)
  102. {
  103. if (item == null) { return; }
  104.  
  105. if (string.IsNullOrEmpty(value) || value.Trim().Length == )
  106. {
  107. dic.Remove(item);
  108. item.MouseEnter -= item_MouseEnter;
  109. item.MouseLeave -= item_MouseLeave;
  110. }
  111. else
  112. {
  113. if (!dic.ContainsKey(item))
  114. {
  115. item.MouseEnter += item_MouseEnter;
  116. item.MouseLeave += item_MouseLeave;
  117. }
  118.  
  119. dic[item] = value;
  120. }
  121. }
  122.  
  123. /// <summary>
  124. /// 设置Control描述
  125. /// </summary>
  126. public void SetDescribeOfControl(Control item, string value)
  127. {
  128. if (item == null) { return; }
  129.  
  130. if (string.IsNullOrEmpty(value) || value.Trim().Length == )
  131. {
  132. dic.Remove(item);
  133. item.MouseEnter -= item_MouseEnter;
  134. item.MouseLeave -= item_MouseLeave;
  135. }
  136. else
  137. {
  138. if (!dic.ContainsKey(item))
  139. {
  140. item.MouseEnter += item_MouseEnter;
  141. item.MouseLeave += item_MouseLeave;
  142. }
  143.  
  144. dic[item] = value;
  145. }
  146. }
  147.  
  148. /// <summary>
  149. /// 设置MenuItem描述
  150. /// </summary>
  151. public void SetDescribeOfMenuItem(MenuItem item, string value)
  152. {
  153. if (item == null) { return; }
  154.  
  155. if (!dic.ContainsKey(item))
  156. {
  157. item.Select += item_MouseEnter;
  158. }
  159.  
  160. dic[item] = value;
  161. }
  162.  
  163. //组件鼠标指向事件
  164. private void item_MouseEnter(object sender, EventArgs e)
  165. {
  166. this.SetViewerText(dic[sender as Component]);
  167. }
  168.  
  169. //组件鼠标移开事件
  170. private void item_MouseLeave(object sender, EventArgs e)
  171. {
  172. this.SetViewerText(string.Empty);
  173. }
  174.  
  175. /// <summary>
  176. /// 设置Viewer的Text值
  177. /// </summary>
  178. private void SetViewerText(string s)
  179. {
  180. if (viewerIsToolStripItem) { viewerAsToolStripItem.Text = s; }
  181. else if (viewerIsStatusBarPanel) { viewerAsStatusBarPanel.Text = s; }
  182. else if (viewerIsControl) { viewerAsControl.Text = s; }
  183. }
  184.  
  185. //实现IExtenderProvider成员
  186. [EditorBrowsable(EditorBrowsableState.Never)]
  187. public bool CanExtend(object extendee)
  188. {
  189. return !(extendee is Form);
  190. }
  191. }
  192. }

- 文毕-

【C#】使用IExtenderProvider为控件添加扩展属性,像ToolTip那样的更多相关文章

  1. 如何给ActiveX控件添加“事件”“属性”“标准事件”“自定义事件”等一些相关操作

    上一篇小编带大家熟悉了一下ActiveX的建立以及相关的概念,(http://blog.csdn.net/u014028070/article/details/38424611) 本文介绍下如何给控件 ...

  2. WPF系列 —— 控件添加依赖属性(转)

    WPF系列 —— 控件添加依赖属性 依赖属性的概念,用途 ,如何新建与使用.本文用做一个自定义TimePicker控件来演示WPF的依赖属性的简单应用. 先上TimePicker的一个效果图. 概念 ...

  3. ATL向控件添加私有属性-成员变量

    https://msdn.microsoft.com/zh-cn/library/cc451389(v=vs.71).aspx ------------------------------------ ...

  4. WPF系列 —— 控件添加依赖属性

    依赖属性的概念,用途 ,如何新建与使用.本文用做一个自定义TimePicker控件来演示WPF的依赖属性的简单应用. 先上TimePicker的一个效果图. 概念 和 用途:依赖属性是对传统.net ...

  5. WPF对某控件添加右键属性

    代码创建右键属性 ContextMenu cm = new ContextMenu(); MenuItem mi = new MenuItem(); mi.Header = "打开此文件所有 ...

  6. asp.net中的ListBox控件添加双击事件

    问题:在Aspx页里的ListBox A中添加双击事件,将选中项添加到另一个ListBox B中,双击ListBox B中的选中项,删除当前选中项 页面: <asp:ListBox ID=&qu ...

  7. iOS 在xib或storyboard里为控件添加圆角、外框和外框颜色

    如果要在xib和storyboard里为控件添加圆角和外框宽度,只要这样做就可以 layer.borderWidth     设置外框宽度属性 layer.cornerRadius    设置圆角属性 ...

  8. TextBoxFor控件的扩展---Bootstrap在mvc上的应用

    TextBoxFor控件的问题: 1:自带了样式,再用bootstrap样式会有冲突. 2:要加水印,js事件,限制输入长度比较麻烦. 因此需要对textboxfor控件进行扩展. 目标: 1:能使用 ...

  9. MFC编程入门之九(对话框:为控件添加消息处理函数)

    这一节讲的主要内容是如何为控件添加消息处理函数. MFC为对话框和控件定义了诸多消息,我们对他们操作时会触发消息,这些消息最终由消息处理函数处理,比如我们点击按钮时就会产生BN_CLICKED消息,修 ...

随机推荐

  1. 手把手教你用python打造网易公开课视频下载软件3-对抓取的数据进行处理

    上篇讲到抓取的数据保存到rawhtml变量中,然后通过编码最终保存到html变量当中,那么html变量还会有什么问题吗?当然会有了,例如可能html变量中的保存的抓取的页面源代码可能有些标签没有关闭标 ...

  2. python:how does subclass call baseclass's __init__()

    First, use baseclass's name to call __init__() I wrote code like this: and we can use 'super' too.

  3. 【团队分享之二】IT团队绩效提升的一些见解

  4. struts2学习笔记之六:struts2的Action访问ServletAPI的几种方式

    方法一:通过ActionContext访问SerlvetAPI,这种方式没有侵入性 Action类部分代码 import com.opensymphony.xwork2.ActionContext; ...

  5. Servlet Filter

    Filter : Java中的Filter 并不是一个标准的Servlet ,它不能处理用户请求,也不能对客户端生成响应. 主要用于对HttpServletRequest 进行预处理,也可以对Http ...

  6. Java Annotation 总结

    Annotation 被称为注解,在Java开发中是相当常见的,通过注解,我们可以简化代码提高开发效率.例如Override Annotation,这个应该算是在开发过程中使用最多的注解了.下面这个例 ...

  7. 阿里云 云解析使用方法/在阿里云ESC服务器解析域名并绑定服务器IP后上传文件通过域名访问步骤教程

    第一步:登录阿里云官网,获取服务器ECS的指定公网IP地址. 1.输入阿里云官网账号进入首页,如下图: 2.点击进入"管理控制台",如下图: 3.点击"云服务器ECS&q ...

  8. vue for 绑定事件

    vue for 绑定事件 <div id="pro_list" v-for="item in pro_list"> <div class=&q ...

  9. Android Studio下载及使用教程(转载)

    (一)下载及相关问题解决: Android Studio 下载地址,目前最新可下载地址,尽量使用下载工具. Android Studio正式发布,给Android开发者带来了不小的惊喜.但是下载地址却 ...

  10. -bash: /usr/local/bin/react-native: No such file or directory

    执行react-native run-android/run-ios的时候出现 -bash: /usr/local/bin/react-native: No such file or director ...