有时候,我发现 Visual Studio .NET 中的 Windows 窗体控件缺少我所构建的应用程序必不可少的特定功能。尽管我不是在说这组 Windows 窗体控件是不充分的,但是通用的工具箱不可能满足所有用户的要求。例如,让我们考虑一下 TextBox 控件。显然,此控件必须支持一小组属性和方法。但是,有很大一组扩展属性将非常有用。例如,您可能希望具备如下功能:当控件获得焦点、输入掩码或者(可能)相关的自动完成列表时,背景颜色发生改变。当然,我宁可拥有一个可扩展的轻量控件,而不愿拥有一个具有所需全部功能的过于繁琐的控件。
在 Windows 窗体中,继承只是一个用来构建现有控件的扩展版本的选项。另一个选项涉及到提供程序控件,本月的专栏将讨论这一主题。提供程序控件向现有的控件添加新属性,而无需继承。这就是说,使用外部扩展程序控件向现有的控件提供新属性和行为。在实现这样的扩展时,不会向基础控件添加重量,而是对继承的轻量替代。提供程序控件不会替换继承,而是以多种方式扩展和补充它。
派生新类
假设您希望文本框在获得焦点时更改其背景颜色。一种选择是派生一个新类。图 1 显示了一个简单类,该类从 TextBox 继承并捆绑 GotFocus 和 LostFocus 事件。当该控件获得焦点时,AutoColorTextBox 类改变背景颜色;当 LostFocus 事件被引发时,该控件恢复初始颜色。您可以很方便地向每个文本框分配不同的颜色。您只需将 AutoColorTextBox 类的实例放到窗体上,并设置该控件获得焦点时的背景颜色:
private void Form1_Load(object sender, EventArgs e) { autoColorTextBox1.SelectedBackColor = Color.Cyan; autoColorTextBox2.SelectedBackColor = Color.Yellow; autoColorTextBox3.SelectedBackColor = Color.LightGreen; }
如果您需要自定义控件的行为和外观,则继承是非常好的选择。但是,如果您希望向 TextBox 类添加另一个功能,该怎么办呢?假设您希望文本框只接受数字,并用红色显示所有的负数。图 2 显示了这个新版本的控件。AutoNumericTextBox 类从以前定义的 AutoColorTextBox 继承。只要您在所有文本框中需要这两种功能,继承就是很好的选择。
![](http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/miscuttingedgefig02.gif)
图 2 修改后的控件
但是,如果您需要将扩展作为单个属性,该怎么办呢?由于这些控件是最新设计的,因此,即使您只需要数字模式,也需要加载所有功能(请参阅图 3)。替代方案单独地管理扩展,这与继承的核心思想不一致。使用继承,对象具有类的所有功能。因此,如果您希望能够将 TextBox 类的基本功能与任何可能的扩展(如自动颜色和数字模式)结合在一起,则需要构建以下某个类:TextBox+AutoColor、TextBox+Numeric 或 TextBox+AutoColor+Numeri。对该模式进行缩放以满足实际类的复杂性(和数量),而且您将会了解为什么继承不总是扩展控件的最佳选择。
提供程序
提供程序控件允许您在不新建特殊类的情况下扩展现有的控件。用来执行扩展的代码(GotFocus 和 LostFocus 处理程序)加载到在运行时插入到基类中的外部类。Visual Studio .NET 中优异的设计时支持使得提供程序控件的存在几乎不被注意。
要获得数字代码和自动颜色功能,您需要编写两个提供程序控件;一个用来实现自动颜色,另一个用来实现数字模式。在最后的窗体中,只放置真正需要的提供程序。这样,不会加载不必要的代码。如果您需要组合两个或更多扩展,则可以通过将更多提供程序绑定到同一个控件来完成。
经过证明,提供程序控件在另一个方案中也极其有用。请再次考虑可更改 TextBox 控件背景颜色的扩展。如果您希望将该功能添加到 RichTextBox 和 ListBox 控件中,又该怎么办呢?如果没有提供程序,则只能创建两个新类。
提供程序控件的妙处在于它们允许您扩展多种类型的基本控件。提供程序的最终角色就是将自定义属性添加到现有控件中。扩展属性显示在 Visual Studio .NET 属性网格中。在运行时,Windows 窗体结构为提供程序生成控件,以便真正实现扩展属性。这样做以后,扩展程序控件获得对 extendee 的引用,并且可以检查类型和名称,以及读取和更新状态。
ToolTip 提供程序控件
在讨论如何构建自定义提供程序控件之前,让我们回顾一下 Windows 窗体工具箱附带的预定义扩展程序:ToolTip、HelpProvider 和 ErrorProvider 控件。所有这些都显示在 Visual Studio .NET IDE 的组件栏区域中,如图 4 所示。每个提供程序控件都可以有其自己的一组属性,这些属性影响控件的工作方式。控件特定的属性与添加到绑定控件的扩展无关。
![](http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/miscuttingedgefig04.gif)
图 4 提供程序控件图标
ToolTip 控件列出的属性允许您配置弹出文本的计时,ErrorProvider 控件允许您更改用来表示错误消息的图标并使文本根据需要闪烁。最后,HelpProvider 控件需要使编译的帮助文件工作。
图 5 ToolTip 属性
在向窗体中添加了几个扩展程序控件之后,在窗体中选择任何其他控件并滚至其各自的一组属性。您可能会惊讶地发现,窗体中所有的控件现在都具有一个额外的 ToolTip 属性(请参阅图 5)。网格中该属性的准确名称是“ToolTip on toolTip1”,其中,toolTip1 是扩展程序控件的名称。为文本框设置此属性并构建该项目。窗体上的控件现在应当显示一个工具提示。选择一个按钮并重复上述操作。该按钮也有一个“ToolTip on toolTip1”属性,如果设置了该属性,则当鼠标指针悬停在该按钮上时,会出现一个工具提示。这意味着 ToolTip 扩展程序控件实际上可以扩展窗体上可能出现的任何控件(唯一的例外是另一个 ToolTip 控件)。可出现在窗体上的每个控件都会被自动赋予 ToolTip 属性,如果设置了该属性,则会导致在运行时出现弹出说明。
这样,属于该窗体的所有控件都共享同一个 ToolTip 对象,该对象维护一个控件/标题对的内部表格。像许多其他 Windows 窗体控件一样,ToolTip 控件是针对基于 Win32 窗口的控件构建的包装。Win32 工具提示窗口位于 ToolTip托管对象后面。在实例化 ToolTip 托管对象并为其配置样式和延迟功能时,就会创建该窗口。接着,添加一对内部表格 — 一个用来存储控件/标题关联,另一个用来存储控件/区域对。
如果您熟悉 Win32 ToolTip 控件,您就会知道可以使用单个实例来在不同的屏幕位置显示弹出说明。换言之,ToolTip 控件在 Windows 中的工作方式与 HTML 工具提示显然不同,HTML 工具提示是绑定到个别元素的属性。Visual Basic 6.0 模型类似于 Web 方案。实际上,Visual Basic 6.0 将每个控件提供给 ToolTipText 属性。但是,不要根据其外观判断此行为。Visual Basic 6.0 机制实际上与 Windows 窗体模型没有什么区别。在 Visual Basic 6.0 IDE 中,ToolTipText 属性是 ActiveX 容器添加到任何宿主控件的扩展属性。在这个看似简单的编程模型下面,只创建了一个 Win32 工具提示窗口,该窗口用来显示每个具有非空标题的连续元素的文本。如何获得一个工具提示窗口,以便在鼠标指针停留在给定控件上时显示文本?这很简单 — 使用区域。
工具提示区域是 ToolTip 调用到显示器的矩形区域。当鼠标进入该区域并在其中停留指定的时间后,ToolTip 会将其窗口移到该区域、设置标题并弹出标题。在 Windows 窗体中,还会为每个具有非空标题的控件提供区域表中的一个条目。该区域包含到控件区域的绑定。
HelpProvider 和 ErrorProvider
当焦点位于窗体控件上时,HelpProvider 控件会在用户按 F1 键时进入游戏中。HelpProvider将三个属性添加到每个控件:HelpNavigator、HelpKeyword 和 HelpString. HelpNavigator 确定在用户请求某个帮助时所发生的事情。该属性接受 HelpNavigator 枚举的值,并提供对帮助文件的特定元素的访问。HelpKeyword 指出要搜索的关键字或主题,尽管 HelpString 是通过 ToolTip 显示的,如图 6 所示。HelpString 属性与任何帮助文件无关,并在按下 F1 时显示工具提示。
图 6 某个帮助文本
ErrorProvider 在任何具有错误消息的控件旁边显示可能会闪烁的图标(请参阅图 7)。添加到每个控件的属性与图标的图形样式有关 — IconAlignment 和 IconPadding。在默认情况下,该图标放在控件的右侧并垂直居中对齐。除非您将 IconPadding 设置为非零值,否则在控件的边界和图标之间不会保留空白。
图 7 错误图标
ErrorProvider 控件向每个控件的属性网格添加第三个条目 — Error。Error 属性包含该控件将显示的错误信息。与在运行时很少确定的工具提示不同,错误信息是动态属性。这显示出扩展程序控件和继承类之间的重要区别。实际上,添加到每个控件的属性(即,Error message)不是可通过编程方式设置的新属性。您可以在设计时设置扩展属性,这是由于 Visual Studio .NET 运用了此技巧,而不是由于实际属性是向控件中动态添加的。以下代码不进行编译,其原因显而易见 — Error 属性不是 TextBox 编程接口的一部分,但您可以在设计时设置它:
txtAge.Error = "Must be > 18";
因此,该如何动态设置错误信息或工具提示标题呢?这可以通过在扩展程序控件上使用某个方法来完成。上一行代码必须按如下方式重写:
ErrorProvider1.SetError(txtAge, 18);
使用几乎相同的语法,在运行时设置 ToolTip 文本属性:
toolTip1.SetToolTip(txtAge, "Enter a major age");在这背后是否有某种模式?扩展程序控件必须为其添加到 extendee 的每个 Xxx 属性提供一对 GetXxx/SetXxx 方法。使用这些方法,控件可以在运行时访问这两个属性。类似方法的原型如下所示:
public string GetError(Control control); public void SetError(Control control, string value);
在这些签名中,字符串类型根据扩展属性的实际类型改变。
总之,扩展程序控件非常重要,其原因有两个:它们提供的替换模型可以扩展不支持类继承的现有控件。第二,巧妙地使用扩展程序控件,可以大大地限制需要为每个窗体编写的代码数量。扩展程序控件必须基于哪个公用基础来构建其功能?必须基于 IExtenderProvider 接口。
IExtenderProvider 接口
IExtenderProvider 只定义一种方法 — CanExtend。该接口按如下方式定义:
public interface IExtenderProvider { bool CanExtend(object extendee); }
Visual Studio .NET 调用 CanExtend 以确定容器中的哪些对象应当接收扩展程序属性。提供扩展程序属性的任何组件必须实现 IextenderProvider:
public class SimpleTextBoxExtender : Component, IExtenderProvider { }
除了扩展程序实现 IExtenderProvider 接口这一事实以外,它主要还是一个控件。因此,除了实现该接口以外,您必须让其继承控件的基本功能。但是,如果您从 Control 继承它,则该控件就不能放在组件任务栏区域中 — 只能放在窗体上。为了让其显示在组件任务栏中,请将 Component 用作基类。但是,在这两种情况下,扩展程序的总体行为不会发生太大变化。
必须用 [ProvideProperty] 属性标记扩展程序提供程序类。ProvideProperty 属性类的构造函数使用两个参数。第一个参数是要添加的属性名,第二个参数是要向其提供属性的对象的类型。
以下代码片断定义了一个扩展程序,该扩展程序向窗体上的所有文本框添加 SelectedBackColor 属性:
[ProvideProperty("SelectedBackColor", typeof(TextBox))] class SimpleTextBoxExtender: Component, IExtenderProvider { }
请注意,还可以将该属性声明为几乎所有组件的扩展属性 — 在属性中使用 IComponent 或 Control,而不使用 TextBox。在本例中,该实现通常包括一些这样的功能:只需通过特定类别的控件即可使其有用。
您实现 CanExtend 方法,以便它针对扩展程序要为其添加属性的每个控件返回“true”。下面是简单 TextBox 扩展程序可能的实现:
public bool CanExtend(object extendee) { return (extendee is TextBox); }
类似的实现还捕捉任何从 TextBox 类(包括我以前构建的自定义类)继承的控件。图 8 显示了非常简单的扩展程序控件的源代码。它在控件获得焦点时实现背景颜色更改。特别是,扩展程序定义 GetSelectedBackColor 和 SetSelectedBackColor 方法对。它们可以由任何文本框调用,从而通过编程方式获得或设置背景颜色。另外,该控件具备公共属性 SelectedBackColor,该属性出现在属性网格中并提供默认颜色。请注意,如果您不使用 get/set 访问器显式实现该属性,它就不出现在属性网格中:
private Color m_SelectedBackColor; public Color SelectedBackColor { get {return m_SelectedBackColor;} set {m_SelectedBackColor = value;} }
如果您在示例应用程序中使用该控件,它就会很好地工作。但是,在按这种方式对该控件进行编码时,该控件将会过于简单并且没有太多的功能。主要缺点在于背景颜色对于所有的文本框都是唯一的。而且,挂钩事件是需要更多关注的难以处理的操作。尝试使用带有扩展程序的任何派生文本框,您将看到这个混淆结果。重要的扩展程序需要维护由绑定控件组成的表格并分别存储设置。
编写自定义扩展程序控件
让我们增强这个简单的 TextBox 扩展程序,以便为每个 extendee 提供更好的支持。扩展程序现在将添加两个属性 — 背景色和前景色(请参阅图 9)。
每个 extendee 控件都被分配给按如下方式构造的信息类:
public class TextBoxInfo { public Color SelectedBackColor; public Color OldBackColor; public Color SelectedForeColor; public Color OldForeColor; public bool EventsWired; }
该类不但跟踪要使用的颜色,而且跟踪要恢复的颜色,并对 GotFocus 和 LostFocus 事件仅绑定一次,这是由 EventsWired 属性指示的。Get/Set 对的代码变得有点复杂。让我们先处理 Get 访问器:
public Color GetSelectedBackColor(Control control) { // Retrieve related info TextBox t = (TextBox) control; TextBoxInfo info = (TextBoxInfo) Extendees[t]; return info.SelectedBackColor; }
控件参数引用实际的 extendee 控件 — 在本例中为 TextBox。在获得了对基础控件的引用之后,还可以基于可访问的名称或任何其他属性筛出控件。将控件引用用作访问哈希表和检索相应 TextBoxInfo 结构的关键字。在此之后,返回要使用的背景色易如反掌。
谁正在哈希表中创建条目,以及何时创建?其中一个属性的 Set 访问器负责在哈希表中插入控件。如果类似的条目已经存在于哈希表中,将不会添加控件。图 10 显示了 SetSelectedBackColor 方法的源代码。
扩展 ToolTip 扩展程序
ToolTip 类被密封并且不能被继承,因此不能添加更多的功能(如气球样式)。在 Windows XP 和更新的操作系统中,ToolTip 窗口已被赋予 TTS_BALLOON 样式,以便在气球样式弹出窗口中显示标题。首先,从现有的 ToolTip 提供程序派生新扩展程序类并为气球样式添加支持,这看上去是相对简单的任务。您遇到的第一个问题就是 ToolTip 类是密封类,即它不再可继承。但是,通过使用聚合(而非继承),您可以设计自定义包装扩展程序,以便利用 ToolTip 类的基础功能。
其思路在于在扩展程序的构造函数中创建 ToolTip 类的新实例,并在新的 ToolTip 属性的 get 和 set 访问器中调用 GetToolTip 和 SetToolTip 方法。
[ProvideProperty("MyText", typeof(TextBox))] public class MyToolTip : Component, IExtenderProvider { private ToolTip _toolTip; public MyToolTip() { toolTip = new ToolTip(); } }
MyToolTip 类嵌入 ToolTip 类的动态创建的实例作为其私有属性,并公开 MyText 扩展属性。此属性的实现通过针对初始 ToolTip 类实现文本属性来传递:
public string GetMyText(Control control) { return _toolTip.GetToolTip(control); } public void SetMyText(Control control, string caption) { // TODO :: Enter your changes... _toolTip.SetToolTip(control, caption); }
通过实现此代码,可以完成自己的自定义扩展程序,并使其在现有 ToolTip 组件的上方工作。扩展 ToolTip 的一个很好的理由是使用自定义的 ToolTip 控件。Windows 窗体 ToolTip 不支持气球和多行样式,只能通过自定义 ToolTip 扩展程序添加这些功能。不幸的是,在三个内置扩展程序中,您需要的扩展程序(即 ToolTip 组件)是密封的,并且不能被继承。因此,您必须采用基于控件聚合的方法。
然而,在实现完全自定义 Windows 窗体 ToolTip 的过程中,主要的障碍却不在这里。ToolTip 控件从 Component(而非 Control)继承。对于哪种方式更好这一问题存在一些争论。如果您从 Component 继承某个类,该类的行为方式类似于控件栏组件,这对于扩展程序非常适合。如果从 Control 派生该类,结果组件无法放在 IDE 栏区域中。然而,它将从基类继承 Handle 属性。
Handle 属性表示基础 Win32 控件的 HWND句柄。由于 Windows 窗体在很大程度上基于 Win32 控件,因此当 Control 对象被实例化时,Windows 窗体会创建一个窗口。要自定义控件的外观和行为,您需要采用 Win32 样式和消息。如果您不能访问基础窗口句柄,则如何设置它们?
ToolTip 托管控件确实有一个被定义为 IntPtr 成员的 Handle 属性。回忆一下,IntPtr 是用来映射 Win32 句柄(如 HWND、HGLOBAL 或 HKEY)的 .NET Framework 类型。困难在于如下事实,ToolTip 的句柄属性被声明为私有属性,所以会因其保护级别而变得不可访问。ToolTip 类可以使用聚合功能进一步扩展,但是,能够实际添加的扩展仅限于在不知道基础 ToolTip 窗口的 HWND 的情况下所能实现的任何事情。
小结
扩展程序控件表示控件继承的替换路径。通过编写扩展程序,可以实现两个目标。首先,您可以实现更细级别的粒度,并决定要向类中添加哪些功能,而不必创建新类并在运行时继承功能。第二,您可以使用单个扩展类向许多控件类型中添加同样的功能。您可以使用唯一扩展程序向文本框和组合框添加属性;如果您使用继承,这将需要两个不同的类。另外,尽管继承代表一种纯粹的编码方法,扩展程序仍尝试在设计时消除一些必需的负担。扩展程序减少了编写和提升声明性更强且基于属性的编程模型所需的代码量。
当然,请记住,扩展程序不替换继承,而是对其进行补充并有助于使编程工具集比以前更加丰富。