WPF-Samples 实战解析:基于 Win32 WINDOWPLACEMENT 实现多显示器感知的窗口状态保存与恢复
示例工程【免费下载链接】WPF-SamplesRepository for WPF related samples项目地址https://gitcode.com/gh_mirrors/wp/WPF-Samples点击查看免费下载本篇文章以 WPF-Samples 仓库中的 SaveWindowState 示例 为主体完整剖析如何在 WPF 应用中保存并恢复窗口的尺寸size、位置location与显示状态state确保窗口下次启动时出现在上次关闭时的位置并且天然具备多显示器multi-monitor感知能力。读完本文你将掌握 WINDOWPLACEMENT 结构体的字段语义、GetWindowPlacement / SetWindowPlacement 两个 Win32 API 的配对用法、WPF 应用设置的持久化方案以及恢复过程中的最小化状态修正等实战细节可直接迁移到自己的项目中使用。一、示例概览它解决什么问题大多数桌面应用都期望「窗口停在哪下次打开还在哪」。但 WPF 本身并不提供开箱即用的窗口位置持久化能力——默认情况下每次启动窗口都会按MainWindow.xaml中声明的Width/Height与系统默认位置重新出现。SaveWindowState 示例给出了一个简洁、可靠且多显示器友好的解决方案其思路是在窗口关闭时通过 Win32 APIGetWindowPlacement把当前窗口的尺寸、位置、最小化/最大化状态读取到WINDOWPLACEMENT结构体中将该结构体以 XML 形式序列化保存到用户级应用设置Application Settings中在窗口初始化OnSourceInitialized时用SetWindowPlacement把上次保存的状态写回窗口从而让窗口「回到上次关闭时的样子」。示例的入口代码位于 MainWindow.cs窗口本身没有任何业务 UIMainWindow.xaml 中仅含一个空Grid全部逻辑聚焦于状态存取非常适合作为模板参考。二、核心数据结构WindowPlacement / Point / Rect 三件套要调用 Win32 的窗口放置 API首先需要在托管代码中定义与WINDOWPLACEMENT对应的结构体。示例将其拆分为三个可序列化的结构体文件2.1 WindowPlacement 主结构Windowplacement.cs 中定义了与原生WINDOWPLACEMENT一一对应的托管结构[Serializable] [StructLayout(LayoutKind.Sequential)] public struct WindowPlacement { public int length; public int flags; public int showCmd; public Point minPosition; public Point maxPosition; public Rect normalPosition; }各字段含义如下字段含义示例代码中的取值length结构体本身的字节大小即Marshal.SizeOf(typeof(WindowPlacement))用于告诉 API 结构版本恢复时动态计算并赋值flags窗口最小化/最大化相关标志WPF_*当前示例统一置 0表示不使用额外标志恢复时强制赋0showCmd窗口当前显示命令对应SW_SHOWNORMAL、SW_SHOWMINIMIZED等 Win32 常量保存时由系统回填恢复时修正minPosition窗口最小化时左上角位置屏幕坐标由系统回填maxPosition窗口最大化时左上角位置屏幕坐标由系统回填normalPosition窗口处于常规非最小化/最大化状态时的矩形边界Left/Top/Right/Bottom由系统回填2.2 坐标与矩形辅助结构PointPOINT.cs对应原生POINT表示屏幕坐标点[Serializable] [StructLayout(LayoutKind.Sequential)] public struct Point { public int X; public int Y; public Point(int x, int y) { X x; Y y; } }RectRECT.cs对应原生RECT以左、上、右、下四条边描述矩形[Serializable] [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left; public int Top; public int Right; public int Bottom; public Rect(int left, int top, int right, int bottom) { Left left; Top top; Right right; Bottom bottom; } }三个结构体均标注了[Serializable]这是后续把整个WindowPlacement以 XML 形式写入应用设置的必要前提。同时它们都使用[StructLayout(LayoutKind.Sequential)]保证托管结构的内存布局与 Win32 原生结构一致从而能安全地与user32.dll互操作。三、保存流程OnClosing 中读取并持久化窗口状态窗口状态的保存发生在MainWindow.OnClosing中MainWindow.cs// WARNING - Not fired when Application.SessionEnding is fired protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); // Persist window placement details to application settings WindowPlacement wp; var hwnd new WindowInteropHelper(this).Handle; GetWindowPlacement(hwnd, out wp); Settings.Default.WindowPlacement wp; Settings.Default.Save(); }执行步骤可拆解为四条获取窗口句柄通过WindowInteropHelper(this).Handle取得当前 WPF 窗口对应的原生 HWND——WPF 窗口本质上是 Win32 窗口的托管封装互操作层就在这里打通调用GetWindowPlacement把 HWND 传入 Win32 API由系统回填窗口当前的尺寸、位置与最小化/最大化状态到WindowPlacement结构体中写入设置Settings.Default.WindowPlacement wp;将整个结构体赋给用户级设置属性持久化落盘Settings.Default.Save();将设置写入磁盘最终落在用户目录下的配置文件里。其中 P/Invoke 声明位于同一文件的#region Win32 API declarations to set and get window placement区域[DllImport(user32.dll)] private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WindowPlacement lpwndpl); [DllImport(user32.dll)] private static extern bool GetWindowPlacement(IntPtr hWnd, out WindowPlacement lpwndpl); private const int SwShownormal 1; private const int SwShowminimized 2;GetWindowPlacement用out参数回填结构体SetWindowPlacement用ref传入结构体两个常量SwShownormal 1与SwShowminimized 2对应 Win32 的SW_SHOWNORMAL与SW_SHOWMINIMIZED用于恢复时的状态修正。四、恢复流程OnSourceInitialized 中写回窗口状态窗口状态的恢复发生在OnSourceInitialized中——这个时机早于窗口完成展示能避免用户看到「先出现在默认位置、再跳回记忆位置」的闪烁protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); try { // Load window placement details for previous application session from application settings // Note - if window was closed on a monitor that is now disconnected from the computer, // SetWindowPlacement will place the window onto a visible monitor. var wp Settings.Default.WindowPlacement; wp.length Marshal.SizeOf(typeof (WindowPlacement)); wp.flags 0; wp.showCmd (wp.showCmd SwShowminimized ? SwShownormal : wp.showCmd); var hwnd new WindowInteropHelper(this).Handle; SetWindowPlacement(hwnd, ref wp); } catch { // ignored } }这里有三处细节值得注意动态修正length从设置反序列化回来的结构体其length字段可能不可靠因此在调用 API 前用Marshal.SizeOf(typeof(WindowPlacement))重新计算并赋值确保向原生层传递的是当前进程视角下正确的结构大小清零flags示例强制把flags置为 0避免历史遗留的标志位干扰恢复行为最小化状态修正wp.showCmd SwShowminimized ? SwShownormal : wp.showCmd这条三元表达式是示例的一个巧妙处理——如果窗口上次关闭时处于最小化状态直接恢复会导致窗口下次以最小化方式出现在任务栏用户几乎察觉不到它「回来了」。因此这里把最小化强制改为正常显示SW_SHOWNORMAL让窗口以常规形态呈现同时位置依然沿用记忆值。最大化状态SW_SHOWMAXIMIZED即 3则原样保留。整个恢复过程被try/catch包裹并静默忽略异常由于状态数据来自用户本地文件存在被篡改、损坏或版本不匹配的可能示例选择「恢复失败就按默认状态打开」把健壮性放在首位。五、持久化层用户级应用设置与 XML 序列化SaveWindowState 示例把窗口状态存入User 作用域的应用设置而非写入注册表或自建配置文件。相关定义集中在三处5.1 Settings.settings设置的定义Properties/Settings.settings 中声明了唯一一个设置项作用域为User类型直接使用项目自定义结构体SettingsFile ... GeneratedClassNamespaceSaveWindowState.Properties GeneratedClassNameSettings Profiles / Settings Setting NameWindowPlacement TypeSaveWindowState.WindowPlacement ScopeUser Value Profile(Default) !-- 默认值全零的 WindowPlacement XML -- /Value /Setting /Settings /SettingsFile5.2 Settings.Designer.cs强类型访问入口由 Visual Studio 自动生成的 Properties/Settings.Designer.cs 将该设置暴露为强类型属性代码中通过SaveWindowState.Properties.Settings.Default.WindowPlacement访问。属性标注了UserScopedSettingAttribute并带有一段 XML 形式的DefaultSettingValueAttribute——这证明默认值就是全零的WindowPlacement结构首次运行尚无历史状态时读取到的即为此默认值。5.3 App.config运行时的序列化形态在应用首次运行前默认值还会体现在 App.config 的userSettings段中userSettings SaveWindowState.Properties.Settings setting nameWindowPlacement serializeAsXml value WindowPlacement xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:xsdhttp://www.w3.org/2001/XMLSchema length0/length flags0/flags showCmd0/showCmd minPosition X0/X Y0/Y /minPosition maxPosition X0/X Y0/Y /maxPosition normalPosition Left0/Left Top0/Top Right0/Right Bottom0/Bottom /normalPosition /WindowPlacement /value /setting /SaveWindowState.Properties.Settings /userSettings可以看到serializeAsXml是关键它让 .NET 设置框架使用 XML 序列化器从而能把自定义结构体的每个公共字段含嵌套的Point、Rect完整落盘。运行Settings.Default.Save()之后用户级设置会被写入用户主目录下的AppData\...\user.config文件下次启动时自动读回。顺带一提结构体被定义为public且字段均为公共字段正是为了配合 XML 序列化如果字段是private序列化结果将不完整。六、多显示器场景的行为细节README 中明确强调该示例是multi-monitor aware多显示器感知。其依据主要来自两点保存的是全局屏幕坐标normalPosition等字段使用的是以主屏幕左上角为原点的屏幕坐标而非窗口所在显示器内的相对坐标。因此窗口在副屏上的位置也能被如实记录系统级兜底逻辑源码注释明确指出——if window was closed on a monitor that is now disconnected from the computer, SetWindowPlacement will place the window onto a visible monitor。也就是说如果用户上次把窗口放在外接显示器上而下次启动时该显示器已断开SetWindowPlacement会由系统自动把窗口重新放置到一块可见的显示器上而不是让窗口「凭空消失」在屏幕外。这一行为是 Win32SetWindowPlacement的内置保障也正是示例不自己维护「显示器 ID 坐标」映射、直接信任系统 API 的原因——代码量更少边界情况处理得更稳妥。七、注意事项与边界情况从源码中可以提炼出几个值得在生产项目中注意的边界点OnClosing不会在会话结束时触发源码注释明确警告WARNING - Not fired when Application.SessionEnding is fired。如果用户通过系统关机/注销结束会话OnClosing可能不会被调用窗口状态也就来不及保存。需要覆盖该场景时可另行处理SessionEnding事件或结合定时保存等策略首次运行的默认值兜底第一次启动时WindowPlacement是 App.config 中的全零默认值。此时length会被动态修正、flags被清零showCmd0既不等于SwShownormal也不等于SwShowminimized三元表达式保持原值传入——此时SetWindowPlacement的行为由系统决定应用按正常默认位置展示窗口不会报错异常静默恢复阶段的catch { /* ignored */ }表明该示例把「状态恢复」定位为尽力而为的增强特性任何反序列化或 API 调用失败都不应阻断窗口正常启动工程文件中的大小写差异从仓库文件列表看磁盘上实际为 POINT.cs、RECT.cs、Windowplacement.cs而 SaveWindowState.csproj 中Compile IncludePoint.cs /、Compile IncludeRect.cs /、Compile IncludeWindowPlacement.cs /存在大小写差异。在 Windows 的大小写不敏感文件系统上可正常编译若在大小写敏感环境下构建需要注意统一命名目标框架当前工程文件 SaveWindowState.csproj 声明TargetFramework为net10.0-windows并启用UseWpfApp.config 中则保留了 .NET Framework 4.5.2 的supportedRuntime声明体现出该示例随仓库演进、兼容新旧运行时的特点。本示例依赖user32.dll互操作本质上是 Windows 专属方案。八、构建、部署与运行按 README.md 的说明构建与运行步骤如下获取源码最便捷的方式是不借助 Git直接在仓库页面点击Download ZIP下载当前版本压缩包解压整个归档后用Visual Studio 2022打开。部署示例选择Build Deploy Solution生成并部署解决方案。部署并运行示例调试运行按F5或选择Debug Start Debugging免调试运行按CtrlF5或选择Debug Start Without Debugging。运行后可以验证完整效果把窗口拖动到某个位置甚至副屏调整大小最大化或最小化一次然后关闭应用再次启动窗口应当出现在关闭时的位置和尺寸上。若在最小化状态下关闭重新打开时会以正常显示状态出现在记忆位置——这正是showCmd修正逻辑在起作用。九、小结与扩展思路SaveWindowState 示例用不到百行核心代码演示了一条完整的「Win32 互操作 应用设置持久化」链路数据结构WindowPlacement含Point/Rect精确镜像原生WINDOWPLACEMENT保存OnClosingGetWindowPlacementSettings.Save()恢复OnSourceInitializedSetWindowPlacement并修正length、flags与最小化状态持久化User 作用域、serializeAsXml的应用设置兼顾强类型访问与人类可读的落盘文件多显示器依赖系统级兜底自动处理显示器断开等边界情况。如果你想进一步扩展可以在同一框架内加入SessionEnding状态保存、窗口是否最大化/全屏等额外状态位或把该逻辑抽象成可复用的窗口基类。核心 API 调用模式不变——掌握GetWindowPlacement/SetWindowPlacement这一对配对也就掌握了 WPF 窗口状态持久化的钥匙。参考文件索引均位于当前仓库示例说明文档Windows/SaveWindowState/README.md核心逻辑保存/恢复 P/InvokeWindows/SaveWindowState/MainWindow.cs数据结构Windowplacement.cs、POINT.cs、RECT.cs设置定义与序列化Properties/Settings.settings、Properties/Settings.Designer.cs、App.config工程与窗口声明SaveWindowState.csproj、MainWindow.xaml、App.xaml赞分享示例工程【免费下载链接】WPF-SamplesRepository for WPF related samples项目地址https://gitcode.com/gh_mirrors/wp/WPF-Samples点击查看免费下载相关推荐RedisInsight 终极指南从零构建专业级Redis可视化平台RedisInsight 终极指南从零构建专业级Redis可视化平台 RedisInsight作为Redis官方推出的可视化GUI工具彻底改变了开发者与Re数据库客户端桌面应用后端前端数据可视化WPF UI窗口管理多窗口通信与状态保存WPF UI窗口管理多窗口通信与状态保存 引言 在WPF应用程序开发中窗口管理是一个核心环节尤其是当应用需要处理多个窗口之间的通信以及状态保存时。WPFUI组件桌面应用Pomotroid 窗口位置记忆基于 Tauri 2 的主窗口几何状态持久化与多显示器恢复方案Pomotroid 窗口位置记忆基于 Tauri 2 的主窗口几何状态持久化与多显示器恢复方案 Pomotroid 项目根目录 https://link.g上一篇开源精品夜莺Nightingale企业级观测平台新选择下一篇如何理解 CSS layerTailwind CSS v4 中的样式优先级控制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

网络基础篇~三次握手和四次挥手

网络基础篇~三次握手和四次挥手

一、TCP 头部数据结构(一)TCP头部数据结构:【源端口号16位】 | 【目标端口号16位】seq(序列号32位):序列号,用于将多个数据包进行排序,形成一个完整有序的数据流ack(确认应答号32位):SYN(报文&…

2026/10/15 1:52:07 阅读更多 →
Rust 常量函数 `const fn` 完全解析:RFC 911 设计初衷、常量表达式规则与三十年的能力演进

Rust 常量函数 `const fn` 完全解析:RFC 911 设计初衷、常量表达式规则与三十年的能力演进

文档 【免费下载链接】rfcs RFCs for changes to Rust 项目地址: https://gitcode.com/gh_mirrors/rf/rfcs 点击查看 免费下载 导读:本文以 Rust RFC 仓库中的 RFC 911《const fn》 为核心,系统梳理 const fn 引入的动机、语法设计与常量表达…

2026/10/15 1:52:07 阅读更多 →
用任意语言实现“Lenguaje Hacker“:MoureDev 2023 编程挑战第 1 题(leet / 1337 字符映射转换)完整实战指南

用任意语言实现“Lenguaje Hacker“:MoureDev 2023 编程挑战第 1 题(leet / 1337 字符映射转换)完整实战指南

示例工程 【免费下载链接】retos-programacion-2023 Ejercicios de cdigo semanales en 2023 de la comunidad MoureDev para practicar lgica en cualquier lenguaje de programacin. 项目地址: https://gitcode.com/gh_mirrors/re/retos-programacion-2023 点击查…

2026/10/15 1:52:07 阅读更多 →

最新新闻

Americhem在中国苏州启用全新洁净配混设施

Americhem在中国苏州启用全新洁净配混设施

全球定制聚合物解决方案领导者Americhem于9月28日举行了其位于苏州的全新洁净配混设施启用仪式。此项投资进一步提升了Americhem服务中国、亚太地区以及在该区域开展生产制造业务的全球客户的能力。 该设施专为满足严苛医疗器械应用所需的ISO 13485和cGMP要求而设计&#xff0c…

2026/10/15 2:28:21 阅读更多 →
如何降低文献综述的AI检测率:策略与技巧

如何降低文献综述的AI检测率:策略与技巧

1. 文献综述写作中的AI检测率问题剖析最近在学术圈里有个现象特别有意思:不少研究生朋友在提交论文查重时,发现文献综述部分的AI检测率总是居高不下。上周就遇到个案例,某高校硕士生小张的文献综述AI率竟然高达38%,远超过学院规定…

2026/10/15 2:28:21 阅读更多 →
青岛栈桥12家老字号美食攻略与避坑指南

青岛栈桥12家老字号美食攻略与避坑指南

1. 青岛栈桥美食地图:本地人私藏的12家老字号青岛栈桥作为城市地标,周边隐藏着许多经营数十年的地道美食。这些店铺往往没有华丽的装潢,却用最朴实的味道征服了几代食客的味蕾。我作为在青岛生活15年的"老饕",今天要分享…

2026/10/15 2:28:21 阅读更多 →
AI辅助科研:驱动科研创新升级的新动能与实践路径

AI辅助科研:驱动科研创新升级的新动能与实践路径

外文文献“白嫖”与高效阅读全攻略 宝子们,是不是一提到下载外文文献就头大? 想找的文献要么全网搜不到,要么下载流程复杂到让人原地爆炸。好不容易在某个小网站找到了资源,又担心版权问题不敢点,更别说有些平台动辄几…

2026/10/15 2:28:21 阅读更多 →
毕业设计-基于微信小程序的广州旅游服务系统

毕业设计-基于微信小程序的广州旅游服务系统

第1章 绪论:背景(政策/市场/技术/广州产业四方面) 国内外现状 目的意义(提升体验/促进产业/创新模式/智慧城市) 第2章 技术介绍:B/S、Uni-app、MySQL、Django、Vue 第3章 分析与设计:用户/系统…

2026/10/15 2:28:21 阅读更多 →
程序员英语词汇宝典词条精讲:exclamation mark(感叹号 `!`)——发音、词义与编程实战用法

程序员英语词汇宝典词条精讲:exclamation mark(感叹号 `!`)——发音、词义与编程实战用法

教程 【免费下载链接】most-frequent-technology-english-words 程序员工作中常见的英语词汇 项目地址: https://gitcode.com/gh_mirrors/mo/most-frequent-technology-english-words 点击查看 免费下载 本文以开源仓库 most-frequent-technology-english-words&am…

2026/10/15 2:27:21 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →