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