Saving Window Placement Information

Whenever I start the average Windows application, it shows up at some arbitrary location on my screen. In many cases, my window positions have been very carefully set. I don't want to reset them every time. So I decided it would be a Good Idea if every program I wrote from now on kept track of its size and position (unless it either can't be resized or doesn't have a window). In the process of doing this, I made so many mistakes that I thought it would be another Good Idea if I told other people what those mistakes were, why they were mistakes, and how to avoid them.

Saving the position isn't hard to do. All I have to do is get the application's main frame window position, and save that to the registry. When I start the application, I read the last known position of the window, and put it back. This is actually really simple, but I made it rather more complex than it needed to be the first time through.

Remember, until you get to "fixing the problems", the code you are reading is WRONG. It's BAD CODE. Do not write code like this. If you have already written code like this, either fix it, hide it, or blame it on an intern. I actually started out with worse code, since I used "extern CMyApp *theApp;" in most of my classes. In the old code, this persists as the variable name, but had been changed some time before. I just wanted to remind people "don't do that".

What Went Wrong

Initially, I wanted to save two major pieces of information.

First, I saved the window rectangle, acquired with GetWindowRect():

void CMainFrame::OnMove(int cx, int cy) 
{
 CFrameWnd::OnMove(x, y);
 CRect r;
 CMyApp *theApp=(CMyApp *)AfxGetApp();

 this->GetWindowRect(&r);
 theApp->SetUpperLeft(r.left,r.top);	
 theApp->SetLowerRight(r.right-r.left,r.bottom-r.top);	
}
Then, I wanted to save the maximised state of the window:
void CMainFrame::OnSize(UINT nType, int cx, int cy) 
{
 CFrameWnd::OnSize(nType, cx, cy);
 CRect r;
 CMyApp *theApp=(CMyApp *)AfxGetApp();

 if(nType==SIZE_MAXIMIZED) 
 {
  theApp->SetMaximized(1);
 }
 else
 {
  theApp->SetMaximized(0);
  this->GetWindowRect(&r);
  theApp->SetUpperLeft(r.left,r.top);	
  theApp->SetLowerRight(r.right-r.left,
                        r.bottom-r.top);
 }
}
In the application object, I implemented this rather simply with five member variables:
void CMyApp::SetMaximized(int max)
{
 m_wnd_max=max;
}

void CMyApp::SetLowerRight(int width, int height)
{
 if(m_wnd_max==0)
 {
  m_wnd_right=width;
  m_wnd_bottom=height;
 }
}

void CMyApp::SetUpperLeft(int left,int top)
{
 if(m_wnd_max==0)
 {
  m_wnd_left=left;
  m_wnd_top=top;
 }
}
And then I stuck this information into the registry when I was leaving the application:
int CMyApp::ExitInstance() 
{
 WriteProfileInt("Window","Maximize",m_wnd_max);
 WriteProfileInt("Window","Top",m_wnd_top);
 WriteProfileInt("Window","Left",m_wnd_left);
 WriteProfileInt("Window","Right",m_wnd_right);
 WriteProfileInt("Window","Bottom",m_wnd_bottom);
 
 return CWinApp::ExitInstance();
}
There are a myriad problems here. First, and most importantly, the message handling is stupid. Incredibly stupid. I track the position of the window every time it changes, but I only save it when I exit. Why didn't I just get the position once, immediately before the window was destroyed? I should have done this:
void CMainFrame::OnClose() 
{
 CRect r;
 CMyApp *theApp=(CMyApp *)AfxGetApp();

 this->GetWindowRect(&r);
 theApp->SetUpperLeft(r.left,r.top);	
 theApp->SetLowerRight(r.right-r.left,r.bottom-r.top);	

 CFrameWnd::OnClose();
}
But now I don't get the maximised state. Not so it really matters, since this is how I load the information in my app's InitInstance():
m_wnd_max=GetProfileInt("Window","Maximized",0);
m_wnd_top=GetProfileInt("Window","Top",0);
m_wnd_left=GetProfileInt("Window","Left",0);
m_wnd_right=GetProfileInt("Window","Right",500);
m_wnd_bottom=GetProfileInt("Window","Bottom",300);
And then I did this to initialise the window:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 CMyApp *theApp=(CMyApp *)AfxGetApp();

 cs.x=theApp->m_wnd_left;
 cs.y=theApp->m_wnd_top;
 cs.cx=theApp->m_wnd_right;
 cs.cy=theApp->m_wnd_bottom;
 
 if(theApp->m_wnd_max) 
  cs.style|=WS_MAXIMIZE;

 if( !CFrameWnd::PreCreateWindow(cs) )
  return FALSE;

 return TRUE;
}
Stupid, stupid, stupid. All these cross-object calls are ridiculous. The design of this method is so incredibly stupid, I'm ashamed to post it, but if you're trying to do this you might very well have done the same thing. Let's look at how I do things in this dumb initial attempt:
  • CMyApp::InitInstance reads the window position
  • CMainFrame::PreCreateWindow sets the position from member variables of CMyApp
  • CMainFrame::OnMove calls two methods of CMyApp to set member variables
  • CMainFrame::OnSize calls three methods of CMyApp to set member variables
  • CMyApp::ExitInstance saves the window position

