Click here to Skip to main content
Click here to Skip to main content

Lengthy Operations Without Multiple Threads

, 1 Mar 2004
Rate this:
Please Sign up or sign in to vote.
A Templated Class, with callback functions, to Make It Easy
Introduction
An Incorrect Approach
An Approach That Works
Using Callback Functions
So, What's Wrong With This?
A Templated Class and a Dialog Template
The Final Class and How To Use It
A Word About The Demo Program
Room For Improvement
Bibliography

Introduction

As programmers, we're often faced with the prospect of implementing a lengthy operation. Large files need opening or writing; data structures need sorting or parsing; image data needs complex manipulation or blitting; and the list goes on. If not implemented carefully, any of these lengthy operations will completely block the user and the user interface until completed.

Threads are one good way to implement lengthy operations, while avoiding the undesired blocking of the user interface. However, threads inject a considerable degree of complexity into a program, which many of us would rather avoid if we can.

There's one large class of lengthy operations for which threads aren't really needed. Which class? Iterative operations. That is, for problems that can be broken down into smaller problems, each alone only taking a small amount of time (like a quarter second or so), then it's not really necessary to start a new thread to keep the user interface alive and responsive. This article tells how.

Before proceeding, it's helpful to point out how many of typical lengthy operations can be broken down into iterative ones. Opening a file can proceed line-by-line or buffer-by-buffer. Sorting naturally proceeds in iterations. Parsing can be performed for arbitrarily-sized chunks of the input. Images can be processed row-by-row. It's generally easy to see how to break a problem up into iterations, although there remain some problems that can't be broken up like this (for example, opening a connection to a remote site, which is already a single iteration and can take an arbitrarily long time).

My objective was to create an easy-to-use class that runs a lengthy operation that's been broken down into iterations. I also wanted to provide the user with a "cancel" button, to abort the operation if desired. Of course, I'm not the first one treading this pathway; many have come before, and probably the most familiar example is the standard "Print" dialog which allows the user to cancel printing as desired.

A lot can be learned about this topic from the experts. Paul DiLascia wrote an excellent article in his "Wicked Code" column from the February 1997 issue of MSJ (see http://www.microsoft.com/msj/0297/wicked/wicked0297.aspx). In it, he describes a CWaitDialog class that implements a "cancel" dialog which you create on the stack before beginning a lengthy operation. I found his example a bit contrived, however, since it requires you to call methods of the CWaitDialog from within iterations of your lengthy operation, something that's often neither natural or convenient. Later on, Mr. DiLascia wrote a follow-up article, in his "C++ Q&A" column from the July 1998 issue of MSJ (see http://www.microsoft.com/msj/0798/c0798.aspx ). This article comes closer to the mark, but again, it requires you to call a method of his class from within iterations of your lengthy procedure. To me, this was inside-out thinking.

An Incorrect Approach

It's useful to see an implementation that doesn't work, and to understand why.

The approach is not uncommon for those who temporarily forget that the Windows OS is a message-based architecture. A dialog class is created with a template that shows a "Cancel" button. An object of the class is instantiated on the stack, and then CDialog::Create is called to run it modelessly. The lengthy operation is then started:

// an incorrect approach - don't do this

// in the dialog class ...

void CLengthyDialog::OnOK() 
{
    m_bAbort = TRUE;    // a BOOL member variable
    CDialog::OnOK();
}

// in the view class, for example ...

void CLengthyDemoView::OnButtonBeginLengthyOperation() 
{
    CLengthyDialog dlg;
    dlg.Create(IDD_DIALOG1, this);
    dlg.ShowWindow(SW_SHOW);
    
    CString info;
    UINT nIter=0;

    while( !dlg.m_bAbort && nIter++<20 )    // arbitrarily run 20 iterations
    {
        GetDocument()->NextIteration();
        info.Format("Working on iteration %d of 20", nIter);
        dlg.m_Static.SetWindowText( info );
    }
}

Of course this won't work. Simply displaying a dialog with a Cancel button won't work, because as long as the application that created the dialog isn't retrieving and dispatching messages (and it won't be because of the lengthy operation), WM_COMMAND messages reporting clicks of the Cancel button can't get through, and the OnOK() handler will never get called to abort the operation. In fact, no messages can get through. This includes WM_PAINT messages, causing the application to cease painting itself. You'll be lucky to get the dialog to even show it's "Cancel" button. Basically, the application is non-responsive.

Non-responsive Application

An Approach That Works

As indicated above, Paul DiLascia and countless programmers of print-cancel dialogs have implemented dialog classes with a "Cancel" button. The basic idea is to process the application's message queue at reasonable time intervals during the lengthy operation, and to permit the lengthy operation to resume its next iteration only after the message queue is completely empty. Since the message queue is being processed, the application remains responsive, provided each iteration is not in itself too lengthy. And since all messages are being processed, including WM_COMMAND messages, it's possible to detect that the user has clicked the "Cancel" button. Here's core code:

// ...
// in the CCancelDialog class

CCancelDialog::OnAbort()
{
    m_bAbort = TRUE;    // handle user's click of the "Abort" button
}

BOOL CCancelDialog::Pump()
{
    MSG msg;
    while (::PeekMessage(&msg, NULL,   // pump message until none
           NULL, NULL, PM_NOREMOVE))   // are left in the queue
    {
        AfxGetThread()->PumpMessage();
    }
    return m_bAbort;
}

// ...
// in the main program

void CMain::OnDoLengthyOperation(int nMaxIterations)
{
    CCancelDialog dlg;               // create and open the dialog
    dlg.Create(IDD_CANCELDLG, this); // modelessly

    int nIter = 0;
    while ( (nIter++<nMaxIterations) // do as many iterations as requested
               && !dlg.Pump() )      // as long as user hasn't clicked "Abort"
    {
        DoAnotherIteration();
    }
}

This approach, like Paul DiLascia's, relies on an undocumented MFC function that's at the heart of the MFC message pump: CWinThread::PumpMessage(). Look for it in your debugger window. This function runs the entire message pump for MFC-based application, and calls the familiar ::GetMessage() and ::DispatchMessage() APIs, as well as a whole lot more. Michael Dunn's C++ FAQ at the Code Project mentions this too, in his answer to "I have a dialog that does some lengthy processing, and I need to have a Cancel button so the user can abort the processing. How do I get the Cancel button to work?"

This approach works great. And from the user's perspective, there's absolutely no difference between this approach and the approach described below.

So, what don't I like about this? As I said above, it's inside-out thinking. You, as the programmer, while your mind is thinking about the lengthy operation itself, must distract yourself to remember unrelated details like creating the dialog modelessly, pumping messages, and checking if the user has clicked "Abort". That's not the kind of encapsulation that C++ is supposed to provide.

Using Callback Functions

For the kind of encapsulation I wanted, I wanted to be able to tell some object that I had a lengthy operation to perform, so please do it for me and tell me if the user cancelled it. This framework, if I could get it top work, would also fit in well with the way I break lengthy operations down into iterations: I write a function (like the DoAnotherIteration() function above) that implements each of the iterations, and I would like to have that function called iteratively.

Windows provides plenty of situations where we write functions and expect Windows to call them for us. They're called "callback" functions, and I think everyone here is familiar with them. The basic idea is that you provide Windows with a pointer to the function that you want it to call (e.g., to enumerate child windows or to stream in rich text), and then wait for Windows to call the function for you.

This framework is perfect for what I wanted to do. It lets me write code like this:

// ...
// in the main program
void CMain::OnDoLengthyOperation(int nMaxIterations)
{
    CLengthyDialog dlg;
 
    // give the dialog a pointer to the function
    dlg.SetFunction(this, CMain::DoAnotherIteration);  
    dlg.Run( nMaxIterations );           // for the iteration, and then run it
}

That looks nice to me - very simple from the programmer's perspective, and good performance from the user's perspective. The CLengthyDialog itself remembers to create itself modelessly, and it's the one that's responsible for ensuring that the message queue is processed between each iteration. You're relieved from these responsibilities, and your life is easier because of it. In fact, this implementation is very similar to what I ended up with in the sample program. The main difference is that I decided to write two functions to implement the lengthy operation: a first function to initialize the lengthy operation, and a second, which is the one we've been talking about so far, to actually perform each iteration. The CLengthyDialog class calls the first function to allow you to set up initialization parameters for the overall operation, such as initializing values that are needed during the subsequent iterations. It expects that this initialization function will return a UINT value that tells the number of times to run the iterations. It then runs the iterations for the requested number of times, and then returns a BOOL value to the calling program which indicates whether the operation completed successfully or the user clicked the "Abort" button. Here's core code from the CLengthyDialog class. Incidentally, for those of you who are a bit unfamiliar with the syntax for "pointer-to-member-function" and the syntax for using such a pointer (and that certainly includes me) then take a look at Marshall Cline's "C++ FAQ Lite: [33] Pointers To Member Functions".

// ...
// in CLengthyDialog's .h header file
// typedef's to help with syntax
    typedef UINT (CChildView::*PFuncInit)();
    typedef BOOL (CChildView::*PFuncIter)();

// ...
// in CLengthyDialog's .cpp implementation file
void CLengthyDialog::SetFunctions(CChildView* p1, PFuncInit f1, PFuncIter f2)
{
    m_fInitializeOperation = f1;  // set function pointers for both
    m_fNextIteration = f2;        // init and iteration functions
    m_pParent = p1;
}

BOOL CLengthyDialog::Run()
{
    this->Create(CLengthyDialog::IDD, m_pParent); // create myself modelessly
    this->ShowWindow( SW_SHOW );                  // and make certain I'm 
                                                  // visible

    UINT nIter = 0;
    BOOL bFinished = FALSE;
    MSG iMsg;

    // now do the lengthy operation; initialize first

    UINT nCount = (m_pParent->*m_fInitializeOperation)();

    while ( !m_bAbort && !bFinished )
    {
        (m_pParent->*m_fNextIteration)();      // do the next iteration

        while ( ::PeekMessage( &iMsg, NULL, NULL, NULL, PM_NOREMOVE ) )
        {
            ::AfxGetThread()->PumpMessage();   // pump messages until queue is 
                                               // empty
        }
        
        ++nIter;
        bFinished = nIter>nCount;     // determine whether we're finished
    }

    this->DestroyWindow();
    return !m_bAbort;
}

The result looks the way the user expects it to look, too:

Lengthy

This is probably a good spot for a brief note about polymorphism and virtual functions in C++. Some might argue that pointers-to-member-functions are not object-oriented, and in any event have no place in C++ where polymorphism provides a much more robust framework for automatically accomplishing the manual operations required with pointers-to-member-functions. These are valid points, but nevertheless I elected to proceed with pointers-to-member-functions, for the reason that my objectives eliminated polymorphism as an option. Remember: I wanted a "one size fits all" solution, where I could use exactly the same class over and over, in any situation, for any DoAnotherIteration() function. Polymorphism would require derivation of a new class each time I wanted to substitute a different function (like a ParseNextLineInFile() function) for the DoAnotherIteration() function. Basically, I wanted a "plug 'n chug" solution that couldn't be provided by polymorphism.

So, What's Wrong With This Class?

Looking back at my work so far, I was quite pleased. As you see from the above screen shot, I had added a progress bar of sorts, which gives the user feedback on how far the operation had progressed. I also realized that most situations needed disabling of the main window, or the user would still have access to all controls in the main window. So, I made a call to ::AfxGetMainWnd()->EnableWindow() inside the dialog, which disabled the main window, without which the user could do many evil things like close down the app or start a new instance of the lengthy operation. In addition, I recognized that not all lengthy operations can predictably decide how many iterations to run in advance of actually running them. Hence, I added another function, called RunUntilDone(), and required the iteration function to return a BOOL value. In this situation, the initialization function still returned a UINT value, but the value was only used as an approximation of the number of iterations, which actually continued to run until the iteration function itself returned TRUE.

So, you might ask, "Why aren't you finished? What's wrong with what you have?"

Well, in fact, although I had simplified life in one place, I had complicated it in another. In Paul DiLascia's implementation, the main program had to make accommodations for a CWaitDialog class that remained constant (by calling its Pump() method). In my implementation above, the program stayed simple, but the CLengthyDialog class needed a change each time I used it. For example, it was now necessary to #include the .h header file for class that housed the iteration function. And still, it was necessary to define that pesky dialog template resource, and make certain that it contained an "Abort" button and perhaps a CStatic if you wanted the progress indicator.

I wanted it even simpler.

A Templated Class and a Dialog Template

Solving these two problems led me to two different and unrelated technologies: templated classes with callback functions, and in-memory dialog templates.

Templated Classes With Callback Functions

I first came across templated classes and pointers to callback functions when trying to determine a way to generalize a base class for virtual list controls. I needed a way to implement a GetText() function. The problem was that the base class for the list control couldn't know beforehand which class held the data for the control, and how to get it.

Templated classes, with pointer-to-member-functions implemented as callback functions, provided the answer for that problem, and it provides the answer here. For those faced with similar situations, I highly recommend an obscure but tremendously useful article from Microsoft's MSDN library archive: Dale Rogerson, "Pointers to Member Functions and Templates", July 1995.

Basically, the idea is to rewrite the class into a templated class, with the owner of the iteration function as the parameter to the template. This lets us write the following code in our main program, and never requires any change (ever) in the dialog class that runs our lengthy operation:

// ...
// in main program
void CMain::OnDoLengthyOperation()
{
    // in this example, CMyDocument owns the iteration functions

    CLengthyOperationTemplatedDialog<CMyDocument> LengthyOp(this);
    LengthyOp.SetFunctions( GetDocument(), CMyDocument::PrepareOperation, 
                    CMyDocument::NextIteration );
    LengthyOp.Run();
}

Cool, huh? No changes are ever needed to the templated class. All you need to do is create an object templated with the correct class, tell it what the iteration functions are, and then tell it to Run() them.

There were two problems in getting the templated class implemented. First is the realization that since this is a templated class, there is only a .h header file and no .cpp implementation file. I should have remembered this, but since I was converting a regular class into a templated one I didn't. If you don't know what's being discussed here, then read Marshall Cline's "C++ FAQ Lite: [34] Container Classes and Templates", and particularly "[34.12] Why can't I separate the definition of my templates class from its declaration and put it inside a .cpp file?"

The second is much more serious. None of the MFC macros for message handlers work for templated classes. They simply weren't designed with templated classes in mind, and you'll get a ton of compiler errors if you try to use them in a templated class.

The solution to this second problem is found at Code Project in Len Holgate's article entitled "Templates and MFC". Len's article gives new macros that are used in place of MFC's macros. After doing that, everything works great.

In-Memory Dialog Templates

As for in-memory dialog templates, creating the templates in memory instead of relying on program resources enabled me to jettison the need for template resources. In-memory templates require use of the CDialog::CreateIndirect() dialog function, as opposed to the far more familiar CDialog::Create() function. In addition, you need to learn far more than you ever wanted about the DLGTEMPLATE and DLGITEMTEMPLATE structures.

I found only a few articles about in-memory dialog templates. The best two I found are both Knowledge Base articles at MSDN: KB 141201: "INFO: DIALOGEX Resource Template Differences from DIALOG Template", and KB 155257: "Dialog Templates in Memory". The second included a sample program and a class, but was frankly way too complicated for this project. The first seemed adequate for my purposes. But in candor, I never was fully able to implement all controls on the dialog using the in-memory template alone, and was forced to insert them manually through use of standard calls to things like CButton::Create() and CStatic::Create().

But the end result is just fine: The need for a template resource for the dialog is eliminated, and you can use the class without worrying about whether you have a properly-formatted template, with the correct types and styles of controls.

Along the way, I learned a few more things. First, I finally started to understand a mostly impenetrable MSDN article on idle loop processing in MFC entitled (what else) "Idle Loop Processing". And I realized that I should be checking for the return value from CWinThread::PumpMessage() to make sure that something bad didn't happen while processing the message queue. Moreover, if I checked the return value, I could also let the programmer decide whether or not to disable the main window (the default value disables the main window) while processing the lengthy operation. The final Run() function looks like this, where the parameter bRunIterated determines whether to run the iterations for a pre-designated number of iterations or until done, and the paramter bDisableMainWnd determines whether or not to disable the main window from user input:

