WPF应用中如何在复制(镜像)与扩展显示模式间平滑切换
WPF应用中如何在复制(镜像)与扩展显示模式间平滑切换
嘿,我完全懂你的需求——主应用在第一屏全屏运行,第二屏固定显示等待图片,点击特定按钮切换到复制(镜像)模式,完成操作后再切回扩展模式继续显示等待图。因为WPF本身没有直接控制系统显示模式的API,咱们得调用Windows原生的User32.dll来实现,下面给你一步步拆解方案:
第一步:导入Windows原生API
首先得在项目里导入User32.dll的相关函数和结构体,显示模式是系统级设置,WPF管不到这一层,得靠系统API来操作:
using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Forms; using System.Linq; public static class DisplayModeManager { // 导入必要的系统API函数 [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern long ChangeDisplaySettingsEx(string lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, uint dwflags, IntPtr lParam); // 定义枚举显示器的回调委托 private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); // 显示器信息结构体 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct MONITORINFOEX { public int cbSize; public RECT rcMonitor; public RECT rcWork; public uint dwFlags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szDeviceName; } // 矩形区域结构体 [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left; public int top; public int right; public int bottom; } // 显示模式配置结构体 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct DEVMODE { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName; public short dmSpecVersion; public short dmDriverVersion; public short dmSize; public short dmDriverExtra; public uint dmFields; public int dmPositionX; public int dmPositionY; public uint dmDisplayOrientation; public uint dmDisplayFixedOutput; public short dmColor; public short dmDuplex; public short dmYResolution; public short dmTTOption; public short dmCollate; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName; public short dmLogPixels; public uint dmBitsPerPel; public uint dmPelsWidth; public uint dmPelsHeight; public uint dmDisplayFlags; public uint dmDisplayFrequency; public uint dmICMMethod; public uint dmICMIntent; public uint dmMediaType; public uint dmDitherType; public uint dmReserved1; public uint dmReserved2; public uint dmPanningWidth; public uint dmPanningHeight; } // 常量定义 private const int DM_POSITION = 0x00000020; private const int CDS_UPDATEREGISTRY = 0x00000001; private const int CDS_NORESET = 0x00000010; private const int DISP_CHANGE_SUCCESSFUL = 0; }
第二步:实现切换到复制模式的方法
复制模式的核心是让第二显示器和主显示器位置重合,实现镜像显示。我们先找到主显示器,再把其他显示器的位置设为主显示器的原点:
public static bool SwitchToDuplicateMode() { IntPtr primaryMonitor = IntPtr.Zero; MONITORINFOEX primaryInfo = new MONITORINFOEX(); primaryInfo.cbSize = Marshal.SizeOf(primaryInfo); // 枚举所有显示器,定位主显示器 EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, ref RECT lprcMonitor, IntPtr dwData) => { MONITORINFOEX info = new MONITORINFOEX(); info.cbSize = Marshal.SizeOf(info); if (GetMonitorInfo(hMonitor, ref info)) { if ((info.dwFlags & 1) == 1) // 主显示器的标志位 { primaryMonitor = hMonitor; primaryInfo = info; } } return true; }, IntPtr.Zero); if (primaryMonitor == IntPtr.Zero) return false; bool success = true; // 遍历非主显示器,设置为镜像位置 EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, ref RECT lprcMonitor, IntPtr dwData) => { if (hMonitor != primaryMonitor) { MONITORINFOEX info = new MONITORINFOEX(); info.cbSize = Marshal.SizeOf(info); if (GetMonitorInfo(hMonitor, ref info)) { DEVMODE devMode = new DEVMODE(); devMode.dmSize = (short)Marshal.SizeOf(devMode); devMode.dmDeviceName = info.szDeviceName; devMode.dmFields = DM_POSITION; devMode.dmPositionX = 0; // 和主显示器同原点,实现镜像 devMode.dmPositionY = 0; long result = ChangeDisplaySettingsEx(info.szDeviceName, ref devMode, IntPtr.Zero, CDS_UPDATEREGISTRY | CDS_NORESET, IntPtr.Zero); if (result != DISP_CHANGE_SUCCESSFUL) { success = false; } } } return success; }, IntPtr.Zero); // 应用所有设置 if (success) { ChangeDisplaySettingsEx(null, ref new DEVMODE(), IntPtr.Zero, 0, IntPtr.Zero); } return success; }
第三步:实现切换回扩展模式的方法
要切回扩展模式,得先在应用启动时保存各显示器的原始位置,这样才能准确恢复:
// 保存扩展模式下的显示器配置 private static List<(string DeviceName, int X, int Y)> _extendedModeConfig = new List<(string, int, int)>(); // 初始化时保存扩展模式的原始配置 public static void SaveExtendedModeConfig() { _extendedModeConfig.Clear(); EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, ref RECT lprcMonitor, IntPtr dwData) => { MONITORINFOEX info = new MONITORINFOEX(); info.cbSize = Marshal.SizeOf(info); if (GetMonitorInfo(hMonitor, ref info)) { _extendedModeConfig.Add((info.szDeviceName, info.rcMonitor.left, info.rcMonitor.top)); } return true; }, IntPtr.Zero); } // 切换回扩展模式 public static bool SwitchToExtendedMode() { if (_extendedModeConfig.Count == 0) return false; bool success = true; foreach (var config in _extendedModeConfig) { DEVMODE devMode = new DEVMODE(); devMode.dmSize = (short)Marshal.SizeOf(devMode); devMode.dmDeviceName = config.DeviceName; devMode.dmFields = DM_POSITION; devMode.dmPositionX = config.X; devMode.dmPositionY = config.Y; long result = ChangeDisplaySettingsEx(config.DeviceName, ref devMode, IntPtr.Zero, CDS_UPDATEREGISTRY | CDS_NORESET, IntPtr.Zero); if (result != DISP_CHANGE_SUCCESSFUL) { success = false; break; } } if (success) { ChangeDisplaySettingsEx(null, ref new DEVMODE(), IntPtr.Zero, 0, IntPtr.Zero); } return success; }
第四步:结合你的WPF窗口逻辑使用
在主窗口里完成初始化、按钮事件绑定,以及等待窗口的显示逻辑:
public MainWindow() { InitializeComponent(); // 启动时保存扩展模式的原始配置 DisplayModeManager.SaveExtendedModeConfig(); // 初始化第二屏的等待窗口 InitializeWaitingWindow(); } private Window _waitingWindow; private void InitializeWaitingWindow() { _waitingWindow = new Window { WindowStyle = WindowStyle.None, WindowState = WindowState.Maximized, // 替换成你的等待图片路径 Background = new ImageBrush(new BitmapImage(new Uri("Images/waiting-screen.jpg", UriKind.Relative))) }; // 将窗口放到第二屏 var secondScreen = Screen.AllScreens.FirstOrDefault(s => !s.Primary); if (secondScreen != null) { _waitingWindow.Left = secondScreen.Bounds.Left; _waitingWindow.Top = secondScreen.Bounds.Top; _waitingWindow.Show(); } } // 切换到复制模式的按钮点击事件 private void SwitchToDuplicateBtn_Click(object sender, RoutedEventArgs e) { // 隐藏等待窗口,复制模式下两屏显示内容一致 _waitingWindow.Hide(); bool success = DisplayModeManager.SwitchToDuplicateMode(); if (!success) { MessageBox.Show("切换到复制模式失败!"); // 失败后重新显示等待窗口 _waitingWindow.Show(); } } // 完成操作后切回扩展模式的方法 private void SwitchBackToExtended() { bool success = DisplayModeManager.SwitchToExtendedMode(); if (success) { // 重新将等待窗口放到第二屏并显示 var secondScreen = Screen.AllScreens.FirstOrDefault(s => !s.Primary); if (secondScreen != null) { _waitingWindow.Left = secondScreen.Bounds.Left; _waitingWindow.Top = secondScreen.Bounds.Top; _waitingWindow.Show(); } } else { MessageBox.Show("切换回扩展模式失败!"); } }
注意事项
- 记得在项目里添加
System.Windows.Forms程序集引用,不然Screen类会报错; - 切换显示模式可能会因为系统权限或硬件限制失败,建议加上异常处理;
- 测试时确保有两个显示器连接,否则第二屏的等待窗口不会显示。
备注:内容来源于stack exchange,提问作者Minh Huynh




