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

Alternative to MFC for GDI programming

By , 19 Jun 2007
 

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

About the Author

lano1106
Web Developer
Canada Canada
Member
I'm located in Montreal,Canada and I graduated in electrical engineering at E.T.S.
 
Highly experienced C++ developer. I have been working 3 years at Nortel Networks on their Next-gen OC-768 products firmware. I then worked for the FAA 3 years on their next-gen Oceanic Air Traffic Control system. Followed by a short period in the video game industry at Quazal, the online multiplayer middleware provider and now I am in the Internet audio/video streaming business at StreamTheWorld.
 
To contact me, visit my website

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralProblem compilingmemberCarlos Fernndez12 Jun '07 - 1:43 
Hi, congratulations for this interesting application!
 
Graphic programming is usually a hard task for starters, and this kind of articles help a lot to make it clearer.
 
Well, I had a compilation error in <code>CGradient::InsertSort</code>. Variable 'j' is not defined out the 'for' statement. I think it's the typical error when a last change has been made, which was so simple that it's not worth recompiling... Smile | :)
 
Congratulations again!

 
CARLOS.
GeneralRe: Problem compilingmemberlano110617 Jun '07 - 16:52 
Hi Carlos,
 
You are right about the error. That is weird that my compiler did not report the error. What is the compiler that you are using? VC++2003.net do not report the problem.
 
Just by moving the j variable declaration out of the for loop should fix the problem. BTW, I am not the author of this module, you should report it as well at:
 
http://www.codeproject.com/miscctrl/gradient.asp[^]
 
I am not even sure that my code is calling this function but I am going to update my zip file so that the files distributed with this article are error free.
 
Thanks for the feedback!
Greetings,

 

GeneralUse a Profiler insteadmemberDamir Valiulin25 Jan '06 - 17:11 
Nice work, but your approach seems to me like a shot in a dark. This looks like the case where use of a profiling program would save you a lot of time and dissapointment.
 
You could have built your demo first, then check where the real bottlenecks are and then decide whether it is something you can improve on or it's just the API's limitations.
 

GeneralRe: Use a Profiler insteadmemberlano110626 Jan '06 - 7:06 
You are right. That is what I should have done. Perhaps prior to see the result, I was so convinced that I would get a speedup and the question was how much that I didn't bother to profile first. It is my mistake. However, I think this experiment did a nice article!
 
Greetings,
 
Olivier Langlois
http://www3.sympatico.ca/olanglois
GeneralRe: Nice work!memberlano110616 Jan '06 - 16:02 
Thanks Jack for the compliment!
 
I went to see your website and I have been very impressed by the quality of it so the compliment comming from you is flattering.
 
I also went to read the articles you wrote some time ago so I take your path as an inspiration and I'll see you in more or less 3 years when I'll have found my own niche market and have a nice website like yours Smile | :)
 
Greetings,

GeneralJust to make you know another rasterizer...memberKochise13 Jan '06 - 10:46 
First, nice work Smile | :) Second, please don't take it bad, I'm not trolling you and I don't want this thread turning into a Napalm war like there for the same reason (posting external links) :
 
http://www.codeproject.com/string/TheStrings.asp
 
I just to point out a very impressive work from Maxim Shemanarev :
 
http://www.antigrain.com/
 
Several platforms are supported as well Smile | :) Anyway, you done another piece of nice code !
 
Kochise
 
In Code we trust !
QuestionWhy not GDI+?memberArtchi13 Jan '06 - 1:07 
Why don't use the great GDI+?
AnswerRe: Why not GDI+?memberlano110613 Jan '06 - 7:01 
What would GDI+ adds?
AnswerRe: Why not GDI+?memberJim Crafton13 Jan '06 - 7:24 
I suspect if he's concerned with performance, then GDI+ won't help there either. While GDI+ adds a nice OO C++ layer, and adds support for anti-aliased drawing, my impression is that it has always been slower than plain old GDI for basic drawing.
 
¡El diablo está en mis pantalones! ¡Mire, mire!
 
Real Mentats use only 100% pure, unfooled around with Sapho Juice(tm)!
 
SELECT * FROM User WHERE Clue > 0
0 rows returned

Save an Orange - Use the VCF!
GeneralInteresting!memberHans Dietrich12 Jan '06 - 12:57 
GDI performance is always of interest to me, so I appreciate this article.
 
Some comments:
1. Are the percentages that you mention for Release mode or Debug mode? What is the difference between the two?
 
2. It's not clear how much time is being used to update the status bar. You might want to output the timings to a separate log file, to make sure this is not affecting the measurement.
 
3. It would also be interesting to add a third alternative to the demo, "raw Win32 GDI", to see how this compares to other two.
 
4. It is customary for CP articles to include a Release mode exe in the zip download, to allow people to try your demo without having to rebuild. When I tried to rebuild the project, I got numerous errors - can't find include file, looking for preprocessor directive, etc. You probably have your system set up to compile this project, but you need to test it on someone else's system, to make sure it will compile correctly.
 
Best wishes,
Hans
 

GeneralRe: Interesting!memberlano110612 Jan '06 - 18:15 
Hi Hans,
 
Thank you for the nice words!
 
Hans Dietrich wrote:
1. Are the percentages that you mention for Release mode or Debug mode? What is the difference between the two?

 
The percentages that I mention are for Release mode. In Debug mode, both for OLIGDI and MFC, the inline functions are not expanded inline and all the ASSERT macros are expanded. It makes the painting much more slower.
 
Hans Dietrich wrote:
2. It's not clear how much time is being used to update the status bar. You might want to output the timings to a separate log file, to make sure this is not affecting the measurement.

 
I see what you are meaning but I don't think that the status bar update code affects the measurements since its computation is performed after the timer has been turned off.
 
Hans Dietrich wrote:
3. It would also be interesting to add a third alternative to the demo, "raw Win32 GDI", to see how this compares to other two.

 
I agree. It would be interesting. If you try that out, please be sure to report the result. My prediction is that it would not change the result much.
 
Hans Dietrich wrote:
4. It is customary for CP articles to include a Release mode exe in the zip download, to allow people to try your demo without having to rebuild. When I tried to rebuild the project, I got numerous errors - can't find include file, looking for preprocessor directive, etc. You probably have your system set up to compile this project, but you need to test it on someone else's system, to make sure it will compile correctly.

 
You are the first one to report to me compilation problems. The workspace included in the zip file is a VC++6 file. After receiving your comment, I tried to unzip the package and recompile it with VC++.NET2003 and as you said I got problems. I will fix these problems and include an exe at the next article update. In the meantime, I noticed something. If you immediatly select the release mode, the project compiles almost instantenously. The only remaining problem is in debug.h where "iostream.h" isn't supported anymore in newer MS compilers. To fix that, just change "iostream.h" to "iostream".
 
Greetings,
 

-- modified at 0:17 Friday 13th January, 2006
GeneralOT: loggingmemberKarstenK19 Jun '07 - 21:00 
If timing is a problem for logging I create a big log string and write him at program end. Wink | ;-)
 
Greetings from Germany

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 19 Jun 2007
Article Copyright 2006 by lano1106
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid