October 1999
Code for this article: Oct99CQ&A.exe (309KB) Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.
|
Q How can I implement a start-up screen that displays a bitmap in MFC?
Turner Munbrey
A The Visual C++® Component Gallery has a component called Splash screen (see Figure 1). Just open the Components window, select Splash screen, and press Insert. Visual C++ adds a new module (splash.h and splash.cpp) to your project, and some lines of code to your app and CMainFrame classes. You even get a blank bitmap like the one in Figure 2. |
Figure 1: Adding a Splash Screen |
The vanilla splash is nice, but it's not very satisfying. Aside from the glum bitmap, it adds code to your app in several places. There's a CWinApp::PreTranslateMessage override to let the splash process messages, some lines to your InitInstance function to parse command line args, and a line in your CMainFrame to show the splash when your app starts up. A line here, a line there, three lines somewhere elsewhy all this spaghetti code when it's totally unnecessary? But that's the problem with code generatorsthey generate code. In just a moment, I'll show you how to write a splash screen you can install with one line of code. But first let me mention two more significant problems with the prepackaged splash screen. Problem number one: the splash appears for a fixed number of seconds that you specifytwo, sayand then disappears. But often the reason you want a splash screen is to give your users something to look at while your app takes forever to start up. While the splash is running, your app is scrambling to get itself in gear. But if your app is computingloading a file or initializing some humongoid list structure with ten bazillion items, sayit can't process messages through its main message pump. The component gallery generates a splash window that runs in the same process and thread as the main app, which means it doesn't get messages while your app is chomping at the CPU. If the user clicks to dismiss the splash, nothing happens. Nor will the splash get any timer messages. |
Figure 2: Scintillating Splash |
Problem number two: the splash screen you get from Redmond uses CBitmap and BitBlt to display its bitmapwhich is to say it doesn't do palette realization. That's fine for the melancholic black-and-gray splash in Figure 2, but if your splash has lots of exotic cheery colors, it'll look pathetic in 256 color mode. Of course, nowadays even your grandmother has a 32MB video card, but I'm told some people still use 256 color mode to get more pixels. They must be derangedbut who listens to me? Needless to say, I wrote my own CSplash class to fix all these inadequacies. What else am I here for? To use CSplash, all you have to do (aside from adding the files to your project and #including the header) is add the promised one-liner to your app's InitInstance function: Technically that's two lines, but comments don't count. This creates a new splash screen with a bitmap resource loaded from IDB_SPLASH, and displays it for two seconds. Unlike the component manager splash, my splash remains visible until your app creates a main window. In other words, the time-to-display argument is really a minimum. The splash appears for at least two seconds, but longer if your application is slow to rise.
That's it; that's all there is. Just create the splash and forget about it. You don't even have to delete it since CSplash manages its own death. This is the way life should be. No code generators, just import the thing and call it. If for some reason you want to kill your splash prematurely (perhaps you're feeling aggressive today), there's a way to do it: If you plan to kill the splash, you must provide the constructor with a "back" pointer to your splash pointer, and you must check the splash pointer for NULL before killing it. If you don't, the splash will retaliate by killing your app! Why this subterfuge? Because the splash may already have died by the time you get around to killing it, and you can't kill it twice. If the splash destroys itselfperhaps the user dismissed itit politely sets your pointer to NULL.
Using CSplash is a stroll in the park, so now let's go inside the code to see how I pulled all those rabbits out my hat. Actually, it's pretty straightforward. First, CSplash is derived from CWinThread, which is to say, a splash is a thread. When you create a new CSplash, the constructor saves its arguments and calls CreateThread (which CSplash inherits from CWinThread) to create a new user interface thread. Control returns immediately to your app, which can hoard the CPU all it likes whilepoof!the splash disappears through a wormhole. Andpoof!the splash pops out the other side, over here in the gamma quadrant, which is to say, in a new thread. As the thread starts up, MFC goes through its usual initialization conniptions before eventually calling the thread's InitInstance function. CSplash::InitInstance parses the main app's command line to check for -Embedding (embedded OLE object), -Automation (automation server), or -nologo. If any of these are present, InitInstance returns FALSE to terminate the thread; otherwise, it creates the splash window. Note that m_pMainWnd is the main window for the splash thread, not your main app. To create the window, InitInstance passes the buck to another function, OnCreateSplashWnd, which does the dirty work.
CSplashWnd is a new CWnd-derived window class that implements the splash screen. Why put window creation in a separate function? To let you override it, of course. OnCreateSplashWnd is virtual, so if you want to derive a specialized CSplash that creates a specialized CSplashWnd (with animations, perhaps), all you have to do is override OnCreateSplashWnd.
I forgot to mention the flags. When you create a CSplash, you give it flags. Every API has to have some flags. Currently, there are only three: CSplash::KillOnClick tells the splash to do the dismiss-if-the-user-presses-a-key thing; IgnoreCmdLine tells the splash to splash, even if the command line says -nologo (I only added this for my test program); and CSplash::NoWaitForMainWnd presents the wait-for-main-window feature. Aside from all that, window creation is totally normal. CSplash::OnCreateSplashWnd calls CSplashWnd::Create, which loads the bitmap and calls CreateEx. To handle DIBs correctly, I (re)used the CDib class from my MFC Goodies articles in the January and March 1997 issues of MSJ. Using CDib, loading the bitmap is as simple as this: Drawing it is equally mindless:
This is where reusable classes really pay off. Version 1 of CSplash used an ordinary MFC CBitmap; once I got that working, all I had to do was dust off my CDib and plug it in. I left the old code behind in case for some perverse reason you don't want to use CDib. All you have to do is #define NODIBbut why you'd turn down a bunch of free code that makes your bitmaps paint correctly is a mystery to me. (Perhaps your bitmap has a color-challenged palette.) If you don't understand all the fuss made about CDib and palette realization, check out the aforementioned articles.
So far, CSplashWnd is pretty straightforward. I've saved the tricky part for last. Terminating a user interface threadthat is, a thread that displays a windowis always a little delicate. In this case, it can happen in one of three ways: the timer pops, the user clicks or types at the splash, or the app calls CSplash::Kill. In the first case (timer pop), CSplashWnd checks to see whether the app has a main window. If not, it sets another timer to wait another tenth of a second. This is the feature I mentioned earlier; the splash doesn't disappear until the main window comes alive. In the second termination case (user clicks or types at the splash), CSplashWnd::PreTranslateMessage handles the event.
Since CSplash runs in its own thread with its own message pump, there's no need to hook the app's PreTranslateMessage as in the component gallery version. PreTranslateMessage is the most convenient place to nip any keystrokes in the bud, before the message pump can translate them. But it's important to post WM_CLOSE (instead of sending it) so Windows® can finish processing the keystroke before the window goes bye-bye.
The last way the thread can die is if the main process calls CSplash::Kill. In this case, CSplash posts WM_CLOSE: So in all three casestimer pop, user input, and killthe result is the same: the splash window gets WM_CLOSE. The Windows default message proc (DefWindowProc) handles WM_CLOSE by calling DestroyWindow to usher your splash window to its eternal resting heap. After the last message has come and gone, MFC calls CSplashWnd::PostNcDestroy to let the object perform any last rites.
CSplashWnd puts the main window in the foreground and deletes itself. This might be a good time to call ::PostQuitMessage to terminate the threadbut there's no need since CWnd::OnNcDestroy does that already if the window is the same as the running thread's m_pMainWnd. When CWinThread::PumpMessage sees WM_QUIT, it stops pumping and terminates the threadbut not before calling CWinThread::Delete, which does a "delete this." Bye-bye CSplash object. Just before the CSplash merges back into the heap like Odo melding with his Founders, it's destructor notifies the world of its impending croakment by NULLing the caller's back pointer.
And so the story ends. Figure 3 shows the final code. By implementing CSplash the way I've shown, all the code is self-contained. There's no need to hook into the message loop or any other bizarreness. Just create the splash when your app starts up and forget about it. Whenever you add new functionality to your app, you should always strive for the narrowest interface possiblethat is, the one with the fewest calls and interactions. Your code is much more likely to work that way. With CSplash, the only real work you have to do is the fun part: namely, create a bitmap that looks splashy. Figure 4 shows my test splash. But don't get too carried away! Visual Studio spends so many CPU cycles displaying its splash screen that it's noticeably faster starting up if you add -nologo to the command string in your registry. |
Figure 4: Splashy Splash |
Q How can I parse the command line in an MFC app? My program has several command line options (such as -l, -x,
-help). I see there's m_lpCmdLine that points to the command line, but do I have to parse it myself? It seems like there should be some way to make CCommandLineInfo do it, but I can't figure out how.
Amy Brechstler
A You're on the right track. CCommandLineInfo is the class MFC uses to parse command-line options; the generic MFC app starts out with the following lines in InitInstance:
ParseCommandLine is a CWinApp member function that calls CCommandLineInfo to do the work. CCommandLineInfo has a virtual function, ParseParam, that parses a single parameter.
CWinApp::ParseCommandLine calls this function repeatedly for each command-line token. Unfortunately, ParseParam is hardcoded to recognize only certain common switches like -Embedding, -Automation, -pt (PrintTo), and so on (see Figure 5). For example, if ParseParam sees
-p, it sets m_nShellCommand = CCommandLineInfo::FilePrintTo, and if it sees -Embedding or -Automation, MFC sets m_bRunEmbedded or m_bRunAutomated = TRUE. There can be other effects as well. In the last two cases, MFC sets m_bShowSplash = FALSE and calls AfxOleSetUserCtrl(FALSE) to kill the UI.
CCommandLineInfo is all very fine and dandy, but there's no easy provision to add your own options, other than to derive a new class and override ParseParam. That's a lot of typing for such a simple feature. What you really want is to call some function that will get the value of any options the user may have typed. Naturally, I wrote a new class, CCommandLineInfoEx, that does this (see Figure 6). It has two overloaded GetOption functions: one to get a boolean option as in the previous case, and another to get a string value:
In this case, m_sBaz is a CString that's filled with whatever the user typed after -baz. For example, if the user typed "MyApp -baz mumble" then m_sBaz will be "mumble." CCommandLineInfoEx has far fewer lines of code than the MFC version with all its hardwired if/then/else statements, and it's totally generic. Why didn't the Redmondtonians give us this? It could be their heads got stuck in assembler mode that day; it happens to the best of us.
How does CCommandLineInfoEx work? Simple. It uses a string hash (CMapStringToString) to store all the options as it parses them. For a string option, it sets the value of the option to the typed string; for a nonstring option, it stores the value "TRUE". The CSplash class I created for the previous question uses CCommandLineInfoEx to parse the -nologo switch. Q I'm writing an application with MFC that needs to add buttons dynamically to the dialog box. How can I implement the message handlers, given a situation like this? Shanker
A The standard message map entry for a menu or button command handler looks like this: But if you create the button/menu item on the fly, you may not know the command ID at compile time. It may be stored in a data member somewhere like CSqueegie::m_nMeMyMineID. What to do? Simple: use ON_COMMAND_EX_
RANGE with an infinite range.
Now MFC routes any command in the range 0 to 0xFFFFwhich is all commandsto your handler. Your handler can check for the specific IDs at runtime.
With ON_COMMAND, your handler function has no arguments and returns void, but with ON_COMMAND_
EX_RANGE, your handler function gets the ID of the command and must return TRUE if you handle the command, FALSE if not. If you return FALSE, MFC will continue routing the command so other objects can get a crack at it.
|
Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com |
From the October 1999 issue of Microsoft Systems Journal.
|