template <class TOWNER>
BOOL CLengthyOperationTemplatedDialog<TOWNER>::Run(BOOL bRunIterated, 
                                                   BOOL bDisableMainWnd)
{
    ASSERT( m_pOwner != NULL );
    ASSERT( m_bAbort == FALSE );
    ASSERT( m_fInitializeOperation != NULL );
    ASSERT( m_fNextIteration != NULL );
        
    // disable main window, if flagged

    ::AfxGetMainWnd()->EnableWindow( !bDisableMainWnd );

    // build in-memory dialog template

    WORD* p = (PWORD) LocalAlloc (LPTR, 1000);
    DLGTEMPLATE* pDlgTemplate = BuildDialogTemplate( p );
    this->CreateIndirect( pDlgTemplate, m_pParent );
    LocalFree (LocalHandle ( p ));

    //
    // add extra controls manually ..omitted for clarity (refer to source code)

    this->ShowWindow( SW_SHOW );   // shouldn't be needed unless we forget to 
                                   // set WS_VISIBLE in dialog template
    UINT nIter = 0;
    BOOL bRet, bFinished = FALSE;
    CString info;
    MSG iMsg;

    // now do the lengthy operation; initialize first

    UINT nCount = (m_pOwner->*m_fInitializeOperation)();

    while ( !m_bAbort && !bFinished )
    {
        // update text of progress indicator

        if ( bRunIterated )
        {
            info.Format("Working on Iteration %d of %d", nIter, nCount );
        }
        else
        {
            info.Format("Working on Iteration %d of approximately %d",
                        nIter, nCount );
        }

        ctlProgress.SetWindowText( info );   // update status for user

        bRet = (m_pOwner->*m_fNextIteration)();   // do the next iteration

        //
        // pump messages in the queue

        while ( ::PeekMessage( &iMsg, NULL, NULL, NULL, PM_NOREMOVE ) )
        {
            if ( !::AfxGetThread()->PumpMessage() )   // PumpMessage normally 
                                                      // returns TRUE
            {
                m_bAbort = TRUE;   // it returned FALSE .. exit the entire app
                ::PostQuitMessage( -1 );
                return FALSE;
            }
        }

        // determine whether we're finished

        ++nIter;

        if ( bRunIterated )
        {
            bFinished = nIter>nCount;
        }
        else
        {
            bFinished = !bRet;
        }

    }

    // re-enable main window and exit

    ::AfxGetMainWnd()->EnableWindow( TRUE );
    this->DestroyWindow();
    return !m_bAbort;

}

I also decided to make the Run() function private, and expose two public functions that called it: RunIterated() and RunUntilDone(). In my mind, these functions reinforced the difference between lengthy operations that required a precisely-determinable number of iterations, and those that should be run continuously until they were finished.

The Final Class and How To Use It

The final class is found in the downloadable source under the class name CLengthyOperationTemplatedDialog and file name LengthyOperationTemplatedDialog.h. The core pseudo-code is much the same as that given above for the non-templated class, except it's templated with parameter TOWNER which is the name of the class that owns the iteration and initailization functions.

The four important publicly-accessible functions are:

  • template <class TOWNER>
    CLengthyOperationTemplatedDialog(CWnd* pParent = NULL)
    - constructs a CLengthyOperationTemplatedDialog object
  • template <class TOWNER>
    void SetFunctions(TOWNER* pOwner, PFuncInit f1, PFuncIter f2)
    - takes a pointer to the class that owns the iteration functions, as well as pointers to the two functions themselves
  • template <class TOWNER>
    BOOL RunIterated(BOOL bDisableMainWnd=TRUE)
    - runs the lengthy operation for the number of iterations returned by the initialization function
  • template <class TOWNER>
    BOOL RunUntilDone(BOOL bDisableMainWnd=TRUE)
    - runs the lengthy operation for as long as the iteration function continues to return TRUE and stops as soon as FALSE is returned

Usage is very simple:

1. Add the .h header file for the CLengthyOperationTemplatedDialog class to your project. In Visual Studio 6.0, select Project->Add To Project->Files... from the main menu, and then locate the LengthyOperationTemplatedDialog.h file that you downloaded from this article.

2. Decide which of your classes should own the two functions needed to run the lengthy operation (i.e., the initialization function and the iteration function), and write the two functions. You can give these functions any names you want, but they must both be in the same class, and they must have the following signatures/declarations:

  • For the initialization function, the function must take no parameters and must return an unsigned integer UINT value. This value is the exact number of iterations to run (for RunIterated()) or an approximate value that's only used for information purposes in a display to the user (for RunUntilDone()).
  • For the iteration function, the function must take no parameters and must return a BOOL value. The value is ignored for RunIterated() mode. For RunUntilDone() mode, the lengthy operation continues until FALSE is returned and continues running so long as TRUE is returned.

3. Decide which of your classes should run the lengthy operation, and then insert code like the following. In this example, the iteration functions are located in the document, and the lengthy operation is being run from the view:

// ...
// at the top of the .cpp implementation file
#include "LengthyOperationTemplatedDialog.h"

// ...
// later on in the body of the .cpp implementation file
void CMyView::OnDoLengthyOperation()
{
    // in this example, CMyDocument owns the iteration functions

    CLengthyOperationTemplatedDialog<CMyDocument> LengthyOp(this);
    LengthyOp.SetFunctions( GetDocument(), CMyDocument::PrepareOperation, 
                   CMyDocument::NextIteration );
    if ( !LengthyOp.RunIterated() )   // or, LengthyOp.RunUntilDone();
    {
       // user cancelled operation .. handle it here
    }
}

That's all there is to it.

A Word About The Demo Program

The demo program allows you to run lengthy operations in three different modes: an incorrect blocking approach, a correct but standard class implementation, and the final templated implementation. The three different modes correspond respectively to the three differently-colored stars.

The nature of the lengthy operation can be controlled by the "chick" icon (anyone else notice that I need help with icons). For purposes of demonstration, the lengthy operation is just a counting loop that counts upwardly for a settable amount of time. I felt this was more realistic than a simple call to Sleep(), for the reason that this counting operation mimics the normal CPU-intensive nature of lengthy operations. But of course it doesn't also mimic other things common to lengthy operations, like disk access and other I/O operations. For this reason, I gave a certain degree of randomness to the time spent counting, to simulate the variability of these operations, although this clearly isn't a perfect simulation.

Anyway, clicking the "Chick" icon lets you set up the parameters of the lengthy operation (timing, randomness, and whether to run the operation in RunIterated() mode or the RunUntilDone() mode). It also lets you decide whether or not to disable the main window during the lengthy operation:

Setup Parameters For The Lengthy Operation

Play with the parameters to see what a typical user experience might be like under a variety of circumstances. For example, vary the amount of time spent in each iteration. For me, I found it annoying if an iteration took more than around one-quarter second each, beyond which the program seemed too sluggish. This experimentation can provide valuable insights into just how long your own real-life iterations should take.

Room For Improvement

There are a few things I still don't like. First among them is my frustration in being unable to figure out how to create the dialog's template completely in memory. If anyone gets this, let me know.

Second, I couldn't figure out how to maintain the "wait cursor" during the lengthy operation. I tried implementing a simple CWaitCursor, but after noticing that it did nothing at all, I finally stumbled across documentation to the effect that the CWaitCursor only maintains an hourglass shape "only for the duration of a single message" (see KB 131991: "HOWTO: Change the Mouse Pointer for a Window in MFC"). So, there was no help here. If anyone else gets a nicely-encapsulated solution to this one, let me know.

Bibliography

Here are the links mentioned in the article.

These have information about implementing a "Cancel" dialog or implementing a lengthy operation:

These have information about templated classes:

These have infromation about pointers to member functions:

These have information about creating in-memory template resources for dialogs:

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Mike O'Neill

United States United States
Mike O'Neill is a patent attorney in Southern California, where he specializes in computer and software-related patents. He programs as a hobby, and in a vain attempt to keep up with and understand the technology of his clients.

Comments and Discussions

 
SuggestionCreate a dialog template in memory PinmemberDr. GUI10-Apr-14 0:10 
GeneralMy vote of 1 Pinmembertrevor.n.webster18-Jul-11 14:57 
GeneralWaitCursor PinmemberPolisetti.Murali.Krishna28-May-10 3:25 
GeneralGood article, pump is often better than threads PinmemberBen Bryant28-Aug-07 4:13 
GeneralKind of interesting.. Pinmemberjuggler16-May-06 2:08 
QuestionUse from window less class? Pinmembertuhin241-Oct-05 5:02 
Questionbuild using vc++.net ? PinmemberMaximilien23-Aug-04 15:12 
AnswerRe: build using vc++.net ? PinmemberMike O'Neill24-Aug-04 6:26 
AnswerRe: build using vc++.net ? PinmemberDPLNeural29-Mar-10 2:15 
Questionhow to implement a message pump in an ATL addin PinmemberJerry Gao30-Jul-04 13:53 
AnswerRe: how to implement a message pump in an ATL addin PinmemberMike O'Neill10-Aug-04 6:01 
GeneralRogerson's &quot;Pointers To Member Functions And Templates&quot; Can Be Found At.... PinmemberMike O'Neill16-Apr-04 16:36 
GeneralPersistent Wait Cursor Pinmemberkewball29-Mar-04 14:03 
GeneralRelated PinmemberRavi Bhavnani2-Mar-04 8:55 
GeneralRe: Related PinmemberMike O'Neill4-Mar-04 13:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 2 Mar 2004
Article Copyright 2004 by Mike O'Neill
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid