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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read