December 1996
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).
Q I'm writing a Single Document Interface (SDI) application using MFC. I realize Microsoft is pushing a document-centric world, but there are still people who run the application and then pick which document they want to open (silly people!).
The problem I have is that, whenever the application starts up, MFC automatically does a File New operation to create a blank document. In my case, File New is fairly expensive; it takes a lot of time, may have to connect to a Web site, and requires the user to pick a document template to use. I want to avoid going through the File New process unless the user actually wants a new document. What would be a reasonable way to do this? Keep in mind that I still want to let Windows 95¨ users do New XYZ Document from many places such as the desktop (right button popup menu) and Windows Explorer (File New command).
Ed Silky
Calico Technology
AIn case you're experiencing a sense of deja vu all over again, it's not because you've slipped into a wrinkle in the time-space continuum that warped you backward a month. This is indeed the same question that appeared in last month's column. Am I so desperate for questions that I have to recycle old ones?
No. Ed's question appears again because last month I only answered half of it. In November, I showed you how to create an SDI app that starts out with an empty, uninitialized document-a view that paints an empty frame, with all document commands disabled until the user explicitly invokes File New. This simulates a no-document condition but keeps MFC happy by giving it the CDocument object it so desperately needs. The cost of initializing the document is deferred until the user explicitly invokes File New from the menu.
This month, I'll answer the second part of Ed's question: how do you deal with File New operations that come from the shell? Just so everyone knows what we're talking about here, Windows 95 lets you create new documents in several places. For example, you can right-click from your desktop or any folder to get the menu in Figure 1. Or you can choose File New from the Windows Explorer (see Figure 2). In both figures, Scribble Drawing is selected. Scribble is the drawing program that comes with the MFC tutorial that I modified last month to implement the first part of Ed's question. How does Windows 95 know to list Scribble Drawing in its File New menu?
Figure 1 Shell New menu
Figure 2 File New from Windows Explorer
Do the words "system registry" mean anything to you? Figure 3 shows the registry key for .SCB (Scribble) files with the special subkey HKEY_CLASSES_ROOT\.SCB\ShellNew, which has a string value NullFile set to "" (the empty string). When Windows sees that, it adds Scribble Drawing to its File New menu, and when the user selects Scribble Drawing, Windows launches the program in HKEY_CLASSES_ROOT\Scribble.Document\shell\open\command, which is something like "C:\fumble\mumble\bumble \scribble.exe %1". The name Scribble Drawing comes from the entry HKEY_CLASSES_ROOT\Scribble.Document, whose value is "Scribble Drawing". Windows 95 knows to look at Scribble.Document because that's the value of HKEY_CLASSES_ROOT\.SCB. Got it?
Figure 3 Scribble registry key entries
If you're wondering how all this stuff got in your registry, MFC put it there. One of the functions Scribble calls from CScribbleApp::InitInstance is RegisterShellFileTypes, a CWinApp function that adds a bunch of junk to your registry. (Figure 4 shows the whole kit and caboodle.) MFC gets the names from the string resource associated with the document template. When you create a document template you give it an ID like IDR_MAINFRAME or IDR_MYDOCTYPE, and the ID identifies a bunch of resources, including a string. In SCRIBBLE.RC, it looks like this:
STRINGTABLE PRELOAD DISCARDABLE
BEGIN
IDR_MAINFRAME "Scribble\nuntitled\nScribbble\n"
"Scribble Files (*.scb)\n.SCB\n"
"Scribble.Document\nScribble Drawing"
END
Whew! This unwieldy string comprises seven substrings, separated by newlines (\n).
Figure 5 shows how MFC uses all the different pieces. Notice in particular the last two substrings, "Scribble.Document" and "Scribble Drawing". These are the substrings MFC uses for the registry. When you call RegisterShellFileTypes, MFC loops over all of your document templates, extracts the substrings for each one, and creates the registry entries that resemble Figure 4. If you call RegisterShellFileTypes with TRUE as the argument (to signify that you're a Windows 95-based app), MFC adds some other stuff, including the ShellNew key with NullFile="".
Figure 5 Components of MFC doc string
So to add your app to the Windows File New menu, all you have to do is call RegisterShellFileTypes from your app's InitInstance function. But now you can see that I have a little problem. Last month I modified the Scribble program to start up empty instead of creating a new document. This is fine standalone, but if the user has invoked a command called File New from the desktop, Explorer, or a shell folder, then presumably he or she really wants to create a new file and Scribble should do just that-not make the user invoke File New again, this time from the Scribble menu.
What you need is a way to distinguish between when a user invokes Scribble from the Start menu or MS-DOS¨ command line (yes, some of us still use it), and the situation where Windows 95 invokes it on behalf of a user trying to create a new document from the shell.
In fact, there is a way-I mentioned the NullFile value. There are others. For example,
HKEY_CLASSES_ROOT\.SCB\ShellNew
FileName="foo.scb"
tells Windows 95, "to create a new Scribble doc, open the file foo.scb," which it does by running "scribble.exe foo.scb." You then have to install foo.scb in the appropriate directory, which is \windows\shellnew, one of the many secret (read: hidden) directories Windows 95 keeps around for just such purposes.
So one way to solve Ed's problem is to create a new, empty document and use the FileName value. Another way is to give Windows an explicit command to use:
HKEY_CLASSES_ROOT\.SCB\ShellNew
Command="SCRIBBLE.EXE /ReallyCreateANewDoc"
Now Windows 95 will run "SCRIBBLE.EXE /ReallyCreateANewDoc" to create a new Scribble Drawing. Pretty neat. There's even a Data value that lets you put the file right in the registry as binary data (not very useful, unless you have teeny-weeny documents and a brain that thinks in hex). Figure 6 summarizes these values. Windows 95 searches for NullFile, FileName, Command, and Data in that order, and uses the first one it finds. So if you want to use FileName, you have to delete NullFile, and if you want to use Command, you have to delete NullFile and FileName.
Windows searches for values in the above order; for example, if you want to use Command, you must remove NullFile and FileName.
For Scribble, the Command approach seems like the best bet. I invented a new Scribble option, /ShellNew, then modified InitInstance to register "Scrible.exe /ShellNew" as the value for Command. CSCribbleApp::InitInstance calls a new function, MaybeRegisterFileTypes, which first checks to see if Scribble is registered and, if it isn't, prompts the user to register it with the dialog in Figure 7. Only if the user responds OK does MaybeRegisterFileTypes go on to register Scribble, calling OnInstall, which calls the infamous RegisterShellFileTypes (see Figure 8). OnInstall then undoes some of MFC's work, removing the NullFile=="" value, and replacing it with Command="SCRIBBLE.EXE /ShellNew".
Figure 7 Ask the user
Using a confirmation dialog is somewhat unorthodox, but I really dislike apps that blithely install themselves and overwrite the registry as they please. It's quite distressing when I've customized my shell in some particular way, only to have some boneheaded app clobber my entries. A good app always prompts before munging the registry. After all, how do you know .SCB isn't the extension for some other app, say a SCaB file in a labor-union tracking program, or a file from Sally's Cook Book? You laugh now, but the original extension for Scribble files was .SCR; Microsoft had to change it because screen savers also use .SCR. Even if you don't clobber anything, you'll contribute to registry clutter, the moral equivalent of littering. Things have gotten so bad there's a market for registry-cleanup tools, like Matt Pietrek's REGCLEAN utility in the September 1996 MSJ. No doubt one reason for all the registry junk is AppWizard-generated programs that call RegisterShellFileTypes.
Shielding users from installation arcana is all well and good, but there are two rules you absolutely must follow to be a good registry citizen. First, never clobber anything silently behind the scenes without telling the user (adding your own profile settings is OK). Second, always provide a way to remove anything you've added. The best place to do all this is in your setup program. Since Scribble is just a demo app, I'm not going to write a setup program for it. Instead, I added two commands: Register Install and Register Remove, to add and remove the registry entries for Scribble (see Figure 9). Figure 8 shows the OnInstall and OnRemove handlers for these commands.
Figure 9 Register menu
So much for registry etiquette. Once the ShellNew\Command value is set up, the only thing remaining to finish Scribble is to actually parse the command-line option. In the old days you had to parse argv/argc manually, but now MFC makes it easy. The standard AppWizard-generated app contains the following lines in InitInstance.
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
if (!ProcessShellCommand)
return FALSE;
CCommandLineInfo is a little object that holds information about the command line; CWinApp::ParseCommandLine parses the command line, converting text options like /Embedding to code like cmdInfo.m_bRunEmbedded = TRUE; and CWinApp::ProcessShellCommand takes appropriate action. CWinApp::ParseCommandLine parses individual command-line tokens and then calls the virtual function CCommandLineInfo::ParseParam for each one. For example, if the command line was "SCRIBBLE.EXE
/foo /bar myfile.scr", CWinApp would call ParseParam three times with "foo", "bar", and "myfile.scr". CWinApp strips the leading / or - from flags (so you don't have to check for it), and passes a BOOL argument bFlag that tells ParseParam whether the token is a flag or not.
Since CCommandLineInfo::ParseParam is virtual (in fact, it's the only virtual function CCommandLineInfo has), you can override it. In other words, parsing the /ShellNew flag for Scribble is as easy as deriving a new class from CCommandLineInfo and overriding one virtual function, ParseParam.
class CMyCmdLineInfo : public CCommandLineInfo {
public:
virtual void
ParseParam(LPCSTR* pszParam,
BOOL bFlag, BOOL bLast);
enum { ShellNew=100 };
};
The implementation is practically trivial:
void CMyCmdLineInfo::ParseParam
(LPCSTR* pszParam, BOOL bFlag, BOOL bLast)
{
if (bFlag && stricmp(pszParam, "ShellNew")= =0)
(UINT&)m_nShellCommand = ShellNew;
else
CCommandLineInfo::ParseParam (pszParam, bFlag, bLast);
}
The nice thing about ParseParam is you don't have to actually parse anything. MFC feeds you the flags one by one; all you have to do is note them and check for ShellNew. I pulled a minor hack here, which is to (re)use CCommandLine::m_nShellCommand by faking out the compiler:
(UINT&)m_nShellCommand = ShellNew;
Since m_nShellCommand is declared enum inside CCommandLineInfo, C++ will only let you set it to one of the symbols declared in the enum. By casting to UINT& (reference to int), I overcome the compiler's natural instinct to keep me honest. This is the sort of thing that would make a C++ professor groan and give you a B in the course, but it works just fine in real life as long as my enum values never clash with CCommandLineInfo's. (That's why I set CMyCmdLineInfo::ShellNew= 100; I don't think MFC will ever support 100 built-in command-line options, and if it does, I'll have long since escaped to the Caymans.) The USDA approved way of doing this would be to introduce a new member, such as m_nMyCommand or m_bShellNew.
Once you implement CMyCommandLineInfo (or CYourCommandLineInfo), you have to use it instead of CCommandLineInfo:
CMyCommandLineInfo cmdInfo; // use your derived class
The next and final step is to override CWinApp::ProcessShellCommand. This function isn't virtual, but it doesn't need to be since you call it directly from InitInstance. You can override ProcessShellCommand and the compiler will generate a call to your version because it's nearest in scope. Remember, the virtualness of virtual functions is relevant only when invoking the function through a pointer to a base class object, and you want to call the function for whichever derived class the object really is-but that's not what's going on here. My implementation of ProcessShellCommand is straightforward.
if (cmdInfo.m_nShellCommand= =CMyCmdLineInfo::ShellNew) {
OnMyFileNew(); // the REAL file new (from last month)
return TRUE;
}
return CWinApp::ProcessShellCommand(cmdInfo); // let MFC
// do it
If the command is ShellNew, ProcessShellCommand calls OnMyFileNew, the function I wrote last month that initializes the document and handles the File New command in Scribble. This is different from OnFileNew, which is what MFC calls when starting up with no command line options.
Just in case you're lost, here's what happens when the user invokes File New Scribble Drawing from the shell: Windows sees the value Command="SCRIBBLE.EXE /ShellNew" in the registry under \HKEY_CLASSES_ ROOT\.SCB\ShellNew, so it runs that command. Scribble starts up and control flows to InitInstance, which instantiates a CMyCommandLineInfo and passes it to CWinApp::ParseCommandLine. ParseCommandLine parses the tokens (there's only one) and calls CCommandLineInfo::ParseParam, which is virtual, so control flows to CMyCmdLineInfo::ParseParam. CMyCmdLineInfo::ParseParam recognizes "ShellNew" and sets m_nShellCommand= CMyCmdLineInfo::ShellNew (100). When control returns, InitInstance calls CScribbleApp::ProcessShellCommand, which detects CMyCmdLineInfo::ShellNew and calls ScribbleApp::OnMyNewFile to really create the file. Voilˆ. All this is different from the situation where the user picks Scribble from the Windows 95 Start menu. In that case, Scribble starts up with no command-line arguments, ParseParam never sees ShellNew, so m_nShellCommand defaults to CCommandLineInfo::FileNew and CWinApp:: ProcessShellCommand calls OnFileNew, just as it does in a plain vanilla MFC app-only CScribbleApp::OnFileNew doesn't really create the document (see last month's column). Whew!
Q I just read your reply to the question regarding removing the title from an app (in the August and November 1995 issues of MSJ). I've been trying to achieve the opposite: I want to create an app that has only a title bar and no client area. No matter what I do, I can't remove all of the client window. I always end up with a very small window, maybe three or four pixels high, underneath the title. What's going on and how can I fix this? I enclose my last attempt at achieving this.
Poor mainframe programmer
who plays with C++
A I just love this never-ending title bar saga. First I get a reader who wants an app with no title bar because he doesn't want the user to move the window (August 1995). I show him how to prevent the sizing, but humbly suggest it's really a good idea to keep the title bar. A few months later (November 1995), another reader writes to complain that I didn't answer the first guy's question and he hates the way title bars look, so would I please tell him how to get rid of them. So I show this second guy how to create a window with no title bar. Now a third reader comes along to ask how to get rid of everything BUT the title bar! Sheesh.
As a general rule of thumb, it's almost always the case that in Windows you can do anything you want. The only catch is it may require writing a gazillion lines of code (more than forty), or learning obscure Windows voodoo. This particular case falls into the latter category. I'm about to visit some of the most arcane Windows minutiae this column has yet descended to, so now's a good time for the anti-emetic pills.
But first, let me describe the program included with the question. MONITOR is a miniapp that displays information about available system memory in its title bar (see Figure 10). I modifed MONITOR to paint the client area red so you can see the problem more easily: the mysterious two-pixel high (on my system) client area, clearly visible in Figure 10. This despite the fact that CMonitorWindow requests a window size that should result in a client area of zero height.
Figure 10 A buggy title bar
So what's going on? To find out, I threw some TRACE statements into MONITOR to display the requested and actual window sizes, and some of the relevant system metrics (see Figure 11). On my system, MONITOR requested a window 22 pixels high, but got one 27 pixels high. Why? The problem is that Windows has a minimum window height, which you can obtain by calling GetSystemMetrics(SM_CYMIN). On my system, this value is 27. Since the caption height (GetSystemMetrics(SM_CYCAPTION)) is 19, that means the window is 8 pixels taller than the caption. The borders account for only 6 pixels-three pixels each-which leaves two extra pixels that, sure enough, get allocated to the client area. Figure 12 shows a full accounting of pixels in the buggy version of MONITOR. (These and all values are taken from my system.)
Figure 11 TRACE output of buggy title bar
The problem is that MONITOR should ideally have a window 25 pixels high, but Windows won't let it create a window less than 27 pixels high. Where, one might wonder, does Windows come up with the value 27? Therein lies the solution to the whole problem. The minimum window height is calculated by assuming a size zero client area:
minimum window height = height of top border +
height of caption +
height of bottom border
This calculation is made for a window with borders that are sizable; that is, the borders you get when you specify WS_THICKFRAME, which is the default for a main window. But you don't want to let users size your window (or have min/max boxes), so you create CMonitorWindow with WS_BORDER (nonsizeable border) instead of WS_THICKFRAME (sizeable border). This results in a slightly thinner frame-three pixels versus four-and accounts for the two extra pixels in the client area.
What to do? One possible solution is to paint the client area the same color as the caption. You can get the caption color by calling GetSystemColor(COLOR_ACTIVECAPTION). Of course, you have to remember to paint it using the inactive color (COLOR_INACTIVECAPTION) when your window is inactive. This would work except for one slight problem. If you look at Figure 10, you can see that Windows paints a thin gray line below the caption. This is part of its new 3D face lift. Getting rid of that line is virtually impossible, even if you handle your own WM_NCPAINT messages (believe me, I tried). Getting rid of the thin gray line falls into category A of Windows alteration, the gazillion lines of code category.
Since requiring users to choose gray as their caption color is not an option, it's time to take a different approach. The whole problem comes from using WS_BORDER to prevent sizing instead of the normal WS_THICKFRAME. Is there some other way to prevent sizing? In fact, there is.
How does Windows know to display that little size arrow cursor when the user moves the mouse into the frame of a sizable window? By sending the window a special message, WM_NCHITTEST. The window returns a code indicating which area of the window the mouse is in. HTCAPTION says the mouse is in the caption; HTMINBUTTON says it's in the minimize button, and so on (see "Dave's Top Ten List of Tricks, Hints, and Techniques for Programming in Windows," MSJ October 1992, for more information on WM_NCHITTEST-Ed.). Of special relevance now are the hit test codes HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, HTBOTTOM, HTBOTTOMLEFT, and HTBOTTOMRIGHT. As the names suggest, these codes indicate that the mouse is in some part of the window frame where sizing is allowed. For example, HTBOTTOMRIGHT tells Windows the mouse is in the bottom, right corner of the frame. When your window proc returns HTBOTTOMRIGHT, Windows displays the northwest/southeast arrow cursor to cue the user that he can size the window by dragging. Likewise, if you return HTLEFT or HTRIGHT, Windows displays the east-west arrow to indicate that the window can be sized horizontally. The Windows default window procedure, DefWindowProc, which is where MFC routes all unhandled messages, figures out where the mouse is and returns the appropriate HTxxx code. If the window doesn't allow sizing-that is, if it has WS_BORDER instead of WS_THICKFRAME as its border style-then DefWindowProc returns another code, HTBORDER, that tells Windows the mouse is in the border but sizing is not allowed. In that case, Windows displays its normal arrow cursor and sizing is not allowed.
Suppose you had a WS_THICKFRAME border but implemented your own handler for WM_NCHITTEST that always returns HTBORDER when the mouse is in the border? That's exactly what I did to fix MONITOR (see Figure 13). The CMonitorWindow constructor creates the window with the WS_THICKFRAME border style to give it the thick sizeable frame. This eliminates the problem with the two-pixel high client area. But then, to prevent the user from sizing the window, I implemented my own WM_ONNCHITTEST handler. CMonitorWindow::OnNcHitTest first calls the base class CFrameWnd::OnNcHitTest to get the hit test code DefWindowProc would normally return, then CMonitorWindow mungs it. If the normal response would be HTTOP or HTBOTTOM, CMonitorWindow returns HTBORDER instead. This has exactly the same effect as a nonsizeable border. What's even cooler is that if CFrameWnd::OnNcHitTest returns HTLEFT, HTTOPLEFT or HTBOTTOMLEFT (any of the LEFT codes), CMonitorWindow returns HTLEFT, and if CFrameWnd returns HTRIGHT, HTTOPRIGHT, or HTBOTTOMRIGHT, CMonitorWindow returns HTRIGHT. The result is that the user can still size the window horizontally, which makes sense for a window that's only a title bar. If the user moves the mouse into one of the corners, Windows displays an east-west size arrow cursor, not a diagonal one. Pretty neat. If you don't want this feature, you can just return HTBORDER in all cases.
Figure 14 shows the improved MONITOR sans racing stripes, and the TRACE output in Figure 15 confirms that CMonitorWindow in fact has a client area zero pixels high. The only drawback to my solution is that the border is thicker. What can I say? A fatty frame seems like a small price to pay for an easy solution to a thorny problem.
Figure 14 Nice title-bar monitor app
Figure 15 Non-buggy TRACE output
Q I have a main window that shows a list of database items the user can select. Since the database field is a fixed length, my window has a fixed width, so I don't let the user size the window horizontally, only vertically. I handle WM_GETMINMAXINFO to prevent changing the width of the window, but Windows still displays the left/right size arrows when the user moves the mouse over the left and right borders. This seems wrong since the window can't be sized horizontally, and users may think it's a bug when they try to size and nothing happens. Is there some way to turn off the left/right cursor but keep the up/down one?
Alan Cardulo
A Yes, just handle WM_NCHITTEST as described in the answer to the previous question. If CFrameWnd::OnNcHitTest returns HTLEFT or HTRIGHT, your handler should return HTBORDER instead. Likewise, you should remap all the HTxxxTOP codes to HTTOP and all the HTxxxBOTTOM codes to HTBOTTOM.
Have a question about programming in C or C++? Send it to Paul DiLascia at 72400.2702@compuserve.com
From the December 1996 issue of Microsoft Systems Journal.