EWin Tutorial - Graphics

How do I draw stuff on my window?

Well, you wait for the operating system to tell you that it needs you to paint yourself. Then your OnPaint() will get called. But DONT catch OnPaint()! This is a special case. It has historical reasons (related to Borland's OWL 1.0) that are not relevant here. But it still provides a useful place for behind-the-scenes work, so it won't be removed. Let me start over...

So when your window needs to be drawn, OnPaint() gets called. And OnPaint() calls your window's Paint() function. [without the "On"]. As you can see, this is a special case. This is why I bring it to your attention. To repeat: normally, whenever the operating system sends you a message, one of your OnXXXX messages gets called, such as OnSized(). But Paint() is a special case. The other special cases are called QueryXXXX, like QueryMinMaxInfo() and QueryEnd().

So all event-handler functions are either OnXXX, QueryXXX, or Paint().

Okay. So you overload Paint to paint your window's client area. What's a client area, you ask. The client area is all of the window that isn't the border or title or menu. It's that big empty whiteish blob in your stock TWin. If you want to draw overtop the borders or the menu bar, that's much harder. But usually that is a bad idea. Anyway:

 
virtual void TMyWin::Paint(TPaintDC &DC)
{
}

This will draw a blank window. But you already had that! How about we set up a font and draw some text in the window.

 
virtual void TMyWin::Paint(TPaintDC &DC)
{
  DC.SetFont(Font()); //this line sets the DC with the window's default font
  DC.XYPrintf(10, 10, "Hey there. This is a test.");
}

Simple! Uh-oh, I hear more questions coming on...


What's a TPaintDC?? (overview of Device Contexts)

Hmm. We will have to go over all of the DC stuff. A DC is a Device Context. Think of it as a robot hand that moves across a piece of canvas, which is your screen. Instead of fingers, though, the robot hand has slots that allow it to hold a single item of each tool it can use. Thus, it has a slot for a brush, a pen, a font, and a color palette. So when you draw things, you give orders to this "hand". You first tell it to use a specific pen or brush or whatever, then you give it an order. Say I wanted to draw a line.

 
DC.SetPen( TPen(CLR_RED) );
DC.Line( TPoint(10, 10), TPoint(50, 50) );

This draws a red line from the point at 10,10 to the point at 50,50.

You could then go on to draw more lines or do other things using that pen. Once the pen is plugged in, it is not unplugged until you plug in a different pen. Most operations use more than just a pen. For instance, if you want to draw a filled rectangle, you'll need a pen and a brush. The pen is used to draw the border of the rectangle, and the brush is used to fill the rectangle.

 
DC.SetPen( TPen(CLR_RED) );
DC.SetBrush( TBrush(CLR_GREEN) );
DC.Rectangle( TRect(10, 10, 50, 50) );

This will draw a rectangle whose corners are at (10,10), (10,50), (50,50), and (50,10). The border will be red, and the inside will be green.

As you can see, this is just the tip of the iceberg. There is a lot of ground here, and I can't really go into too terribly much detail. But I will give you some general advice. Read chapter 4 of Charles Petzold's Programming Windows. Don't pay much attention to his code, just get the concepts down. You don't have to do any of that nasty crap where you store the old values and select in the old values before you release the DC. My objects do all of that for you, so you can concentrate on drawing stuff. Just set up a basic EWin program and overload Paint(), and test out the various functions from in there. Play with a few things. None of the stuff he talks about is directly used in an EWin program: you never call SelectPalette(); you use a TPalette and TDC::SetPalette(). But if you don't go look at his examples you'll never figure out what various abilities are available. There are just so many friggin' functions out there that I can't just list them. Another way to find out what's out there is to look at TDC in the help, and to look at TPen, TFont, TBitmap, TBrush, and TPalette.

I try to protect you from most of the nasty parts of the GDI (Graphical Device Interface). You never need to worry that any of your GDI objects will ever be "leaked" -- the various TFont, TBrush, TBitmap, etc. objects all clean themselves up, unless you explicitly tell them not to.

Most of the functions that take an HDC have been encapsulated into a TDC. (A TPaintDC is a derived version of a TDC.) If I missed something (and I missed several functions), you can just call the API functions yourself. You can use a TDC [or a TPaintDC] in place of an HDC. So the following are equivalent:

 
DC.Rectangle( 10, 10, 500, 500);

and

 
Rectangle(DC, 10, 10, 500, 500);

The first uses a utility function of DC; the second calls a Windows API function. This was one of the design goals of the GDI wrapper objects: you can always just "bail out" of the objects and do everything yourself using API functions. But you have to know what you're doing when you use SelectObject() or you'll leak GDI objects...

Maybe some examples will get you on the right track.
TODO: put some examples in here.
 

 

 


How do bitmaps work? What's a memory DC?

A bitmap is a special kind of GDI object. A bitmap is block of pixels. You plug a bitmap into a special kind of DC, a memory DC.

All the other varieties of TDC represent a concrete thing or device: a TPaintDC is your window, a TScreenDC is the whole screen, a TPrintDC is the printer -- but a TMemDC is sort of "blank," waiting for you to put a bitmap onto it. Think of it as an easel, and a bitmap as a piece of paper. You can write

 
TMemDC MemDC;
MemDC.Line(10,10,50,50);

And you will draw a line, but you won't ever be able to tell. You are effectively drawing onto the easel itself. There's no paper on the easel yet! Before you draw on a memory DC, you need to put a piece of paper (aka a bitmap) on the easel. Otherwise you won't be able to see what you draw, ever.

 
TMemDC MemDC;
TBitmap MyPicture(DC, 500, 500); //makes a bitmap of 500x500
MemDC.SetBitmap(MyPicture);
MemDC.Line(10,10,50,50);

Now you have a bitmap with a line from 10,10 to 50,50 on it! However, it probably also has all kinds of other tripe on it. Bitmaps come uninitialized, so you need to erase what's on them before you go drawing things on them:

 
MemDC.ClearToWhite(0, 0, 500, 500);

Put that line before the MemDC.Line() call and the bitmap will get cleared to white.

Note that I could do this:

 
MemDC.Line(0, 0, 5000, 5000);

This will draw a line right off my paper. That's okay. The part that doesnt fit on the bitmap just gets thrown away. This is the case with all DCs: if you draw outside of the valid range, the rest just gets thrown away.

Anyway, that was off topic. Where were we? Oh, bitmaps. So, why use a bitmap? Well, if you draw straight onto the screen a lot, you may get flickers. The user will see the screen during the various in-between stages before the screen is fully painted. A way to avoid getting this flicker is to draw everything onto a bitmap and then dump that bitmap onto the screen. Think of it as smushing two easels together. You do this with the strangely-named BitBlt() function. Or you can use StretchBlt(), which stretches or shrinks the paper to any given size as it puts it onto the other DC (but this does not cause the bitmap to permanently be a different size.) So, in our example:

 
DC.BitBlt(0, 0, MemDC.Width(), MemDC.Height(), MemDC, 0, 0, SRCCOPY);

This says, "take whatever is on the easel of MemDC and smash it onto DC." You don't have to use TMemDC's with these functions. For instance, if you had a TPrintDC, you could put the contents of your screen onto your printer:

 
TPrintDC Printer("Title of my print job goes here");
Printer.BitBlt(0, 0, Printer.Width(), Printer.Height(), DC, 0, 0, SRCCOPY);

But this would use only a small part of the page, since the screen's resolution is smaller than the page's. To make this fill the page we'd use StretchBlt.

 
Printer.StretchBlt(0, 0, Printer.Width(), Printer.Height(), DC, 0, 0,
                   DC.Width(), DC.Height(), SRCCOPY);

Now I want to address a few problems that often come up when you are first working with bitmaps. So that's what the next few topics will be about.


Help! I want to draw a bitmap onto my screen, but it comes out black and white, or very small, or not at all!

Okay. See the previous question on how bitmaps work, more or less. Here I will cover a few common problems.

* Common problem A: trying to put a bitmap into a screen DC or other non-memory DC. Most types of DCs CANNOT take a bitmap! Only a TMemDC has a SetBitmap() function. So what you do is you whip up a memory-DC, put your bitmap into that, and tell the memory DC to draw itself onto your main DC.

* Common problem B: trying to create a bitmap the wrong way. There are many ways to make a bitmap. Most of them don't require you to know about color depths. If you load a bitmap from a resource, like this:

 
TBitmap Pic(IDB_BMP1); //load IDB_BMP1 from resources

It will automatically figure out how many colors are in the bitmap. But if you make one from scratch, like the line below, you tell it how many colors to use by telling it to "use as many colors as this DC has":

 
TBitmap MyPicture(DC, 500, 500);  //where DC is a TPaintDC

Here you are telling it "I want to make a bitmap that has as many colors as DC, and is 500x500." If you instead said

 
TMemDC MemDC;
TBitmap MyPicture(MemDC, 500, 500);

You are saying, "Make it have as many colors as a memory DC." But memory DCs aren't real devices, and so the operating system just gives it a default number of colors: 2. So if you accidentally did this, your bitmap would be black and white.

* Common problem C: trying to create a memory DC the wrong way. This is very similar to problem B. When you create a memory DC, you tell it what it should be able to draw itself onto -- this determines its color depth. You pass it another DC that it should be "compatible" with. So what you want to do is tell it to be compatible with a TPaintDC or a TScreenDC or something like that.You do NOT want to tell it to be compatible with another memory DC! If you do that, then it will only be able to draw in black and white. But here's a nice trick: if you just don't give the TMemDC ANY parameters when you create it, it will choose a screen DC for you.

So. Here's some code. It just draws a bitmap (from a resource) onto the screen.

 
virtual void TMyWin::Paint(TPaintDC &DC)
{
  TBitmap MyBmp( TResourceName( ID_MYBITMAP ) ); //create a TBitmap by
                                                 //loading a resource with
                                                 //the ID of ID_MYBITMAP
  TMemDC MemDC; //make a memory DC compatible with the screen
  MemDC.SetBitmap( MyBmp ); //plug the bitmap into the memory DC
  DC.BitBlt(0, 0, MyBmp.Width(), MyBmp.Height(), MemDC, 0, 0, SRCCOPY);
  //the line above draws the bitmap! Yay.
}


Help! My picture flickers. How do I fix it?

First off, is your window a dialog? If so, then you have fewer options, because you don't have full control over how the dialog is drawn. But much of the following material is still appropriate. I don't know if the BkBrush technique works for dialogs, but the other techniques are still helpful.

Ok. There are several reasons why your window may flicker. One common reason that it flickers is because you have a noticeable background brush. If you have a bright background brush (either the default background brush, or a brush set by a call to

 
BkBrush( CLR_WHITE );

for instance), then just before your Paint() function is called, the invalid part of your screen will get painted to white. If you then paint the screen to black:

 
virtual void TMyWin::Paint( TPaintDC &DC )
{
  DC.ClearToBlack(0, 0, DC.Width(), DC.Height());
}

the user will see that as a brief flash of white that changes to black, and will interpret that as "flicker". If this happens to you, one fix is to not use a background brush, and to draw the whole screen yourself.

 
BkBrush( TBrush() );

will do the trick. But NOW YOU MUST DRAW ALL OF YOUR WINDOW! If you don't, you'll see weirdness. If you set your brush to NULL [using the code above], then it is up to your Paint() function to repaint all of the invalidated area every time. If it doesn't, the screen will get all garbled and weird. In other words, you shouldn't just do this:

 
[in constructor]

BkBrush( TBrush() );
virtual void TMyWin::Paint( TPaintDC &DC )
{
  DC.XYPrintf(10, 10, "Hi!");
}

because everything except for the pixels under the "Hi!" will not get redrawn. Experiment with it; move the window around. You'll see what I'm talking about. To fix this, you need to manually clear all the pixels:

 
virtual void TMyWin::Paint( TPaintDC &DC )
{
  DC.ClearToWhite(0, 0, DC.Width(), DC.Height());
  DC.XYPrintf(10, 10, "Hi!");
}

Okay. That is the biggest cause of flicker. Another cause of flicker is when you have controls (such as edit boxes and listviews) on your window. Each time you redraw, the main window draws itself, and then the windows draw themselves atop the main window. Well, the main window doesn't need to draw the places underneath the edit boxes and stuff -- the edit boxes will draw that part themselves. To tell your window not to draw under it's children, give your window the WS_CLIPCHILDREN style:

 
[in constructor]

MergeStyle( WS_CLIPCHILDREN ); //add in WS_CLIPCHILDREN without turning
                               //off any other styles; this is the same
                               //as "Style( Style() | WS_CLIPCHILDREN );"

Okay. So your program no longer flickers because of those things, right? But things still don't look quite right? You should try double-buffering. This is where you draw everything onto a bitmap, and then smush the bitmap onto the screen. This avoids flicker caused by the screen being redrawn in between various parts of the drawing sequence. For instance, if you are drawing 50 lines, you don't want the user to be able to see the picture while those lines are being drawn -- they will see a half-finished picture and interpret that as flicker. The standard way to get around this is to make a memory DC and put a bitmap in it, and draw onto the bitmap, then BitBlt() the bitmap back onto the screen. This way there is only one graphics operation on the screen DC itself, and thus no flicker. So, if your current code is this:

 
virtual void TMyWin::Paint( TPaintDC &DC )
{
  DC.ClearToBlack(0, 0, Width(), Height());
  for (int loop = 0; loop < 100; loop++)
  {
    DC.SetPen( TPen(RGB(0, 0, loop * 5)) ); //set the pen to a blue color
    DC.Line(0, 0, loop*3, loop*5); //draw some lines
  }
}

Then you could double-buffer this by changing it to this:

 
virtual void TMyWin::Paint( TPaintDC &DC )
{
  TMemDC MemDC;
  TBitmap DoubleBuffer(DC, Width(), Height()); 
                     //make a bitmap the width & height of window!
  MemDC.SetBitmap(DoubleBuffer);

  MemDC.ClearToBlack(0, 0, Width(), Height());
  for (int loop = 0; loop < 100; loop++)
  {
    MemDC.SetPen( TPen(RGB(0, 0, loop * 5)) ); //set the pen to a blue color
    MemDC.Line(0, 0, loop*3, loop*5); //draw some lines
  }

  DC.BitBlt(0, 0, Width(), Height(), MemDC, 0, 0, SRCCOPY);
}

Simple as that! Just do all your drawing on the memory DC instead of the paint DC. Go get that working and then come back. Got it working? No more flicker? Good. Now, I should tell you about another little step that you can take to make your program more efficient.

Right now, each time you go into the Paint() routine, you create a TBitmap the size of your window. You use it, and then throw it away. This is inefficient because you keep re-allocating the bitmap over and over. It would be much more efficient to just allocate it once when your program starts up, and not reallocate it again unless your window resizes. Well, it turns out that this is easy to do. First, you need to keep the TBitmap around, instead of creating a temporary-use one in your Paint() function. So, add that in your class header. Also, we will need to re-size this bitmap each time the window gets resized, so overload OnSized():

 
class TMyWin : public TWin
{
  public:

  TBitmap DoubleBuffer; //ADDED THIS LINE

  TMyWin( TWin *PParent, const TWinAttr &Changes );

  virtual void Paint( TPaintDC &DC );
  virtual bool OnSized(TSize WH, UINT SizeType); //ADDED THIS LINE
};

So each time your window gets resized, you need to re-size the bitmap. Do this with the TBitmap::Create() function:

 
virtual bool TMyWin::OnSized(TSize, UINT )
{
  TRect r = GetClientRect();
  DoubleBuffer.Create( TScreenDC(), r.Width(), r.Height() );
  return true;
}

You may be wondering why I got the "client rect" and used that width and height instead of just using TWin::Width() and TWin::Height(). Well, the TWin versions would work also, but they would be a little bit larger than we actually need. See, TWin::Width() and TWin::Height() return the entire window's width and height. But we only actually care about the width and height of the "client area", that part of the window that we draw on. So that is what the code above does: it finds out how big the client area is, and makes the bitmap that big. Yeah, I know that my earlier examples used Width() and Height(). That's because I just didn't care that the resulting values were going to be larger than I needed. But if you want it to be exactly the size of your client area and no larger, don't use TWin::Width() and TWin::Height(); use TWin::GetClientRect().Width() and TWin::GetClientRect().Height().

And now, just use DoubleBuffer instead of Buffer in your Paint:

 
virtual void TMyWin::Paint( TPaintDC &DC )
{
  TMemDC MemDC;
  //REMOVED LINE THAT WAS HERE -- NO LONGER NEEDED
  MemDC.SetBitmap(DoubleBuffer); //CHANGED THIS LINE

  MemDC.ClearToBlack(0, 0, Width(), Height());
  for (int loop = 0; loop < 100; loop++)
  {
    MemDC.SetPen( TPen(RGB(0, 0, loop * 5)) ); //set the pen to a blue color
    MemDC.Line(0, 0, loop*3, loop*5); //draw some lines
  }

  DC.BitBlt(0, 0, Width(), Height(), MemDC, 0, 0, SRCCOPY);
}

There! Efficient double-buffering!

You may be worrying, "what if Paint() gets called before OnSized() ever gets called? In that case, DoubleBuffer wouldn't be the right size yet!" But not to fear. It turns out that OnSized() always gets called before Paint(). Windows just sends you an OnSized() right when you start up, for good luck or something. Note that this doesn't apply to dialogs. Doh! So, if you want to use this code in a dialog, or you just want to be sure that the bitmap will get sized properly before you paint, you can set the size of the bitmap in OnInit().

 
virtual void OnInit()
{
  TRect r = GetClientRect();
  DoubleBuffer.Create( r.Width(), r.Height() );
}

There! Now don't worry about it anymore.

 

 (go on to the next page in the tutorial...)