• Qt之自定义托盘(二)


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

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

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

     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这个消息的时候,我们把窗口移动到正确的位置,这样一个完美的右键菜单就完成啦。Open-mouthed smile

    二、托盘信息

        说到托盘信息,那么就得说说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 }

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

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

    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(&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 = 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的大量应用,还有界面美化工作啦Thumbs up

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

     

      


    很重要--转载声明

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

  • 相关阅读:
    PaaS 7层动态路由的若干实现
    05-OC对象的内存分析
    04-类与对象的练习(第二个OC的类)
    03-类的声明和实现(第一个OC的类)
    02-类与对象的关系
    01-面向对象和面向过程
    06-BOOL类型的使用
    05-初识OC多文件编程(第4个OC程序)
    04-初识OC多文件编程(第3个OC程序)
    03-第二个OC程序
  • 原文地址:https://www.cnblogs.com/swarmbees/p/5812031.html
Copyright © 2020-2023  润新知