diff --git a/src/Magpie.App/App.cpp b/src/Magpie.App/App.cpp index 3c95a78d..db654c6d 100644 --- a/src/Magpie.App/App.cpp +++ b/src/Magpie.App/App.cpp @@ -30,7 +30,7 @@ #include "EffectsService.h" #include "UpdateService.h" #include "LocalizationService.h" -#include "Logger.h" +#include "ToastService.h" namespace winrt::Magpie::App::implementation { @@ -47,17 +47,7 @@ App::App() { EffectsService::Get().StartInitialize(); // 初始化 XAML 框架 - _windowsXamlManager = Hosting::WindowsXamlManager::InitializeForCurrentThread(); - - const bool isWin11 = Win32Utils::GetOSVersion().IsWin11(); - if (!isWin11) { - // Win10 中隐藏 DesktopWindowXamlSource 窗口 - if (CoreWindow coreWindow = CoreWindow::GetForCurrentThread()) { - HWND hwndDWXS; - coreWindow.as()->get_WindowHandle(&hwndDWXS); - ShowWindow(hwndDWXS, SW_HIDE); - } - } + _xamlManagerWrapper.emplace(); LocalizationService::Get().EarlyInitialize(); } @@ -72,8 +62,7 @@ void App::Close() { } _isClosed = true; - _windowsXamlManager.Close(); - _windowsXamlManager = nullptr; + _xamlManagerWrapper.reset(); Exit(); } @@ -98,6 +87,7 @@ StartUpOptions App::Initialize(int) { result.IsNeedElevated = settings.IsAlwaysRunAsAdmin(); LocalizationService::Get().Initialize(); + ToastService::Get().Initialize(); ShortcutService::Get().Initialize(); ScalingService::Get().Initialize(); UpdateService::Get().Initialize(); @@ -110,6 +100,7 @@ void App::Uninitialize() { // 不显示托盘图标的情况下关闭主窗口仍会在后台驻留数秒,推测和 XAML Islands 有关 // 这里提前取消热键注册,这样关闭 Magpie 后立即重新打开不会注册热键失败 ShortcutService::Get().Uninitialize(); + ToastService::Get().Uninitialize(); } bool App::IsShowNotifyIcon() const noexcept { diff --git a/src/Magpie.App/App.h b/src/Magpie.App/App.h index 16e74403..09b2d347 100644 --- a/src/Magpie.App/App.h +++ b/src/Magpie.App/App.h @@ -1,6 +1,6 @@ #pragma once #include "App.g.h" -#include +#include "XamlHostingHelper.h" namespace winrt::Magpie::App::implementation { @@ -44,7 +44,7 @@ class App : public App_base { void Restart() const noexcept; private: - Hosting::WindowsXamlManager _windowsXamlManager{ nullptr }; + std::optional _xamlManagerWrapper; weak_ref _rootPage{ nullptr }; HWND _hwndMain = NULL; bool _isClosed = false; diff --git a/src/Magpie.App/Magpie.App.vcxproj b/src/Magpie.App/Magpie.App.vcxproj index 494cdae1..592054db 100644 --- a/src/Magpie.App/Magpie.App.vcxproj +++ b/src/Magpie.App/Magpie.App.vcxproj @@ -228,12 +228,18 @@ TitleBarControl.xaml Code + + ToastPage.xaml + Code + + WrapPanel.idl Code + @@ -403,14 +409,24 @@ TitleBarControl.xaml Code + + ToastPage.xaml + Code + + WrapPanel.idl Code + + + ToastPage.xaml + Code + Designer @@ -591,6 +607,9 @@ Designer + + Designer + diff --git a/src/Magpie.App/Magpie.App.vcxproj.filters b/src/Magpie.App/Magpie.App.vcxproj.filters index 46e5e8df..b632087c 100644 --- a/src/Magpie.App/Magpie.App.vcxproj.filters +++ b/src/Magpie.App/Magpie.App.vcxproj.filters @@ -59,6 +59,12 @@ Helpers + + Services + + + Helpers + @@ -123,6 +129,12 @@ Helpers + + Services + + + Helpers + @@ -282,6 +294,9 @@ Styles + + Pages + diff --git a/src/Magpie.App/ToastPage.cpp b/src/Magpie.App/ToastPage.cpp new file mode 100644 index 00000000..754936ef --- /dev/null +++ b/src/Magpie.App/ToastPage.cpp @@ -0,0 +1,9 @@ +#include "pch.h" +#include "ToastPage.h" +#if __has_include("ToastPage.g.cpp") +#include "ToastPage.g.cpp" +#endif + +namespace winrt::Magpie::App::implementation { + +} diff --git a/src/Magpie.App/ToastPage.h b/src/Magpie.App/ToastPage.h new file mode 100644 index 00000000..aed470f4 --- /dev/null +++ b/src/Magpie.App/ToastPage.h @@ -0,0 +1,13 @@ +#pragma once +#include "ToastPage.g.h" + +namespace winrt::Magpie::App::implementation { + struct ToastPage : ToastPageT { + + }; +} + +namespace winrt::Magpie::App::factory_implementation { + struct ToastPage : ToastPageT { + }; +} diff --git a/src/Magpie.App/ToastPage.idl b/src/Magpie.App/ToastPage.idl new file mode 100644 index 00000000..d6d74fd4 --- /dev/null +++ b/src/Magpie.App/ToastPage.idl @@ -0,0 +1,9 @@ +namespace Magpie.App { + [default_interface] + runtimeclass ToastPage : Windows.UI.Xaml.Controls.Page { + ToastPage(); + + // https://github.com/microsoft/microsoft-ui-xaml/issues/7579 + void UnloadObject(Windows.UI.Xaml.DependencyObject object); + } +} diff --git a/src/Magpie.App/ToastPage.xaml b/src/Magpie.App/ToastPage.xaml new file mode 100644 index 00000000..a62cd060 --- /dev/null +++ b/src/Magpie.App/ToastPage.xaml @@ -0,0 +1,15 @@ + + + + + diff --git a/src/Magpie.App/ToastService.cpp b/src/Magpie.App/ToastService.cpp new file mode 100644 index 00000000..967e4404 --- /dev/null +++ b/src/Magpie.App/ToastService.cpp @@ -0,0 +1,113 @@ +#include "pch.h" +#include "ToastService.h" +#include "XamlHostingHelper.h" +#include "CommonSharedConstants.h" +#include "Utils.h" +#include + +using namespace winrt; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Hosting; + +namespace winrt::Magpie::App { + +void ToastService::Initialize() noexcept { + _toastThread = std::thread(std::bind_front(&ToastService::_ToastThreadProc, this)); +} + +void ToastService::Uninitialize() noexcept { + if (!_toastThread.joinable()) { + return; + } + + const HANDLE hToastThread = _toastThread.native_handle(); + + if (!wil::handle_wait(hToastThread, 0)) { + const DWORD threadId = GetThreadId(hToastThread); + + // 持续尝试直到 _toastThread 创建了消息队列 + while (!PostThreadMessage(threadId, CommonSharedConstants::WM_TOAST_QUIT, 0, 0)) { + if (wil::handle_wait(hToastThread, 1)) { + break; + } + } + } + + _toastThread.join(); +} + +void ToastService::_ToastThreadProc() noexcept { +#ifdef _DEBUG + SetThreadDescription(GetCurrentThread(), L"Toast 线程"); +#endif + + winrt::init_apartment(winrt::apartment_type::single_threaded); + + static Utils::Ignore _ = [] { + WNDCLASSEXW wcex{ + .cbSize = sizeof(wcex), + .lpfnWndProc = DefWindowProc, + .hInstance = wil::GetModuleInstanceHandle(), + .lpszClassName = CommonSharedConstants::TOAST_WINDOW_CLASS_NAME + }; + RegisterClassEx(&wcex); + + return Utils::Ignore(); + }(); + + // 创建窗口失败也应进入消息循环。Win10 中关闭任意线程的 DesktopWindowXamlSource 都会使主线程会崩溃, + // 在程序退出前,xamlSource 不能析构。见 https://github.com/microsoft/terminal/pull/15397 + HWND hwndToast = CreateWindowEx( + WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW, + CommonSharedConstants::TOAST_WINDOW_CLASS_NAME, + L"Toast", + WS_POPUP | WS_VISIBLE, + 200, 200, 0, 0, + NULL, + NULL, + wil::GetModuleInstanceHandle(), + nullptr + ); + + // DesktopWindowXamlSource 在控件之前创建则无需调用 WindowsXamlManager::InitializeForCurrentThread + DesktopWindowXamlSource xamlSource; + com_ptr xamlSourceNative2 = + xamlSource.try_as(); + + xamlSourceNative2->AttachToWindow(hwndToast); + + HWND hwndXamlIsland; + xamlSourceNative2->get_WindowHandle(&hwndXamlIsland); + SetWindowPos(hwndXamlIsland, NULL, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOZORDER | SWP_SHOWWINDOW); + + ToastPage toastPage; + xamlSource.Content(toastPage); + + auto tt = toastPage.FindName(L"MessageTeachingTip").as(); + tt.IsOpen(true); + + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) { + if (msg.message == CommonSharedConstants::WM_TOAST_QUIT) { + DestroyWindow(hwndToast); + break; + } + + { + BOOL processed = FALSE; + HRESULT hr = xamlSourceNative2->PreTranslateMessage(&msg, &processed); + if (SUCCEEDED(hr) && processed) { + continue; + } + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // 必须手动重置 Content,否则会内存泄露 + xamlSource.Content(nullptr); + xamlSource.Close(); +} + +} diff --git a/src/Magpie.App/ToastService.h b/src/Magpie.App/ToastService.h new file mode 100644 index 00000000..d7e7fca2 --- /dev/null +++ b/src/Magpie.App/ToastService.h @@ -0,0 +1,28 @@ +#pragma once +#include + +namespace winrt::Magpie::App { + +class ToastService { +public: + static ToastService& Get() noexcept { + static ToastService instance; + return instance; + } + + ToastService(const ToastService&) = delete; + ToastService(ToastService&&) = delete; + + void Initialize() noexcept; + + void Uninitialize() noexcept; + +private: + ToastService() = default; + + void _ToastThreadProc() noexcept; + + std::thread _toastThread; +}; + +} diff --git a/src/Magpie.App/XamlHostingHelper.cpp b/src/Magpie.App/XamlHostingHelper.cpp new file mode 100644 index 00000000..fa54a2c6 --- /dev/null +++ b/src/Magpie.App/XamlHostingHelper.cpp @@ -0,0 +1,39 @@ +#include "pch.h" +#include "XamlHostingHelper.h" +#include +#include "Win32Utils.h" + +using namespace winrt; +using namespace Windows::UI::Xaml::Hosting; + +namespace winrt::Magpie::App { + +XamlHostingHelper::ManagerWrapper::ManagerWrapper() { + _windowsXamlManager = WindowsXamlManager::InitializeForCurrentThread(); + + if (!Win32Utils::GetOSVersion().IsWin11()) { + // Win10 中隐藏 DesktopWindowXamlSource 窗口 + if (CoreWindow coreWindow = CoreWindow::GetForCurrentThread()) { + HWND hwndDWXS; + coreWindow.as()->get_WindowHandle(&hwndDWXS); + ShowWindow(hwndDWXS, SW_HIDE); + } + } +} + +XamlHostingHelper::ManagerWrapper::~ManagerWrapper() { + if (!_windowsXamlManager) { + return; + } + + _windowsXamlManager.Close(); + + // 做最后的清理,见 + // https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.hosting.windowsxamlmanager.close + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { + DispatchMessage(&msg); + } +} + +} diff --git a/src/Magpie.App/XamlHostingHelper.h b/src/Magpie.App/XamlHostingHelper.h new file mode 100644 index 00000000..7e8bf724 --- /dev/null +++ b/src/Magpie.App/XamlHostingHelper.h @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace winrt::Magpie::App { + +struct XamlHostingHelper { + class ManagerWrapper { + public: + ManagerWrapper(); + ~ManagerWrapper(); + + ManagerWrapper(const ManagerWrapper&) = delete; + ManagerWrapper(ManagerWrapper&&) = default; + + private: + Hosting::WindowsXamlManager _windowsXamlManager{ nullptr }; + }; +}; + +} diff --git a/src/Magpie.Core/Renderer.cpp b/src/Magpie.Core/Renderer.cpp index e7f6e17a..59ea0b95 100644 --- a/src/Magpie.Core/Renderer.cpp +++ b/src/Magpie.Core/Renderer.cpp @@ -27,11 +27,19 @@ Renderer::~Renderer() noexcept { _hKeyboardHook.reset(); if (_backendThread.joinable()) { - DWORD backendThreadId = GetThreadId(_backendThread.native_handle()); - // 持续尝试直到 _backendThread 创建了消息队列 - while (!PostThreadMessage(backendThreadId, WM_QUIT, 0, 0)) { - Sleep(1); + const HANDLE hThread = _backendThread.native_handle(); + + if (!wil::handle_wait(hThread, 0)) { + const DWORD threadId = GetThreadId(_backendThread.native_handle()); + + // 持续尝试直到 _backendThread 创建了消息队列 + while (!PostThreadMessage(threadId, WM_QUIT, 0, 0)) { + if (wil::handle_wait(hThread, 1)) { + break; + } + } } + _backendThread.join(); } } diff --git a/src/Magpie.Core/ScalingRuntime.cpp b/src/Magpie.Core/ScalingRuntime.cpp index e3745b66..c1ff4ebe 100644 --- a/src/Magpie.Core/ScalingRuntime.cpp +++ b/src/Magpie.Core/ScalingRuntime.cpp @@ -7,7 +7,7 @@ namespace Magpie::Core { ScalingRuntime::ScalingRuntime() : - _scalingThread(std::bind(&ScalingRuntime::_ScalingThreadProc, this)) { + _scalingThread(std::bind_front(&ScalingRuntime::_ScalingThreadProc, this)) { } ScalingRuntime::~ScalingRuntime() { @@ -16,28 +16,30 @@ ScalingRuntime::~ScalingRuntime() { if (_scalingThread.joinable()) { const HANDLE hScalingThread = _scalingThread.native_handle(); - { - const DWORD magWndThreadId = GetThreadId(hScalingThread); + if (!wil::handle_wait(hScalingThread, 0)) { + const DWORD threadId = GetThreadId(hScalingThread); // 持续尝试直到 _scalingThread 创建了消息队列 - while (!PostThreadMessage(magWndThreadId, WM_QUIT, 0, 0)) { - Sleep(0); + while (!PostThreadMessage(threadId, WM_QUIT, 0, 0)) { + if (wil::handle_wait(hScalingThread, 1)) { + break; + } } - } - // 等待缩放线程退出,在此期间必须处理消息队列,否则缩放线程调用 - // SetWindowLongPtr 会导致死锁 - while (true) { - MSG msg; - while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } + // 等待缩放线程退出,在此期间必须处理消息队列,否则缩放线程调用 + // SetWindowLongPtr 会导致死锁 + while (true) { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } - if (MsgWaitForMultipleObjectsEx(1, &hScalingThread, - INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE) == WAIT_OBJECT_0) { - // WAIT_OBJECT_0 表示缩放线程已退出 - // WAIT_OBJECT_0 + 1 表示有新消息 - break; + if (MsgWaitForMultipleObjectsEx(1, &hScalingThread, + INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE) == WAIT_OBJECT_0) { + // WAIT_OBJECT_0 表示缩放线程已退出 + // WAIT_OBJECT_0 + 1 表示有新消息 + break; + } } } diff --git a/src/Magpie.Core/ScalingRuntime.h b/src/Magpie.Core/ScalingRuntime.h index 4ce5cd02..daceb6b8 100644 --- a/src/Magpie.Core/ScalingRuntime.h +++ b/src/Magpie.Core/ScalingRuntime.h @@ -30,6 +30,8 @@ class ScalingRuntime { // 确保 _dispatcher 完成初始化 const winrt::DispatcherQueue& _Dispatcher() noexcept; + std::thread _scalingThread; + enum class _State { Idle, Initializing, @@ -41,8 +43,6 @@ class ScalingRuntime { std::atomic _dispatcherInitialized = false; // 只能在主线程访问,省下检查 _dispatcherInitialized 的开销 bool _dispatcherInitializedCache = false; - // 应在 _dispatcher 后初始化 - std::thread _scalingThread; }; } diff --git a/src/Magpie/XamlWindow.h b/src/Magpie/XamlWindow.h index 01a2a8a5..b3750b49 100644 --- a/src/Magpie/XamlWindow.h +++ b/src/Magpie/XamlWindow.h @@ -82,10 +82,8 @@ class XamlWindowT { // 初始化 XAML Islands _xamlSource = winrt::DesktopWindowXamlSource(); _xamlSourceNative2 = _xamlSource.as(); - - auto interop = _xamlSource.as(); - interop->AttachToWindow(_hWnd); - interop->get_WindowHandle(&_hwndXamlIsland); + _xamlSourceNative2->AttachToWindow(_hWnd); + _xamlSourceNative2->get_WindowHandle(&_hwndXamlIsland); _xamlSource.Content(content); // 焦点始终位于 _hwndXamlIsland 中 diff --git a/src/Shared/CommonSharedConstants.h b/src/Shared/CommonSharedConstants.h index 5eb5ce2c..b341226a 100644 --- a/src/Shared/CommonSharedConstants.h +++ b/src/Shared/CommonSharedConstants.h @@ -12,6 +12,7 @@ struct CommonSharedConstants { static constexpr const wchar_t* DDF_WINDOW_CLASS_NAME = L"Window_Magpie_C322D752-C866-4630-91F5-32CB242A8930"; static constexpr const wchar_t* TOUCH_HELPER_WINDOW_CLASS_NAME = L"Magpie_TouchHelper"; static constexpr const wchar_t* TOUCH_HELPER_HOLE_WINDOW_CLASS_NAME = L"Magpie_TouchHelperHole"; + static constexpr const wchar_t* TOAST_WINDOW_CLASS_NAME = L"Magpie_Toast"; static constexpr const wchar_t* TOUCH_HELPER_EXE_NAME = L"TouchHelper.exe"; // TouchHelper 有重要更改则提高版本号 @@ -42,6 +43,7 @@ struct CommonSharedConstants { static constexpr UINT WM_NOTIFY_ICON = WM_USER; static constexpr UINT WM_QUIT_MAGPIE = WM_USER + 1; static constexpr UINT WM_RESTART_MAGPIE = WM_USER + 2; + static constexpr UINT WM_TOAST_QUIT = WM_USER + 3; static constexpr const wchar_t* WM_MAGPIE_SHOWME = L"WM_MAGPIE_SHOWME"; static constexpr const wchar_t* WM_MAGPIE_QUIT = L"WM_MAGPIE_QUIT";