Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > June 1997
June 1997


How to Exploit Multiple Monitor Support in Memphis and Windows NT 5.0

David Campbell

This article assumes you're familiar with Win32

Code for this article: MultiMonitors.exe (11KB)
David Campbell is a support engineer in Microsoft Developer Support who supports user interface issues for Windows, specializing in shell extensions.

Have you noticed how precious screen real estate has become? The monitor never seems big enough and the resolution never seems high enough. I want to see Outlook™, Microsoft® Internet Explorer, Microsoft Developer Studio, and the app that I'm debugging all at the same time—I just don't have enough room!
If you're too low on the company totem pole to requisition a 35-inch video monitor, there is an alternative: multiple monitors. Memphis (the codename for the next version in the Windows® 95 family) and Windows NT® 5.0 both contain a set of features that will allow you to use multiple display devices at the same time; that is, multiple video cards and monitors on the same machine, all part of the same virtual desktop, all with seamless support built right into the operating system.
Previous versions of Windows 95 and Windows NT had no built-in support for multiple monitors. Only a few custom solutions existed, many of which imposed serious restrictions on a system such as the hardware required or supported, the shape of the desktop, the color depth and resolution, and, most significantly, compatibility with existing applications.
In this article I'll review the theory and operation of multiple monitor support, look at the new APIs and some of the programming issues you should consider, and go over how to actually install multiple monitors, including how the user interface changes in response to multiple monitors. My information is based on beta versions of the new operating systems. As with all articles that discuss beta software, keep in mind that the information here is preliminary, and could change suddenly and drastically. Don't ship a product based on this information until you have built and tested it on release versions of the software.

Theory and Operation of Multiple Monitors

Although your needs obviously will dictate how you set up your system, I'll discuss three options here for using multiple monitors.

Large desktops The Windows desktop can now cover more than one monitor with no restrictions on size, position, resolution, or refresh rates (see Figure 1). The system can be configured to the size and relative position of each monitor. Applications can be moved seamlessly from one monitor to another, or be displayed simultaneously on more than one monitor.
Figure 1 and 2

Screen duplication/remote display Alternatively, you can use secondary monitors to display the same data as the primary monitor (see Figure 2). This would be useful for training or for presentations to a group.Screen duplication could also be used to control remote applications such as in a support situation or for telecommuting.
Multiple independent displays A monitor does not need to be part of the Windows desktop for applications to have access to it. Applications can make use of an additional display even if it isn't part of the desktop. For example, if you have a large, high-resolution display for a CAD application, your application can use that monitor for output through Windows APIs, without requiring it to be part of the virtual desktop. That means you don't have to worry about accidentally dragging windows onto that screen. It's like having a display monitor you can draw on via GDI, but it isn't part of the Windows desktop so you don't have a taskbar or any other shell goodies to worry about.

Virtual Desktop

On single-monitor systems, the actual desktop is the same size and shape as the only monitor on the system. On a multimonitor system, each monitor is actually a view onto the underlying virtual desktop. The area that each monitor presents can be adjusted in the control panel. The primary monitor will always have compatible coordinates corresponding to 0,0 for the upper-left corner and the x and y resolution for the lower-right corner (see Figure 3). The actual coordinates that the secondary monitors view will depend on the layout of the monitors, which is also decided in the control panel and is usually modeled on the actual physical layout of the monitors on the user's desk.
Figure 3: Virtual Desktop
Figure 3: Virtual Desktop

You can use the control panel to change the resolution of any of the monitors, but you can only change the coordinates of the secondary monitors. The primary monitor's top-left coordinates must remain 0,0 for compatibility. In addition, all the monitors must touch each other on the virtual desktop. This restriction allows the system to maintain the illusion of a single, large desktop that you can maneuver on freely, seamlessly crossing from one monitor to another. At no point do you lose track of the mouse while travelling between monitors.
Since the desktop area that each monitor actually views must be adjacent to another monitor, the virtual desktop can be calculated as the bounding rectangle of all of the rectangular areas that can be seen on all of the existing monitors. Given that the coordinate system must be continuous, the coordinates for the secondary monitor simply continue from the primary. For example, if a secondary monitor is adjacent to the right edge of the primary monitor, its coordinates will start at the primary monitor's x resolution + 1 and continue to primary x resolution + secondary x resolution. If the primary and secondary monitors each have a resolution of 1024 X 768, then a secondary monitor attached to the right of the primary monitor will have coordinates from 1024,0 to 2047,767.
Also, some of the virtual desktop area may actually be offscreen in the sense that there is no monitor that views that area. This may occur if the monitors are not completely lined up or if there are monitors with different resolutions. For example, say I have a 1024X768 primary monitor and an 800 X 600 secondary monitor. The primary monitor has coordinates 0,0 to 1023,767, and the secondary monitor, which is attached to the left of my primary, has coordinates –800,168 to -1,767. This results in an area with coordinates from –800,0 to -1,167 that is not displayed on any monitor. For the most part, you don't have to worry about this area since Windows will not let the user move the mouse there, but keep in mind that the area is included in the calculation of the virtual desktop. Therefore, for my system the virtual desktop has coordinates from –800,0 to 1023,767.

What's New

OK, you've installed multiple monitors and now you want to take it further. Maybe you want to develop a custom app that is multimonitor-aware, or maybe you want to make use of a custom display device. Maybe you just want to make sure your existing application isn't doing anything that looks odd on a multimonitor system.
Several new APIs have been added for determining which monitor something is displayed on and for getting the settings for each monitor that a window may be visible on. Figure 4 is a summary of some of the key APIs.
You can now develop an application that is multimonitor-aware yet still runs on existing Windows 95 and Windows NT 4.0 machines. There is a new include file (see Figure 5) that uses GetProcAddress to link these APIs to the corresponding operating system APIs, if they exist. If not, the include file provides default implementations so the same EXE will run on Windows 95, Windows NT 4.0, Memphis, and Windows NT 5.0. On a Windows 95 or Windows NT 4.0 machine, your code will get stubbed to the versions of the APIs in the header file (which return correct values for those systems to your code). However, on an operating system that is multimonitor-aware, the code will pass through to the actual system APIs.
Now let's take a detailed look at the APIs for multimonitor support. Each physical display device is represented to the application by a monitor handle called an HMONITOR. A physical device has the same HMONITOR value throughout its lifetime, even across changes to display settings, as long as it remains a part of the desktop. A valid HMONITOR is guaranteed to be non-NULL. When a WM_DISPLAYCHANGE message is broadcast, any HMONITOR may have its settings changed in some way, or it may be removed from the desktop and become invalid.
The MonitorFromPoint API returns the monitor that contains pt.
HMONITOR MonitorFromPoint(POINT pt, DWORD dwFlags);
If no monitor contains pt, the return value depends upon the dwFlags field, which can be MONITOR_DEFAULTTONULL to return NULL, MONITOR_DEFAULTTOPRIMARY to return the HMONITOR of the primary monitor, or MONITOR_DEFAULTTONEAREST to return the HMONITOR nearest to the point pt.
The MonitorFromRect API returns the monitor that intersects lprc.
HMONITOR WINUSERAPI MonitorFromRect(LPCRECT lprc,
                                    DWORD dwFlags);
If no monitor intersects lprc, the return value depends upon the dwFlags field. The flags from MonitorFromPoint are used. If the rect intersects more than one monitor, this returns the monitor containing most of the rectangle.
The MonitorFromWindow API returns the monitor that a window belongs to.
HMONITOR WINUSERAPI MonitorFromWindow(HWND hwnd,
                                      DWORD dwFlags);
If a window doesn't belong to a monitor, the return value depends upon the dwFlags field. The flags from MonitorFromPoint are used. If the window intersects more than one monitor, this returns the monitor containing the majority of the window.
The well-known SystemParametersInfo API now includes changes to the uiAction values SPI_GETWORKAREA and SPI_SETWORKAREA. SPI_GETWORKAREA retrieves the size of the working area, which is the portion of the screen not obscured by the taskbar. The pvParam parameter points to the RECT structure that receives the coordinates of the working area. Likewise, SPI_SETWORKAREA sets the size of the work area. The pvParam parameter points to the RECT structure that contains the coordinates of the work area. SPI_SETWORKAREA has been modified to change the work area of the monitor that pvParam belongs to. If pvParam is NULL, the work area of the primary monitor is modified. SPI_GETWORKAREA always returns the work area of the primary monitor. If an app needs the work area of a monitor other than the primary one, it should call GetMonitorInfo (which I'll describe later).
The GetSystemMetrics API has had changes and clarifications made to some of its nIndex values. If you use SM_CXSCREEN or SM_CYSCREEN, you still get the pixel width and height of the screen, but this is only for the primary screen.
Figure 6: New GetSystemMetrics Values
Figure 6: New GetSystemMetrics Values

The same goes for GetDeviceCaps(hdcPrimaryMonitor, HORZRES/VERTRES). If you use SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SM_CXVIRTUALSCREEN, or SM_CYVIRTUALSCREEN, you get the left, top, width, and height of the virtual screen in pixels, respectively (see Figure 6). The SM_SAMEDISPLAYFORMAT returns true if all monitors have the same color format. Note that two displays can have the same bit depth but different color formats if the red, green, and blue pixels have different sizes or are located in different places in a pixel. SM_CMONITORS tells you how many monitors are on the desktop.
This piece of sample code (just pretend there really is a Print function like this)
Print("SM_CMONITORS         is %d", GetSystemMetrics(SM_CMONITORS));
Print("SM_SAMEDISPLAYFORMAT is %d", GetSystemMetrics(SM_SAMEDISPLAYFORMAT));
Print("SM_XVIRTUALSCREEN    is %d", GetSystemMetrics(SM_XVIRTUALSCREEN));
Print("SM_YVIRTUALSCREEN    is %d", GetSystemMetrics(SM_YVIRTUALSCREEN));
Print("SM_CXVIRTUALSCREEN   is %d", GetSystemMetrics(SM_CXVIRTUALSCREEN));
Print("SM_CYVIRTUALSCREEN   is %d", GetSystemMetrics(SM_CYVIRTUALSCREEN));
would produce the following output on my system:
SM_CMONITORS         is 2
SM_SAMEDISPLAYFORMAT is 1
SM_XVIRTUALSCREEN    is -800
SM_YVIRTUALSCREEN    is 0
SM_CXVIRTUALSCREEN   is 1824
SM_CYVIRTUALSCREEN   is 768
As you can see, my sample system has two monitors, both using the same pixel color depth. One of my monitors is 1024X768 pixels and the other is 800 X 600 pixels. The 800 X 600 display is on the left, the 1024 X 768 display is on the right, and their bottom pixels are aligned.
The GetMonitorInfo API returns metric information relevant to a particular monitor (see Figure 7). cbSize is the size of the MONITORINFO struct. A valid instance of a MONITORINFO struct must have this field set equal to sizeof(MONITORINFO) or sizeof(MONITORINFOEX) before a call to GetMonitorInfo is made. rcMonitor is the rectangle of the monitor in the virtual screen. rcWork is the rectangle of the work area of the monitor in the virtual screen. dwFlags provide some additional information about the monitor. The only flag currently defined is MONITORF_ PRIMARY. szDevice is the name of the device, and it is only present in the MONITORINFOEX struct. Most apps will never use this field, and can pass in a MONITORINFO struct instead of a MONITORINFOEX. For example, this piece of code

 MONITORINFOEX mi;
         mi.cbSize = sizeof(mi);
         GetMonitorInfo(hMonitor, (MONITORINFO*)&mi);
 
 Print("Monitor %08X", hMonitor);
 Print("      szDevice  = '%s'", (LPSTR)mix.szDevice);
 Print("      rcMonitor = [%d,%d,%d,%d]", mi.rcMonitor);
 Print("      rcWork    = [%d,%d,%d,%d]", mi.rcWork);
 Print("      dwFlags   = %08X", mi.dwFlags);
produces the output:

 Monitor 00000FAE
       szDevice  = '\\.\Display1'
       rcMonitor = [0,0,1024,768]
       rcWork    = [0,0,1024,740]
       dwFlags   = 00000001
The EnumDisplayMonitors API lets you paint into a DC that spans more than one display. It calls you back for each monitor that intersects your window and gives you an HDC that is appropriate to that monitor. The capabilities and color depth information from that HDC will match those of the monitor. The app can then paint the piece of its window on that monitor into that DC.
To illustrate, here's how an app like PowerPoint
® could use this API. Assume half of a slide show window is on a 256-color monitor and the other half is on a 24-bit true color monitor. The operating system would call the app once for the 256-color monitor piece, and the app would dither the wash for the background. Then the operating system would call the app a second time for the piece on the 24-bit display. The presentation app would take advantage of all of the colors to draw a higher resolution screen.
Keep in mind that applications are not forced to do this; they can just continue painting, assuming the whole screen is the color depth of the primary monitor which will look as good as GDI can do by itself. But if an app wants to, it can paint optimally for the particular display using cus- tom algorithms smarter than GDI's defaults. In the API declaration

 BOOL WINAPI EnumDisplayMonitors(
         HDC                     hdc, 
         LPCRECT                 lprcClip,
         MONITORENUMPROC         lpfnEnum, 
         LPARAM                  dwData)
hdc is an HDC with a particular visible region. The hdcMonitor passed to MonitorEnumProc will have the capabilities of that monitor, with its visible region clipped to the monitor and hdc. If hdc is NULL, the hdcMonitor passed to MonitorEnumProc will be NULL. lprcClip is a rectangle for clipping the area. If hdc is non-NULL, the coordinates have the origin of hdc. If hdc is NULL, the coordinates are virtual screen coordinates. If lprcClip is NULL, no clipping is performed.
lpfnEnum is a pointer to the enumeration function. dwData is application-defined data that is passed through to the enumeration function

 BOOL CALLBACK MonitorEnumProc(
         HMONITOR              hmonitor,
         HDC                   hdcMonitor,
         LPRC                  lprcMonitor,
         DWORD                 dwData)
where hmonitor is the monitor. The callback is called only if it intersects the visible region of hdc and is non-NULL and lprcClip is non-NULL. hdcMonitor is an HDC with capabilities specific to the monitor and clipping set to the intersection of hdc, lprcClip, and the dimensions of the monitor. If hdc is NULL, hdcMonitor will be NULL. lprcMonitor is the clipping area that intersects that monitor. If hdcMonitor is non-NULL, the coordinates have the origin of hdcMonitor. If hdcMonitor is NULL, the coordinates are virtual screen coordinates. dwData is application-defined data that is passed in EnumDisplayMonitors.
Here are some examples of how to use EnumDisplayMonitors. To paint in response to a WM_PAINT message using the capabilities of each monitor, an app would write the following in its window procedure:

 case WM_PAINT:
         hdc = BeginPaint(hwnd, &ps);
         EnumDisplayMonitors(hdc, NULL,
                            MyPaintEnumProc, 0);
         EndPaint(hwnd, &ps);
To paint the top half of a window using the capabilities of each monitor, an app would write the following:

 GetClientRect(hwnd, &rc);
 rc.bottom = (rc.bottom - rc.top) / 2;
 hdc = GetDC(hwnd);
 EnumDisplayMonitors(hdc, &rc, MyPaintEnumProc, 0);
 ReleaseDC(hwnd, hdc);
To paint the entire screen using the capabilities of each monitor, the app would call:

 hdc = GetDC(NULL);
 EnumDisplayMonitors(hdc, NULL, MyPaintScreenEnumProc, 0);
 ReleaseDC(NULL, hdc);
To get information about all the displays on the desktop, the app would call:

 EnumDisplayMonitors(NULL, NULL, MyInfoEnumProc, 0);
The EnumDisplayDevices API allows you to determine the actual list of devices available on a given machine:

 BOOL WINAPI *EnumDisplayDevices(LPVOID lpReserved,
                       int iDeviceNum,
                       DISPLAY_DEVICE * pDisplayDevice,
                       DWORD dwFlags);
lpReserved is reserved for future use and must be zero. iDeviceNum is a zero-based index on the device from which you want to retrieve information. pDisplayDevice is a pointer to a DISPLAY_DEVICE structure for the return information. dwFlags must currently be zero. The DISPLAY_DEVICE structure looks like

 typedef struct {
     DWORD  cb;
     CHAR   DeviceName[32];
     CHAR   DeviceString[128];
     DWORD  StateFlags;
 } DISPLAY_DEVICE;
where the state flags are defined as

 #define DISPLAY_DEVICE_ATTACHED_TO_DESKTOP 0x00000001
 #define DISPLAY_DEVICE_MULTI_DRIVER        0x00000002
 #define DISPLAY_DEVICE_PRIMARY_DEVICE      0x00000004
 #define DISPLAY_DEVICE_MIRRORING_DRIVER    0x00000008
 #define DISPLAY_DEVICE_VGA                 0x00000010
Here's more sample code

 DISPLAY_DEVICE dd;
     ZeroMemory(&dd, sizeof(dd));
     dd.cb = sizeof(dd);
     for(i=0; EnumDisplayDevices(NULL, i, &dd, 0); i++)
     {
       Print("Device %d:", i);
       Print("    DeviceName:   '%s'", dd.DeviceName);
       Print("    DeviceString: '%s'", dd.DeviceString);
       Print("    StateFlags:   %s%s%s%s",
            ((dd.StateFlags &
              DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) ?
              "desktop " : ""),
             ((dd.StateFlags &
              DISPLAY_DEVICE_PRIMARY_DEVICE     ) ?
              "primary " : ""),
            ((dd.StateFlags & DISPLAY_DEVICE_VGA) ?
              "vga "     : ""),
             ((dd.StateFlags &
              DISPLAY_DEVICE_MULTI_DRIVER       ) ?
              "multi "   : ""),
            ((dd.StateFlags &
              DISPLAY_DEVICE_MIRRORING_DRIVER   ) ?
              "mirror "  : ""));
     }
and the output it produces:

 Device 0:
     DeviceName:   '\\.\Display1'
     DeviceString: 'ATI Graphics Pro Turbo PCI (mach64 GX)'
     StateFlags:   desktop primary vga 
 Device 1:
     DeviceName:   '\\.\Display2'
     DeviceString: 'ATI Graphics Pro Turbo PCI (mach64 VT)'
     StateFlags:   desktop

Programming Considerations

Okay, maybe you don't need multimonitor support in your application, but what can you do to avoid being "multimonitor challenged"? There are some common Windows development practices that can make your application misbehave on a multimonitor system.
The most common problem to expect with multiple monitors is centering windows and dialogs on a monitor. In my opinion, this practice has questionable benefits to begin with. However, if you are going to do this you should center your window to the main application, not the monitor. If you must center on a monitor make sure you're using the correct one. Nothing looks cheesier than a window centered on the wrong monitor! If you are trying to center a dialog, use the DS_CENTER dialog style. This lets the operating system do the work and it will place the centered dialog on the correct monitor. Also, keep in mind that SM_CXSCREEN and SM_CYSCREEN always refer to the primary monitor, not necessarily the monitor that displays your application.
Using SM_xxVIRTUALSCREEN as a replacement or quick fix isn't a good idea either, since this results in your window being centered on the virtual desktop. There may be some cases where you can replace SM_xxSCREEN with SM_xxVIRTUALSCREEN, but if you do this, take into account what the code is doing rather than just using a global search and replace. (In particular, if you're calculating the center of a monitor for your splash screen, SM_xxVIRTUALSCREEN is not the right way to do it.)
Even without multiple monitors, centering windows and dialogs can be problematic. Let's say a user has a high-resolution monitor and the application is using only a small percentage of the screen area. If I center new windows to the monitor, they may come up in a location completely unrelated to where the application is.
Currently, most developers use code similar to the following to center a window or dialog:


 void CenterWindowOld(HWND hWnd)
 {
     RECT rcWnd;
 
     GetWindowRect(hWnd, &rcWnd);
 
         // because assumptions were made about the 
         // origin and the screen, the equation
         // to center was simplified in many cases to:
         rcWnd.left = ((GetSystemMetrics(SM_CXSCREEN) - 
                       (rcWnd.right - rcWnd.left)) / 2);
         rcWnd.top  = ((GetSystemMetrics(SM_CYSCREEN) - 
                       (rcWnd.bottom - rcWnd.top )) / 2);
 
     SetWindowPos(hWnd, NULL, rcWnd.left, rcWnd.top,0,0,
             SWP_NOSIZE | SWP_NOZORDER SWP_NOACTIVATE);
 }
Assuming that you cannot use the DS_CENTER window style (which is really the best way to center a dialog), you could try something similar to the code in Figure 8.
As I mentioned earlier, SM_CXSCREEN and SM_ CYSCREEN are now going to return the x and y resolution of the primary monitor, and SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SM_CXVIRTUALSCREEN, and SM_CYVIRTUALSCREEN are provided to get the origin and extent of the virtual desktop. Keeping that in mind, you can see that the code in Figure 8 is functionally equivalent to the code above for systems with only one monitor.

Coordinate Issues

Another problem existing apps will face is that 0,0 is no longer guaranteed to be the most visible upper-left position. This means that negative coordinates can not only exist, but they may be visible. Applications that use negative coordinates to hide their app or assume that there are no negative coordinates may run into problems. This is also true for coordinates greater than SM_CXSCREEN and SM_CYSCREEN.
Similarly, you may have problems if you use 0,0 or SM_CXSCREEN or SM_CYSCREEN for maintaining visibility. Some apps assume that if their coordinates are negative or greater than SM_CXSCREEN or SM_ CYSCREEN that they've somehow wandered off the screen and will move their windows or dialogs back into what they believe to be the visible region. This results in the unusual situation of an application "snapping" its windows or dialogs onto the primary monitor. These are all reasons that the primary monitor will have 0,0 as the upper-left coordinate and SM_CXSCREEN, SM_CYSCREEN as the lower right for compatibility. This will ensure that almost all applications will function as expected if they are running on the primary monitor.
Another potential problem area with SM_CXSCREEN and SM_CYSCREEN lies with the application desktop toolbars (also called appbars). An appbar is a window that is similar to the Microsoft Windows taskbar. The appbar is anchored to an edge of the screen and contains buttons that give the user quick access to other applications and windows. The system prevents other applications from using the desktop area occupied by an appbar. Any number of appbars can exist on the desktop at any given time.
Currently, most appbars can only support a single monitor because they use SM_CXSCREEN and SM_CYSCREEN in their calculations to determine the rectangle they want to occupy. For multimonitor support, you want to allow an appbar to be placed on any edge of any monitor. This requires that you handle your coordinates carefully and that you use the appropriate system metrics to calculate the edges of monitors for use by your appbar. (For more information on appbars, see the Application Desktop Toolbars section of the Win32
® SDK available on MSDN, as well as at http://www.microsoft.com/msdn.)
It is important to note that you must also carefully crack coordinates to get the proper signed values. This was less of a problem in the past because there were no negative coordinates delivered; if you mixed up signed and unsigned coordinates, it didn't matter. Now that negative coordinates are valid, you may end up with invalid results if you do not properly extract the coordinates. I've included a sample macro that you can use to properly extract coordinates and maintain the correct sign. For example, this


 (int)LOWORD(lParam) 
should be:

 (int)(short)LOWORD(lParam)
Better still, use the new macros provided in the SDK in Windowsx.h:

 GET_X_LPARAM(lParam) 
 GET_Y_LPARAM(lParam)
A screen saver will only display on the primary monitor unless you link with the new Scrnsave.lib. The current ScrnSave.lib gets the Window size like this:

 X=0; 
 Y=0;  
 dX=GetSystemMetrics(SM_CXSCREEN); 
 dY=GetSystemMetrics(SM_CYSCREEN);  
As you can see, this only blanks the primary monitor. It has been updated and now does this:

 hdc = GetDC(NULL);
 GetClipBox(hdc, &rc);
 ReleaseDC(NULL, hdc);
 X = rc.left;
 Y = rc.top;
 dX = rc.right  - rc.left;
 dY = rc.bottom - rc.top;
This gets the RECT of the virtual desktop. This method will work correctly on Windows 95, Memphis, Windows NT 3.1, and Windows NT 4.0, so applications linked with the new Scrnsave.lib will also work on those systems.
If you want your screen saver to cover all the monitors by applying it to the virtual desktop, relinking with the new lib is all that is required. However, if you want independent images on the monitors, you'll need to use the new APIs described above and handle each screen separately. Likewise, you can use the new APIs to optimize the display of your images on the various monitors via EnumDisplayMonitors.
Anyone developing with DirectX
is probably wondering about the impact of multiple monitors on DirectX. Any existing DirectX application should continue to work correctly. However, if your application runs in full-screen mode, it may run only on the primary monitor. Windows-based applications should work on any monitor or device that is supported by DirectX. In fact, no new APIs have been added to DirectDraw® as DirectDrawEnumerate is all that is required (see Figure 9).
Figure 9 Information displayed with DXView
Figure 9 Information displayed with DXView

Some minor changes to ShellExecute and ShellExecuteEx ensure that any spawned applications come up on the same monitor as the parent application. If you specify an hWnd when calling ShellExecute or ShellExecuteEx, the new application window will appear on the same monitor as the window referred to by the hWnd.
The sample program (see Figure 10) simply calls the new APIs to get information on the system configuration and dumps out that information. The code was built with the multimon.h header file so it works on single-monitor systems as well as on Windows 95 and Windows NT 4.0.
Let's walk through some of the sample code, starting with multimon.h and then Figure 5. I'm going to skip over most of the constants and structure definitions of multimon.h since they're pretty self-explanatory. I'll start with the section labeled (via comments) "Implement the API stubs." Notice that just before this comment is an #ifdef COMPILE_ MULTIMON_STUBS. This file actually contains code and, therefore, must be included in only one module or it will generate "multiply defined" errors at link time. You should define this constant in one source file before including this include file; you should not define it in any other source files that require you to include this file.
If COMPILE_MULTIMON_STUBS is defined, then the code that follows in the header will be included. This declares a number of global function pointers that will be used by the stub code to locate the corresponding APIs built into the operating system, if present.
Looking at the first function, InitMultiplMonitorStubs, will clarify things somewhat. It should be called once before any of the APIs defined in the header can proceed, although you don't have to worry about it since the included API stub code calls it as necessary. This function determines if the underlying operating system has built-in support for multiple monitors. If it does, then it gets the correct addresses for the APIs (in the system file USER32) that correspond to those in this header file and initializes the global function pointers appropriately. If the underlying operating system doesn't support multiple monitors, then these pointers are set to NULL. In either case, the function sets a static flag that indicates whether this API was correctly initialized, and returns TRUE on a system that has built-in multimonitor support or FALSE on a system that does not. (You can see this in the very first if construct: if this function was already called, the function quickly exits using one of the function pointers to determine if there is built-in support on the platform.)
At this point, the file moves into stub function implementations. Since many of the stubs are implemented in the same manner, I'll cover only a few in detail. The first real API stub is the GetSystemMetrics function. Notice that the name of the stub is actually xGetSystemMetrics. This allows you to enhance or replace the underlying operating system API without getting compile errors by later redefining GetSystemMetrics as xGetSystemMetrics (see the #defines at the end of the include file).
First, the xGetSystemMetrics implementation makes sure InitMultipleMonitorStubs code was called to initialize things and to determine if you're on a multimonitor-aware operating system. If you are, control is passed directly to the operating system's implementation of GetSystemMetrics. If your system isn't multimonitor-aware, then review the flags, handle those that would not have been recognized on a system without multiple monitors, and return appropriate values (knowing that on such a system there will be only one monitor and that it'll have the standard coordinates). Finally, if the flag is not one of the new ones, you pass control to the operating system for handling as usual. This is a general theme throughout the stubs.
TestMM.C is a basic Windows application with many familiar features, including WinMain with a message loop and some support routines to allow easy printing into the main client area. It also has some standard menu items for executing API calls that illustrate the effect of using those APIs on the application window.
The interesting portion of the application is actually the DoTestMM function, which is called whenever the application window is moved or the user presses F5 (the standard refresh key). It is called every time the application is moved so that it can reprint information specific to the monitor the application is actually displayed on. Notice that the DoTestMM function includes a check that quickly exits if it's on the same monitor as it was the last time it was called (since none of the information would have changed in that case). From that point on, the code just calls the various multimonitor APIs and prints the resulting information into an edit box created and sized to fill the client area of the application window. Although not a very exciting application, it does show how to call the various APIs, their relevant structures and flags, and how to use them, as well as what to expect in response to those APIs.
Finally, the listings include an MMHelp file that contains routines that handle common tasks on a multimonitor system. Although the routines are fairly simple, I'll quickly review some of them here.
GetMonitorRect is used to determine the screen or work area, depending on the flag passed in the third parameter, for a window. This basically passes in the area of the screen that is closest to the window handle. You use this to clip a window onto a visible portion of the screen. The ClipRectToMonitor function uses GetMonitorRect to determine the best monitor to clip a rectangle to, and then returns the updated rectangle that represents the best place to put it. This might be useful if you want to display a dialog and make sure that it's visible. You can pass this function the coordinates that you'd like to use and it will find the closest location where that entire dialog can be visible. In fact, the new function ClipWindowToMonitor does just that. Given a window handle, it gets the bounding rectangle of the window, uses ClipRectToMonitor to find an appropriate location, and then moves the window to that location.
Similarly, CenterRectToMonitor determines the correct monitor on which to center a rectangle and then updates the rectangle so that it is centered on that monitor. CenterWindowToMonitor uses CenterRectToMonitor to determine the appropriate center location and then moves the window to that location. IsWindowOnScreen determines if your window is actually visible anywhere on any screen, and MakeSureWindowIsVisible makes sure your window becomes visible on a screen.

Installing Multiple Monitors

Multiple monitor support is available only on the Memphis and Windows NT 5.0 operating systems that have updated video drivers (that is, updated to support multiple monitors). Any video card that is supported by Windows can be used as your primary display, but the extra cards must have updated drivers to support the new features. Currently, Microsoft has updated drivers for a number of video cards designed for use on a PCI bus that will be shipped with the operating system. (For more details on hardware, see the section on supported hardware in your documentation.) At the time I wrote this article, drivers for the following cards were available:

  • ATI Mach64, Rage I, II & III
  • S3 764(Trio), 764V+(765)
  • Cirrus 5436,7548,5446
  • Imagine 128 I and II
  • S3 M65
  • Matrox Millenium
  • Matrox Mystique
  • Cirrus Laguna
  • ET6000
  • Rendition
  • 3DFx
  • STB
Setup is pretty much plug and play. First, you must get your system working with one monitor. Then, shut down the system and install another video card and monitor. When you restart your machine, Windows should automatically detect the new card (and possibly the monitor). Once the proper drivers get copied to your machine, you can go to the control panel to configure the physical mapping of your virtual desktop to your monitors, as well as the resolution, color depth, and refresh rates for each.
Note that the order of the cards on the bus may impact the configuration. In particular, the VGA monitor where startup MS-DOS text is initially displayed is chosen by the system before Windows even starts. As a result, you may need to rearrange the cards in order to get the configuration the way you want it.

User Interface Changes

One of the nice things that you will notice about adding multiple monitors is that not much has changed in order to provide this support. In fact, if you have only one monitor there will be no noticeable difference on your machine because most of the changes were actually made beneath the surface of the operating system. However, you will notice a few things if you install multiple video cards and monitors.

Figure 11 Monitors tab for multimonitors
Figure 11 Monitors tab for multimonitors

There was a small change to the Display Properties control panel. The Settings tab was replaced with a Monitors tab, under which the settings for each monitor can be adjusted (see Figure 11). Any changes are then made on a per-monitor basis. If you only have one monitor, you'll still see the Settings tab as illustrated in Figure 12 since there is no need to choose the monitor you want to configure.
Figure 12 Settings tab for a single monitor
Figure 12 Settings tab for a single monitor

Some minor changes were made to make the shell multimonitor-aware. This included having the shell's desktop appear on all monitors and adding support for placing the taskbar on any edge of any monitor. (If you combine this with the auto-hide feature, you'll have a lot more places to lose it!) These changes allow you to drag items, such as shortcuts, from the desktop of one monitor to the desktop of another. The system will also try to start an application on the monitor that contained the shortcut. For example, if you want to start an app on a specific monitor, one way would be to place a shortcut on the part of the desktop that is on that monitor.

Differences Between Memphis and Windows NT 5.0

There are some minor differences in the implementation of multimonitor support between Memphis and Windows NT 5.0. However, it's not clear at this time which, if any, of these differences will remain in the final release. Therefore, rather than getting into details of current differences, I'll give you my standard "test on both" speech.
As with most Win32 applications, the user can generally use the application on either Windows 95 or Windows NT since both are Win32 platforms. As a developer, you must make sure your application works as expected on each targeted platform. Test with one monitor and with two, test at high resolutions and in VGA mode, test on fast machines and on slow ones—are you getting the picture? Often there is no difference, but in some cases there may be and it's a lot easier to address the differences before you release an application than it is afterward. All too often developers fail to notice serious flaws because the application works great on the super whiz-bang machine it was developed on, only to fail miserably on the target low-end machine.

The Cliff Notes

Writing multimonitor applications is easy, and making your current application multimonitor-aware is even easier. Let's just summarize what you need to know.

  • When centering windows and dialogs, make sure you center them to your application window. If you want to center on a monitor, make sure you use the correct system metrics. (Use the MMHelp routines to make it simpler.)
  • If you are developing screen savers, make sure you are using the latest version of Scrnsave.lib. It runs properly on existing systems as well as multimonitor machines. Make sure you are correctly extracting coordinates to ensure you get the correct sign. Negative coordinates are now valid.
  • If you are saving window positions to restore later, make sure you check that the positions are still valid before using them. Since the user can move the coordinates of secondary monitors on the fly in the control panel, or even remove a monitor altogether, the position you saved may no longer be valid. (You can use the routines in MMHelp to address this problem.)
  • Remember that negative coordinates or coordinates larger than SM_CXSCREEN, SM_CYSCREEN may be visible. If you are using off-screen coordinates as a method of hiding windows, use the appropriate system metrics or you may be in for a surprise.
  • Likewise, don't use 0,0 and SM_CXSCREEN, SM_CYSCREEN to clip windows, menus, and so on onto the screen. This results in inappropriate behavior when your application is not on the primary monitor. (I know of several Microsoft applications that must now be corrected, including Visual C++ 5.0.) Here again, the routines in MMHelp will come in handy.
  • If you use ShellExecute or ShellExecuteEx in your application, supply an hWnd so that the system can open any new windows on the same monitor as the calling application. This is what the user will expect.
  • Use multimonitor support to run your development and debugging environment on one monitor and your application on another. This is particularly true for full-screen DirectDraw applications.
  • For developers, the golden rule is Test! Test! Test! Make sure you test your application thoroughly on all target platforms, with and without multiple monitors. Make sure you test your app on both the primary and secondary monitors. If it's going to mess up, it'll probably be on a secondary monitor.
  • Finally, don't panic over existing applications. The primary display is always set up to be as compatible as possible with existing applications. If you have released applications that may be multimonitor challenged, they should still work on the primary monitor.

Conclusion

Having multiple monitors on the same machine and as part of the same desktop is very cool, but it also opens up a whole new set of possibilities for applications—from games that use multiple monitors to give you a more panoramic view to CAD and presentation applications that have built-in support for custom display devices. Since the support is built into the operating system, compatibility with existing applications is excellent and the impact on existing applications is minimal.
If you have a PCI machine and a couple of old monitors lying around, get a few more video cards and I guarantee you'll never go back to just one monitor!

Terminology

The following terminology for multimonitor support is currently used in the Windows documentation.
VGA monitor This is the main monitor that you see text on when the computer initially boots. It's also the monitor that DOS apps will run on when running exclusively (in DOS mode) or when they are running full-screen.
Primary monitor Another name might be the compatibility monitor since this is the monitor that is guaranteed to contain traditional screen coordinates. Almost all multimonitor-challenged apps will run correctly on this monitor. The primary monitor may or may not be the VGA monitor. The VGA monitor is determined by the system because of its location on the bus. On the other hand, Windows determines the primary monitor.
Secondary monitors These are all of the monitors that are not the primary monitor but are included in the Windows desktop area.
Independent displays These monitors are present on the system, but are not part of the Windows desktop. These monitors can still be used by applications, but they do not contain any part of the Windows desktop. The calculation of the virtual desktop does not include this area and, as a result, you cannot drag application windows to or from this monitor to other monitors on the system—even if an application is running that makes use of this monitor.
Mirrored monitors These are monitors that are present on the system that receive a duplicate of all activity on the primary monitor. This allows a user to present the same information on multiple displays. A user can configure devices to be mirrors using the control panel. (There aren't really any development aspects to this type of monitor, but it's worth knowing about.)

From the June 1997 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.

© 1997 Microsoft Corporation. All rights reserved. Legal Notices.

© 2015 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft