恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
WPF WindowState 与 MVVM:在 ViewModel 中控制窗口最大化、最小化与还原
首页
资讯中心
/
WPF WindowState 与 MVVM:在 ViewModel 中控制窗口最大化、最小化与还原
WPF WindowState 与 MVVM:在 ViewModel 中控制窗口最大化、最小化与还原
发布时间:2026/9/4 6:27:14
在 C#/WPF 项目里窗口的最大化、最小化和还原看起来是系统标题栏自带的能力。可一旦进入自定义标题栏、无边框窗口、托盘菜单、快捷键联动这类场景窗口状态就需要由 ViewModel 里的命令来驱动。这个过程虽然不复杂但如果对 WPF 的 MVVM 机制理解不深很容易把代码写成 View 里直接访问窗口、ViewModel 里又通过全局窗口列表查找窗口的混合模式时间一长窗口管理工作越来越难维护。这里围绕一个真实常见的问题展开如何在 ViewModel 中定义命令切换 WPF 窗口的最大化、最小化和还原状态。文章会先从 WPF 的 WindowState 机制讲起再给出一个可以直接运行的 MVVM 最小示例最后补充自定义标题栏场景下的细节、常见问题和生产环境建议。读完以后你可以把这套写法直接放进自己的 WPF 项目中。1. 窗口状态控制的本质是 WindowState 属性不是窗口方法1.1 WindowState 是一个依赖属性不是一组窗口方法WPF 的Window类没有提供Maximize()、Minimize()、Restore()这类公开方法。窗口的最大化、最小化和还原本质上是修改Window.WindowState这个依赖属性的值。WindowState是枚举类型主要有三个值枚举值含义典型使用场景WindowState.Normal普通还原状态表示窗口恢复为之前设置的大小和位置WindowState.Maximized最大化窗口铺满屏幕工作区WindowState.Minimized最小化窗口收缩到任务栏所以在代码里想让窗口最大化并不需要调用某种特殊 API只需要执行window.WindowState WindowState.Maximized;如果想让窗口从最大化状态还原把WindowState设置回WindowState.Normal即可。这个机制决定了 MVVM 模式下的实现思路窗口状态更适合作为一个状态属性来管理而不是作为一个动作来调用。只要 ViewModel 能把自己的状态同步给窗口窗口自然就会切换状态。1.2 为什么不能把所有逻辑都写在 Button.Click 里如果只在默认标题栏上运行系统已经帮我们处理了最小化、最大化、还原按钮。但是下面这些场景会逼着你把控制权从 View 中拿出来窗口使用了自定义标题栏右上角的最小化、最大化、还原按钮是开发人员自己画的。窗口状态需要和设置项联动比如开机后记住上次是最大化还是还原。托盘菜单需要控制某个窗口最小化或还原。快捷键需要触发窗口切换状态。需要自动化测试窗口逻辑。如果把状态逻辑直接写在 Button.Click 事件里窗口动作和 ViewModel 状态很容易脱节。例如你在代码里用this.WindowState WindowState.Maximized;把窗口放大了但 ViewModel 组件如果需要知道当前窗口是否处于最大化状态它只能通过另一个窗口事件再去同步状态来源就变得分散。MVVM 的做法是让 ViewModel 持有“窗口应该处于什么状态”这个事实View 只负责把 ViewModel 的状态作用到真正的窗口对象上。这样无论按钮点击、快捷键还是菜单命令触发入口都统一在 ViewModel 的命令层。1.3 一个先要确认的架构判断ViewModel 是否允许依赖 System.WindowsWPF 中WindowState枚举位于System.Windows命名空间。设计 ViewModel 时要先确定一个边界分层要求可以在 ViewModel 中使用的状态类型说明单项目 WPFViewModel 和 View 在同一程序集可以直接使用System.Windows.WindowState代码最简单绑定最直接ViewModel 独立类库且不允许引用 WPF 类型建议自定义WindowDisplayState枚举或使用 string需要额外做一层值转换需要跨 WPF、Avalonia 或 MAUI 共用 ViewModel建议通过接口或者自定义 UI 状态枚举窗口控制逻辑下沉到服务层如果项目本身只在 WPF 中运行并且 ViewModel 不会被其他 UI 框架复用直接使用WindowState完全可行。文章后面的示例默认采用这种做法。若你的项目严格要求 ViewModel 不依赖System.Windows可以保留“控制命令”和“状态属性”的核心结构只是把属性的枚举类型换成自定义类型再增加一个转换器。2. 准备工作搭建可以跑起来的最小 MVVM 项目2.1 项目环境与结构下面示例基于 Visual Studio 2022 和 .NET 8 创建 WPF 项目。项目名假设为ViewModelWindowStateDemo。其他现代版本例如 .NET 6 或 .NET 9操作方式相同。创建 WPF 项目后项目文件主要包含Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet8.0-windows/TargetFramework Nullableenable/Nullable UseWPFtrue/UseWPF /PropertyGroup /Project示例代码会涉及下面这些文件ViewModelWindowStateDemo ├── App.xaml ├── App.xaml.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── ViewModels │ ├── ViewModelBase.cs │ ├── RelayCommand.cs │ └── MainWindowViewModel.csViewModelBase.cs和RelayCommand.cs是 MVVM 中最基础的类型不依赖具体业务。2.2 ViewModelBase 只负责属性通知为了把窗口状态显示到界面上属性必须实现INotifyPropertyChanged。这里写一个简洁的基类using System.ComponentModel; using System.Runtime.CompilerServices; namespace ViewModelWindowStateDemo.ViewModels { public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected bool SetPropertyT( ref T field, T value, [CallerMemberName] string? propertyName null) { if (EqualityComparerT.Default.Equals(field, value)) { return false; } field value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged([CallerMemberName] string? propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }SetProperty方法的作用有两个第一只在值真正变化时才通知界面避免无效刷新第二用CallerMemberName自动带出调用属性的名字这样属性 setter 里不需要手动写字符串。2.3 RelayCommand 把按钮动作包装成命令WPF 中的Button支持Command属性。为了让 ViewModel 里的方法能被按钮调用需要实现ICommand。常见的最小实现如下using System; using System.Windows.Input; namespace ViewModelWindowStateDemo.ViewModels { public sealed class RelayCommand : ICommand { private readonly Actionobject? _execute; private readonly Predicateobject?? _canExecute; public RelayCommand( Actionobject? execute, Predicateobject?? canExecute null) { _execute execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute canExecute; } public bool CanExecute(object? parameter) { return _canExecute?.Invoke(parameter) ?? true; } public void Execute(object? parameter) { _execute(parameter); } public event EventHandler? CanExecuteChanged { add CommandManager.RequerySuggested value; remove CommandManager.RequerySuggested - value; } } }要让Command绑定生效需要注意两个点。第一RelayCommand的构造函数要求传入一个Actionobject?。即使方法不需要参数也要写成_ 这样能够适配 object 参数的形式。第二CanExecuteChanged使用了CommandManager.RequerySuggested这是 WPF 中比较常见的写法。它保证当界面焦点、鼠标等系统状态变化时WPF 会重新查询命令是否可执行。后面如果某个命令需要根据窗口状态控制是否可用会用到这个机制。3. 核心方案在 ViewModel 中暴露窗口状态属性3.1 不建议在 ViewModel 里持有 Window 对象设计方案时最容易想到的做法是让 ViewModel 命令直接拿到窗口对象private void MinimizeButtonClick(object? parameter) { if (parameter is Window window) { window.WindowState WindowState.Minimized; } }这种写法虽然在简单场景下能用但它有几个问题ViewModel 需要感知Window类型View 层特点被带进了业务逻辑层。在多窗口项目中按钮到底控制哪个窗口必须靠 CommandParameter 传入参数传错或没有传就会出现按钮点了没反应。单元测试不方便测试时要先创建真实窗口而不是只测一个状态。因此更稳妥的主方案是ViewModel 只维护一个WindowState属性WPF 通过双向绑定把属性值应用到真实窗口上。3.2 编写 MainWindowViewModel在ViewModels文件夹下添加MainWindowViewModel.csusing System.Windows; using System.Windows.Input; using ViewModelWindowStateDemo.ViewModels; namespace ViewModelWindowStateDemo.ViewModels { public class MainWindowViewModel : ViewModelBase { private WindowState _currentWindowState WindowState.Normal; public MainWindowViewModel() { MinimizeCommand new RelayCommand(_ CurrentWindowState WindowState.Minimized); ToggleMaximizeRestoreCommand new RelayCommand(_ ToggleMaximizeRestore()); } public WindowState CurrentWindowState { get _currentWindowState; set { if (SetProperty(ref _currentWindowState, value)) { OnPropertyChanged(nameof(IsMaximized)); OnPropertyChanged(nameof(MaximizeRestoreButtonText)); } } } public bool IsMaximized CurrentWindowState WindowState.Maximized; public string MaximizeRestoreButtonText CurrentWindowState WindowState.Maximized ? 还原 : 最大化; public ICommand MinimizeCommand { get; } public ICommand ToggleMaximizeRestoreCommand { get; } private void ToggleMaximizeRestore() { CurrentWindowState CurrentWindowState WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; } } }这里有几个关键点需要解释。第一个关键点是CurrentWindowState的类型直接是WindowState。它保存的是 ViewModel 认为“窗口当前应该处于什么状态”。MinimizeCommand只做一件事就是把这个状态改成WindowState.Minimized。第二个关键点是Windows 的还原和最大化通常使用同一个按钮。窗口处于普通状态时按钮叫“最大化”处于最大化状态时按钮叫“还原”。因此 ViewModel 提供了ToggleMaximizeRestoreCommand用它来处理这个切换逻辑。第三个关键点是CurrentWindowState发生变化后除了通知自身变化还要通知依赖它的两个属性IsMaximized供后续界面触发器使用。MaximizeRestoreButtonText让按钮文案自动切换。如果不通知这两个依赖属性界面上的文字仍然保持旧值就会出现“窗口已经最大化了按钮还显示最大化”的问题。3