We've already established that to improve application performance and maintainability, we need to call back to CMyApp in CMainFrame::OnClose, but that doesn't know if the window's maximised. However, it doesn't even really matter! Look back up there at CMainFrame::PreCreateWindow() again:

	if(theApp->m_wnd_max) cs.style|=WS_MAXIMIZE;
The MSDN viewer made this mistake in some earlier releases. This does not maximise the window! It sets the window to the proper maximised dimensions, but the window is not actually maximised -- it merely fills the whole window. If you click the maximise button, the border disappears. If you click it again to restore the window, it simply redraws the border. And the original position of the window when it wasn't maximised has been lost! Clearly, this isn't what we wanted in the first place. So we need to address this.

Now I'll call your attention to the hardcoded defaults in the GetProfileInt() calls. That's exceptionally stupid. It breaks any window with a predefined size, and has to be modified for each application. The defaults ought to come from somewhere that makes more sense. At the very least, they should be coming from #defines, more preferably from enums, even better from const variables, but best of all from the O/S... which knows a whole hell of a lot more about this user's system than I did when I wrote the application in the first place.

Any object-oriented design aficionados will also note that we called SetUpperLeft() and SetLowerRight() to write to the members of CMyApp, but then read them directly when we restored the window size. That means they're public members. So why did we call (or write) these access methods in the first place? And SetLowerRight() doesn't take the coordinates of the lower right corner of the window, either -- it takes the width and height of the window. It would be okay if it calculated the width and height to save them, but that has to be done before you call it. The name of the function is wrong! In fact, so are the names of the member variables and registry entries used to store it!

What a godawful piece of crap!

Fixing the Problems

Before we write any new code, we're going to turn to some philosophical issues. First, the initialisation of the application is the job of the application object. Reading and writing the configuration should all happen in CMyApp. That way, CMyApp is responsible for all the configuration variables, and when you want to know whether something is in the configuration you only have to examine CMyApp. From a coding style standpoint, we will furthermore go so far as to say that all configuration variables will be private and retrieved or updated only from public member functions. We will then go on to say that all member variable retrieval functions will be called "TYPE& CMyApp::GetVariable()" and mapped to a variable named m_variable of type TYPE. Similarly, we will say that functions to set such variables will be named "void CMyApp::PutVariable(TYPE& newval)". So any member variable accessible from external classes will be declared as follows:
private:
 TYPE m_variable;

public:
 const TYPE& GetVariable() const;
 void PutVariable(const TYPE& newval);
We will also provide two functions for the configuration: "void CMyApp::ReadConfig()" and "void CMyApp::WriteConfig()". Furthermore, we will divide the configuration into several groups, which will reside in specific keys. For any given subkey, we will provide two member functions: "void CMyApp::ReadKeyConfig()" and "void CMyApp::WriteKeyConfig()" where there is a subkey of the configuration named "Key". Currently, these will look like this:
void CMyApp::ReadConfig(void)
{
 ReadWindowConfig();
}

