Gribble2 - CGribbleWnd goes blitting.






4.96/5 (10 votes)
Some full screen blitting leads to yet another look at Windows painting messages.
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 < 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 < 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 < 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 < 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 < 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 < 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 < 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