Programming Windows with MFC, Second Edition

[Previous] [Next]

In the real world, you don't usually have the luxury of starting a thread and just letting it run. More often than not, that thread must coordinate its actions with other threads in the application. If two threads share a linked list, for example, accesses to the linked list must be serialized so that both threads don't try to modify it at the same time. Simply letting a thread go off and do its own thing can lead to all sorts of synchronization problems that show up only randomly in testing and that can often be fatal to the application.

Windows supports four types of synchronization objects that can be used to synchronize the actions performed by concurrently running threads:

MFC encapsulates these objects in classes named CCriticalSection, CMutex, CEvent, and CSemaphore. It also includes a pair of classes named CSingleLock and CMultiLock that further abstract the interfaces to thread synchronization objects. In the sections that follow, I'll describe how to use these classes to synchronize the actions of concurrently executing threads.

Critical Sections

The simplest type of thread synchronization object is the critical section. Critical sections are used to serialize accesses performed on linked lists, simple variables, structures, and other resources that are shared by two or more threads. The threads must belong to the same process, because critical sections don't work across process boundaries.

The idea behind critical sections is that each thread that requires exclusive access to a resource can lock a critical section before accessing that resource and unlock it when the access is complete. If thread B attempts to lock a critical section that is currently locked by thread A, thread B blocks until the critical section comes free. While blocked, thread B waits in an extremely efficient wait state that consumes no processor time.

CCriticalSection::Lock locks a critical section, and CCriticalSection::Unlock unlocks it. Let's say that a document class includes a linked-list data member created from MFC's CList class and that two separate threads use the linked list. One writes to the list, and the other reads from it. To prevent the two threads from accessing the list at exactly the same time, you can protect the list with a critical section. The following example uses a globally declared CCriticalSection object to demonstrate how. (I've used global synchronization objects in the examples to ensure that the objects are equally visible to all the threads in a process, but no, synchronization objects don't have to have global scope.)

// Global data CCriticalSection g_cs; // Thread A g_cs.Lock (); // Write to the linked list. g_cs.Unlock (); // Thread B g_cs.Lock (); // Read from the linked list. g_cs.Unlock ();

Now it's impossible for threads A and B to access the linked list at the same time because both guard the list with the same critical section. The diagram in Figure 17-3 illustrates how the critical section prevents overlapping read and write accesses by serializing the threads' actions.

Figure 17-3. Protecting a shared resource with a critical section.

An alternate form of CCriticalSection::Lock accepts a time-out value, and some MFC documentation states that if you pass Lock a time-out value, it will return if the time-out period expires before the critical section comes free. The documentation is wrong. You can specify a time-out value if you want to, but Lock won't return until the critical section is unlocked.

It's obvious why a linked list should be protected from concurrent thread accesses, but what about simple variables? For example, suppose thread A increments a variable with the statement

nVar++;

and thread B does something else with the variable. Should nVar be protected with a critical section? In general, yes. What looks to be an atomic operation in a C++ program—even the application of a simple ++ operator—might compile into a sequence of several machine instructions. And one thread can preempt another between any two machine instructions. As a rule, it's a good idea to protect any data subject to simultaneous write accesses or simultaneous read and write accesses. A critical section is the perfect tool for the job.

The Win32 API includes a family of functions named ::InterlockedIncrement, ::InterlockedDecrement, ::InterlockedExchange, ::InterlockedCompareExchange, and ::InterlockedExchangeAdd that you can use to safely operate on 32-bit values without explicitly using synchronization objects. For example, if nVar is a UINT, DWORD, or other 32-bit data type, you can increment it with the statement

::InterlockedIncrement (&nVar);

and the system will ensure that other accesses to nVar performed using Interlocked functions don't overlap. nVar should be aligned on a 32-bit boundary, or the Interlocked functions might fail on multiprocessor Windows NT systems. Also, ::InterlockedCompareExchange and ::InterlockedExchangeAdd are supported only in Windows NT 4.0 and higher and Windows 98.

Mutexes

Mutex is a contraction of the words mutually and exclusive. Like critical sections, mutexes are used to gain exclusive access to a resource shared by two or more threads. Unlike critical sections, mutexes can be used to synchronize threads running in the same process or in different processes. Critical sections are generally preferred to mutexes for intraprocess thread synchronization needs because critical sections are faster, but if you want to synchronize threads running in two or more different processes, mutexes are the answer.

Suppose two applications use a block of shared memory to exchange data. Inside that shared memory is a linked list that must be protected against concurrent thread accesses. A critical section won't work because it can't reach across process boundaries, but a mutex will do the job nicely. Here's what you do in each process before reading or writing the linked list:

// Global data CMutex g_mutex (FALSE, _T ("MyMutex")); g_mutex.Lock (); // Read or write the linked list. g_mutex.Unlock ();

The first parameter passed to the CMutex constructor specifies whether the mutex is initially locked (TRUE) or unlocked (FALSE). The second parameter specifies the mutex's name, which is required if the mutex is used to synchronize threads in two different processes. You pick the name, but both processes must specify the same name so that the two CMutex objects will reference the same mutex object in the Windows kernel. Naturally, Lock blocks on a mutex locked by another thread, and Unlock frees the mutex so that others can lock it.

By default, Lock will wait forever for a mutex to become unlocked. You can build in a fail-safe mechanism by specifying a maximum wait time in milliseconds. In the following example, the thread waits for up to 1 minute before accessing the resource guarded by the mutex.

g_mutex.Lock (60000); // Read or write the linked list. g_mutex.Unlock ();

Lock's return value tells you why the function call returned. A nonzero return means that the mutex came free, and 0 indicates that the time-out period expired first. If Lock returns 0, it's normally prudent not to access the shared resource because doing so could result in an overlapping access. Thus, code that uses Lock's time-out feature is normally structured like this:

if (g_mutex.Lock (60000)) { // Read or write the linked list. g_mutex.Unlock (); }

There is one other difference between mutexes and critical sections. If a thread locks a critical section and terminates without unlocking it, other threads waiting for the critical section to come free will block indefinitely. However, if a thread that locks a mutex fails to unlock it before terminating, the system deems the mutex to be "abandoned" and automatically frees the mutex so that waiting threads can resume.

Events

MFC's CEvent class encapsulates Win32 event objects. An event is little more than a flag in the operating system kernel. At any given time, it can be in either of two states: raised (set) or lowered (reset). A set event is said to be in a signaled state, and a reset event is said to be nonsignaled. CEvent::SetEvent sets an event, and CEvent::ResetEvent resets it. A related function, CEvent::PulseEvent, sets and clears an event in one operation.

Events are sometimes described as "thread triggers." One thread calls CEvent::Lock to block on an event and wait for it to become set. Another thread sets the event and thereby releases the waiting thread. Setting the event is like pulling a trigger: it unblocks the waiting thread and allows it to resume executing. An event can have one thread or several threads blocking on it, and if your code is properly written, all waiting threads will be released when the event becomes set.

Windows supports two different types of events: autoreset events and manual-reset events. The difference between them is very simple, but the implications are far-reaching. An autoreset event is automatically reset to the nonsignaled state when a thread blocking on it is released. A manual-reset event doesn't reset automatically; it must be reset programmatically. The rules for choosing between autoreset and manual-reset events—and for using them once you've made your selection—are as follows:

It's vital to use a manual-reset event to trigger multiple threads. Why? Because an autoreset event would be reset the moment one of the threads was released and would therefore trigger just one thread. It's equally important to use PulseEvent to pull the trigger on a manual-reset event. If you use SetEvent and ResetEvent, you have no guarantee that all waiting threads will be released. PulseEvent not only sets and resets the event, but it also ensures that all threads waiting on the event are released before resetting the event.

An event is created by constructing a CEvent object. CEvent::CEvent accepts four parameters, all of them optional. It's prototyped as follows:

CEvent (BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)

The first parameter, bInitiallyOwn, specifies whether the eventobject is initially signaled (TRUE) or nonsignaled (FALSE). The default is fine in most cases. bManualReset specifies whether the event is a manual-reset event (TRUE) or an autoreset event (FALSE). The third parameter, lpszName, assigns a name to the event object. Like mutexes, events can be used to coordinate threads running in different processes, and for an event to span process boundaries, it must be assigned a name. If the threads that use the event belong to the same process, lpszName should be NULL. The final parameter, lpsaAttribute, is a pointer to a SECURITY_ATTRIBUTES structure describing the object's security attributes. NULL accepts the default security attributes, which are appropriate for most applications.

So how do you use events to synchronize threads? Here's an example involving one thread (thread A) that fills a buffer with data and another thread (thread B) that does something with that data. Assume that thread B must wait for a signal from thread A saying that the buffer is initialized and ready to go. An autoreset event is the perfect tool for the job:

// Global data CEvent g_event; // Autoreset, initially nonsignaled // Thread A InitBuffer (&buffer); // Initialize the buffer. g_event.SetEvent (); // Release thread B. // Thread B g_event.Lock (); // Wait for the signal.

Thread B calls Lock to block on the event object. Thread A calls SetEvent when it's ready to release thread B. Figure 17-4 shows what happens as a result.

Figure 17-4. Triggering a thread with an autoreset event.

The lone parameter passed to Lock specifies how long the caller is willing to wait, in milliseconds. The default is INFINITE, which means wait as long as necessary. A nonzero return value means that Lock returned because the object became signaled; 0 means that the time-out period expired or an error occurred. MFC isn't doing anything fancy here. It's simply recasting the kernel's thread synchronization objects and the API functions that operate on them in a more object-oriented mold.

Autoreset events are fine for triggering single threads, but what if a thread C running in parallel with thread B does something entirely different with the buffered data? You need a manual-reset event to release B and C together because an autoreset event would release one or the other but not both. Here's the code to trigger two or more threads with a manual-reset event:

// Global data CEvent g_event (FALSE, TRUE); // Nonsignaled, manual-reset // Thread A InitBuffer (&buffer); // Initialize the buffer. g_event.PulseEvent (); // Release threads B and C. // Thread B g_event.Lock (); // Wait for the signal. // Thread C g_event.Lock (); // Wait for the signal.

Notice that thread A uses PulseEvent to pull the trigger, in accordance with the second of the two rules prescribed above. Figure 17-5 illustrates the effect of using a manual-reset event to trigger two threads.

Figure 17-5. Triggering two threads with a manual-reset event.

To reiterate, use autoreset events and CEvent::SetEvent to release single threads blocking on an event, and use manual-reset events and CEvent::PulseEvent to release multiple threads. Abide by these simple rules and events will serve you capably and reliably.

Sometimes events aren't used as triggers but as primitive signaling mechanisms. For example, maybe thread B wants to know whether thread A has completed some task, but it doesn't want to block if the answer is no. Thread B can check the state of an event without blocking by passing ::WaitForSingleObject the event handle and a time-out value of 0. The event handle can be retrieved from a CEvent's m_hObject data member:

if (::WaitForSingleObject (g_event.m_hObject, 0) == WAIT_OBJECT_0) { // The event is signaled. } else { // The event is not signaled. }

One caveat to be aware of when using an event in this manner is that if thread B will be checking the event repeatedly until it becomes set, make sure that the event is a manual-reset event and not an autoreset event. Otherwise, the very act of checking the event will reset it.

Semaphores

The fourth and final type of synchronization object is the semaphore. Events, critical sections, and mutexes are "all or nothing" objects in the sense that Lock blocks on them if any other thread has them locked. Semaphores are different. Semaphores maintain resource counts representing the number of resources available. Locking a semaphore decrements its resource count, and unlocking a semaphore increments the resource count. A thread blocks only if it tries to lock a semaphore whose resource count is 0. In that case, the thread blocks until another thread unlocks the semaphore and thereby raises the resource count or until a specified time-out period has elapsed. Semaphores can be used to synchronize threads within a process or threads that belong to different processes.

MFC represents semaphores with instances of the class CSemaphore. The statement

CSemaphore g_semaphore (3, 3);

constructs a semaphore object that has an initial resource count of 3 (parameter 1) and a maximum resource count of 3 (parameter 2). If the semaphore will be used to synchronize threads in different processes, you should include a third parameter assigning the semaphore a name. An optional fourth parameter points to a SECURITY_ATTRIBUTES structure (default=NULL). Each thread that accesses a resource controlled by a semaphore can do so like this:

g_semaphore.Lock (); // Access the shared resource. g_semaphore.Unlock ();

As long as no more than three threads try to access the resource at the same time, Lock won't suspend the thread. But if the semaphore is locked by three threads and a fourth thread calls Lock, that thread will block until one of the other threads calls Unlock. (See Figure 17-6.) To limit the time that Lock will wait for the semaphore's resource count to become nonzero, you can pass a maximum wait time (in milliseconds, as always) to the Lock function.

Figure 17-6. Using a semaphore to guard a shared resource.

CSemaphore::Unlock can be used to increment the resource count by more than 1 and also to find out what the resource count was before Unlock was called. For example, suppose the same thread calls Lock twice in succession to lay claim to two resources guarded by a semaphore. Rather than call Unlock twice, the thread can do its unlocking like this:

LONG lPrevCount; g_semaphore.Unlock (2, &lPrevCount);

There are no functions in either MFC or the API that return a semaphore's resource count other than CSemaphore::Unlock and its API equivalent, ::ReleaseSemaphore.

A classic use for semaphores is to allow a group of m threads to share n resources, where m is greater than n. For example, suppose you launch 10 worker threads and charge each with the task of gathering data. Whenever a thread fills a buffer with data, it transmits the data through an open socket, clears the buffer, and starts gathering data again. Now suppose that only three sockets are available at any given time. If you guard the socket pool with a semaphore whose resource count is 3 and code each thread so that it locks the semaphore before claiming a socket, threads will consume no CPU time while they wait for a socket to come free.

The CSingleLock and CMultiLock Classes

MFC includes a pair of classes named CSingleLock and CMultiLock that have Lock and Unlock functions of their own. You can wrap a critical section, mutex, event, or semaphore in a CSingleLock object and use CSingleLock::Lock to apply a lock, as demonstrated here:

CCriticalSection g_cs; CSingleLock lock (&g_cs); // Wrap it in a CSingleLock. lock.Lock (); // Lock the critical section.

Is there any advantage to locking a critical section this way instead of calling the CCriticalSection object's Lock function directly? Sometimes, yes. Consider what happens if the following code throws an exception between the calls to Lock and Unlock:

g_cs.Lock (); g_cs.Unlock ();

If an exception occurs, the critical section will remain locked forever because the call to Unlock will be bypassed. But look what happens if you architect your code this way:

CSingleLock lock (&g_cs); lock.Lock (); lock.Unlock ();

The critical section won't be left permanently locked. Why? Because the CSingleLock object is created on the stack, its destructor is called if an exception is thrown. CSingleLock's destructor calls Unlock on the contained synchronization object. In other words, CSingleLock is a handy tool for making sure that a locked synchronization object gets unlocked even in the face of inopportune exceptions.

CMultiLock is an altogether different animal. By using a CMultiLock, a thread can block on up to 64 synchronization objects at once. And depending on how it calls CMultiLock::Lock, the thread can block until one of the synchronization objects comes free or until all of them come free. The following example demonstrates how a thread can block on two events and one mutex simultaneously. Be aware of the fact that events, mutexes, and semaphores can be wrapped in CMultiLock objects, but critical sections can't.

CMutex g_mutex; CEvent g_event[2]; CSyncObject* g_pObjects[3] = { &g_mutex, &g_event[0], &g_event[1] }; // Block until all three objects become signaled. CMultiLock multiLock (g_pObjects, 3); multiLock.Lock (); // Block until one of the three objects becomes signaled. CMultiLock multiLock (g_pObjects, 3); multiLock.Lock (INFINITE, FALSE);

CMultiLock::Lock accepts three parameters, all of which are optional. The first specifies a time-out value (default=INFINITE). The second specifies whether the thread should be awakened when one of the synchronization objects becomes unlocked (FALSE) or when all of them come unlocked (TRUE, the default). The third is a wakeup mask that specifies other conditions that will wake up the thread—for example, WM_PAINT messages or mouse-button messages. The default wakeup mask value of 0 prevents the thread from being awakened for any reason other than that the synchronization object (or objects) came free or the time-out period expired.

If a thread comes unblocked after calling CMultiLock::Lock to block until just one synchronization object becomes signaled, it's very often the case that the thread will need to know which synchronization object became signaled. The answer can be ascertained from Lock's return value:

CMutex g_mutex; CEvent g_event[2]; CSyncObject* g_pObjects[3] = { &g_mutex, &g_event[0], &g_event[1] }; CMultiLock multiLock (g_pObjects, 3); DWORD dwResult = multiLock.Lock (INFINITE, FALSE); DWORD nIndex = dwResult _ WAIT_OBJECT_0; if (nIndex == 0) { // The mutex became signaled. } else if (nIndex == 1) { // The first event became signaled. } else if (nIndex == 2) { // The second event became signaled. }

Be aware that if you pass Lock a time-out value other than INFINITE, you should compare the return value to WAIT_TIMEOUT before subtracting WAIT_OBJECT_0 in case Lock returned because the time-out period expired. Also, if Lock returns because an abandoned mutex became signaled, you must subtract WAIT_ABANDONED_0 from the return value instead of WAIT_OBJECT_0. For further details, consult the documentation for CMultiLock::Lock.

Here's one example of a situation in which CMultiLock can be useful. Suppose three separate threads—threads A, B, and C—are working together to prepare data in a buffer. Once the data is ready, thread D transmits the data through a socket or writes it to a file. However, thread D can't be called until threads A, B, and C have completed their work. The solution? Create separate event objects to represent threads A, B, and C, and let thread D use a CMultiLock object to block until all three events become signaled. As each thread completes its work, it sets the corresponding event object to the signaled state. Thread D therefore blocks until the last of the three threads signals that it's done.

Writing Thread-Safe Classes

MFC classes are thread-safe at the class level but not at the object level. Translated, this means that it's safe for two threads to access two separate instances of the same class but that problems could result if two threads are allowed to access the same instance at the same time. MFC's designers chose not to make it thread-safe at the object level for performance reasons. The simple act of locking an unlocked critical section can consume hundreds of clock cycles on a Pentium processor. If every access to an object of an MFC class locked a critical section, the performance of single-threaded applications would suffer needlessly.

To illustrate what it means for a class to be thread-safe, think about what might happen if two threads using the same CString object made no attempt to synchronize their actions. Let's say that thread A decides to set the string, whose name is g_strFileName, equal to the text string referenced by pszFile:

g_strFileName = pszFile;

At about the same time, thread B decides to display g_strFileName on the screen by passing it to CDC::TextOut:

pDC->TextOut (x, y, g_strFileName);

What gets displayed on the screen? The old value of g_strFileName or the new value? Maybe neither. Copying text to a CString object is a multistep operation that involves allocating buffer space to hold the text, performing a memcpy to copy the characters, setting the CString data member that stores the string length equal to the number of characters that were copied, adding a terminating 0 to the end, and so on. If thread B interrupts this process at the wrong moment, there's no telling what state the CString might be in when it's passed to TextOut. The output might be improperly truncated. Or TextOut might display garbage on the screen or cause an access violation.

One way to synchronize access to g_strFileName is to protect it with a critical section, as shown here:

// Global data CCriticalSection g_cs; // Thread A g_cs.Lock (); g_strFileName = pszFile; g_cs.Unlock (); // Thread B g_cs.Lock (); pDC->TextOut (x, y, g_strFileName); g_cs.Unlock ();

An alternative approach is to derive a class from CString and make the derived class thread-safe by building in a critical section that's automatically locked anytime an access occurs. Then the object itself ensures that accesses are performed in a thread-safe way, and it's no longer incumbent upon the application that uses the object to synchronize the actions of its threads.

Deriving a class and making it thread-safe is basically a matter of overriding every member function that reads or writes an object's data and wrapping calls to member functions in the base class with calls to lock and unlock a synchronization object that's a member of the derived class. Ditto for thread-safe classes that aren't derived from other classes but are designed from the ground up: add a CCriticalSection or CMutex data member to the class, and lock and unlock the synchronization object before and after every access.

It's not always possible to make a class entirely thread-safe. If a thread uses GetBuffer or an LPCTSTR operator to get a pointer to the text of a CString, for example, the CString itself has no control over what the caller does with that pointer. In that case, it's still the responsibility of the thread that uses the CString object to coordinate its accesses with those of other threads.

The point to take home from all of this is that objects are not thread-safe by default. You can use synchronization objects to access other objects in a thread-safe way, and you can develop classes that are inherently thread-safe by controlling access to objects created from those classes. But allowing one thread to read data from an object while another thread modifies the object's data—or vice versa—is a recipe for disaster. To make matters worse, errors of this nature often show up randomly in testing. You might run the application 1,000 times and never experience the debilitating effects of an overlapping access. But as sure as the possibility exists, someone using your application will experience a dual access that occurs at the worst possible moment and brings the entire application (and possibly the operating system, too) crashing to the ground.

The ImageEdit Application

The ImageEdit application shown in Figure 17-7 is an enhanced version of Chapter 15's Vista application, one that uses a separate thread to perform a complex image processing task in the background. When you select Convert To Gray Scale from the Effects menu, ImageEdit scans the current bitmap pixel by pixel, converts each pixel to a shade of gray, and adjusts the color palette to display an accurate gray-scale rendition of the original color image. The conversion function is an ideal candidate for a worker thread because it can take anywhere from a few seconds to several minutes to run, depending on the size of the bitmap, the speed of the CPU, and other factors. The code that performs the conversion is far from optimal; in fact, its speed could be improved by a factor of 10 or more if it were rewritten to operate directly on the bitmap's bits rather than to call CDC::GetPixel and CDC::SetPixel on every pixel. But for demonstration purposes, it's fine. And using CDC pixel functions to get and set pixel colors allows us to do in about 20 lines of code what could easily require several hundred if we rewrote ImageEdit to process raw bitmap data.

Figure 17-7. The ImageEdit window.

The bulk of ImageEdit's source code is reproduced in Figure 17-8. I wanted to show a multithreaded document/view application in this chapter because there are certain issues unique to writing multithreaded document/view programs that don't come up in multithreaded SDK applications or in multithreaded MFC applications that don't use documents and views. For example, it's not unusual for a document object to launch a worker thread to process the document's data. But how can a background thread let the document object know that processing is complete? It can't post a message to the document because a document isn't a window. It's a bad idea for the document to block on an event waiting for the thread to complete its mission, because doing so would block the application's primary thread and effectively suspend the message loop. Yet the document usually needs to know when the thread is finished so that it can update its views. The question is, How?

Figure 17-8. The ImageEdit application.

MainFrm.h

// MainFrm.h : interface of the CMainFrame class // /////////////////////////////////////////////////////////////////////////// #if !defined( AFX_MAINFRM_H__9D77AEE8_AA14_11D2_8E53_006008A82731__INCLUDED_) #define AFX_MAINFRM_H__9D77AEE8_AA14_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 class CMainFrame : public CFrameWnd { protected: // create from serialization only CMainFrame(); DECLARE_DYNCREATE(CMainFrame) // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation public: virtual ~CMainFrame(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // control bar embedded members CSpecialStatusBar m_wndStatusBar; // Generated message map functions protected: int m_nPercentDone; //AFX_MSG afx_msg LRESULT OnUpdateImageStats (WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnThreadUpdate (WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnThreadFinished (WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnThreadAborted (WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined(AFX_MAINFRM_H__9D77AEE8_AA14_11D2_8E53_006008A82731__INCLUDED_)

MainFrm.cpp

// MainFrm.cpp : implementation of the CMainFrame class // #include "stdafx.h" #include "ImageEdit.h" #include "ImageEditDoc.h" #include "SpecialStatusBar.h" #include "MainFrm.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CMainFrame IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //AFX_MSG_MAP ON_MESSAGE (WM_USER_UPDATE_STATS, OnUpdateImageStats) ON_MESSAGE (WM_USER_THREAD_UPDATE, OnThreadUpdate) ON_MESSAGE (WM_USER_THREAD_FINISHED, OnThreadFinished) ON_MESSAGE (WM_USER_THREAD_ABORTED, OnThreadAborted) END_MESSAGE_MAP() static UINT indicators[] = { ID_SEPARATOR, ID_SEPARATOR, ID_SEPARATOR }; /////////////////////////////////////////////////////////////////////////// // CMainFrame construction/destruction CMainFrame::CMainFrame() { m_nPercentDone = -1; } CMainFrame::~CMainFrame() { } int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndStatusBar.Create(this)) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } return 0; } BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; return TRUE; } /////////////////////////////////////////////////////////////////////////// // CMainFrame diagnostics #ifdef _DEBUG void CMainFrame::AssertValid() const { CFrameWnd::AssertValid(); } void CMainFrame::Dump(CDumpContext& dc) const { CFrameWnd::Dump(dc); } #endif //_DEBUG /////////////////////////////////////////////////////////////////////////// // CMainFrame message handlers BOOL CMainFrame::OnQueryNewPalette() { CDocument* pDoc = GetActiveDocument (); if (pDoc != NULL) GetActiveDocument ()->UpdateAllViews (NULL); return TRUE; } void CMainFrame::OnPaletteChanged(CWnd* pFocusWnd) { if (pFocusWnd != this) { CDocument* pDoc = GetActiveDocument (); if (pDoc != NULL) GetActiveDocument ()->UpdateAllViews (NULL); } } LRESULT CMainFrame::OnUpdateImageStats (WPARAM wParam, LPARAM lParam) { m_wndStatusBar.SetImageStats ((LPCTSTR) lParam); return 0; } LRESULT CMainFrame::OnThreadUpdate (WPARAM wParam, LPARAM lParam) { int nPercentDone = ((int) wParam * 100) / (int) lParam; if (nPercentDone != m_nPercentDone) { m_wndStatusBar.SetProgress (nPercentDone); m_nPercentDone = nPercentDone; } return 0; } LRESULT CMainFrame::OnThreadFinished (WPARAM wParam, LPARAM lParam) { CImageEditDoc* pDoc = (CImageEditDoc*) GetActiveDocument (); if (pDoc != NULL) { pDoc->ThreadFinished (); m_wndStatusBar.SetProgress (0); m_nPercentDone = -1; } return 0; } LRESULT CMainFrame::OnThreadAborted (WPARAM wParam, LPARAM lParam) { CImageEditDoc* pDoc = (CImageEditDoc*) GetActiveDocument (); if (pDoc != NULL) { pDoc->ThreadAborted (); m_wndStatusBar.SetProgress (0); m_nPercentDone = -1; } return 0; }

ImageEditDoc.h

// ImageEditDoc.h : interface of the CImageEditDoc class // /////////////////////////////////////////////////////////////////////////// #if !defined( AFX_IMAGEEDITDOC_H__9D77AEEA_AA14_11D2_8E53_006008A82731__INCLUDED_) #define AFX_IMAGEEDITDOC_H__9D77AEEA_AA14_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 UINT ThreadFunc (LPVOID pParam); LOGPALETTE* CreateGrayScale (); class CImageEditDoc : public CDocument { protected: // create from serialization only CImageEditDoc(); DECLARE_DYNCREATE(CImageEditDoc) // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation public: void ThreadAborted(); void ThreadFinished(); CPalette* GetPalette(); CBitmap* GetBitmap(); virtual ~CImageEditDoc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: CCriticalSection m_cs; CEvent m_event; HANDLE m_hThread; BOOL m_bWorking; CPalette m_palette; CBitmap m_bitmap; //AFX_MSG DECLARE_MESSAGE_MAP() }; /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_IMAGEEDITDOC_H__9D77AEEA_AA14_11D2_8E53_006008A82731__INCLUDED_)

ImageEditDoc.cpp

// ImageEditDoc.cpp : implementation of the CImageEditDoc class // #include "stdafx.h" #include "ImageEdit.h" #include "ImageEditDoc.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CImageEditDoc IMPLEMENT_DYNCREATE(CImageEditDoc, CDocument) BEGIN_MESSAGE_MAP(CImageEditDoc, CDocument) //AFX_MSG_MAP END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CImageEditDoc construction/destruction CImageEditDoc::CImageEditDoc() : m_event (FALSE, TRUE) // Manual-reset event, initially unowned { m_hThread = NULL; m_bWorking = FALSE; } CImageEditDoc::~CImageEditDoc() { } BOOL CImageEditDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; return TRUE; } /////////////////////////////////////////////////////////////////////////// // CImageEditDoc diagnostics #ifdef _DEBUG void CImageEditDoc::AssertValid() const { CDocument::AssertValid(); } void CImageEditDoc::Dump(CDumpContext& dc) const { CDocument::Dump(dc); } #endif //_DEBUG /////////////////////////////////////////////////////////////////////////// // CImageEditDoc commands BOOL CImageEditDoc::OnOpenDocument(LPCTSTR lpszPathName) { // // Return now if an image is being processed. // if (m_bWorking) { AfxMessageBox (_T ("You can't open an image while another is " \ "being converted")); return FALSE; } // // Let the base class do its thing. // if (!CDocument::OnOpenDocument (lpszPathName)) return FALSE; // // Open the file and create a DIB section from its contents. // HBITMAP hBitmap = (HBITMAP) ::LoadImage (NULL, lpszPathName, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE œ LR_CREATEDIBSECTION); if (hBitmap == NULL) { CString string; string.Format (_T ("%s does not contain a DIB"), lpszPathName); AfxMessageBox (string); return FALSE; } m_bitmap.Attach (hBitmap); // // Return now if this device doesn't support palettes. // CClientDC dc (NULL); if ((dc.GetDeviceCaps (RASTERCAPS) & RC_PALETTE) == 0) return TRUE; // // Create a palette to go with the DIB section. // if ((HBITMAP) m_bitmap != NULL) { DIBSECTION ds; m_bitmap.GetObject (sizeof (DIBSECTION), &ds); int nColors; if (ds.dsBmih.biClrUsed != 0) nColors = ds.dsBmih.biClrUsed; else nColors = 1 << ds.dsBmih.biBitCount; // // Create a halftone palette if the DIB section contains more // than 256 colors. // if (nColors > 256) m_palette.CreateHalftonePalette (&dc); // // Create a custom palette from the DIB section's color table // if the number of colors is 256 or less. // else { RGBQUAD* pRGB = new RGBQUAD[nColors]; CDC memDC; memDC.CreateCompatibleDC (&dc); CBitmap* pOldBitmap = memDC.SelectObject (&m_bitmap); ::GetDIBColorTable ((HDC) memDC, 0, nColors, pRGB); memDC.SelectObject (pOldBitmap); UINT nSize = sizeof (LOGPALETTE) + (sizeof (PALETTEENTRY) * (nColors - 1)); LOGPALETTE* pLP = (LOGPALETTE*) new BYTE[nSize]; pLP->palVersion = 0x300; pLP->palNumEntries = nColors; for (int i=0; i<nColors; i++) { pLP->palPalEntry[i].peRed = pRGB[i].rgbRed; pLP->palPalEntry[i].peGreen = pRGB[i].rgbGreen; pLP->palPalEntry[i].peBlue = pRGB[i].rgbBlue; pLP->palPalEntry[i].peFlags = 0; } m_palette.CreatePalette (pLP); delete[] pLP; delete[] pRGB; } } return TRUE; } void CImageEditDoc::DeleteContents() { if ((HBITMAP) m_bitmap != NULL) m_bitmap.DeleteObject (); if ((HPALETTE) m_palette != NULL) m_palette.DeleteObject (); CDocument::DeleteContents(); } CBitmap* CImageEditDoc::GetBitmap() { return ((HBITMAP) m_bitmap == NULL) ? NULL : &m_bitmap; } CPalette* CImageEditDoc::GetPalette() { return ((HPALETTE) m_palette == NULL) ? NULL : &m_palette; } void CImageEditDoc::ThreadFinished() { ASSERT (m_hThread != NULL); ::WaitForSingleObject (m_hThread, INFINITE); ::CloseHandle (m_hThread); m_hThread = NULL; m_bWorking = FALSE; // // Replace the current palette with a gray scale palette. // if ((HPALETTE) m_palette != NULL) { m_palette.DeleteObject (); LOGPALETTE* pLP = CreateGrayScale (); m_palette.CreatePalette (pLP); delete[] pLP; } // // Tell the view to repaint. // UpdateAllViews (NULL); } void CImageEditDoc::ThreadAborted() { ASSERT (m_hThread != NULL); ::WaitForSingleObject (m_hThread, INFINITE); ::CloseHandle (m_hThread); m_hThread = NULL; m_bWorking = FALSE; } void CImageEditDoc::OnGrayScale() { if (!m_bWorking) { m_bWorking = TRUE; m_event.ResetEvent (); // // Package data to pass to the image processing thread. // THREADPARMS* ptp = new THREADPARMS; ptp->pWnd = AfxGetMainWnd (); ptp->pBitmap = &m_bitmap; ptp->pPalette = &m_palette; ptp->pCriticalSection = &m_cs; ptp->pEvent = &m_event; // // Start the image processing thread and duplicate its handle. // CWinThread* pThread = AfxBeginThread (ThreadFunc, ptp, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); ::DuplicateHandle (GetCurrentProcess (), pThread->m_hThread, GetCurrentProcess (), &m_hThread, 0, FALSE, DUPLICATE_SAME_ACCESS); pThread->ResumeThread (); } else // // Kill the image processing thread. // m_event.SetEvent (); } void CImageEditDoc::OnUpdateGrayScale(CCmdUI* pCmdUI) { if (m_bWorking) { pCmdUI->SetText (_T ("Stop &Gray Scale Conversion")); pCmdUI->Enable (); } else { pCmdUI->SetText (_T ("Convert to &Gray Scale")); pCmdUI->Enable ((HBITMAP) m_bitmap != NULL); } } ///////////////////////////////////////////////////////////////////////// // Thread function and other globals UINT ThreadFunc (LPVOID pParam) { THREADPARMS* ptp = (THREADPARMS*) pParam; CWnd* pWnd = ptp->pWnd; CBitmap* pBitmap = ptp->pBitmap; CPalette* pPalette = ptp->pPalette; CCriticalSection* pCriticalSection = ptp->pCriticalSection; CEvent* pKillEvent = ptp->pEvent; delete ptp; DIBSECTION ds; pBitmap->GetObject (sizeof (DIBSECTION), &ds); int nWidth = ds.dsBm.bmWidth; int nHeight = ds.dsBm.bmHeight; // // Initialize one memory DC (memDC2) to hold a color copy of the // image and another memory DC (memDC1) to hold a gray scale copy. // CClientDC dc (pWnd); CBitmap bitmap1, bitmap2; bitmap1.CreateCompatibleBitmap (&dc, nWidth, nHeight); bitmap2.CreateCompatibleBitmap (&dc, nWidth, nHeight); CDC memDC1, memDC2; memDC1.CreateCompatibleDC (&dc); memDC2.CreateCompatibleDC (&dc); CBitmap* pOldBitmap1 = memDC1.SelectObject (&bitmap1); CBitmap* pOldBitmap2 = memDC2.SelectObject (&bitmap2); CPalette* pOldPalette1 = NULL; CPalette* pOldPalette2 = NULL; CPalette grayPalette; if (pPalette->m_hObject != NULL) { LOGPALETTE* pLP = CreateGrayScale (); grayPalette.CreatePalette (pLP); delete[] pLP; pOldPalette1 = memDC1.SelectPalette (&grayPalette, FALSE); pOldPalette2 = memDC2.SelectPalette (pPalette, FALSE); memDC1.RealizePalette (); memDC2.RealizePalette (); } // // Copy the bitmap to memDC2. // CDC memDC3; memDC3.CreateCompatibleDC (&dc); pCriticalSection->Lock (); CBitmap* pOldBitmap3 = memDC3.SelectObject (pBitmap); memDC2.BitBlt (0, 0, nWidth, nHeight, &memDC3, 0, 0, SRCCOPY); memDC3.SelectObject (pOldBitmap3); pCriticalSection->Unlock (); // // Convert the colors in memDC2 to shades of gray in memDC1. // int x, y; COLORREF crColor; BYTE grayLevel; for (y=0; y<nHeight; y++) { for (x=0; x<nWidth; x++) { crColor = memDC2.GetPixel (x, y); grayLevel = (BYTE) (((((UINT) GetRValue (crColor)) * 30) + (((UINT) GetGValue (crColor)) * 59) + (((UINT) GetBValue (crColor)) * 11)) / 100); memDC1.SetPixel (x, y, PALETTERGB (grayLevel, grayLevel, grayLevel)); } // // Kill the thread if the pKillEvent event is signaled. // if (::WaitForSingleObject (pKillEvent->m_hObject, 0) == WAIT_OBJECT_0) { memDC1.SelectObject (pOldBitmap1); memDC2.SelectObject (pOldBitmap2); if (pPalette->m_hObject != NULL) { memDC1.SelectPalette (pOldPalette1, FALSE); memDC2.SelectPalette (pOldPalette2, FALSE); } pWnd->PostMessage (WM_USER_THREAD_ABORTED, y + 1, 0); return (UINT) -1; } pWnd->SendMessage (WM_USER_THREAD_UPDATE, y + 1, nHeight); } // // Copy the gray scale image over the original bitmap. // CPalette* pOldPalette3 = NULL; if (pPalette->m_hObject != NULL) { pOldPalette3 = memDC3.SelectPalette (&grayPalette, FALSE); memDC3.RealizePalette (); } pCriticalSection->Lock (); pOldBitmap3 = memDC3.SelectObject (pBitmap); memDC3.BitBlt (0, 0, nWidth, nHeight, &memDC1, 0, 0, SRCCOPY); memDC3.SelectObject (pOldBitmap3); pCriticalSection->Unlock (); // // Clean up the memory DCs. // memDC1.SelectObject (pOldBitmap1); memDC2.SelectObject (pOldBitmap2); if (pPalette->m_hObject != NULL) { memDC1.SelectPalette (pOldPalette1, FALSE); memDC2.SelectPalette (pOldPalette2, FALSE); memDC3.SelectPalette (pOldPalette3, FALSE); } // // Tell the frame window we're done. // pWnd->PostMessage (WM_USER_THREAD_FINISHED, 0, 0); return 0; } LOGPALETTE* CreateGrayScale () { UINT nSize = sizeof (LOGPALETTE) + (sizeof (PALETTEENTRY) * 63); LOGPALETTE* pLP = (LOGPALETTE*) new BYTE[nSize]; pLP->palVersion = 0x300; pLP->palNumEntries = 64; for (int i=0; i<64; i++) { pLP->palPalEntry[i].peRed = i * 4; pLP->palPalEntry[i].peGreen = i * 4; pLP->palPalEntry[i].peBlue = i * 4; pLP->palPalEntry[i].peFlags = 0; } return pLP; }

ImageEditView.h

// ImageEditView.h : interface of the CImageEditView class // /////////////////////////////////////////////////////////////////////////// #if !defined( AFX_IMAGEEDITVIEW_H__9D77AEEC_AA14_11D2_8E53_006008A82731__INCLUDED_) #define AFX_IMAGEEDITVIEW_H__9D77AEEC_AA14_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 class CImageEditView : public CScrollView { protected: // create from serialization only CImageEditView(); DECLARE_DYNCREATE(CImageEditView) // Attributes public: CImageEditDoc* GetDocument(); // Operations public: // Overrides // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation public: virtual ~CImageEditView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: //AFX_MSG DECLARE_MESSAGE_MAP() }; #ifndef _DEBUG // debug version in ImageEditView.cpp inline CImageEditDoc* CImageEditView::GetDocument() { return (CImageEditDoc*)m_pDocument; } #endif /////////////////////////////////////////////////////////////////////////// // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_IMAGEEDITVIEW_H__9D77AEEC_AA14_11D2_8E53_006008A82731__INCLUDED_)

ImageEditView.cpp

// ImageEditView.cpp : implementation of the CImageEditView class // #include "stdafx.h" #include "ImageEdit.h" #include "ImageEditDoc.h" #include "ImageEditView.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CImageEditView IMPLEMENT_DYNCREATE(CImageEditView, CScrollView) BEGIN_MESSAGE_MAP(CImageEditView, CScrollView) //AFX_MSG_MAP END_MESSAGE_MAP() /////////////////////////////////////////////////////////////////////////// // CImageEditView construction/destruction CImageEditView::CImageEditView() { } CImageEditView::~CImageEditView() { } BOOL CImageEditView::PreCreateWindow(CREATESTRUCT& cs) { return CScrollView::PreCreateWindow(cs); } /////////////////////////////////////////////////////////////////////////// // CImageEditView drawing void CImageEditView::OnDraw(CDC* pDC) { CImageEditDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CBitmap* pBitmap = pDoc->GetBitmap (); if (pBitmap != NULL) { CPalette* pOldPalette; CPalette* pPalette = pDoc->GetPalette (); if (pPalette != NULL) { pOldPalette = pDC->SelectPalette (pPalette, FALSE); pDC->RealizePalette (); } DIBSECTION ds; pBitmap->GetObject (sizeof (DIBSECTION), &ds); CDC memDC; memDC.CreateCompatibleDC (pDC); CBitmap* pOldBitmap = memDC.SelectObject (pBitmap); pDC->BitBlt (0, 0, ds.dsBm.bmWidth, ds.dsBm.bmHeight, &memDC, 0, 0, SRCCOPY); memDC.SelectObject (pOldBitmap); if (pPalette != NULL) pDC->SelectPalette (pOldPalette, FALSE); } } void CImageEditView::OnInitialUpdate() { CScrollView::OnInitialUpdate (); CString string; CSize sizeTotal; CBitmap* pBitmap = GetDocument ()->GetBitmap (); // // If a bitmap is loaded, set the view size equal to the bitmap size. // Otherwise, set the view's width and height to 0. // if (pBitmap != NULL) { DIBSECTION ds; pBitmap->GetObject (sizeof (DIBSECTION), &ds); sizeTotal.cx = ds.dsBm.bmWidth; sizeTotal.cy = ds.dsBm.bmHeight; string.Format (_T ("\t%d x %d, %d bpp"), ds.dsBm.bmWidth, ds.dsBm.bmHeight, ds.dsBmih.biBitCount); } else { sizeTotal.cx = sizeTotal.cy = 0; string.Empty (); } AfxGetMainWnd ()->SendMessage (WM_USER_UPDATE_STATS, 0, (LPARAM) (LPCTSTR) string); SetScrollSizes (MM_TEXT, sizeTotal); } /////////////////////////////////////////////////////////////////////////// // CImageEditView diagnostics #ifdef _DEBUG void CImageEditView::AssertValid() const { CScrollView::AssertValid(); } void CImageEditView::Dump(CDumpContext& dc) const { CScrollView::Dump(dc); } CImageEditDoc* CImageEditView::GetDocument() // non-debug version is inline { ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CImageEditDoc))); return (CImageEditDoc*)m_pDocument; } #endif //_DEBUG /////////////////////////////////////////////////////////////////////////// // CImageEditView message handlers

SpecialStatusBar.h

// SpecialStatusBar.h: interface for the CSpecialStatusBar class. // ////////////////////////////////////////////////////////////////////// #if !defined( AFX_SPECIALSTATUSBAR_H__4BA7D301_AA24_11D2_8E53_006008A82731__INCLUDED_) #define AFX_SPECIALSTATUSBAR_H__4BA7D301_AA24_11D2_8E53_006008A82731__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 class CSpecialStatusBar : public CStatusBar { public: void SetProgress (int nPercent); void SetImageStats(LPCTSTR pszStats); CSpecialStatusBar(); virtual ~CSpecialStatusBar(); protected: CProgressCtrl m_wndProgress; afx_msg int OnCreate (LPCREATESTRUCT lpcs); afx_msg void OnSize (UINT nType, int cx, int cy); DECLARE_MESSAGE_MAP () }; #endif // !defined( // AFX_SPECIALSTATUSBAR_H__4BA7D301_AA24_11D2_8E53_006008A82731__INCLUDED_)

SpecialStatusBar.cpp

// SpecialStatusBar.cpp: implementation of the CSpecialStatusBar class. // ////////////////////////////////////////////////////////////////////// #include "stdafx.h" #include "ImageEdit.h" #include "SpecialStatusBar.h" #ifdef _DEBUG #undef THIS_FILE static char THIS_FILE[]=__FILE__; #define new DEBUG_NEW #endif ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// BEGIN_MESSAGE_MAP(CSpecialStatusBar, CStatusBar) ON_WM_CREATE () ON_WM_SIZE () END_MESSAGE_MAP() CSpecialStatusBar::CSpecialStatusBar() { } CSpecialStatusBar::~CSpecialStatusBar() { } int CSpecialStatusBar::OnCreate (LPCREATESTRUCT lpcs) { static UINT nIndicators[] = { ID_SEPARATOR, ID_SEPARATOR, ID_SEPARATOR }; if (CStatusBar::OnCreate (lpcs) == -1) return -1; // // Add panes to the status bar. // SetIndicators (nIndicators, sizeof (nIndicators) / sizeof (UINT)); // // Size the status bar panes. // TEXTMETRIC tm; CClientDC dc (this); CFont* pFont = GetFont (); CFont* pOldFont = dc.SelectObject (pFont); dc.GetTextMetrics (&tm); dc.SelectObject (pOldFont); int cxWidth; UINT nID, nStyle; GetPaneInfo (1, nID, nStyle, cxWidth); SetPaneInfo (1, nID, nStyle, tm.tmAveCharWidth * 24); GetPaneInfo (2, nID, nStyle, cxWidth); SetPaneInfo (2, nID, SBPS_NOBORDERS, tm.tmAveCharWidth * 24); // // Place a progress control in the rightmost pane. // CRect rect; GetItemRect (2, &rect); m_wndProgress.Create (WS_CHILD œ WS_VISIBLE œ PBS_SMOOTH, rect, this, -1); m_wndProgress.SetRange (0, 100); m_wndProgress.SetPos (0); return 0; } void CSpecialStatusBar::OnSize (UINT nType, int cx, int cy) { CStatusBar::OnSize (nType, cx, cy); // // Resize the rightmost pane to fit the resized status bar. // CRect rect; GetItemRect (2, &rect); m_wndProgress.SetWindowPos (NULL, rect.left, rect.top, rect.Width (), rect.Height (), SWP_NOZORDER); } void CSpecialStatusBar::SetImageStats(LPCTSTR pszStats) { SetPaneText (1, pszStats, TRUE); } void CSpecialStatusBar::SetProgress(int nPercent) { ASSERT (nPercent >= 0 && nPercent <= 100); m_wndProgress.SetPos (nPercent); }

ImageEdit demonstrates a practical solution to the problem of how a worker thread can let a document object know when it's finished. When Convert To Gray Scale is selected from the Effects menu, the document's OnGrayScale function launches a background thread that executes the ThreadFunc function. ThreadFunc processes the bitmap and posts a WM_USER_THREAD_FINISHED message to the application's frame window just before it terminates. The frame window, in turn, calls the document's ThreadFinished function to notify the document that the image has been converted, and ThreadFinished calls UpdateAllViews.

Posting a message to the frame window and having it call down to the document object is not the same as having the thread function call a function in the document object directly because the PostMessage call performs a virtual transfer of control to the primary thread. If ThreadFunc called the document object itself, UpdateAllViews would be called in the context of the background thread and would fail.

For good measure, ThreadFunc sends a WM_USER_THREAD_UPDATE message to the main window each time it finishes converting another line in the bitmap. The frame window responds by updating a progress control embedded in the status bar, so the user is never left wondering when the gray-scale image will appear. WM_USER_THREAD_UPDATE messages are sent rather than posted to make sure that the progress control is updated in real time. If WM_USER_THREAD_UPDATE messages were posted rather than sent, the background thread might post messages faster than the main window could process them on fast CPUs.

ImageEdit uses two thread synchronization objects: a CEvent object named m_event and a CCriticalSection object named m_cs. Both are members of the document class, and both are passed by address to the thread function in a THREADPARMS structure. The event object is used to terminate the worker thread if the user stops a gray-scale conversion midstream by selecting the Stop Gray Scale Conversion command from the Effects menu. To kill the thread, the primary thread sets the event to the signaled state:

m_event.SetEvent ();

Upon completion of each scan line, the conversion routine inside ThreadFunc checks the event object and terminates the thread if the event is signaled:

if (::WaitForSingleObject (pKillEvent->m_hObject, 0) == WAIT_OBJECT_0) { pWnd->PostMessage (WM_USER_THREAD_ABORTED, y + 1, 0); return (UINT) -1; }

The WM_USER_THREAD_ABORTED message alerts the frame window that the thread has been aborted. The frame window notifies the document by calling CImageEditDoc::ThreadAborted, and ThreadAborted blocks on the thread handle just in case the thread hasn't quite terminated. Then it resets an internal flag indicating that the thread is no longer running.

The critical section prevents the application's two threads from trying to select the bitmap into a device context at the same time. The primary thread selects the bitmap into a device context when the view needs updating; the background thread selects the bitmap into a memory device context once when a gray-scale conversion begins and again when it ends. A bitmap can be selected into only one device context at a time, so if either thread tries to select the bitmap into a device context while the other has it selected into a device context, one of the threads will fail. (Palettes, on the other hand, can be selected into several device contexts concurrently, and ThreadFunc takes advantage of that fact when it performs a gray-scale conversion on a palettized device.) The odds that the two threads will try to select the bitmap at the same time are small, but the use of a critical section ensures that the code executed between calls to SelectObject won't be interrupted by a call to SelectObject from another thread. The bitmap doesn't stay selected into a device context for any appreciable length of time, so neither thread should have to wait long if the critical section is locked.

ImageEdit also demonstrates how to place a progress control in a status bar. ImageEdit's status bar is an instance of CSpecialStatusBar, which I derived from CStatusBar. CSpecialStatusBar::OnCreate adds three panes to the status bar. Then it creates a progress control and positions the control to exactly fit the rightmost pane. Because the sizes and positions of a status bar's panes can change when the status bar is resized, CSpecialStatusBar also includes an OnSize handler that adjusts the progress control to the rightmost pane. The result is a progress control that looks like an ordinary status bar pane until you begin stepping it with CProgressCtrl::SetPos.

Категории