前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
原文链接:https://learn.microsoft.com/en-us/windows/win32/gdi/multiple-display-monitors
11 多个显示监视器
- 多显示监视器是一组相关的功能,允许应用程序同时使用多个显示设备。有两种方式可以使用多个显示器:作为一个大桌面或作为多个独立的显示器。
11.1 关于多个显示监视器
-
当多个监视器是桌面的一部分时,对象可以在监视器之间无缝传输。用户可以使用监视器选项卡安排系统中的监视器的位置布局以反映物理显示单元的排布。
-
多个监视器系统会影响某些组合键:
- CTRL + PRINTSCRN 会对整个虚拟屏幕进行快照,具体请参见虚拟屏幕。
- ALT + PRINTSCRN 获取前景窗口的快照。
- PRINTSCRN 对整个虚拟屏幕进行快照;除非在 Windows 设置中打开了“使用打印屏幕打开屏幕截图”,在这种情况下, PRINTSCRN 会启动安装的快照工具类型的应用程序。
-
支持多显示器不会影响应用程序在单一显示环境中运行时的性能。也就是说,当在单个显示系统上运行时,高性能图形操作代码中不会出现额外的开销。但是,在多监视器系统中,如果应用程序仅在其中一个图形设备上运行,则性能会受到轻微影响。此外,如果应用程序跨越多个显示器,特别是图形密集型操作,性能可能会受到很大影响。
-
全屏是操作系统提供的一个功能,它允许用户将应用程序切换到一个特殊的状态,在这个状态下应用程序可以直接访问 VGA 图形硬件。对于需要高性能的游戏和其他以图像为重点的应用程序来说,这是一个关键特性。此外,它经常被开发人员用于文本编辑,因为它支持非常快速的文本滚动。
-
在多显示器环境中,只有一个图形设备可以与 VGA 兼容。这是计算机硬件的一个限制,它要求只有一个设备响应任何硬件地址。因为 VGA 硬件兼容性标准需要特定的硬件地址,所以一台机器中只能有一个 VGA 图形设备,并且只有这个设备可以物理地响应 VGA 地址。因此,需要全屏的应用程序只能在支持 VGA 硬件兼容性的特定设备上运行。
11.1.1 虚拟屏幕
-
所有显示器的边界矩形是虚拟屏幕。桌面覆盖虚拟屏幕,而不是单个显示器。
-
主监视器包含原点(0,0)。这是为了与已存在的期望有原点的监视器的应用程序兼容。但是,主显示器不必位于虚拟屏幕的左上角。当主显示器不在虚拟屏幕的左上方时,虚拟屏幕的部分坐标为负。因为显示器的排列是由用户设置的,所以所有应用程序都应该设计成使用负坐标。有关详细信息,请参见旧程序的多监视器注意事项。
11.1.2 HMONITOR 和设备上下文
-
每个物理显示器由 HMONITOR 类型的监视器句柄表示。一个有效的 HMONITOR 被保证为非 NULL 。只要物理显示器是桌面的一部分,它就具有相同的 HMONITOR 。当发送 WM_DISPLAYCHANGE 消息时,任何监视器都可能从桌面上移除,因此它的 HMONITOR 变得无效或其设置被更改。因此,当发送此消息时,应用程序应该检查是否所有 HMONITOR 都是有效的。
-
任何返回显示设备上下文( DC )的函数通常返回主监视器的 DC 。如果需要获取其他显示器的 DC ,请使用 EnumDisplayMonitors 函数。或者,可以使用 GetMonitorInfo 函数中的设备名称来调用 CreateDC 来创建 DC 。但是,如果函数(例如 GetWindowDC 或 BeginPaint )为跨越多个显示的窗口获取 DC ,则该 DC 也将跨越两个显示。
11.1.3 枚举和显示控件
-
要枚举计算机上的所有设备,请调用 EnumDisplayDevices 函数。返回的信息还表明哪个监视器是桌面的一部分。
-
要枚举桌面上与裁剪区域相交的设备,请调用 EnumDisplayMonitors 。这将返回每个监视器的 HMONITOR 句柄,该句柄与 GetMonitorInfo 一起使用。要枚举虚拟屏幕中的所有设备,请使用 EnumDisplayMonitors 。
-
若要获取有关显示设备的信息,请使用 EnumDisplaySettings 或 EnumDisplaySettingsEx 。
-
ChangeDisplaySettingsEx 函数用于控制计算机上的显示设备。它可以修改设备的配置,例如指定虚拟桌面中监视器的位置和更改任何显示器的位深度。通常,应用程序不使用此函数。要以编程方式向多监视器系统添加显示监视器,请将 DEVMODE.dmFields 设置为 DM_POSITION 并为要添加的显示器指定一个位置(使用 DEVMODE.dmPosition ),该位置与现有显示器的显示区域的至少一个像素相邻。若要卸载监视器,请将 DEVMODE.dmFields 设置为 DM_POSITION ,并设置 DEVMODE.dmPelsWidth 和 DEVMODE.dmPelsHeight 为 0 。
-
对于每个显示设备,应用程序可以在注册表中保存描述设备配置参数以及位置参数的信息。应用程序还可以通过 DISPLAY_DEVICE 结构中的 DISPLAY_DEVICE_ATTACHED_TO_DESKTOP 标志确定哪些显示是桌面的一部分,哪些不是。一旦所有配置信息都存储在注册表中,应用程序就可以再次调用 ChangeDisplaySettingsEx 来动态更改设置,而不需要重新启动。
11.1.4 多监视器系统指标
-
GetSystemMetrics 函数返回主监视器的值,除了 SM_CXMAXTRACK 和SM_CYMAXTRACK,它们指的是整个桌面。以下指标对于所有设备驱动程序都是相同的: SM_CXCURSOR 、 SM_CYCURSOR 、 SM_CXICON 、 SMCYICON 。以下显示功能对于所有监视器都是相同的: LOGPIXELSX , LOGPIXELSY , DESTOPHORZRES , DESKTOPVERTRES 。
-
GetSystemMetrics 还有一些常量,它们只指代多监视器系统。 SM_XVIRTUALSCREEN 和 SM_YVIRTUALSCREEN 表示虚拟屏幕的左上角, SM_CXVIRTUALSCREEN 和 SM_CYVIRTUALSCREEN 表示虚拟屏幕的纵横尺寸, SM_CMONITORS 表示连接到桌面的显示器数量, SM_SAMEDISPLAYFORMAT 表示桌面上所有显示器的颜色格式是否一致。
-
要获取有关桌面中单个显示器或所有显示器的信息,请使用 EnumDisplayMonitors 。 GettWindowRect 或 GetClientRect 返回的桌面窗口矩形始终等于主监视器的矩形,以便与现有应用程序兼容。
-
要更改监视器的工作区域,调用 SystemParametersInfo ,其中 SPI_SETWORKAREA 和 pvParam 指向所需监视器上的 RECT 结构。如果 pvParam 为 NULL ,则修改主监视器的工作区域。使用 SPI_GETWORKAREA 总是返回主监视器的工作区域。要获取主监视器以外的监视器的工作区域,请调用 GetMonitorInfo 。
11.1.5 将多个监视器用作独立显示器
-
当使用多个显示器作为独立显示器时,桌面包含一个或一组显示器。这组显示器始终包括主监视器,其行为与本主题的其他部分中提到的一致。应用程序可以使用任何其他监视器作为独立的显示器。
-
窗口管理器对独立显示一无所知。它们完全由应用程序控制,并且没有窗口管理器功能可供应用程序使用(所有窗口管理器调用都会自动转到主显示)。每个独立的显示都有自己的原点和水平和垂直坐标,并通过 GDI 函数(如 CreateDC )或 DirectX 函数(如 DirectDrawCreate )访问。
-
要定位独立的显示器,调用 EnumDisplayDevices 并在 DISPLAY_DEVICE 结构中查找没有 DISPLAY_DEVICE_ATTACHED_TO_DESKTOP 标志的显示。
11.1.6 多个显示器上的颜色
-
每个显示器都可以有自己的颜色深度。当窗口在不同颜色深度的显示器上移动时,系统会自动调整颜色。一般来说,这会产生良好的结果。然而,这并不总是最佳的。要利用不同显示器的色彩功能,请参阅下面的“在多显示器上绘制”一节。
-
要确定所有显示器是否具有相同的颜色格式,请使用 SM_SAMEDISPLAYFORMAT 调用 GetSystemMetrics 。
-
如果主监视器是托盘化的,则 SelectPalette 和 RealizePalette 的工作方式与以前相同,但是跨所有监视器。此外,所有调色板设备的调色板是同步的。如果主监视器没有托盘化, SelectPalette 和 RealizePalette 将把调色板选择到后台,并且托盘化的设备将不会同步。
11.1.6.1 在多个显示监视器上绘制
-
系统自动处理绘制到跨越多个监视器的设备上下文( DC ),即使监视器具有不同的颜色深度。通常这会产生良好的结果,但它可能不是最优的。例如,两个颜色深度相差很大的显示器上的一个窗口可能具有较差的色彩再现性。同样,具有相同颜色深度的显示器可能具有不同的颜色格式,例如,颜色可以用不同的位数编码,或者位于像素颜色值的不同位置。
-
要为跨越多个显示器的 DC 中的每个显示器获得最佳结果,请调用 EnumDisplayMonitors 枚举与 DC 相交的显示器,并根据该显示器的显示属性分别在每个显示器中绘制相交区域。请参阅在跨多个显示器的DC上绘画的示例。
-
如果你在 WM_PAINT 代码中做所有的绘制操作,如果你的 WM_PAINT 代码处理所有的各种视频模式,那么你应该能够将你的 WM_PAINT 代码放在 EnumDisplayMonitors 的 MonitorEnumProc 中,只需要做一些修改。
11.1.7 定位多个显示监视器上的对象
-
在多个显示器上显示的窗口或菜单会对查看者造成视觉干扰。为了最小化这个问题,系统在一个显示器上显示菜单和新的最大化的窗口。下表显示了如何选择监视器。
对象 位置 窗口 CreateWindow 在监视器上显示一个包含该窗口最大部分的窗口。在包含最小化窗口之前的最大部分的监视器上最大化。
ALT-TAB 组合键在监视器上显示一个具有当前激活窗口的窗口。被拥有的窗口 与其所有者位于同一监视器上。 子菜单 显示在包含相应菜单项最大部分的监视器上。 上下文菜单 显示在发生右键单击的监视器上。 下拉列表 显示在包含组合框矩形的监视器上。 对话框 显示在拥有它的窗口的监视器上。如果它是用 DS_CENTERMOUSE 样式定义的,它就会随着鼠标出现在监视器上。
如果它没有所有者,并且激活窗口和对话框位于同一应用程序中,则对话框将出现在当前激活窗口的监视器上。
如果对话框没有所有者,并且激活窗口与对话框不在同一个应用程序中,则对话框将出现在主监视器上。消息框 显示在拥有它的窗口的监视器上。 -
如果一个窗口横跨两个显示器,其中一个显示器被重新定位,系统将窗口定位到包含原始窗口最大部分的显示器上。
-
应用程序通常还需要定位对象。例如,可能需要在与另一个窗口相同的监视器上创建一个窗口。为了在多监视器系统上定位一个对象:
- 确定合适的监视器。
- 把坐标发给监视器。
- 使用坐标定位对象。
-
要识别给定点、矩形或窗口所在的监视器,可以使用 MonitorFromPoint 、 MonitorFromRect 和 MonitorFromWindow 。
-
要获取监视器的坐标,请使用 GetMonitorInfo ,它提供了工作区域和整个监视器矩形。注意, SM_CXSCREEN 和 SM_CYSCREEN 总是指向主监视器,而不一定是显示应用程序的监视器。另外,避免使用 SM_xxVIRTUALSCREEN ,因为它将窗口集中在虚拟屏幕上,而不是显示器上。
-
要在窗口的工作区域中居中对话框,请使用 DS_CENTER 样式。要将对话框居中到应用程序窗口,请使用 GetWindowRect 。 Windows 自动将菜单和对话框限制在一个监视器上。但是,自定义菜单、自定义下拉框、自定义工具面板和保存的应用程序位置可能存在问题。
-
使用 SM_CXSCREEN 和 SM_CYSCREEN 来确定应用程序桌面工具栏(也称为 appbar )的位置将 appbar 限制在主监视器上。要允许 appbar 位于任何监视器的任何边缘,请使用适当的系统度量来计算监视器的边缘。另外,使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏提取坐标,否则坐标的符号可能是错误的。这些宏包含在 windows.h 中。
-
当全屏窗口在不同分辨率的显示器之间移动时,它的大小需要改变。要做到这一点,应用程序必须检查它所在的窗口,使用 MonitorFromWindow 或 MonitorFromPoint ,然后使用 GetMonitorInfo 来获取监视器的大小。作为一种替代方法,可以使用 DirectX 的 DirectDrawEnumerateEx 函数中的 HMONITOR 。然后使用 SetWindowPos 来定位和调整窗口的大小以覆盖显示器。
-
最大化窗口不覆盖具有“始终在顶部”属性的任务栏。然而,全屏窗口覆盖了任务栏。
-
要保存并稍后恢复应用程序退出时窗口的位置,请使用 GetWindowPlacement 和 SetWindowPlacement 函数。但是,在使用它之前,请检查该位置是否仍然有效,因为监视器可能已经从系统中移动或移除。如果窗口的 HMONITOR 无效,则应用程序在主监视器上显示该窗口。
-
系统尝试在包含快捷方式的监视器上启动应用程序。因此,定位应用程序的一种方法是在所需的监视器上设置其快捷方式。
-
如果使用 ShellExecute 或 ShellExecuteEx ,请提供一个 hWnd ,以便系统将在与调用应用程序相同的监视器上打开任何新窗口。
-
请注意,对于具有多个监视器的系统, MINMAXINFO 结构的值略有变化。
11.1.8 不同系统上的多个监视应用程序
-
要使多监视器感知应用程序在支持和不支持多监视器的系统上都能工作,请将应用程序与 Multimon.h 链接起来。还必须在一个 C 文件中定义 COMPILE_MULTIMON_STUBS 。如果系统不支持多个监视器,则返回 GetSystemMetrics 的默认值,并且多个监视器函数就像只有一个显示器一样。在多个监视器系统上,应用程序将正常工作。
-
因为在多监视器系统中很容易出现负坐标,所以应该通过使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏来查询打包在 lParam 中的坐标。
-
不要使用负坐标或大于 SM_CXSCREEN 和 SM_CYSCREEN 的坐标来隐藏窗口。使用这些限制隐藏的窗口可能会出现在另一个显示器上。同样,不要使用这些限制来保持窗口可见,因为这可能会导致窗口与主监视器绑定。最好重新检查现有的应用程序以解决这些问题。但是,可以通过在主监视器上运行应用程序或将主监视器保持在虚拟屏幕的左上角来最小化现有应用程序中的问题。
-
注意, SM_CXMAXTRACK 和 SM_CYMAXTRACK 是为桌面定义的,而不仅仅是为一个监视器定义的。使用这些限制的窗口可能需要重新定义。
-
父窗口或相关窗口可能与子窗口不在同一监视器上。要定位窗口的监视器,应用程序应该使用 MonitorFromWindow 函数。
-
要在所有显示器上显示屏幕保护程序,请链接到最新版本的 Scrsave.lib 。否则,屏幕保护程序可能只出现在主显示器上,而不影响其他显示器。与最新的屏幕保存链接的屏幕保护程序 Scrsave.lib 可以在单个和多个监视器系统上工作。要在每个监视器上使用不同的屏幕保护程序,请使用多个监视器功能分别处理每个监视器。
-
以绝对坐标向系统传递坐标的输入设备,如平板电脑,其光标输入仅限于主显示器。要在显示器之间切换平板电脑输入,请参阅 OEM 的说明。
-
要将以绝对坐标发送的鼠标输入映射到整个虚拟屏幕,请使用带有 MOUSEEVENTF_ABSOLUTE 和 MOUSEEVENTF_VIRTUALDESKTOP 的 INPUT 结构。
-
BitBlt 功能适用于多监视器系统。但是,如果源设备和目标设备上下文不同, MaskBlt 、 PlgBlt 、 StretchBlt 和 TransparentBlt 函数将失败。
11.1.9 旧程序的多监视器注意事项
-
通常,旧的应用程序不需要更改就可以在多个监视器系统上工作。对大多数人来说,系统似乎只有一个显示器。系统指标反映主显示器和全屏应用程序显示在主显示器上。系统处理最小化、最大化、菜单和对话框。
然而,一些程序会有问题。一些可能有问题的是远程控制应用程序,那些有直接硬件访问的应用程序,以及那些修补了 GDI 或 DISPLAY 驱动程序的应用程序。在这些情况下,可能需要用户禁用除主监视器之外的任何监视器。
11.2 使用多个显示监视器
11.2.1 在跨多个显示器的 DC 上绘制
- 有关详细信息,请参阅在跨多个显示器的 DC 上绘制。
11.2.2 在多个显示设置上定位对象
- 有关详细信息,请参阅在多个显示设置上定位对象。
11.3 多显示监视器参考
11.3.1 多显示监视器函数
- 有关详细信息,请参阅多显示监视器函数。
11.3.2 多显示监视器结构体
- 有关详细信息,请参阅多显示监视器结构体。