Qt之自定义托盘(二) qt自定义tooltip
liebian365 2024-10-30 04:50 28 浏览 0 评论
上一篇文章讲述了自定义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 = 0;
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(), 1)) == false)
10 {
11 wRect.translate(widget->width(), 0);
12 }
13
14 if (rect.contains(QPoint(wRect.right(), 1)) == false)
15 {
16 wRect.translate(-widget->width(), 0);
17 }
18
19 if (rect.contains(QPoint(1, wRect.bottom())) == false)
20 {
21 wRect.translate(0, -widget->height());
22 }
23
24 if (rect.contains(QPoint(1, wRect.top())) == false)
25 {
26 wRect.translate(0, 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 1:
14 {
15 int missionHeight = MissionToolHeight();
16 QPoint pos(wRect.topLeft().x() - wRect.width() / 2, missionHeight);
17 move(pos);
18 }
19 break;
20 case 2:
21 {
22 if (rect.contains(QPoint(wRect.right(), 1)) == false)
23 {
24 wRect.translate(-this->width(), 0);
25 }
26 QRect r = desktop->availableGeometry(desktop->primaryScreen());
27 move(wRect.topLeft() + QPoint(-(m_TrayIconVerCenter.x() - r.width()), -wRect.height() / 2));
28 }
29 break;
30 case 3:
31 {
32 QRect r = desktop->availableGeometry(desktop->primaryScreen());
33 QPoint pos(wRect.topLeft().x() - wRect.width() / 2, 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() / 2));
41 }
42 }
43 }
44
45 show();
46 }
上边的代码是不是也是不是比较简单啊,呵呵,其实还好。关于上述怎么获取任务栏高度和宽度的方法我就不贴代码了,有兴趣的同学,自行百度。
点击领取Qt学习资料+视频教程~「链接」
接下来,我要再补充一下,怎么获取任务栏图标的坐标
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 = 1;
9 notify.guidItem = GUID_NULL;
10
11 RECT rect;
12 HRESULT hr = Shell_NotifyIconGetRect(?ify, &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 = 0;
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 <= 0)
31 return ret;
32
33 HANDLE trayProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ, 0, processID);
34 if (!trayProcess)
35 return ret;
36
37 int buttonCount = SendMessage(trayHandle, TB_BUTTONCOUNT, 0, 0);
38 LPVOID data = VirtualAllocEx(trayProcess, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE);
39
40 if (buttonCount < 1 || !data) {
41 CloseHandle(trayProcess);
42 return ret;
43 }
44
45 //search for our icon among all toolbar buttons
46 for (int toolbarButton = 0; toolbarButton < buttonCount; ++toolbarButton) {
47 SIZE_T numBytes = 0;
48 AppData appData = { 0, 0 };
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 = { 0, 0, 0, 0 };
62 if (ReadProcessMemory(trayProcess, data, &iconRect, sizeof(RECT), &numBytes)) {
63 MapWindowPoints(trayHandle, NULL, (LPPOINT)&iconRect, 2);
64 QRect geometry(iconRect.left + 1, iconRect.top + 1,
65 iconRect.right - iconRect.left - 2,
66 iconRect.bottom - iconRect.top - 2);
67 if (geometry.isValid())
68 ret = geometry;
69 break;
70 }
71 }
72 }
73 VirtualFreeEx(trayProcess, data, 0, MEM_RELEASE);
74 CloseHandle(trayProcess);
以上代码我是在xp、win7和iwn10上测试通过的,没有问题。这篇文章我也没有提供demo,最近实在是太忙了,根本没有时间整理,记录这些的原因也是想整理下思路,并且想帮助一些有问题的同学。文章看到这里,实现一个自定义的托盘逻辑基本上都走通了,剩下的就是qwidget的大量应用,还有界面美化工作啦
原作者:朝十晚八 or Twowords
原文转载:https://www.cnblogs.com/swarmbees/p/5812031.html
相关推荐
- [西门子PLC] S7-1200PLC中所支持的数据类型详解
-
数据类型呢,就是讲数据的长度和属性的,也就是指定数据元素的大小,还有怎么去解释数据。每个指令起码得支持一种数据类型,有的指令还能支持好多种数据类型。所以呀,指令上用的操作数的数据类型一定得跟指令支持的...
- C语言wctomb函数详解:宽字符到多字节字符的「翻译官」
-
核心定位wctomb是C语言中用于将宽字符转换为多字节字符的「翻译官」,它能将单个宽字符(wchar_t)转换为多字节字符(如UTF-8编码的中文)。就像一位翻译官,它能将一种语言(宽字符)翻译成...
- Python 中数组和列表之间的区别(python列表和c语言数组区别)
-
在这篇文章中,您将了解Python中数组和列表之间的区别。Python列表Python列表是一种内置数据结构,是包含在方括号[]的元素集合。它们具有许多独特的属性,使它们与其他数据结构不同。有...
- Linux内核设计与实现—进程管理(linux内核原理与实现)
-
进程进程就是处于执行期的程序(目标码存放在某种存储介质上)。进并不仅仅局限于一段可执行程序代码(Unix称其为代码段,textsection)。通常进程还要包含其他资源,像打开的文件,挂起的信号,...
- 实际工程项目中西门子S7-1500如何批量读取和写入机器人信号
-
方法一:DPRD_DAT:读取DP标准从站的一致性数据该指令适用于中央模块以及DP标准从站和PROFINETIO设备。可以使用以下数据类型:BOOL,BYTE,CHAR,WCHAR,WO...
- C语言mbstowcs函数详解:多字节字符串到宽字符的「翻译官」
-
核心定位mbstowcs是C语言中用于将多字节字符串转换为宽字符字符串的「翻译官」,它能将多字节字符(如UTF-8编码的中文)转换为宽字符(wchar_t)。就像一位翻译官,它能将一种语言(多字节...
- C语言mbtowc函数详解:多字节字符到宽字符的「翻译官」
-
核心定位mbtowc是C语言中用于将多字节字符转换为宽字符的「翻译官」,它能将单个多字节字符(如UTF-8编码的中文)转换为宽字符(wchar_t)。就像一位翻译官,它能将一种语言(多字节字符)翻...
- 西门子PLC系列连载|No.5 初识西门子1200PLC数据类型
-
导语:在之前的文章中我们介绍了PLC的相关基础知识和一些小的程序段,也讲解过博途软件使用的一些基本方法。那么我们在本章内容将为大家讲解关于西门子1200系列PLC的常用数据类型,以及这些数据类型的区别...
- 计算机中常见的字符编码及存储方式
-
常见的字符编码ASCII、GBK、GB2312、Unicode等等常识用多个字节来代表的字符称之为宽字符,而Unicode码只是宽字符编码的一种实现,宽字符并不一定是Unicodechar窄字...
- 西门子SCL高级语言之数据转换介绍
-
(整数转浮点数INT_TO_REAL)我们在做项目中经常用到各种类型的数据,这就需要转换(CONVERT)指令来转换,由于博途数据转换指令只有它一个,那我们就只记住它就可以了,注意设置需要转换...
- SCL编程语言学习(2)-启保停电路(起保停电路plc程序)
-
“启保停”电路是学习过程中最常见的一个案例,也是最简单易懂的控制程序。如果采用梯形图编程,如图1所示。在实际工程的电路中,很少有这么简单的起保停电路,一般都需要考虑急停、限位、过载保护等多项因素,启停...
- GCC的常用编译选项(gcc编译工具)
-
GCC(GNUCompilerCollection,GNU编译器套件)是由GNU开发的编程语言译器。对于C语言源代码文件,使用GCC生成可执行文件的过程不仅仅是编译的过程,而是要经历四个相...
- 「C语言」初始化数组,C语言中初始化特定列表和元素
-
如果没有显式地初始化数组变量,那么就会采用一般规则:如果数组具有动态存储周期,那么数组元素的值就是没有定义的。否则,所有的元素都会被默认地初始化为0(如果数组元素是指针,则会被初始化为NULL)。编...
- C++11新特性(c++11新特性 lambda)
-
1、智能指针2、Lambda表达式3、线程库4、原子操作5、统一的列表初始化{}6、右值引用和移动构造7、引入nullptr指针8、类型推导auto和decltype智能指针:智能指针是一个...
- 西门子 S7-1200 PLC 数据类型详解
-
关注“PLC发烧友”,一起涨知识!回复:西门子全套,领西门子系列PLC电子资料包!数据类型用来描述数据的长度和属性,即用于指定数据元素的大小及如何解释数据,每个指令至少支持一个数据类型,而部分指令支持...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)