上一篇文章讲述了自定义Qt托盘,不过不是使用QSystemTrayIcon这个类,而是我们自己完全自定义的一个类,我们只需要处理这个类的鼠标hover、鼠标左键点击、鼠标右键点击和鼠标左键双击,就可以完全模拟出qq的托盘样式来。文章的最后我也是提供了一个demo的下载链接,那是一个可以完全运行的demo,处理了鼠标hover事件,并模拟出了鼠标离开和进入事件,这一节我将一步一步讲解怎么实现一个完美的托盘,包括托盘菜单的显示、托盘tooltip和托盘hover时的弹框显示。

看本片文章之前,同学们最好把Qt之自定义托盘文章读一读,这篇文章中有win32的几个api的讲解,虽然不细致,但是讲到了他们的作用,并说明了一些用法。

在本篇内容讲解之前,我先贴一段代码,也算是对上一届内容的回顾吧,这个接口是QAbstractNativeEventFilter类的,该接口如果要处理app的消息,需要使用qApp这个指针把CSystemTrayIcon对象注册下,具体代码上一节中有介绍,这里我就不多说啦。

  1. bool CSystemTrayIcon::nativeEventFilter(const QByteArray & eventType, void * message, long * result)
  2. {
  3. if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG")
  4. {
  5. MSG * pMsg = reinterpret_cast<MSG *>(message);
  6.  
  7. if (pMsg->message == WM_TRAYNOTIFY)
  8. {
  9. switch (pMsg->lParam)
  10. {
  11. case WM_MOUSEMOVE:
  12. m_PTrayPos.OnMouseMove();
  13. break;
  14. case WM_MOUSEHOVER:
  15. {
  16. HandleMouseHover();
  17. }
  18. break;
  19. case WM_MOUSELEAVE:
  20. {
  21. HandleMouseLeave();
  22. }
  23. break;
  24. case WM_LBUTTONUP:
  25. {
  26. TrayActivateSlot(QSystemTrayIcon::Trigger);
  27. }
  28. break;
  29. case NIN_BALLOONUSERCLICK: //用户单击气泡处理
  30. {
  31.  
  32. }
  33. break;
  34. case WM_LBUTTONDBLCLK:
  35. {
  36. emit DblClickTray();
  37. }
  38. break;
  39. case WM_RBUTTONUP:
  40. {
  41. m_MenuPopPos = QCursor::pos();
  42. emit ShowPopupWidget(m_MenuPopPos, false);
  43. m_Menu->show();
  44. *result = ;
  45. }
  46. break;
  47. }
  48. }
  49. }
  50.  
  51. return false;
  52. }

这个本地事件过滤器,可以处理经过这个app的所有事件,因此他可以处理鼠标移动到托盘上的消息,有了hover这个消息,我们自己就可以模拟出enter和leave这两个消息了(enter和leave消息windows托盘区域没有提供),其他鼠标事件都是可以直接拿到,后边只需要处理我们自己的具体业务。

一、菜单

一个完美的托盘,往往都有右键菜单,而右键菜单也是托盘的一项重要功能,如果想实现自定义的托盘菜单,请看文章qt之菜单项定制,这篇文章中讲述到了自定义菜单,应该可以满足大多数人的需求,起码实现360或者电脑管家那样的右键菜单是没有问题。

上边给出的链接就可以实现一个自定义并且美观的菜单项,接下来,我主要说下右键菜单显示的问题,首先我说明一个问题,右键菜单显示的位置应该是我们右键点击的位置,我强调这句话的原因是后边我们讲解鼠标hover弹框时,会和右键菜单有所区别。Qt的菜单也是一个窗口,他继承自QWidget,只不过菜单含有Qt::Popup属性,当他失去焦点的时候,就会自动隐藏。

鼠标右键在托盘区域点击右键,我们响应WM_RBUTTONUP消息,然后show出右键菜单,这个时候我们就需要做一件事情,必须保证我们自己显示的右键菜单在屏幕内,关于这个我问题,我也不多说,一切看代码,代码逻辑也比较简单,首先把菜单移动到鼠标右键点击的位置,然后判断鼠标鼠标是否在界面内,如果需要移动的话,水平移动就移动菜单的宽度,垂直方向就移动菜单的高度,具体怎么移动需要判断窗口的那个边出屏幕。

