MSDN Home > MSJ > June 1997 |
How to Exploit Multiple Monitor Support in Memphis and Windows NT 5.0 |
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.
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. |
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.
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 |
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.
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, |
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, |
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 |
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)); |
would produce the following output on my system: |
SM_CMONITORS is 2 |
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 |
|
produces the output: |
|
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 |
|
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 |
|
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: |
|
To paint the top half of a window using the capabilities of each monitor, an app would write the following: |
|
To paint the entire screen using the capabilities of each monitor, the app would call: |
|
To get information about all the displays on the desktop, the app would call: |
|
The EnumDisplayDevices API allows you to determine the actual list of devices available on a given machine: |
|
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 |
|
where the state flags are defined as |
|
Here's more sample code |
|
and the output it produces: |
|
|
|
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.
|
|
should be: |
|
Better still, use the new macros provided in the SDK in Windowsx.h: |
|
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: |
|
As you can see, this only blanks the primary monitor. It has been updated and now does this: |
|
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 |
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.
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.
|
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 |
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.
|
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.) |
Get it at your local newsstand, or better yet, subscribe. |
沒有留言:
張貼留言