C/C++ Q & A

Paul DiLascia

Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows++: Writing Reusable Code in C++ (Addison-Wesley, 1992).

Click to open or copy the TRAYTEST project files.

Click to open or copy the TRACEWIN project files.

Click to open or copy the UPDTENMD project files.

QI noticed that some programs install an icon in the Windows® 95 task bar, on the right edge near the clock. For example, the System Agent that comes with the Windows 95 Plus! pack installs an icon. Does MFC have a class that will let me add my own icon to the task bar?

Bruce Eddington

ANo, but it's easy to write your own. Putting your own icon in the Windows 95 "system tray" is pretty straightforward. (In fact, you can do it from Visual Basic, as Josh Trupin detailed in last month's Visual Programmer column.) I'll teach you C++ folks how to do it this go-round. One function, Shell_NotifyIcon, is all you need. Shell_NotifyIcon is pretty simple. The first argument is a code (NIM_ADD, NIM_MODIFY, or NIM_DELETE) that says whether you want to add, modify, or delete an icon from the tray. The second and only other argument is a pointer to a NOTIFYICONDATA struct.


 // (From SHELLAPI.H)
typedef struct _NOTIFYICONDATA {
DWORD cbSize; // sizeofstruct,youmustset
HWND hWnd; // HWND sending notification
UINT uID; // IDoficon(callbackWPARAM)
UINT uFlags; // see below
UINT uCallbackMessage; // sent to your wndproc
HICON hIcon; // handle of icon
CHAR szTip[64]; // tip text
} NOTIFYICONDATA;

// uFlags
#define NIF_MESSAGE 0x1 // uCallbackMessage is valid
#define NIF_ICON 0x2 // hIcon is valid
#define NIF_TIP 0x4 // szTip is valid

hWnd is a handle to your window that "owns" the icon. uID can be any ID you like that identifies your tray icon (in case you have more than one). Typically, you'll use its resource ID. hIcon can be a handle to any icon, including predefined system icons like IDI_HAND, IDI_QUESTION, IDI_EXCLAMATION, or IDI_WINLOGO, the Windows logo.

Displaying icons is nice, but what's really fun are events. To receive notification when the user moves the mouse over or clicks on your tray icon, you can set uCallbackMessage to your very own message ID, and set the NIF_MESSAGE flag. When the user moves or clicks the mouse over the icon, Windows will call your window proc with hWnd equal to your window handle, specified in hWnd; messageID is the value you specified in uCallbackMessage; wParam is the value you specified as uID; and lParam is a mouse event (such as WM_LBUTTONDOWN).

Shell_NotifyIcon is short, simple, and sweet. But like most Windows API functions, it's a little clunky and assemblerish. So I encapsulated it in a C++ class, CTrayIcon (see Figure 1). CTrayIcon hides NOTIFYICONDATA, message codes, flags, and all that rot. It presents a more programmer-friendly interface to tray icons. But CTrayIcon is more than just a wrapper for Shell_NotifyIcon—it's a miniframework. It enforces the correct user interface behavior for tray icons, as per the Windows Interface Guidelines for Software (available on MSDN). Here's the executive sumary:

Figure 1 CTrayIcon

TRAYTEST.H


 ////////////////////////////////////////////////////////////////
// TRAYTEST 1996 Microsoft Systems Journal.
// See TRAYTEST.CPP for description of program.
//
#include "resource.h"

class CMyApp : public CWinApp {
public:
virtual BOOL InitInstance();
//{{AFX_MSG(CMyApp)
afx_msg void OnAppAbout();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

TRAYTEST.CPP


 ////////////////////////////////////////////////////////////////
// TRAYTEST 1996 Microsoft Systems Journal.
// If this program works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
//
// TRAYTEST illustrates how to use CTrayIcon.
// All the activity takes place in MainFrm.cpp.

#include "stdafx.h"
#include "TrayTest.h"
#include "mainfrm.h"

CMyApp theApp;

BEGIN_MESSAGE_MAP(CMyApp, CWinApp)
//{{AFX_MSG_MAP(CMyApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

BOOL CMyApp::InitInstance()
{
// Create main frame window (don't use doc/view stuff)
//
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
pMainFrame->ShowWindow(SW_HIDE);
pMainFrame->UpdateWindow();
m_pMainWnd = pMainFrame;
OnAppAbout();
return TRUE;
}

void CMyApp::OnAppAbout()
{
CDialog(IDD_ABOUTBOX).DoModal();
}

MAINFRM.H


