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()
{
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 {
m_zeroTrap = m_ScreenDC.Attach( ::GetDC(m_hWnd));
Grb2Fn_InitGribble2Stuff();
m_ShellTrayHwnd = ::FindWindow(_T("Shell_TrayWnd"),
NULL);
if(m_ShellTrayHwnd != NULL) {
LONG tb_style = GetWindowLong(m_ShellTrayHwnd,
GWL_EXSTYLE);
if(!(tb_style & WS_EX_TOPMOST)) {
m_ShellTrayHwnd = NULL;
}
}
SetWindowPos(&wndTopMost, 0,0,m_pixelsX, m_pixelsY,
SWP_SHOWWINDOW | SWP_DEFERERASE );
m_zeroTrap = m_QuadrantsDC.CreateCompatibleDC(&m_ScreenDC);
m_zeroTrap = m_QuadrantsBitmap.CreateCompatibleBitmap(
&m_ScreenDC, m_pixelsX, m_pixelsY);
(void)m_QuadrantsDC.SelectObject(m_QuadrantsBitmap);
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);
TRACE(_T("Error in OnCreate: %s\n"), strMsg);
return FALSE;
}
m_QuadCircleRgn.CreateEllipticRgn(m_Q1PointTopLeft.x,
m_Q1PointTopLeft.y,
m_Q1PointTopLeft.x+m_QuadSize*2,
m_Q1PointTopLeft.y+m_QuadSize*2);
SendMessage(WM_SYNCPAINT, 0,0 );
SetWindowPos(&wndNoTopMost, 0,0, m_pixelsX,
m_pixelsY, SWP_DEFERERASE );
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();
m_QuadrantsDC.DeleteDC();
m_GribbleDC.DeleteDC();
m_QuadrantsBitmap.DeleteObject();
m_GribbleBitmap.DeleteObject();
m_QuadCircleRgn.DeleteObject();
m_ScreenDC.Detach();
m_bErased = false;
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)
{
VERIFY(pDC->m_hDC == m_ScreenDC.m_hDC);
if(!m_bErased) {
int ret = CWnd::OnEraseBkgnd(&m_ScreenDC);
try {
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;
}
if(m_bUseCircle) {
m_QuadrantsDC.SelectClipRgn(&m_QuadCircleRgn,
RGN_COPY);
}
m_bErased = true;
return ret;
}
else {
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()
{
ValidateRect(&m_ScreenRect);
if(GetForegroundWindow() == this) {
Grb2Fn_BlitGribbleToQuadrantsDC();
}
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);
}
}
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.)
BOOL CGribbleWnd::Grb2Fn_DrawSomethingToGribbleDC()
{
BOOL retval = TRUE;
CRgn rgn1, rgn2;
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);
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);
}
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);
}
m_GribbleDC.SelectClipRgn(&rgn2, RGN_COPY);
for (i = 0; i < m_nLines*2; i+=2 ) {
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: {
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 ) {
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;
}
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.
void CGribbleWnd::Grb2Hlp_ForcePointInRgn(CRgn *rgn, POINT& p)
{
if(!rgn->PtInRegion(p)) {
register int x,y;
x = p.x - m_Q1PointTopLeft.x;
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.
BOOL CGribbleWnd::Grb2Fn_BlitGribbleToQuadrantsDC()
{
BOOL retval = TRUE;
try {
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 );
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,
SRCCOPY );
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,
SRCCOPY );
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
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.