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
qwindowsservices.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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
5#include <QtCore/qt_windows.h>
6
7#include <QtCore/qurl.h>
8#include <QtCore/qdebug.h>
9#include <QtCore/qdir.h>
10#include <QtCore/qscopedpointer.h>
11#include <QtCore/qthread.h>
12
13#include <QtCore/private/qwinregistry_p.h>
14#include <QtCore/private/qfunctions_win_p.h>
15
16#include <shlobj.h>
17#include <shlwapi.h>
18#include <intshcut.h>
19
21
22using namespace Qt::StringLiterals;
23
25{
26public:
27 explicit QWindowsShellExecuteThread(const wchar_t *operation, const wchar_t *file,
28 const wchar_t *parameters)
29 : m_operation(operation)
30 , m_file(file)
31 , m_parameters(parameters) { }
32
34 {
35 QComHelper comHelper;
36 if (comHelper.isValid())
37 m_result = ShellExecute(nullptr, m_operation, m_file, m_parameters, nullptr,
38 SW_SHOWNORMAL);
39 }
40
41 HINSTANCE result() const { return m_result; }
42
43private:
44 HINSTANCE m_result = nullptr;
45 const wchar_t *m_operation;
46 const wchar_t *m_file;
47 const wchar_t *m_parameters;
48};
49
50static QString msgShellExecuteFailed(const QUrl &url, quintptr code)
51{
52 QString result;
53 QTextStream(&result) <<"ShellExecute '" << url.toString() << "' failed (error " << code << ").";
54 return result;
55}
56
57// Retrieve the web browser and open the URL. This should be used for URLs with
58// fragments which don't work when using ShellExecute() directly (QTBUG-14460,
59// QTBUG-55300).
60static bool openWebBrowser(const QUrl &url)
61{
62 WCHAR browserExecutable[MAX_PATH] = {};
63 const wchar_t operation[] = L"open";
64 DWORD browserExecutableSize = MAX_PATH;
65 if (FAILED(AssocQueryString(0, ASSOCSTR_EXECUTABLE, L"http", operation,
66 browserExecutable, &browserExecutableSize))) {
67 return false;
68 }
69 QString browser = QString::fromWCharArray(browserExecutable, browserExecutableSize - 1);
70 // Workaround for "old" MS Edge entries. Instead of LaunchWinApp.exe we can just use msedge.exe
71 if (browser.contains("LaunchWinApp.exe"_L1, Qt::CaseInsensitive))
72 browser = "msedge.exe"_L1;
73 const QString urlS = url.toString(QUrl::FullyEncoded);
74
75 // Run ShellExecute() in a thread since it may spin the event loop.
76 // Prevent it from interfering with processing of posted events (QTBUG-85676).
77 QWindowsShellExecuteThread thread(operation,
78 reinterpret_cast<const wchar_t *>(browser.utf16()),
79 reinterpret_cast<const wchar_t *>(urlS.utf16()));
80 thread.start();
81 thread.wait();
82
83 const auto result = reinterpret_cast<quintptr>(thread.result());
84 qCDebug(lcQpaServices) << urlS << QString::fromWCharArray(browserExecutable) << result;
85 // ShellExecute returns a value greater than 32 if successful
86 if (result <= 32) {
87 qCWarning(lcQpaServices, "%s", qPrintable(msgShellExecuteFailed(url, result)));
88 return false;
89 }
90 return true;
91}
92
93static inline bool shellExecute(const QUrl &url)
94{
95 const QString nativeFilePath = url.isLocalFile() && !url.hasFragment() && !url.hasQuery()
96 ? QDir::toNativeSeparators(url.toLocalFile())
97 : url.toString(QUrl::FullyEncoded);
98
99
100 // Run ShellExecute() in a thread since it may spin the event loop.
101 // Prevent it from interfering with processing of posted events (QTBUG-85676).
102 QWindowsShellExecuteThread thread(nullptr,
103 reinterpret_cast<const wchar_t *>(nativeFilePath.utf16()),
104 nullptr);
105 thread.start();
106 thread.wait();
107
108 const auto result = reinterpret_cast<quintptr>(thread.result());
109
110 // ShellExecute returns a value greater than 32 if successful
111 if (result <= 32) {
112 qCWarning(lcQpaServices, "%s", qPrintable(msgShellExecuteFailed(url, result)));
113 return false;
114 }
115 return true;
116}
117
118// Retrieve the commandline for the default mail client. It contains a
119// placeholder %1 for the URL. The default key used below is the
120// command line for the mailto: shell command.
121static inline QString mailCommand()
122{
123 enum { BufferSize = sizeof(wchar_t) * MAX_PATH };
124
125 const wchar_t mailUserKey[] = L"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\mailto\\UserChoice";
126
127 // Check if user has set preference, otherwise use default.
128 QString keyName = QWinRegistryKey(HKEY_CURRENT_USER, mailUserKey)
129 .stringValue( L"Progid");
130 const auto mailto = keyName.isEmpty() ? "mailto"_L1 : QLatin1StringView();
131 keyName += mailto + "\\Shell\\Open\\Command"_L1;
132 qCDebug(lcQpaServices) << "keyName=" << keyName;
133 const QString command = QWinRegistryKey(HKEY_CLASSES_ROOT, keyName).stringValue(L"");
134 // QTBUG-57816: As of Windows 10, if there is no mail client installed, an entry like
135 // "rundll32.exe .. url.dll,MailToProtocolHandler %l" is returned. Launching it
136 // silently fails or brings up a broken dialog after a long time, so exclude it and
137 // fall back to ShellExecute() which brings up the URL association dialog.
138 if (command.isEmpty() || command.contains(u",MailToProtocolHandler"))
139 return QString();
140 wchar_t expandedCommand[MAX_PATH] = {0};
141 return ExpandEnvironmentStrings(reinterpret_cast<const wchar_t *>(command.utf16()),
142 expandedCommand, MAX_PATH)
143 ? QString::fromWCharArray(expandedCommand) : command;
144}
145
146static inline bool launchMail(const QUrl &url)
147{
148 QString command = mailCommand();
149 if (command.isEmpty()) {
150 qCWarning(lcQpaServices, "Cannot launch '%ls': There is no mail program installed.", qUtf16Printable(url.toString()));
151 return false;
152 }
153 // Fix mail launch if no param is expected in this command.
154 if (command.indexOf("%1"_L1) < 0) {
155 qWarning(lcQpaServices) << "The mail command lacks the '%1' parameter.";
156 return false;
157 }
158 //Make sure the path for the process is in quotes
159 const QChar doubleQuote = u'"';
160 if (!command.startsWith(doubleQuote)) {
161 const int exeIndex = command.indexOf(".exe "_L1, 0, Qt::CaseInsensitive);
162 if (exeIndex != -1) {
163 command.insert(exeIndex + 4, doubleQuote);
164 command.prepend(doubleQuote);
165 }
166 }
167 // Pass the url as the parameter. Should use QProcess::startDetached(),
168 // but that cannot handle a Windows command line [yet].
169 command.replace("%1"_L1, url.toString(QUrl::FullyEncoded));
170 qCDebug(lcQpaServices) << "Launching" << command;
171 //start the process
172 PROCESS_INFORMATION pi;
173 ZeroMemory(&pi, sizeof(pi));
174 STARTUPINFO si;
175 ZeroMemory(&si, sizeof(si));
176 si.cb = sizeof(si);
177 if (!CreateProcess(nullptr, reinterpret_cast<wchar_t *>(const_cast<ushort *>(command.utf16())),
178 nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
179 qErrnoWarning("Unable to launch '%ls'", qUtf16Printable(command));
180 return false;
181 }
182 CloseHandle(pi.hProcess);
183 CloseHandle(pi.hThread);
184 return true;
185}
186
187bool QWindowsServices::openUrl(const QUrl &url)
188{
189 const QString scheme = url.scheme();
190 if (scheme == u"mailto" && launchMail(url))
191 return true;
192 return url.isLocalFile() && url.hasFragment()
193 ? openWebBrowser(url) : shellExecute(url);
194}
195
196bool QWindowsServices::openDocument(const QUrl &url)
197{
198 return shellExecute(url);
199}
200
201QT_END_NAMESPACE
QWindowsShellExecuteThread(const wchar_t *operation, const wchar_t *file, const wchar_t *parameters)
static bool launchMail(const QUrl &url)
static QString msgShellExecuteFailed(const QUrl &url, quintptr code)
static QString mailCommand()
static bool shellExecute(const QUrl &url)
static bool openWebBrowser(const QUrl &url)