Virtual Screen Coordinates

Home
Back To Tips Page

In the multi-monitor universe, many programs stop working.  There are a lot of issues involved in generalizing a uni-monitor application to run on multi-monitor systems.  This essay will not attempt to address all of those issues.  This essay is going to concentrate on just one aspect of multi-monitor programming, the virtual screen coordinate system.

This was prompted by a question in the newsgroup by someone who wanted to set the dropped rectangle size to span multiple monitors.  The problem was that the amount of span was not readily determinable. I solved the problem, as demonstrated by this (admittedly low-resolution) screen snapshot.  Note that the dropdown extends across both screens.

 But the trick is to make this work when the screen positions are reversed.

The "virtual screen" is the bounding box that surrounds all windows.  The 0,0 coordinate is the top left corner of this virtual screen.  Like a physical screen, coordinates on this virtual screen increase rightward in x and downward in y.  However, the monitors can be arranged in a variety of ways. This is particularly true when the monitors have different screen resolutions.  In the world of flat-panel displays, each monitor should be run at its "natural" resolution, or the aliasing problems will produce singularly ugly results.

For example, on my system, I have two monitors, the primary monitor running at 1920  1200 and the secondary monitor at 1280  1024.  The following layouts represent some of the possible arrangements:, where the numbers represent the top left corner coordinate of each monitor

 
1:Screen (0, 0) (0, 0) (0, 0) (0, 0) (0, 0)
1:Virtual (0, 0) (1024, 0) (0, 0) (0, 1024) (0,0)
2:Screen (1920, 0) (-1024, 0) (0, 1200) (640, -1024) (1920, 1200)
2:Virtual (1920, 0) (0,0) (0, 1200) (640, 0) (1920. 1200)

Note that the arrangement of the windows can be controlled; compare the two images, first and last.  Note that in both, monitor 1 (the primary monitor, the 1900  1200 display) is on the left, and monitor 2 is on the right.  In the leftmost image, the screen coordinates of the top left corner of monitor 2  (1920, 0) but in the rightmost image, the screen coordinates of the top left corner of monitor 2 are (1920, 1200).  So the "Display properties" allows you to arrange the relationship of the windows.

Taking the second arrangement, we look at the screen and virtual coordinates of each monitor

The dotted line is the virtual screen. 

The (0, 0) screen coordinate is always the top left pixel of the primary monitor. But because monitor 2 is to its left, the virtual screen coordinate of this pixel is (1280, 0).  The screen coordinates of the top left pixel of the secondary monitor are therefore (-1280, 0), given the layout shown.

Note a very important feature here: negative screen coordinates are potentially valid.  There are people who "hid" windows by merely moving them "off the screen to the left", and in this arrangement that will not work.

In cases where a screen coordinate is packed into a single LPARAM value, it is very important that you do not unpack the screen coordinates by using LOWORD and HIWORD.  The latest Platform SDK has two special macros that do this: GET_X_LPARAM and GET_Y_LPARAM, which can be defined roughly as

int GET_X_LPARAM(LPARAM lParam);
int GET_Y_LPARAM(LPARAM lParam);

but what is really going on here is

#define GET_X_LPARAM(lParam) ((int)(short)LOWORD(lParam))
#define GET_Y_LPARAM(lParam) ((int)(short)HIWORD(lParam))

Note that it first casts the 16-bit value to a short, a signed value, so that when it casts it to a 32-bit int, there will be a sign extension applied, and the value will be correctly represented.

Note that some monitors do not fill up all of the virtual screen.  The virtual screen is a bounding box of all monitors, the smallest rectangle that encloses the monitors.  Therefore, knowing a valid coordinate in virtual space does not guarantee that the selected pixel is visible on any monitor.

The coordinates of the virtual screen always place (0, 0) at the top left corner, and the width is ::GetSystemMetrics(SM_CXVIRTUALSCREEN) and the height is ::GetSystemMetrics(SM_CYVIRTUALSCREEN)

The CMonitors class

What I had to do was deduce the screen layout from the coordinates of each of the monitors.  To handle all this, I wrote a little class, CMonitors, defined below

class CMonitors {
    public:
       CMonitors();
       BOOL Initialize();

       BOOL ScreenToVirtual(CRect & r);
       BOOL ScreenToVirtual(CPoint & pt);

       BOOL VirtualToScreen(CRect & r);
       BOOL VirtualToScreen(CPoint & pt);

       BOOL LeftLimitFrom(CPoint & pt);
       BOOL RightLimitFrom(CPoint & pt);
       BOOL TopLimitFrom(CPoint & pt);
       BOOL BottomLimitFrom(CPoint & pt);
    protected:
       BOOL valid;
       CArray monitors;
       CArraySortX;
       CArraySortY;
       CArray VirtualRects;
       
       // The real and object monitorprocs
       BOOL monitorproc(HMONITOR monitor, HDC, LPRECT rect);
       static BOOL CALLBACK monitorproc(HMONITOR monitor, HDC dc, LPRECT rect, LPARAM data);
       typedef enum {ByX, ByY} SortBy;
       void Sort(CArray & data, SortBy what);
       
       BOOL CheckValid();
};

This class  provides four interface functions for coordinate conversion: ScreenToVirtual on a point and rectangle, and VirtualToScreen on a point and rectangle.  Note that these take CRect & parameters or CPoint & parameters. There is a method to initialize the structure.

It also provides four methods for determining the maximum point accessible starting from a given (x,y) point on any part of the screen.  These "limit" functions tell the maximum projection you can reasonably go in any direction.

The need for an initialization method which is explicit is important.  A correctly-functioning program must be able to respond to WM_DISPLAYCHANGE messages at any time.  In the two screen snapshots showing the dropdown list, I had used one screen arrangement, shown the dropdown items; then gone to the Display Properties, swapped the screens around, dragged the application to its new location at the "far left" of the new arrangement, and dropped the control again.  Because I had properly handled the WM_DISPLAYCHANGE message, the program continued to function correctly.

The methods all return BOOL values indicating their success or failure.  The valid flag in the class is initialized to FALSE, is set to TRUE by any successful call on Initialize, and changed to FALSE if there is any error during initialization.  The CheckValid method returns TRUE if all the validity checks are met, and FALSE if there is an error, and as a side effect it will call SetLastError to set an error code the caller can retrieve with GetLastError.

Handling changes in display parameters: WM_DISPLAYCHANGE and Initialize

To handle WM_DISPLAYCHANGE, there is no wizard option, not even in VS.NET 2003, so you have to hand-edit it into the code.  First, add a method to your top-level window class handler:

LRESULT OnDisplayChange(WPARAM, LPARAM);

The prototype must return type LRESULT and it takes precisely two parameters, of types WPARAM and LPARAM.  Nothing else is acceptable.

Next, add the following line to the Message Map of the main window:

ON_MESSAGE(WM_DISPLAYCHANGE, OnDisplayChange)

Note that in VS6, this should be outside any of the "magic" AFX comments.

Finally, add a handler for WM_DISPLAYCHANGE. In this example, the class contains a declaration

CMonitors monitors;

and we need to re-initialize this on a display change

LRESULT CWhateverMyClassIs::OnDisplayChange(WPARAM, LPARAM)
    {
     monitors.Initialize();
     return 0;
    }

The first initialization should be part of the initialize of the top-level window.  For a dialog-based application, this would be in the OnInitDialog method. 

If you need to do this at other than the top-level window, you should consider "forwarding" the WM_DISPLAYCHANGE method from the top-level window.  Note that child windows do not receive broadcast messages such as WM_DISPLAYCHANGE, so if there is a need to "forward" it, one option is to do

RESULT CWhateverMyClassIs::OnDisplayChange(WPARAM, LPARAM)
    {
     SendMessageToDescendants(WM_DISPLAYCHANGE);
     return 0;
    }

You will need to place a WM_DISPLAYCHANGE handler in each of the lower-level windows that need to handle monitors.  Now, you might be tempted to say "Well, I could optimize performance if I used a global variable, but global variables are considered Poor Practice, so being a Good C++ Programmer, I will put a CMonitors structure in my CWinApp class and everyone can use it!"  This is very poor reasoning.  First, what "optimization" is going on here?  We are not talking massive amounts of space, or any significant amount of time (after all, how often do users change the display layout?).  It violates every known good principle of modularization; some child control can't function unless it knows the class of the CWinApp so the programmer can write something as absurd as

((CMyApp *)AfxGetApp())->monitors.Initialize();

while harboring the delusion that this constitutes good programming practice.  No child control should know anything about variables outside its own class, or classes which are not related to its definition.  Code like the above line indicate a very deep failure in program design. The use of variables in the user's derived CWinApp class in any class other than that class doesn't even count as syntactic saccharine (which isn't as good as syntactic sugar); it is more like substituting a toxic chemical as a sweetener (soluble lead compounds taste very sweet, which is why children are at risk eating lead paint--to them it tastes sweet, so it must be candy).

Note that while the documentation of WM_DISPLAYCHANGE says "This message is only sent to top-level windows.  For all other windows it is posted", note that examining the message traffic of child windows using Spy++ shows that no WM_DISPLAYCHANGE message is sent to any child window. Hence the need to do forwarding.

In the above example, I also ignore the parameters of WM_DISPLAYCHANGE, since their values are irrelevant; all we need is the notification event itself.

To create the array of all monitors, it is necessary to use ::EnumDisplayMonitors, which requires a callback function.  For details of how to use callbacks, see my essay on callback functions in MFC.  I will simply use those principles here without much explanation.  I use overloading; there are two monitorproc functions, a static one to satisfy the needs of the API and an "object" one which executes in the context of the current CMonitors instance.

Initialize

he Initialize method is

BOOL CMonitors::Initialize()
    {
     valid = TRUE; // assume true, any error will set it false
     monitors.RemoveAll();
     SortX.RemoveAll();
     SortY.RemoveAll();
     VirtualRects.RemoveAll();

     if(!::EnumDisplayMonitors(NULL, NULL, monitorproc, (LPARAM)this))
        { /* failed */
         valid = FALSE;
         return FALSE;
        } /* failed */

     if(valid)
        { /* sort them */
         Sort(SortX, ByX);
         Sort(SortY, ByY);
        } /* sort them */
        
     // Now fill in the rectangles
     VirtualRects.SetSize(monitors.GetSize());

     for(int i = 0; i < monitors.GetSize(); i++)
        { /* fix up virtual */
         VirtualRects[i] = monitors[i].rcMonitor;
         ScreenToVirtual(VirtualRects[i]);
        } /* fix up virtual */        
     return valid;
    } // CMonitors::Initialize

Initialize: monitor enumeration

I use this as the LPARAM value for the callback, and use it to switch myself back into C++ space from C space:

/* static */ BOOL CALLBACK CMonitors::monitorproc(HMONITOR monitor, HDC dc, LPRECT rect, LPARAM data)
   {
    CMonitors * self = (CMonitors *)data;
    return self->monitorproc(monitor, dc, rect);
   }

BOOL CMonitors::monitorproc(HMONITOR monitor, HDC, LPRECT)
    {
     CString s;
     MONITORINFOEX info;
     info.cbSize = sizeof(info);
     if(!::GetMonitorInfo(monitor, (LPMONITORINFO)&info))
        { /* failed */
         valid = FALSE;
         return FALSE;  // stop enumeration if error
        } /* failed */

     monitors.Add(info);
     return TRUE;
    } 

Note that all I'm really concerned with here is enumerating the monitors; DCs, clipping regions and such do not matter.  So those parameters are specified as NULL to ::EnumDisplayMonitors.  I could have simply eliminated these parameters entirely from the second-level "object" method, but left them in so others who wanted to do something more elaborate would have a prototype to base their modifications on.

In the callback, I call ::GetMonitorInfo and use the MONITORINFOEX structure to retrieve everything; my thought was that if I want to extended the functionality of CMonitors I would have operations such as GetFirstMonitor and GetNextMonitor, which would allow me to return a MONITORINFOEX pointer from a method GetInfo(POSITION), but I have not implemented such features in this version.

Initialize: the VirtualRects table

In order to improve performance of the VirtualToScreen methods, I create an array of virtual rectangles corresponding to the screen rectangles.  This simplifies the code tremendously.  Initially, I had been computing these "on-the-fly", but the code became convoluted and hard to understand.  This not only improves the performance, it makes the code much easier to read!

VirtualToScreen(CRect &) and ScreenToVirtual(CRect &)

A RECT is just two POINT values, so these are very simple.  I show only one of them here; the other is correspondingly simple

BOOL CMonitors::ScreenToVirtual(CRect & r)
    {
     if(!ScreenToVirtual((CPoint &)r.left))
        return FALSE;
     if(!ScreenToVirtual((CPoint &)r.right))
        return FALSE;
     return TRUE;
    } // CMonitors::ScreenToVirtual

ScreenToVirtual(CPoint &)

Now things get interesting.  To compute the layout, I need to have a sorted list of the monitors in X-order, and a sorted list of the monitors in Y-order.  But it can't be in both orders.  So what I do is create two "views" on the data, one view sorted by X and one by Y.  These "views" merely contain the indices of the monitor entries, in sorted order.

Suppose I have three windows; to simplify the discussion, we will make all of these 800  600 only because it is easier to do "arithmetic in our heads" with nice round numbers like this.  The chosen layout of the windows for this discussion is as shown.  The parameters are

Monitor Screen Virtual
1 (0, 0) (800, 600)
2 (-800, 0) (0, 600)
3 (0, -600) (800, 0)

Sorted by X-coordinate, the possible sorts are <2,1,3> and <2,3,1>.  Either order will be acceptable, so a stable sort or a secondary sort on the Y-coordinate is not required.

The virtual coordinates are (0,0) to (1600, 1200).

In the code below, I'm eliminating some debug trace output that appears in the actual source.  This is just to simplify the presentation.

BOOL CMonitors::ScreenToVirtual(CPoint & pt)
    {
     if(!CheckValid())
        return FALSE;

     if(monitors.GetSize() == 1)
        return TRUE;  // already in virtual coordinates...only one monitor

     CPoint v(0,0);
     //****************************************************************
     // Convert X
     //****************************************************************
     BOOL found = FALSE;
     
     for(int i = 0; i < monitors.GetSize(); i++)
        { /* scan X and accumulate */
         if(pt.x <= monitors[SortX[i]].rcMonitor.right)
            { /* within current */
             v.x = - monitors[SortX[0]].rcMonitor.left + pt.x;
             found = TRUE;
             break; // we've resolved it
            } /* within current */
        } /* scan X and accumulate */

     if(!found)
        { /* out of range */
         ::SetLastError(MONITORS_ERR_OUT_OF_BOUNDS);
         return FALSE;  // point is outside virtual screen
        } /* out of range */

     //****************************************************************
     // Convert Y
     //****************************************************************
     found = FALSE;
     for(int i = 0; i < monitors.GetSize(); i++)
        { /* scan Y and accumulate */
         if(pt.y <= monitors[SortY[i]].rcMonitor.bottom)
            { /* within current */
             v.y = -monitors[SortY[0]].rcMonitor.top + pt.y;
             found = TRUE;
             break; // we've resolved it
            } /* within current */
        } /* scan Y and accumulate */

     if(!found)
        { /* out of range */
         ::SetLastError(MONITORS_ERR_OUT_OF_BOUNDS);
         return FALSE;  // point is outside virtual screen
        } /* out of range */

     pt = v;
     
     return TRUE;
    } // CMonitors::ScreenToVirtual
 

The default checks are done by CheckValid()

This code is designed to work on single-monitor systems, and the next test is a performance improvement: there is no need to do any computations on a single-monitor system; by definition, the virtual coordinates are identical to the screen coordinates of the one-and-only monitor.

First, it converts the X-coordinate.  To do this, I scan the sorted list from left-to-right.  If the input coordinate is within the range of any window horizontally, then I am able to resolve it immediately.  However, I will not convert any screen coordinate that lies outside the virtual coordinate space.  The SortX array allows me to scan the windows in increasing X-coordinate value.

The Y-coordinate conversion works the same way.  Note that the algorithm is identical, scanning the monitors in increasing Y-coordinate order.

We can see how this works in our above example.  Consider a coordinate in monitor 3, at (400, 300).  For the X-conversion, either sort order, <2, 1, 3> or <2, 3, 1> will work, so it first checks monitors[0] which has screen coordinates (­800, 600).  The test pt.x <= monitors[SortX[i]].rcMonitor.right fails because 400 is not < ­1. So the loop iterates to monitors[1], which, no matter whether it is 1 or 3, will have a right coordinate of 799, and 400 < 799, so the break is taken after computing v.x = ­monitors[0].rcMonitor.left + pt.x, or v.x = ­(800) + 400 or 1200.

That's all there is to it.

VirtualToScreen(CPoint & pt)

This is a bit more challenging, which is why I did the original computation of the array of virtual coordinates.  Once the monitors have been enumerated, and the SortX and SortY arrays have been computed, ScreenToVirtual will work.  So all I have to do is make a copy of the rcMonitor rectangle for each monitor, and convert it from screen to virtual coordinates. 

Monitors cannot overlap, so we can uniquely identify each monitor given its virtual coordinates.

BOOL CMonitors::VirtualToScreen(CPoint & pt)
    {
     if(!CheckValid())
        return FALSE;

     if(monitors.GetSize() == 1)
        return TRUE; // Already in virtual coordinates; only one monitor

     BOOL found = FALSE;
     CPoint s(0,0);

     for(int i = 0; i < VirtualRects.GetSize(); i++)
        { /* scan rects */
         if(VirtualRects[i].PtInRect(pt))
            { /* found it */
             found = TRUE;
             s.x = monitors[i].rcMonitor.left + (pt.x - VirtualRects[i].left);
             s.y = monitors[i].rcMonitor.top  + (pt.y - VirtualRects[i].top);
             break;
            } /* found it */
        } /* scan rects */

     if(found)
        { /* success */
         pt = s;
         return TRUE;
        } /* success */
     else
        { /* not valid */
         ::SetLastError(MONITORS_ERR_OUT_OF_BOUNDS);
         return FALSE;
        } /* not valid */
    } // CMonitors::VirtualToScreen 

As before, the CheckValid simply tests for validity and sets error codes if there is a problem. 

The virtual and screen coordinates will be identical in a single-monitor system, so that special case is handled early.

Since the coordinates are unique, we do not need to scan the virtual monitor array in any particular order, so I chose to iterate simply 0..n­1 for n the number of monitors. Since these are in the same order as the original array, if I find the virtual rectangle that contains the point, I have the index of the corresponding screen rectangle.  So I compute the delta into the virtual rectangle and add that to the left or top coordinate of the corresponding screen coordinate.  Done.

Right/Left/Top/BottomLimitFrom(CPoint & pt)

These were added to solve a problem relating to the let's-extend-the-dropdown-as-far-to-the-right-as-possible (in my case, which prompted my interest in this, I want to be able to extend-downward or extend-upward, to fix a long-standing multi-monitor bug in my automatic-resizing-combo-box, so my solution was not entirely altruistic).

The problem is that you can't rely on the fact that a monitor is edge-aligned with the adjacent monitor.  In my case, my secondary monitor is 12801024, while my primary monitor is 19201200.  If I have aligned the tops of the monitors, there is a gap of 176 pixels at the bottom.  Of course, I could align the bottoms, center the secondary monitor vertically on the first, or place the secondary monitor entirely below, or entirely above, the primary monitor. So whether or not I can extend a dropdown across two monitors depends on the actual relationship of those monitors.  To do this, I have to start at a screen (xy) and see how far I can go to the right.  Therefore, I need a way to compute the extension distance that will allow the extended object to remain visible.

Here is one of the functions, RightLimitFrom, which computes the right extension from a given screen X-coordinate.  The input is the starting (xy) coordinate, expressed in screen coordinates, and the effect is to update the .x member to reflect that maximum possible extension to the right.  This function will return FALSE if there can't be a valid computation.

This is done by scanning the monitor array in X-coordinate order until the screen coordinate is found in the .rcMonitor rectangle for that monitor (remember that it is not possible to create overlapping monitors, so this determination uniquely represents the monitor that contains the point).  If no valid point can be found, the result is an error and the function returns FALSE.  Having located the starting monitor, the function sets the maximum extension to be the right edge of that monitor.

Now it does a second iteration.  Starting at the "next" monitor, it asks if the (right + 1, y) coordinate is in the next higher monitor.  If it is, this means that the next monitor is vertically positioned so an extension into it is visible.  So it sets the right extension to the the right end of that monitor, and continues the iteration.  The iteration proceeds until either all monitors have been examined, or a monitor is found that would mean that the extension would be invisible.

BOOL CMonitors::RightLimitFrom(CPoint & pt)
    {
     if(!CheckValid())
        return FALSE;

     if(monitors.GetSize() == 1)
        { /* update to width */
         pt.x = monitors[0].rcMonitor.right - 1;
         return TRUE; // Already in virtual coordinates; only one monitor
        } /* update to width */

     BOOL found = FALSE;
     int first;
     for(first = 0; first < monitors.GetSize(); first++)
        { /* scan monitors */
         CRect r = monitors[SortX[first]].rcMonitor;
         if(r.PtInRect(pt))
            { /* found starting monitor */
             found = TRUE;
             break;
            } /* found starting monitor */
        } /* scan monitors */

     if(!found)
        { /* can't find start monitor */
         MON_TRACE(("RightLimitFrom: out of bounds\n"));
         ::SetLastError(MONITORS_ERR_OUT_OF_BOUNDS);
         return FALSE;
        } /* can't find start monitor */

     CPoint result = pt;
     result.x = monitors[SortX[first]].rcMonitor.right;

     // now project outwards
     for(int i = first + 1; i < monitors.GetSize(); i++)
        { /* check end */
         CPoint test = result;
         test.x++; // see if it is in the next monitor
         CRect target = monitors[SortX[i]].rcMonitor;
         if(!target.PtInRect(test))
            break; // we have the max
         result.x = target.right;
        } /* check end */
     pt = result;
     return TRUE;
    } // CMonitors::RightLimitFrom

To test these, I created a button on my test program.  When the button is pressed, four projections are made from the button position to indicate the maximum extension point. The low-resolution image shown below illustrates what I display.  In order to be able to tell that the actual end is visible, I put a red end on each of the four projections

But notice that there are the 176 missing pixels.  If I move the test program lower on the left, I get a different picture:

Observe that in this case, the rightmost limit is now the right edge of the left monitor, rather than the right edge of the right monitor.

The search for the bottommost limit BottomLimitFrom, is the same except it runs on the Y-axis. which it searches top-to-bottom.

The implementation of LeftLimitFrom and TopLimitFrom work the same, except they scan the arrays from right-to-left or bottom-to-top.

Doing the Combo Dropdown

Those first pictures don't tell the whole story.  I discovered, for example, that it is not sufficient to ask for the dropdown width (CB_GETDROPPEDWIDTH, CComboBox::GetDroppedWidth) because these are misleading.  The dropped width is insufficient, because it doesn't account for the height of the dropdown.  Using it alone means that it is possible to get the error shown below.  I have emphasized the error by hand-retouching the screen snapshot to show, in hatched yellow, the hidden part of the dropped rectangle.

Obviously, I need to take the full dropped rectangle into account.  So I tried using CB_GETDROPPEDCONTROLRECT (actually,  CComboBox::GetDroppedControlRect),  and it produced exactly the same result!  Well, not quite.  The first time I dropped the combo box down when it was in a position to be partially obscured by the other window, it got it wrong, just like above.  But when I tried it a second time, it got it right.  Similarly, if I moved the combo box up so it would not be obscured, the first time I dropped it down, I got the wrong answer again:

where I have hand-retouched the dropdown box to be in yellow so the error is obvious.  Dropping down the box a second time then produced the correct result:

The solution I used to get this right applies only to Windows XP, because it is the first version of the operating system to support the GetComboBoxInfo API.  This API gives me an HWND to the ListBox itself.  This gives me the ability to get the current combo box dropdown size.  But the combo box position, as returned by GetDroppedRect, is erroneous.  What I have to do is take the height of the rectangle, and add it to the bottom position of the control in its current position.  What I have deduced is that the dropdown is left, in terms of its rectangle, in the position it formerly occupied, even though the combo box itself has changed screen position when the application is moved! 

This works up to the point where the dropdown list will be moved above the combo box because the combo box is too close to the bottom of the screen.

Coping with Windows Versions

This project was created under VS.NET 2003.  However, it required hand-editing the stdafx.h file, because the tools still assume that I'm running on NT4 with IE4.  The following lines had to be changed:

#ifndef WINVER				// Allow use of features specific to Windows 95 and Windows NT 4 or later.
#define WINVER 0x0501		// Change this to the appropriate value to target Windows 98 and Windows 2000 or later.
#endif

#ifndef _WIN32_WINNT		// Allow use of features specific to Windows NT 4 or later.
#define _WIN32_WINNT 0x501		// Change this to the appropriate value to target Windows 98 and Windows 2000 or later.
#endif						

#ifndef _WIN32_WINDOWS		// Allow use of features specific to Windows 98 or later.
#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later.
#endif

#ifndef _WIN32_IE			// Allow use of features specific to IE 4.0 or later.
#define _WIN32_IE 0x0500	// Change this to the appropriate value to target IE 5.0 or later.
#endif 

If all of these are not changed, you get an error that the symbol CCM_SETWINDOWTHEME is not defined.

Feeling Out Of Sorts?

To do this, I needed to sort the array.  The problem with qsort is that it must be a static method, which means it cannot access the members of the monitors class.  Because I'm doing the sort vectors as "views" on the array of MONITORINFOEX objects, I need to access those objects.

In an ideal C++ environment, the qsort would probably take an additional LPVOID parameter for whatever purpose we want.  But we live in a far-than-ideal world, and we don't have this option.  I could use global variables, but this would not be multithread-safe, which makes me nervous.  So I decided to implement the classic Bubble Sort.  Wow, I can hear you saying, why would he use an n2 sort?  He's supposed to understand this stuff!  But let's look at the other side of the problem.  "Optimizations", particularly in algorithm design, serve the purpose of improving the computation while maintaining the correctness. So let's look at the typical computations.  Bubble sort is O(n2) in the number of elements, and qsort is O(n * log2 n)

n 1 2 3 4 5 6 7 8 9
O(n2) 1 4 9 16 25 36 49 64 81
log2 n 0 1 1.585 2 2.322 2.585 2.807 3 3.1699
O(n * log2 n 1 2 4.755 8 11.660 17.370 19.649 24 28.53
Performance factor 1 2 1.89 2.00 2.14 2.07 2.49 2.67 2.84

Note the "performance factor", for 9 monitors, is only three times worse using Bubble Sort than qsort.  Big deal.  Have you ever actually changed your multimonitor layout?  We're talking microseconds of performance here, perhaps single-digit microseconds! And this cost is incurred only when Initialize is called, and that only happens once per CMonitors struct at program initialization, and once per WM_DISPLAYCHANGE message.  Yet when I rearrange my monitors, there are seconds of screen flashing while the monitors rearrange their layouts and generate dozens of WM_PAINT messages to the various applications.  Therefore, the use of Bubble Sort is irrelevant here.  It is easier to use than qsort because I can write it as a non-static class method, and hence have access to the other class members, which I need to have access to in order to actually sort the data.

download.gif (1234 bytes)

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright 2000, The Joseph M. Newcomer Co. All Rights Reserved
Last modified: May 14, 2011