Introduction
gribble n. "Random binary data rendered as unreadable text. Noise characters in a data
stream are displayed as gribble. Modems with mismatched bitrates usually generate gribble (more specifically, baud
barf). Dumping a binary file to the screen is an excellent source of gribble, and (if the bell/speaker is active)
headaches." The Jargon Lexicon
When I first started playing with 'pooters mumble years ago, one of the "fun" things to do was play
with some simple graphics routines. Usually, there wasn't a lot you could do in terms of graphics, but that just
made it all the more fun to try and get something going.
A little while after starting to play with Visual C and MFC, someone asked me to write a program that needed
to display some graphics - and nothing else. It actually took me a while to figure out how to use a CWnd without
all the attendant bells and whistles - frame, title bar, etc. But lo and behold, it's not very hard at all, and
provides a neat canvas for playing with, well, whatever you want to play with.
I think the Gribble window I offer here is useful if you're thinking of writing a "kiosk" type program
(as I was). I'm also hoping that the Gribble1 project can be a useful learning tool for those wishing to explore
the world of Windows graphics, so I am writing this primarily as a beginners tutorial. That said, I don't go very
deep into the subject of device contexts and GDI objects in this short article - the goal here is just to get the
Gribble window up and running, and provide a simple example (the classic 'Lissajou' figure) to get things going.
The Gribble1 project
Gribble1 is a stock off-the-shelf VC6 AppWizard-generated MFC based EXE project, with no Doc/View support. With
that in place, I just added a menu item called Gribble, with a Go option handled in CGribble1App.
Next, I made a CGribbleWnd class with the Class Wizard - selecting New Class then having my CGribbleWnd derive
from a 'Generic CWnd'. (You have to scroll a bit to get to this one.) I then declared an instance of a CGribbleWnd
in the App class, and called it m_wndGribble
.
Now, a CWnd isn't a window until it gets created, and it's here where you get a chance to define some very basic
properties. This is the only code we need outside of the CGribbleWnd itself, and you would use this if you were
to incorporate a CGribbleWnd in your own project. In my case, the Gribble window gets created in CGribble1App::OnGribbleGo()
with its own window class, and the very minimal property set of WS_VISIBLE|WS_POPUP
. It's this minimal
property set that gets rid of those nasty borders and stuff.
All we want to do here is register the window class and call m_wndGribble.CreateEx()
- a built-in
CWnd method:
void CGribble1App::OnGribbleGo()
{
if(!m_wndGribble.m_hWnd) {
CString csWndClass = AfxRegisterWndClass(CS_OWNDC, 0, 0, 0);
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;
}
}
}
Our Gribble window should be able to take it from there, which it does in its OnCreate
method:
int CGribbleWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
m_hCursor = AfxGetApp()->LoadCursor(IDC_CURSOR2);
SetCursor(m_hCursor);
ShowCursor(true);
m_pDC = GetDC();
m_pixelsX = m_pDC->GetDeviceCaps(HORZRES);
m_pixelsY = m_pDC->GetDeviceCaps(VERTRES);
m_bitsPixel = m_pDC->GetDeviceCaps(BITSPIXEL);
TRACE("Screen is %d by %d\n", m_pixelsX, m_pixelsY);
TRACE("Color depth is %d bits per pixel\n", m_bitsPixel);
MoveWindow(0,0,m_pixelsX, m_pixelsY);
return 0;
}
Here, we do a bit of setup. First, we need the device context so we can find out what the size of the screen
is. There's a lot more info we could ask for with GetDeviceCaps()
, but right now all we're really
concerned with is the maximum values for x and y. (The call to get the color depth is a holdover from the original
class, which needed to check whether the settings were high enough for the bitmaps it was using.)
The next bit of housekeeping is to size the window to the screen size, with a call to MoveWindow()
.
(Note that when we called CreateEx()
we specified 0s for top, left, width and height, so we need to
do this here.)
The CWnd::MoveWindow()
method has a fifth parameter bRepaint which defaults to TRUE and
starts the ball rolling for us - telling the OS that we'd like to be in line for some painting messages. Which
in turn will result is us getting some painting messages.
So far so good - but our generic CWnd derived class needs to handle these painting messages, especially WM_ERASEBKGND
and WM_PAINT
, because the base class (CWnd) doesn't do anything for us. Class Wizard to the rescue
again - with CGribbleWnd selected, highlight the WM_ERASEBKGND
entry and select Add Function, then
Edit Code. My CGribbleWnd::OnEraseBkgnd()
looks like this:
BOOL CGribbleWnd::OnEraseBkgnd(CDC* pDC)
{
m_backColor = RGB(0,0,0);
CBrush cb(m_backColor);
HBRUSH hOldBrush = (HBRUSH)pDC->SelectObject(cb);
RECT rect = {0,0,m_pixelsX,m_pixelsY};
pDC->FillRect(&rect,&cb);
pDC->SelectObject(hOldBrush);
cb.DeleteObject();
return TRUE;
}
Here we create a brush, a special type of GDI object, and use it to pain the screen black. This gets
us a big black screen.
Note that there is a very important code sandwich here that you must adhere to if your program is to be a
good Windows citizen, and that is that when a GDI object is selected into a device context the old one must be
saved and restored. This allows the device context to properly return the resource when it is done, and saves you
from the terrible sin of resource leaks. While we're on the subject, two more points - 1. Creating a window class
as I have above that owns its own device context is also considered a bit of a no-no. I do it to (hopefully) save
time, in the context of calling GetDC
. 2. The code sandwich above can be replaced with calls to SaveDC()
and RestoreDC()
, which can be handy when a lot of objects have been selected into a device context.
Next, using the Class Wizard to create the OnPaint()
method, we get a nice little place to practice
our windows graphics. Mine looks like this:
void CGribbleWnd::OnPaint()
{
CPaintDC dc(this);
static int s_nLisXCoef = 1;
static int s_nLisYCoef = 3;
static int s_nLisYOffset = 44;
++s_nLisXCoef;
++s_nLisYCoef;
int x,y;
for (float t = 0; t < 32767; t+=.2)
{
x = sin(s_nLisXCoef*t) * m_pixelsX/2 + m_pixelsX/2;
y = sin(s_nLisYCoef*t+s_nLisYOffset) * m_pixelsY/2 + m_pixelsY/2;
dc.SetPixel(x, y, RGB(255,80,255));
}
}
There is code in CGribbleWnd::OnLButtonDown()
that calls Invalidate()
to start the
whole painting process again, resulting in a new figure being displayed in response to a left mouse click. The
code makes 32767 calls to set the pixels involved, so it won't respond too quickly to your mouse clicks, but is
not too bad. As the figures get more complex, its kind of neat to watch 'em fill in.
Probably the only other thing to mention is the nifty cursor. There are two cursors defined in the resource
editor, and OnCreate
loads and uses the second one, kind of a StarCraft-like arrow. Maintaining this
cursor requires handling one last message:
void CGribbleWnd::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
CWnd::OnActivate(nState, pWndOther, bMinimized);
if(nState != WA_INACTIVE )
{
SetCursor(m_hCursor);
ShowCursor(true);
}
}
One last added handler not mentioned yet is the OnKeyDown
, which is used to exit via DestroyWindow()
when the escape key is pressed.
Summary
So there you have it - a standard CWnd with six message handlers added (some of these quite simple) and you
have a nice full screen window to play graphics on. There are colors you can play with, and before leaving the
Lissajou theme you might want to try different starting coefficients etc. The only graphic call here is SetPixel()
,
which doesn't require any nasty GDI objects or SelectObject()
calls, so you can play safely, without
fear of depleting your resources.
The Lissajou figures are a classic bit of "easy" graphing stuff. Maybe some of the more mathematically
minded among us have some other cool formulae to share. That would be great - I've tried to make the OnPaint()
more or less self-contained with the use of local static
vars rather
than class members, and if you stick to this convention you can pretty much submit fun new Gribble concepts just
by pasting your OnPaint()
code into a message and posting it. Perhaps someone will be able to discover
exactly what gribble should look like.
Happy Gribbling