您正在查看: MFC 分类下的文章

Thread Synchronization

Thread Synchronization

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:

  • Critical sections
  • Mutexes
  • Events
  • Semaphores

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:

  • If just one thread will be triggered by the event, use an autoreset event and release the waiting thread with SetEvent. There's no need to call ResetEvent because the event is reset automatically the moment the thread is released.
  • If two or more threads will be triggered by the event, use a manual-reset event and release all waiting threads with PulseEvent. Once more, you don't need to call ResetEvent because PulseEvent resets the event for you after releasing the threads.

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(CMainFrame)
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
//}}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(CMainFrame)
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg BOOL OnQueryNewPalette();
afx_msg void OnPaletteChanged(CWnd* pFocusWnd);
//}}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()
};

///////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// 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(CMainFrame)ON_WM_CREATE()
ON_WM_QUERYNEWPALETTE()
ON_WM_PALETTECHANGED()
//}}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(CImageEditDoc)
public:
virtual BOOL OnNewDocument();
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
virtual void DeleteContents();
//}}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(CImageEditDoc)
afx_msg void OnGrayScale();
afx_msg void OnUpdateGrayScale(CCmdUI* pCmdUI);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

///////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// 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(CImageEditDoc)
ON_COMMAND(ID_EFFECTS_GRAY_SCALE, OnGrayScale)
ON_UPDATE_COMMAND_UI(ID_EFFECTS_GRAY_SCALE, OnUpdateGrayScale)
//}}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_)
#defineAFX_IMAGEEDITVIEW_H__9D77AEEC_AA14_11D2_8E53_006008A82731__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000class 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(CImageEditView)
public:
virtual void OnDraw(CDC* pDC); // overridden to draw this view
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
virtual void OnInitialUpdate(); // called first time after construct
//}}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(CImageEditView)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

#ifndef _DEBUG // debug version in ImageEditView.cpp
inline CImageEditDoc* CImageEditView::GetDocument()
{ return (CImageEditDoc*)m_pDocument; }
#endif

///////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// 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(CImageEditView)
//}}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.

MFC and the Component Object Model

Chapter 18

mfc and the Component Object Model

In the beginning, when mfc was still in its infancy, C++ programmers who began migrating from the Microsoft Windows API in favor of MFC did so because they wanted a class library to aid them in developing Windows applications. The conventional wisdom at the time said that MFC made Windows programming easier, but the truth of the matter was that Windows programming was still Windows programming. MFC simplified certain aspects of the development process, and for those few programmers prescient enough to adopt it early on, it eased the pain of porting 16-bit Windows applications to 32 bits. But even MFC could hardly claim to put a dent in the legendary Windows learning curve. That was true then, and it's still true today.

Today there is another, more compelling reason to use MFC. If the applications you develop have anything whatsoever to do with COM, OLE, or ActiveX, MFC can dramatically simplify the development process. By that, I mean MFC can cut the time required to develop an application (or a software component) by an order of magnitude. In this day and time, there is simply no good reason to develop certain types of software from scratch given that such good class libraries are available. COM, OLE, and ActiveX have been criticized for being overly complex and hopelessly arcane, but for better or worse, they're here to stay, and there's a very real chance that in the future you'll have to be a COM programmer if you want to program Windows.

So what are COM, OLE, and ActiveX, and what does MFC do to make them so much easier to program? I'm glad you asked, because the rest of this book is about MFC's support for all things COM. In this chapter, I'll begin by defining COM, OLE, and ActiveX, and then I'll introduce some of the unique and interesting ways in which MFC wraps its arms around them. In subsequent chapters, we'll tackle specific COM-based technologies such as Automation and ActiveX controls and you'll see how to use MFC to make them come to life.

The Component Object Model

The Component Object Model

COM is an acronym for Component Object Model. Simply put, COM is a way of building objects that is independent of any programming language. If you want the gory details, you can download the COM specification from Microsoft's Web site. But don't be too quick to pull out your browser: if this is your first exposure to COM, the specification might be a bit overwhelming. A better approach is to start slowly and allow yourself time to understand the big picture rather than risk getting mired in details that for the moment are unimportant.

C++ programmers are accustomed to writing classes that other C++ programmers can use. The problem with these classes is that only other C++ programmers can use them. COM tells us how to build objects in any programming language that can also be used in any programming language. In other words, COM transcends language-specific ways of building reusable objects and gives us a true binary standard for object architectures.

C++ classes have member functions; COM objects have methods. Methods are grouped into interfaces and are called through interface pointers. Interfaces exist to semantically bind together groups of related methods. For example, suppose you're writing a COM class that has methods named Add, Subtract, and CheckSpelling. Rather than make all three methods members of the same interface, you might assign Add and Subtract to an interface named IMath and CheckSpelling to an interface named ISpelling. (Prefacing interface names with a capital I for Interface is an almost universal COM programming convention.) Microsoft has predefined more than 100 interfaces that any COM object can support. These interfaces are called standard interfaces. User-defined interfaces such as IMath and ISpelling are custom interfaces. COM objects can use standard interfaces, custom interfaces, or a combination of the two.

Every COM object implements an interface named IUnknown. IUnknown contains just three methods:

Method Name Description
QueryInterface Returns a pointer to another interface
AddRef Increments the object's reference count
Release Decrements the object's reference count

One of the rules of COM says that given a pointer to an interface, a client can call any IUnknown method through that pointer as well as any methods that are specific to the interface. In other words, all interfaces must support the three IUnknown methods in addition to their own methods. This means that if you define an IMath interface with methods named Add and Subtract, the interface actually contains five methods: QueryInterface, AddRef, Release, Add, and Subtract. Most objects don't implement IUnknown as a separate interface. Because all interfaces include the IUnknown methods, most objects, if asked for an IUnknown pointer, simply return a pointer to one of their other interfaces.

Figure 18-1 shows a schematic of a simple COM object. The sticks, or "lollipops" as they're sometimes called, represent the object's interfaces. The IUnknown lollipop is often omitted because it's understood that every COM object implements IUnknown.

Figure 18-1. A simple COM object.

I've been using human-readable names such as IMath to refer to interfaces, but in truth, interfaces are identified by number, not by name. Every interface is uniquely identified by a 128-bit value called an interface identifier, or IID. So many different 128-bit numbers are possible that the chances of you and I ever picking the same IID at random are virtually nil. Therefore, it doesn't matter if two people on different sides of the planet happen to define incompatible versions of a custom interface named IMath. What counts is that the two IMath interfaces have different IIDs.

Microsoft Visual C++ comes with two tools for generating IIDs. One is a command line utility named Uuidgen. The other is a GUI application named Guidgen. Both utilities do their best to maximize the randomness of the 128-bit numbers they generate, even factoring in variables such as your network card's Ethernet ID and the time of day. You can generate IIDs programmatically with the COM API function CoCreateGuid. The Guid in CoCreateGuid stands for globally unique identifier, a generic term that describes any 128-bit identifier. An IID is simply a special GUID.

Instantiating a COM Object

COM classes, like interfaces, are identified by 128-bit values. GUIDs that identify classes are called class IDs, or CLSIDs. All a client needs to know in order to instantiate an object is the object's CLSID. COM has an API of its own that includes activation functions for creating object instances. The most commonly used activation function is CoCreateInstance, which accepts a CLSID and returns an interface pointer to an object. The following statements instantiate the COM class whose CLSID is CLSID_Object and cache a pointer to the object's IMath interface in pMath:

IMath* pMath;
CoCreateInstance (CLSID_Object, NULL,
 CLSCTX_SERVER, IID_IMath, (void**) &pMath);

IID_IMath is simply a variable that holds IMath's 128-bit interface ID.

Once it has an interface pointer, a C++ client can call methods on that interface using the -> operator. The following statements call IMath::Add to add a pair of numbers:

int sum;
pMath->Add (2, 2, &sum);

Add doesn't return the sum of the two inputs directly; instead, it copies the result to an address specified by the caller—in this case, to the variable named sum. That's because COM methods return special 32-bit values called HRESULTs. An HRESULT tells the caller whether a call succeeded or failed. It can also provide detailed information about the nature of the failure if the call doesn't succeed. You might think that a method as simple as Add can never fail, but it could fail if the object that implements the method is running on a remote network server and the client is unable to contact the server because a cable has been disconnected. If that happens, the system steps in and returns an HRESULT informing the caller that the call didn't go through.

One aspect of COM that newcomers frequently find confusing is the fact that every externally creatable COM class (that is, every COM class that can be instantiated by passing a CLSID to CoCreateInstance) is accompanied by a class object. A class object is also a COM object. Its sole purpose in life is to create other COM objects. Passing a CLSID to CoCreateInstance appears to instantiate an object directly, but internally, CoCreateInstance first instantiates the object's class object and then asks the class object to create the object. Most class objects implement a special COM interface known as IClassFactory (or IClassFactory2, a newer version of the interface that is a functional superset of IClassFactory). A class object that implements IClassFactory is called a class factory. Given an IClassFactory interface pointer, a client creates an object instance by calling IClassFactory::CreateInstance. This method— CreateInstance—has been described as the COM equivalent of the new operator in C++.

Not all COM classes are externally creatable. Some are intended only for private use and can't be instantiated with CoCreateInstance because they have no CLSIDs and no class factories. C++ programmers instantiate these objects by calling new on the C++ classes that implement the objects. Typically, these objects play a part in implementing a COM-based protocol such as drag-and-drop data transfers. Some of mfc's COM classes fit this profile. You'll learn more about them when we discuss the various COM and ActiveX technologies that mfc supports.

Object Lifetimes

C++ programmers are used to creating heap-based objects using the C++ new operator. They're also accustomed to calling delete to delete the objects that they create with new. COM differs from C++ in this respect, because clients create object instances but they don't delete them. Instead, COM objects delete themselves. Here's why.

Suppose two or more clients are using the same instance of an object. Client A creates the object, and Client B attaches to the object by somehow acquiring an interface pointer. If Client A, unaware that Client B exists, deletes the object, Client B is left with an interface pointer that no longer points to anything. Because a COM client typically doesn't know (and doesn't care) whether it's the sole user of an object or one of many users, COM leaves it up to the object to delete itself. Deletion occurs when an internal reference count maintained by the object drops to 0. The reference count is a running count of the number of clients holding pointers to the object's interfaces.

For COM classes implemented in C++, the reference count is typically stored in a member variable. The count is incremented when AddRef is called and decremented when Release is called. (Remember that because AddRef and Release are IUnknown methods, they can be called through any interface pointer.) Implementations of AddRef and Release are normally no more complicated than this:

ULONG __stdcall CComClass::AddRef ()
{
 return ++m_lRef;
}

ULONG __stdcall CComClass::Release ()
{
if (—m_lRef == 0) {delete this;return 0;
}
return m_lRef;
}

In this example, CComClass is a C++ class that represents a COM class. m_lRef is the member variable that holds the object's reference count. If every client calls Release when it's finished using an interface, the object conveniently deletes itself when the last client calls Release.

A bit of protocol is involved in using AddRef and Release. It's the responsibility of the object—not the client—to call AddRef whenever it hands out an interface pointer. However, it's the client's responsibility to call Release. Clients sometimes call AddRef themselves to indicate that they're making a copy of the interface pointer. In such cases, it's still up to the client (or whomever the client hands the copied interface pointer to) to call Release when the interface pointer is no longer needed.

Acquiring Interface Pointers

The CoCreateInstance example we examined earlier created an object and asked for an IMath interface pointer. Now suppose that the object also implements ISpelling. How would a client that holds an IMath pointer ask the object for an ISpelling pointer?

That's what the third of the three IUnknown methods is for. Given an interface pointer, a client can call QueryInterface through that pointer to get a pointer to any other interface that the object supports. Here's how it looks in code:

IMath* pMath;
HRESULT hr = CoCreateInstance (CLSID_Object, NULL,
 CLSCTX_SERVER, IID_IMath, (void**) &pMath);

if (SUCCEEDED (hr)) { // CoCreateInstance worked.
ISpelling* pSpelling;
hr = pMath->QueryInterface (IID_ISpelling, (void**) &pSpelling);
if (SUCCEEDED (hr)) {// Got the interface pointer! pSpelling->Release ();
}
pMath->Release ();
}

Notice that this time, the client checks the HRESULT returned by CoCreateInstance to make sure that the activation request succeeded. Sometime after the object is created, the client uses QueryInterface to request an ISpelling pointer, once more checking the HRESULT rather than simply assuming that the pointer is valid. (The SUCCEEDED macro tells a client whether an HRESULT code signifies success or failure. A related macro named FAILED can be used to test for failure.) Both interfaces are released when they're no longer needed. When Release is called through the IMath pointer, the object deletes itself if no other clients are holding interface pointers.

NOTE There is no COM function that you can call to enumerate all of an object's interfaces. The assumption is that the client knows what interfaces an object supports, so it can call QueryInterface to obtain pointers to any and all interfaces. An object can publish a list of the interfaces that it supports using a mechanism known as type information. Some COM objects make type information available to their clients, and some don't. Certain types of COM objects, ActiveX controls included, are required to publish type information. You'll see why when we examine the ActiveX control architecture in Chapter 21.

COM Servers

If COM is to create objects in response to activation requests, it must know where to find each object's executable file. An executable that implements a COM object is called a COM server. The HKEY_CLASSES_ROOT\CLSID section of the registry contains information that correlates CLSIDs and executable files. For example, if a server named MathSvr.exe implements Math objects and a client calls CoCreateInstance with Math's CLSID, COM looks up the CLSID in the registry, extracts the path to MathSvr.exe, and launches the EXE. The EXE, in turn, hands COM a class factory, and COM calls the class factory's CreateInstance method to create an instance of the Math object.

COM servers come in two basic varieties: in-process and out-of-process. In-process servers (often referred to as in-proc servers) are DLLs. They're called in-procs because in the win32 environment, a DLL loads and runs in the same address space as its client. EXEs, in contrast, run in separate address spaces that are physically isolated from one another. In most cases, calls to in-proc objects are very fast because they're little more than calls to other addresses in memory. Calling a method on an in-proc object is much like calling a subroutine in your own application.

Out-of-process servers (also known as out-of-proc servers) come in EXEs. One advantage to packaging COM objects in EXEs is that clients and objects running in two different processes are protected from one another if one crashes. The disadvantage is speed. Calls to objects in other processes are roughly 1,000 times slower than calls to in-proc objects because of the overhead incurred when a method call crosses process boundaries.

Microsoft Windows NT 4.0 introduced Distributed COM (DCOM), which gives out-of-proc servers the freedom to run on remote network servers. It's simple to take an out-of-proc server that has been written, tested, and debugged locally and deploy it on a network. (As of Windows NT 4.0 Service Pack 2, in-proc servers can also run remotely using a mechanism that relies on surrogate EXEs to host the DLLs.) CoCreateInstance and other COM activation functions are fully capable of creating objects that reside elsewhere on the network. Even legacy COM servers written before DCOM came into existence can be remoted with a few minor registry changes.

To differentiate out-of-proc servers that serve up objects on the same machine from out-of-proc servers that run on remote machines, COM programmers use the terms local server and remote server. A local server is an EXE that runs on the same machine as its client; a remote server, in contrast, runs elsewhere on the network. Although there are important structural differences between in-proc and out-of-proc servers, there are no differences between local and remote servers. Objects designed with DCOM in mind are often tweaked to leverage the operating system's underlying security model or to improve performance. But optimizations aside, the fact remains that local servers and remote servers share the exact same server and object architectures.

Location Transparency

One of COM's most powerful features is location transparency. Simply put, location transparency means that a client neither knows nor cares where an object lives. The exact same sequence of instructions that calls a method on an object running in the same address space as the client also calls a method on an object running in another process or even on another machine. A lot of magic goes on behind the scenes to make location transparency work, but COM handles the bulk of it.

