Qt之自定義托盤(二)

    上一篇文章講述了自定義Qt托盤,不過不是使用QSystemTrayIcon這個類,而是咱們本身徹底自定義的一個類,咱們只須要處理這個類的鼠標hover、鼠標左鍵點擊、鼠標右鍵點擊和鼠標左鍵雙擊,就能夠徹底模擬出qq的托盤樣式來。文章的最後我也是提供了一個demo的下載連接,那是一個能夠徹底運行的demo,處理了鼠標hover事件,並模擬出了鼠標離開和進入事件,這一節我將一步一步講解怎麼實現一個完美的托盤,包括托盤菜單的顯示、托盤tooltip和托盤hover時的彈框顯示。html

    看本片文章以前,同窗們最好把Qt之自定義托盤文章讀一讀,這篇文章中有win32的幾個api的講解,雖然不細緻,可是講到了他們的做用,並說明了一些用法。shell

    在本篇內容講解以前,我先貼一段代碼,也算是對上一屆內容的回顧吧,這個接口是QAbstractNativeEventFilter類的,該接口若是要處理app的消息,須要使用qApp這個指針把CSystemTrayIcon對象註冊下,具體代碼上一節中有介紹,這裏我就很少說啦。Sarcastic smilewindows

 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托盤區域沒有提供),其餘鼠標事件都是能夠直接拿到,後邊只須要處理咱們本身的具體業務。api

1、菜單app

    一個完美的托盤,每每都有右鍵菜單,而右鍵菜單也是托盤的一項重要功能,若是想實現自定義的托盤菜單,請看文章qt之菜單項定製,這篇文章中講述到了自定義菜單,應該能夠知足大多數人的需求,起碼實現360或者電腦管家那樣的右鍵菜單是沒有問題。ide

    上邊給出的連接就能夠實現一個自定義而且美觀的菜單項,接下來,我主要說下右鍵菜單顯示的問題,首先我說明一個問題,右鍵菜單顯示的位置應該是咱們右鍵點擊的位置,我強調這句話的緣由是後邊咱們講解鼠標hover彈框時,會和右鍵菜單有所區別。Qt的菜單也是一個窗口,他繼承自QWidget,只不過菜單含有Qt::Popup屬性,當他失去焦點的時候,就會自動隱藏。函數

    鼠標右鍵在托盤區域點擊右鍵,咱們響應WM_RBUTTONUP消息,而後show出右鍵菜單,這個時候咱們就須要作一件事情,必須保證咱們本身顯示的右鍵菜單在屏幕內,關於這個我問題,我也很少說,一切看代碼,代碼邏輯也比較簡單,首先把菜單移動到鼠標右鍵點擊的位置,而後判斷鼠標鼠標是否在界面內,若是須要移動的話,水平移動就移動菜單的寬度,垂直方向就移動菜單的高度,具體怎麼移動須要判斷窗口的那個邊出屏幕。post

    說了這麼多,其實修正代碼也比較簡單,以下:測試

 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 smileui

2、托盤信息

    說到托盤信息,那麼就得說說NOTIFYICONDATA這個結構,這個結構中保存了托盤的基本信息,包括托盤圖標、托盤tooltip、托盤句柄和托盤關注消息id等一系列成員,這一節Qt之自定義托盤中講到了怎麼建立和刪除一個托盤圖標,具體的怎麼修改其餘信息我也在這裏大概的說下,由於NOTIFYICONDATA結構的百度百科說的已經很是詳細,我在這兒只作大概描述。

一、修改托盤圖標

 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接口修改托盤。

二、修改托盤tooltips

    修改托盤提示信息其實和修改圖標是已給道理,首先須要搞清楚修改那個托盤的提示信息,而後在設置uFlags標誌,並重置NOTIFYICONDATA結構的具體成員信息,最後調用shell接口修改托盤,代碼我就不粘貼了

3、托盤hover窗口

    托盤hover時所彈出的框,主要用於顯示未接受的消息,能夠快速的瀏覽用戶消息,並響應用戶的交互,爲了和鼠標右鍵菜單有所區分,在本小節中我把托盤有消息時hover所彈出的界面統稱爲hover彈框。

一、首先是根據ui設計師的要求,定製好美觀的托盤hover彈框,這個彈框通常都包含有消息項,相似於qq的好友消息,這個窗口應該須要支持和咱們自定義的托盤類交互的能力,並保持和托盤圖標閃爍同步,比較圖標閃爍那就說明有消息,進而會出現鼠標hover時,彈出未接消息提示框

二、在托盤菜單發出須要顯示hover窗口時,咱們把彈框顯示出來

三、在第一節菜單內容中,我重點提到了菜單顯示位置的問題,hover彈框也存在這個問題,那麼我首先先解釋下這個hover彈框的規則,我下邊的規則都是基於任務欄是在屏幕底下時發生的。

  • 托盤圖標未在溢出區:hover彈框的中心位置x座標和托盤圖標的中心位置x座標同樣,bottom值和任務欄的top值同樣
  • 托盤圖標在溢出區:hover彈框的中心位置x座標和托盤圖標的中心位置x座標同樣,bottom值和任務欄的top值同樣

四、若是任務欄在屏幕的頂部、左側和右側,都是相似的處理方式

    下邊是我自定義窗口的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 }

上邊的代碼是否是也是否是比較簡單啊,呵呵,其實還好。關於上述怎麼獲取任務欄高度和寬度的方法我就不貼代碼了,有興趣的同窗,自行百度。

    接下來,我要在補充一下,怎麼獲取任務欄圖標的座標

一、首先我說下Shell_NotifyIconGetRect這個接口,微軟明確說明了這個接口只有在win7後纔開始提供,因此若是自定義托盤要在xp系統和win7(win10)系列系統上跑,那麼就須要作兼容性處理。

二、下面是一個判斷接口,判斷指定的動態庫是否包含指定接口

 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 }

三、若是你的系統是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     }

四、若是你的系統是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. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時經過修改本文達到有利於轉載者的目的。 

相關文章
相關標籤/搜索