Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Alternative to MFC for GDI programming

0.00/5 (No votes)
19 Jun 2007 1  
Exploring the MFC GDI classes' inner working and proposing an alternative.

Contents

Introduction

I got the idea of developing this project after I started to study MFC GDI related classes. I saw a lot of code for features that I have never used. From there, I wanted to perform an experiment where I could verify if by using a stripped down version of GDI classes, it would translate to a better painting performance. As you will see in the conclusion, I did not obtain the results I was hoping for, and I hesitated to write an article about the code I wrote. I concluded that if a fellow programmer was looking for an OO GDI encapsulation in a non MFC project, this code could be useful to him. In this article, I will first describe what I did not like in MFC code, highlight the important points of the new class set, present the demo program, and conclude by presenting the result of the experiment.

MFC features for GDI classes not so useful

CDC::m_hAttribDC is one of these features. Most of the time m_hAttribDC == m_hDC and the presence of m_hAttribDC just add superfluous overhead all over the CDC code. Here is an example:

CPoint CDC::MoveTo(int x, int y)
{
    ASSERT(m_hDC != NULL);
    CPoint point;

    if (m_hDC != m_hAttribDC)
        VERIFY(::MoveToEx(m_hDC, x, y, &point));
    if (m_hAttribDC != NULL)
        VERIFY(::MoveToEx(m_hAttribDC, x, y, &point));
    return point;
}

Also, related to m_hAttribDC, the class CMetafileDC is flawed. It forces its users to explicitly set m_hAttribDC, and CMetafileDC forbids you to set m_hAttribDC to m_hDC, but this is wrong! It should work because the first parameter of CreateEnhanced() is:

  • pDCRef - which identifies a reference device for the enhanced metafile.

When it is NULL, the reference device will be the display. The reason why MFC ignores pDCRef and sets m_hAttribDC to NULL is probably because CMetaFileDC also supports the old Windows 1.0 metafile format and these metafiles have no notion of reference devices. Most of the functions overridden in CMetaFileDC from CDC are virtual, and in almost all cases, it was done like that only to enforce the flawed rule that a metafile attribute DC should not equal the output DC. Hence, virtual function calls overhead is applied to all the CDC objects for nothing.

And finally, the last source of overhead in MFC GDI classes is the handle maps. Handle maps are essential for window objects but I still have to see a situation with GDI objects where the handle maps are crucial. Handle maps are invoked when the functions Attach(), Detach(), and FromHandle() are called. You could think that if you are not calling these functions then you are not using them, right? Well, this is wrong. Every time an object is selected in a CDC object, the object pointer returned by the SelectObject() function comes from CGdiObject::FromHandle(). To get an idea of the magnitude of this source of overhead, consider the pseudocode for CGdiObject::FromHandle():

  • Try to find the requested handle in the permanent object map.
  • If not found, try to find the requested handle in the temporary object map.
  • If not found, create a temporary object.

All these temporary objects will then be deleted the next time the MFC framework enters its idle function.

OLIGDI features highlight

OLIGDI consists of the following classes:

  • ODrawObj
  • OBitmap
  • OPen
  • OBrush
  • OFont
  • ORgn
  • OIC
  • ODisplayInfo
  • ODC
  • OClientDC
  • OWindowDC
  • OPaintDC
  • OMemDC
  • OFlickerFreeDC
  • OMetaFileDC

OLIGDI does not claim to be a complete solution as only a small percentage of the hundreds of the GDI functions have been implemented. It would have been too tedious to implement wrappers for every function just to try out the concept my experiment wants to verify. However, the framework is laid out, and it should be very easy to add functions as and when needed.

The primary design requirement for this new class set is to keep the same API as MFC for compatibility purposes. With this requirement, it is easy to modify code from using MFC objects to OLIGDI objects. All that is needed is to change the object types in the variable declaration statements and recompile. The second requirement is to remove all the unwanted features from MFC. That includes:

  • m_hAttribDC
  • virtual functions
  • handle maps

Also, OLIGDI introduces two new features borrowed from the book Windows++ written by Paul Dilascia. The first improvement is that, with MFC, if you want to reuse an object to store a different GDI handle, you must first explicitly call DeleteObject(). This is error prone as if you forget to make this call, it will create a GDI resource leak. In OLIGDI, this is done explicitly in every creation function:

inline BOOL OFont::CreateFontIndirect(CONST LOGFONT *lf)
{
    HFONT hRes;

    LASTERRORDISPLAYD( hRes = ::CreateFontIndirect(lf) );
    DeleteObj();
    set(hRes,OTRUE);
    return (BOOL)hRes;
}

The next borrowed feature is the ability that the DC object has to remember the default objects when they are replaced with a SelectObject() call, and to reselect them into the DC at object destruction. This feature removes the burden of keeping track of the default objects from the user. Here is the relevant code for this feature:

class OIC
{
public:
    /*
     * Each type of drawing object has an ID, used as offset to store
     * handle in a table.
     */ 
    enum WHICHOBJ { SELPEN=0, SELFONT, SELBRUSH, SELBITMAP,
                    NDRAWOBJ };
protected:
    HDC m_hDC;                  // Windows handle to DC

    BOOL m_del;

    HANDLE m_origObj[NDRAWOBJ]; // original drawing objects

    int    m_anySelected;       // whether any new objects are selected

// Other stuff omitted

};

/*
 * OIC::select function
 *
 * Protected method to select a display object
 * Destroys old selected object if required.
 * "which" specifies whether object is a pen, brush, etc.
 * "del" specifies whether to delete this object.
 */ 
HGDIOBJ OIC::select(WHICHOBJ which, HGDIOBJ h)
{
    HGDIOBJ old;

    WINASSERTD(h);
    old = ::SelectObject(m_hDC, h);
    WINASSERTD(old && old != HGDI_ERROR);

    if( m_origObj[which] == NULL )
    {
        m_origObj[which] = old;
        m_anySelected++;
        WINASSERTD( m_anySelected <= NDRAWOBJ );
    }
    else if( m_origObj[which] == h )
    {
        m_origObj[which] = NULL;
        m_anySelected--;
        WINASSERTD( m_anySelected >= 0 );
    }

    return old;
}

OIC::~OIC()
{
    if (m_hDC)
    {
        restoreSelection();
        if( m_del )
        {
            LASTERRORDISPLAYD(::DeleteDC(m_hDC)); 
        }
    }
}

/*
 * OIC::restoreSelection function
 *
 * Restore selected display objects (pens, brushes, etc.).
 */ 
void OIC::restoreSelection(void)
{
    for (int i = 0; m_anySelected && i < NDRAWOBJ; i++)
    {
        restoreSelection((WHICHOBJ)i);
    }
}

inline void OIC::restoreSelection(WHICHOBJ which)
{
    if( m_origObj[which] )
    {
        WINASSERTD(m_hDC != NULL);
        ::SelectObject(m_hDC, m_origObj[which]);
        m_origObj[which] = NULL;        // don't restore twice!

        m_anySelected--;
        WINASSERTD( m_anySelected >= 0 );
    }
}

There is one situation you have to be cautious about. If the DC object and the selected GDI objects are located on the stack, I believe that the destructors call order will be the reversed from which the variables have been declared:

void foo(void)
{
    OPen p(PS_SOLID,1,RGB(255,0,0));
    OClientDC dc(hwnd);
    dc.SelectObject(&p);
    // Ok, OClientDC destructor will be called first

}

void foo2(void)
{
    OClientDC dc(hwnd);
    OPen p(PS_SOLID,1,RGB(255,0,0));
    dc.SelectObject(&p);
    // Boom, the pen will be destructed before being unselected

}

To avoid this type of problems, restoreSelection() can be called explicitly at the end of the function.

A word of warning, it is not a good idea to use OLIGDI if you are planning to share code between the painting routine and the print code. Unless you write your own print previewing code on top of OLIGDI, MFC provides a special class called CPreviewDC that substantially alters the CDC behavior for print previewing, and if you want to use MFC in that area, you will not be able to reuse the code written for OLIGDI. That being said, it might still be advisable to use OLIGDI if you have painting performance problems, if you consider that the window will be painted much more often than the number of times a document will be printed.

The demo program

Essentially, what the demo program needs to do is draw a bunch of things by either using OLIGDI or MFC, and time the operation and display the difference between the two paint methods. My starting point for the demo program is the cute clover program written by Charles Petzold for his book Programming Windows. His clover program draws a clover with lines and a complex clipping region. From the demo program menu, you can select three display methods: OLIGDI, MFC, and Alternate. The first two can be used so the user can try to observe subjectively the difference between the two painting methods by resizing the window. The third option, Alternate, with the help of the timer option that periodically forces the repainting of the window, allows the demo program to compute the difference between the two painting modes. The timing is performed with the help of this small helper class:

class cHighResolutionTimer
{
public:
    cHighResolutionTimer();

    void start();
    double stop();

private:
    LARGE_INTEGER frequency, startTime;
};

cHighResolutionTimer::cHighResolutionTimer()
{
    startTime.QuadPart = 0;
    LASTERRORDISPLAYD(QueryPerformanceFrequency(&frequency));
}

void cHighResolutionTimer::start()
{
    LASTERRORDISPLAYD(QueryPerformanceCounter(&startTime));
}

double cHighResolutionTimer::stop()
{
    LARGE_INTEGER stopTime;
    LASTERRORDISPLAYD(QueryPerformanceCounter(&stopTime));
    return 
      (double)(stopTime.QuadPart - startTime.QuadPart)/frequency.QuadPart;
}

The most challenging part of programming the demo program has been to output meaningful numbers out of the timing measurements. Something that I have noticed during the development is that, measuring the same drawing method multiple times results in large variations in the timing. This could be caused by multiple factors such as software inconsistencies (task switching) and hardware inconsistencies (GDI device driver having to wait for a particular moment in the video card refresh cycle to perform writes). Since the timing variations are of the same order as the speed differences, I had great difficulties to highlight this difference. After many attempts with different methods, I have devised the following scheme:

  1. NUMSAMP measurements for each method are taken.
  2. Sort the measurements.
  3. Scrap the NUMSAMP/3 lowest and the NUMSAMP/3 highest measurements.
  4. Return the remaining measurements average.
#define NUMSAMP 12

class CTimingStat
{
public:
    CTimingStat()
    { reset(); }

    void reset(void) { m_nSamples = 0; }
    void set(double s) { m_samplArr[m_nSamples++] = s; }
    const UINT getnSamples(void) const { return m_nSamples; }
    double getAverage(void);
private:
    double m_samplArr[NUMSAMP];
    UINT m_nSamples;

    static int __cdecl compare(const void *elem1, 
                              const void *elem2);
};

double CTimingStat::getAverage(void)
{
    int a;
    double xa = 0.0;

    qsort(m_samplArr,NUMSAMP,sizeof(double), 
                      CTimingStat::compare);

    for( a = NUMSAMP/3; a < (2*NUMSAMP/3); a++ )
    {
        xa += m_samplArr[a];
    }

    xa /= NUMSAMP/3.0;

    return xa;
}

int CTimingStat::compare(const void *elem1, 
                         const void *elem2)
{
    return (int)(*(double *)elem1 - *(double *)elem2);
}

To complete the demo program description, there is an interesting bug that slipped away from my attention. When using the memory DC as a double buffer to remove flickers, the painting was fine almost all the time except when only a small portion of the window needed to be repainted. You could resize the window and the repainting was performed flawlessly, but if you opened the About dialog box and dragged it around the client area, the repainting was all screwed up. The problem comes from the fact that the clipping region is computed for the whole client area and the memory DC window origin is set to the invalid rectangle upper left corner. When the window is resized, the whole client area is invalidated and everything fits, but when only a small portion of the client area is invalidated, the memory DC window origin is not (0,0) and the clipping region needs to be moved to consider this difference. To see the problem yourself, just comment out the OffsetClipRgn() calls and select the double buffer option from the menu.

    dc.SelectClipRgn((HRGN)RgnClip.GetSafeHandle());
    /*
     * Since Clip region is in device point, it is important to offset
     * it because the double buffering DC window origin is set at the top
     * corner of the invalidated rect.
     */
    dc.OffsetClipRgn(p.x,p.y);

Conclusion

The results are very disappointing. On my machine, I got a shy improvement varying from 1% to 3%. It seems that the result depends largely on the hardware on which the demo program is run; as I tested it on different machines, with a few exceptions where I witnessed 10%-15% improvement, the improvement is generally below 5%. Without measurements, the difference is not visually perceptible. The conclusion that can be drawn from this experiment is that despite MFC's overhead, it is negligible compared to the time spent inside the GDI functions themselves.

That is it! I hope you enjoyed this article, and if you did and found it useful, please take a few seconds to rank it. You can do so right at the bottom of the article. Also, if you get amazing results with the demo program on your machine, or if you found an application for this code, I would love to hear from you!

Bibliography

History

  • 06-19-2007
    • Download updated: bug fixed.
  • 01-09-2006
    • Original article.

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