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