When a method call goes out to an object in another process or on another machine, COM remotes the call. As part of the remoting process, COM marshals the method's parameters and return values. Marshaling comes in many forms, but the most common type of marshaling essentially reproduces the caller's stack frame in the call recipient's address space. Proxies and stubs carry out most marshaling and remoting. When a client is handed an interface pointer to an object running in a process other than its own, COM creates an interface proxy in the client process and an interface stub in the server process. Interface pointers held by the client are really interface pointers to the proxy, which implements the same interfaces and methods as the real object. When a client calls a method on the object, the call goes to the proxy, which uses some type of interprocess communication (IPC) to forward the call to the stub. The stub unpackages the method parameters, calls the object, and marshals any return values back to the proxy. Figure 18-2 illustrates the relationship between clients, objects, proxies, and stubs.

Figure 18-2. Proxies and stubs.

Where do proxies and stubs come from? If an object uses only standard interfaces, COM supplies the proxies and stubs. If an object uses custom interfaces, it's up to the object implementor to provide the proxies and stubs in the form of a proxy/stub DLL. The good news is that you rarely need to write a proxy/stub DLL by hand. Visual C++ comes with a tool called the MIDL (Microsoft Interface Definition Language) compiler that "compiles" IDL (Interface Definition Language) files, producing the source code for proxy/stub DLLs. The bad news is that now you have to learn another language—IDL. IDL has been called the lingua franca of COM. The better you know your IDL, the better equipped you are to optimize the performance of local and remote servers. You can avoid IDL and MIDL altogether by using an alternative marshaling strategy known as custom marshaling, but custom marshaling is so difficult to implement correctly that proxies and stubs are the way to go unless you have clear and compelling reasons to do otherwise. You can opt for other ways to avoid writing proxies and stubs if you're willing to make a few trade-offs in flexibility and performance. One of those other ways is Automation, which we'll discuss in Chapter 20.

The key to location transparency is the fact that when clients communicate with objects in other processes, they don't know that they're really communicating through proxies and stubs. All a client knows is that it has an interface pointer and that method calls through that interface pointer work. Now you know why.

Object Linking and Embedding

Before there was COM, there was object linking and embedding, better known by the acronym OLE. OLE allows you to place content objects created by one application in documents created by another application. One use for OLE is to place Excel spreadsheets inside Word documents. (See Figure 18-3.) In such a scenario, Excel acts as an OLE server by serving up an embedded or linked spreadsheet object (a "content object") and Word acts as an OLE container by hosting the object.

Figure 18-3. A Microsoft Excel chart embedded in a Microsoft Word document.

OLE is a complex software protocol that describes how OLE servers talk to OLE containers and vice versa. Microsoft built OLE 1.0 on top of Dynamic Data Exchange (DDE). DDE proved to be a less than ideal IPC mechanism, so Microsoft invented COM to serve as the underlying IPC mechanism for OLE 2.0. For a long time, Microsoft affixed the OLE label to all new COM technologies: Automation became OLE Automation, ActiveX controls were named OLE controls, and so on. Microsoft even went so far as to say that OLE was no longer an acronym; it was a word. It wasn't until the term ActiveX was coined in 1995 that Microsoft reversed itself and said, in effect, "We've changed our minds; OLE once again stands for object linking and embedding." Despite this reversal, many programmers still (erroneously) use the terms COM and OLE interchangeably. They are not synonymous. COM is the object model that forms the foundation for all OLE and ActiveX technologies. OLE is the technology that allows you to place Excel spreadsheets inside Word documents. Get used to this new world order, and you'll avoid the confusion that has stricken so many programmers.

Just how does OLE use COM? When an OLE server such as Excel serves up a spreadsheet object to a container such as Word, it creates one or more COM objects that implement certain standard interfaces such as IOleObject and IViewObject. Word, too, creates COM objects that conform to published specifications. The architecture is generic in that it isn't limited only to Word and Excel; any application can be an OLE container or server, or both. The container and the server communicate by calling methods through interface pointers. Thanks to location transparency, it doesn't matter that the container and the server are running in different processes, although some of OLE's COM interfaces must be implemented in proc to work around certain limitations of Windows. Because device context handles aren't portable between processes (for example, when a container asks a server to draw an object in the container's window), that part of the server must be implemented in proc.

Figure 18-4 shows a schematic of a simple embedding container. For each content object embedded in the container's document, the container implements a site object. At a minimum, a site object must implement the COM interfaces IOleClientSite and IAdviseSink. To talk to the container, the server calls methods through pointers to these interfaces. The simplicity of this diagram belies the inward complexity of real-life linking and embedding servers, but it nonetheless illustrates the role that COM plays as an enabling technology.

Figure 18-4. A simple embedding container.

For the record, linked objects and embedded objects are fundamentally different. Embedded objects are stored in the container's document file alongside the container's native document data. Linked objects, on the other hand, are stored in external files. The container's document stores only a link to the object, which is a fancy way of saying that the container stores the name of and path to the file that holds the object's data. Links can be more sophisticated than that. If you create a link to a range of cells in an Excel spreadsheet, for example, the link includes information identifying the range as well as the path to the file.

Active Documents

In my opinion, OLE is the least interesting of all the COM technologies that Microsoft has defined, so I won't cover it further in this book. (If you want to learn more about it, start with the OLE lessons in the Scribble tutorial that comes with Visual C++.) However, one COM-based technology that has grown out of OLE at least deserves mention because it is potentially very useful. That technology is Active Documents.

The Active Documents protocol is a superset of object linking and embedding. It permits Active Document containers such as Microsoft Internet Explorer to open document files created by Active Document servers such as Word and Excel. Ever notice how you can open a Word DOC file or an Excel XLS file inside Internet Explorer? Internet Explorer appears to understand the Word and Excel file formats. It doesn't. What's really happening is that Internet Explorer talks to Word or Excel through—you guessed it—COM interfaces. Word or Excel runs in the background (you can prove that by viewing the task list while a DOC or XLS file is open in Internet Explorer) and essentially takes over the interior of Internet Explorer's window. You're really using Word or Excel, although it certainly doesn't look that way.

Active Documents really pay off when you post a Word or an Excel document on a Web site. If the machine on which Internet Explorer is running has Word and Excel installed, you can view DOC and XLS files as effortlessly as you do html pages. That's Active Documents at work.

ActiveX

First there was OLE. Next there was COM. And then along came ActiveX. When Microsoft turned its attention to the Internet in 1995, the software giant coined the term ActiveX to refer to a suite of COM-based technologies designed to make the Internet—and the World Wide Web in particular—more interactive. ActiveX controls are probably the best-known ActiveX technology, but there are others. If "Active" is in the name, it's an ActiveX technology: ActiveX controls, ActiveX Data Objects (ADO), Active Server Pages (ASP), and Active Documents, to name but a few. The roster is growing every day.

The one thing all ActiveX technologies have in common is that they're all COM-based. ActiveX controls, for example, are COM objects that conform to the rules of behavior set forth in Microsoft's OLE control (OCX) specifications. Applications that host ActiveX controls also implement COM interfaces; officially, they're known as ActiveX control containers.

Writing a full-blown ActiveX control—that is, one that can be plugged into a Web page or displayed in a window or a dialog box—is not a trivial undertaking. The ActiveX control architecture is complex. A typical ActiveX control implements more than a dozen COM interfaces, some of which contain more than 20 methods. Even something as seemingly simple as plugging an ActiveX control into a dialog box is far more complex than most people realize. To host an ActiveX control, a dialog box has to be an ActiveX control container, and containers must implement a number of COM interfaces of their own.

Fortunately, MFC does an excellent job of wrapping ActiveX controls and control containers. Check a box in AppWizard, and any dialog box instantly becomes a control container. You don't have to write a single line of code because MFC provides all the necessary infrastructure. MFC also simplifies ActiveX control development. Writing an ActiveX control from scratch can easily require two months of development time, but do it with MFC and you can write a fully functional control in a matter of hours. Why? Because MFC provides stock implementations of COM's ActiveX control interfaces. All you have to do is override a virtual function here and there and add the elements that make your control different from the rest.

IP Address Controls and Other Data-Entry Controls

IP Address Controls and Other Data-Entry Controls

IP address controls, hotkey controls, month calendar controls, and date-time picker controls all have one characteristic in common: they exist to make it easy to solicit specially formatted input from the user. Some of them, such as the IP address control, are exceedingly simple; others, such as the date-time picker control, offer an intimidating array of options. All are relatively easy to program, however, especially when you use the wrapper classes provided by mfc. The sections that follow provide an overview of all four control types and present code samples demonstrating their use.

IP Address Controls

IP address controls facilitate the effortless entry of 32-bit IP addresses consisting of four 8-bit integer values separated by periods, as in 10.255.10.1. The control accepts numeric input only and is divided into four 3-digit fields, as shown in Figure 16-12. When the user types three digits into a field, the input focus automatically moves to the next field. IP address controls exist only on systems that have Internet Explorer 4.0 or later installed.

Figure 16-12. The IP address control.

mfc codifies the interface to IP address controls with CIPAddressCtrl. CIPAddressCtrl functions named SetAddress and GetAddress get IP addresses in and out. If m_wndIPAddress is a CIPAddressCtrl data member in a dialog class, the following OnInitDialog and OnOK functions initialize the control with the IP address stored in m_nField1 through m_nField4 when the dialog box is created and retrieve the IP address from the control when the dialog box is dismissed:

// In CMyDialog's class declaration
BYTE m_nField1, m_nField2, m_nField3, m_nField4;BOOL CMyDialog::OnInitDialog ()
{
 CDialog::OnInitDialog ();
 m_wndIPAddress.SetAddress (m_nField1, m_nField2,m_nField3, m_nField4);
 return TRUE;
}

void CMyDialog::OnOK ()
{
m_wndIPAddress.GetAddress (m_nField1, m_nField2,m_nField3, m_nField4);
CDialog::OnOK ();
}

You can also empty an IP address control with CIPAddressCtrl::ClearAddress or find out whether it is currently empty with CIPAddressCtrl::IsBlank. Another CIPAddressCtrl member function, SetFieldFocus, programmatically moves the input focus to a specified field.

By default, each field in an IP address control accepts a value from 0 to 255. You can change the range of values that a given field will accept with CIPAddressCtrl::SetFieldRange. The following statement configures the control to restrict values entered into the control's first field to 10 through 100 and values entered in the final field to 100 through 155, inclusive:

m_wndIPAddress.SetFieldRange (0,  10, 100); // Field 1
m_wndIPAddress.SetFieldRange (3, 100, 155); // Field 4

The control prevents invalid values from being entered into a field by automatically converting values that fall outside the allowable range to the upper or lower limit of that range, whichever is appropriate.

IP address controls send four types of notifications to their owners. EN_SETFOCUS and EN_KILLFOCUS notifications signify that the control gained or lost the input focus. EN_CHANGE notifications indicate that the data in the control has changed. All three notifications are encapsulated in WM_COMMAND messages. IP address controls also send IPN_FIELDCHANGED notifications when a field's value changes or the input focus moves from one field to another. IPN_FIELDCHANGED is unique among IP address control notifications in that it is transmitted in WM_NOTIFY messages.

Hotkey Controls

Hotkey controls are similar in concept to IP address controls. The chief difference is that hotkey controls accept key combinations instead of IP addresses. A hotkey control is essentially a glorified edit control that automatically converts key combinations such as Ctrl-Alt-P into text strings suitable for displaying on the screen. Hotkey controls are so-called because the key combinations entered in them are sometimes converted into hotkeys with WM_SETHOTKEY messages. Data entered into a hotkey control doesn't have to be used for hotkeys, however; it can be used any way that you, the developer, see fit.

MFC represents hotkey controls with instances of CHotKeyCtrl. Member functions named SetHotKey and GetHotKey convert key combinations into text strings displayed by the control, and vice versa. The following statement initializes a hotkey control represented by the CHotKeyCtrl object m_wndHotkey with the key combination Ctrl-Alt-P. The control responds by displaying the text string "Ctrl + Alt + P":

m_wndHotkey.SetHotKey (_T (`P'), HOTKEYF_CONTROL ¦ HOTKEYF_ALT);

The next two statements read data from the hotkey control into variables named wKeyCode, which holds a virtual key code, and wModifiers, which holds bit flags specifying which, if any, modifier keys—Ctrl, Alt, and Shift—are included in the key combination:

WORD wKeyCode, wModifiers;
m_wndHotkey.GetHotKey (wKeyCode, wModifiers);

You can include similar calls to SetHotKey and GetHotKey in a dialog class's OnInitDialog and OnOK functions to transfer data between a hotkey control and data members of the dialog class.

By default, a hotkey control accepts key combinations that include any combination of the Ctrl, Shift, and Alt keys. You can restrict the combinations that the control will accept by calling CHotKeyCtrl::SetRules. SetRules accepts two parameters: an array of bit flags identifying invalid combinations of Ctrl, Shift, and Alt, and an array of bit flags specifying the combination of Ctrl, Shift, and Alt that should replace an invalid combination of modifier keys. For example, the statement

m_wndHotkey.SetRules (HKCOMB_A ¦ HKCOMB_CA ¦ HKCOMB_SA ¦ HKCOMB_SCA, 0);

disallows any key combination that includes the Alt key, and the statement

m_wndHotkey.SetRules (HKCOMB_A ¦ HKCOMB_CA ¦ HKCOMB_SA ¦ HKCOMB_SCA,
 HOTKEYF_CONTROL);

does the same but also directs the control to replace the modifiers in any key combination that includes the Alt key with the Ctrl key. See the SetRules documentation for a list of other supported HKCOMB flags.

Month Calendar Controls

The month calendar control, which I'll refer to simply as the calendar control, lets users input dates by picking them from a calendar rather than typing them into an edit control. (See Figure 16-13.) A calendar control can support single selections or multiple selections. Clicking a date in a single-selection calendar control makes that date the "current date." In a multiple-selection calendar control, the user can select a single date or a contiguous range of dates. You can set and retrieve the current selection, be it a single date or a range of dates, programmatically by sending messages to the control. MFC wraps these and other calendar control messages in member functions belonging to the CMonthCalCtrl class.

Figure 16-13. The month calendar control.

In a single-selection calendar control, CMonthCalCtrl::SetCurSel sets the current date and CMonthCalCtrl::GetCurSel retrieves it. The statement

m_wndCal.SetCurSel (CTime (1999, 9, 30, 0, 0, 0));

sets the current date to September 30, 1999, in the calendar control represented by m_wndCal. Ostensibly, the statements

CTime date;
m_wndCal.GetCurSel (date);

retrieve the date from the control by initializing date with the currently selected date. But watch out. Contrary to what the documentation says, a calendar control sometimes returns random data in the hours, minutes, seconds, and milliseconds fields of the SYSTEMTIME structure it uses to divulge dates in response to MCM_GETCURSEL messages. Because CTime factors the time into the dates it obtains from SYSTEMTIME structures, incrementing the day by 1, for example, if hours equals 25, CTime objects initialized by CMonthCalCtrl::GetCurSel can't be trusted. The solution is to retrieve the current date by sending the control an MCM_GETCURSEL message and zeroing the time fields of the SYSTEMTIME structure before converting it into a CTime, as demonstrated here:

SYSTEMTIME st;
m_wndCal.SendMessage (MCM_GETCURSEL, 0, (LPARAM) &st);
st.wHour = st.wMinute = st.wSecond = st.wMilliseconds = 0;
CTime date (st);

If you prefer, you can also use CMonthCalCtrl's SetRange function to place upper and lower bounds on the dates that the control will allow the user to select.

The alternative to SetCurSel and GetCurSel is to use DDX to get dates in and out of a calendar control. MFC includes a DDX function named DDX_MonthCalCtrl that you can put in a dialog's DoDataExchange function to automatically transfer data between a calendar control and a CTime or COleDateTime data member. It even includes DDV functions for date validation. But guess what? DDX_MonthCalCtrl doesn't work because it uses GetCurSel to read the current date. Until this bug is fixed, your best recourse is to forego DDX and use the techniques described above to get and set the current date.

You can create a calendar control that allows the user to select a range of contiguous dates by including an MCS_MULTISELECT bit in the control's style. By default, a selection can't span more than 7 days. You can change that with CMonthCalCtrl::SetMaxSelCount. The statement

m_wndCal.SetMaxSelCount (14);

sets the upper limit on selection ranges to 14 days. The complementary GetMaxSelCount function returns the current maximum selection count.

To programmatically select a date or a range of dates in a multiple-selection calendar control, you must use CMonthCalCtrl::SetSelRange instead of CMonthCalCtrl::SetCurSel. (The latter fails if it's called on a multiple-selection calendar control.) The statements

m_wndCal.SetSelRange (CTime (1999, 9, 30, 0, 0, 0),
 CTime (1999, 9, 30, 0, 0, 0));

select September 30, 1999, in an MCS_MULTISELECT-style calendar control, and the statements

m_wndCal.SetSelRange (CTime (1999, 9, 16, 0, 0, 0),
 CTime (1999, 9, 30, 0, 0, 0));

select September 16 through September 30. This call will fail unless you first call SetMaxSelCount to set the maximum selection range size to 15 days or higher. To read the current selection, use CMonthCalCtrl::GetSelRange as demonstrated here:

CTime dateStart, dateEnd;
m_wndCal.GetSelRange (dateStart, dateEnd);

This example sets dateStart equal to the selection's start date and dateEnd to the end date. If just one day is selected, dateStart will equal dateEnd. Fortunately, GetSelRange doesn't suffer from the randomness problems that GetCurSel does.

Three calendar control styles allow you to alter a calendar control's appearance. MCS_NOTODAY removes the line that displays today's date at the bottom of the calendar; MCS_NOTODAYCIRCLE removes the circle that appears around today's date in the body of the calendar; and MCS_WEEKNUMBERS displays week numbers (1 through 52). You can further modify a calendar's appearance with CMonthCalCtrl functions. For example, you can change today's date (as displayed by the control) with SetToday, the day of the week that appears in the calendar's leftmost column with SetFirstDayOfWeek, and the control's colors with SetColor. You can even command the control to display certain dates in boldface type by calling its SetDayState function or processing MCN_GETDAYSTATE notifications. Be aware that SetDayState works (and MCN_GETDAYSTATE notifications are sent) only if MCS_DAYSTATE is included in the control style.

If you'd like to know when the current date (or date range) changes in a calendar control, you can process either of two notifications. MCN_SELECT notifications are sent when the user selects a new date or range of dates. MCN_SELCHANGE notifications are sent when the user explicitly makes a selection and when the selection changes because the user scrolled the calendar backward or forward a month. In an MFC application, you can map these notifications to member functions in the parent window class with ON_NOTIFY or reflect them to functions in a derived control class with ON_NOTIFY_REFLECT.

Date-Time Picker Controls

Date-time picker controls, or DTP controls, provide developers with a simple, convenient, and easy-to-use means for soliciting dates and times from a user. A DTP control resembles an edit control, but rather than display ordinary text strings, it displays dates and times. Dates can be displayed in short format, as in 9/30/99, or long format, as in Thursday, September 30, 1999. Times are displayed in standard HH:MM:SS format followed by AM or PM. Custom date and time formats are also supported. Times and dates can be edited visually—for example, by clicking the control's up and down arrows or picking from a drop-down calendar control—or manually. MFC simplifies the interface to DTP controls with the wrapper class named CDateTimeCtrl.

Using a DTP control to solicit a time requires just one or two lines of code. First you assign the control the style DTS_TIMEFORMAT to configure it to display times rather than dates. Then you call CDateTimeCtrl::SetTime to set the time displayed in the control and CDateTimeCtrl::GetTime when you're ready to retrieve it. Assuming m_wndDTP is a CDateTimeCtrl data member in a dialog class and that m_wndDTP is mapped to a DTP control in the dialog, the following OnInitDialog and OnOK functions transfer data between the control and a CTime member variable in the dialog class:

// In CMyDialog's class declaration
CTime m_time;BOOL CMyDialog::OnInitDialog ()
{
 CDialog::OnInitDialog ();
 m_wndDTP.SetTime (&m_time);
 return TRUE;
}

void CMyDialog::OnOK ()
{
m_wndDTP.GetTime (m_time);
CDialog::OnOK ();
}

Rather than call SetTime and GetTime explicitly, you can use a DDX_DateTimeCtrl statement in the dialog's DoDataExchange function instead:

DDX_DateTimeCtrl (pDX, IDC_DTP, m_time);

If you use DDX_DateTimeCtrl to connect a DTP control to a dialog data member, you might also want to use MFC's DDV_MinMaxDateTime function to validate times retrieved from the control.

To display dates rather than times in a DTP control, replace DTS_TIMEFORMAT with either DTS_SHORTDATEFORMAT for short dates or DTS_LONGDATEFORMAT for long dates. You set and retrieve dates the same way you do times: with SetTime and GetTime or DDX_DateTimeCtrl. You can use CDateTimeCtrl::SetRange to limit the dates and times that a DTP control will accept.

A DTP control whose style includes DTS_UPDOWN has up and down arrows that the user can use to edit times and dates. If DTS_UPDOWN is omitted from the control style, a downward-pointing arrow similar to the arrow in a combo box replaces the up and down arrows. Clicking the downward-pointing arrow displays a drop-down calendar control, as illustrated in Figure 16-14. Thus, combining either of the date styles (DTS_SHORTDATEFORMAT or DTS_LONGDATEFORMAT) with DTS_UPDOWN produces a DTP control in which dates are entered using up and down arrows; using either of the date styles without DTS_UPDOWN creates a control in which dates are picked from a calendar. By default, a calendar dropped down from a DTP control is left-aligned with the control. You can alter the alignment by including DTS_RIGHTALIGN in the control style. You can also use the DTS_APPCANPARSE style to allow the user to manually edit the text displayed in a DTP control. Even without this style, the keyboard's arrow keys can be used to edit time and date entries.

Figure 16-14. Date-time picker controls with and without the style DTS_UPDOWN.

CDateTimeCtrl's SetFormat function assigns custom formatting strings to a DTP control. For example, a formatting string of the form "H': `mm': `ss" programs a DTP control to display the time in 24-hour military format. Here's how SetFormat would be used to apply this formatting string:

m_wndDTP.SetFormat (_T ("H\':\'mm\':\'ss"));

In a formatting string, H represents a one-digit or two-digit hour in 24-hour format, mm represents a two-digit minute, and ss represents a two-digit second. The following table shows all the special characters that you can use in formatting strings. You can include literals, such as the colons in the example above, by enclosing them in single quotation marks. If you really want to get fancy, you can use Xs to define callback fields. A DTP control uses DTN_FORMAT and DTN_FORMATQUERY notifications to determine what to display in a callback field, enabling an application that processes these notifications to provide text to a DTP control at run time.

DTP Formatting Characters

Character(s) Description
d One-digit or two-digit day
dd Two-digit day
ddd Three-character day of the week abbreviation (for example, Mon or Tue)
dddd Full day of the week name (for example, Monday or Tuesday)
h One-digit or two-digit hour in 12-hour format
hh Two-digit hour in 12-hour format
H One-digit or two-digit hour in 24-hour format
HH Two-digit hour in 24-hour format
m One-digit or two-digit minute
mm Two-digit minute
M One-digit or two-digit month
MM Two-digit month
MMM Three-character month abbreviation (for example, Jan or Feb)
MMMM Full month name (for example, January or February)
s One-digit or two-digit second
ss Two-digit second
t Displays A for a.m. or P for p.m.
tt Displays AM for a.m. or PM for p.m.
X Callback field
y One-digit year
yy Two-digit year
yyyy Four-digit year

DTP controls send a variety of other notifications to their parents. If you want to know when a drop-down calendar control is displayed, listen for DTN_DROPDOWN notifications. When a DTN_DROPDOWN notification arrives, you can call CDateTimeCtrl::GetMonthCalCtrl to acquire a CMonthCalCtrl pointer that you can use to modify the calendar control. If you simply want to know when the time or the date in a DTP control changes, process DTN_DATETIMECHANGE notifications. Consult the Platform SDK documentation on DTP controls for details concerning these and other DTP control notifications.

Threads and Thread Synchronization

Chapter 17

Threads and Thread Synchronization

In the Microsoft win32 environment, every running application constitutes a process and every process contains one or more threads of execution. A thread is a path of execution through a program's code, plus a set of resources (stack, register state, and so on) assigned by the operating system.

A fundamental difference between 16-bit and 32-bit versions of Microsoft Windows is that 32-bit Windows doesn't limit its applications to just one thread each. A process in a 32-bit Windows application begins its life as a single thread, but that thread can spawn additional threads. A preemptive scheduler in the operating system kernel divides CPU time among active threads so that they appear to run simultaneously. Threads are ideal for performing tasks in the background while processing user input in the foreground. They can also play more visible roles by creating windows and processing messages to those windows, just as the primary thread processes messages sent to an application's main window.

Multithreading isn't for everyone. Multithreaded applications are difficult to write and debug because the parallelism of concurrently running threads adds an extra layer of complexity to a program's code. Used properly, however, multiple threads can dramatically improve an application's responsiveness. A word processor that does its spell checking in a dedicated thread, for example, can continue to process messages in the primary thread and allow the user to continue to work while the spelling checker runs its course. What makes writing a threaded spelling checker difficult is that the spell checking thread will invariably have to synchronize its actions with other threads in the application. Most programmers have been conditioned to think about their code in synchronous terms—function A calls function B, function B performs some task and returns to A, and so on. But threads are asynchronous by nature. In a multithreaded application, you have to think about what happens if, say, two threads call function B at the same time or one thread reads a variable while another writes it. If function A launches function B in a separate thread, you also must anticipate the problems that could occur if function A continues to run while function B executes. For example, it's common to pass the address of a variable created on the stack in function A to function B for processing. But if function B is in another thread, the variable might no longer exist when function B gets around to accessing it. Even the most innocent-looking code can be fatally flawed when it involves two different threads.

mfc encapsulates threads of execution in the CWinThread class. It also encapsulates events, mutexes, and other win32 thread synchronization objects in easy-to-use C++ classes. Does mfc make multithreading easier? Not exactly. Developers who have written multithreaded Windows applications in C are often surprised to learn that MFC adds complexities all its own. The key to writing multithreaded programs in MFC is having a keen understanding of what you're doing and knowing where the trouble spots are. This chapter will help you do both.