Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwindowssystemtrayicon.cpp
Go to the documentation of this file.
1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtCore/qt_windows.h>
5
9#include "qwindowsmenu.h"
10#include "qwindowsscreen.h"
11
12#include <QtGui/qguiapplication.h>
13#include <QtGui/qpixmap.h>
14#include <QtCore/qdebug.h>
15#include <QtCore/qlist.h>
16#include <QtCore/qrect.h>
17#include <QtCore/qsettings.h>
18#include <qpa/qwindowsysteminterface.h>
19
20#include <QtGui/private/qguiapplication_p.h>
21
22#include <commctrl.h>
23#include <shellapi.h>
24#include <shlobj.h>
25#include <windowsx.h>
26
28
29using namespace Qt::StringLiterals;
30
31static const UINT q_uNOTIFYICONID = 0;
32
34#define MYWM_NOTIFYICON (WM_APP+101)
35
36Q_GUI_EXPORT HICON qt_pixmapToWinHICON(const QPixmap &);
37
38// Copy QString data to a limited wchar_t array including \0.
39static inline void qStringToLimitedWCharArray(QString in, wchar_t *target, int maxLength)
40{
41 const int length = qMin(maxLength - 1, in.size());
42 if (length < in.size())
43 in.truncate(length);
44 in.toWCharArray(target);
45 target[length] = wchar_t(0);
46}
47
48static inline void initNotifyIconData(NOTIFYICONDATA &tnd)
49{
50 memset(&tnd, 0, sizeof(NOTIFYICONDATA));
51 tnd.cbSize = sizeof(NOTIFYICONDATA);
52 tnd.uVersion = NOTIFYICON_VERSION_4;
53}
54
55static void setIconContents(NOTIFYICONDATA &tnd, const QString &tip, HICON hIcon)
56{
57 tnd.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP;
58 tnd.uCallbackMessage = MYWM_NOTIFYICON;
59 tnd.hIcon = hIcon;
60 qStringToLimitedWCharArray(tip, tnd.szTip, sizeof(tnd.szTip) / sizeof(wchar_t));
61}
62
63static void setIconVisibility(NOTIFYICONDATA &tnd, bool v)
64{
65 tnd.uFlags |= NIF_STATE;
66 tnd.dwStateMask = NIS_HIDDEN;
67 tnd.dwState = v ? 0 : NIS_HIDDEN;
68}
69
70// Match the HWND of the dummy window to the instances
76
77using HwndTrayIconEntries = QList<QWindowsHwndSystemTrayIconEntry>;
78
80
81static int indexOfHwnd(HWND hwnd)
82{
83 const HwndTrayIconEntries *entries = hwndTrayIconEntries();
84 for (int i = 0, size = entries->size(); i < size; ++i) {
85 if (entries->at(i).hwnd == hwnd)
86 return i;
87 }
88 return -1;
89}
90
91LRESULT QT_WIN_CALLBACK qWindowsTrayIconWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
92{
93 if (message == MYWM_TASKBARCREATED || message == MYWM_NOTIFYICON
94 || message == WM_INITMENU || message == WM_INITMENUPOPUP
95 || message == WM_CLOSE || message == WM_COMMAND) {
96 const int index = indexOfHwnd(hwnd);
97 if (index >= 0) {
98 MSG msg;
99 msg.hwnd = hwnd; // re-create MSG structure
100 msg.message = message; // time and pt fields ignored
101 msg.wParam = wParam;
102 msg.lParam = lParam;
103 msg.pt.x = GET_X_LPARAM(lParam);
104 msg.pt.y = GET_Y_LPARAM(lParam);
105 long result = 0;
106 if (hwndTrayIconEntries()->at(index).trayIcon->winEvent(msg, &result))
107 return result;
108 }
109 }
110 return DefWindowProc(hwnd, message, wParam, lParam);
111}
112
113// Note: Message windows (HWND_MESSAGE) are not sufficient, they
114// will not receive the "TaskbarCreated" message.
116{
118 if (!ctx)
119 return nullptr;
120 // Register window class in the platform plugin.
121 const QString className =
122 ctx->registerWindowClass(QWindowsContext::classNamePrefix() + "TrayIconMessageWindowClass"_L1,
123 qWindowsTrayIconWndProc);
124 const wchar_t windowName[] = L"QTrayIconMessageWindow";
125 return CreateWindowEx(0, reinterpret_cast<const wchar_t *>(className.utf16()),
126 windowName, WS_OVERLAPPED,
127 CW_USEDEFAULT, CW_USEDEFAULT,
128 CW_USEDEFAULT, CW_USEDEFAULT,
129 nullptr, nullptr,
130 static_cast<HINSTANCE>(GetModuleHandle(nullptr)), nullptr);
131}
132
133/*!
134 \class QWindowsSystemTrayIcon
135 \brief Windows native system tray icon
136
137 \internal
138*/
139
143
145{
146 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this;
147 ensureCleanup();
148}
149
151{
152 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this;
153 m_visible = true;
154 if (!setIconVisible(m_visible))
155 ensureInstalled();
156}
157
159{
160 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this;
161 m_visible = false;
162 ensureCleanup();
163}
164
165void QWindowsSystemTrayIcon::updateIcon(const QIcon &icon)
166{
167 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << icon << ')' << this;
168 m_icon = icon;
169 const HICON hIconToDestroy = createIcon(icon);
170 if (ensureInstalled())
171 sendTrayMessage(NIM_MODIFY);
172 if (hIconToDestroy)
173 DestroyIcon(hIconToDestroy);
174}
175
176void QWindowsSystemTrayIcon::updateToolTip(const QString &tooltip)
177{
178 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << tooltip << ')' << this;
179 if (m_toolTip == tooltip)
180 return;
181 m_toolTip = tooltip;
182 if (isInstalled())
183 sendTrayMessage(NIM_MODIFY);
184}
185
187{
188 if (!isIconVisible())
189 return QRect();
190
191 NOTIFYICONIDENTIFIER nid;
192 memset(&nid, 0, sizeof(nid));
193 nid.cbSize = sizeof(nid);
194 nid.hWnd = m_hwnd;
195 nid.uID = q_uNOTIFYICONID;
196 RECT rect;
197 const QRect result = SUCCEEDED(Shell_NotifyIconGetRect(&nid, &rect))
198 ? QRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top)
199 : QRect();
200 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this << "returns" << result;
201 return result;
202}
203
204void QWindowsSystemTrayIcon::showMessage(const QString &title, const QString &messageIn,
205 const QIcon &icon,
206 QPlatformSystemTrayIcon::MessageIcon iconType,
207 int msecsIn)
208{
209 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << title << messageIn << icon
210 << iconType << msecsIn << ')' << this;
212 return;
213 // For empty messages, ensures that they show when only title is set
214 QString message = messageIn;
215 if (message.isEmpty() && !title.isEmpty())
216 message.append(u' ');
217
218 NOTIFYICONDATA tnd;
219 initNotifyIconData(tnd);
220 qStringToLimitedWCharArray(message, tnd.szInfo, 256);
221 qStringToLimitedWCharArray(title, tnd.szInfoTitle, 64);
222
223 tnd.uID = q_uNOTIFYICONID;
224
225 const auto size = icon.actualSize(QSize(256, 256));
226 QPixmap pm = icon.pixmap(size);
227 if (m_hMessageIcon) {
228 DestroyIcon(m_hMessageIcon);
229 m_hMessageIcon = nullptr;
230 }
231 if (pm.isNull()) {
232 tnd.dwInfoFlags = NIIF_INFO;
233 } else {
234 tnd.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON;
235 m_hMessageIcon = qt_pixmapToWinHICON(pm);
236 tnd.hBalloonIcon = m_hMessageIcon;
237 }
238 tnd.hWnd = m_hwnd;
239 tnd.uTimeout = msecsIn <= 0 ? UINT(10000) : UINT(msecsIn); // 10s default
240 tnd.uFlags = NIF_INFO | NIF_SHOWTIP;
241
242 Shell_NotifyIcon(NIM_MODIFY, &tnd);
243}
244
246{
247 // The key does typically not exist on Windows 10, default to true.
248 return QWindowsContext::readAdvancedExplorerSettings(L"EnableBalloonTips", 1) != 0;
249}
250
252{
253 if (QWindowsTheme::useNativeMenus() && m_menu.isNull())
254 m_menu = new QWindowsPopupMenu;
255 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this << "returns" << m_menu.data();
256 return m_menu.data();
257}
258
259// Delay-install until an Icon exists
260bool QWindowsSystemTrayIcon::ensureInstalled()
261{
262 if (isInstalled())
263 return true;
264 if (m_hIcon == nullptr)
265 return false;
266 m_hwnd = createTrayIconMessageWindow();
267 if (Q_UNLIKELY(m_hwnd == nullptr))
268 return false;
269 // For restoring the tray icon after explorer crashes
270 if (!MYWM_TASKBARCREATED)
271 MYWM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
272 // Allow the WM_TASKBARCREATED message through the UIPI filter
273 ChangeWindowMessageFilterEx(m_hwnd, MYWM_TASKBARCREATED, MSGFLT_ALLOW, nullptr);
274 qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this << "MYWM_TASKBARCREATED=" << MYWM_TASKBARCREATED;
275
276 QWindowsHwndSystemTrayIconEntry entry{m_hwnd, this};
277 hwndTrayIconEntries()->append(entry);
278 sendTrayMessage(NIM_ADD);
279 return true;
280}
281
282void QWindowsSystemTrayIcon::ensureCleanup()
283{
284 if (isInstalled()) {
285 const int index = indexOfHwnd(m_hwnd);
286 if (index >= 0)
287 hwndTrayIconEntries()->removeAt(index);
288 sendTrayMessage(NIM_DELETE);
289 DestroyWindow(m_hwnd);
290 m_hwnd = nullptr;
291 }
292 if (m_hIcon != nullptr)
293 DestroyIcon(m_hIcon);
294 if (m_hMessageIcon != nullptr)
295 DestroyIcon(m_hMessageIcon);
296 m_hIcon = nullptr;
297 m_hMessageIcon = nullptr;
298 m_menu = nullptr; // externally owned
299 m_toolTip.clear();
300}
301
302bool QWindowsSystemTrayIcon::setIconVisible(bool visible)
303{
304 if (!isInstalled())
305 return false;
306 NOTIFYICONDATA tnd;
307 initNotifyIconData(tnd);
308 tnd.uID = q_uNOTIFYICONID;
309 tnd.hWnd = m_hwnd;
310 setIconVisibility(tnd, visible);
311 return Shell_NotifyIcon(NIM_MODIFY, &tnd) == TRUE;
312}
313
314bool QWindowsSystemTrayIcon::isIconVisible() const
315{
316 NOTIFYICONIDENTIFIER nid;
317 memset(&nid, 0, sizeof(nid));
318 nid.cbSize = sizeof(nid);
319 nid.hWnd = m_hwnd;
320 nid.uID = q_uNOTIFYICONID;
321 RECT rect;
322 const HRESULT hr = Shell_NotifyIconGetRect(&nid, &rect);
323 // Windows 10 returns S_FALSE if the icon is hidden
324 if (FAILED(hr) || hr == S_FALSE)
325 return false;
326
327 HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST);
328 MONITORINFO info;
329 info.cbSize = sizeof(MONITORINFO);
330 GetMonitorInfo(monitor, &info);
331 // Windows 11 seems to return a geometry outside of the current monitor's geometry in case of
332 // the icon being hidden. As it's impossible to change the alignment of the task bar on Windows
333 // 11 this check should be fine.
334 return rect.bottom <= info.rcMonitor.bottom;
335}
336
337bool QWindowsSystemTrayIcon::sendTrayMessage(DWORD msg)
338{
339 NOTIFYICONDATA tnd;
340 initNotifyIconData(tnd);
341 tnd.uID = q_uNOTIFYICONID;
342 tnd.hWnd = m_hwnd;
343 tnd.uFlags = NIF_SHOWTIP;
344 if (msg != NIM_DELETE && !m_visible)
345 setIconVisibility(tnd, m_visible);
346 if (msg == NIM_ADD || msg == NIM_MODIFY)
347 setIconContents(tnd, m_toolTip, m_hIcon);
348 if (!Shell_NotifyIcon(msg, &tnd))
349 return false;
350 return msg != NIM_ADD || Shell_NotifyIcon(NIM_SETVERSION, &tnd);
351}
352
353// Return the old icon to be freed after modifying the tray icon.
354HICON QWindowsSystemTrayIcon::createIcon(const QIcon &icon)
355{
356 const HICON oldIcon = m_hIcon;
357 m_hIcon = nullptr;
358 if (icon.isNull())
359 return oldIcon;
360 const QSize requestedSize = QSize(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
361 const QSize size = icon.actualSize(requestedSize);
362 const QPixmap pm = icon.pixmap(size);
363 if (!pm.isNull())
364 m_hIcon = qt_pixmapToWinHICON(pm);
365 return oldIcon;
366}
367
368bool QWindowsSystemTrayIcon::winEvent(const MSG &message, long *result)
369{
370 *result = 0;
371 switch (message.message) {
372 case MYWM_NOTIFYICON: {
373 Q_ASSERT(q_uNOTIFYICONID == HIWORD(message.lParam));
374 const int trayMessage = LOWORD(message.lParam);
375 switch (trayMessage) {
376 case NIN_SELECT:
377 case NIN_KEYSELECT:
378 if (m_ignoreNextMouseRelease)
379 m_ignoreNextMouseRelease = false;
380 else
381 emit activated(Trigger);
382 break;
383 case WM_LBUTTONDBLCLK:
384 m_ignoreNextMouseRelease = true; // Since DBLCLICK Generates a second mouse
385 emit activated(DoubleClick); // release we must ignore it
386 break;
387 case WM_CONTEXTMENU: {
388 // QTBUG-67966: Coordinates may be out of any screen in PROCESS_DPI_UNAWARE mode
389 // since hi-res coordinates are delivered in this case (Windows issue).
390 // Default to primary screen with check to prevent a crash.
391 const QPoint globalPos = QPoint(GET_X_LPARAM(message.wParam), GET_Y_LPARAM(message.wParam));
392 // QTBUG-130832: QMenu relies on lastCursorPosition being up to date. When this code
393 // is called it still holds the last known mouse position inside a Qt window. Do a
394 // forced update of this position.
395 QGuiApplicationPrivate::lastCursorPosition = QCursor::pos().toPointF();
396 const auto &screenManager = QWindowsContext::instance()->screenManager();
397 const QPlatformScreen *screen = screenManager.screenAtDp(globalPos);
398 if (!screen)
399 screen = screenManager.screens().value(0);
400 if (screen) {
401 emit contextMenuRequested(globalPos, screen);
402 emit activated(Context);
403 if (m_menu) {
404 // Set the foreground window to the controlling window so that clicking outside
405 // of the menu or window will cause the menu to close
406 SetForegroundWindow(m_hwnd);
407 m_menu->trackPopupMenu(message.hwnd, globalPos.x(), globalPos.y());
408 }
409 }
410 }
411 break;
412 case NIN_BALLOONUSERCLICK:
413 emit messageClicked();
414 break;
415 case WM_MBUTTONUP:
416 emit activated(MiddleClick);
417 break;
418 default:
419 break;
420 }
421 }
422 break;
423 case WM_INITMENU:
424 case WM_INITMENUPOPUP:
425 QWindowsPopupMenu::notifyAboutToShow(reinterpret_cast<HMENU>(message.wParam));
426 break;
427 case WM_CLOSE:
428 QWindowSystemInterface::handleApplicationTermination<QWindowSystemInterface::SynchronousDelivery>();
429 break;
430 case WM_COMMAND:
431 QWindowsPopupMenu::notifyTriggered(LOWORD(message.wParam));
432 break;
433 default:
434 if (message.message == MYWM_TASKBARCREATED) {
435 // self-registered message id to handle that
436 // - screen resolution/DPR changed
437 const QIcon oldIcon = m_icon;
438 m_icon = QIcon(); // updateIcon is a no-op if the icon doesn't change
439 updateIcon(oldIcon);
440 // - or tray crashed
441 sendTrayMessage(NIM_ADD);
442 }
443 break;
444 }
445 return false;
446}
447
448#ifndef QT_NO_DEBUG_STREAM
449
451{
452 d << static_cast<const void *>(this) << ", \"" << m_toolTip
453 << "\", hwnd=" << m_hwnd << ", m_hIcon=" << m_hIcon << ", menu="
454 << m_menu.data();
455}
456
457QDebug operator<<(QDebug d, const QWindowsSystemTrayIcon *t)
458{
459 QDebugStateSaver saver(d);
460 d.nospace();
461 d.noquote();
462 d << "QWindowsSystemTrayIcon(";
463 if (t)
465 else
466 d << "0x0";
467 d << ')';
468 return d;
469}
470#endif // !QT_NO_DEBUG_STREAM
471
472QT_END_NAMESPACE
\inmodule QtCore\reentrant
Definition qpoint.h:29
Singleton container for all relevant information.
QWindowsScreenManager & screenManager()
static QWindowsContext * instance()
const QWindowsScreen * screenAtDp(const QPoint &p) const
Windows native system tray icon.
void cleanup() override
This method is called to cleanup the platform dependent implementation.
bool supportsMessages() const override
Returns true if the system tray supports messages on the platform.
bool winEvent(const MSG &message, long *result)
void formatDebug(QDebug &d) const
QPlatformMenu * createMenu() const override
This method allows platforms to use a different QPlatformMenu for system tray menus than what would n...
void updateToolTip(const QString &tooltip) override
This method is called when the tooltip text did change.
void init() override
This method is called to initialize the platform dependent implementation.
void updateIcon(const QIcon &icon) override
This method is called when the icon did change.
void showMessage(const QString &title, const QString &msg, const QIcon &icon, MessageIcon iconType, int msecs) override
Shows a balloon message for the entry with the given title, message msg and icon for the time specifi...
QRect geometry() const override
This method returns the geometry of the platform dependent system tray icon on the screen.
Q_GLOBAL_STATIC(QReadWriteLock, g_updateMutex)
#define MYWM_NOTIFYICON
static void setIconVisibility(NOTIFYICONDATA &tnd, bool v)
static HWND createTrayIconMessageWindow()
static const UINT q_uNOTIFYICONID
static void initNotifyIconData(NOTIFYICONDATA &tnd)
static void setIconContents(NOTIFYICONDATA &tnd, const QString &tip, HICON hIcon)
static uint MYWM_TASKBARCREATED
static void qStringToLimitedWCharArray(QString in, wchar_t *target, int maxLength)
static int indexOfHwnd(HWND hwnd)