wpf程序启动居中并且最小化到托盘修复记录

启动居中 + 最小化到托盘(无闪烁)修复记录

本文记录本次关于"wpf应用启动时窗口居中,且在启用启动最小化时直接最小化到托盘(无任何可见闪烁与缩略窗)"的排查与修复过程,供日后参考。

该软件的功能类似《基于MFC实现的快速输入小工具》,当然这不重要,软件是修复过程的一个参照。

问题现象

  • 启动勾选"启动最小化"后,窗口会出现以下不良体验:
    • 先在屏幕左上角闪现,之后居中,再最小化到托盘。
    • 某些方案会出现黑块(透明度/布局期间的绘制)。
    • 也有方案会在任务栏左下角出现最小化缩略窗(而非直接进入系统托盘)。
  • 期望行为:
    • 启动最小化开启时,应用应直接最小化到系统托盘,不占用任务栏,也不出现左下角缩略窗;
    • 从托盘恢复时,窗口应居中显示;
    • 启动未勾选最小化时,窗口应"直接居中显示",且无居中过程的可见动画/闪烁。

根因分析

  • 传统做法在 LoadedShown 时再调整位置与状态,期间窗口已可见,导致"左上角→居中→最小化"的可见过程。
  • 使用 WindowState = Minimized 启动,会让系统创建任务栏最小化缩略窗(左下角出现)。
  • 使用透明度(Opacity=0)在某些显卡/主题上可能出现绘制黑块,且仍可能看到状态变化过程。

关键改动概览

涉及文件:

  • App.xaml.cs
  • MainWindow.xaml.cs
  • Services/TrayService.cs
  • (已有)publish.ps1

1) App.xaml.cs:启动最小化路径的"无可见"初始化

位置:Application_Startup 内部

变更点:

  • 启动时提前加载设置:

    csharp 复制代码
    var storage = new StorageService();
    startupSettings = storage.LoadSettings();
  • 根据 StartMinimized 决定两条启动路径:

    • 非最小化:正常 mw.Show()(此时 MainWindow 会自行确保居中)。
    • 启动最小化:
      1. 标记 mw.StartMinimizedHandled = true,避免 MainWindow_Loaded 再次隐藏引发闪烁;

      2. 将窗口放到屏幕外侧,禁止任务栏显示、禁止激活:

        csharp 复制代码
        mw.WindowStartupLocation = WindowStartupLocation.Manual;
        mw.Left = -10000; mw.Top = -10000;
        mw.ShowInTaskbar = false; mw.ShowActivated = false;
        mw.WindowState = WindowState.Normal; // 刻意保持 Normal,避免生成任务栏最小化缩略图
      3. 调用 mw.Show() 仅用于创建句柄和触发 Loaded,随后立即 mw.Hide()

        csharp 复制代码
        mw.Show();
        mw.Hide();
      4. 为保留"启动到托盘"的通知体验,在 ApplicationIdle 时机调用 mw.ShowStartupBalloon()

        csharp 复制代码
        mw.Dispatcher.BeginInvoke(() => (mw as MainWindow)?.ShowStartupBalloon(), DispatcherPriority.ApplicationIdle);

这一流程保证:

  • 启动最小化时不出现任何可见窗口或任务栏缩略窗;
  • 托盘与热键初始化仍能在 Loaded 中完成;
  • 点击通知即可恢复窗口。

2) MainWindow.xaml.cs:始终计算居中位置 & 托盘恢复

关键点:

  • 新增公共属性:public bool StartMinimizedHandled { get; set; },用于告知 Loaded 阶段无需再次最小化/隐藏。

  • ApplyWindowSettings(AppSettings? settings)

    • 修改为"即使勾选启动最小化,也要执行居中计算",保证"恢复时天然居中":

      csharp 复制代码
      bool shouldCenter = (settings?.WindowSettings?.AutoCenterOnStartup ?? true);
    • 使用 WindowStartupLocation.CenterScreen + LoadedCenterWindowOnCurrentScreen() 双保险,考虑多显示器与 DPI。

  • MainWindow_Loaded 中的启动最小化逻辑调整:

    • StartMinimizedHandled == true 时,跳过再次隐藏,避免闪烁:

      csharp 复制代码
      if (MnuStartMinimized.IsChecked == true && !_loaded && !StartMinimizedHandled) { Hide(); ... }
  • 托盘恢复(_tray.ShowMainRequested 回调):

    • 恢复前先 ShowInTaskbar = true; ShowActivated = true;,再 Show() + WindowState = Normal; Activate();,确保恢复到任务栏并前置显示。
  • 新增方法 ShowStartupBalloon()

    • 用于 App 在启动最小化时机调用,文案固定为:
      • 标题:"HotkeyPaster 已启动"
      • 内容:"程序已最小化到系统托盘"
