![]() |
![]() |
很重要--轉載聲明
- 本站文章無特別說明,皆爲原創,版權全部,轉載時請用連接的方式,給出原文出處。同時寫上原做者:朝十晚八 or Twowords
- 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時經過修改本文達到有利於轉載者的目的。
上一篇文章講述了自定義Qt托盤,不過不是使用QSystemTrayIcon這個類,而是咱們本身徹底自定義的一個類,咱們只須要處理這個類的鼠標hover、鼠標左鍵點擊、鼠標右鍵點擊和鼠標左鍵雙擊,就能夠徹底模擬出qq的托盤樣式來。文章的最後我也是提供了一個demo的下載連接,那是一個能夠徹底運行的demo,處理了鼠標hover事件,並模擬出了鼠標離開和進入事件,這一節我將一步一步講解怎麼實現一個完美的托盤,包括托盤菜單的顯示、托盤tooltip和托盤hover時的彈框顯示。html
看本片文章以前,同窗們最好把Qt之自定義托盤文章讀一讀,這篇文章中有win32的幾個api的講解,雖然不細緻,可是講到了他們的做用,並說明了一些用法。shell
在本篇內容講解以前,我先貼一段代碼,也算是對上一屆內容的回顧吧,這個接口是QAbstractNativeEventFilter類的,該接口若是要處理app的消息,須要使用qApp這個指針把CSystemTrayIcon對象註冊下,具體代碼上一節中有介紹,這裏我就很少說啦。windows
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這個消息的時候,咱們把窗口移動到正確的位置,這樣一個完美的右鍵菜單就完成啦。ui
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彈框的規則,我下邊的規則都是基於任務欄是在屏幕底下時發生的。
四、若是任務欄在屏幕的頂部、左側和右側,都是相似的處理方式
下邊是我自定義窗口的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(¬ify, &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的大量應用,還有界面美化工做啦。