WPF 多窗口与弹窗管理
大约 15 分钟约 4422 字
WPF 多窗口与弹窗管理
简介
在复杂的企业级 WPF 应用中,往往需要同时管理多个窗口:主窗口、对话框、浮动工具窗口、通知弹窗等。如何优雅地管理窗口的生命周期、协调窗口间的通信、处理多显示器布局、实现窗口状态的持久化,是构建高质量桌面应用的关键课题。
良好的多窗口管理方案应具备以下特征:统一的窗口创建策略、清晰的生命周期管理、可测试的弹窗服务、可靠的状态持久化,以及良好的多显示器支持。
特点
- 统一窗口管理:通过服务层集中管理所有窗口的创建和销毁
- MVVM 友好:弹窗逻辑与 ViewModel 解耦,通过服务接口调用
- 生命周期控制:完整的窗口打开、激活、关闭流程管理
- 多显示器感知:窗口位置与显示器配置适配
- 状态持久化:窗口位置、大小、状态跨会话保存
- 弹窗队列:避免多个弹窗互相遮挡或冲突
实现
Window 生命周期管理
// 窗口生命周期管理器
public class WindowLifecycleManager
{
private readonly Dictionary<string, Window> _activeWindows = new();
private readonly ILogger _logger;
public WindowLifecycleManager(ILogger logger)
{
_logger = logger;
}
// 注册窗口
public void Register(string key, Window window)
{
window.Closed += (s, e) =>
{
_activeWindows.Remove(key);
_logger.LogInformation($"窗口 {key} 已关闭");
};
_activeWindows[key] = window;
_logger.LogInformation($"窗口 {key} 已注册");
}
// 获取活跃窗口
public T GetWindow<T>(string key) where T : Window
{
if (_activeWindows.TryGetValue(key, out var window))
{
return window as T;
}
return null;
}
// 关闭所有窗口
public void CloseAll()
{
foreach (var window in _activeWindows.Values.ToList())
{
window.Close();
}
_activeWindows.Clear();
}
// 激活或创建窗口(单例模式)
public T ActivateOrCreate<T>(string key, Func<T> factory) where T : Window
{
var existing = GetWindow<T>(key);
if (existing != null)
{
if (existing.WindowState == WindowState.Minimized)
{
existing.WindowState = WindowState.Normal;
}
existing.Activate();
return existing;
}
var newWindow = factory();
Register(key, newWindow);
return newWindow;
}
}对话框管理服务
IDialogService 接口设计
// 对话框服务接口(MVVM 友好)
public interface IDialogService
{
Task<bool> ShowMessageAsync(string title, string message, MessageType type);
Task<bool> ShowConfirmationAsync(string title, string message);
Task<string> ShowInputAsync(string title, string message, string defaultValue = "");
Task<T> ShowCustomDialogAsync<T>(string dialogType, object parameter = null);
void ShowNotification(string title, string message, NotificationType type);
}DialogService 实现
public class DialogService : IDialogService
{
private readonly IWindowFactory _windowFactory;
private readonly ILogger _logger;
public DialogService(IWindowFactory windowFactory, ILogger logger)
{
_windowFactory = windowFactory;
_logger = logger;
}
public Task<bool> ShowMessageAsync(string title, string message, MessageType type)
{
var tcs = new TaskCompletionSource<bool>();
Application.Current.Dispatcher.Invoke(() =>
{
var window = new MessageDialogWindow
{
Title = title,
Message = message,
Type = type
};
window.Closed += (s, e) =>
{
tcs.TrySetResult(window.Result);
};
SetOwner(window);
window.ShowDialog();
});
return tcs.Task;
}
public Task<bool> ShowConfirmationAsync(string title, string message)
{
return ShowMessageAsync(title, message, MessageType.Confirmation);
}
public Task<string> ShowInputAsync(string title, string message, string defaultValue = "")
{
var tcs = new TaskCompletionSource<string>();
Application.Current.Dispatcher.Invoke(() =>
{
var window = new InputDialogWindow
{
Title = title,
Message = message,
InputValue = defaultValue
};
window.Closed += (s, e) =>
{
tcs.TrySetResult(window.DialogResult == true ? window.InputValue : null);
};
SetOwner(window);
window.ShowDialog();
});
return tcs.Task;
}
public Task<T> ShowCustomDialogAsync<T>(string dialogType, object parameter = null)
{
var tcs = new TaskCompletionSource<T>();
Application.Current.Dispatcher.Invoke(() =>
{
var window = _windowFactory.Create(dialogType, parameter);
var dialog = window as ICustomDialog<T>;
window.Closed += (s, e) =>
{
tcs.TrySetResult(dialog.GetResult());
};
SetOwner(window);
window.ShowDialog();
});
return tcs.Task;
}
public void ShowNotification(string title, string message, NotificationType type)
{
Application.Current.Dispatcher.Invoke(() =>
{
var notification = new NotificationWindow
{
Title = title,
Message = message,
NotificationType = type
};
notification.Show();
});
}
private void SetOwner(Window dialog)
{
var activeWindow = Application.Current.Windows.OfType<Window>()
.FirstOrDefault(w => w.IsActive);
if (activeWindow != null)
{
dialog.Owner = activeWindow;
}
}
}自定义对话框基类
// 可复用的对话框基类
public abstract class DialogWindowBase : Window, IDisposable
{
protected DialogWindowBase()
{
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ShowInTaskbar = false;
ResizeMode = ResizeMode.NoResize;
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
// 统一样式
var border = new Border
{
Background = Brushes.White,
CornerRadius = new CornerRadius(8),
BorderBrush = Brushes.LightGray,
BorderThickness = new Thickness(1),
Effect = new DropShadowEffect
{
BlurRadius = 10,
ShadowDepth = 2,
Opacity = 0.3
}
};
var content = CreateDialogContent();
border.Child = content;
this.Content = border;
}
protected abstract FrameworkElement CreateDialogContent();
// ESC 关闭
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
DialogResult = false;
Close();
}
base.OnKeyDown(e);
}
// 点击外部关闭
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
}
public void Dispose()
{
// 清理资源
}
}
// 消息对话框实现
public class MessageDialogWindow : DialogWindowBase
{
public string Message { get; set; }
public MessageType Type { get; set; }
public bool Result { get; private set; }
protected override FrameworkElement CreateDialogContent()
{
var stack = new StackPanel { Margin = new Thickness(20) };
// 图标
var icon = new TextBlock
{
FontSize = 32,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 10)
};
switch (Type)
{
case MessageType.Info:
icon.Text = "(i)";
break;
case MessageType.Warning:
icon.Text = "(!)";
break;
case MessageType.Error:
icon.Text = "(X)";
break;
case MessageType.Confirmation:
icon.Text = "(?)";
break;
}
stack.Children.Add(icon);
// 消息文本
stack.Children.Add(new TextBlock
{
Text = Message,
TextWrapping = TextWrapping.Wrap,
FontSize = 14,
Margin = new Thickness(0, 0, 0, 20),
HorizontalAlignment = HorizontalAlignment.Center
});
// 按钮区域
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Center
};
if (Type == MessageType.Confirmation)
{
buttonPanel.Children.Add(CreateButton("确定", () =>
{
Result = true;
DialogResult = true;
}));
buttonPanel.Children.Add(CreateButton("取消", () =>
{
Result = false;
DialogResult = false;
}));
}
else
{
buttonPanel.Children.Add(CreateButton("关闭", () =>
{
Result = true;
DialogResult = true;
}));
}
stack.Children.Add(buttonPanel);
return stack;
}
private Button CreateButton(string text, Action onClick)
{
return new Button
{
Content = text,
Width = 80,
Height = 30,
Margin = new Thickness(5, 0, 5, 0),
Command = new RelayCommand(onClick)
};
}
}
public enum MessageType { Info, Warning, Error, Confirmation }非模态窗口协调
// 非模态窗口协调器
public class NonModalWindowCoordinator
{
private readonly Dictionary<string, WeakReference<Window>> _windows = new();
private readonly Dictionary<string, WindowState> _savedStates = new();
// 显示或激活非模态窗口
public void ShowOrActivate<TWindow>(string key, object viewModel = null)
where TWindow : Window, new()
{
if (_windows.TryGetValue(key, out var weakRef) &&
weakRef.TryGetTarget(out var existing) &&
existing.IsLoaded)
{
// 窗口已存在,激活它
if (existing.WindowState == WindowState.Minimized)
{
existing.WindowState = WindowState.Normal;
}
existing.Activate();
existing.Focus();
return;
}
// 创建新窗口
var window = new TWindow();
if (viewModel != null)
{
window.DataContext = viewModel;
}
// 恢复保存的状态
if (_savedStates.TryGetValue(key, out var state))
{
window.Left = state.Left;
window.Top = state.Top;
window.Width = state.Width;
window.Height = state.Height;
}
_windows[key] = new WeakReference<Window>(window);
window.Closed += (s, e) =>
{
// 保存窗口状态
var w = s as Window;
_savedStates[key] = new WindowState
{
Left = w.Left,
Top = w.Top,
Width = w.Width,
Height = w.Height
};
_windows.Remove(key);
};
window.Show();
}
// 关闭指定窗口
public void Close(string key)
{
if (_windows.TryGetValue(key, out var weakRef) &&
weakRef.TryGetTarget(out var window))
{
window.Close();
}
}
// 关闭所有非模态窗口
public void CloseAll()
{
foreach (var kvp in _windows.ToList())
{
if (kvp.Value.TryGetTarget(out var window))
{
window.Close();
}
}
}
}
public record WindowState
{
public double Left { get; init; }
public double Top { get; init; }
public double Width { get; init; }
public double Height { get; init; }
}Owner-Owned 窗口关系
// 窗口父子关系管理
public class WindowOwnershipManager
{
// 设置窗口父子关系
public static void SetOwnership(Window owner, Window owned)
{
owned.Owner = owner;
// 父窗口关闭时关闭子窗口
owner.Closed += (s, e) =>
{
if (owned.IsLoaded)
{
owned.Close();
}
};
// 父窗口最小化时子窗口也最小化
owner.StateChanged += (s, e) =>
{
if (owner.WindowState == WindowState.Minimized)
{
owned.WindowState = WindowState.Minimized;
}
else if (owned.WindowState == WindowState.Minimized)
{
owned.WindowState = owner.WindowState;
}
};
}
// 获取所有子窗口
public static IEnumerable<Window> GetOwnedWindows(Window owner)
{
return Application.Current.Windows.OfType<Window>()
.Where(w => w.Owner == owner);
}
// 获取顶层所有者
public static Window GetTopOwner(Window window)
{
var current = window;
while (current.Owner != null)
{
current = current.Owner;
}
return current;
}
}通知窗口
// 类似 Toast 的通知窗口
public class ToastNotificationWindow : Window
{
private DispatcherTimer _autoCloseTimer;
private readonly double _screenHeight;
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register("Message", typeof(string),
typeof(ToastNotificationWindow));
public string Message
{
get => (string)GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public ToastNotificationWindow(string message, int durationMs = 3000)
{
Message = message;
// 窗口样式
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
ShowInTaskbar = false;
Topmost = true;
Focusable = false;
Width = 320;
SizeToContent = SizeToContent.Height;
// 获取屏幕尺寸
_screenHeight = SystemParameters.PrimaryScreenHeight;
InitializeComponent();
StartAutoClose(durationMs);
}
private void InitializeComponent()
{
var border = new Border
{
Background = new SolidColorBrush(Color.FromArgb(230, 50, 50, 50)),
CornerRadius = new CornerRadius(6),
Padding = new Thickness(16, 12, 16, 12),
Margin = new Thickness(8)
};
var stack = new StackPanel();
stack.Children.Add(new TextBlock
{
Text = Message,
Foreground = Brushes.White,
TextWrapping = TextWrapping.Wrap,
FontSize = 13
});
// 进度条
var progress = new ProgressBar
{
Height = 3,
Margin = new Thickness(0, 8, 0, 0),
Foreground = Brushes.White,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0)
};
var anim = new DoubleAnimation(100, 0,
TimeSpan.FromMilliseconds(_autoCloseTimer?.Interval.TotalMilliseconds ?? 3000));
progress.BeginAnimation(ProgressBar.ValueProperty, anim);
stack.Children.Add(progress);
border.Child = stack;
this.Content = border;
// 鼠标交互
border.MouseLeftButtonUp += (s, e) => Close();
}
private void StartAutoClose(int durationMs)
{
_autoCloseTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(durationMs)
};
_autoCloseTimer.Tick += (s, e) =>
{
_autoCloseTimer.Stop();
PerformCloseAnimation();
};
_autoCloseTimer.Start();
}
private void PerformCloseAnimation()
{
var animation = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
animation.Completed += (s, e) => Close();
this.BeginAnimation(OpacityProperty, animation);
}
// 通知队列管理器
public static class ToastManager
{
private static readonly List<ToastNotificationWindow> _activeToasts = new();
private const double ToastHeight = 80;
private const double ToastSpacing = 8;
private const double BottomMargin = 60;
public static void Show(string message, int durationMs = 3000)
{
Application.Current.Dispatcher.Invoke(() =>
{
var toast = new ToastNotificationWindow(message, durationMs);
// 计算位置(右下角堆叠)
var screen = SystemParameters.WorkArea;
toast.Left = screen.Right - toast.Width - 16;
toast.Top = screen.Bottom - BottomMargin -
(_activeToasts.Count * (ToastHeight + ToastSpacing));
toast.Closed += (s, e) =>
{
_activeToasts.Remove(toast);
RearrangeToasts();
};
_activeToasts.Add(toast);
// 淡入动画
toast.Opacity = 0;
toast.Show();
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200));
toast.BeginAnimation(OpacityProperty, fadeIn);
});
}
private static void RearrangeToasts()
{
var screen = SystemParameters.WorkArea;
for (int i = 0; i < _activeToasts.Count; i++)
{
var toast = _activeToasts[i];
var targetTop = screen.Bottom - BottomMargin -
(i * (ToastHeight + ToastSpacing));
var animation = new DoubleAnimation(toast.Top, targetTop,
TimeSpan.FromMilliseconds(150));
toast.BeginAnimation(TopProperty, animation);
}
}
}
}Splash Screen(启动画面)
// 带进度报告的启动画面
public class SplashScreenManager
{
private Window _splashWindow;
private TextBlock _statusText;
private ProgressBar _progressBar;
public async Task ShowAsync(Func<IProgress<SplashProgress>, Task> initializationAction)
{
CreateSplashWindow();
_splashWindow.Show();
var progress = new Progress<SplashProgress>(p =>
{
_statusText.Text = p.Message;
_progressBar.Value = p.Percentage;
});
try
{
await initializationAction(progress);
}
catch (Exception ex)
{
_statusText.Text = $"初始化失败: {ex.Message}";
await Task.Delay(2000);
}
// 淡出动画
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(500));
fadeOut.Completed += (s, e) => _splashWindow.Close();
_splashWindow.BeginAnimation(OpacityProperty, fadeOut);
}
private void CreateSplashWindow()
{
_splashWindow = new Window
{
WindowStyle = WindowStyle.None,
ResizeMode = ResizeMode.NoResize,
WindowState = WindowState.Normal,
ShowInTaskbar = false,
Topmost = true,
Width = 480,
Height = 320,
AllowsTransparency = true,
Background = Brushes.Transparent,
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
var border = new Border
{
Background = Brushes.White,
CornerRadius = new CornerRadius(12),
Effect = new DropShadowEffect
{
BlurRadius = 20,
ShadowDepth = 3,
Opacity = 0.3
}
};
var stack = new StackPanel
{
Margin = new Thickness(30),
VerticalAlignment = VerticalAlignment.Center
};
// 应用 Logo
stack.Children.Add(new TextBlock
{
Text = "My Application",
FontSize = 28,
FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 30)
});
_statusText = new TextBlock
{
Text = "正在初始化...",
FontSize = 13,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 10)
};
stack.Children.Add(_statusText);
_progressBar = new ProgressBar
{
Height = 6,
Minimum = 0,
Maximum = 100,
Value = 0
};
stack.Children.Add(_progressBar);
border.Child = stack;
_splashWindow.Content = border;
}
}
public record SplashProgress(string Message, double Percentage);系统托盘弹窗
// 系统托盘弹窗管理
public class TrayPopupManager : IDisposable
{
private readonly NotifyIcon _notifyIcon;
private Window _popupWindow;
private bool _disposed;
public TrayPopupManager()
{
_notifyIcon = new NotifyIcon
{
Icon = System.Drawing.Icon.ExtractAssociatedIcon(
Assembly.GetExecutingAssembly().Location),
Visible = true,
Text = "My Application"
};
_notifyIcon.Click += OnTrayIconClick;
_notifyIcon.BalloonTipClicked += OnBalloonTipClicked;
}
// 托盘图标的右键菜单
public void SetContextMenu(System.Windows.Forms.ToolStripItem[] items)
{
var menu = new System.Windows.Forms.ContextMenuStrip();
menu.Items.AddRange(items);
_notifyIcon.ContextMenuStrip = menu;
}
// 点击托盘图标显示弹窗
private void OnTrayIconClick(object sender, EventArgs e)
{
if (((System.Windows.Forms.MouseEventArgs)e).Button ==
System.Windows.Forms.MouseButtons.Left)
{
TogglePopup();
}
}
private void TogglePopup()
{
if (_popupWindow != null && _popupWindow.IsLoaded)
{
_popupWindow.Close();
_popupWindow = null;
return;
}
_popupWindow = new Window
{
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent,
ShowInTaskbar = false,
Width = 320,
Height = 400,
Topmost = true,
ResizeMode = ResizeMode.NoResize
};
// 定位在托盘图标上方
var screen = System.Windows.Forms.Screen.PrimaryScreen;
_popupWindow.Left = screen.WorkingArea.Right - _popupWindow.Width - 16;
_popupWindow.Top = screen.WorkingArea.Bottom - _popupWindow.Height - 16;
var content = CreatePopupContent();
_popupWindow.Content = content;
// 点击外部关闭
_popupWindow.Deactivated += (s, e) => _popupWindow.Close();
_popupWindow.Show();
}
// 显示气泡通知
public void ShowBalloonTip(string title, string text, int timeoutMs = 3000)
{
_notifyIcon.ShowBalloonTip(timeoutMs, title, text,
ToolTipIcon.Info);
}
private void OnBalloonTipClicked(object sender, EventArgs e)
{
// 用户点击了气泡通知
Application.Current.MainWindow.Show();
Application.Current.MainWindow.Activate();
}
private FrameworkElement CreatePopupContent()
{
// 创建弹窗内容
var border = new Border
{
Background = Brushes.White,
CornerRadius = new CornerRadius(8),
Effect = new DropShadowEffect
{
BlurRadius = 10,
ShadowDepth = 2,
Opacity = 0.3
}
};
// ... 添加内容
return border;
}
public void Dispose()
{
if (!_disposed)
{
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
_disposed = true;
}
}
}多显示器窗口管理
// 多显示器感知的窗口管理器
public class MultiMonitorWindowManager
{
// 获取所有显示器信息
public List<MonitorInfo> GetMonitors()
{
var monitors = new List<MonitorInfo>();
foreach (var screen in System.Windows.Forms.Screen.AllScreens)
{
monitors.Add(new MonitorInfo
{
DeviceName = screen.DeviceName,
IsPrimary = screen.Primary,
Bounds = new Rect(
screen.Bounds.X, screen.Bounds.Y,
screen.Bounds.Width, screen.Bounds.Height),
WorkingArea = new Rect(
screen.WorkingArea.X, screen.WorkingArea.Y,
screen.WorkingArea.Width, screen.WorkingArea.Height)
});
}
return monitors;
}
// 将窗口移动到指定显示器
public void MoveToMonitor(Window window, int monitorIndex)
{
var screens = System.Windows.Forms.Screen.AllScreens;
if (monitorIndex < 0 || monitorIndex >= screens.Length) return;
var screen = screens[monitorIndex];
var area = screen.WorkingArea;
// 转换为 WPF 像素单位
var source = PresentationSource.FromVisual(window);
double dpiX = source?.CompositionTarget?.TransformFromDevice.M11 ?? 1.0;
double dpiY = source?.CompositionTarget?.TransformFromDevice.M22 ?? 1.0;
window.Left = area.X * dpiX;
window.Top = area.Y * dpiY;
window.Width = Math.Min(window.Width, area.Width * dpiX);
window.Height = Math.Min(window.Height, area.Height * dpiY);
}
// 跨显示器窗口恢复
public void EnsureWindowVisible(Window window)
{
var screens = System.Windows.Forms.Screen.AllScreens;
bool isVisible = false;
foreach (var screen in screens)
{
if (screen.WorkingArea.Contains(
(int)window.Left, (int)window.Top))
{
isVisible = true;
break;
}
}
if (!isVisible)
{
// 窗口在不可见的显示器上,移到主显示器
var primary = System.Windows.Forms.Screen.PrimaryScreen;
window.Left = primary.WorkingArea.Left + 100;
window.Top = primary.WorkingArea.Top + 100;
}
}
// 记住窗口在哪个显示器上
public WindowPosition SaveWindowPosition(Window window)
{
var position = new WindowPosition
{
Left = window.Left,
Top = window.Top,
Width = window.Width,
Height = window.Height,
WindowState = window.WindowState
};
// 记录所在显示器
var screens = System.Windows.Forms.Screen.AllScreens;
for (int i = 0; i < screens.Length; i++)
{
if (screens[i].WorkingArea.Contains(
(int)window.Left, (int)window.Top))
{
position.MonitorIndex = i;
break;
}
}
return position;
}
// 恢复窗口位置
public void RestoreWindowPosition(Window window, WindowPosition position)
{
if (position == null) return;
var screens = System.Windows.Forms.Screen.AllScreens;
// 检查记录的显示器是否还存在
if (position.MonitorIndex < screens.Length)
{
window.Left = position.Left;
window.Top = position.Top;
}
else
{
// 显示器已断开,移到主显示器
var primary = System.Windows.Forms.Screen.PrimaryScreen;
window.Left = primary.WorkingArea.Left + 100;
window.Top = primary.WorkingArea.Top + 100;
}
window.Width = position.Width;
window.Height = position.Height;
if (position.WindowState == WindowState.Maximized)
{
window.WindowState = WindowState.Maximized;
}
}
}
public record MonitorInfo
{
public string DeviceName { get; init; }
public bool IsPrimary { get; init; }
public Rect Bounds { get; init; }
public Rect WorkingArea { get; init; }
}
public record WindowPosition
{
public double Left { get; init; }
public double Top { get; init; }
public double Width { get; init; }
public double Height { get; init; }
public WindowState WindowState { get; init; }
public int MonitorIndex { get; init; }
}窗口状态持久化
// 窗口状态持久化服务
public class WindowStateService
{
private readonly string _settingsPath;
private Dictionary<string, WindowPosition> _savedPositions;
public WindowStateService()
{
_settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp", "window_state.json");
Load();
}
public void RegisterWindow(Window window, string windowKey)
{
// 恢复上次的位置
if (_savedPositions.TryGetValue(windowKey, out var position))
{
var monitor = new MultiMonitorWindowManager();
monitor.RestoreWindowPosition(window, position);
}
// 保存位置变化
window.Closing += (s, e) =>
{
var pos = new WindowPosition
{
Left = window.RestoreBounds.Left,
Top = window.RestoreBounds.Top,
Width = window.RestoreBounds.Width,
Height = window.RestoreBounds.Height,
WindowState = window.WindowState
};
var monitorMgr = new MultiMonitorWindowManager();
var screens = System.Windows.Forms.Screen.AllScreens;
for (int i = 0; i < screens.Length; i++)
{
if (screens[i].WorkingArea.Contains(
(int)window.RestoreBounds.Left,
(int)window.RestoreBounds.Top))
{
pos = pos with { MonitorIndex = i };
break;
}
}
_savedPositions[windowKey] = pos;
Save();
};
}
private void Load()
{
try
{
if (File.Exists(_settingsPath))
{
var json = File.ReadAllText(_settingsPath);
_savedPositions = JsonSerializer.Deserialize<
Dictionary<string, WindowPosition>>(json)
?? new Dictionary<string, WindowPosition>();
}
else
{
_savedPositions = new Dictionary<string, WindowPosition>();
}
}
catch
{
_savedPositions = new Dictionary<string, WindowPosition>();
}
}
private void Save()
{
try
{
var dir = Path.GetDirectoryName(_settingsPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(_savedPositions,
new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_settingsPath, json);
}
catch
{
// 静默失败,不影响用户体验
}
}
}MDI 风格的标签页窗口
// 在单窗口中实现类似 MDI 的标签页管理
public class TabbedWindowManager
{
private readonly TabControl _tabControl;
private readonly Dictionary<string, TabItem> _tabs = new();
public TabbedWindowManager(TabControl tabControl)
{
_tabControl = tabControl;
}
public void OpenTab(string key, string title, FrameworkElement content,
bool activate = true, bool canClose = true)
{
if (_tabs.TryGetValue(key, out var existingTab))
{
if (activate)
{
_tabControl.SelectedItem = existingTab;
}
return;
}
var tabItem = new TabItem
{
Header = CreateTabHeader(title, canClose, () => CloseTab(key)),
Content = content,
Tag = key
};
_tabs[key] = tabItem;
_tabControl.Items.Add(tabItem);
if (activate)
{
_tabControl.SelectedItem = tabItem;
}
}
public void CloseTab(string key)
{
if (_tabs.TryGetValue(key, out var tab))
{
int index = _tabControl.Items.IndexOf(tab);
_tabControl.Items.Remove(tab);
_tabs.Remove(key);
// 激活相邻标签页
if (_tabControl.Items.Count > 0)
{
_tabControl.SelectedIndex = Math.Min(index,
_tabControl.Items.Count - 1);
}
}
}
public void CloseAllExcept(string key)
{
foreach (var tabKey in _tabs.Keys.Where(k => k != key).ToList())
{
CloseTab(tabKey);
}
}
private object CreateTabHeader(string title, bool canClose, Action closeAction)
{
var dock = new DockPanel();
var text = new TextBlock { Text = title, VerticalAlignment = VerticalAlignment.Center };
if (canClose)
{
var closeBtn = new Button
{
Content = "x",
Width = 18,
Height = 18,
FontSize = 10,
Margin = new Thickness(5, 0, 0, 0),
Padding = new Thickness(0),
Command = new RelayCommand(closeAction)
};
DockPanel.SetDock(closeBtn, Dock.Right);
dock.Children.Add(closeBtn);
}
dock.Children.Add(text);
return dock;
}
}优点
- 统一管理:所有窗口通过服务层集中管理,便于控制和监控
- MVVM 解耦:ViewModel 不直接引用 Window 类型,便于测试
- 状态持久化:窗口位置和大小自动保存恢复,提升用户体验
- 多显示器支持:感知显示器配置变化,避免窗口"消失"
- 弹窗队列:避免弹窗冲突和遮挡
缺点
- 复杂性增加:引入服务层和管理器增加了架构复杂度
- 线程安全:多窗口场景下需要处理跨窗口的线程同步
- 内存占用:多个窗口同时存在会增加内存开销
- 调试困难:窗口间通信和状态同步问题不易定位
性能注意事项
窗口创建优化
// 预创建窗口池
public class WindowPool<T> where T : Window, new()
{
private readonly ConcurrentBag<T> _pool = new();
private readonly int _maxSize;
public WindowPool(int maxSize = 3)
{
_maxSize = maxSize;
}
public T Acquire()
{
if (_pool.TryTake(out var window))
{
return window;
}
return new T();
}
public void Release(T window)
{
if (_pool.Count < _maxSize)
{
// 重置窗口状态
window.DataContext = null;
window.Owner = null;
_pool.Add(window);
}
else
{
window.Close();
}
}
}减少窗口重绘
// 批量更新时冻结窗口
public static class WindowUpdateHelper
{
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
private const int WM_SETREDRAW = 0x0B;
public static void BeginUpdate(Window window)
{
var hwnd = ((HwndSource)PresentationSource.FromVisual(window)).Handle;
SendMessage(hwnd, WM_SETREDRAW, 0, 0);
}
public static void EndUpdate(Window window)
{
var hwnd = ((HwndSource)PresentationSource.FromVisual(window)).Handle;
SendMessage(hwnd, WM_SETREDRAW, 1, 0);
window.InvalidateVisual();
}
}总结
WPF 多窗口管理是企业级应用的重要组成部分。通过 DialogService 实现 MVVM 友好的弹窗机制,通过 WindowLifecycleManager 统一管理窗口生命周期,通过 MultiMonitorWindowManager 支持多显示器场景,通过 WindowStateService 实现状态持久化。核心原则是将窗口管理逻辑从业务逻辑中分离出来,通过服务层进行统一管理。
关键知识点
- 模态与非模态:ShowDialog() vs Show(),前者阻塞调用线程
- Owner 窗口:设置 Owner 可确保子窗口在父窗口前面、随父窗口最小化
- Dispatcher 切换:跨窗口操作需要使用 Dispatcher
- RestoreBounds:最大化/最小化状态下获取实际位置应使用 RestoreBounds
- WeakReference:非模态窗口引用应使用弱引用避免内存泄漏
- HwndSource:获取 WPF 窗口的 Win32 句柄
常见误区
- 直接在 ViewModel 中创建 Window:违反 MVVM 原则
- 忽略窗口关闭事件:未处理 OnClosing 导致数据丢失
- 模态窗口滥用:过多的模态窗口严重影响用户体验
- 窗口位置硬编码:不考虑不同分辨率和显示器配置
- 忽略多显示器场景:断开外接显示器后窗口消失
- 不释放窗口资源:窗口关闭后未释放绑定的数据和事件
进阶路线
- MVVM 框架集成:学习 Prism/Caliburn 的窗口管理机制
- 窗口间通信:Event Aggregator、Messenger 等模式
- 自定义窗口 Chrome:完全自定义窗口外观
- 虚拟桌面支持:Windows 虚拟桌面感知
- 窗口动画:窗口打开/关闭的过渡动画
- 无障碍支持:窗口导航的键盘和屏幕阅读器支持
适用场景
- IDE 风格应用:多个浮动工具窗口
- 数据分析工具:多个图表/表格窗口
- 监控系统:多个实时数据面板
- 工作流应用:向导式多窗口流程
- 即时通讯:聊天窗口管理
- 系统工具:带托盘和通知的后台应用
落地建议
- 统一注册:所有窗口类型通过 DI 容器注册
- 服务接口:通过 IDialogService 抽象弹窗操作
- 配置驱动:窗口的默认大小、位置通过配置文件管理
- 日志记录:窗口的打开/关闭/异常统一记录日志
- 用户偏好:窗口布局作为用户偏好持久化
- 优雅退出:关闭主窗口时有序关闭所有子窗口
// 推荐的启动代码
public partial class App : Application
{
private ServiceProvider _serviceProvider;
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<WindowStateService>();
services.AddSingleton<MultiMonitorWindowManager>();
_serviceProvider = services.BuildServiceProvider();
// 显示启动画面
var splash = new SplashScreenManager();
await splash.ShowAsync(async progress =>
{
progress.Report(new SplashProgress("正在加载模块...", 20));
await InitializeModules();
progress.Report(new SplashProgress("正在恢复窗口状态...", 60));
var stateService = _serviceProvider.GetRequiredService<WindowStateService>();
progress.Report(new SplashProgress("启动完成", 100));
await Task.Delay(500);
});
// 显示主窗口
var mainWindow = new MainWindow
{
DataContext = _serviceProvider.GetRequiredService<MainViewModel>()
};
var windowState = _serviceProvider.GetRequiredService<WindowStateService>();
windowState.RegisterWindow(mainWindow, "MainWindow");
mainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
_serviceProvider?.Dispose();
base.OnExit(e);
}
}排错清单
复盘问题
- 你的窗口管理是否通过统一的服务层?还是散落在各处?
- 模态窗口是否可以改为非模态?用户体验是否更好?
- 窗口关闭时是否正确保存状态和释放资源?
- 多显示器用户是否测试过?断开显示器后是否正常?
- 弹窗服务是否支持单元测试?能否 Mock?
- 应用退出时是否有未关闭的窗口?
