Skip to content

Commit

Permalink
feat: 初步实现弹窗随源窗口移动
Browse files Browse the repository at this point in the history
  • Loading branch information
Blinue committed Oct 16, 2024
1 parent 7e0a118 commit cf6ae6c
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 64 deletions.
2 changes: 1 addition & 1 deletion src/Magpie.App/HomePage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ void HomePage::SimulateExclusiveFullscreenToggleSwitch_Toggled(IInspectable cons
}

// 这个回调被触发时 UI 还没有更新,需要异步处理
Dispatcher().TryRunAsync(CoreDispatcherPriority::Low, [weakThis(get_weak())]() {
Dispatcher().RunAsync(CoreDispatcherPriority::Low, [weakThis(get_weak())]() {
auto strongThis = weakThis.get();
if (!strongThis) {
return;
Expand Down
6 changes: 3 additions & 3 deletions src/Magpie.App/RootPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ fire_and_forget RootPage::NavigationView_ItemInvoked(MUXC::NavigationView const&
_newProfileViewModel.PrepareForOpen(dpi, isLightTheme, Dispatcher());

// 同步调用 ShowAt 有时会失败
co_await Dispatcher().TryRunAsync(CoreDispatcherPriority::Normal, [this]() {
NewProfileFlyout().ShowAt(NewProfileNavigationViewItem());
});
co_await Dispatcher();

NewProfileFlyout().ShowAt(NewProfileNavigationViewItem());
}
}

Expand Down
16 changes: 7 additions & 9 deletions src/Magpie.App/ShortcutService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,13 @@ LRESULT CALLBACK ShortcutService::_LowLevelKeyboardProc(int nCode, WPARAM wParam
that._keyboardHookShortcutActivated = true;

// 延迟执行回调以缩短钩子的处理时间
[](ShortcutAction action) -> fire_and_forget {
co_await CoreWindow::GetForCurrentThread().Dispatcher().TryRunAsync(
CoreDispatcherPriority::Normal,
[action]() {
Logger::Get().Info(fmt::format("热键 {} 激活(Keyboard Hook)", ShortcutHelper::ToString(action)));
Get()._FireShortcut(action);
}
);
}(action);
CoreWindow::GetForCurrentThread().Dispatcher().RunAsync(
CoreDispatcherPriority::Normal,
[action]() {
Logger::Get().Info(fmt::format("热键 {} 激活(Keyboard Hook)", ShortcutHelper::ToString(action)));
Get()._FireShortcut(action);
}
);

if (curKeys.win && !curKeys.ctrl && !curKeys.shift && !curKeys.alt) {
// 防止激活开始菜单
Expand Down
74 changes: 36 additions & 38 deletions src/Magpie.App/ToastPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,59 @@ using namespace Windows::UI::Xaml::Controls;

namespace winrt::Magpie::App::implementation {

fire_and_forget ToastPage::ShowMessage(const hstring& message) {
MUXC::TeachingTip ToastPage::ShowMessage(const hstring& message) {
// !!! HACK !!!
// 重用 TeachingTip 有一个 bug: 前一个 Toast 正在消失时新的 Toast 不会显示。为了
// 规避它,我们每次都创建新的 TeachingTip,但要保留旧对象的引用,因为播放动画时销毁
// 会导致崩溃。oldToastTeachingTip 的生存期可确保动画播放完毕。
MUXC::TeachingTip oldTeachingTip = MessageTeachingTip();
if (oldTeachingTip) {
// 先卸载再关闭,始终关闭 TeachingTip 确保调用者可以检查是否可见
UnloadObject(oldTeachingTip);
oldTeachingTip.IsOpen(false);
}

weak_ref<MUXC::TeachingTip> weakTeachingTip;
{
// 创建新的 TeachingTip
MUXC::TeachingTip newTeachingTip = FindName(L"MessageTeachingTip").as<MUXC::TeachingTip>();
MessageTextBlock().Text(message);
newTeachingTip.IsOpen(true);
// 创建新的 TeachingTip
MUXC::TeachingTip curTeachingTip = FindName(L"MessageTeachingTip").as<MUXC::TeachingTip>();
MessageTextBlock().Text(message);
curTeachingTip.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<IControlProtected>();
// !!! HACK !!!
// 移除关闭按钮。必须在模板加载完成后做,TeachingTip 没有 Opening 事件,但可以监听 MessageTextBlock 的
// LayoutUpdated 事件,它在 TeachingTip 显示前必然会被引发。
MessageTextBlock().LayoutUpdated([weak(weak_ref(curTeachingTip))](IInspectable const&, IInspectable const&) {
auto teachingTip = weak.get();
if (!teachingTip) {
return;
}

// 隐藏关闭按钮
if (DependencyObject closeButton = protectedAccessor.GetTemplateChild(L"AlternateCloseButton")) {
closeButton.as<FrameworkElement>().Visibility(Visibility::Collapsed);
}
IControlProtected protectedAccessor = teachingTip.as<IControlProtected>();

// 减小 Flyout 尺寸
if (DependencyObject container = protectedAccessor.GetTemplateChild(L"TailOcclusionGrid")) {
container.as<FrameworkElement>().MinWidth(0.0);
}
});
// 隐藏关闭按钮
if (DependencyObject closeButton = protectedAccessor.GetTemplateChild(L"AlternateCloseButton")) {
closeButton.as<FrameworkElement>().Visibility(Visibility::Collapsed);
}

weakTeachingTip = newTeachingTip;
}
// 减小 Flyout 尺寸
if (DependencyObject container = protectedAccessor.GetTemplateChild(L"TailOcclusionGrid")) {
container.as<FrameworkElement>().MinWidth(0.0);
}
});

auto weakThis = get_weak();
CoreDispatcher dispatcher = Dispatcher();
// 显示时长固定 2 秒
co_await 2s;
co_await dispatcher;
// 第三个参数用于延长 oldTeachingTip 的生存期,确保关闭动画播放完毕
[](CoreDispatcher dispatcher, weak_ref<MUXC::TeachingTip> weakCurTeachingTip, MUXC::TeachingTip) -> fire_and_forget {
// 显示时长固定 2 秒
co_await 2s;
co_await dispatcher;

if (weakThis.get()) {
MUXC::TeachingTip curTeachingTip = MessageTeachingTip();
if (curTeachingTip == weakTeachingTip.get()) {
// 如果已经显示新的 Toast 则无需关闭,因为 newTeachingTip 已被卸载(但仍在生存期内)
MUXC::TeachingTip curTeachingTip = weakCurTeachingTip.get();
// 如果 curTeachingTip 已被卸载则无需关闭
if (curTeachingTip && curTeachingTip.IsLoaded()) {
curTeachingTip.IsOpen(false);
}
}
}(Dispatcher(), curTeachingTip, oldTeachingTip);

return curTeachingTip;
}

}
2 changes: 1 addition & 1 deletion src/Magpie.App/ToastPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace winrt::Magpie::App::implementation {

struct ToastPage : ToastPageT<ToastPage> {
fire_and_forget ShowMessage(const hstring& message);
MUXC::TeachingTip ShowMessage(const hstring& message);
};

}
Expand Down
2 changes: 1 addition & 1 deletion src/Magpie.App/ToastPage.idl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Magpie.App {
runtimeclass ToastPage : Windows.UI.Xaml.Controls.Page {
ToastPage();

void ShowMessage(String message);
Microsoft.UI.Xaml.Controls.TeachingTip ShowMessage(String message);

// https://github.com/microsoft/microsoft-ui-xaml/issues/7579
void UnloadObject(Windows.UI.Xaml.DependencyObject object);
Expand Down
53 changes: 48 additions & 5 deletions src/Magpie.App/ToastService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>
#include "Win32Utils.h"
#include "XamlUtils.h"

using namespace winrt;
using namespace Windows::UI::Xaml::Controls;
Expand Down Expand Up @@ -38,12 +39,12 @@ void ToastService::Uninitialize() noexcept {
}

void ToastService::ShowMessageOnWindow(std::wstring_view message, HWND hWnd) noexcept {
_Dispatcher().TryRunAsync(CoreDispatcherPriority::Normal, [this, capturedMessage(std::wstring(message)), hWnd]() {
_Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this, capturedMessage(std::wstring(message)), hWnd]() {
RECT frameRect;
if (!Win32Utils::GetWindowFrameRect(hWnd, frameRect)) {
return;
}

// 更改所有者关系使弹窗始终在 hWnd 上方
SetWindowLongPtr(_hwndToast, GWLP_HWNDPARENT, (LONG_PTR)hWnd);
// _hwndToast 的输入已被附加到了 hWnd 上,这是所有者窗口的默认行为,但我们不需要。
Expand All @@ -64,7 +65,34 @@ void ToastService::ShowMessageOnWindow(std::wstring_view message, HWND hWnd) noe
SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOREDRAW
);

_toastPage.ShowMessage(capturedMessage);
MUXC::TeachingTip teachingTip = _toastPage.ShowMessage(capturedMessage);

// 定期更新弹窗位置
[](CoreDispatcher dispatcher, MUXC::TeachingTip teachingTip, HWND hWnd, HWND hwndToast) -> fire_and_forget {
do {
co_await 10ms;
co_await dispatcher;

if (!IsWindow(hwndToast)) {
break;
}

RECT frameRect;
if (!Win32Utils::GetWindowFrameRect(hWnd, frameRect)) {
break;
}

SetWindowPos(
hwndToast,
NULL,
(frameRect.left + frameRect.right) / 2,
(frameRect.top + frameRect.bottom * 4) / 5,
0,
0,
SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOREDRAW
);
} while (teachingTip.IsOpen());
}(_dispatcher, std::move(teachingTip), hWnd, _hwndToast);
});
}

Expand All @@ -78,7 +106,7 @@ void ToastService::_ToastThreadProc() noexcept {
static Utils::Ignore _ = [] {
WNDCLASSEXW wcex{
.cbSize = sizeof(wcex),
.lpfnWndProc = DefWindowProc,
.lpfnWndProc = _ToastWndProc,
.hInstance = wil::GetModuleInstanceHandle(),
.lpszClassName = CommonSharedConstants::TOAST_WINDOW_CLASS_NAME
};
Expand All @@ -93,13 +121,15 @@ void ToastService::_ToastThreadProc() noexcept {
WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT,
CommonSharedConstants::TOAST_WINDOW_CLASS_NAME,
L"Toast",
WS_POPUP | WS_VISIBLE,
WS_POPUP,
0, 0, 0, 0,
NULL,
NULL,
wil::GetModuleInstanceHandle(),
nullptr
);
SetWindowPos(_hwndToast, NULL, 0, 0, 0, 0,
SWP_SHOWWINDOW | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW | SWP_NOZORDER);

// DesktopWindowXamlSource 在控件之前创建则无需调用 WindowsXamlManager::InitializeForCurrentThread
DesktopWindowXamlSource xamlSource;
Expand Down Expand Up @@ -144,6 +174,19 @@ void ToastService::_ToastThreadProc() noexcept {
xamlSource.Close();
}

LRESULT ToastService::_ToastWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (msg == WM_MOVE) {
if (Get()._toastPage) {
// 使弹窗随窗口移动
XamlUtils::RepositionXamlPopups(Get()._toastPage.XamlRoot(), false);
}

return 0;
}

return DefWindowProc(hWnd, msg, wParam, lParam);
}

const CoreDispatcher& ToastService::_Dispatcher() noexcept {
if (!_dispatcherInitializedCache) {
_dispatcherInitialized.wait(false, std::memory_order_acquire);
Expand Down
4 changes: 3 additions & 1 deletion src/Magpie.App/ToastService.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class ToastService {

void _ToastThreadProc() noexcept;

// 确保 _dispatcher 完成初始化
static LRESULT CALLBACK _ToastWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

// 确保 _dispatcher 完成初始化。供主线程使用,toast 线程应直接使用 _dispatcher
const CoreDispatcher& _Dispatcher() noexcept;

std::thread _toastThread;
Expand Down
8 changes: 3 additions & 5 deletions src/Magpie/XamlWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,9 @@ class XamlWindowT {
PostMessage(hwndDWXS, WM_SIZE, wParam, lParam);
}

[](C const& content)->winrt::fire_and_forget {
co_await content.Dispatcher().RunAsync(winrt::CoreDispatcherPriority::Normal, [xamlRoot(content.XamlRoot())]() {
XamlUtils::RepositionXamlPopups(xamlRoot, true);
});
}(_content);
_content.Dispatcher().RunAsync(winrt::CoreDispatcherPriority::Normal, [xamlRoot(_content.XamlRoot())]() {
XamlUtils::RepositionXamlPopups(xamlRoot, true);
});
}
}

Expand Down

0 comments on commit cf6ae6c

Please sign in to comment.