refactor:规范代码格式和注释

This commit is contained in:
ShaoHua
2026-04-06 22:59:16 +08:00
parent 758f6772c6
commit d00a907da0
34 changed files with 1095 additions and 200 deletions
+32
View File
@@ -0,0 +1,32 @@
#if !WINDOWS
using Microsoft.Maui.Controls;
namespace Hua.Todo.Maui;
/// <summary>
/// 非 Windows 平台下 <see cref="App"/> 的默认实现(分部类)。
/// 用于提供 Windows 专属分部方法的默认行为,避免在其它平台出现缺失实现的编译错误。
/// </summary>
public partial class App
{
/// <summary>
/// 非 Windows 平台不依赖 WinUI 句柄,因此视为已就绪。
/// </summary>
/// <param name="window">MAUI Window。</param>
/// <returns>始终返回 true。</returns>
private partial bool IsPlatformViewReady(Window window)
{
return true;
}
/// <summary>
/// 非 Windows 平台不处理“恢复/激活窗口”等特定逻辑,由调用方走默认 OpenWindow 流程。
/// </summary>
/// <param name="window">MAUI Window。</param>
/// <returns>始终返回 false。</returns>
private partial bool PlatformShowMainWindow(Window window)
{
return false;
}
}
#endif
+62 -166
View File
@@ -4,15 +4,13 @@ using System.IO;
using Hua.Todo.Maui.Services;
using Hua.Todo.Maui.Views;
using Hua.Todo.Maui.Models;
#if WINDOWS
using System.Runtime.InteropServices;
using Windowing = Microsoft.UI.Windowing;
using WinUiWindow = Microsoft.UI.Xaml.Window;
using WinRT.Interop;
#endif
namespace Hua.Todo.Maui;
/// <summary>
/// 应用程序主入口类。
/// 该类仅包含跨平台通用逻辑;各平台差异通过分部类拆分到 Platforms 目录中,避免在同一文件内混写大量条件编译代码。
/// </summary>
public partial class App : global::Microsoft.Maui.Controls.Application
{
private readonly IServiceProvider _serviceProvider;
@@ -23,6 +21,10 @@ public partial class App : global::Microsoft.Maui.Controls.Application
private bool _isHotkeyRegistered;
private bool _isWindowCentered;
/// <summary>
/// 创建 <see cref="App"/> 实例。
/// </summary>
/// <param name="serviceProvider">依赖注入容器。</param>
public App(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
@@ -33,7 +35,10 @@ public partial class App : global::Microsoft.Maui.Controls.Application
_trayService = serviceProvider.GetRequiredService<ISystemTrayService>();
}
protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? activationState)
/// <summary>
/// 创建主窗体,并绑定系统托盘与热键等生命周期逻辑。
/// </summary>
protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? activationState)
{
_mainWindow = new Microsoft.Maui.Controls.Window(_serviceProvider.GetRequiredService<MainPage>())
{
@@ -42,9 +47,8 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState?
Title = AppMetadata.GetWindowTitle()
};
#if WINDOWS
_mainWindow.TitleBar = CreateWindowTitleBar();
#endif
// 平台差异通过分部类实现(例如 Windows 标题栏、窗口显示方式等)。
InitializePlatform();
_mainWindow.Destroying += (s, e) =>
{
@@ -58,49 +62,42 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState?
_mainWindow.Created += (s, e) =>
{
// 系统托盘初始化需要持有 Window 引用,并提供“显示主窗体/退出应用”的回调。
_trayService.Initialize(_mainWindow, ShowMainWindow, ExitApplication);
RegisterHotkeyWhenReady();
#if WINDOWS
MainThread.BeginInvokeOnMainThread(() =>
{
if (_mainWindow.Handler?.PlatformView is WinUiWindow platformWindow)
{
// Ensure app doesn't shutdown when main window closes (we hide it)
platformWindow.AppWindow.Closing += (sender, args) =>
{
args.Cancel = true;
new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().HideWindow(_mainWindow);
};
CenterMainWindow(platformWindow);
ConfigureWindowsTitleBar(platformWindow);
}
});
#endif
// Window 已创建但 Handler 可能尚未完全就绪,平台侧可在此做进一步处理(例如订阅关闭事件、居中等)。
OnPlatformWindowCreated(_mainWindow);
};
return _mainWindow;
}
/// <summary>
/// 当平台视图准备就绪时注册热键。
/// 在某些平台(尤其 Windows)中,窗口 Handler 创建具有延迟,过早注册可能失败,因此采用重试策略。
/// </summary>
/// <param name="attempt">当前重试次数。</param>
private void RegisterHotkeyWhenReady(int attempt = 0)
{
if (_mainWindow == null) return;
#if WINDOWS
if (_mainWindow.Handler?.PlatformView is not WinUiWindow)
if (!IsPlatformViewReady(_mainWindow))
{
if (attempt < 30)
{
// 使用短间隔重试,避免阻塞 UI 线程,同时保证窗口句柄就绪后尽快完成注册。
_mainWindow.Dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(100), () => RegisterHotkeyWhenReady(attempt + 1));
}
return;
}
#endif
RegisterHotkey();
}
/// <summary>
/// 热键按下时的回调
/// </summary>
private void OnHotKeyPressed()
{
MainThread.BeginInvokeOnMainThread(() =>
@@ -109,36 +106,41 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState?
});
}
/// <summary>
/// 显示主窗口。
/// 优先使用平台特定逻辑(例如 Windows 恢复并激活窗口),否则走默认 MAUI 打开窗口流程。
/// </summary>
private void ShowMainWindow()
{
if (_mainWindow != null)
{
_mainWindow.Dispatcher.Dispatch(() =>
{
#if WINDOWS
if (_mainWindow.Handler != null)
{
new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().RestoreWindow(_mainWindow);
var platformWindow = _mainWindow.Handler.PlatformView as WinUiWindow;
platformWindow?.Activate();
}
#else
if (global::Microsoft.Maui.Controls.Application.Current != null &&
!global::Microsoft.Maui.Controls.Application.Current.Windows.Contains(_mainWindow))
{
global::Microsoft.Maui.Controls.Application.Current.OpenWindow(_mainWindow);
}
#endif
});
if (!PlatformShowMainWindow(_mainWindow))
{
// 默认显示逻辑
if (global::Microsoft.Maui.Controls.Application.Current != null &&
!global::Microsoft.Maui.Controls.Application.Current.Windows.Contains(_mainWindow))
{
global::Microsoft.Maui.Controls.Application.Current.OpenWindow(_mainWindow);
}
}
});
}
}
/// <summary>
/// 退出应用程序
/// </summary>
private void ExitApplication()
{
_trayService?.Dispose();
global::Microsoft.Maui.Controls.Application.Current?.Quit();
}
/// <summary>
/// 注册全局热键,并监听配置变更以动态更新。
/// </summary>
private void RegisterHotkey()
{
if (_hotKeyService == null || _settingsService == null) return;
@@ -169,125 +171,19 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState?
}
}
#if WINDOWS
private void ConfigureWindowsTitleBar(WinUiWindow platformWindow)
{
var title = AppMetadata.GetWindowTitle();
platformWindow.Title = title;
if (_mainWindow != null)
{
_mainWindow.Title = title;
}
var appWindow = platformWindow.AppWindow;
if (appWindow != null)
{
appWindow.Title = title;
var hWnd = WindowNative.GetWindowHandle(platformWindow);
if (hWnd != IntPtr.Zero)
{
SetWindowText(hWnd, title);
}
var iconPath = Path.Combine(AppContext.BaseDirectory, "icon.ico");
if (File.Exists(iconPath))
{
appWindow.SetIcon(iconPath);
}
var titleBar = appWindow.TitleBar;
titleBar.IconShowOptions = Windowing.IconShowOptions.ShowIconAndSystemMenu;
titleBar.BackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.InactiveBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonHoverBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonPressedBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonInactiveForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonHoverForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonPressedForegroundColor = Microsoft.UI.Colors.Black;
}
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "SetWindowTextW")]
private static extern bool SetWindowText(IntPtr hWnd, string lpString);
private static TitleBar CreateWindowTitleBar()
{
return new TitleBar
{
BackgroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#F5F5F5"),
Icon = string.Empty,
Title = string.Empty,
ForegroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"),
LeadingContent = new HorizontalStackLayout
{
Spacing = 4,
Padding = new Microsoft.Maui.Thickness(10, 0, 0, 0),
VerticalOptions = LayoutOptions.Center,
Children =
{
new Image
{
Source = "icon.jpg",
WidthRequest = 22,
HeightRequest = 22,
VerticalOptions = LayoutOptions.Center
},
new Label
{
Text = AppMetadata.GetTitleBarVersionText(),
FontFamily = "Microsoft YaHei UI",
TextColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"),
VerticalTextAlignment = Microsoft.Maui.TextAlignment.Center,
VerticalOptions = LayoutOptions.Center,
FontSize = 14
}
}
}
};
}
private void CenterMainWindow(WinUiWindow platformWindow)
{
if (_isWindowCentered) return;
var appWindow = platformWindow.AppWindow;
if (appWindow == null) return;
var displayArea = Windowing.DisplayArea.GetFromWindowId(
appWindow.Id,
Windowing.DisplayAreaFallback.Primary);
var workArea = displayArea.WorkArea;
var windowWidthPx = appWindow.Size.Width;
var windowHeightPx = appWindow.Size.Height;
if (windowWidthPx <= 0 || windowHeightPx <= 0)
{
var scale = platformWindow.Content?.XamlRoot?.RasterizationScale ?? 1.0;
windowWidthPx = windowWidthPx <= 0 ? (int)Math.Round((_mainWindow?.Width ?? 450) * scale) : windowWidthPx;
windowHeightPx = windowHeightPx <= 0 ? (int)Math.Round((_mainWindow?.Height ?? 640) * scale) : windowHeightPx;
}
if (windowWidthPx <= 0 || windowHeightPx <= 0) return;
var x = workArea.X + (workArea.Width - windowWidthPx) / 2;
var y = workArea.Y + (workArea.Height - windowHeightPx) / 2;
if (windowWidthPx >= workArea.Width) x = workArea.X;
if (windowHeightPx >= workArea.Height) y = workArea.Y;
x = Math.Max(workArea.X, Math.Min(x, workArea.X + workArea.Width - windowWidthPx));
y = Math.Max(workArea.Y, Math.Min(y, workArea.Y + workArea.Height - windowHeightPx));
appWindow.Move(new Windows.Graphics.PointInt32(x, y));
_isWindowCentered = true;
}
#endif
// 平台特定分部方法声明(实现位于 Platforms/* 目录)
partial void InitializePlatform();
partial void OnPlatformWindowCreated(Window window);
/// <summary>
/// 判断平台视图是否已就绪(用于热键/窗口等依赖平台句柄的逻辑)。
/// </summary>
/// <param name="window">MAUI Window。</param>
/// <returns>视图已就绪返回 true;否则返回 false。</returns>
private partial bool IsPlatformViewReady(Window window);
/// <summary>
/// 使用平台特定方式显示主窗口。
/// </summary>
/// <param name="window">MAUI Window。</param>
/// <returns>如果平台已处理显示逻辑返回 true;否则返回 false。</returns>
private partial bool PlatformShowMainWindow(Window window);
}
+27 -2
View File
@@ -10,8 +10,14 @@ using Hua.Todo.Maui.Services.Platforms;
namespace Hua.Todo.Maui;
/// <summary>
/// MAUI 程序启动类
/// </summary>
public static class MauiProgram
{
/// <summary>
/// 创建并配置 MAUI 应用程序
/// </summary>
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
@@ -23,9 +29,10 @@ public static class MauiProgram
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// 加载应用程序配置
var appSettings = LoadAppSettings();
// Set default connection string if not provided
// 如果未提供连接字符串,则设置默认的 SQLite 连接字符串
if (string.IsNullOrEmpty(appSettings.WebServer.ConnectionString))
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
@@ -41,12 +48,18 @@ public static class MauiProgram
builder.Services.AddSingleton(appSettings);
var connectionString = appSettings.WebServer.ConnectionString;
// 注册应用服务
builder.Services.AddApplicationServices(connectionString);
builder.Services.AddTransient<Views.MainPage>();
// 注册热键设置服务
builder.Services.AddSingleton<IHotKeySettingsService>(sp =>
new HotKeySettingsService(sp.GetRequiredService<AppSettings>()));
// 注册全局热键服务(工厂模式)
builder.Services.AddSingleton<IGlobalHotKeyService>(sp => GlobalHotKeyServiceFactory.Create());
// 注册系统托盘服务(平台相关)
builder.Services.AddSingleton<ISystemTrayService>(sp =>
{
#if WINDOWS
@@ -55,6 +68,8 @@ public static class MauiProgram
return new NullSystemTrayService();
#endif
});
// 注册嵌入式 Web 服务器(平台相关)
#if WINDOWS
builder.Services.AddSingleton<IEmbeddedWebServerService, EmbeddedWebServerService>();
#elif ANDROID
@@ -69,6 +84,7 @@ public static class MauiProgram
var app = builder.Build();
// 异步初始化数据库和 Web 服务器
_ = Task.Run(async () =>
{
try
@@ -85,6 +101,9 @@ public static class MauiProgram
return app;
}
/// <summary>
/// 从 appsettings.json 加载配置
/// </summary>
private static AppSettings LoadAppSettings()
{
try
@@ -114,6 +133,9 @@ public static class MauiProgram
}
}
/// <summary>
/// 初始化数据库(执行迁移)
/// </summary>
private static void InitializeDatabase(IServiceProvider services, string connectionString)
{
using var scope = services.CreateScope();
@@ -133,7 +155,7 @@ public static class MauiProgram
}
}
// Ensure WAL mode to avoid locking issues
// 确保使用 WAL 模式以避免锁定问题
dbContext.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;");
dbContext.Database.Migrate();
}
@@ -152,6 +174,9 @@ public static class MauiProgram
}
}
/// <summary>
/// 启动嵌入式 Web 服务器
/// </summary>
private static async Task StartWebServer(IServiceProvider services)
{
try
@@ -5,8 +5,15 @@ using Android.OS;
namespace Hua.Todo.Maui;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
/// <summary>
/// Android 平台入口 Activity。
/// </summary>
public class MainActivity : MauiAppCompatActivity
{
/// <summary>
/// Activity 创建时回调。
/// </summary>
/// <param name="savedInstanceState">上次保存状态。</param>
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
@@ -1,15 +1,27 @@
using Android.App;
using Android.App;
using Android.Runtime;
namespace Hua.Todo.Maui;
[Application]
/// <summary>
/// Android 平台 Application。
/// </summary>
public class MainApplication : MauiApplication
{
/// <summary>
/// 创建 <see cref="MainApplication"/>。
/// </summary>
/// <param name="handle">JNI 句柄。</param>
/// <param name="ownership">句柄所有权。</param>
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
/// <summary>
/// 创建 MAUI 应用。
/// </summary>
/// <returns>MAUI 应用实例。</returns>
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
@@ -9,6 +9,10 @@ using Hua.Todo.Maui.Models;
namespace Hua.Todo.Maui.Services;
/// <summary>
/// Android 平台嵌入式 Web 服务器服务。
/// 使用 TcpListener 自定义实现简单的 HTTP 服务器,用于离线模式下托管 API 和静态资源。
/// </summary>
public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
{
private readonly AppSettings _appSettings;
@@ -18,15 +22,34 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
private CancellationTokenSource? _cts;
private Task? _acceptLoop;
/// <summary>
/// 服务器是否正在运行。
/// </summary>
public bool IsRunning => _listener != null;
/// <summary>
/// 服务器基础 URL。
/// </summary>
public string BaseUrl => $"http://localhost:{_appSettings.WebServer.Port}";
/// <summary>
/// 初始化 <see cref="MobileEmbeddedWebServerService"/>。
/// </summary>
/// <param name="appSettings">应用程序配置。</param>
/// <param name="services">依赖注入服务提供者。</param>
public MobileEmbeddedWebServerService(AppSettings appSettings, IServiceProvider services)
{
_appSettings = appSettings;
_services = services;
}
/// <summary>
/// 异步启动服务器。
/// 启动时机:在 Android 平台初始化或手动开启时调用。
/// 错误处理:启动失败会记录日志,建议外部调用时关注 <see cref="IsRunning"/>。
/// 线程安全:可在任何线程调用;内部通过状态检查避免重复启动。
/// </summary>
/// <returns>表示启动操作的任务。</returns>
public Task StartAsync()
{
if (_listener != null) return Task.CompletedTask;
@@ -39,6 +62,11 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
return Task.CompletedTask;
}
/// <summary>
/// 异步停止服务器。
/// 释放所有资源并停止监听。
/// </summary>
/// <returns>表示停止操作的任务。</returns>
public async Task StopAsync()
{
if (_listener == null) return;
@@ -0,0 +1,212 @@
using Microsoft.Maui.Controls;
using Hua.Todo.Maui.Services;
using System.Runtime.InteropServices;
using Windowing = Microsoft.UI.Windowing;
using WinUiWindow = Microsoft.UI.Xaml.Window;
using WinRT.Interop;
namespace Hua.Todo.Maui;
/// <summary>
/// Windows 平台特定的 App 逻辑(分部类)。
/// 该文件仅在 Windows 目标框架下参与编译,用于承载窗口句柄相关能力与 WinUI API 调用。
/// </summary>
public partial class App
{
/// <summary>
/// 初始化 Windows 平台特定设置。
/// 例如:自定义 MAUI TitleBar(不是 WinUI TitleBar)。
/// </summary>
partial void InitializePlatform()
{
if (_mainWindow != null)
{
_mainWindow.TitleBar = CreateWindowTitleBar();
}
}
/// <summary>
/// Windows 平台窗口创建后的处理。
/// 该回调发生在 Window Created 之后,但依然可能需要等待 Handler/PlatformView 就绪,因此使用 UI 线程调度。
/// </summary>
/// <param name="window">MAUI Window。</param>
partial void OnPlatformWindowCreated(Window window)
{
MainThread.BeginInvokeOnMainThread(() =>
{
if (window.Handler?.PlatformView is WinUiWindow platformWindow)
{
// 关闭按钮行为:拦截关闭并隐藏到系统托盘,避免进程退出。
// 注意:该逻辑与系统托盘功能配套,托盘菜单提供“退出应用”入口。
platformWindow.AppWindow.Closing += (sender, args) =>
{
args.Cancel = true;
new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().HideWindow(window);
};
CenterMainWindow(platformWindow);
ConfigureWindowsTitleBar(platformWindow);
}
});
}
/// <summary>
/// 检查 Windows 平台视图是否准备就绪
/// </summary>
/// <param name="window">MAUI Window。</param>
/// <returns>当 PlatformView 是 WinUI Window 时返回 true。</returns>
private partial bool IsPlatformViewReady(Window window)
{
return window.Handler?.PlatformView is WinUiWindow;
}
/// <summary>
/// 在 Windows 平台上显示主窗口
/// </summary>
/// <param name="window">MAUI Window。</param>
/// <returns>已恢复并激活窗口返回 true;否则返回 false。</returns>
private partial bool PlatformShowMainWindow(Window window)
{
if (window.Handler != null)
{
new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().RestoreWindow(window);
var platformWindow = window.Handler.PlatformView as WinUiWindow;
platformWindow?.Activate();
return true;
}
return false;
}
/// <summary>
/// 配置 Windows 标题栏(WinUI AppWindow.TitleBar)。
/// 用于设置窗口标题、图标以及标题栏按钮样式等。
/// </summary>
/// <param name="platformWindow">WinUI Window。</param>
private void ConfigureWindowsTitleBar(WinUiWindow platformWindow)
{
var title = AppMetadata.GetWindowTitle();
platformWindow.Title = title;
if (_mainWindow != null)
{
_mainWindow.Title = title;
}
var appWindow = platformWindow.AppWindow;
if (appWindow != null)
{
appWindow.Title = title;
var hWnd = WindowNative.GetWindowHandle(platformWindow);
if (hWnd != IntPtr.Zero)
{
SetWindowText(hWnd, title);
}
var iconPath = Path.Combine(AppContext.BaseDirectory, "icon.ico");
if (File.Exists(iconPath))
{
appWindow.SetIcon(iconPath);
}
var titleBar = appWindow.TitleBar;
titleBar.IconShowOptions = Windowing.IconShowOptions.ShowIconAndSystemMenu;
titleBar.BackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.InactiveBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonHoverBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonPressedBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonInactiveForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonHoverForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonPressedForegroundColor = Microsoft.UI.Colors.Black;
}
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "SetWindowTextW")]
private static extern bool SetWindowText(IntPtr hWnd, string lpString);
/// <summary>
/// 创建 MAUI 自定义标题栏内容。
/// 用于在窗口标题栏区域展示应用图标与版本信息。
/// </summary>
private static TitleBar CreateWindowTitleBar()
{
return new TitleBar
{
BackgroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#F5F5F5"),
Icon = string.Empty,
Title = string.Empty,
ForegroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"),
LeadingContent = new HorizontalStackLayout
{
Spacing = 4,
Padding = new Microsoft.Maui.Thickness(10, 0, 0, 0),
VerticalOptions = LayoutOptions.Center,
Children =
{
new Image
{
Source = "icon.jpg",
WidthRequest = 22,
HeightRequest = 22,
VerticalOptions = LayoutOptions.Center
},
new Label
{
Text = AppMetadata.GetTitleBarVersionText(),
FontFamily = "Microsoft YaHei UI",
TextColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"),
VerticalTextAlignment = Microsoft.Maui.TextAlignment.Center,
VerticalOptions = LayoutOptions.Center,
FontSize = 14
}
}
}
};
}
/// <summary>
/// 居中显示主窗口
/// </summary>
/// <param name="platformWindow">WinUI Window。</param>
private void CenterMainWindow(WinUiWindow platformWindow)
{
if (_isWindowCentered) return;
var appWindow = platformWindow.AppWindow;
if (appWindow == null) return;
var displayArea = Windowing.DisplayArea.GetFromWindowId(
appWindow.Id,
Windowing.DisplayAreaFallback.Primary);
var workArea = displayArea.WorkArea;
var windowWidthPx = appWindow.Size.Width;
var windowHeightPx = appWindow.Size.Height;
if (windowWidthPx <= 0 || windowHeightPx <= 0)
{
var scale = platformWindow.Content?.XamlRoot?.RasterizationScale ?? 1.0;
windowWidthPx = windowWidthPx <= 0 ? (int)Math.Round((_mainWindow?.Width ?? 450) * scale) : windowWidthPx;
windowHeightPx = windowHeightPx <= 0 ? (int)Math.Round((_mainWindow?.Height ?? 640) * scale) : windowHeightPx;
}
if (windowWidthPx <= 0 || windowHeightPx <= 0) return;
var x = workArea.X + (workArea.Width - windowWidthPx) / 2;
var y = workArea.Y + (workArea.Height - windowHeightPx) / 2;
if (windowWidthPx >= workArea.Width) x = workArea.X;
if (windowHeightPx >= workArea.Height) y = workArea.Y;
x = Math.Max(workArea.X, Math.Min(x, workArea.X + workArea.Width - windowWidthPx));
y = Math.Max(workArea.Y, Math.Min(y, workArea.Y + workArea.Height - windowHeightPx));
appWindow.Move(new Windows.Graphics.PointInt32(x, y));
_isWindowCentered = true;
}
}
@@ -0,0 +1,34 @@
using Microsoft.Maui.Controls;
namespace Hua.Todo.Maui.Views
{
/// <summary>
/// Windows 平台特定的 MainPage 逻辑(分部类)。
/// 该文件仅在 Windows 目标框架下参与编译,用于接入 Windows 全局键盘事件等能力。
/// </summary>
public partial class MainPage
{
private Platforms.Windows.WindowsKeyboardHandler? _keyboardHandler;
/// <summary>
/// Windows 平台特定的键盘处理器设置。
/// 当前仅监听 Esc 键,用于快速最小化窗口(与桌面端交互习惯保持一致)。
/// </summary>
partial void PlatformSetupKeyboardHandler()
{
_keyboardHandler = new Platforms.Windows.WindowsKeyboardHandler();
_keyboardHandler.EscKeyPressed += OnEscKeyPressed;
_keyboardHandler.Start();
}
/// <summary>
/// Windows 平台特定的 Esc 键处理(最小化窗口)。
/// </summary>
/// <param name="window">当前窗口。</param>
partial void PlatformOnEscKeyPressed(Window window)
{
var windowService = new Platforms.Windows.WindowsWindowService();
windowService.MinimizeWindow(window);
}
}
}
@@ -2,19 +2,32 @@ using System.Runtime.InteropServices;
namespace Hua.Todo.Maui.Platforms.Windows
{
/// <summary>
/// Windows 全局键盘事件处理器。
/// 用于监听按键并向上层暴露事件(例如 Esc)。
/// </summary>
public class WindowsKeyboardHandler : IDisposable
{
private KeyboardHook _keyboardHook;
private bool _isDisposed;
/// <summary>
/// 当检测到 Esc 键抬起时触发。
/// </summary>
public event EventHandler? EscKeyPressed;
/// <summary>
/// 创建 <see cref="WindowsKeyboardHandler"/>。
/// </summary>
public WindowsKeyboardHandler()
{
_keyboardHook = new KeyboardHook();
_keyboardHook.KeyPressed += OnKeyPressed;
}
/// <summary>
/// 开始监听键盘事件。
/// </summary>
public void Start()
{
_keyboardHook.Hook();
@@ -28,6 +41,9 @@ namespace Hua.Todo.Maui.Platforms.Windows
}
}
/// <summary>
/// 停止监听并释放资源。
/// </summary>
public void Dispose()
{
if (!_isDisposed)
@@ -126,4 +142,4 @@ namespace Hua.Todo.Maui.Platforms.Windows
IsKeyDown = isKeyDown;
}
}
}
}
@@ -6,6 +6,9 @@ using WinRT.Interop;
namespace Hua.Todo.Maui.Platforms.Windows
{
/// <summary>
/// Windows 平台窗口操作服务。
/// </summary>
public class WindowsWindowService
{
private const int SW_HIDE = 0;
@@ -15,6 +18,10 @@ namespace Hua.Todo.Maui.Platforms.Windows
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
/// <summary>
/// 隐藏窗口(不退出进程)。
/// </summary>
/// <param name="window">MAUI Window。</param>
public void HideWindow(Window window)
{
if (window == null) return;
@@ -29,6 +36,10 @@ namespace Hua.Todo.Maui.Platforms.Windows
ShowWindow(hWnd, SW_HIDE);
}
/// <summary>
/// 恢复窗口并显示。
/// </summary>
/// <param name="window">MAUI Window。</param>
public void RestoreWindow(Window window)
{
if (window == null) return;
@@ -44,6 +55,10 @@ namespace Hua.Todo.Maui.Platforms.Windows
ShowWindow(hWnd, SW_RESTORE);
}
/// <summary>
/// 最小化窗口。
/// </summary>
/// <param name="window">MAUI Window。</param>
public void MinimizeWindow(Window window)
{
if (window == null) return;
@@ -13,19 +13,41 @@ using AppSettings = Hua.Todo.Maui.Models.AppSettings;
namespace Hua.Todo.Maui.Services;
/// <summary>
/// Windows 平台嵌入式 Web 服务器实现
/// 使用 ASP.NET Core 运行
/// </summary>
public class EmbeddedWebServerService : IEmbeddedWebServerService
{
private WebApplication? _webApp;
private readonly AppSettings _appSettings;
/// <summary>
/// 服务器是否正在运行
/// </summary>
public bool IsRunning => _webApp != null;
/// <summary>
/// 服务器基础 URL
/// </summary>
public string BaseUrl => _appSettings.WebServer.HostUrl;
/// <summary>
/// 初始化嵌入式 Web 服务器服务
/// </summary>
/// <param name="appSettings">应用程序配置</param>
public EmbeddedWebServerService(AppSettings appSettings)
{
_appSettings = appSettings;
}
/// <summary>
/// 异步启动服务器。
/// 启动时机:通常在应用启动或用户手动开启服务时调用。
/// 错误处理:启动失败会抛出 ASP.NET Core 相关异常,建议在调用方进行 catch 处理。
/// 线程安全:非 UI 线程相关,可在后台线程调用;方法内部已处理重入(若已运行则直接返回)。
/// </summary>
/// <returns>表示启动操作的任务</returns>
public async Task StartAsync()
{
if (_webApp != null) return;
@@ -34,6 +56,7 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
builder.WebHost.UseUrls(_appSettings.WebServer.HostUrl);
// 配置控制器和 JSON 选项
builder.Services.AddControllers()
.AddApplicationPart(typeof(Hua.Todo.Application.ServiceCollectionExtensions).Assembly)
.AddJsonOptions(options =>
@@ -43,9 +66,10 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
builder.Services.AddEndpointsApiExplorer();
// 注册应用逻辑服务
builder.Services.AddApplicationServices(_appSettings.WebServer.ConnectionString);
// 配置跨域策略
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
@@ -58,6 +82,7 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
var app = builder.Build();
// 如果配置为使用静态文件(前端托管),则配置静态文件服务
if (_appSettings.WebServer.IsUsingStatic)
{
ServeStaticFiles(app);
@@ -73,6 +98,10 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
await _webApp.StartAsync();
}
/// <summary>
/// 配置静态文件服务(用于托管 Vue 前端)
/// </summary>
/// <param name="app">Web 应用程序实例</param>
private void ServeStaticFiles(WebApplication app)
{
try
@@ -100,6 +129,7 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
};
app.UseStaticFiles(staticFileOptions);
// 处理 SPA 路由
app.Use(async (context, next) =>
{
if (context.Request.Path.HasValue)
@@ -125,6 +155,11 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
}
}
/// <summary>
/// 异步停止服务器。
/// 释放服务器资源并停止监听。
/// </summary>
/// <returns>表示停止操作的任务</returns>
public async Task StopAsync()
{
if (_webApp == null) return;
@@ -4,13 +4,30 @@ using Hua.Todo.Maui.Models;
namespace Hua.Todo.Maui.Services
{
/// <summary>
/// 热键设置服务接口
/// </summary>
public interface IHotKeySettingsService
{
/// <summary>
/// 获取热键配置
/// </summary>
HotKeyConfig GetConfig();
/// <summary>
/// 保存热键配置
/// </summary>
void SaveConfig(HotKeyConfig config);
/// <summary>
/// 重置为默认配置
/// </summary>
void ResetToDefault();
}
/// <summary>
/// 热键设置服务实现,使用 Preferences 存储配置
/// </summary>
public class HotKeySettingsService : IHotKeySettingsService
{
private const string SettingsKey = "HotKeyConfig";
@@ -21,6 +38,9 @@ namespace Hua.Todo.Maui.Services
_appSettings = appSettings;
}
/// <summary>
/// 获取当前热键配置,如果不存在则返回默认配置
/// </summary>
public HotKeyConfig GetConfig()
{
var json = Preferences.Get(SettingsKey, string.Empty);
@@ -39,18 +59,27 @@ namespace Hua.Todo.Maui.Services
}
}
/// <summary>
/// 将热键配置序列化并保存到 Preferences
/// </summary>
public void SaveConfig(HotKeyConfig config)
{
var json = JsonSerializer.Serialize(config);
Preferences.Set(SettingsKey, json);
}
/// <summary>
/// 重置为从 appsettings.json 中读取的默认值
/// </summary>
public void ResetToDefault()
{
var defaultConfig = GetDefaultConfig();
SaveConfig(defaultConfig);
}
/// <summary>
/// 获取默认热键配置
/// </summary>
private HotKeyConfig GetDefaultConfig()
{
return new HotKeyConfig
@@ -1,9 +1,27 @@
namespace Hua.Todo.Maui.Services;
/// <summary>
/// 嵌入式 Web 服务器服务接口
/// </summary>
public interface IEmbeddedWebServerService
{
/// <summary>
/// 服务器是否正在运行
/// </summary>
bool IsRunning { get; }
/// <summary>
/// 服务器基础 URL
/// </summary>
string BaseUrl { get; }
/// <summary>
/// 启动服务器
/// </summary>
Task StartAsync();
/// <summary>
/// 停止服务器
/// </summary>
Task StopAsync();
}
@@ -43,6 +43,9 @@ namespace Hua.Todo.Maui.Services.Platforms
/// <summary>
/// 注册全局热键
/// </summary>
/// <param name="modifiers">修饰键字符串,多个键用逗号分隔(如 "Control,Alt"</param>
/// <param name="key">主键字符串(如 "X"</param>
/// <param name="callback">热键触发时的回调操作</param>
public void RegisterHotKey(string modifiers, string key, Action callback)
{
if (_window == null)
@@ -97,6 +100,8 @@ namespace Hua.Todo.Maui.Services.Platforms
/// <summary>
/// 更新热键配置
/// </summary>
/// <param name="modifiers">新的修饰键字符串</param>
/// <param name="key">新的主键字符串</param>
public void UpdateHotKey(string modifiers, string key)
{
if (_callback != null)
+43 -13
View File
@@ -4,14 +4,20 @@ using Hua.Todo.Maui.Services;
namespace Hua.Todo.Maui.Views
{
/// <summary>
/// 应用程序主页面。
/// 该页面承载 WebView(前端 UI),并通过 JavaScript 注入与事件机制实现与 MAUI 的通讯。
/// </summary>
public partial class MainPage : ContentPage
{
private readonly AppSettings _appSettings;
private readonly IEmbeddedWebServerService? _webServer;
#if WINDOWS
private Platforms.Windows.WindowsKeyboardHandler? _keyboardHandler;
#endif
/// <summary>
/// 创建 <see cref="MainPage"/>。
/// </summary>
/// <param name="appSettings">应用配置。</param>
/// <param name="webServer">嵌入式 Web 服务器服务(在不同平台可能为不同实现)。</param>
public MainPage(AppSettings appSettings, IEmbeddedWebServerService webServer)
{
InitializeComponent();
@@ -23,7 +29,10 @@ namespace Hua.Todo.Maui.Views
SetupKeyboardHandler();
}
/// <summary>
/// 设置 WebView 数据源。
/// 当启用嵌入式服务器且使用静态文件时,直接指向本地服务器;否则使用配置的前端 URL。
/// </summary>
private void SetupWebViewSource()
{
if (_appSettings.WebServer.IsUsingStatic)
@@ -38,27 +47,32 @@ namespace Hua.Todo.Maui.Views
MainWebView.Source = NormalizeUrl(_appSettings.WebServer.ForEndUrl);
}
/// <summary>
/// 设置键盘处理器(平台差异通过分部类实现)。
/// </summary>
private void SetupKeyboardHandler()
{
#if WINDOWS
_keyboardHandler = new Platforms.Windows.WindowsKeyboardHandler();
_keyboardHandler.EscKeyPressed += OnEscKeyPressed;
_keyboardHandler.Start();
#endif
PlatformSetupKeyboardHandler();
}
/// <summary>
/// 当 Esc 键按下时的回调
/// </summary>
private void OnEscKeyPressed(object? sender, EventArgs e)
{
var window = Microsoft.Maui.Controls.Application.Current?.Windows.FirstOrDefault();
if (window != null)
{
#if WINDOWS
var windowService = new Platforms.Windows.WindowsWindowService();
windowService.MinimizeWindow(window);
#endif
PlatformOnEscKeyPressed(window);
}
}
/// <summary>
/// 设置 WebView 通讯逻辑。
/// 约定:
/// - 通过 window.__API_BASE_URL__ 注入后端 API 基地址(仅在嵌入式服务器运行时)
/// - 通过自定义事件在 Web 与 MAUI 间传递热键配置等数据
/// </summary>
private void SetupWebViewCommunication()
{
MainWebView.Navigated += async (s, e) =>
@@ -76,6 +90,7 @@ namespace Hua.Todo.Maui.Views
await MainWebView.EvaluateJavaScriptAsync($"window.__API_BASE_URL__ = '{apiBase}';");
}
// 注入前端与 MAUI 的通讯桥(事件名/字段名属于协议的一部分,修改需同步前端)。
await MainWebView.EvaluateJavaScriptAsync(@"
window.mauiInterop = {
onHotKeyConfigUpdated: null,
@@ -100,6 +115,10 @@ namespace Hua.Todo.Maui.Views
};
}
/// <summary>
/// 规格化 URL(针对 Android 模拟器处理 localhost)。
/// Android 模拟器中 localhost 指向模拟器自身,需要替换为 10.0.2.2 才能访问宿主机服务。
/// </summary>
private static string NormalizeUrl(string url)
{
if (string.IsNullOrWhiteSpace(url)) return url;
@@ -116,5 +135,16 @@ namespace Hua.Todo.Maui.Views
return url;
}
// 平台特定分部方法声明(实现位于 Platforms/* 目录)
/// <summary>
/// 平台特定的键盘处理器初始化。
/// </summary>
partial void PlatformSetupKeyboardHandler();
/// <summary>
/// 平台特定的 Esc 键处理逻辑。
/// </summary>
/// <param name="window">当前窗口。</param>
partial void PlatformOnEscKeyPressed(Window window);
}
}