Click here to Skip to main content
15,879,535 members
Articles / Desktop Programming / MFC
Article

Gribble2 - CGribbleWnd goes blitting.

Rate me:
Please Sign up or sign in to vote.
4.96/5 (10 votes)
17 Jul 2001CPOL18 min read 93.1K   2.1K   29   5
Some full screen blitting leads to yet another look at Windows painting messages.

Sample Image - grb2dots.gif

Sample Image - grb2lines.gif

Sample Image - grb2triangles.gif

Introduction

Gribble1 outlined the basics of using a CWnd in full screen mode. Gribble2 experiments with the BitBlt and StretchBlt GDI functions, revisits WM_PAINT and WM_ERASEBKGND, and discovers the WM_SYNCPAINT message along the way.

The Gribble2 project

There are a few changes in the mechanics of the Gribble2 CWnd, some of which arise from the desire to do some blitting operations, some just for convenience. Gribble2, like Gribble1, is a stock off the shelf VC6 App Wizard generated MFC based exe project, with no Doc/View support. A Gribble menu item 'Go' is handled in CGribble2App to create the gribble window:

void CGribble2App::OnGribbleGo() 
{
    // TODO: Add your command handler code here
    // Lets create the Gribble window!
    if(!m_wndGribble.m_hWnd) {

        CString csWndClass = AfxRegisterWndClass(CS_OWNDC 
            | CS_BYTEALIGNCLIENT,
            ::LoadCursor(AfxGetInstanceHandle(), 
            MAKEINTRESOURCE(IDC_CURSOR1)),
            (HBRUSH)::GetStockObject(BLACK_BRUSH),
            ::LoadIcon(AfxGetInstanceHandle(), 
            MAKEINTRESOURCE(IDI_G2ICON)));

        if(!(m_wndGribble.CreateEx(WS_EX_LEFT,
            (LPCTSTR)csWndClass,
            "Gribble Window",
            WS_VISIBLE|WS_POPUP,
            0,0,0,0,    
            NULL,
            NULL
            ))) {
                AfxMessageBox(
                    "Failed to Create Gribble Window)");
                return;
            }
    }   
}

As in Gribble1, Gribble2 registers its own window class to provide the OS with information on how windows of this class should be maintained. Again, style of CS_OWNDC is used, and the CS_BYTEALIGNCLIENT style is added - this will help the efficiency of the BitBlt calls later. (At least, thats what the documentation says - in practice, I haven't noticed a difference with or without this, or the CS_BYTEALIGNWINDOW style. It may be that working with a full screen window makes this superfluous.)

Gribble1 maintained its own cursor by loading the resource in its OnCreate method and overriding CWnd::OnActivate() to maintain it. The call to AfxRegisterWndClass here tells the OS what cursor to associate with this class of window, though of course the cursor will only be valid while the app is running. Doing this provides much cleaner mouse activation.

Gribble1 passed a 0 for the background brush parameter and handled background paints with its own member CBrush. Gribble2 assigns a system brush loaded with GetStockObject to the window class itself. This now means that OnEraseBkgnd doesn't need to explicitly paint the background. The default window procedure will use this brush to fill the background if we don't do it.

Lastly, now an icon is supplied for the window. The gribble window has no title bar, but this icon will display when you hit Alt+Tab to switch between apps - which allows you to differentiate between it and the Gribble2 application.

Setup and Cleanup

One of the quirks of coding windows wrapper classes is that you often deal with two levels of creation and destruction. The class itself can live through many create/destroy cycles of its underlying window, so the initialization and cleanup one would normally associate with the constructor and destructor of a C++ class often gets moved to the OnCreate and OnDestroy message handlers.

Gribble2 follows this pattern:

int CGribbleWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (CWnd::OnCreate(lpCreateStruct) == -1)
        return -1;

    try {
        // since this window has its own DC 
        // we can stash it away... 
        // Don't just store a CDC pointer returned 
        // by CWnd::GetDC!
        m_zeroTrap = m_ScreenDC.Attach( ::GetDC(m_hWnd));

        Grb2Fn_InitGribble2Stuff();

        // save the taskbar...
        m_ShellTrayHwnd = ::FindWindow(_T("Shell_TrayWnd"), 
            NULL);

        if(m_ShellTrayHwnd != NULL) {
            // ... will we need to set topmost?
            LONG tb_style = GetWindowLong(m_ShellTrayHwnd, 
                GWL_EXSTYLE);
            if(!(tb_style & WS_EX_TOPMOST)) {
                m_ShellTrayHwnd = NULL; // not to worry...
            }
        }

        // Go full screen. SetWindowPos 
        // is more effective 
        // than MoveWindow at obscuring task bar - 
        // but suppress WM_ERASEBKGND 
        SetWindowPos(&wndTopMost, 0,0,m_pixelsX, m_pixelsY,
            SWP_SHOWWINDOW | SWP_DEFERERASE );

        /****************************************************/
        /*WARNING DO _NOT_ STICK A BREAK POINT IN THIS AREA */
        /****************************************************/

        // this creates a memory device context compatible 
        // with our display
        m_zeroTrap = m_QuadrantsDC.CreateCompatibleDC(&m_ScreenDC);

        // this creates a bitmap compatible with our display
        m_zeroTrap = m_QuadrantsBitmap.CreateCompatibleBitmap(
            &m_ScreenDC, m_pixelsX, m_pixelsY);

        // note that we don't bother saving the 
        // old bitmap - while their is
        // a default bitmap associated with a newly created 
        // device context, it is not 
        // really an object that is currently selected 
        // into the DC, and there's 
        // no point in saving it for restoration.
        (void)m_QuadrantsDC.SelectObject(m_QuadrantsBitmap);

        // repeat the above for the Extra DC that will 
        // aid in reflecting image regions
        m_zeroTrap = m_GribbleDC.CreateCompatibleDC(&m_ScreenDC);
        m_zeroTrap = m_GribbleBitmap.CreateCompatibleBitmap(
            &m_ScreenDC, m_pixelsX, m_pixelsY);
        (void)m_GribbleDC.SelectObject(m_GribbleBitmap);
    }
    catch(DWORD) {
        ValidateRect(&m_ScreenRect);
        CString  strMsg;
        m_zeroTrap.GetMessage(strMsg);
        // bad place for a messagebox
        //MessageBox(strMsg, "Error");
        TRACE(_T("Error in OnCreate: %s\n"), strMsg);
        return FALSE;
    }

    // used as clip rgn in QuadrantsDC if desired
    m_QuadCircleRgn.CreateEllipticRgn(m_Q1PointTopLeft.x, 
        m_Q1PointTopLeft.y, 
        m_Q1PointTopLeft.x+m_QuadSize*2,
        m_Q1PointTopLeft.y+m_QuadSize*2);

    // Ok - enough stuff available for OnEraseBkgnd to 
    // do its stuff, so send the WM_SYNCPAINT message
    SendMessage(WM_SYNCPAINT, 0,0 );

    // ok - we've forced our window topmost, without altering the 
    // state of the system tray/taskbar thingy - 
    // but if we leave our window topmost, other windows 
    // will be unshowable on this monitor - so lets
    // get rid of the topmost-ness. Its work here is done...
    SetWindowPos(&wndNoTopMost, 0,0, m_pixelsX, 
        m_pixelsY, /* SWP_SHOWWINDOW |*/ SWP_DEFERERASE );

    /*****************************************************/
    /******** END WARNING ( NO BREAK POINTS ) ************/
    /*****************************************************/

    Grb2Fn_DrawSomethingToGribbleDC();

    return 0;
}

