А у вас сколько мониторов?

Дата публикации 6 май 2005

А у вас сколько мониторов? — Архив WASM.RU

Скачать исходник

В одном проекте мне понадобилась поддержка более одного монитора. В доступном информационном пространстве я особенно ничего не нашёл. Или плохо искал. Хотя на WASM. RU форуме такие вопросы возникали. Поэтому вооружился SDK и пошёл шаманить сам. О результатах такого шаманства хочу рассказать. Полный проект MASM32: RadASM прилагается.

Начнём с того, что для целей определения количества и расположения мониторов есть интерфейс в GDI, но он появился лишь с Win98. Поэтому в старом SDK от Win95 информации об этом не найти. Основное исследование происходило на WinXP Pro SP1. Здесь я столкнулся с первыми подводными камнями. XP с пеной у рта доказывал мне, что у меня на машине 2 видеоадаптера и целых 8!!! мониторов (по 4 на каждом). 98-ая была скромнее и сообщала, что на 2 дисплейных устройствах есть по одному монитору. Потребовалось выработать единую тактику.

Но сначала немного теории. В начале был один видеоадаптер и один монитор и один рабочий стол и рабочий стол = монитор. Но с 98-ой жизнь усложнилась. Во-первых, теоретически в один компьютер можно было поставить более, чем один видеоадаптер. Правда, нормально (и прямо) можно работать лишь с тем, который основной и VGA-совместимый. У него может быть больше, чем один монитор. Обычно два. Но Windows может себе представить и три. Теперь мониторы есть определённые прямоугольники на виртуальном экране ( virtual screen). Есть основной монитор ( primary monitor) и остальные, как показано на этом изображении, позаимствованном из SDK.

 

Правый верхний угол основного монитора определяет точку на виртуальном экране, в которой координаты 0,0. Т.о. при работе с координатами монитора помните, что они могут быть отрицательными. Теперь вы можете себе наглядно представить, что творится с мониторами. А получив информацию о каждом мониторе, можно любое окно поместить на нужный монитор (в любую его часть). Если края мониторов касаются друг друга, то это связанные мониторы. Они становятся «продолжением» друг друга. Иначе монитор становится независимым. Расположение мониторов на виртуальном экране не фиксировано. С помощью имени соответствующего монитора и ChangeDisplaySettingsEx можно перемещать его в нужную точку. Для этого в структуре DEVMODE кроме требуемого разрешения, глубины цвета и частоты ещё указываем DEVMODE.dmPosition , которая устанавливает новые координаты левой верхней точки соответствующего монитора. А ещё есть dmDisplayOrientation. Это значение позволяет определить ориентацию монитора (поворот на 0, 90, 180 и 270 градусов). Это вся теория. Приступим к делу.

Для получения информации о доступных мониторах SDK рекомендует использовать EnumDisplayMonitors. Она, как и все другие функции, лежит в user32.dll:

BOOL  EnumDisplayMonitors(  
HDC
hdc, // handle to display DC
LPCRECT
lprcClip, // clipping rectangle
MONITORENUMPROC lpfnEnum, // callback function
LPARAM dwData // data for callback function
);

Это вообще весьма полезная функция. Стоит познакомиться с ней поближе. Но она может возвращать информацию не только о реальных мониторах в системе, но также и о виртуальных псевдо-мониторах. Например, NetMeeting может создавать такие. Количество реальных мониторов можно получить следующим образом:

SM_CMONITORS  equ 50h
invoke GetSystemMetrics, SM_CMONITORS

Для обнаружения всех мониторов в системе функцию EnumDisplayMonitors надо вызывать так:

invoke EnumDisplayMonitors,0,0,offset MonitorEnumProc,0 

При этом hdc монитора мы не знаем и не указываем – нам нужны все; lprcClip ставим 0, чтобы работать со всем виртуальным экраном; MonitorEnumProc – это ваша callback процедура, в которой вы обрабатываете полученную информацию об очередном мониторе. Вот её прототип:

BOOL CALLBACK MonitorEnumProc(
HMONITOR hMonitor, // handle to display monitor
HDC
hdcMonitor, // handle to monitor DC
LPRECT
lprcMonitor, // monitor intersection rectangle
LPARAM
dwData // data
);

Обратите внимание, что эта функция всегда должна возвращать TRUE, чтобы продолжался поиск следующих мониторов. Иначе верните FALSE, чтобы остановить обработку мониторов. В моём варианте из этой функции я получаю и сохраняю информацию о каждом найденном дисплейном устройстве с помощью GetMonitorInfo.

BOOL GetMonitorInfo(  
HMONITOR
hMonitor, // handle to display monitor
LPMONITORINFO
lpmi // display monitor information
);

