气球工具提示控件






4.79/5 (34投票s)
一个实现IExtender接口的气球工具提示控件。
引言
大家都知道ToolTip控件(程序员用的那种!!),它自Win95起就包含在Common Controls 4.0版本中(我第一次看到的是这个版本,没见过Win3.11)。从那时起,控件本身经过了多次修改和增强,过了一段时间,WinXP诞生了,Common Controls 6.0版本也随之出现,其中就包含了气球工具提示控件,这也是本文的主题。
创建气球工具提示或实现IExtender都不是新主题,甚至都不难,但同时具备这两种功能可能引起一些关注,而这正是本文讨论的内容:如何以一种像使用原生.NET ToolTip控件一样简单的方式,将两者的优点结合起来。
在搜索过程中,我发现了一篇关于气球工具提示控件(实际上是关于ToolTip控件,及其各种形态和用途)的优秀文章。这篇文章(也可以在CodeProject上找到)对我来说是一个很好的参考,也是使用气球工具提示控件的一个很好的、生动的例子,但其中描述的控件存在复杂性(并非真正意义上的复杂,而是需要通过编程操作),这促使我研究IExtender并完成这项工作。
背景
阅读本文将让你对这里讨论的基本思想有一个大致的了解,但要完全理解每个概念,你必须对Win32 API调用及其用途有所了解。你不必成为专业人士才能正确理解,也不应该是新手(你不必去翻阅最近的编程书籍来弄清楚Win32 LPTSTR是什么,或者如何在非托管堆中分配一些字节)。我知道这是.NET的第一个承诺——能够避免使用Win32 API,但如果你不知道,API并没有过时,也不会很快过时,所以你必须让自己习惯在自己的代码中使用它们。
使用代码
经过这些理论性的冗谈,我们可以开始动手实践了。
IExtenderProvider 接口
IExtender接口的理念是为另一个控件提供服务,将你编写的功能赋予其他类,而不是赋予你自己的类。你在代码中解决的问题呈现给其他类使用,而不是像所有普通类那样由你自己的类直接使用。换句话说,你在其他组件自己的领域解决它们的问题。
这个接口提供了一个名为CanExtend的方法,它期望接收一个对象作为参数,并返回一个布尔值作为响应。传递给此方法Object代表设计时选定的控件,而布尔值结果指定实现此方法的类是否应为此Object提供服务。这就是简化的全部故事。
对于几乎所有实现IExtender接口的控件,CanExtend方法应为它应该扩展的每个控件返回true,除了它本身以及任何可能不合逻辑的控件。在我们的例子中,除了这个控件和Form控件外,其他所有控件都非常欢迎获得服务。
public bool CanExtend(object extendee)
{
if(extendee is control && !(extendee is BalloonToolTip)
&& !(extendee is From))
{
return true;
}
return false;
}
基于此调用的结果,VS设计器或任何其他第三方设计器决定是否向该控件提供指定服务。因此,当你选择一个控件时,设计器会在设计时调用此方法,将选定的控件作为参数传递给它,如果返回true,则提供的功能将作为新属性出现在选定控件的属性页(以及代码中),就像它原本在原始控件代码中实现一样。
ToolTip控件
阅读MSDN时,ToolTip控件有点令人困惑,要正确理解它,你应该区分两个概念:ToolTip控件本身和它支持的工具。前者,ToolTip控件,是绘制文本并按指示行为的父窗口,而工具是包含要绘制文本并控制提示如何显示的逻辑结构。因此,一个ToolTip控件可以包含任意数量的工具,每个工具对应一个你选择的控件。
就是这样。你创建ToolTip控件,它独立于其关联的工具,并拥有自己的通用属性(宽度、颜色和显示周期)。然后,你创建任意数量的工具,并将这些工具添加到ToolTip控件中。
与其他任何控件一样,通过向CreateWindowEX API函数提供正确的参数,就可以创建一个ToolTip控件,并返回一个句柄。有关参数使用及其含义的信息,你可以参考MSDN并获得每个参数的完整描述,但出于本文的考虑,如下所示:
IntPtr toolwindow;
Toolwindow = CreateWindowEx(0, “tooltips_class32”, string.Empty,
WS_POPUP | TTS_BALLOON | TTS_NOPREFIX | TTS_ALWAYSTIP,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, IntPtr.Zero, 0, 0, 0);
其中最重要的常量是TTS_BALLOON,它强制函数创建一个具有卡通风格气球的ToolTip控件。第二个参数指定要创建窗口的类(在本例中是ToolTip类),对于父参数,我们传递一个零指针,表示该窗口没有父窗口(它是一个弹出窗口)。最后,返回值是一个代表我们的ToolTip控件的句柄,在类的生命周期内有效。
其他细节
目前,我们已经准备好一个ToolTip控件供使用,但如何与之通信呢?从.NET的角度来看,这个ToolTip控件是我们组件的核心,但从Win32的角度来看,它只是一个窗口,要操作它,我们必须遵循基于消息的Win32通信规则,通过SendMessage API函数。
ToolTip控件定义了一系列消息,作为命令发送给它,以指定其外观和行为。这些消息列在MSDN中,我们的控件并不使用所有这些消息,只使用对我们的实现有用的那些。
- TTM_ACTIVATE:启用和禁用ToolTip控件本身。
- TTM_ADDTOOL:将一个工具添加到ToolTip控件。
- TTM_DELTOOL:从ToolTip控件中删除一个工具。
- TTM_SETTITLE:为ToolTip气球窗口添加标题。
- TTM_SETTIPBKCOLOR:为ToolTip气球窗口设置自定义背景颜色。
- TTM_SETTIPTEXTCOLOR:为ToolTip气球窗口文本设置自定义前景颜色。
- TTM_SETDELAYTIME:设置ToolTip的行为时间。
- TTM_UPDATETIPTEXT:更新ToolTip气球窗口的文本。
- TTM_SETTOOLINFO:将指定的工具与指定的TOOLINFO结构关联。
- TTM_GETTOOLINFO:获取与指定工具关联的TOOLINFO结构。
这个总结列表绝不是ToolTip控件使用的消息的参考。关于所列出的以及未列出的消息的详细描述,请参阅MSDN文档。大多数消息的名称都有意义,不需要进一步解释。
TTM_ADDTOOL消息是我们向ToolTip控件添加工具的关键,正如我之前所说,ToolTip控件与其关联的工具是分开的。ToolTip控件负责管理工作,并根据指定的设置显示气球窗口,而其关联的每个工具都是一个TOOLINFO结构,其中包含显示气球窗口所需的信息。
看看这里的TOOLINFO结构
typedef struct tagTOOLINFO { UNIT cbSize; UNIT uFlag; HWND hwnd; UNIT_PTR uId; RECT rect; HINSTANCE hinst; LPTSTR lpszText; #if(_WIN32_IE >= 0x0300) LPARAM lparam #endif }TOOLINFO
此结构代表ToolTip控件中包含的工具。cbSize成员必须指定此结构的大小(以字节为单位),uFlag成员控制ToolTip的显示,hwnd是控件的句柄(你窗体设计中的控件)包含此工具,rect是保存包含此工具的控件的ClientRectangle的成员,lpszText是包含要在ToolTip气球窗口中显示的字符串的缓冲区。
离开托管世界
如果你必须处理像这些Win32 API调用这样的非托管代码,我将向你介绍一个好朋友,System.Runtime.InteropServices.Marshal类。任何离开托管世界的旅行,这个坚强的家伙都是你最好的朋友。Marshal类中有许多方便实用的静态函数,可以解决你与非托管代码相关的几乎所有问题,其中一个函数展示了如何将托管版本的TOOLINFO结构转换为内存指针,以传递给SendMessage API函数。这个函数使用非托管语言,不能接受你的托管TOOLINFO对象,所以你必须稍微低级一点来处理这种情况,方法如下:
// first we have to construct a managed toolinfo structure
toolinfo tf = new toolinfo;
// now specify the size of this structure in the size member
tf.size = Marshal.SizeOf(typeof(toolinfo));
// specify how the balloon window should be displayed
tf.flag = TTF_SUBCLASS | TTF_TRANSPARENT;
// associate the structure with the target control (the one on your form)
tf.parent = targetcontrol.Handle;
// specify the text you want to be displayed
tf.text = “Tooltip text to be displayed”;
// we need a temporary pointer to hold our unmanaged copy of TOOLINFO object
IntPtr tempptr;
// now allocate enough memory to hold this structure in the unmanaged heap
tempptr = Marshal.AllocHGlobal(tf.size);
// copy the content of our filled managed
// TOOLINFO object to the newly allocated memory space
Marshal.StructureToPtr(tf, tempptr, false);
// now send a message to our ToolTip control that we have a tool to be added
// and remember that our ToolTip is just a pointer return by CreateWindowEx
SendMessage(tooltipptr, TTM_ADDTOOL, 0, tempptr);
// now our tool have been added to the ToolTip control,
// and we have to clean out what we did
Marshal.FreeHGlobal(tempptr);
对于那些开始挠头(问这到底是什么?)的人,我只想说,解释如何处理非托管代码超出了本文的范围。任何在Marshal类或ToolTip常量和消息中出现的东西,都在注释中简要解释了,如果你还没明白,你必须参考MSDN(如果你还是不明白,那么要么我写得不好,要么你读得不好)。
我们和IExtender的工作完成了吗?
答案是,还没。还有一个小细节还没有讨论。IExtender接口为其他类提供了扩展属性,但实现IExtender的类必须用ProviderProperty属性标记。这个属性和IExtender一起完成了这项任务。此属性的构造函数接受两个参数:一个字符串,指定你的类将提供给其他组件的属性名称,以及此扩展属性的接收者类型。
[ProvideProperty(“BalloonText”, typeof(Control))]
public class BalloonToolTip : System.ComponentModel.Component,
IExtenderProvider
{
...
}
添加此属性后,当我们向设计器添加控件时,窗体上的每个受支持的控件都将有一个新属性添加到其属性列表中,该属性的名称是ProviderProperty属性(BalloonText)中指定的文本。
现在我们已经标记了类的提供属性名称和接收者类型,但从代码角度来看,这仍然不够。在我们的代码中,必须有一对与我们的扩展属性同名的Get/Set函数。换句话说:我们将“BalloonText”指定为新属性的名称,并且我们必须提供“GetBalloonText”和“SetBalloonText”函数。
Get函数是一个返回字符串(惊讶?!)的函数,该字符串是传递给它的控件的字符串。此参数必须与ProvideProperty属性中的接收者类型相同。
public string GetBalloonText(Control parent)
{
... // return the string associated with the tool
// that have its parent equal to the passed control.
}
Set函数是一个void函数(也惊讶?!),它期望接收两个参数:你想为其添加属性值的控件,以及要添加的字符串值。
public void SetBalloonText(Control parent, string value)
{
... // Add the passed string to a new tool
// with its parent equal to the passed control.
}
这些函数在代码中不以普通函数的形式出现,而是作为扩展它们的控件的属性出现,所以不要混淆(毕竟,这就是IExtender应该做的)。
最后要说的是,如何实现这些函数取决于你。你可以使用Hashtable来存储值,通过控件包含的工具正确获取,使用某种索引的数组来存储这些值,甚至可以编写一个全新的类来完成这项工作,但这取决于你,我选择了Hashtable选项。
哈希表是存储键/值对集合的地方,在我们的例子中,我们已经有了这些键/值对。它是我们的控件/属性对。对于每个控件,我们都有一个唯一的字符串作为其BalloonText值,所以我使用了一个哈希表来存储这些信息。控件是键,其关联的“BallonText”是值。
对于Get函数,没有什么可说的了,但对于Set函数,这里有一个
public void SetBalloonText(Control parent, string value)
{
if(value == null)
value = string.Empty;
if(value == string.Empty)
// the user delete our property value,
// so he don’t want our service
{
hashtable.Remove(parent);
// remove our pair from the collection
... // create a toolinfo object, assign its parent
// member the handle of the passed control
// and send a message with TTM_DELTOOL
}
else
// the user has assigned our property a value,
// so he want our service
{
if(hashtable.Contains(parent))
// check whether we have added the control before or not
{
// if we did, then the user has just modified
// the property text, so update
// the hashtable and the tool
hashtable[parent] = value;
...
// create a toolinfo object, assign its parent member
// the handle of the passed control
// and send a message with TTM_UPDATETIPTEXT
}
else
{
// the user assign a new value to the property,
// so add it to the hashtable and add a new tool
hashtable.Add(parent, value);
...
// create a toolinfo object fill its
// values and send a message with TTM_ADDTOL
}
}
}
恭喜,任务圆满完成!
我在控件中添加了几个属性,但这里没有提到,它们太简单了,无需解释。我尽量在代码示例中注释了任何有趣或重要的点。
关注点
如果你查看代码示例,你可能会想,为什么我要为目标控件的Resize事件添加一个新的EventHandler?
简单来说,如果你省略了这一点,BalloonToolTip将无法识别你的控件的新尺寸,并且不会在设计时添加到控件的原始客户矩形之外的所有客户矩形上激活。因此,控件尺寸的任何变化都必须与该已调整大小的控件关联的Win32 ToolTip工具一起更新,而哪里比在Resize事件中更适合做这件事呢?
你可能还会注意到,我没有为目标控件的MouseHover添加任何EventHandler。这是因为我在创建TOOLINFO结构时使用了TTF_SUBCALSS标志,如果你参考MSDN,你会发现这个标志强制Win32 ToolTip控件本身添加EventHandler并免费执行任何必要的管理工作!
希望本文对您有所帮助。