没错,这个函数返回了 TRUE

这篇日志的标题其实应该起《为啥我的气泡提示无效》,不过如果你看过《Windows 编程启示录》中那篇《没错,我们实现了这个功能》短文就大致可以理解为啥我取了这么个标题

周末被叫去加班,自己手头的事已基本已经完结,于是蛋疼地帮忙调查下托盘气泡提示在 XP 下无法显示的原因。那段托盘相关的代码是从闪电邮原先的代码中挪过来,对 NOTIFYICONDATA 进行了一层包装。撇开那些无谓的封装,一个简单的更新气泡提示方法应该是这样的:

NOTIFYICONDATA data;
memset(&data,0,sizeof(data));
data.cbSize = sizeof(NOTIFYICONDATA);
data.hWnd = hwnd; // 继承类窗口句柄
data.uFlags |= NIF_INFO;
data.szInfoTitle = _T("I'm a Title");
data.szInfo = _T("I'm the Content");
data.dwInfoFlags = NIIF_INFO;
data.uTimeout = 10000;
data.uID = 1;
data.uCallbackMessage = WM_USER + 0x1010; // 自定义消息
data.hIcon = hIcon;
Shell_NotifyIcon(NIM_MODIFY, &data);

整个方法很简单,无非就是填充一个 NOTIFYICONDATA 的结构体信息,并调用 Shell_NotifyIcon 而已。但是就是这么简单的代码在 XP 下就可耻地失败了 (函数返回 TRUE,却没有任何显示),反倒在 Win7 下可以正常执行。通过查看 NOTIFYICONDATA 结构体的定义可以知道,其定义在不同系统下是是不一样的:

typedef struct _NOTIFYICONDATAW 
{
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
#if (NTDDI_VERSION < NTDDI_WIN2K)
WCHAR szTip[64];
#endif
#if (NTDDI_VERSION >= NTDDI_WIN2K)
WCHAR szTip[128];
DWORD dwState;
DWORD dwStateMask;
WCHAR szInfo[256];
union {
UINT uTimeout;
UINT uVersion; // used with NIM_SETVERSION, values 0, 3 and 4
} DUMMYUNIONNAME;
WCHAR szInfoTitle[64];
DWORD dwInfoFlags;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
GUID guidItem;
#endif
#if (NTDDI_VERSION >= NTDDI_LONGHORN)
HICON hBalloonIcon;
#endif
} NOTIFYICONDATAW, *PNOTIFYICONDATAW;

这样就可以推测实际上这个方法在不同系统上是有不同的定义和表现的,只有在满足其要求才可能调用成功。google 了一把,果然如此:如果当前系统低于程序编译时制定的平台定义,这个函数虽然会成功实际上却是做了 nothing。坑爹呢,为啥返回 TRUE,为啥连个 LastError 都木有……

正确的做法就是修改编译时指定的 NTDDI_VERSION,即 _WIN32_WINNT。当然也有人指出需要改 WINVER,比如这篇文章:《I can’t get a Balloon Tooltip to work 》。于是索性全改了:以前的工程配置里面指定的 _WIN32_WINNT 竟然是 0×0600,难怪只有我的 WIN7 系统能够出气泡。至于坑爹的 Shell_NotifyIcon 函数内部到底做了什么判断就只能问微软了。这让我想起了 JUCE 里面有个 URL 类:它有个类方法叫做 isWellFormed() 用于判断传入的 URL 是否有效,当时看到这方法觉得很有意思,跳进去看源代码,结果就悲剧了:

bool URL::isWellFormed() const
{
//xxx TODO
return url.isNotEmpty();
}

不过怎么说呢,至少 JUCE 有机会让我们知道他是怎么坑爹的,Shell_NotifyIcon 就只能靠猜了。