 #include "trayicon.h"

//////////////////
// Main frame for TRAYTEST.
//
class CMainFrame : public CFrameWnd {
public:
CMainFrame();
virtual ~CMainFrame();
protected:
DECLARE_DYNAMIC(CMainFrame)
CStatusBar m_wndStatusBar;

CTrayIcon m_trayIcon; // my tray icon
CEdit m_wndEdit; // to display tray notifications
int m_iWhichIcon; // 0/1 which HICON to use
BOOL m_bShutdown; // OK to terminate TRAYTEST
BOOL m_bShowTrayNotifications; // display info in main window

//{{AFX_MSG(CMainFrame)
afx_msg LRESULT OnTrayNotification(WPARAM wp, LPARAM lp);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnToggleIcon();
afx_msg void OnViewClear();
afx_msg void OnViewNotifications();
afx_msg void OnUpdateViewClear(CCmdUI* pCmdUI);
afx_msg void OnUpdateViewNotifications(CCmdUI* pCmdUI);
afx_msg void OnClose();
afx_msg void OnAppOpen();
afx_msg void OnAppSuspend();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

MAINFRM.CPP


 ////////////////////////////////////////////////////////////////
// Main frame window implementation
//
#include "stdafx.h"
#include "TrayTest.h"
#include "mainfrm.h"

// Message ID used for tray notifications
#define WM_MY_TRAY_NOTIFICATION WM_USER+0

IMPLEMENT_DYNAMIC(CMainFrame, CFrameWnd)

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_MESSAGE(WM_MY_TRAY_NOTIFICATION, OnTrayNotification)
ON_WM_CREATE()
ON_COMMAND(ID_VIEW_CLEAR, OnViewClear)
ON_COMMAND(ID_TOGGLE_ICON, OnToggleIcon)
ON_COMMAND(ID_VIEW_NOTIFICATIONS, OnViewNotifications)
ON_UPDATE_COMMAND_UI(ID_VIEW_CLEAR, OnUpdateViewClear)
ON_UPDATE_COMMAND_UI(ID_VIEW_NOTIFICATIONS, OnUpdateViewNotifications)
ON_WM_CLOSE()
ON_COMMAND(ID_APP_OPEN, OnAppOpen)
ON_COMMAND(ID_APP_SUSPEND, OnAppSuspend)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

static UINT BASED_CODE indicators[] = {
ID_SEPARATOR, // status line indicator
};

CMainFrame::CMainFrame() : m_trayIcon(IDR_TRAYICON)
{
m_bShowTrayNotifications = TRUE;
m_bShutdown = FALSE;
}

CMainFrame::~CMainFrame()
{
}

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return -1;

if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT)))
return -1; // fail to create

// Create child edit control for displaying messages
CRect rc;
if (!m_wndEdit.Create(
WS_VISIBLE|WS_CHILD|WS_VSCROLL|ES_MULTILINE|ES_READONLY, rc, this,
AFX_IDW_PANE_FIRST))

return -1;

// Set up tray icon
m_trayIcon.SetNotificationWnd(this, WM_MY_TRAY_NOTIFICATION);
m_iWhichIcon = 1;
m_trayIcon.SetIcon(IDI_MYICON);

return 0;
}

//////////////////
// Close window. Unless we are shutting down, just hide it.
//
void CMainFrame::OnClose()
{
if (m_bShutdown)
CFrameWnd::OnClose();
else
ShowWindow(SW_HIDE);
}

//////////////////
// Handle notification from tray icon: display a message.
//
LRESULT CMainFrame::OnTrayNotification(WPARAM uID, LPARAM lEvent)
{
if (m_bShowTrayNotifications) {
static LPCSTR MouseMessages[] = { "WM_MOUSEMOVE",
"WM_LBUTTONDOWN", "WM_LBUTTONUP", "WM_LBUTTONDBLCLK",
"WM_RBUTTONDOWN", "WM_RBUTTONUP", "WM_RBUTTONDBLCLK",
"WM_MBUTTONDOWN", "WM_MBUTTONUP", "WM_MBUTTONDBLCLK" };

CString s;
s.Format("Tray notification: ID=%d, lEvent=0x%04x %s\r\n",
uID, lEvent, WM_MOUSEFIRST<=lEvent && lEvent<=WM_MOUSELAST ?
MouseMessages[lEvent-WM_MOUSEFIRST] : "(Unknown)");

m_wndEdit.SetSel(-1, -1); // end of edit text
m_wndEdit.ReplaceSel(s); // append string..
m_wndEdit.SendMessage(EM_SCROLLCARET); // ..and make visible
}

// let tray icon do default stuff
return m_trayIcon.OnTrayNotification(uID, lEvent);
}

////////////////////////////////////////////////////////////////
// Command handlers below.
//
void CMainFrame::OnViewClear()
{
m_wndEdit.SetWindowText("");
}

void CMainFrame::OnUpdateViewClear(CCmdUI* pCmdUI)
{
pCmdUI->Enable(m_wndEdit.GetLineCount() > 1 || m_wndEdit.LineLength() > 0);
}

void CMainFrame::OnToggleIcon()
{
m_iWhichIcon=!m_iWhichIcon;
m_trayIcon.SetIcon(m_iWhichIcon ? IDI_MYICON : IDI_MYICON2);
}

void CMainFrame::OnViewNotifications()
{
m_bShowTrayNotifications = !m_bShowTrayNotifications;
}

void CMainFrame::OnUpdateViewNotifications(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_bShowTrayNotifications);
}

void CMainFrame::OnAppOpen()
{
ShowWindow(SW_NORMAL);
SetForegroundWindow();
}

void CMainFrame::OnAppSuspend()
{
m_bShutdown = TRUE; // really exit
SendMessage(WM_CLOSE);
}

TRAYICON.H


 ////////////////////////////////////////////////////////////////
// CTrayIcon Copyright 1996 Microsoft Systems Journal.
//
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.

#ifndef _TRAYICON_H
#define _TRAYICON_H

////////////////
// CTrayIcon manages an icon in the Windows 95 system tray.
//
class CTrayIcon : public CCmdTarget {
protected:
DECLARE_DYNAMIC(CTrayIcon)
NOTIFYICONDATA m_nid; // struct for Shell_NotifyIcon args

public:
CTrayIcon(UINT uID);
~CTrayIcon();

// Call this to receive tray notifications
void SetNotificationWnd(CWnd* pNotifyWnd, UINT uCbMsg);

// SetIcon functions. To remove icon, call SetIcon(0)
//
BOOL SetIcon(UINT uID); // main variant you want to use
BOOL SetIcon(HICON hicon, LPCSTR lpTip);
BOOL SetIcon(LPCTSTR lpResName, LPCSTR lpTip)
{ return SetIcon(lpResName ?
AfxGetApp()->LoadIcon(lpResName) : NULL, lpTip); }
BOOL SetStandardIcon(LPCTSTR lpszIconName, LPCSTR lpTip)
{ return SetIcon(::LoadIcon(NULL, lpszIconName), lpTip); }

virtual LRESULT OnTrayNotification(WPARAM uID, LPARAM lEvent);
};

#endif

TRAYICON.CPP


 ////////////////////////////////////////////////////////////////
// CTrayIcon Copyright 1996 Microsoft Systems Journal.
//
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.

#include "stdafx.h"
#include "trayicon.h"
#include <afxpriv.h> // for AfxLoadString

IMPLEMENT_DYNAMIC(CTrayIcon, CCmdTarget)

CTrayIcon::CTrayIcon(UINT uID)
{
// Initialize NOTIFYICONDATA
memset(&m_nid, 0 , sizeof(m_nid));
m_nid.cbSize = sizeof(m_nid);
m_nid.uID = uID; // never changes after construction

// Use resource string as tip if there is one
AfxLoadString(uID, m_nid.szTip, sizeof(m_nid.szTip));
}

CTrayIcon::~CTrayIcon()
{
SetIcon(0); // remove icon from system tray
}

//////////////////
// Set notification window. It must created already.
//
void CTrayIcon::SetNotificationWnd(CWnd* pNotifyWnd, UINT uCbMsg)
{
// If the following assert fails, you're probably
// calling me before you created your window. Oops.
ASSERT(pNotifyWnd==NULL || ::IsWindow(pNotifyWnd->GetSafeHwnd()));
m_nid.hWnd = pNotifyWnd->GetSafeHwnd();

ASSERT(uCbMsg==0 || uCbMsg>=WM_USER);
m_nid.uCallbackMessage = uCbMsg;
}

//////////////////
// This is the main variant for setting the icon.
// Sets both the icon and tooltip from resource ID
// To remove the icon, call SetIcon(0)
//
BOOL CTrayIcon::SetIcon(UINT uID)
{
HICON hicon=NULL;
if (uID) {
AfxLoadString(uID, m_nid.szTip, sizeof(m_nid.szTip));
hicon = AfxGetApp()->LoadIcon(uID);
}
return SetIcon(hicon, NULL);
}

