Joe said:
Holy crap, that's a gold mine.
I'l inform my editor(s) that someone, somewhere, thinks that... ;-)
I guess this gets OT now, but can you explain more here? (or, if it's
covered in your book, just say so) Is
http://ruby-gnome2.sourceforge.jp/hiki.cgi?cmd=view&p=Gtk#Gtk.timeout_add
an example of a "window timer"?
Healthy threads may drift in and out of topicality. The difference is we
know the difference. Here's the verbiage around progress bars. They are
symbolic of all asynchronous GUI issues:
During a long process, users need more than assurance the program did not
hang. They need to know how long a process will take, so they can schedule a
break, lunch, or a vacation. And they need the option to cancel progress
without deleting your program from their task list.
Progress bars' lowly status, as event-driven supplements to procedural
features (often added late, after those features work), exposes them to some
common AntiPatterns. Don't:
* raise a separate dialog box just to display the progress bar
* put the logical procedure into a separate thread
* write a loop statement in the GUI thread to
spin until the procedure advances
* guess or fudge the number of ticks to progress-count them
* convert the mouse pointer to an hourglass if clicking still works
* block the event queue, disabling clicking and the Paint() event
* display a cancel button that does nothing.
Most of those admonitions have common exceptions. Some procedures by nature
are not interruptible. Some, such as Web browsers, communicate with distant
unreliable servers, and cannot predict the number of ticks a progress bar
will consume.
Here's a naïve WTL implementation of a progress bar:
LRESULT
ProjectDlg::OnSlowOperation(WORD, WORD, HWND, BOOL &)
{
CListBox aList = GetDlgItem(IDC_LIST_CUSTOMERS);
int count(aList.GetCount());
CProgressBarCtrl aBar = GetDlgItem(IDC_PROGRESS);
aBar.SetRange(0, count);
aBar.SetPos(0);
for (int x(0); x < count; ++x)
{
Sleep(25);
aBar.SetPos(x);
}
return 0;
}
The function Sleep(25) simulates some Logic Layer function that slowly
processes one record.
That design is not good enough yet, but it could be worse. The design could
call aBar.SetPos(x) from somewhere deep inside the function that Sleep(25)
represents. That would couple the Logic Layer to a GUI Layer identifier,
aBar. This design decouples a little, but tests will soon force it to
decouple more.
This test forces the IDC_SLOW_OPERATION button to start a timer:
TEST_(TestDialog, SetTimer)
{
CButton slowButton = m_aDlg.GetDlgItem(IDC_SLOW_OPERATION);
slowButton.SendMessage(BM_CLICK);
BOOL thereWasaTimerToKill = m_aDlg.KillTimer(0);
CPPUNIT_ASSERT(thereWasaTimerToKill);
}
The test detects the timer by successfully killing it. (Future refactors
should replace the "Magic Number" 0 with a named constant.)
This code sets the timer, and moves the loop index into ProjectDlg's member
list. A third data member, for a dialog with so many features, is a small
price to pay for event queue freedom:
class
ProjectDlg:
public CDialogImpl<ProjectDlg>
{
.....
BEGIN_MSG_MAP(ProjectDlg)
.....
COMMAND_ID_HANDLER(IDC_SLOW_OPERATION, OnSlowOperation)
MESSAGE_HANDLER(WM_TIMER, OnTimer)
END_MSG_MAP()
LRESULT OnTimer(UINT, WPARAM, LPARAM, BOOL &);
ProjectDlg(char const *xml):
m_aCA(xml),
m_index(0)
{}
.....
private:
CustomerAddress m_aCA;
CString m_fileName;
int m_index;
};
OnSlowOperation() will now work by checking if m_index is at the beginning
or end of its progress. When no slow operation is under way the method
starts it, otherwise it stops it.