Wednesday, April 18, 2007

Handling Multiple Monitors

One of the things I did when I got my new laptop is to set up multiple monitors. Developers I know and respect have raved about this for years, so I figured it was time to give it a try. Needless to say, I love it. I typically have browser and explorer windows open on the second monitor and keep the primary monitor for those things I live in all day long (VFP and Outlook, mostly). I'm more productive now that I'm not digging through stacks of windows and constantly moving or resizing one window or another.

However, one of the things I discovered today is that some of my applications don't respect the second monitor. For example, I have a class called SFPersistentForm that I drop on most of my forms. It saves the form size and position when the form is closed and restores it when the form is reopened, giving the user the experience they expect when working with that form. However, I discovered that if I opened a form and moved it to the second monitor then closed it, when I reopened it, the form displayed on the primary monitor instead (this was a form with Desktop set to .T. so it can exist outside the application's window).

I quickly found out why: the persistence code was trying to prevent the situation where the form may open outside the screen boundaries, making it invisible. The following code handled that:
Thisform.Width  = min(max(Thisform.Width,  0, Thisform.MinWidth), ;
_screen.Width)
Thisform.Height = min(max(Thisform.Height, 0, Thisform.MinHeight), ;
_screen.Height)
Thisform.Left = min(max(Thisform.Left, 0), _screen.Width - 50)
Thisform.Top = min(max(Thisform.Top, 0), _screen.Height - 50)
("- 50" is used to ensure the form doesn't start at exactly the right or bottom boundaries of the monitor, making it essentially invisible.)

There are actually two problems with this code. First, the reliance on _SCREEN assumed the form exists within _SCREEN; with a top-level form or one with Desktop set to .T., that's not necessarily the case. Second, even if _SCREEN is maximized, that only fits it in the current monitor. If the form is on the other monitor, _SCREEN's dimension are irrelevant.

I initially changed the code to:
if Thisform.Desktop or Thisform.ShowWindow = 2
lnWidth = sysmetric(1)
lnHeight = sysmetric(2)
else
lnWidth = _screen.Width
lnHeight = _screen.Height
endif Thisform.Desktop ...
Thisform.Width = min(max(Thisform.Width, 0, Thisform.MinWidth), ;
lnWidth)
Thisform.Height = min(max(Thisform.Height, 0, Thisform.MinHeight), ;
lnHeight)
Thisform.Left = min(max(Thisform.Left, 0), lnWidth - 50)
Thisform.Top = min(max(Thisform.Top, 0), lnHeight - 50)
However, it turns out that SYSMETRIC() only returns values for the primary monitor. So, I changed those two statements to:
declare integer GetSystemMetrics in Win32API integer
#define SM_CXVIRTUALSCREEN 78 && Virtual Width
#define SM_CYVIRTUALSCREEN 79 && Virtual Height
lnWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN)
lnHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN)
Now the form reopens in the exact same size and position, including the monitor it was on.

Update: As I posted in a comment, this code correctly handle the situation where the form was originally placed on a second monitor but now that monitor doesn't exist. However, it didn't handle the case where the secondary monitor is on the left, so the starting X position is negative. See the code in the comment to deal with that.

4 comments:

Jamie Osborn said...

You should try out UltraMon. I posted about it recently - http://flynnosborn.com/jamie/blog/?p=14

I find it a really great addition when using multiple monitors.

(I'm not a salesman, I'm a VFP developer).

Doug Hennig said...

Thanks for the comment, Jamie. I'll check that out.

Bud said...

How does the form react if it was last opened on the 2nd monitor but now your working without a 2nd monitor? I have seen applications that will open the form where the 2nd monitor would be but you can't see it. To make matters worse the form didn't support the move keyboard short cut.

Doug Hennig said...

Yes, it handles that no problem because the sysmetric values will change. However, in thinking about this, it doesn't handle the case where the secondary monitor is on the left, so the starting X position is negative. I changed the code to the following to deal with that:

if Thisform.Desktop or Thisform.ShowWindow = 2
declare integer GetSystemMetrics in Win32API integer
#define SM_XVIRTUALSCREEN 76 && virtual left
#define SM_YVIRTUALSCREEN 77 && virtual top
#define SM_CXVIRTUALSCREEN 78 && virtual width
#define SM_CYVIRTUALSCREEN 79 && virtual height
lnMaxLeft = GetSystemMetrics(SM_XVIRTUALSCREEN)
lnMaxTop = GetSystemMetrics(SM_YVIRTUALSCREEN)
lnMaxWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN)
lnMaxHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN)
else
lnMaxLeft = 0
lnMaxTop = 0
lnMaxWidth = _screen.Width
lnMaxHeight = _screen.Height
endif Thisform.Desktop ...
Thisform.Width = min(max(Thisform.Width, 0, Thisform.MinWidth), ;
lnMaxWidth)
Thisform.Height = min(max(Thisform.Height, 0, Thisform.MinHeight), ;
lnMaxHeight)
do case

* If we're past the left edge, move it to the left edge.

case Thisform.Left < lnMaxLeft
Thisform.Left = lnMaxLeft

* If we're past the right edge of the screen, move it to the right edge.

case Thisform.Left + Thisform.Width > lnMaxWidth + lnMaxLeft
Thisform.Left = lnMaxWidth + lnMaxLeft - Thisform.Width
endcase
do case

* If we're past the top edge, move it to the top edge.

case Thisform.Top < lnMaxTop
Thisform.Top = lnMaxTop

* If we're past the bottom edge of the screen, move it to the bottom edge.

case Thisform.Top + Thisform.Height > lnMaxHeight + lnMaxTop
Thisform.Top = lnMaxHeight + lnMaxTop - Thisform.Height
endcase