Ей передаю ручку ( handle) от монитора ( hMonitor) и указатель на структуру MONITORINFOEXA. Она не объявлена в стандартном windows.inc от MASM32. Поэтому приходится объявлять конвертированный вариант из wingdi.h в своей программе. Не забудьте инициализировать .cbSize размером структуры перед вызовом. Ниже дана структура MONITORINFOEXA в варианте для MASM и полезные константы к ней.

MONITORINFOF_PRIMARY equ 00000001h
CCHDEVICENAME equ 32
;this structure was not in my windows.inc thus it had to be added here
MONITORINFOEXA struct
cbSize DWORD ?
rcMonitor RECT<?>
rcWork RECT<?>
dwFlags DWORD ?
szDevice BYTE CCHDEVICENAME dup (?)
MONITORINFOEXA ends

Какую информацию мы получаем здесь? .rcMonitor сообщает нам верхнюю левую и правую нижнюю точки текущего монитора в виде прямоугольника на виртуальном экране. .rcWork указывает рабочую зону монитора. Во флаге может быть только одно значение: MONITORINFOF_PRIMARY, которое показывает нам, является ли текущий монитор основным. Такой флаг может быть установлен лишь у одного монитора. . szDevice – это строка, в которую записывается внутреннее имя устройства. Оно обычно имеет такой вид: «\\.\ DISPLAY1». Кроме MONITORINFOEX есть ещё структура MONITORINFO. Она отличается от нашей структуры лишь отсутствием . szDevice. Нам лучше использовать расширенный вариант, потому что он даёт возможность для дальнейших исследований текущего монитора по его внутреннему имени. Пользователю строка «\\.\ DISPLAY1» многого не скажет. Хочется более нормальное название, как видеоадаптера, так и монитора. Здесь нам поможет EnumDisplayDevices. Вот её прототип :

BOOL EnumDisplayDevices( 
LPCTSTR
lpDevice , // device name
DWORD iDevNum , // display device
PDISPLAY_DEVICE
lpDisplayDevice, // device information
DWORD
dwFlags // reserved
);

Для работы с ней понадобится структура DISPLAY_DEVICE. Она есть в windows.inc, но я всё равно привожу её объявление.

DISPLAY_DEVICEA STRUCT
cb DWORD ?
DeviceName BYTE 32 dup (?)
DeviceString BYTE 128 dup (?)
StateFlags DWORD ?
DISPLAY_DEVICEA ENDS

Она бывает в ANSI и Unicode версии. Лучше использовать ANSI, так как юникод вариант поддерживается лишь с Win2000. Перед использованием этой структуры не забывайте инициализировать .cb размером структуры.

Сначала получаем название видеоадаптера следующим образом:

mov DispInfo.cb,sizeof DISPLAY_DEVICE
invoke EnumDisplayDevices,0,nthDevice,addr DispInfo,0

Здесь nthDevice – это глобальная переменная, которая инициализируется в 0 и инкрементируется каждый раз в конце MonitorEnumProc. В ней у нас хранится порядковый номер текущего устройства. При помощи приведённого вызова мы получаем заполненную структуру DISPLAY_DEVICE.

DeviceName в данном случае должен быть равен .szDevice из структуры MONITORINFOEX, а DeviceString как раз будет содержать название видеоадаптера, к которому подключен монитор. StateFlags сообщает дополнительную информацию о мониторе. Ниже приведён список известных мне флагов.

DISPLAY_DEVICE_ATTACHED_TO_DESKTOP 	equ 00000001h
DISPLAY_DEVICE_MULTI_DRIVER equ 00000002h
DISPLAY_DEVICE_PRIMARY_DEVICE equ 00000004h
DISPLAY_DEVICE_MIRRORING_DRIVER equ 00000008h
DISPLAY_DEVICE_VGA_COMPATIBLE equ 00000010h

DISPLAY_DEVICE_ATTACHED_TO_DESKTOP показывает, является ли текущий монитор частью рабочего стола.

DISPLAY_DEVICE_MULTI_DRIVER есть в заголовочных файлах ( wingdi.h), но описание на него я так и не сумел найти.

DISPLAY_DEVICE_PRIMARY_DEVICE установлен лишь у одного устройства в системе, к которому подключен основной монитор.

DISPLAY_DEVICE_MIRRORING_DRIVER установлен лишь у псевдо адаптера, который используется для отражения отрисовки приложения. К нему присоединён псевдо монитор. Такие системы используются, например, в NetMeeting.

DISPLAY_DEVICE_VGA_COMPATIBLE устройство является VGA совместимым.

В SDK указаны ещё 2 флага ( DISPLAY_DEVICE_MODESPRUNED и DISPLAY_DEVICE_REMOVABLE), но их объявления я не сумел найти.