说了这么多,其实修正代码也比较简单,如下:

  1. QPoint MenuWholePos(const QWidget * widget, const QPoint & proposal)//获取能完全显示菜单的位置
  2. {
  3. QRect wRect = widget->rect();
  4. if (QDesktopWidget * desktop = qApp->desktop())
  5. {
  6. QRect rect = desktop->screenGeometry(desktop->primaryScreen());
  7. wRect.moveTo(proposal);
  8.  
  9. if (rect.contains(QPoint(wRect.left(), )) == false)
  10. {
  11. wRect.translate(widget->width(), );
  12. }
  13.  
  14. if (rect.contains(QPoint(wRect.right(), )) == false)
  15. {
  16. wRect.translate(-widget->width(), );
  17. }
  18.  
  19. if (rect.contains(QPoint(, wRect.bottom())) == false)
  20. {
  21. wRect.translate(, -widget->height());
  22. }
  23.  
  24. if (rect.contains(QPoint(, wRect.top())) == false)
  25. {
  26. wRect.translate(, widget->height());
  27. }
  28. }
  29.  
  30. return wRect.topLeft();
  31. }

在接受到QEvent::Show这个消息的时候,我们把窗口移动到正确的位置,这样一个完美的右键菜单就完成啦。

二、托盘信息

说到托盘信息,那么就得说说NOTIFYICONDATA这个结构,这个结构中保存了托盘的基本信息,包括托盘图标、托盘tooltip、托盘句柄和托盘关注消息id等一系列成员,这一节Qt之自定义托盘中讲到了怎么创建和删除一个托盘图标,具体的怎么修改其他信息我也在这里大概的说下,因为NOTIFYICONDATA结构的百度百科说的已经非常详细,我在这儿只做大概描述。

1、修改托盘图标

  1. HICON CSystemTrayIcon::CreateIcon()
  2. {
  3. const HICON oldIcon = m_TrayHIcon;
  4. const QIcon icon = m_TrayIcon;
  5.  
  6. if (icon.isNull())
  7. {
  8. return oldIcon;
  9. }
  10. const int iconSizeX = GetSystemMetrics(SM_CXSMICON);
  11. const int iconSizeY = GetSystemMetrics(SM_CYSMICON);
  12. const QSize size = icon.actualSize(QSize(iconSizeX, iconSizeY));
  13. const QPixmap pm = icon.pixmap(size);
  14. if (pm.isNull())
  15. {
  16. return oldIcon;
  17. }
  18. m_TrayHIcon = qt_pixmapToWinHICON(pm);
  19.  
  20. return m_TrayHIcon;
  21. }
  1. m_NotifyIconData.hIcon = CreateIcon();
  2.  
  3. m_ToolTips = QStringLiteral("");
  4.  
  5. if (!m_ToolTips.isNull())
  6. qStringToLimitedWCharArray(m_ToolTips, m_NotifyIconData.szTip, sizeof(m_NotifyIconData.szTip) / sizeof(wchar_t));
  7.  
  8. Shell_NotifyIcon(NIM_MODIFY, &m_NotifyIconData);

修改托盘图标主要步骤就是构造NOTIFYICONDATA结构,然后把uFlags设置为NIF_ICON,使hIcon字段有效,我们在讲QImage处理好的图片句柄传递给hIcon,调用Shell_NotifyIcon接口修改托盘。

2、修改托盘tooltips

修改托盘提示信息其实和修改图标是已给道理,首先需要搞清楚修改那个托盘的提示信息,然后在设置uFlags标志,并重置NOTIFYICONDATA结构的具体成员信息,最后调用shell接口修改托盘,代码我就不粘贴了

三、托盘hover窗口

托盘hover时所弹出的框,主要用于显示未接受的消息,可以快速的浏览用户消息,并响应用户的交互,为了和鼠标右键菜单有所区分,在本小节中我把托盘有消息时hover所弹出的界面统称为hover弹框。

1、首先是根据ui设计师的要求,定制好美观的托盘hover弹框,这个弹框一般都包含有消息项,类似于qq的好友消息,这个窗口应该需要支持和我们自定义的托盘类交互的能力,并保持和托盘图标闪烁同步,比较图标闪烁那就说明有消息,进而会出现鼠标hover时,弹出未接消息提示框

2、在托盘菜单发出需要显示hover窗口时,我们把弹框显示出来

3、在第一节菜单内容中,我重点提到了菜单显示位置的问题,hover弹框也存在这个问题,那么我首先先解释下这个hover弹框的规则,我下边的规则都是基于任务栏是在屏幕底下时发生的。

  • 托盘图标未在溢出区:hover弹框的中心位置x坐标和托盘图标的中心位置x坐标一样,bottom值和任务栏的top值一样
  • 托盘图标在溢出区:hover弹框的中心位置x坐标和托盘图标的中心位置x坐标一样,bottom值和任务栏的top值一样

4、如果任务栏在屏幕的顶部、左侧和右侧,都是类似的处理方式

下边是我自定义窗口的Show函数代码,参数pos是托盘图标的中心位置。

  1. void CMessagePopupWidget::Show(const QPoint & pos)
  2. {
  3. m_TrayIconVerCenter = pos;
  4. m_CanHide = false;
  5. QRect wRect = this->rect();
  6. if (QDesktopWidget * desktop = qApp->desktop())
  7. {
  8. QRect rect = desktop->screenGeometry(desktop->primaryScreen());
  9. wRect.moveTo(m_TrayIconVerCenter);
  10.  
  11. switch (MissionToolBar())
  12. {
  13. case :
  14. {
  15. int missionHeight = MissionToolHeight();
  16. QPoint pos(wRect.topLeft().x() - wRect.width() / , missionHeight);
  17. move(pos);
  18. }
  19. break;
  20. case :
  21. {
  22. if (rect.contains(QPoint(wRect.right(), )) == false)
  23. {
  24. wRect.translate(-this->width(), );
  25. }
  26. QRect r = desktop->availableGeometry(desktop->primaryScreen());
  27. move(wRect.topLeft() + QPoint(-(m_TrayIconVerCenter.x() - r.width()), -wRect.height() / ));
  28. }
  29. break;
  30. case :
  31. {
  32. QRect r = desktop->availableGeometry(desktop->primaryScreen());
  33. QPoint pos(wRect.topLeft().x() - wRect.width() / , r.height() - wRect.height());
  34. move(pos);
  35. }
  36. break;
  37. default:
  38. {
  39. int missionWidth = MissionToolWidth();
  40. move(wRect.topLeft() + QPoint(missionWidth - m_TrayIconVerCenter.x(), -wRect.height() / ));
  41. }
  42. }
  43. }
  44.  
  45. show();
  46. }

上边的代码是不是也是不是比较简单啊,呵呵,其实还好。关于上述怎么获取任务栏高度和宽度的方法我就不贴代码了,有兴趣的同学,自行百度。

接下来,我要在补充一下,怎么获取任务栏图标的坐标

1、首先我说下Shell_NotifyIconGetRect这个接口,微软明确说明了这个接口只有在win7后才开始提供,所以如果自定义托盘要在xp系统和win7(win10)系列系统上跑,那么就需要做兼容性处理。

2、下面是一个判断接口,判断指定的动态库是否包含指定接口

  1. void * common::LibraryContainsInterface(LPWSTR lpDesc, LPCSTR pGuid)
  2. {
  3. HINSTANCE hinstLib = ::LoadLibrary(lpDesc);
  4. if (hinstLib != nullptr)
  5. {
  6. void* proc = GetProcAddress(hinstLib, pGuid);
  7. return proc;
  8. }
  9. FreeLibrary(hinstLib);
  10.  
  11. return NULL;
  12. }

3、如果你的系统是win7,包含之后的系统,那么你获取托盘图标的代码看起来像下面这样

  1. static PtrShell_NotifyIconGetRect Shell_NotifyIconGetRect
  2. = (PtrShell_NotifyIconGetRect)LibraryContainsInterface(L"shell32", "Shell_NotifyIconGetRect");
  3. if (Shell_NotifyIconGetRect)
  4. {
  5. NOTIFYICONIDENTIFIER notify;
  6. notify.cbSize = sizeof notify;
  7. notify.hWnd = (HWND)m_TrayMessageWidget->winId();
  8. notify.uID = ;
  9. notify.guidItem = GUID_NULL;
  10.  
  11. RECT rect;
  12. HRESULT hr = Shell_NotifyIconGetRect(&notify, &rect);
  13.  
  14. return QRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
  15. }

4、如果你的系统是xp,或者更早,那么Shell_NotifyIconGetRect这个接口是用不了了,如果使用,直接会导致程序起不来,那么我们的代码会像下边这样

  1. struct AppData
  2. {
  3. HWND hwnd;
  4. UINT uID;
  5. };
  6.  
  7. QRect ret;
  8.  
  9. TBBUTTON buttonData;
  10. DWORD processID = ;
  11. HWND trayHandle = FindWindow(L"Shell_TrayWnd", NULL);
  12.  
  13. //find the toolbar used in the notification area
  14. if (trayHandle) {
  15. trayHandle = FindWindowEx(trayHandle, NULL, L"TrayNotifyWnd", NULL);
  16. if (trayHandle) {
  17. HWND hwnd = FindWindowEx(trayHandle, NULL, L"SysPager", NULL);
  18. if (hwnd) {
  19. hwnd = FindWindowEx(hwnd, NULL, L"ToolbarWindow32", NULL);
  20. if (hwnd)
  21. trayHandle = hwnd;
  22. }
  23. }
  24. }
  25.  
  26. if (!trayHandle)
  27. return ret;
  28.  
  29. GetWindowThreadProcessId(trayHandle, &processID);
  30. if (processID <= )
  31. return ret;
  32.  
  33. HANDLE trayProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ, , processID);
  34. if (!trayProcess)
  35. return ret;
  36.  
  37. int buttonCount = SendMessage(trayHandle, TB_BUTTONCOUNT, , );
  38. LPVOID data = VirtualAllocEx(trayProcess, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE);
  39.  
  40. if (buttonCount < || !data) {
  41. CloseHandle(trayProcess);
  42. return ret;
  43. }
  44.  
  45. //search for our icon among all toolbar buttons
  46. for (int toolbarButton = ; toolbarButton < buttonCount; ++toolbarButton) {
  47. SIZE_T numBytes = ;
  48. AppData appData = { , };
  49. SendMessage(trayHandle, TB_GETBUTTON, toolbarButton, (LPARAM)data);
  50.  
  51. if (!ReadProcessMemory(trayProcess, data, &buttonData, sizeof(TBBUTTON), &numBytes))
  52. continue;
  53.  
  54. if (!ReadProcessMemory(trayProcess, (LPVOID)buttonData.dwData, &appData, sizeof(AppData), &numBytes))
  55. continue;
  56.  
  57. bool isHidden = buttonData.fsState & TBSTATE_HIDDEN;
  58.  
  59. if (m_NotifyIconData.hWnd == appData.hwnd && appData.uID == m_NotifyIconData.uID && !isHidden) {
  60. SendMessage(trayHandle, TB_GETITEMRECT, toolbarButton, (LPARAM)data);
  61. RECT iconRect = { , , , };
  62. if (ReadProcessMemory(trayProcess, data, &iconRect, sizeof(RECT), &numBytes)) {
  63. MapWindowPoints(trayHandle, NULL, (LPPOINT)&iconRect, );
  64. QRect geometry(iconRect.left + , iconRect.top + ,
  65. iconRect.right - iconRect.left - ,
  66. iconRect.bottom - iconRect.top - );
  67. if (geometry.isValid())
  68. ret = geometry;
  69. break;
  70. }
  71. }
  72. }
  73. VirtualFreeEx(trayProcess, data, , MEM_RELEASE);
  74. CloseHandle(trayProcess);

以上代码我是在xp、win7和iwn10上测试通过的,没有问题。这篇文章我也没有提供demo,最近实在是太忙了,根本没有时间整理,记录这些的原因也是想整理下思路,并且想帮助一些有问题的同学。文章看到这里,实现一个自定义的托盘逻辑基本上都走通了,剩下的就是qwidget的大量应用,还有界面美化工作啦

如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!! 

 

很重要--转载声明

  1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords
  2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。

Qt之自定义托盘(二)的更多相关文章

  1. Qt之自定义托盘

    说起Qt,真是个不错的ui库,不仅仅ui做的好,其他方面也不差,在平台扩展方面也是非常的强大.这篇文章我将会分析下qt的托盘,QSystemTrayIcon是qt的托盘类,托盘类的用途是什么我就不说了 ...

  2. Qt之自定义托盘(两种方法)

    http://www.cnblogs.com/swarmbees/p/5789482.html http://www.cnblogs.com/swarmbees/p/5812031.html

  3. QT中自定义系统托盘的实现—c++语言为例

    将要介绍的是:QT中自定义系统托盘(systemtray)的一个Demo,希望能帮需要的读者快速上手. 前提假设是诸位已经知道QT中的signals .slot以及资源文件,所以关于这些不会再累述. ...

  4. Qt之自定义QLineEdit右键菜单

    一.QLineEdit说明 QLineEdit是单行文本框,不同于QTextEdit,他只能显示一行文本,通常可以用作用户名.密码和搜索框等.它还提供了一些列的信号和槽,方便我们使用,有兴趣的小伙伴可 ...

  5. Qt之自定义搜索框

    简述 关于搜索框,大家都经常接触.例如:浏览器搜索.Windows资源管理器搜索等. 当然,这些对于Qt实现来说毫无压力,只要思路清晰,分分钟搞定. 方案一:调用QLineEdit现有接口 void ...

  6. Qt之自定义搜索框——QLineEdit里增加一个Layout,还不影响正常输入文字(好像是一种比较通吃的方法)

    简述 关于搜索框,大家都经常接触.例如:浏览器搜索.Windows资源管理器搜索等. 当然,这些对于Qt实现来说毫无压力,只要思路清晰,分分钟搞定. 方案一:调用QLineEdit现有接口 void ...

  7. Qt DLL总结【二】-创建及调用QT的 DLL(三篇)good

    目录 Qt DLL总结[一]-链接库预备知识 Qt DLL总结[二]-创建及调用QT的 DLL Qt DLL总结[三]-VS2008+Qt 使用QPluginLoader访问DLL 开发环境:VS20 ...

  8. 【Qt】Qt之自定义搜索框【转】

    简述 关于搜索框,大家都经常接触.例如:浏览器搜索.Windows资源管理器搜索等. 当然,这些对于Qt实现来说毫无压力,只要思路清晰,分分钟搞定. 简述 效果 细节分析 Coding 源码下载 效果 ...

  9. Qt之自定义检索框

    1.效果展示 今天这篇文章主要讲解的是自定义搜索框,不仅仅支持搜索,而且可以支持搜索预览,具体请看效果图1.网上也有一些比较简单明了的自定义搜索框,比如Qt之自定义搜索框,讲的也比较详细,不过本文的侧 ...

随机推荐

  1. redis的雪崩与穿透原理的浅理解

    首先列一下主要说什么, 1.什么是Redis缓存的雪崩? 2.什么是Redis缓存的穿透? 3.Redis缓存崩溃会怎么样? 4.怎么预防Redis缓存崩溃? 1.什么是Redis缓存的雪崩? 举个栗 ...

  2. Django缓存机制

    缓存介绍 在动态网站中,用户所有的请求,服务器都会去数据库中进行相应的增删改查,渲染模板,执行业务逻辑,最后生成用户看到的页面. 当一个网站的用户访问量很大的时候,每一次的后台操作,都会消耗很多的服务 ...

  3. MDK的一些小应用

    一:MDK生成bin文件 Options(魔术棒)  ->  User  ->  After Build/rebuild  ->  Run#1(前边打钩) (后边的方框输入一段内容) ...

  4. Python-写文件

    写文件需要三步:打开文件写入内容关闭文件 写入内容一般要选择打开的模式:f = open('out.txt','w')此处的w就是writing,代表以写入文件的模式打开,原文件里的内容会被新写入覆盖 ...

  5. 一. 优化小程序自身的Storage

    小程序中的存储只有 Storage ,特性如下: 上限为 10MB 以用户纬度隔离,同一个设备,A 无法访问 B 用户的数据. 持久缓存,只有在用户关掉小程序才会删除,如果空间不足,会进行 LRU , ...

  6. VMware Workstation Pro下载密钥

    热门虚拟机软件VMware Workstation Pro现已更新至14.1.2,14.0主要更新了诸多客户机操作系统版本,此外全面兼容Wind10创建者更新.12.0之后属于大型更新,专门为Win1 ...

  7. DataTable的Merge\COPY\AcceptChange使用说明

    在C#内使用DataTable的Merge().Copy().AcceptChange().Clone()方法的用途如下: 1.Merge()可将两个不同的表结构的表进行合并,合并后新表的列为之前两表 ...

  8. node06

    1.数据库: server端:数据存在 client端:管理工具,node mysql内有两个单位: 库:类似文件夹,容纳表 表:存储数据 行:一条数据 列(字段,域):一个数据项 主键:数据的唯一标 ...

  9. [Educational Round 13][Codeforces 678F. Lena and Queries]

    题目连接:678F - Lena and Queries 题目大意:要求对一个点集实现二维点对的插入,删除,以及询问\(q\):求\(max(x\cdot q+y)\) 题解:对每个点集内的点\(P( ...

  10. Linux 搭建 Nginx+PHP-FPM环境

    安装PHP.Nginx和PHP-FPM sudo apt-get install php sudo apt-get install nginx sudo apt-get install php7-fp ...