CTrayIconPosition - 我的托盘图标在哪里?






4.67/5 (29投票s)
一直想知道你的托盘图标在哪里吗?Windows没有提供这样的API。这个类是一个可行的精简解决方案。
引言
这个精简的类使得一项不可能的事情成为可能——它能够检测你应用程序的托盘图标的位置。
问题所在
我是流行的应用程序Tray Helper的作者。该应用程序利用Shell_NotifyIcon
WinAPI函数,并将自己的图标放入系统托盘。很长一段时间以来,我在这方面都没有遇到任何问题,直到……我发现了一个由Joshua Heyer (Shog9)编写的**很棒**的类——CBalloonHelp。Shog9的类能够显示很酷的气球消息,我决定我的应用程序也将显示来自我的托盘图标的气球。在阅读了MSDN和许多WWW网站之后,我发现无法确定我的托盘图标到底在哪里!根本没有这样的API!
两种方法(2+)
- 直接方法:如果最终用户使用的不是MS Windows自带的托盘管理器,而是其他不同的托盘管理器,那么这种方法似乎是完美的。
这种方法由Neal Andrews提出,我将其从VB(源代码)移植到了C++。这种方法的主要思想是,系统托盘使用一个普通的工具栏控件来显示图标(如果你不相信我,可以使用Spy++应用程序检查一下)。找到这个控件的句柄并直接询问图标的矩形区域也很容易。有两个地方需要实现。首先,我们需要找到工具栏控件的句柄。这可以通过枚举系统中的所有窗口并找到类名为
Shell_TrayWnd
的窗口(这是系统托盘的主窗口)来实现。然后,我们枚举托盘的所有子窗口以找到工具栏(类名为ToolbarWindow32
)。一旦我们有了工具栏的句柄,我们就可以查询它当前拥有的图标数量。
//now we check how many buttons is there - should be more than 0 int iButtonsCount = SendMessage(hWndTray, TB_BUTTONCOUNT, 0, 0);
如果图标数量看起来正常(大于0),我们就可以开始考虑如何向该控件查询我们的图标。如果工具栏是我们应用程序的一部分,我们可以直接向它发送TB_GETBUTTON和TB_GETITEMRECT消息。它可能看起来像这样
for(int iButton=0; iButton<iButtonsCount; iButton++) { TBBUTTON buttonData; //this structure will be filled with data about button SendMessage(hWndTray, TB_GETBUTTON, iButton, (LPARAM)&buttonData); }
但是在我们的代码中,这样的消息会失败,甚至可能导致一般保护性故障错误!主要原因是,我们不能将本地分配的
TBBUTTON
结构的指针传递给另一个进程(Windows托盘应用程序的进程)。要解决这个问题,我们需要在托盘应用程序进程内部分配TBBUTTON
结构。然后,我们可以将包含该已分配内存地址的消息发送给工具栏,最后——我们可以将这块内存读回我们的应用程序。代码示例(为便于阅读,已省略错误检查)
BOOL FindOutPositionOfIconDirectly(const HWND a_hWndOwner, const int a_iButtonID, CRect& a_rcIcon) { HWND hWndTray = GetTrayToolbarControl(); //now we have to get an ID of the parent process for system tray DWORD dwTrayProcessID = -1; GetWindowThreadProcessId(hWndTray, &dwTrayProcessID); //here we get a handle to tray application process HANDLE hTrayProc = OpenProcess(PROCESS_ALL_ACCESS, 0, dwTrayProcessID); //now we check how many buttons is there - should be more than 0 int iButtonsCount = SendMessage(hWndTray, TB_BUTTONCOUNT, 0, 0); //We want to get data from another process - it's not possible //to just send messages like TB_GETBUTTON with a locally //allocated buffer for return data. Pointer to locally allocated //data has no usefull meaning in a context of another //process (since Win95) - so we need //to allocate some memory inside Tray process. //We allocate sizeof(TBBUTTON) bytes of memory - //because TBBUTTON is the biggest structure we will fetch. //But this buffer will be also used to get smaller //pieces of data like RECT structures. LPVOID lpData = VirtualAllocEx(hTrayProc, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE); BOOL bIconFound = FALSE; for(int iButton=0; iButton<iButtonsCount; iButton++) { //first let's read TBUTTON information //about each button in a task bar of tray DWORD dwBytesRead = -1; TBBUTTON buttonData; SendMessage(hWndTray, TB_GETBUTTON, iButton, (LPARAM)lpData); //we filled lpData with details of iButton icon of toolbar //- now let's copy this data from tray application //back to our process ReadProcessMemory(hTrayProc, lpData, &buttonData, sizeof(TBBUTTON), &dwBytesRead); //let's read extra data of each button: //there will be a HWND of the window that //created an icon and icon ID DWORD dwExtraData[2] = { 0,0 }; ReadProcessMemory(hTrayProc, (LPVOID)buttonData.dwData, dwExtraData, sizeof(dwExtraData), &dwBytesRead); HWND hWndOfIconOwner = (HWND) dwExtraData[0]; int iIconId = (int) dwExtraData[1]; if(hWndOfIconOwner != a_hWndOwner || iIconId != a_iButtonID) { continue; } //we found our icon - in WinXP it could be hidden - let's check it: if( buttonData.fsState & TBSTATE_HIDDEN ) { break; } //now just ask a tool bar of rectangle of our icon RECT rcPosition = {0,0}; SendMessage(hWndTray, TB_GETITEMRECT, iButton, (LPARAM)lpData); ReadProcessMemory(hTrayProc, lpData, &rcPosition, sizeof(RECT), &dwBytesRead); MapWindowPoints(hWndTray, NULL, (LPPOINT)&rcPosition, 2); a_rcIcon = rcPosition; bIconFound = TRUE; break; } VirtualFreeEx(hTrayProc, lpData, NULL, MEM_RELEASE); CloseHandle(hTrayProc); return bIconFound; }
- 视觉扫描方法:也有其他可能的实现方法。我们可以找到系统托盘的矩形区域(我们在前一种方法中已经做到了),然后手动扫描该区域以寻找我们的图标。这个想法很简单,但实现起来却并不容易。正如你可能猜到的,
Shell_NotifyIcon
在将你的图标添加到系统托盘时,通常会处理你的图标的很多事情。具体做什么取决于很多因素,例如Windows的版本,有时还取决于你的图形模式(颜色数量)。换句话说,如果你要求Shell_NotifyIcon
将你漂亮的32x32像素彩色图标添加到托盘——它可能会以减小的尺寸和颜色数量显示在那里,而且几乎不可能预测它在系统托盘中看起来会是什么样子。因此,尝试搜索你彩色的图标是不明智的。但是有一个简单可靠的解决方案(它几乎可以在所有机器上正常工作!)。何不将你的默认图标更改为纯黑色,在系统托盘中搜索黑色的矩形区域,然后恢复你应用程序的图标?还不相信?好吧,我也曾对此表示怀疑——但它确实效果很好:)
- 同时使用两种方法:直接扫描方法似乎是完美的——但如果用户将默认的托盘应用程序更改为市场上可用的某些第三方软件怎么办?这种情况不太可能发生,但如果你编写的应用程序需要在每台PC上都能工作,你应该考虑这一点。第二种方法(视觉扫描)有可能在第一种方法失败时成功。最终的解决方案很简单——同时使用两种方法——如果一种失败了,就尝试另一种。这里提供的代码让你能够轻松地采用这种方法。
在你的项目中进行使用
我编写了一个精简的类CTrayIconPosition
。如果你想在你的项目中使用它——请按照以下几个简单的步骤进行操作
- 将TrayIconPosition.h和TrayIconPosition.cpp添加到你的项目中。
- 在你计划使用该类的文件中添加
#include "TrayIconPosition.h"
。 - 声明该类的一个变量(在我看来,最好将其设为主对话框窗口的成员变量,或者类似的东西)。
- 将示例项目中的
IDI_BLANK_BLACK
图标复制到你的应用程序中。 - 使用下面描述的API。
CTrayIconPosition API
void InitializePositionTracking(HWND hwndOfIconOwner, int iIconID);
在调用此函数之前,你应该已经将你的图标放入系统托盘。此函数初始化跟踪机制。
int iIconID
- 这是你在使用Shell_NotifyIcon
添加图标到托盘时设置的图标ID。BOOL GetTrayIconPosition(TrackType a_eTrackType = UseBothTechniquesDirectPrefered, Precision a_ePrec = Default);
此函数计算托盘图标的位置,如果找到图标则返回
TRUE
,如果未找到则返回FALSE
。但即使返回值是FALSE
——你也可以使用点值——因为它很可能包含有用的数据。例如,在Windows XP下,你的托盘图标可能会被隐藏——那么此函数的返回值将是FALSE
。但点值将包含系统托盘的左侧、中间部分(在WinXP中,它是隐藏/显示图标按钮)。请注意,调用此函数**可能会**将你的托盘图标更改为黑色——如果你想撤销此效果,请调用RestoreTrayIcon
。请注意
a_eTrackType
参数:它控制类应该如何进行跟踪。允许的值有UseBothTechniquesDirectPrefered
- 类将首先尝试使用直接方法检测你的图标;如果失败,它将对系统托盘进行视觉扫描。UseBothTechniquesVisualScanPrefered
- 与UseBothTechniquesDirectPrefered
类似,但检测顺序是先视觉扫描,然后是直接方法,如果视觉扫描失败。UseDirectOnly
- 不言而喻。UseVisualScanOnly
- 不言而喻。
void RestoreTrayIcon(HICON icon);
恢复由
GetTrayIconPosition
设置的黑色图标。由于我的Tray Helper应用程序中的图标经常更改,我将图标恢复实现在一个单独的函数调用中。如果你的应用程序有一个静态的、始终相同的图标,你可以方便地修改这个类,使其自动调用此恢复函数。void SetDefaultPrecision(Precision newPrecision);
让我通过一个例子来解释这个函数的意思
你每秒调用
GetTrayIconPostion
成员函数很多次。由于此函数会在托盘中设置黑色图标——如此频繁的调用在短时间内可能会显得很糟糕(闪烁)。但通常情况下,托盘图标的位置不会经常改变。这就是为什么CTrayIconPosition
会缓存最后计算的位置,如果你调用GetTrayIconPosition
——它能够返回缓存的值,而不是一遍又一遍地检查。缓存的值只在一定时间内有效——使用此函数,你可以根据需要设置更精确的结果,或不太精确但闪烁较少的结果。可接受的值
CTrayIconPosition::Default
CTrayIconPosition::High
- 缓存位置将在10秒后过期CTrayIconPosition::Medium
- 缓存位置将在30秒后过期CTrayIconPosition::Low
- 缓存位置将在60秒后过期
默认情况下,假定为
High
精度。void Invalidate();
此函数强制下一次调用
GetTrayIconPosition
不使用缓存值。
用法示例
//let's add icon to system tray first NOTIFYICONDATA nid; nid.cbSize = sizeof(nid); nid.hWnd = m_hWnd; //ID of icon - you have to pass this //value to InitializePositionTracking nid.uID = 1; nid.uFlags = NIF_ICON; nid.hIcon = AfxGetApp()->LoadIcon(IDI_YOUR_ICON); Shell_NotifyIcon(NIM_ADD, &nid); //let's initialize tray icon position tracking //second argument it's ID of icon (nid.uID) m_tipPosition.InitializePositionTracking(m_hWnd,1); //ok now let's find out the position of our tray icon: //use m_tipPosition.Invalidate(); //if you want to avoid few-seconds position cashing CPoint ptIcon; BOOL bIconFound = m_tipPosition.GetTrayIconPosition(ptIcon, CTrayIconPosition::UseBothTechniquesDirectPrefered); //GetTrayIconPosition in order to find out position //can (unless UseDirectOnly method is used) //sets a black icon in tray - let's restore it now m_tipPosition.RestoreTrayIcon(AfxGetApp()->LoadIcon(IDI_YOUR_ICON)); //use returned CPoint value here :-D
一些说明:
- 我在Win 98、ME、2000和XP上测试了这个类——在所有这些系统上,它都能正常工作。如果你担心这会暂时将图标变成黑色(在视觉扫描方法中)——我想说的是,在大多数情况下,这是注意不到的。即使在缓慢、过载的机器上,检测位置也只需要眨眼的时间。所以这应该不是问题。
- 正如你所见,为了使用这个类,你必须在项目中添加
IDI_BLANK_BLACK
图标——我知道这不是最好的做法。如果你愿意,你可以编写自己的代码,在运行时创建一个纯黑色的图标,摆脱这个资源。我的目标是展示一个紧凑且能工作的类——你可以随意改进它! - 有些人问我为什么不使用Windows 2000和XP的气球提示功能。如果你问自己同样的问题,那就意味着你不明白这个类的目的——它旨在找出托盘图标的确切位置——显示气球只是一个例子——你当然也可以以完全不同的方式使用它。
- 如果你**有**改进此类使其更有效更可靠的建议——我将非常感谢你的反馈。请在此网站上发表评论,或通过电子邮件联系我:irekzielinski-DEL_THIS@wp.pl。
- 函数
GetTrayIconPosition
返回CPoint
类类型的数据。有些人可能会失望为什么它不是CRect
或RECT
结构。嗯——那是因为我不需要那些——我认为修改此类以返回更有价值的数据会很容易。在当前实现中,GetTrayIconPosition
返回的不是托盘图标的中心点——而是左上角部分。 - 抱歉我的英语——这不是我的母语 :-D
更新历史
2004年12月22日
- 添加了由Neal Andrews提出的直接扫描方法,在此。
2004年5月28日
- 更新了检测黑色图标(
CheckIfColorIsBlackOrNearBlack
)的算法,由Harald提出,在此。