Следующим шагом будем получать название монитора. Для этого в новой структуре DISPLAY_DEVICE инициализируем .cb, а первым аргументом функции EnumDisplayDevices передаём DeviceName предыдущей структуры, в которой мы получали название адаптера. Вторым аргументом передаём номер монитора, начиная с 0. Мониторов может быть много, поэтому будем в цикле перебирать все, пока не найдём тот, у которого StateFlags будет не равен 0. Так мы пропустим все псевдо мониторы. DeviceString такого монитора будет содержать нормальное название монитора. Однако, если монитор установлен в системе как стандартный монитор, а не со своим собственным драйвером, то название ему и будет «стандартный монитор».

Вот и все инструменты для обнаружения мониторов в системе и получения их параметров. Чтобы узнать взаимное расположение найденных мониторов на виртуальном экране, нужно проанализировать структуры .rcMonitor из MONITORINFOEX каждого монитора. Так же можно высчитать позицию верхнего левого угла монитора (его начало) в координатах виртуального экрана. А как изменять эту позицию я рассказывал в самом начале статьи в теоретической части.

Остаётся ещё вопрос о том, как определить доступные видеопараметры (разрешение, глубину цвета и частоту) для каждого монитора. А также те параметры, которые установлены сейчас и параметры, которые указаны в реестре (те, с которыми происходит загрузка). Это выполняется с помощью функции EnumDisplaySettings.

BOOL  EnumDisplaySettings(
LPCTSTR lpszDeviceName, // display device
DWORD
iModeNum, // graphics mode
LPDEVMODE
lpDevMode // graphics mode settings
);

Как вы уже догадались, первым аргументом ей передаём имя адаптера (например \\.\ DISPLAY1). Вторым аргументом передаём номер видеорежима. Третий аргумент есть указатель на структуру DEVMODE. В ней возвращается информация о видеорежиме. Эта структура была уже в Win95, поэтому её объявление приводить не буду. Перед её использованием не забудьте инициализировать .dmSize размером этой структуры, а .dmDriverExtra нулём, потому, что нам эта информация не нужна. Это важно, потому что может получиться следующая ситуация. Вы выделили в стеке место под эту структуру. Нуль не поставили. Функция считает, что ниже есть место для дополнительной информации и пишет туда, затирая другие переменные, а то и адрес возврата из функции. Нехорошо.

Чтобы получить все доступные видеорежимы, начинаем с нуля, в цикле получаем видеорежим и инкрементируем номер режима. Так выполняем пока функция не вернёт FALSE, указывая, что больше видеорежимов нет.

ENUM_CURRENT_SETTINGS  equ -1
ENUM_REGISTRY_SETTINGS equ -2
mov nthMode ,0
mov dm . dmSize , sizeof DEVMODE
mov dm. dmDriverExtra,0
@@:
invoke EnumDisplaySettings,lpDeviceName,nthMode,addr dm
.if eax!=FALSE
...
inc nthMode
jmp @B
.endif

Чтобы получить текущий видеорежим для конкретного монитора, вторым аргументом передайте ENUM_CURRENT_SETTINGS. А если текущий видеорежим для монитора был динамически изменён другой программой. Как узнать исходный? Для этого существует ENUM_REGISTRY_SETTINGS.

Вот и всё. Я рассказал, что узнал сам.

К статье прилагается исходник с программой, которая получает информацию о мониторах, пользуясь описанной здесь техникой.

Предложения, возражения, опровержения, выражения, искажения и другие жжения, а также комментарии, дополнения и благодарности принимаются к этой статье на сайте WASM. RU – самом лучшем портале для исследователей Windows и адептов древнего искусства писать приложения полностью на ассемблере.

Успешного творчества,

SolidCode.

© SolidCode

1 1.701
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532

Комментарии


      1. pu008 6 мар 2023
        К сожалению, ссылка на исходник в 2 местах страницы не рабочая. Я пытаюсь понять, почему координата окна, развёрнутого на втором мониторе (maximized) выступает за пределы прямоугольников, полученных посредством GetMonitorInfo. Окно выступает на 4 пикселя влево и вверх. Рамочка при maximized состоянии окна меняется, как будто система скрывает несколько пикселов рамки в полноэкранном режиме. В windows 11 координаты доходят до -8 и причём если одно окно полноэкранное, а другое нет, с координатой left = -1, то оставшаяся полоса слева от второго на первом вполне себе видна на экране. Удивительно и творчески разрабы из мс подошли к вопросу прямоугольников мониторов на виртуальном экране и координатам развёрных окон на этих мониторах, других слов и объяснений я не нахожу