//////////////////
// Common SetIcon for all overloads.
//
BOOL CTrayIcon::SetIcon(HICON hicon, LPCSTR lpTip)
{
UINT msg;
m_nid.uFlags = 0;

// Set the icon
if (hicon) {
// Add or replace icon in system tray
msg = m_nid.hIcon ? NIM_MODIFY : NIM_ADD;
m_nid.hIcon = hicon;
m_nid.uFlags |= NIF_ICON;
} else { // remove icon from tray
if (m_nid.hIcon==NULL)
return TRUE; // already deleted
msg = NIM_DELETE;
}

// Use the tip, if any
if (lpTip)
strncpy(m_nid.szTip, lpTip, sizeof(m_nid.szTip));
if (m_nid.szTip[0])
m_nid.uFlags |= NIF_TIP;

// Use callback if any
if (m_nid.uCallbackMessage && m_nid.hWnd)
m_nid.uFlags |= NIF_MESSAGE;

// Do it
BOOL bRet = Shell_NotifyIcon(msg, &m_nid);
if (msg==NIM_DELETE || !bRet)
m_nid.hIcon = NULL; // failed
return bRet;
}

/////////////////
// Default event handler handles right-menu and doubleclick.
// Call this function from your own notification handler.
//
LRESULT CTrayIcon::OnTrayNotification(WPARAM wID, LPARAM lEvent)
{
if (wID!=m_nid.uID ||
(lEvent!=WM_RBUTTONUP && lEvent!=WM_LBUTTONDBLCLK))
return 0;

// If there's a resource menu with the same ID as the icon, use it as
// the right-button popup menu. CTrayIcon will interprets the first
// item in the menu as the default command for WM_LBUTTONDBLCLK
//
CMenu menu;
if (!menu.LoadMenu(m_nid.uID))
return 0;
CMenu* pSubMenu = menu.GetSubMenu(0);
if (!pSubMenu)
return 0;

if (lEvent==WM_RBUTTONUP) {

// Make first menu item the default (bold font)
::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);

// Display the menu at the current mouse location. There's a "bug"
// (Microsoft calls it a feature) in Windows 95 that requires calling
// SetForegroundWindow. To find out more, search for Q135788 in MSDN.
//
CPoint mouse;
GetCursorPos(&mouse);
::SetForegroundWindow(m_nid.hWnd);
::TrackPopupMenu(pSubMenu->m_hMenu, 0, mouse.x, mouse.y, 0,
m_nid.hWnd, NULL);

} else // double click: execute first menu item
::SendMessage(m_nid.hWnd, WM_COMMAND, pSubMenu->GetMenuItemID(0), 0);

return 1; // handled
}

CTrayIcon encapsulates all but the last of these rules. To show how it works, I wrote a little program. When you run TRAYTEST, it displays the dialog in Figure 2, then installs an icon in the system tray and goes into hiding. If you double-click the tray icon, TRAYTEST appears with a window that displays tray notifications as you move or click the mouse in the tray icon (see Figure 3).

Figure 2 TRAYTEST

Figure 3 TRAYTEST

To use CTrayIcon, the first thing you have to do is instantiate a CTrayIcon someplace where it'll live for the lifetime of the icon. TRAYTEST does it in its frame window.


 class CMainFrame : public CFrameWnd {
protected:
CTrayIcon m_trayIcon;
// my tray icon . . . };

When you instantiate a CTrayIcon, you must supply an ID. This is the one-and-only ID used for the lifetime of the icon, even if you later change the actual icon displayed. This ID is the one you'll get when mouse events happen. It need not be the resource ID of the icon; for TRAYTEST, it's IDR_TRAYICON, initialized by the CMainFrame constructor.


 CMainFrame::CMainFrame() : m_trayIcon(IDR_TRAYICON)
{ . . . }

To add the icon, call one of the overloaded SetIcon functions:


 m_trayIcon.SetIcon(IDI_MYICON);         //resource ID
m_trayIcon.SetIcon("myicon"); //resourcename
m_trayIcon.SetIcon(hicon); //HICON
m_trayIcon.SetStandardIcon(IDI_WINLOGO);//system icon

All these functions take an optional LPCSTR argument to use as the tip text, except for SetIcon(UINT uID) which looks for a string resource with the same uID as the tip. For example, TRAYTEST contains the line,


 // (In mainframe.cpp)
m_trayIcon.SetIcon(IDI_MYICON);

which also sets the tip, because TRAYTEST has a string with the same ID:


 // (In TRAYTEST.RC)
STRINGTABLE PRELOAD DISCARDABLE
BEGIN
IDI_MYICON"Double-clickthebananatoactivateTRAYTEST."
END

If you want to change the icon, you can call one of the SetIcon functions again with a different ID or HICON. CTrayTest will know to do NIM_MODIFY instead of NIM_ADD. The same function even works to remove the icon:


 m_trayIcon.SetIcon(0);//removeicon

CTrayIcon will translate this into NIM_DELETE. All those codes, all those flags replaced with a single overloaded function: isn't C++ great? Now, what about notifications and all that UI stuff I mentioned? To handle tray notifications, call CTrayIcon::SetNotificationWnd sometime before you set the icon, but after your window is created. The perfect place is in your OnCreate handler, which is where TRAYTEST does it.


 // Private message used for tray notifications
#define WM_MY_TRAY_NOTIFICATION WM_USER+0
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{ . . . // Notify me, please m_trayIcon.SetNotificationWnd(this,
WM_MY_TRAY_NOTIFICATION);
m_trayIcon.SetIcon(IDI_MYICON);
return 0;
}

Once you've registered yourself, you handle tray notifications in the normal message map manner.


 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_MESSAGE(WM_MY_TRAY_NOTIFICATION,
OnTrayNotification)
// (or ON_REGISTERED_MESSAGE)
END_MESSAGE_MAP()