First, OnCreate sets up a CDC member object with a handle to the windows device context - this is unusual for a Windows program. Most windows do not have their own device context. When they need to draw to the screen, they call GetDC and are given a DC from the system cache. They then should call ReleaseDC so that the DC goes back into the cache for other programs to use. In our case, we registered the window class with the CS_OWNDC style. This means that the device context is not taken from the system cache, and is permanently available for our use. Even if it wasn't stored it in our own CDC class, accessing the DC through GetDC would be more efficient, as the system would be able to supply the owned DC more quickly than if it had to go to the cache, but attaching it to a member CDC object makes the setup of the second and third device contexts simpler.

Next, Grb2Fn_InitGribble2Stuff is called to set up some screen metrics and initialize the variables that control the patterns that will be drawn.

You'll see a call to FindWindow that's used to get a handle to the system tray window, aka Windows 95 task bar. This is the beginning of a whole bunch of jumping through hoops to try to make sure that the gribble window is truly full screen at least long enough for the initial screen capture to the background DCs to be free of the taskbar. Gribble1 didn't go to such lengths, and while it usually worked in clearing the whole screen, there were times when it didn't - leaving the taskbar on screen while important gribbling was being performed. Gribble2 needs to be more careful, because the first time OnEraseBkgnd is called, whatever is on the screen will be blitted to the background DCs.

Instead of a simple call to MoveWindow, Gribble2 uses SetWindowPos. The call to SetWindowPos uses the &wndTopMost value, and in doing so makes the window the very topmost window in the system. The call to SetWindowPos can cause the WM_SYNCPAINT (and hence WM_ERASEBKGND) messages to be sent. The SWP_DEFERERASE flag is used here so that doesn't happen - the secondary device contexts and bitmaps need to be set up before OnEraseBkgnd can operate properly.

Armed with the screen metrics set in Grb2Fn_InitGribble2Stuff, OnCreate then sets up the second and third device contexts. These will be used to do the 'background' drawing. The calls to CreateCompatibleDC set up device contexts of the same type as our screens device context (now held in m_ScreenDC). The new device contexts don't duplicate any GDI objects that may be selected into the source DC, but do provide defaults. However, the default bitmap is not what we want for the upcoming BitBlt/StretchBlt exitement, so the code creates bitmaps compatible with our screens device context then selects those bitmap into the newly created 'compatible' DCs. As innocent as this looks, we've now set things up so that images existing on one DC can be rapidly blitted to the other, laying the groundwork for some very smooth graphic display updates.

Next, one circular region is created. This will be selected as the clipping region to the quadrants DC later if the user wants to view the output that way.

Finally, the call to SendMessage(WM_SYNCPAINT,0,0) will trigger the first WM_ERASEBKGND (after a WM_NCPAINT) and OnEraseBkgnd will do its stuff. This is where the initial blits of the erased screen will take place. The windows documentation is a tad vague on what exactly a WM_SYNCPAINT message is for.  I think of it as a 'hey Windows, do your thing like we just got rolled over by some other window' - i.e. a convenient way to get a proper stream of painting messages for a window, rather than trying to fake it by sending or posting paint messages or calling handlers directly. The documentation states that this message is for Win98 and above, but this OnCreate seems to work ok in Win95 as well.

As a side note, its interesting to see what the OS sends to a window when its invalidated by another window:

Windows 98 sends the following to the gribble window when another window moves over it, or at least why my Win98 Spy utility shows as being sent:

<00001> 00000F98 S WM_NCPAINT hrgn:000009E4
<00002> 00000F98 R WM_NCPAINT 
<00003> 00000F98 S WM_ERASEBKGND hdc:0000251E
<00004> 00000F98 R WM_ERASEBKGND fErased:True
<00005> 00000F98 P WM_PAINT hdc:00000000

Windows NT shows the following:

<00001> 001802B4 S WM_SYNCPAINT
<00002> 001802B4 S .WM_NCPAINT hrgn:1D0405AC
<00003> 001802B4 R .WM_NCPAINT
<00004> 001802B4 S .WM_ERASEBKGND hdc:62010332
<00005> 001802B4 R .WM_ERASEBKGND fErased:True
<00006> 001802B4 R WM_SYNCPAINT
<00007> 001802B4 P WM_PAINT hdc:00000000

(The lines with 'S' indicate sent messages - the 'R' lines show the return, and the 'P' stands for a posted message - windows normally 'posts' WM_PAINT messages, giving them a lower priority in the input queue.) 

Note that on NT, the paint messages are nested in the WM_SYNCPAINT processing, which would indicate that they come from the windows default procedure, not the OS. So the WM_SYNCPAINT message, on NT at least, has the effect of reducing the inter-thread communication involved with this painting message sequence.

Also, I had thought that WM_ERASEBKGND messages were only sent as a side effect of processing WM_PAINT messages, but it looks as if this is not true - more thoughts on this below.

Now, where was I. Oh yes - another call to SetWindowPos is made to remove the topmost property - this is important! Without this, our window would obscure all the other applications on this monitor. Also, you might want to avoid setting breakpoints in this code between the two calls to SetWindowPos. Especially if you are working with a single monitor machine. Trust me on this one. Gets a bit annoying. If you do want to do some spelunking here, at least make sure your Task Manager is set to be 'Always on Top'. And if you do get stuck here, kill VC, not Gribble - trying to kill the gribble window or the Gribble2.exe will result in a message box to the effect that TM can't kill a process that is being debugged, and this message box won't be visible. Like I say, gets a bit annoying. 

This might seem like a lot of effort to go to just to keep the task bar off the screen - and in fact, it doesn't work 100% of the time - if you launch VC from a non-primary monitor and are using breakpoints in OnCreate, you might end up with the taskbar showing. Which isn't so bad, really. What I'm really trying to avoid is having a bitmap of the taskbar on the screen (annoying).

Finally, I call a function to draw something to the secondary background DC (the gribble DC), and we're done.

Cleanup goes thus wise:

void CGribbleWnd::OnDestroy() 
{

    CWnd::OnDestroy();

    // Cleanup compatibles
    m_QuadrantsDC.DeleteDC();
    m_GribbleDC.DeleteDC();

    m_QuadrantsBitmap.DeleteObject();
    m_GribbleBitmap.DeleteObject();

    m_QuadCircleRgn.DeleteObject();

    // detatch the HDC we got from ::GetDC.
    // Note that since we are using a private DC there is 
    // no need to Release the DC - it does not
    // come from the DC cache, unless we ask for one 
    // from the cache by using GetDCEx with the 
    // DCX_CACHE flag set.
    m_ScreenDC.Detach();

    m_bErased = false;

    // if necessary, restore the topmost property of 
    // the taskbar...
    if(m_ShellTrayHwnd != NULL) {
        TRACE(_T("Setting tray window to top\n"));
        ::SetWindowPos(m_ShellTrayHwnd, HWND_TOPMOST, 
            0,0,0,0, SWP_NOMOVE);
    }
}

Notice that we don't need to select the bitmaps out of their respective DCs - bitmaps are different from most 'selectable' objects in this regard, so our cleanup becomes quite simple. Just delete the DCs, bitmaps, and region we created in OnCreate.

We don't need to delete the main DC, as it is part of the window - we'll just detach it so that the destructor of the CDC object doesn't get confused. Also, and this is important to note if your new to this device context stuff, we don't need to call ReleaseDC

You'll read a lot of texts that discuss device contexts that will tell you that you handle WM_PAINT messages by calling BeginPaint (which calls GetDC), doing your painting, and calling EndPaint (which calls ReleaseDC). If you are doing your rendering outside of the context of a WM_PAINT message, you can call GetDC and ReleaseDC. The point made, correctly, is that when GetDC is called in these cases you receive a DC from the system cache, and it is a resource to be repected and replaced when you are done. Not releasing a DC taken from the system cache is a serious no-no, and can cause resource depletion system wide. However, this gribble window has its own device context associated with it. It doesn't come from the system cache. In fact, you should see very little impact on the GDI resources bar in the Win98 Resource Meter utility while running Gribble2.exe. But you don't want to add this OWN_DC style to all your windows and controls. A device context is a conglomerate of a whole pile of stuff, some of which (e.g. fonts) can take up a lot of memory. The point I'm trying to make here is that the gribble window is departing from convention, but that this is intended to be a special full screen window, and I hope I'm describing the rules well enough that you can see why I'm breaking them.

Finally, if the taskbar was 'always on top' when we created the window, our first call to SetWindowPos would have robbed it of its topmost status - so we'll be nice and restore that. If we don't, after Gribble2.exe exits, the taskbar will appear on screen, but the user will be able to obscure it with other windows, which may not be the way the system was when we found it. If m_ShellTrayWnd is NULL, that means the taskbar didn't have the topmost style bit set when we checked in OnCreate, so no wurries.

OnEraseBkgnd

There seem to be differing philosophies about the proper use of the WM_ERASEBKGND and WM_PAINT handlers in windows programming. The only thread offered so far for the Gribble1 applies to this - why would we have two different messages sent to our window for what is essentially the same task? And why write code in both handlers when we could conceivably do all the work in one?

It happens in our gribble windows case that this setup is a very convenient one. But lets take a quick look at what all this 'painting' stuff is on about first in order to understand why.

The windows OS knows about all the windows that have been created. It knows when they are sleeping, it knows when they're awake, it knows if they've been bad or good, etc. More importantly, it will take action if they become invalid. A window can become invalid (or, perhaps more to the point, a region or rectangle of a window can become invalid) when we explicitly make it so by calling InvalidateRect or InvalidateRgn, or when another window invalidates all or part of the window by appearing over it and moving or closing.

In the case where we invalidate the window explicitly we have some degree of control over whether the WM_ERASEBKGND message will be sent to our application, though a Boolean parameter in the InvalidateRect and InvalidateRgn calls. Actually, this parameter is more of a hint - if other regions are slated for background erasure, the WM_ERASEBKGND message will be sent when the BeginPaint message is called. We'll be able to examine the Boolean fErase flag in the PAINTSTRUCT filled in by the call to BeginPaint after it returns.

This is the normal procedure for a windows application processing the WM_PAINT message, as noted above. The call to BeginPaint also returns the device context handle, validates the update region, and hides the caret (if necessary) while painting is being carried out. A call to EndPaint restores the caret, if one was hidden by BeginPaint, and releases the device context.

WM_ERASEBKGND will also be sent to our app by the system in among the message sequence used to tell a window to paint itself when invalidated by another window or through an explicit WM_SYNCPAINT message. To wit, WM_NCPAINT, WM_ERASEBKGND, and WM_PAINT (posted).

As it turns out, this is a Good Thing, as shown in the code below:

BOOL CGribbleWnd::OnEraseBkgnd(CDC* pDC) 
{
    // the first version of this window used its 
    // own brush to erase the background 
    // - this version passes a background 
    // brush to AfxRegisterWndClass in OnGribbleGo, 
    // so we can
    // concentrate on other stuff.

    //TRACE(_T("Inside OnEraseBkgnd...\n"));

    // Lets assure ourselves that the HDC OnEraseBkgnd  
    // is handing us is the same as our private one - 
    // if so we can assume that 
    // the MFC is respecting our privacy in this regard.

    VERIFY(pDC->m_hDC == m_ScreenDC.m_hDC);

    if(!m_bErased) {
        // here, we only actually erase once, 
        // at the start.
        // Since the window does have its own brush, 
        // returning 0 would probably have 
        // the same effect, but we want to do some init.

        int ret = CWnd::OnEraseBkgnd(&m_ScreenDC);

        // blt to the compat... in effect, 
        // clear our working DC as well
        try {
            // wipe both working DCs - allows for testing 
            // of new tricks etc...
            // Win9x users - comment out the call to 
            // m_GribbleDC for an interesting effect!
            m_zeroTrap = m_QuadrantsDC.BitBlt(0, 0, 
                m_pixelsX,m_pixelsY, &m_ScreenDC, 0, 0, 
                SRCCOPY);
            m_zeroTrap = m_GribbleDC.BitBlt(0, 0, 
                m_pixelsX,m_pixelsY, &m_ScreenDC, 0, 0, 
                SRCCOPY);

        }
        catch(DWORD) {
            ValidateRect(&m_ScreenRect);
            CString  strMsg;
            m_zeroTrap.GetMessage(strMsg);
            MessageBox(strMsg, "Error");
            return FALSE;
        }

        // select special clip region, if desired
        if(m_bUseCircle) {
            m_QuadrantsDC.SelectClipRgn(&m_QuadCircleRgn, 
                RGN_COPY);
        }

        m_bErased = true;
        return ret;
    }
    else {
        // only erase the background if another window is 
        // being dragged over us - 
        // better yet, just blit the screen bitmap -
        // and our window just stays the way it is - 
        // very smooth!
        if(GetForegroundWindow() != this) {
            m_ScreenDC.BitBlt(0, 0, m_pixelsX,m_pixelsY, 
                &m_QuadrantsDC, 0, 0, SRCCOPY );
        }
        return true;
    }

}

So, what gives here? Well, the first time we enter this function (indirectly by way of the SendMessage(WM_SYNCPAINT,0,0) call in OnCreate) we erase the background by calling CWnd::OnEraseBknd. Actually, we could just return false and gain the same result (the default window proc will use the class background brush to erase the background), but we want to do one last bit of setup here. After the return from CWnd::OnEraseBknd, we want to copy the newly blotted out screen to our background device contexts. Now we can work with a clean slate as it were.

Note that its also in this 'one time only' processing that the clipping region of the primary background DC (what I call the quadrants DC) is set to the circular region set up in OnCreate, if that Boolean is set.

During the periods in which the gribble window has focus, painting will be triggered by calls to InvalidateRect with a value of FALSE for the bErase parameter, so OnEraseBkgnd shouldn't fire if we call BeginPaint. But, as noted above, the WM_SYNCPAINT type message sequence that occurs when the gribble window is invalidated by another can send us here as well, and I make a call to GetForegroundWindow to determine (almost a given) if we are indeed dealing with forces beyond our control. (Note that not all windows can use such a simple test - it definitely helps to be a full screen window!) All OnEraseBkgnd needs to do in this situation is blit the primary background DC bitmap to the screen DC and we're done! Our window's invalid region is updated with the absolute minimum of flicker and other kafuffle of that nature. This is beautifully smooth. Try it, you'll like it!

OnPaint

And now, ladies and gents, the lovely and talented OnPaint.

void CGribbleWnd::OnPaint() 
{
    //CPaintDC dc(this);      
    // thanks, we already got one...

    // since we are using a private device context, 
    // and not calling BeginPaint
    // (by way of CPaintDC), we are responsible 
    // for validating the affected 
    // area - if we don't do this, the system will 
    // continue to send us WM_PAINT 
    // messages.  Also, this seems to work only 
    // if we validate the whole screen, 
    // even though we called InvalidateRect on a 
    // smaller portion.
    ValidateRect(&m_ScreenRect);

    // if we're not just painting because some nasty 
    // window is dancing the Macarena over us...
    if(GetForegroundWindow() == this) {
        // call whatever nifty paint function you got...
        // the gribble DC is drawn to at start and on 
        // left click 
        Grb2Fn_BlitGribbleToQuadrantsDC();
    }

    // ...then transfer the artwork wholesale 
    // to our screens DC
    try {
        m_zeroTrap = m_ScreenDC.BitBlt(0, 0, 
            m_pixelsX,m_pixelsY, &m_QuadrantsDC, 
            0, 0, SRCCOPY );
    }
    catch(DWORD) {
        ValidateRect(&m_ScreenRect);
        CString  strMsg;
        m_zeroTrap.GetMessage(strMsg);
        MessageBox(strMsg, "Error");
    }

    if(GetFocus()==this) {
        Sleep(m_nSpeed);
        InvalidateRect(&m_KaleideRect, FALSE);
    }


    // Do not call CWnd::OnPaint() 
    // for painting messages
}

Points of note - firstly, we don' need no stinking don't need to call BeginPaint. There's no caret to hide and we have our own DC to play with, thanks. 

We do, however, need to validate our window. Note that you can sort-of-kind-of get away with not calling ValidateRect here, but it means that the OS will continually harass your application with WM_PAINT messages. Not a good thing. Even though WM_PAINT messages are typically posted to the thread and have a low priority, having a surplus of them in the threads input queue will make it difficult for other messages to get through. The call to Sleep helps a bit, since when the thread wakes up the important messages tend to get the respect they deserve, but its still good advice to validate your window post haste inside a WM_PAINT handler.

OnEraseBkgnd will not be called, not because we set bErase to false when we invalidated, but because we're not using BeginPaint. (The CPaintDC object whose creation is commented out would have caused BeginPaint to be called).

Next, we call a method that draws whatever updates we need to the primary background compatible DC (set up in OnCreate and cleared in the initial call to OnEraseBkgnd) and then blit that to the screen DC.

However, its still nice to determine, as we did in OnEraseBknd, whether we're being asked to paint something new (our own InvalidateRect) or in response to some VB based bloated cow of an application slobbering all over our real estate - so I put the call in to GetForegroundWindow here as well. Actually making the call to BitBlt in this situation may be overkill. If a WM_PAINT message arrives when the gribble window doesn't have focus, chances are that OnEraseBkgnd has done the blit work. Note that calling ValidateRect inside OnEraseBkgnd in this situation will not suppress the WM_PAINT message posted at the end of the WM_SYNCPAINT type message flow.

Lastly, if we have focus, a call to Sleep allows the user to slow things down (to a max of 1 second, given the property dialogs restrictions) and we call InvalidateRect to start again. You might want to change this to use a timer, which is the normal way of things for screen savers and the like. Using sleep makes the app less responsive, but invalidating here is convenient - if we validate the rect when we lose focus, or in response to a right click, or an exception handler, we stop the process without further ado. 

Gribble me this, Blitman!

So, what's all this blitting stuff going to accomplish? Nice of you to ask. Bloody amazing you're still reading at this point, actually Well, originally my idea was to make a kaleidoscope. Really. But making a realistic kaleidoscope requires intelligence the ability to rotate images in non-trivial ways, and all the good rotation transforms are only available on NT, not Windows 9x, so I settled for... um... well, whatever. Call it a Gribeidoscope I guess...

All the interesting stuff here takes place in two CGribbleWnd member functions, Grb2Fn_DrawSomethingToGribbleDC and Grb2Fn_BlitGribbleToQuadrantsDC

Grb2Fn_DrawSomethingToGribbleDC draws, as the name suggests, a something to the gribble DC. This is background DC behind the real background DC, which I call the quadrants DC. Grb2Fn_DrawSomethingToGribbleDC splits the gribble square into two triangles, and reflects each item drawn by swapping x and y coordinates and alternately setting each triangle as the clip region for the gribble DC. This allows reflection on the diagonal dissecting the square, which is unavailable with the simple flips on the x and y axis available with the StretchBlt function.

Then, in OnPaint, Grb2Fn_BlitGribbleToQuadrantsDC is called and, starting at the top left corner, and performs three StretchBlt calls that copy a quarter of the gribble square to the top left of the quadrants square (DC), then reflect that square into the bottom left quadrant, then reflect that half to the right half, and we have our gribeidosopic effect. 

Grb2Fn_DrawSomethingToGribbleDC is called when the gribble window is first created, and when the user left clicks in the window. (You'll have to left click a few times before the gribble gets interesting.)

/* ***********************************************

For a Kaleidescopic effect, we would really want 
to flip a portion of an existing image 
(1) along its diagonal, to generate the first 
reflection. All we can do with the Win9x
API is flip on the X or Y axis - simulating a flip 
on the hypotenuse would require one h flip
and a rotation. With Win NT/2000 rotations are 
available - but not Win9x. So, we'll fake 
it by trying to do the 'flip' in the rendering 
to the gribble DC - essentially, draw everthing
twice...

- - - - - - - - - - - - - -  
| \                       |
|   \                     |        
|     \        rgn2       |         
|       \                 |      
|         \               |      
|           \             |      
|             \           |      
|               \         |      
|     rgn1        \       |      
|                   \     |      
|                     \   |      
|                       \ |      
- - - - - - - - - - - - - - 

*********************************************/

BOOL CGribbleWnd::Grb2Fn_DrawSomethingToGribbleDC()
{
    BOOL retval = TRUE;
    CRgn rgn1, rgn2;        // for clipping

    POINT   pPoints1[3] = {m_KaleideRect.left, 
        m_KaleideRect.top, 
        m_KaleideRect.left, m_KaleideRect.bottom,
        m_KaleideRect.right, m_KaleideRect.bottom };
    POINT   pPoints2[3] = {m_KaleideRect.left, 
        m_KaleideRect.top, 
        m_KaleideRect.right, m_KaleideRect.top,
        m_KaleideRect.right, m_KaleideRect.bottom };

    rgn1.CreatePolygonRgn(pPoints1, 3, ALTERNATE);
    rgn2.CreatePolygonRgn(pPoints2, 3, ALTERNATE);


    // get ready to draw, podner...
    srand( (unsigned)time( NULL ) );
    COLORREF  clr = RGB(rand()%256, rand()%256, rand()%256);

    static POINT pts[4096];
    for (int i = 0; i &lt; 4096; i++) {
        pts[i].x = rand()%(m_QuadSize*2);   
        pts[i].y = rand()%(m_QuadSize*2);
    }

    // lines, triangles and dots - oh my!
    switch(m_gribbleType) {
    case dots: 
        m_GribbleDC.SelectClipRgn(&rgn1, RGN_COPY);
        for (i = 0; i &lt; m_nDots; i++ ) {
            m_GribbleDC.SetPixel ( m_KaleideRect.left + pts[i].x, 
                m_KaleideRect.top + pts[i].y, clr);
        }
        m_GribbleDC.SelectObject(&rgn2);
        for (i = 0; i &lt; m_nDots; i++ ) {
            m_GribbleDC.SetPixel ( m_KaleideRect.left + pts[i].y, 
                m_KaleideRect.top + pts[i].x, clr);
        }
        break;
    case lines: {
        CPen pen(PS_SOLID, 0, clr);
        CPen *pOldpen = m_GribbleDC.SelectObject(&pen);
        POINT p[2];

        m_GribbleDC.SelectClipRgn(&rgn1, RGN_COPY);
        for (i = 0; i &lt; m_nLines*2; i+=2 ) {
            p[0].x = m_KaleideRect.left + pts[i].x;
            p[0].y = m_KaleideRect.top + pts[i].y;
            p[1].x = m_KaleideRect.left + pts[i+1].x;
            p[1].y = m_KaleideRect.top + pts[i+1].y;
            m_GribbleDC.Polyline(p,2);
            // the LineTo call was easier on the eyes, 
            // but resulted in some unwanted 
            // horizontal lines in the output...
            // m_GribbleDC.LineTo(m_KaleideRect.left + 
            // pts[i].x,m_KaleideRect.top + pts[i].y);
        }

        m_GribbleDC.SelectClipRgn(&rgn2, RGN_COPY);
        for (i = 0; i &lt; m_nLines*2; i+=2 ) {
            // there is an xy swap going on here 
            p[0].x = m_KaleideRect.left + pts[i].y;
            p[0].y = m_KaleideRect.top + pts[i].x;
            p[1].x = m_KaleideRect.left + pts[i+1].y;
            p[1].y = m_KaleideRect.top + pts[i+1].x;
            m_GribbleDC.Polyline(p,2);
        }
        m_GribbleDC.SelectObject(pOldpen);
                }
                break;
    case triangles: {
        // much like lines, but with 3 points to worry about...
        CPen    pen(PS_SOLID, 0, clr);
        CBrush  brsh(clr);
        CPen *pOldpen = m_GribbleDC.SelectObject(&pen);
        CBrush *pOldbrsh = m_GribbleDC.SelectObject(&brsh);
        POINT p[3];

        m_GribbleDC.SelectClipRgn(&rgn1, RGN_COPY);
        for (i = 0; i &lt; m_nTriangles*3; i+=3 ) {
            p[0].x = m_KaleideRect.left + pts[i].x;
            p[0].y = m_KaleideRect.top + pts[i].y;
            p[1].x = m_KaleideRect.left + pts[i+1].x;
            p[1].y = m_KaleideRect.top + pts[i+1].y;
            p[2].x = m_KaleideRect.left + pts[i+2].x;
            p[2].y = m_KaleideRect.top + pts[i+2].y;

            if(m_bConstrain) {
                Grb2Hlp_ForcePointInRgn(&rgn1, p[0]);
                Grb2Hlp_ForcePointInRgn(&rgn1, p[1]);
                Grb2Hlp_ForcePointInRgn(&rgn1, p[2]);
            }

            m_GribbleDC.Polygon(p,3);
        }

        m_GribbleDC.SelectClipRgn(&rgn2, RGN_COPY);
        for (i = 0; i &lt; m_nTriangles*3; i+=3 ) {
            // there is an xy swap going on here 
            p[0].x = m_KaleideRect.left + pts[i].y;
            p[0].y = m_KaleideRect.top + pts[i].x;
            p[1].x = m_KaleideRect.left + pts[i+1].y;
            p[1].y = m_KaleideRect.top + pts[i+1].x;
            p[2].x = m_KaleideRect.left + pts[i+2].y;
            p[2].y = m_KaleideRect.top + pts[i+2].x;

            if(m_bConstrain) {
                Grb2Hlp_ForcePointInRgn(&rgn2, p[0]);
                Grb2Hlp_ForcePointInRgn(&rgn2, p[1]);
                Grb2Hlp_ForcePointInRgn(&rgn2, p[2]);
            }

            m_GribbleDC.Polygon(p,3);
        }
        m_GribbleDC.SelectObject(pOldpen);
        m_GribbleDC.SelectObject(pOldbrsh);
                    }
                    break;

    default:
        break;
    }

    // note that SelectClipRgn with RGN_COPY := SelectObject, 
    // but for nMode param - 
    // we could use SelectObject(&rgn1) here with the 
    // same result.

    return retval;

}

The Grb2Hlp_ForcePointInRgn function is used to fit triangles into a region. You can try this effect by selecting the Constrain checkbox in the properties dialog.

// expects a region which is a half square split 
// on the diagonal.
// any point in the square that is not in the region in
// question can be mapped into it by swapping x and y.
// DrawSomethingToGribbleDC uses this to stop the spread of
// mutant blobby triangles
void CGribbleWnd::Grb2Hlp_ForcePointInRgn(CRgn *rgn, POINT& p)
{
    if(!rgn->PtInRegion(p)) {
        // swap x and y
        register int x,y;
        x = p.x - m_Q1PointTopLeft.x;   // strip offset
        y = p.y - m_Q1PointTopLeft.y;
        p.x = m_Q1PointTopLeft.x + y;
        p.y = m_Q1PointTopLeft.y + x;
    }
}

Grb2Fn_BlitGribbleToQuadrantsDC copies ever changing portions of the gribble square to the quadrants DC to give the effect of movement. This is called in OnPaint when we have focus.

/* **************************************************

Imagine a square divided into four quadrants: 

     - - - - - - - - - - - - - - 
Q1   |            |            |  
     |            |            | 
     |            |            | 
     |            |            | 
     |            |            | 
H1 { | - - - - - - - - - - - - | } H2
     |            |            | 
     |            |            | 
     |            |            | 
Q4   |            |            |  
     |            |            | 
     |            |            | 
     - - - - - - - - - - - - - - 

1. StretchBlt some area of the Gribble DC into Q1.
2. Flip Q1 into Q4 with a 1 to 1 StretchBlt, mirroring y axis.
3. Flip H1 into H2 with a 1 to 1 StretchBlt, mirroring x axis.

// *************************************************** */
BOOL CGribbleWnd::Grb2Fn_BlitGribbleToQuadrantsDC()
{
    BOOL retval = TRUE;
    try {
        // blit stuff to Q1 - we can affect the 
        // whole reflection here
        m_zeroTrap = m_QuadrantsDC.StretchBlt(
            m_Q1PointTopLeft.x, m_Q1PointTopLeft.y, 
            m_QuadSize, m_QuadSize,
            &m_GribbleDC, 
            m_Q1PointTopLeft.x + m_nBlitPos, 
            m_Q1PointTopLeft.y + m_nBlitPos, 
            m_QuadSize/m_nStretchX, m_QuadSize/m_nStretchY, 
            SRCCOPY );    

        // stretch blit Q1 into Q4...
        m_zeroTrap = m_QuadrantsDC.StretchBlt(
            m_Q1PointBottomLeft.x, m_Q1PointBottomLeft.y, 
            m_QuadSize, m_QuadSize, 
            &m_QuadrantsDC, 
            m_Q1PointBottomLeft.x, m_Q1PointBottomLeft.y-1, 
            m_QuadSize, -m_QuadSize,  // mirror Y
            SRCCOPY );

        // then flip H1 to H2.
        m_zeroTrap = m_QuadrantsDC.StretchBlt(
            m_Q1PointTopRight.x, m_Q1PointTopRight.y,
            m_QuadSize, m_QuadSize*2,
            &m_QuadrantsDC, 
            m_Q1PointTopRight.x-1, m_Q1PointTopRight.y,
            -m_QuadSize, m_QuadSize*2,     // mirror X
            SRCCOPY );

        // if we've travelled halfway down the diagonal, 
        // or gone back to the top, reverse direction. 
        m_nBlitPos += m_nDirection;
        if(m_nBlitPos == m_QuadSize) {
            m_nDirection = -1;
        }
        else {
            if(m_nBlitPos == 0) {
                m_nDirection = 1;
            }
        }

    }
    catch(DWORD) {
        ValidateRect(&m_ScreenRect);
        CString  strMsg;
        m_zeroTrap.GetMessage(strMsg);
        MessageBox(strMsg, "Error");
        retval = FALSE;
    }


    return retval;
}

Misc

There is a properties dialog implemented to allow the user to select the type of gribble to be used (dots, lines, or triangles) and various other settings. Current settings are stored in the registry under HKCU\Software\Gribble\Gribble2.

Error handling

I noticed that most GDI calls returned 0 for failure, and that some (but not all) required a call to GetLastError to determine the cause of the failure. I got tired of doing error checks on every call, so I created a small class called CZeroResultTrap whose sole purpose in life is to grab the last error and throw an exception if it is assigned a 0. If the GetLastError call returns 0, it will report a generic message. I leave it in to make the error checking less intrusive, but don't recommend you rush out and use it in any production apps. Its also interesting to note that some GDI calls simply don't fail - selecting a default (stock) object into a screen DC with MM_TEXT mapping mode, for example, or palette selection, which has no memory requirements. If you'd like to see where I stole that info See the article "GDI OBJECTS" in the MSDN for more on this.

Summary

The performance of this gribble graphic is not going to win any awards, and the math (if you can call it that) is pretty simple. I'm hoping the article is useful for its discussion of the windows paint messages and how they can be handled, and perhaps some simple blitting ideas. I don't think I've exhausted all the issues here, so check for flames feedback on this article before betting your salary on these techniques. 

Happy Gribbling

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Canada Canada
This ageing code mechanic still grumbles at the screen, still clings to Win32, and still hopes to make sense of it all before the inevitable onset of mature adulthood.

Comments and Discussions

 
GeneralVery Useful Article for a newbie Pin
Carla F.17-Feb-04 12:50
Carla F.17-Feb-04 12:50 
GeneralRe: Very Useful Article for a newbie Pin
Tim Deveaux19-Feb-04 4:27
Tim Deveaux19-Feb-04 4:27 
GeneralHelp with Screen transitions Pin
Pat O'Neil18-Oct-01 13:25
Pat O'Neil18-Oct-01 13:25 
GeneralRe: Help with Screen transitions Pin
Tim Deveaux19-Oct-01 9:14
Tim Deveaux19-Oct-01 9:14 
GeneralNice article! Pin
16-Jul-01 15:26
suss16-Jul-01 15:26 

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

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