void CMyApp::WriteConfig(void)
{
 WriteWindowConfig();
}

void CMyApp::ReadWindowConfig(void)
{
 m_wnd_max=GetProfileInt("Window","Maximized",0);
 m_wnd_top=GetProfileInt("Window","Top",0);
 m_wnd_left=GetProfileInt("Window","Left",0);
 m_wnd_right=GetProfileInt("Window","Right",500);
 m_wnd_bottom=GetProfileInt("Window","Bottom",300);
}

void CMyApp::WriteWindowConfig(void)
{
 WriteProfileInt("Window","Maximize",m_wnd_max);
 WriteProfileInt("Window","Top",m_wnd_top);
 WriteProfileInt("Window","Left",m_wnd_left);
 WriteProfileInt("Window","Right",m_wnd_right);
 WriteProfileInt("Window","Bottom",m_wnd_bottom);
}
In CMyApp::InitInstance(), we can now simply set our registry key and add a ReadConfig() call. At this point, we have just about everything we need from an architecture point of view. However, we still have the problem of a flawed design, so before we move on to setting up the CMainFrame class, we'll address the design.

It turns out that GetWindowRect() is the wrong solution. What we really want here is a function called GetWindowPlacement(), which has a corresponding SetWindowPlacement() function. (Paul DiLascia wrote more about this in Microsoft Systems Journal in March of 1996.) These functions do everything we want done. These functions take a WINDOWPLACEMENT structure, which means we only need to pass a single reference around. Furthermore, we can call GetWindowPlacement() directly from CMyApp() through the m_pMainWnd member. So we can actually do this:

private:
 WINDOWPLACEMENT m_WP;

public:
 const WINDOWPLACEMENT& GetWP() const;
 void PutWP(const WINDOWPLACEMENT& newval);

Then we implement them as follows, along with new implementations of ReadWindowConfig() and WriteWindowConfig():

const WINDOWPLACEMENT& CMyApp::GetWP(void) const
{
 return m_WP;
}

void CMyApp::PutWP(const WINDOWPLACEMENT& newval)
{	
 m_WP=newval;
 m_WP.length=sizeof(m_WP); // validation ;)
}
	
void CMyApp::ReadWindowConfig(void)
{
 m_WP.length=sizeof(m_WP);
 m_WP.showCmd=GetProfileInt("Window","show",0);
 m_WP.flags=GetProfileInt("Window","flags",0);
 m_WP.ptMinPosition.x=GetProfileInt("Window","minposx",0);
 m_WP.ptMinPosition.y=GetProfileInt("Window","minposy",0);
 m_WP.ptMaxPosition.x=GetProfileInt("Window","maxposx",0);
 m_WP.ptMaxPosition.y=GetProfileInt("Window","maxposy",0);
 m_WP.rcNormalPosition.left=GetProfileInt("Window","left",0);
 m_WP.rcNormalPosition.top=GetProfileInt("Window","top",0);
 m_WP.rcNormalPosition.right=GetProfileInt("Window","right",0);
 m_WP.rcNormalPosition.bottom=GetProfileInt("Window","bottom",0);
}

void CMyApp::WriteWindowConfig(void)
{
 WriteProfileInt("Window","show",m_WP.showCmd);
 WriteProfileInt("Window","flags",m_WP.flags);
 WriteProfileInt("Window","minposx",m_WP.ptMinPosition.x);
 WriteProfileInt("Window","minposy",m_WP.ptMinPosition.y);
 WriteProfileInt("Window","maxposx",m_WP.ptMaxPosition.x);
 WriteProfileInt("Window","maxposy",m_WP.ptMaxPosition.y);
 WriteProfileInt("Window","left",m_WP.rcNormalPosition.left);
 WriteProfileInt("Window","top",m_WP.rcNormalPosition.top);
 WriteProfileInt("Window","right",m_WP.rcNormalPosition.right);
 WriteProfileInt("Window","bottom",m_WP.rcNormalPosition.bottom);
}
Now, here we have something of a catch-22. I'd like to read my entire configuration immediately after LoadStdProfileSettings(), so I can set the registry key and load the config all in one localised block of code. That would look nice and clean:
SetRegistryKey(_T("Darklock Communications"));

// Load standard INI file options (including MRU)
LoadStdProfileSettings(10);  

ReadConfig();
Unfortunately, this isn't going to work. In ReadWindowConfig(), we don't have any reasonable defaults. It makes sense to me that these defaults should come from what MFC thinks they ought to be, so I should get them from a call to m_pMainWnd's GetWindowPlacement() function. But until ProcessShellCommand() is called, m_pMainWnd is NULL. So I can't call anything in m_pMainWnd until after ProcessShellCommand().

But I also like to have a configuration setting that determines whether to load the file that was loaded when the application was last running. In order to do that, I need to tell the app to load this file instead of creating a new one, so I need to change the processing of cmdInfo in ProcessShellCommand(). This means some of my config has to be read before ProcessShellCommand(), and some of it has to be read afterward. This can't be helped, so we need to figure out how to address this issue -- specifically, which part of the configuration is a special case, and which part is normal.

On further examination, I determine that there are two varieties of configuration data: data that modifies document templates and load commands, and data that doesn't -- which is just about everything else. Since it makes things a lot easier to separate these functions into a small special-case group and a large generic group, the main ReadConfig() call should come after ProcessShellCommand(), and any configuration subkeys that need to modify the document template registration or the command processing will have to be specifically read beforehand. In my case, I stick all these under a subkey called "Init", and call ReadInitConfig() immediately after the "LoadStdProfileSettings()" call.

Not that it affects what we're doing here, but it affects your use of it later.

So now we go into ReadWindowConfig, and add in some reasonable defaults:

void CMyApp::ReadWindowConfig(void)
{
 WINDOWPLACEMENT wp;

 wp.length=sizeof(wp);
 m_pMainWnd->GetWindowPlacement(&wp);

 m_WP.length=sizeof(m_WP);
 m_WP.showCmd=GetProfileInt("Window","show",wp.showCmd);
 m_WP.flags=GetProfileInt("Window","flags",wp.flags);
 m_WP.ptMinPosition.x=GetProfileInt("Window","minposx",
 wp.ptMinPosition.x);
 m_WP.ptMinPosition.y=GetProfileInt("Window","minposy",
 wp.ptMinPosition.y);
 m_WP.ptMaxPosition.x=GetProfileInt("Window","maxposx",
 wp.ptMaxPosition.x);
 m_WP.ptMaxPosition.y=GetProfileInt("Window","maxposy",
 wp.ptMaxPosition.y);
 m_WP.rcNormalPosition.left=GetProfileInt("Window","left",
 wp.rcNormalPosition.left);
 m_WP.rcNormalPosition.top=GetProfileInt("Window","top",
 wp.rcNormalPosition.top);
 m_WP.rcNormalPosition.right=GetProfileInt("Window","right",
 wp.rcNormalPosition.right);
 m_WP.rcNormalPosition.bottom=GetProfileInt("Window","bottom",
 wp.rcNormalPosition.bottom);
}
And now, we add in the real meat of the matter, the validation. Who knows what may have happened since the last time we ran? Maybe the user went mucking about in the registry. Maybe the user changed screen resolutions. So we're going to check a few things.

In my initial cut at this, I used some code written by Paul DiLascia in the March, 1996 issue of Microsoft Systems Journal. This code initialised rcScreen to (0,0, GetSystemMetrics(SM_CXSCREEN),GetSystemMetrics(SM_CYSCREEN)). I'm sure this worked just fine for anyone who either kept the system taskbar on the bottom of the screen or set the taskbar to "autohide". Unfortunately, I keep my system taskbar on the right side of the screen, and I hate autohiding taskbars. So when I went to test the program by setting it partially offscreen, I dragged it off the right side of the screen and the code proceeded to stick it on the screen -- under the taskbar. The minimise, maximise, and close buttons, as well as the handy resize handle on the lower right, were covered. While this was only a minor annoyance, imagine if the application had been at the top of the screen and someone had the Microsoft Office toolbar up there with large icons enabled... concealing the entire title bar and menu. What we really need here is the ::SystemParametersInfo(SPI_GETWORKAREA) call, which in all fairness was either very new or not even in the SDK when Paul wrote the article in question.

CRect rcTemp;
CRect rcScreen(0,0,
               GetSystemMetrics(SM_CXSCREEN),
               GetSystemMetrics(SM_CYSCREEN));

// ::SystemParametersInfo(SPI_GETWORKAREA) gives us the screen space
// minus the system taskbar and application toolbars, which is what 
// we want our application to fit into.

::SystemParametersInfo(SPI_GETWORKAREA,0,&rcScreen,0);

// Normalise the existing rectangle, and force equality

rcTemp=m_WP.rcNormalPosition;
rcTemp.NormalizeRect();
m_WP.rcNormalPosition=rcTemp;

// See if any part of the window is on the screen

::IntersectRect(&m_WP.rcNormalPosition, &rcTemp, &rcScreen);

// if intersection==window, entire window is on screen

if(rcTemp!=m_WP.rcNormalPosition)
{	
 // some part of the window is off the screen

 // off the left edge
 if(rcTemp.left<rcScreen.left) 		
  rcTemp.OffsetRect(rcScreen.left-rcTemp.left,0);

 // off the right edge
 if(rcTemp.right>rcScreen.right) 	
  rcTemp.OffsetRect(rcScreen.right-rcTemp.right,0);

 // won't fit, shrink
 if(rcTemp.left<rcScreen.left) 		
  rcTemp.left=rcScreen.left;

 // off the top edge
 if(rcTemp.top<rcScreen.top) 		
  rcTemp.OffsetRect(0,rcScreen.top-rcTemp.top);

 // off the bottom edge
 if(rcTemp.bottom>rcScreen.bottom) 	
  rcTemp.OffsetRect(0,rcScreen.bottom-rcTemp.bottom);

 // won't fit, shrink
 if(rcTemp.top<rcScreen.top) 		
  rcTemp.top=rcScreen.top;

 m_WP.rcNormalPosition=rcTemp;
}
However, I think if the whole window is off the screen, something really weird is going on. The user has probably changed the screen resolution drastically since the last run, and the application's window size is undoubtedly far too large for the new resolution. There are really very few cases I can think of where moving the window from entirely offscreen to entirely onscreen will do what the user wants. In fact, I tend to think "window is completely off the screen" indicates that the registry information is likely to be corrupt to begin with. So I'm going to change that logic above a little:
if(::IntersectRect(&m_WP.rcNormalPosition, &rcTemp, &rcScreen))
{
 if(rcTemp!=m_WP.rcNormalPosition)
 {	
  // some part of the window is off the screen
  
  // off the left edge
  if(rcTemp.left<rcScreen.left) 		
   rcTemp.OffsetRect(rcScreen.left-rcTemp.left,0);

  // off the right edge
  if(rcTemp.right>rcScreen.right) 	
   rcTemp.OffsetRect(rcScreen.right-rcTemp.right,0);

  // won't fit, shrink
  if(rcTemp.left<rcScreen.left) 		
   rcTemp.left=rcScreen.left;

  // off the top edge
  if(rcTemp.top<rcScreen.top) 		
   rcTemp.OffsetRect(0,rcScreen.top-rcTemp.top);

  // off the bottom edge
  if(rcTemp.bottom>rcScreen.bottom) 	
   rcTemp.OffsetRect(0,rcScreen.bottom-rcTemp.bottom);

  // won't fit, shrink
  if(rcTemp.top<rcScreen.top) 		
   rcTemp.top=rcScreen.top;

  m_WP.rcNormalPosition=rcTemp;
 }
}
else // entire window is offscreen
{	
 m_WP=wp;
}
By using the initial defaults, we let MFC do the work of figuring out where the application goes. Now that we've validated the window's normal size and position, let's go a little further with this. In the m_WP.showCmd, we store the state of an application as minimised or maximised. If the application was minimised when it was closed, there are four user desires involved.

If the application is minimised in its normal running state, the user wants it to start (and end) minimised, so the application will be set to run minimised. If the application is not minimised in its normal running state, there are three reasons it might think it is supposed to be minimised. Either the user minimised it and closed it expecting it to remember it was minimised, the user closed it while it was minimised without expecting this, or the program was minimised during system shutdown. There is no way to distinguish between the final three cases. So here's our list of things to handle:

  • Program is set to start minimised: start minimised
  • Program was minimised and expected to start minimised: start minimised
  • Program was minimised and expected to start restored: start restored
  • Program was minimised and system was shut down: start restored
If the program is set to start minimised, we can check this by looking at the default settings MFC found on the window. Since it was set to start minimised, MFC would have minimised it. In the other three cases, we can't distinguish the user's expectations without asking him. We don't want to ask him "Do you want to run the application minimised?" -- that's just plain dumb. In two out of three cases, the answer is "no". So instead of annoying everyone with this stupid dialog box, we'll just assume the program is not supposed to start minimised. If the user ever gets frustrated, he has "run minimised" in the program's properties, so he's not overly inconvenienced.
if(m_WP.showCmd==SW_MINIMIZE
&& wp.showCmd!=SW_MINIMIZE)
{	// check default to see if this was requested
 m_WP.showCmd=SW_RESTORE;		// set restore flag
}
The next question is whether to do this on maximised windows, and the answer is NO. Starting a program maximised is something users commonly want to do. The basic rule of thumb on maximised applications is that you either have it maximised all the time, or you never maximise it in the first place. So if we're set to start maximised, we will simply trust that this is what the user wants. However, the user may have "start maximised" set in the shortcut, and he may have only just turned this option on. In that case, we will be set not to start maximised, but the user will have specifically asked us to start maximised -- and we'll be doing the wrong thing. So instead, we'll do the reverse, forcing maximise on if MFC thinks we're supposed to be maximised:
if(m_WP.showCmd!=SW_MAXIMIZE
&& wp.showCmd==SW_MAXIMIZE)
{	// check default to see if this was requested
 m_WP.showCmd=SW_MAXIMIZE;		// set maximise flag
}
And now that we've done all this work, we'll simply go back to InitInstance() and call SetWindowPlacement() to put the window where it belongs...
	m_pMainWnd->SetWindowPlacement(&m_WP);
That concludes all our processing needs in this section of the configuration. The only thing left to do now is save the configuration. This is incredibly simple; we simply tell CMainFrame to update the application's window information:
void CMainFrame::OnClose()
{
 CMyApp *app=(CMyApp *)AfxGetApp();
 WINDOWPLACEMENT wp;

 this->GetWindowPlacement(&wp);
 app->PutWP(wp);

 CFrameWnd::OnClose();
}
And finally, in CMyApp::ExitInstance(), we add a call to WriteConfig():
int CMyApp::ExitInstance() 
{
 WriteConfig();
 return CWinApp::ExitInstance();
}
WriteConfig will, of course, include a call to WriteWindowConfig(). That's it, pretty much. But it's an awful lot. In the process of getting here, we've defined a complete configuration mechanism which can be extended easily to cover all of your app's needs. While the code changes above have been made on an SDI application, the demo project is MDI just to prove that the process is identical (excepting the base class of CMainFrame).

Downloads

Download source - 2 Kb
Download demo project - 18 Kb


Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • A help desk is critical to the operations of an IT services business. As a centralized intake location for technical issues, it allows for a responsive and timely solution to get clients and their staff back to business as usual. In addition to handling immediate IT issues, a help desk performs several proactive tasks to ensure clients' IT systems remain operational and downtime is minimized. Thus, utilizing a help desk and following best practices can improve the productivity, efficiency and satisfaction of …

  • The latest release of SugarCRM's flagship product gives users new tools to build extraordinary customer relationships. Read an in-depth analysis of SugarCRM's enhanced ability to help companies execute their customer-facing initiatives from Ovum, a leading technology research firm.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds