diff --git a/src/Magpie.App/ToastPage.cpp b/src/Magpie.App/ToastPage.cpp index 754936ef..54e2a19e 100644 --- a/src/Magpie.App/ToastPage.cpp +++ b/src/Magpie.App/ToastPage.cpp @@ -4,6 +4,66 @@ #include "ToastPage.g.cpp" #endif +using namespace winrt; +using namespace Windows::UI::Xaml::Controls; + namespace winrt::Magpie::App::implementation { - + +IAsyncAction ToastPage::ShowMessage(const hstring& message) { + // !!! HACK !!! + // 重用 TeachingTip 有一个 bug: 前一个 Toast 正在消失时新的 Toast 不会显示。为了 + // 规避它,我们每次都创建新的 TeachingTip,但要保留旧对象的引用,因为播放动画时销毁 + // 会导致崩溃。oldToastTeachingTip 的生存期可确保动画播放完毕。 + MUXC::TeachingTip oldTeachingTip = MessageTeachingTip(); + if (oldTeachingTip) { + UnloadObject(oldTeachingTip); + } + + weak_ref weakTeachingTip; + { + // 创建新的 TeachingTip + MUXC::TeachingTip newTeachingTip = FindName(L"MessageTeachingTip").as(); + MessageTextBlock().Text(message); + newTeachingTip.IsOpen(true); + + // !!! HACK !!! + // 移除关闭按钮。必须在模板加载完成后做,TeachingTip 没有 Opening 事件,但可以监听 MessageTextBlock 的 + // LayoutUpdated 事件,它在 TeachingTip 显示前必然会被引发。 + MessageTextBlock().LayoutUpdated([weak(weak_ref(newTeachingTip))](IInspectable const&, IInspectable const&) { + auto teachingTip = weak.get(); + if (!teachingTip) { + return; + } + + IControlProtected protectedAccessor = teachingTip.as(); + + // 隐藏关闭按钮 + if (DependencyObject closeButton = protectedAccessor.GetTemplateChild(L"AlternateCloseButton")) { + closeButton.as().Visibility(Visibility::Collapsed); + } + + // 减小 Flyout 尺寸 + if (DependencyObject container = protectedAccessor.GetTemplateChild(L"TailOcclusionGrid")) { + container.as().MinWidth(0.0); + } + }); + + weakTeachingTip = newTeachingTip; + } + + auto weakThis = get_weak(); + CoreDispatcher dispatcher = Dispatcher(); + // 显示时长固定 2 秒 + co_await 2s; + co_await dispatcher; + + if (weakThis.get()) { + MUXC::TeachingTip curTeachingTip = MessageTeachingTip(); + if (curTeachingTip == weakTeachingTip.get()) { + // 如果已经显示新的 Toast 则无需关闭,因为 newTeachingTip 已被卸载(但仍在生存期内) + curTeachingTip.IsOpen(false); + } + } +} + } diff --git a/src/Magpie.App/ToastPage.h b/src/Magpie.App/ToastPage.h index aed470f4..b667e1ea 100644 --- a/src/Magpie.App/ToastPage.h +++ b/src/Magpie.App/ToastPage.h @@ -2,12 +2,16 @@ #include "ToastPage.g.h" namespace winrt::Magpie::App::implementation { - struct ToastPage : ToastPageT { - - }; + +struct ToastPage : ToastPageT { + IAsyncAction ShowMessage(const hstring& message); +}; + } namespace winrt::Magpie::App::factory_implementation { - struct ToastPage : ToastPageT { - }; + +struct ToastPage : ToastPageT { +}; + } diff --git a/src/Magpie.App/ToastPage.idl b/src/Magpie.App/ToastPage.idl index d6d74fd4..6b131b5f 100644 --- a/src/Magpie.App/ToastPage.idl +++ b/src/Magpie.App/ToastPage.idl @@ -1,7 +1,8 @@ namespace Magpie.App { - [default_interface] runtimeclass ToastPage : Windows.UI.Xaml.Controls.Page { ToastPage(); + + Windows.Foundation.IAsyncAction ShowMessage(String message); // 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 index a62cd060..981d96e7 100644 --- a/src/Magpie.App/ToastPage.xaml +++ b/src/Magpie.App/ToastPage.xaml @@ -7,9 +7,9 @@ xmlns:muxc="using:Microsoft.UI.Xaml.Controls" mc:Ignorable="d"> - + diff --git a/src/Magpie.App/ToastService.cpp b/src/Magpie.App/ToastService.cpp index 637fde9a..6629aa06 100644 --- a/src/Magpie.App/ToastService.cpp +++ b/src/Magpie.App/ToastService.cpp @@ -36,6 +36,12 @@ void ToastService::Uninitialize() noexcept { _toastThread.join(); } +void ToastService::ShowMessage(std::wstring_view message) noexcept { + _Dispatcher().TryRunAsync(CoreDispatcherPriority::Normal, [this, captured(std::wstring(message))]() { + _toastPage.ShowMessage(captured); + }); +} + void ToastService::_ToastThreadProc() noexcept { #ifdef _DEBUG SetThreadDescription(GetCurrentThread(), L"Toast 线程"); @@ -58,11 +64,11 @@ void ToastService::_ToastThreadProc() noexcept { // 创建窗口失败也应进入消息循环。Win10 中关闭任意线程的 DesktopWindowXamlSource 都会使主线程会崩溃, // 在程序退出前,xamlSource 不能析构。见 https://github.com/microsoft/terminal/pull/15397 HWND hwndToast = CreateWindowEx( - WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW, + WS_EX_TOPMOST | WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW, CommonSharedConstants::TOAST_WINDOW_CLASS_NAME, L"Toast", WS_POPUP | WS_VISIBLE, - 200, 200, 0, 0, + 0, 0, 0, 0, NULL, NULL, wil::GetModuleInstanceHandle(), @@ -80,11 +86,13 @@ void ToastService::_ToastThreadProc() noexcept { xamlSourceNative2->get_WindowHandle(&hwndXamlIsland); SetWindowPos(hwndXamlIsland, NULL, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOZORDER | SWP_SHOWWINDOW); - ToastPage toastPage; - xamlSource.Content(toastPage); + _toastPage = ToastPage(); + xamlSource.Content(_toastPage); - auto tt = toastPage.FindName(L"MessageTeachingTip").as(); - tt.IsOpen(true); + _dispatcher = _toastPage.Dispatcher(); + // 如果主线程正在等待则唤醒主线程 + _dispatcherInitialized.store(true, std::memory_order_release); + _dispatcherInitialized.notify_one(); MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { @@ -110,4 +118,13 @@ void ToastService::_ToastThreadProc() noexcept { xamlSource.Close(); } +const CoreDispatcher& ToastService::_Dispatcher() noexcept { + if (!_dispatcherInitializedCache) { + _dispatcherInitialized.wait(false, std::memory_order_acquire); + _dispatcherInitializedCache = true; + } + + return _dispatcher; +} + } diff --git a/src/Magpie.App/ToastService.h b/src/Magpie.App/ToastService.h index d7e7fca2..f1c7ea43 100644 --- a/src/Magpie.App/ToastService.h +++ b/src/Magpie.App/ToastService.h @@ -17,12 +17,23 @@ class ToastService { void Uninitialize() noexcept; + void ShowMessage(std::wstring_view message) noexcept; + private: ToastService() = default; void _ToastThreadProc() noexcept; + // 确保 _dispatcher 完成初始化 + const CoreDispatcher& _Dispatcher() noexcept; + std::thread _toastThread; + + ToastPage _toastPage{ nullptr }; + CoreDispatcher _dispatcher{ nullptr }; + std::atomic _dispatcherInitialized = false; + // 只能在主线程访问,省下检查 _dispatcherInitialized 的开销 + bool _dispatcherInitializedCache = false; }; }