Tuesday, January 09, 2007

Adapting CSizingControlBar to work without idle time

Since MFC generally relies on the existence of idle time, an MFC app that doesn't have idle time is arguably incorrectly designed. However, the theoretical multithreaded alternative is so difficult to implement correctly, that in practice, timer-driven MFC apps often do real work in their message loops, potentially using up all their idle time. My app (FFRend) is an example of this: since it processes video frames in OnTimer, if the processing becomes complex enough, idle time drops to zero.

This approach causes serious problems for Cristi Posea's otherwise delightful CSizingControlBar. It also causes problems elsewhere in the app, but these are minor issues related to the UpdateCmdUI mechanism, or the status bar message line. In an app without idle time, CSizingControlBar exhibits the following undesirable behaviors:

1. While the bar is docked, resizing it doesn't work: the bar is not repainted. This occurs because the sizing bar's implementation uses DelayRecalcLayout, which helps prevents flicker, but also relies on idle processing.

2. When the bar is floated, the main frame's layout is not updated. Again, the cause is DelayRecalcLayout. Note that this problem also occurs with other control bars, not only the sizing bar.

3. When the bar is floating, its Close button doesn't work: the bar remains visible, and left-clicking the edge of its frame causes the bar to shrink to a tiny rectangle.

4. The docked bar's close button doesn't react to mouse-overs.

I have found simple solutions for the first three problems. The last problem is still unsolved, but luckily it's the least serious one.

Problems 1 and 2 can both be solved by conditionally calling RecalcLayout from main frame's OnTimer handler. The condition is a simple test for pending idle layout, specifically, if (m_nIdleFlags & idleLayout) is true. I discovered this technique quite by accident in the "Professional UI Solutions" support forum.
 
void CMainFrame::OnTimer(UINT nIDEvent)
{
Sleep(40); // emulate some work that consumes all our idle time

#if ENABLE_NO_IDLE_FIXES // ck
// CSizingControlBar uses DelayRecalcLayout, in order to avoid flicker when
// resizing docked bars. DelayRecalcLayout requires idle processing, which
// normally isn't a problem, but this app typically doesn't have idle time.
// Our solution is to periodically test the idle flags, and if there's idle
// layout pending, call RecalcLayout. This also ensures that the layout is
// updated when controls bars are floated (any bars, not just sizing ones).
if (m_nIdleFlags & idleLayout)
RecalcLayout();
#endif

CFrameWnd::OnTimer(nIDEvent);
}

Problem 3 is solved by customizing CMiniDockFrameWnd to handle WM_SYSMESSAGE. In OnSysMessage, if the ID is SC_CLOSE, hide the dock frame, via ShowWindow(FALSE).

void CSizingDockFrame::OnSysCommand(UINT nID, LPARAM lParam)
{
CMiniDockFrameWnd::OnSysCommand(nID, lParam);
// in the default implementation, if there's no idle time, the close button
// doesn't work: the bar remains visible, and left-clicking the edge of its
// frame causes the bar to shrink to a tiny rectangle
if (nID == SC_CLOSE)
ShowWindow(SW_HIDE); // but this does work
}

Note that the derived dock frame must be installed in CMainFrame::OnCreate, by setting the CFrameWnd member m_pFloatingFrameClass, immediately after the call to EnableDocking.

EnableDocking(CBRS_ALIGN_ANY);

// use a custom dock frame that can handle zero idle time
m_pFloatingFrameClass = RUNTIME_CLASS(CSizingDockFrame);

With these two simple, lightweight fixes, CSizingControlBar works quite happily in an app without idle time. Ever now and then there's a happy ending. Enjoy!

No comments: