June 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).
Click to open or copy the DLGIDLE project files.
Click to open or copy the SCBMOD3 project files.
Click to open or copy the SCBMOD4 project files.
The first question this month is really a continuation of a question from last month's column. Phillipe Bernous asked how to implement a "modified indicator" that displays an asterisk in the title bar next to the document name if the document is modified. He implemented an indicator in the status bar using the normal MFC ON_UPDATE_COMMAND_UI mechanism. This enabled or disabled his status bar's "MOD" pane based on the return value from CDocument::IsModified. Phillipe wanted to know if there is a similar way to update the title bar with ON_UPDATE_COMMAND_UI.
In my answer, I explained that MFC doesn't use ON_UPDATE_COMMAND_UI to update the window title. I went on to show two different ways to solve his problem. In the first solution, I overrode CDocument::SetModifiedFlag to call CDocument::SetTitle whenever some other part of the code changed the document's modified flag. In the second solution, I overrode CFrameWnd::OnUpdateFrameTitle to append an asterisk to the document title if the document was modified, and wrote a CChildFrame::OnIdleUpdateCmdUI handler for WM_IDLEUPDATECMDUI. This handler called OnUpdateFrameTitle, making MFC update the title, whenever the current modified state changed from the last WM_IDLEUPDATCMDEUI message. This second solution resembled the ON_UPDATE_COMMAND_UI mechanism, and it wouldn't take much extra effort to actually implement title bar "panes" with ON_UPDATE_COMMAND_UI. I promised I'd show you how in the next column. Well, here we are!
I'll start with a question that makes sense whether or not you saw the previous column: how can you implement general-purpose title bar panes that are analogous to status bar panes and that you can update using ON_UPDATE_COMMAND_UI? By title bar pane I mean some portion of the window title that displays something interesting. It could display "*" for a modified document, the document's time and date of creation, or the name of the item you've selected (if your document has a notion of named items). Each pane should have an integer ID that you use to identify it in your message map entries.
ON_UPDATE_COMMAND_UI(ID_MOD_INDICATOR, OnUpdateMod)
void CScribbleDoc::OnUpdateMod(CCmdUI* pCmdUI)
{
pCmdUI->SetText(IsModified() ? "*" : "");
}
This code should display "*" in the title bar if you modify the current document.
It's not hard to set this up if you understand how MFC updates user-interface objects. The best way is to see how MFC handles status panes, then mimic that scheme for the title bar.
MFC updates status bar panes, toolbar buttons, and menu items by implementing a special CCmdUI-derived class for each particular kind of UI element. The base class, CCmdUI, provides access to a menu item. CStatusCmdUI and CToolCmdUI let you control the state of a status bar pane or toolbar button. CCmdUI has virtual functions like SetText and Enable that manipulate the item. In the case of a menu item, CCmdUI::SetText sets the menu item text and CCmdUI::Enable grays or ungrays it. For a toolbar button, CToolCmdUI::SetText does nothing (toolbar buttons don't have text) and CToolCmdUI::Enable enables the button. For status bar panes, CStatusCmdUI::SetText sets the text in the status bar pane and CStatusCmdUI::Enable shows or hides it. You don't ever need to know that there are several different kinds of CCmdUI floating around; you only need to call the right functions, SetText, Enable, SetCheck, and so on, from your ON_COMMAND_UPDATE_UI handler. Because these functions are virtual, they do the right thing for whatever kind of UI element the CCmdUI really represents: menu item, toolbar button, or status bar pane. So, to implement title bar panes, you need a new kind of CCmdUI.
The next question is, how and when does your UI handler get called? As part of its normal idle processing, MFC sets up a CCmdUI object for every menu item, toolbar button, and status bar pane in your app, then calls CCmdUI::DoUpdate for each one. CCmdUI::DoUpdate ends up groveling over your message maps looking for ON_UPDATE_ COMMAND_UI handlers with IDs that match the ID of the menu item, toolbar button, or status bar pane. (See my article "Meandering through the Maze of MFC Message and Command Routing," MSJ, July 1995.) Each object that owns user-interface items does this. For example, CStatusBar and CToolBar update their panes and buttons, respectively. CWinFrm updates menu items in response to WM_INITMENUPOPUP, not during the idle loop, because you need to update the menus only when the user clicks on one.
To review quickly, there are three steps for implementing title bar panes. First, implement a CTitleBar class that describes the panes. Second, derive a new class from CCmdUI with overrides for SetText, Enable, SetCheck, and so on. Third, hitch a ride on WM_IDLEUPDATECMDUI to sail these little CCmdUI beasties around the system so anybody can catch one just by adding an ON_UPDATE_COMMAND_UI handler in their message map. By "anybody," I mean any object that would normally receive command notifications, such as the frame, view, or document.
Since Scribble is the mother of all sample programs, I started with the Scribble program from the MFC tutorial and modified it to display two panes in the title: first, the asterisk that indicates the current document's modified state; and second, the number of strokes in the Scribble drawing formatted as "[S:n]" where n is the number of strokes. Since real estate on the title bar is expensive, I figured it would be OK to display "[S:n]" instead of something more user-friendly like, "The number of strokes in this here document you are right now looking at is: n". Besides, we're all nerds here.
Last month's solutions were SCBMOD1 and SCBMOD2, so I gave this solution the surprising name of SCBMOD3 (see Figure 1). I've only shown the changes from the original Scribble program, which are confined to ScribDoc, ChildFrm, and an entirely new module, TitleBar. Figure 2 shows SCBMOD3 in action with a document that has five strokes. (In Scribble, a stroke represents a single mouse-down/mouse-up movement, and may contain many individual line segments.)
Figure 2 SCBMOD3 changes in Scribble
I also need a class to represent the whole title bar, which I will call CTitleBar. As titlebar.h shows, CTitleBar is just a pointer to an array of TITLEPANEs. To create a title bar in your app, instantiate a CTitleBar object in your frame window class and call CTitleBar::SetIndicators with an array of ints, the IDs for the indicators.
class CChildFrame : public CMDIChildWnd {
CTitleBar m_titleBar; // title bar
·
·
·
};
CChildFrame::CChildFrame()
{
static UINT indicators[] =
{ ID_TITLE_MOD, ID_TITLE_NUMSTROKES };
// Create title bar indicators
m_titleBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(indicators[0]));
}
This looks just like it would for status bar panes, only I do my initialization in the CChildFrame constructor instead of OnCreate. (I already had an empty constructor function and was too lazy to implement OnCreate and hook it up to the message map. Anything to avoid typing.) Also, note that the title bar belongs to the child frame, not the main frame. CTitleBar::SetIndicators allocates and initializes an array of TITLEPANEs based on the array of ints passed.
The only other thing you have to do to hook up the title panes is implement a handler for WM_IDLEUPDATECMDUI.
LRESULT
CChildFrame::OnIdleUpdateCmdUI(WPARAM wParam, LPARAM)
{
m_titleBar.OnIdleUpdate(this, (BOOL)wParam);
CMDIChildWnd::OnIdleUpdateCmdUI();
return 0L;
}
The implementation is trivial; you just call the CTitleBar::OnIdleUpdate function I wrote, then call the default implementation for CMDIChildWnd (the order isn't really important). This is the only ugly part of the implementation. Ideally, you wouldn't need CChildFrame::OnIdleUpdateCmdUI; however, MFC is notoriously difficult when it comes to writing extension classes, and CTitleBar is really an extension class. There's no way for CTitleBar to hook WM_IDLEUPDATECMDUI invisibly on behalf of another window-aside from inserting its own WndProc, thereby circumventing MFC. Working by the book, you would derive a new class (CNewMDIChildWnd, for example) that handles the message, and make developers derive from that. Unfortunately, this tactic begs the question: what if someone else defines a CEvenNewerMDIChildWnd? Which one does the developer derive from? You can only use one.
My compromise solution is to give you the handler function, CTitleBar::OnIdleUpdate, but make you hook it up to WM_IDLEUPDATECMDUI yourself. What does CTitleBar::OnIdleUpdate actually do? This is where the CCmdUIs come in.
In titlebar.cpp, I derive a new class, CTitleCmdUI, that represents a user interface to a single TITLEPANE. It has SetText, Enable, and SetCheck overrides. SetText sets the text, Enable sets the m_bEnabled flag on, and SetCheck does nothing, but is needed to hide the SetCheck CTitleCmdUI inherits from CCmdUI. CTitleBar::OnIdleUpdate creates a CTitleCmdUI on the stack and loops over all the TITLEPANEs, successively pointing the CTitleCmdUI at each one before calling the magic CCmdUI::DoUpdate. If you remember, DoUpdate is the CCmdUI function that routes the darn thing to all the message maps. If there's an ON_UPDATE_COMMAND_UI anywhere for this title pane, MFC calls the handler, which calls SetText, Enable, or whatever it wants to update the title pane based on the current state of your app.
Once CTitleBar::OnIdleUpdate routes the panes all over the world, each TITLEPANE has the correct text and enable flag and you can actually set the title in the title bar. This is where my implementation differs slightly from menu items and status bar panes. When you call CCmdUI::SetText for a menu or status bar pane, the change is reflected immediately; with my CTitleCmdUI, SetText only changes the text in the TITLEPANE structure. Only after all the TITLEPANEs are updated do I actually change the window's title bar. This is necessary because the whole window title must be assembled by concatenating the normal MFC title (app and document names) with all the TITLEPANEs before setting the title with SetWindowText. Note that the order of the IDs in the original SetIndicators array of ints is the order they appear in the title bar. CTitleBar::OnIdleUpdate compares the new title with the previous one and calls SetWindowText only if the title changed since the last update. This avoids the screen flicker that would result if you set the window title on every idle update cycle.
That's about it. The only thing left to do is write the actual ON_UPDATE_COMMAND_UI handlers and put them in the message maps. Here's one for the stroke indicator:
BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)
·
·
·
ON_UPDATE_COMMAND_UI(ID_TITLE_NUMSTROKES,
OnUpdateNumStrokes)
END_MESSAGE_MAP()
void CScribbleDoc::OnUpdateNumStrokes(CCmdUI* pCmdUI)
{
CString s;
s.Format(" [S:%d]", m_strokeList.GetCount());
pCmdUI->SetText(s);
pCmdUI->m_bContinueRouting = TRUE;
}
The last line lets other objects update the same CCmdUI item. In my case, I update the contents ("[S:%d]") in CScribbleDoc, but I update the enabled status in CChildFrame since it owns the m_bViewTitleInfo flag. Without setting m_bContinueRouting to TRUE, MFC would stop routing the CCmdUI as soon as CScribbleDoc handled it.
So, there you have it. Whether or not you want to implement title bar panes, at least now you understand how CCmdUI works. You should be able to implement any kind of UI indicator with ON_UPDATE_COMMAND_UI. For example, you can update a gallery, palette, or other UI widget with ON_UPDATE_CMD_UI. In general, it's much easier to update UI elements on demand than maintain them as your program's state changes. It also makes your code cleaner and more object-oriented.
Just to prove it could be done, I implemented another version of Scribble, SCBMOD4, where I derive a CMyTitleBar that handles the "View Title Info" command and manages the indicator panes. (In SCBMOD3, this is done in CChildFrame.) It seems more logical that the title bar should manage itself, rather than having the child frame do it. Both versions are included in the source code for this column. Use them with my blessing.
QI'm having a problem with idle processing in a dialog-based app. My application is a "helper app" that runs under the Netscape browser. It primarily decodes incoming audio from an asynchronous socket and sends it to the wave device. My app begins by reading a number of audio buffers, then it decodes them and sends them to the device. I keep four or five buffers queued in the driver in cases of long network latency times, when another app is launched. On a 486/66, one second of audio takes 650 milliseconds to decode, which doesn't leave time to spare. The app must run under Win32sÒ 1.3 and WindowsÒ for Workgroups 3.11, which is quite a constraint. To speed things up, I start reading data into my buffers as soon as the app is initialized. I do this in the OnIdle handler. Only when the user stops the audio or it finishes playing do I stop idle processing.
My problem is that OnIdle works fine for normal document/view apps, but it doesn't seem to work for a dialog-based app. My CApp::InitInstance calls dlg.DoModal, which calls CWnd::RunModalLoop, which never calls OnIdle. I thought I could do some background processing in WM_ENTERIDLE, but that message is sent to the dialog's parent. In my case, there is no parent window. Help!
Jim Kallimani
AAs you discovered, "modal" dialogs are really modeless in MFC 4.0. When you call CDialog::DoModal, MFC doesn't call ::DialogBox like it used to; instead, it calls ::Create-DialogIndirect (after great deliberation), then simulates the modal behavior by disabling the parent window and going into its own message loop. This is essentially what ::DialogBox does anyway. The benefit of doing it this way is that MFC owns the dialog's message loop, whereas the message loop was formerly hidden inside the Windows API function ::DialogBox. This lets MFC pump modal dialog messages through the normal MFC channels (CWinThread::PumpMessage) as it does for other kinds of windows. In particular, you can override CWnd::PreTranslateMessage for modal dialogs-for example, to implement accelerator keys. Earlier releases of MFC allowed you to implement your own PreTranslateMessage for a modal dialog. However, it was never called because CDialog::DoModal went directly to ::DialogBox, which doesn't return control to your program until one of your dialog message handlers calls EndDialog. Also, with ::DialogBox, it's impossible to do idle processing the normal MFC way since control disappears into ::DialogBox and doesn't come back until the dialog is over.
Instead, Windows has its own mechanism, WM_ENTERIDLE, for doing idle processing in a modal dialog. After processing one or more messages, Windows sends WM_ ENTERIDLE to the owner window of a modal dialog box or menu if no more messages are waiting in its queue. Only modal dialogs send WM_ENTERIDLE, not modeless ones. Since MFC now uses modeless dialogs even for modal ones, MFC has to manually send WM_ENTERIDLE itself in order to mimic modal dialogs-but only if the dialog has a parent window. Jim ran into trouble because there's no parent window to receive WM_ENTERIDLE. Are you totally confused yet?
If MFC pumps modal dialog messages through its standard message pump, why doesn't it call CWinApp::OnIdle as part of that processing? The problem is that CWnd::RunModalLoop calls CWinThread::PumpMessage, but the OnIdle stuff happens in CWinThread::Run. MFC calls CWinThread::Run to run your app after calling your app's InitInstance function. CWinThread::Run looks like this in condensed form:
// (from THRDCORE.CPP)
int CWinThread::Run()
{
// for doing idle cycle
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
for (;;) {
while (bIdle && !::PeekMessage(...)) {
// call OnIdle while in bIdle state
if (!OnIdle(lIdleCount++))
// assume "no idle" state
bIdle = FALSE;
}
// Get/Translate/Dispatch the message
// (calls CWinThread::PumpMessage)
·
·
·
}
}
I've cut a lot to highlight how idle processing works. If there are no messages waiting, MFC calls CWinThread::OnIdle repeatedly with a count argument that's incremented every time. You can use the count to prioritize different kinds of idle processing. You might do formatting when the idle count is 1, then update a time-of-day clock on idle count 2. When your OnIdle returns FALSE, MFC stops calling it and waits until your thread gets another message, whereupon the idle cycle starts all over again.
The point is that modal dialogs never go through this code because CWnd::RunModalLoop calls CWinThread::PumpMessage directly from inside its own message loop. It doesn't call CWinThread::Run, so it never calls CWinThread::OnIdle. Folks in Redmond tell me this is by design. Apparently, it's dangerous to call OnIdle inside a modal dialog, because many message handlers create temporary CWnd objects, which they expect will exist through the duration of the dialog. Part of the default OnIdle processing is to free temporary handle maps.
(I can't resist telling you that, in my humble opinion, the whole temporary/permanent handle map mechanism MFC uses to associate HWNDs with CWnds is one of the biggest disaster areas in the entire framework-worse even than message maps themselves. The "temporary map" problem continually haunts programmers-especially in multithreaded apps, which makes them difficult to write in MFC.)
So, how do you handle idle processing in a dialog-based app where the dialog has no parent window? Fortunately, it's trivial. The MFC developers provided a hook: WM_KICKIDLE. RunModalLoop sends this MFC-private message repeatedly when there are no messages in your dialog's queue-just the way CWinThread::Run calls OnIdle. RunModalLoop even passes a counter and increments it for you. In effect, WM_KICKIDLE is the dialog equivalent of OnIdle. (Historical note: earlier versions of MFC did the modal/modeless swap and WM_KICKIDLE thing for property sheets. Apparently it worked so well they decided to make all modal dialogs modeless.)
Figure 3 shows DLGIDLE, a dialog-based app I wrote that does idle processing. Figure 4 shows it running (pretty boring), and Figure 5 shows the TRACE messages displayed as I ran the mouse over the dialog. The TRACE messages represent idle processing you would do in your dialog.
Figure 4 DLGIDLE
Figure 5 Tracing DLGIDLE
One word of warning: you might be tempted to call your main app's OnIdle function from OnKickIdle.
LRESULT CMyDlg::OnKickIdle(WPARAM, LPARAM lCount)
{
return AfxGetApp()->OnIdle(lCount);
}
The MFC folks tell me this is dangerous because of the temporary map problem. It's safer to do your idle processing directly inside OnKickIdle. If you want, you can combine common idle processing into a helper function that you call from both CApp::OnIdle and CMyDlg::OnKickIdle.
While I'm on the subject of idle processing, not all programmers know that there are OnIdle functions for CDocTemplate and CDocument! If you want to do idle processing in a document or document template, all you have to do is override one of these functions.
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:
| Paul DiLascia |