Working with Multiple Monitors
Windows 98/2000 systems and above provide support for multiple monitors. This is a great thing except that it messes up old programs which attempt to do things like centre Windows or otherwise restrict their position to the visible area of the screen. This tip provides some simple code to allow you to work with multiple monitors.
About Multiple Monitors
Multiple monitor support in Windows is achieved by providing a rectangular Virtual Screen area onto which the physical monitors are mapped. The diagram below, taken directly from the MSDN documentation, demonstrates the kind of setup that could result if multi-monitor support were to be used by a madman or madwoman with plenty of old hardware and some bizarrely shapped monitors:
The useful things to note are that the monitors may or may not contain all of the actual virtual screen information and that a screen may have coordinates which are offset from the origin in either direction. Finally, the size of the virtual screen is currently not supposed to exceed +/-32767, since there are a number of old-school API calls and messages in Win32 which follow the sort of sharp thinking that led to the idea in DOS that no-one would ever have more than 64k memory in their PC. (Unfortunately, I feel this strategy is already pretty much kcufed since 200dpi screens are on their way. Although I can't afford any of them, never mind a 30" version...)
Using Multiple Monitors
The only times you really need to know about multiple monitors are:
Unfortunately, the second case is pretty much beyond the scope of VB unless you choose to do all painting by intercepting WM_PAINT messages. This would really be making life extraordinarily hard for yourself, even by vbAccelerator standards.
In any case, most multiple monitor systems are driven from a single graphics card and have the same colour depth in each monitor. When that doesn't occur, your users shouldn't really expect to be able to drag your lovely alpha-blended window onto their amber-screen EGA debugging display and expect everything will still look perfect.
So back to centring and sizing, which is relatively simple. You need to be able to determine the size of the virtual screen area and each of the monitors within that area.
Determing the Size of the Virtual Screen
In the same way that the usable screen area used to be provided through the SystemParametersInfo API call, the Virtual Screen size is also, as well as the number of physical monitors and whether each has the same colour depth. The code below demonstrates how to retrieve this information:
Private Const SM_CXVIRTUALSCREEN = 78 Private Const SM_CYVIRTUALSCREEN = 79 Private Const SM_CMONITORS = 80 Private Const SM_SAMEDISPLAYFORMAT = 81 Private Declare Function GetSystemMetrics Lib "user32" ( _ ByVal nIndex As Long) As Long Public Property Get VirtualScreenWidth() As Long VirtualScreenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN) End Property Public Property Get VirtualScreenHeight() As Long VirtualScreenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN) End Property Public Property Get DisplayMonitorCount() As Long DisplayMonitorCount = GetSystemMetrics(SM_CMONITORS) End Property Public Property Get AllMonitorsSame() As Long AllMonitorsSame = GetSystemMetrics(SM_SAMEDISPLAYFORMAT) End Property
Evaluating Monitor Size and Work Area
Each monitor has an overall size and a work area, which represents the size with any explorer bars (like the task bar, or the misguided Office docking help thingies) subtracted. The EnumDisplayMonitors API call is provided to allow this to be performed. This is a callback method which provides details of all attached monitors, along with a hMonitor handle which can be used to interrogate the monitor.
As with any callback function, unless you implement a Machine Code Thunk you must implement the function which gets called within a VB module. As my personal knowledge of machine code can be approximated as (or at least tends to) none, I've used module, which means the enumeration works as follows:
Private Declare Function EnumDisplayMonitors Lib "user32" ( _ ByVal hDC As Long, _ lprcClip As Any, _ ByVal lpfnEnum As Long, _ ByVal dwData As Long _ ) As Long Private Function MonitorEnumProc( _ ByVal hMonitor As Long, _ ByVal hDCMonitor As Long, _ ByVal lprcMonitor As Long, _ ByVal dwData As Long _ ) As Long ' Do something with the monitor here... MonitorEnumProc = 1 End Function Public Sub EnumMonitors(cM As cMonitors) EnumDisplayMonitors 0, ByVal 0&, AddressOf MonitorEnumProc, 0 End Sub
The parameters returned by the MonitorEnumProc are primarily aimed at responding to a WM_PAINT message. More usefully, the hMonitor handle can be converted into the details of the monitor in question: its name, its size and work area size and whether it is the primary monitor or not. To do this you use the GetMonitorInfo API function. This function is provided in both Unicode and ANSI versions to account for monitor names in either language. Both versions are translated in this code:
Private Type RECT Left As Long Top As Long right As Long bottom As Long End Type Private Const CCHDEVICENAME = 32 Private Type MONITORINFOEXA cbSize As Long rcMonitor As RECT rcWork As RECT dwFlags As Long b(0 To CCHDEVICENAME - 1) As Byte End Type Private Type MONITORINFOEXW cbSize As Long rcMonitor As RECT rcWork As RECT dwFlags As Long b(0 To CCHDEVICENAME * 2 - 1) As Byte End Type Private Declare Function GetMonitorInfoA Lib "user32" ( _ ByVal hMonitor As Long, _ lpmi As MONITORINFOEXA _ ) As Long Private Declare Function GetMonitorInfoW Lib "user32" ( _ ByVal hMonitor As Long, _ lpmi As MONITORINFOEXW _ ) As Long Private Const MONITORINFOF_PRIMARY = &H1 Private m_hMonitor As Long Private m_sName As String Private m_rcMonitor As RECT Private m_rcWork As RECT Private m_bIsPrimary As Boolean ... If (IsNt) Then Dim tMIW As MONITORINFOEXW tMIW.cbSize = Len(tMIW) GetMonitorInfoW hMonitor, tMIW With tMIW LSet m_rcMonitor = .rcMonitor LSet m_rcWork = .rcWork m_bIsPrimary = _ ((.dwFlags And MONITORINFOF_PRIMARY) = MONITORINFOF_PRIMARY) sName = .b iPos = InStr(sName, vbNullChar) End With Else Dim tMIA As MONITORINFOEXA tMIA.cbSize = Len(tMIA) GetMonitorInfoA hMonitor, tMIA With tMIA LSet m_rcMonitor = .rcMonitor LSet m_rcWork = .rcWork m_bIsPrimary = _ ((.dwFlags And MONITORINFOF_PRIMARY) = MONITORINFOF_PRIMARY) sName = StrConv(.b, vbUnicode) End With End If iPos = InStr(sName, vbNullChar) If (iPos > 0) Then m_sName = Left(sName, iPos - 1) Else m_sName = sName End If
Note that if you don't need the monitor name, then you can simplify this call by missing out the name parameter entirely. In this case you only ever need the ANSI version of the function and the declares look like this:
Private Type MONITORINFO cbSize As Long rcMonitor As RECT rcWork As RECT dwFlags As Long End Type Private Declare Function GetMonitorInfoA Lib "user32" ( _ ByVal hMonitor As Long, _ lpmi As MONITORINFO _ ) As Long
Wrapping It Up
With this code its easy to create wrapper for the function. The class cMonitors is responsible for providing information about the Virtual Screen area, and to store a collection of monitors which contain information about the individual monitors. As VB provides no easily accessible support to a strongly typed collection, the implemented collection is an index only version, even though access by the hMonitor handle would be sensible. The code is arranged so that cMonitors class is responsible for responding to callbacks from the mMonitors module containing the callback and the creates an array of cMonitor objects which can be used to access the position, name and primary information about the monitors.
The sample code demonstrates some of the types of functions you might want to achieve in a multiple monitor system, such as ensuring your form is on a specific monitor, centred on a specific monitor or maximised there.
Working with multiple monitor systems is generally trivial unless you have to save or restore a screen position. In this case you can easily get access to the details of the current monitor arrangement using the simple classes provided with this tip.