2.1) 非最小化启动的"首帧即居中"优化(避免左上角→居中)

问题:未勾选"启动最小化"时,窗口会先在左上角出现,然后再居中,存在可见的移动过程。

修复:在 MainWindow 构造函数中,于 InitializeComponent() 之后立刻预加载设置并调用 ApplyWindowSettings(earlySettings),确保在 App.Show() 之前就完成窗口的居中定位,从而实现"首帧即居中"。

关键代码片段(MainWindow() 构造函数内):

csharp 复制代码
// 预先加载设置并应用窗口位置,确保在 App.Show() 之前就已居中,避免先显示在左上角再移动
try
{
    var earlySettings = _storage.LoadSettings();
    MnuStartMinimized.IsChecked = earlySettings?.StartMinimized ?? false;
    ApplyWindowSettings(earlySettings);
}
catch { }

3) TrayService.cs:通知点击恢复

  • 已有的 BalloonTipClicked 事件绑定到 ShowMainRequested,保持点击通知即可恢复主窗口:

    csharp 复制代码
    _notifyIcon.BalloonTipClicked += (_, __) => ShowMainRequested?.Invoke();

4) 发布脚本 publish.ps1:单文件(依赖框架)

  • 现有脚本参数:
    • --self-contained false
    • /p:PublishSingleFile=true
    • 输出到 ./publish/
  • 用于快速验证修复后的可执行文件。

用户体验验证要点

  • 勾选"启动最小化"后启动:
    • 不显示窗口、不占任务栏、不出现左下角缩略窗;
    • 托盘气球提示出现,文案为"程序已最小化到系统托盘";
    • 点击气球提示可直接恢复主窗口,且居中显示。
  • 取消"启动最小化"后启动:
    • 窗口直接居中显示,不出现"先左上后居中"的可见过程。

其它可选方案(备忘)

  • 仅用 Opacity = 0 显示再隐藏,可能在部分环境出现黑块或短暂闪烁,不如屏外+Hide 方案稳妥。
  • 通过 Visibility = Collapsed 延迟 Show() 也可达成类似效果,但托盘初始化时机需谨慎处理。
  • 直接使用 WindowState = Minimized 启动会导致任务栏缩略窗,不符合"直接托盘"的需求。

注意事项与坑点

  • 单文件发布(PublishSingleFile=true)下,Assembly.Location 可能为空,应使用 AppContext.BaseDirectory(本项目中已有使用,编译器也提示过 IL3000)。
  • 托盘通知时机需在托盘初始化完成之后(用 DispatcherPriority.ApplicationIdle 安排调用),否则可能丢失通知。
  • 多显示器 + DPI:CenterWindowOnCurrentScreen() 需要使用 PresentationSource.FromVisual(...).CompositionTarget.TransformToDevice 获取缩放因子,保证居中位置准确。

变更清单(文件与核心片段)

  • App.xaml.cs
    • 启动最小化分支:屏外创建 + 不激活 + 不显示任务栏 + Normal 状态 + 立即 Hide + Idle 时通知。
  • MainWindow.xaml.cs
    • StartMinimizedHandled 标记;
    • ApplyWindowSettings() 在最小化时亦执行居中计算;
    • ShowMainRequested 恢复时设置 ShowInTaskbar/ShowActivated
    • ShowStartupBalloon() 新增并统一通知文案。
  • Services/TrayService.cs
    • BalloonTipClicked 已指向 ShowMainRequested

如未来需要改为"自包含发布"(无需安装 .NET Runtime),可在 publish.ps1 中将 --self-contained 切换为 true 并指定 -r win-x64,同时考虑 PublishTrimmed 与原生库自解压参数是否保留。

相关推荐
木心爱编程5 小时前
C++程序员速通C#:从Hello World到数据类型
c++·c#
※※冰馨※※5 小时前
【c#】 使用winform如何将一个船的图标(ship.png)添加到资源文件
开发语言·windows·c#
咕白m6256 小时前
C# 实现 Word 与 TXT 文本格式互转
c#·.net
土了个豆子的19 小时前
04.事件中心模块
开发语言·前端·visualstudio·单例模式·c#
@areok@20 小时前
C++mat传入C#OpencvCSharp的mat
开发语言·c++·opencv·c#
时光追逐者1 天前
C# 哈希查找算法实操
算法·c#·哈希算法
三千道应用题1 天前
C#语言入门详解(18)传值、输出、引用、数组、具名、可选参数、扩展方法
开发语言·c#
micoos1 天前
C#-LinqToObject-Element
c#