LRESULT
CMainFrame::OnTrayNotification(WPARAM wp, LPARAM lp)
{ . . // display message . return m_trayIcon.OnTrayNotification(wp, lp); }

When your handler gets control, WPARAM is the ID you specified when you constructed the CTrayIcon; LPARAM is the mouse event (for example, WM_LBUTTONDOWN). You can do whatever you like when you get the notification; TRAYTEST displays information about the notification (see Figure 1, in MAINFRM.CPP, for details). When you're finished, call CTrayIcon::OnTrayNotification to perform default processing. This virtual (so you can override it) function implements the default UI behavior I mentioned earlier. In particular, it handles WM_LBUTTONDBLCLK and WM_RBUTTONUP. CTrayIcon looks for a menu with the same ID as the icon (for example, IDR_TRAYICON). If a menu with this ID exists, CTrayIcon displays it when the user right-clicks the icon; when the user double-clicks, CTrayIcon executes the first command in the menu. Only two things require further explanation.

Before displaying the menu, CTrayIcon makes the first item the default, so it appears bold. But how do you make a menu item bold? After several minutes of grep'ing in \MSDEV\INCLUDE\*.H, I discovered Get/SetMenuDefaultItem. There are no CMenu wrappers for these (as of MFC 4.0), so I had to call them directly.


 // Make first menu item the default (bold font)
::SetMenuDefaultItem(pSubMenu->m_hMenu, 0, TRUE);

Here 0 identifies the first menu item, and the TRUE specifies that the item ID is by position, not ID. How come there's no MFC wrapper for Get/SetMenuDefaultItem? The folks in Redmond explained that it's because these functions (and several others, like ::Get/SetMenuItemInfo, ::LoadImage, and so on) are not yet implemented in Windows NT(asof3.51).As soon as Windows NT gets its facelift, wrappers will be added to MFC.

The second item of interest in CTrayIcon::OnTrayNotification is what it does to display the context menu:


 ::SetForegroundWindow(m_nid.hWnd);  
::TrackPopupMenu(pSubMenu->m_hMenu, ...);

To make TrackPopupMenu work properly in the context of a tray, you must first call SetForegroundWindow on the window that owns the popup. Otherwise, the menu will not disappear when the user presses Escape or clicks the mouse outside the menu. This totally obscure behavior caused me hours of grief until I finally uncovered a problem report about it on MSDN. To find out more, search for Q135788 in MSDN. What I love most is that after describing the problem and workaround at great length, the PRB concludes by stating "This behavior is by design."

As you can see, CTrayIcon makes tray icons almost trivial. All TRAYTEST does to make its tray menu work are implement a notification handler that calls CTrayIcon::OnTrayNotification, and provide a menu with the same ID as the CTrayIcon.


 // (In TRAYTEST.RC)
IDR_TRAYICON MENU DISCARDABLE
BEGIN
POPUP "&Tray"
BEGIN
MENUITEM "&Open", ID_APP_OPEN
MENUITEM "&About TRAYTEST...",ID_APP_ABOUT
MENUITEM SEPARATOR
MENUITEM "&Suspend TRAYTEST", ID_APP_SUSPEND
END
END

When the user right-clicks the tray icon, CTrayIcon displays this menu (see Figure 4). And if the user double-clicks, CTrayIcon executes the first item: Open, which activates TRAYTEST (normally, it's hidden). To terminate TRAYTEST, you must select Suspend TRAYTEST from the tray menu. If you do File Exit or close the TRAYTEST main window, TRAYTEST doesn't really close, it merely hides itself.TRAYTESToverrides CMainframe::OnClose to provide this behavior (see Figure 1 MAINFRM.CPP).

Figure 4 TRAYTEST menu.

Before leaving you with CTrayIcon, some words of advice. I was almost afraid to answer this question, because I know everyone is going to run out and implement a tray icon as soon as they find out how. (I did.) It's just one of those things about being a programmer: as soon as there's some new little graphic gizmo, you have to try it out. Go ahead, do it. Add a tray icon to your app, stare at it, feel good, show it to your friends, have a party. Then take it out. Because most apps have no need for them. Unless you're writing some sort of system add-in like a replacement shell or improved print spooler or fontware DLL that loads invisibly, all tray icons will do is contribute to screen pollution. Figure 5 shows my nightmare vision of tray icons gone amok.

Figure 5 Tray icon abbundanza.

Update

A few issues back (December 1995), someone wrote asking how to save and restore the position of an MDI child window in a document. The answer I gave focused primarily on the mechanics of changing the frame window position at the appropriate time during loading. I showed how to override CDocTemplate::InitialUpdateFrame to get the saved position from the document, and then move the frame. As far as the actual moving was concerned, I did the obvious thing: GetWindowRect to get the position of the window for saving it, and MoveWindow to restore the saved position.

I recently noticed a problem with my TRACEWIN program (October 1995), which also uses GetWindowRect/MoveWindow to save and restore the window position across user sessions. Sometimes, when I ran TRACEWIN, it would come up invisible. TRACEWIN would appear in the Windows 95 task bar, but when I clicked to activate it, there was no window anywhere I could see. Moreover, I'd noticed the same behavior with a commercial app I have, but it wasn't reproducible so I ignored it.

A little investigation revealed what was going on. When I looked at the registry entries for my saved window position, the top left corner was at (x,y) coordinates (3000,3000). On an 800 x 600 display, that's somewhere roughly near Saturn. Likewise with my commercial app: the window was at (3000, 3000). How were these windows getting moved to outer space?

Well! As anyone knows who's ever used it, Windows 95 doesn't minimize windows the same way Windows 3.1 did. Instead, Windows 95 moves your window off the screen. When I shut down my computer while TRACEWIN was minimized, TRACEWIN saved these bogus coordinates and restored them the next time it ran. Once I figured out what was going on, I was able to consistently reproduce the bug by minimizing TRACEWIN, then closing it from the task bar without restoring it first. Sure enough, the next time I ran TRACEWIN, it came up in outer space.

I thought, OK, I won't save the position if the window is minimized. But I quickly realized that wouldn't do: what if the user changed the window size before minimizing it? The new size wouldn't get saved. So what was I supposed to do, save the window position every time the user minimized or maximized the window? Sheesh. And if this remember-the-position feature was really what it claimed to be, TRACEWIN should come up minimized if that's how it was the last time it was used. Suddenly I was contemplating several lines of code just to save the measly window position.

At this point I let out a long groan, because I suddenly remembered how you're supposed to do this stuff. There's a pair of little-known but really useful Windows API functions whose only roles in life are to manage the saving and restoring of window positions: GetWindowPlacement and SetWindowPlacement. Placement refers to the size and position of the window when it's in restored state (neither minimized nor maximized), whether it's minimized or not, and whether activating it should go to maximized state instead of restored state. This last situation arises when the user maximizes a window, then minimizes it. Activating the window should restore it to maximized state, not restored state—but the window should still remember its restored position in case the user clicks the restore button in the title bar. It's all very complicated, but GetSetWindowPlacement make a molehill out of the mountain. All you have to do is call GetWindowPlacement to get the placement, then call SetWindowPlacement to restore it.

GetWindowPlacement returns everything you could ever want to know about your window's placement in a struct called WINDOWPLACEMENT. You can save this information in the registry, then read it back when your program starts up, and call SetWindowPlacement to restore the window to the exact same placement as before, including all the bizarre minimize/maximize/restore semantics. It all works just like it should. The only thing Get/SetWindowPlacement doesn't do is read and write the information to your app's profile (registry key or INI file). Since that seems like such a natural thing to do, I wrote a little class that does it.

Figure 6 shows how I implemented CWindowPlacement, and Figure 7 shows how I modified TRACEWIN to use it. The implementation is brainless—the hardest part was thinking up registry key names for the items in WINDOWPLACEMENT. I could've used the MFC functions Get/WriteProfileBinary to save the whole struct in one fell swoop, but I wanted something more readable than hex, so if something ever goes wrong, I can always manually edit my profile with REGEDIT (see Figure 8). The only thing CWindowPlacement does that could be considered remotely clever is check that the restored position is in fact visible before restoring it, in case the user changes display resolution from something like 1024x68 to SVGA's lowly 800x600 (which is what I use, so I'll be laughing when I'm old and all my programmer friends have gone alexic from staring at too many tiny pixels).

Figure 6 CWindowPlacement

WINPLACE.H


 ////////////////////////////////////////////////////////////////
// CWindowPlacement 1996 Microsoft Systems Journal.
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.

////////////////
// CWindowPlacement reads and writes WINDOWPLACEMENT 
// from/to application profile and CArchive.
//
struct CWindowPlacement : public WINDOWPLACEMENT {
   CWindowPlacement();
   ~CWindowPlacement();
   
   // Read/write to app profile
   void GetProfileWP(LPCSTR lpKeyName);
   void WriteProfileWP(LPCSTR lpKeyName);

   // Save/restore window pos (from app profile)
   void Save(LPCSTR lpKeyName, CWnd* pWnd);
   BOOL Restore(LPCSTR lpKeyName, CWnd* pWnd);

   // Save/restore from archive
   friend CArchive& operator<<(CArchive& ar, const CWindowPlacement& wp);
   friend CArchive& operator>>(CArchive& ar, CWindowPlacement& wp);
};

WINPLACE.CPP


 ////////////////////////////////////////////////////////////////
// CWindowPlacement 1996 Microsoft Systems Journal.
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
#include "stdafx.h"
#include "winplace.h"

CWindowPlacement::CWindowPlacement()
{
   // Note: "length" is inherited from WINDOWPLACEMENT
   length = sizeof(WINDOWPLACEMENT);
}

CWindowPlacement::~CWindowPlacement()
{
}

//////////////////
// Restore window placement from profile key
BOOL CWindowPlacement::Restore(LPCSTR lpKeyName, CWnd* pWnd)
{
   GetProfileWP(lpKeyName);

   // Only restore if window intersets the screen.
   //
   CRect rcTemp, rcScreen(0,0,GetSystemMetrics(SM_CXSCREEN),
      GetSystemMetrics(SM_CYSCREEN));
   if (!::IntersectRect(&rcTemp, &rcNormalPosition, &rcScreen))
      return FALSE;

   pWnd->SetWindowPlacement(this);  // set placement
   return TRUE;
}

//////////////////
// Get window placement from profile.
void CWindowPlacement::GetProfileWP(LPCSTR lpKeyName)
{
   CWinApp *pApp = AfxGetApp();
   ASSERT_VALID(pApp);

   showCmd = pApp->GetProfileInt(lpKeyName, "wp.showCmd", showCmd);
   flags   = pApp->GetProfileInt(lpKeyName, "wp.flags", flags);

ptMinPosition.x = pApp->GetProfileInt(lpKeyName, "wp.ptMinPosition.x", 
      ptMinPosition.x);
ptMinPosition.y = pApp->GetProfileInt(lpKeyName, "wp.ptMinPosition.y",
      ptMinPosition.y);
ptMaxPosition.x = pApp->GetProfileInt(lpKeyName, "wp.ptMaxPosition.x", 
      ptMaxPosition.x);
ptMaxPosition.y = pApp->GetProfileInt(lpKeyName, "wp.ptMaxPosition.y",
      ptMaxPosition.y);

   RECT& rc = rcNormalPosition;  // because I hate typing
   rc.left  = pApp->GetProfileInt(lpKeyName, "wp.left",   rc.left);
   rc.right = pApp->GetProfileInt(lpKeyName, "wp.right",  rc.right);
   rc.top   = pApp->GetProfileInt(lpKeyName, "wp.top",    rc.top);
   rc.bottom= pApp->GetProfileInt(lpKeyName, "wp.bottom", rc.bottom);
}

////////////////
// Save window placement in app profile
void CWindowPlacement::Save(LPCSTR lpKeyName, CWnd* pWnd)
{
   pWnd->GetWindowPlacement(this);
   WriteProfileWP(lpKeyName);
}

//////////////////
// Write window placement to app profile
void CWindowPlacement::WriteProfileWP(LPCSTR lpKeyName)
{
   CWinApp *pApp = AfxGetApp();
   ASSERT_VALID(pApp);
   pApp->WriteProfileInt(lpKeyName, "wp.showCmd",         showCmd);
   pApp->WriteProfileInt(lpKeyName, "wp.flags",           flags);
   pApp->WriteProfileInt(lpKeyName, "wp.ptMinPosition.x", ptMinPosition.x);
   pApp->WriteProfileInt(lpKeyName, "wp.ptMinPosition.y", ptMinPosition.y);
   pApp->WriteProfileInt(lpKeyName, "wp.ptMaxPosition.x", ptMaxPosition.x);
   pApp->WriteProfileInt(lpKeyName, "wp.ptMaxPosition.y", ptMaxPosition.y);
   pApp->WriteProfileInt(lpKeyName, "wp.left",  rcNormalPosition.left);
   pApp->WriteProfileInt(lpKeyName, "wp.right", rcNormalPosition.right);
   pApp->WriteProfileInt(lpKeyName, "wp.top",   rcNormalPosition.top);
   pApp->WriteProfileInt(lpKeyName, "wp.bottom",rcNormalPosition.bottom);
}

// The ugly casts are required to help the VC++ 3.0 compiler decide which
// operator<< or operator>> to use. If you're using VC++ 4.0 or later, you 
// can delete this stuff.
//
#if (_MSC_VER < 1000)      // 1000 = VC++ 4.0
#define UINT_CAST (LONG)
#define UINT_CASTREF (LONG&)
#else
#define UINT_CAST
#define UINT_CASTREF
#endif

//////////////////
// Write window placement to archive
// WARNING: archiving functions are untested.
CArchive& operator<<(CArchive& ar, const CWindowPlacement& wp)
{
   ar << UINT_CAST wp.length;
   ar << UINT_CAST wp.flags;
   ar << UINT_CAST wp.showCmd;
   ar << wp.ptMinPosition;
   ar << wp.ptMaxPosition;
   ar << wp.rcNormalPosition;
   return ar;
}

//////////////////
// Read window placement from archive
// WARNING: archiving functions are untested.
CArchive& operator>>(CArchive& ar, CWindowPlacement& wp)
{
   ar >> UINT_CASTREF wp.length;
   ar >> UINT_CASTREF wp.flags;
   ar >> UINT_CASTREF wp.showCmd;
   ar >> wp.ptMinPosition;
   ar >> wp.ptMaxPosition;
   ar >> wp.rcNormalPosition;
   return ar;
}

Figure 7 TWMOD.CPP


 ///////////////////////////////////////////////////////////////////////////
// Changes to TRACEWIN that implement saving/restoring the window position.

// (From MainFrm.h)
class CMainFrame : public CFrameWnd {
public:
   static const char* REGKEY; // registry key for saved settings
                                     .
                                     .
                                     .
};

// (From MainFrm.cpp, TRACEWIN's main window)
const char* CMainFrame::REGKEY = "Settings";
void CMainFrame::OnClose() 
{
   // Save current settings in registry
   CWindowPlacement wp;
   wp.Save(REGKEY, this);
                                     .
                                     . (Save other stuff too)
                                     .
CFrameWnd::OnClose();
}

// (From App.cpp, TRACEWIN's app object)
BOOL CApp::InitInstance()
{
   // Save settings in registry, not INI file
   SetRegistryKey("MSJ");
                                     .
                                     . Create main window
                                     .
// Load window placement from profile
   CWindowPlacement wp;
   if (!wp.Restore(CMainFrame::REGKEY, pMainFrame))
      pMainFrame->ShowWindow(m_nCmdShow);
                                     .
                                     .
                                     .
   return TRUE;
}

Figure 8 Manually editing profiles with REGEDIT.

Have a question about programming in C or C++? You can mail it directly to C/C++ Q&A, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: C/C++ Q&A) via:


Internet:


Internet:

Paul DiLascia
72400.2702@compuserve.com

Eric Maffei
ericm@microsoft.com


This article is reproduced from Microsoft Systems Journal. Copyright © 1995 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.