52949.fb2 theForgers Win32 API Tutorial - читать онлайн бесплатно полную версию книги . Страница 4

theForgers Win32 API Tutorial - читать онлайн бесплатно полную версию книги . Страница 4

Graphics Device Interface

Bitmaps, Device Contexts and BitBlt

Example: bmp_one

GDI

The really great thing about MS Windows is that unlike DOS, you don't need to know anything about what video hardware you are using to display graphics. Instead, windows provides an API called the Graphics Device Interface, or GDI. The GDI uses a set of generic graphics objects that can be used to draw to the screen, to memory, or even to printers.

Device Contexts

The GDI revolves around an object called the Device Context (DC), represented by the data type HDC (Handle to Device Context). An HDC is basically a handle to something you can draw on; it can represent the entire screen, an entire window, the client area of a window, a bitmap stored in memory, or a printer. The nice part is that you don't even need to know which one it refers to, you can use it basically the same way, which is especially handy for writing custom drawing functions which you can then use on any of these devices without changing it for each one.

An HDC like most GDI objects is opaque, meaning that you can't access it's data directly… but you can pass it to various GDI functions that will operate on it, either to draw something, get information about it, or change the object in some way.

For example, if you wanted to draw on a window, first you would retreive an HDC representing the window with GetDC(), then you could use any of the GDI functions that take an HDC like BitBlt() for drawing images, TextOut() for drawing text, LineTo() for lines and so on.

Bitmaps

Bitmaps can be loaded much like icons in earlier examples, there is LoadBitmap() for the most basic functionality of simply loading a bitmap resource, and LoadImage() can be used to load bitmaps from a *.bmp file just as it can for icons.

One of the quirks of GDI is that you can't draw to bitmap objects (HBITMAP type) directly. Remember that drawing operations are abstracted by Device Contexts, so in order to use these drawing functions on a bitmap, you need to create a Memory DC, and then select the HBITMAP into it with SelectObject(). The effect is that the "device" that the HDC refers to is the bitmap in memory, and when you operate on the HDC , the resulting graphic operations are applied to the bitmap. As I mentioned, this is actually a very conveiniant way of doing things, as you can write code that draws to an HDC and you can use it on a Window DC or a Memory DC without any checks or changes.

You do have the option of manipulating the bitmap data in memory yourself. You can do this with Device Independant Bitmaps (DIB), and you can even combine GDI and manual operations on the DIB. However for the time being, this is beyond the scope of the basic tutorial and for now we're just cover the simpler GDI operations on their own.

GDI Leaks

Once you're finished with an HDC , it's very important to release it (just how you do that depends on how you got it, which we'll talk about in a bit). GDI objects are limited in number. In versions of windows prior to Windows 95, they were not only incredably limited but also shared system wide, so that if one program used up too many, none of the rest would be able to draw anything! Fortunately this isn't the case any longer, and you could get away with using up quite a lot of resources in Windows 2000 or XP before anything too bad happened… but it's easy to forget to free GDI objects and they can quickly run your program out of GDI resources under Windows 9x. Theorehtically you shouldn't be able to drain the system of GDI resources in NT systems (NT/2K/XP) but it still happens in extreme cases, or if you hit the right bug on the nose.

If your program runs fine for a few minutes and then starts drawing strangely or not at all, it's a good sign that you're leaking GDI resources. HDCs aren't the only GDI objects you need to be careful about releasing, but generally it's ok to keep things like bitmaps and fonts around for the entire lifetime of your program, since it's much more efficiant than reloading them each time you need them.

Also, an HDC can only contain one of each type of object (bitmap, font, pen…) at a time, and when you select a new one in it will return the last one. It's very important that you deal with this object properly. If you ignore it completely, it will be lost and they will pile up in memory causing GDI leaks. When an HDC is created, it's also created with some default objects selected into it… it's a good idea to store these when they are returned to you, and then when you are completed drawing with the HDC select them back into it. This will not only remove any of your own objects from the HDC (which is a good thing) but it will also cause the default objects to be properly disposed of when you release or destroy the HDC (a VERY good thing).

Important Update: Not all objects have defaults selected into HDC s, and you can refer to MSDN for the few that don't. Because of this I was previously uncertain as to wether HBITMAP s were one of them, since there doesn't seem to be any definitive documentation on it, and examples (even those by Microsoft) often ignored the default bitmap. Since the writing of the original tutorial several years ago, it was confirmed to me that there was in fact a default bitmap that needs releasing. This information is courtesy of Shaun Ivory, a software engineer for MS and a friend of mine from #winprog.

Apparently there was a bug in a screensaver written at MS, and it turns out it was because the default bitmap wasn't getting replaced or destroyed, and it eventually ran out of GDI resources. Be warned! It's an easy mistake to make.

Displaying Bitmaps

Ok, down to business. The simplest drawing operations on a window occure by handling WM_PAINT. When your window is first displayed, restored from being minimised, or uncovered from having another window on top of it, Windows sends the WM_PAINT message to the window to let it know that it needs to redraw it's contents. When you draw something on the screen it is NOT permanent, it's only there untill something else draws over it, and at that point you need to draw it again when the time comes.

HBITMAP g_hbmBall = NULL;

 case WM_CREATE:

  g_hbmBall = LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_BALL));

  if (g_hbmBall == NULL) MessageBox(hwnd, "Could not load IDB_BALL!", "Error", MB_OK | MB_ICONEXCLAMATION);

  break;

The first step is of course loading the bitmap, this is quite simple with a bitmap resource, there are no significant differences from loading other resource types. Then we can get down to drawing…

case WM_PAINT:

 {

  BITMAP bm;

  PAINTSTRUCT ps;

  HDC hdc = BeginPaint(hwnd, &ps);

  HDC hdcMem = CreateCompatibleDC(hdc);

  HBITMAP hbmOld = SelectObject(hdcMem, g_hbmBall);

  GetObject(g_hbmBall, sizeof(bm), &bm);

  BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

  SelectObject(hdcMem, hbmOld);

  DeleteDC(hdcMem);

  EndPaint(hwnd, &ps);

 }

 break;

Getting the Window DC

To start off we declare a couple of variables we need. Notice that the first one is a BITMAP, not an HBITMAP. BITMAP is a struct that holds information about an HBITMAP which is the actual GDI object. We need a way to get the height and width of the HBITMAP so we use GetObject() which contrary to it's name doesn't really get an object, but rather information about an existing one. "GetObjectInfo" would have been a more appropriate label. GetObject() works for various GDI object types which it can distinguish based on the value of the second parameter, the size of the structure.

The PAINTSTRUCT is a structure that contains information about the window being painted and what exactly is going on with the paint message. For most simple tasks, you can simply ignore the information it contains, but it's required for the call to BeginPaint(). BeginPaint() as it's name suggests is designed specifically for handling the WM_PAINT message. When not handling a WM_PAINT message you would use GetDC() which we will see in the timer animation examples in a while… but in WM_PAINT, it's important to use BeginPaint() and EndPaint().

BeginPaint() returns us an HDC that represents the HWND that we pass to it, the one that WM_PAINT is being handled for. Any drawing operation we perform on this HDC will immediately display on the screen.

Setting up a Memory DC for the Bitmap

As I mention above, in order to draw on or with bitmaps, we need to create a DC in memory… the easiest way to do that here is to CreateCompatibleDC() with the one we already have. This gives us a Memory DC that is compatible with the color depth and display properties of the HDC for the window.

Now we call SelectObject() to select the bitmap into the DC being careful to store the default bitmap so that we can replace it later on and not leak GDI objects.

Drawing

Once we've gotten the dimentions of the bitmap filled into the BITMAP struct, we can call BitBlt() to copy the image from our Memory DC to the Window DC, thus displaying on the screen. As always, you can look up each parameter in MSDN, but in short they are: The destination, the position and size, the source and source position, and finally the Raster Operation (ROP code), which specifies how to do the copy. In this case, we want a simple exact copy of the source made, no fancy stuff.

BitBlt() is probably the all time happiest function in all of the Win32 API and is the staple diet of anyone learning to write games or other graphics applications in windows. It was probably the first API that I memorised all the parameters to.

Cleanup

At this point the bitmap should be on the screen, and we need to clean up after ourselves. The first thing to do is restore the Memory DC to the state it was when we got it, which means replacing our bitmap with the default one that we saved. Next we can delete it altogether with DeleteDC().

Finally we release the Window DC we got from BeginPaint() using EndPaint().

Destroying an HDC is a little confusing sometimes because there are at least 3 ways to do it depending on how you got it in the first place. Here's a list of the common methods of gaining an HDC, and how to release it when you're done.

• GetDC() — ReleaseDC()

• BeginPaint() — EndPaint()

• CreateCompatibleDC() — DeleteDC()

And finally, at the termination of our program, we want to free any resources that we allocated. Technically speaking this isn't absolutely required, since modern Windows platforms are pretty good at freeing everything when your program exists, but it's always a good idea to keep track of your own objects because if get lazy and don't delete them they have a habit of getting loose. And no doubt, there are still bugs in windows especially older versions that won't clean up all of your GDI objects if you don't do a thorough job.

case WM_DESTROY:

 DeleteObject(g_hbmBall);

 PostQuitMessage(0);

 break;

Transparent Bitmaps

Example: bmp_two

Transparency

Giving bitmaps the appearance of having transparent sections is quite simple, and involves the use of a black and white Mask image in addition to the colour image that we want to look transparent.

The following conditions need to be met for the effect to work correctly: First off, the colour image must be black in all areas that we want to display as transparent. and second, the mask image must be white in the areas we want transparent, and black elsewhere. The colour and mask images are displayed as the two left most images in the example picture on this page.

BitBlt operations

How does this get us transparency? First we BitBlt() the mask image using the SRCAND operation as the last parameter, and then on top of that we BitBlt() the colour image using the SRCPAINT operation. The result is that the areas we wanted transparent don't change on the destination HDC while the rest of the image is drawn as usual.

SelectObject(hdcMem, g_hbmMask);

BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCAND);

SelectObject(hdcMem, g_hbmBall);

BitBlt(hdc, 0, bm.bmHeight, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCPAINT);

Pretty simple eh? Fortunately it is, but one question remains… where does the mask come from? There are basically two ways to get the mask…

• Make it yourself in whatever graphics program you made the colour bitmap in, and this is a reasonable solution if you are using a limited number of graphics in your program. This way you can just add the mask resource to your program and load it with LoadBitmap().

• Generate it when your program runs, by selecting one colour in your original image to be your "transparent" colour, and create a mask that is white everywhere that colour exists, and black everywhere else.

Since the first one is nothing new, you should be able to do things that way yourself if you want to. The second way involves from BitBlt() trickery, and so I will show one way of accomplishing this.

Mask Creation

The simplest way to do it, would be to loop through every pixel on the colour image, check it's value and then set the corresponding pixel on the mask to black or white… SetPixel() is a very slow way to draw images however, and it's not really practical.

A much more efficient way involves using the way BitBlt() converts from colour images to black and white. If you BitBlt() (using SRCCOPY) from an HDC holding a colour image into an HDC holding a black and white image, it will check what colour is set as the Background Colour on the colour image, and set all of those pixels to White, any pixel that is not the background colour will end up Black.

This works perfectly to our advantage, since all we need to do is set the background colour to the colour we want transparent, and BitBlt() from the colour image to the mask image. Note that this only works with a mask bitmap that is monochrome (black and white)… that is bitmaps with a bit depth of 1 bit per pixel. If you try it with a colour image that only has black and white pixels, but the bitmap itself is greater than 1 bit (say 16 or 24 bit) then it won't work.

Remember the first condition for succesful masking above? It was that the colour image needs to be black everywhere we want transparent. Since the bitmap I used in this example already meets that condition it doesn't really need anything special done, but if you're going to use this code for another image that has a different colour that you want transparent (hot pink is a common choice) then we need to take a second step, and that is use the mask we just created to alter the original image, so that everywhere we want transparent is black. It's ok if other places are black too, because they aren't white on the mask, they won't end up transparent. We can accomplish this by BitBlt()ing from the new mask to the original colour image, using the SRCINVERT operation, which sets all the areas that are white in the mask to black in the colour image.

This is all a bit of a complex process, and so it's nice to have a handy utility function that does this all for us, and here it is:

HBITMAP CreateBitmapMask(HBITMAP hbmColour, COLORREF crTransparent) {

 HDC hdcMem, hdcMem2;

 HBITMAP hbmMask; BITMAP bm;

 // Create monochrome (1 bit) mask bitmap.

 GetObject(hbmColour, sizeof(BITMAP), &bm);

 hbmMask = CreateBitmap(bm.bmWidth, bm.bmHeight, 1, 1, NULL);

 // Get some HDCs that are compatible with the display driver

 hdcMem = CreateCompatibleDC(0);

 hdcMem2 = CreateCompatibleDC(0);

 SelectBitmap(hdcMem, hbmColour);

 SelectBitmap(hdcMem2, hbmMask);

 // Set the background colour of the colour image to the colour

 // you want to be transparent.

 SetBkColor(hdcMem, crTransparent);

 // Copy the bits from the colour image to the B+W mask… everything

 // with the background colour ends up white while everythig else ends up

 // black… Just what we wanted.

 BitBlt(hdcMem2, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

 // Take our new mask and use it to turn the transparent colour in our

 // original colour image to black so the transparency effect will

 // work right.

 BitBlt(hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem2, 0, 0, SRCINVERT);

 // Clean up.

 DeleteDC(hdcMem);

 DeleteDC(hdcMem2);

 return hbmMask;

}

NOTE: This function call SelectObject() to temporarily select the colour bitmap we pass it into an HDC. A bitmap can't be selected into more than one HDC at a time, so make sure the bitmap isn't selected in to another HDC when you call this function or it will fail. Now that we have our handy dandy function, we can create a mask from the original picture as soon as we load it:

case WM_CREATE:

 g_hbmBall = LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_BALL));

 if (g_hbmBall == NULL) MessageBox(hwnd, "Could not load IDB_BALL!", "Error", MB_OK | MB_ICONEXCLAMATION);

 g_hbmMask = CreateBitmapMask(g_hbmBall, RGB(0, 0, 0));

 if (g_hbmMask == NULL) MessageBox(hwnd, "Could not create mask!", "Error", MB_OK | MB_ICONEXCLAMATION);

 break;

The second parameter is of course the colour from the original image that we want to be transparent, in this case black.

How does all this work?

.. you may be asking. Well hopefully your experience with C or C++ means that you understand binary operations such as OR, XOR, AND, NOT and so on. Ii'm not going to exaplain this process completely, but I will try to show how I used it for this example. If my explanation isn't clear enough (which it's bound to not be), reading up on binary operations should help you understand it. Understanding it isn't critical for using it right now, and you can just get away with trusting that it works if you want.

SRCAND

The SRCAND raster operation, or ROP code for BitBlt() means to combine the bits using AND that is only bits that are set both in the source AND the destination get set in the final result. We use this with our mask to set to black all the pixels that will eventually have colour on them from the colour image. The mask image has black (which in binary is all 0's) where we want colour, and white (all 1's) where we want transparency. Any value combined with 0 using AND is 0, and therefor all the pixels that are black in the mask are set to 0 in the result and end up black as well. Any value that is combined with 1 using AND is left unaffected, so if it was 1 to begin with it stays 1, and if it was 0 to begin with it stays 0… therefor all the pixels that are white in our mask, are completely unaffected after the BitBlt() call. The result is the top right image in the example picture.

SRCPAINT

SRCPAINT uses the OR operation, so if either (or both) of the bits are set, then they will be set in the result. We use this on the colour image. When the black (transparent) part of our colour image is combined with the data on the destination using OR, the result is that the data is untouched, because any value combined with 0 using the OR operation is left unaffected.

However, the rest of our colour image isn't black, and if the destination also isn't black, then we get a combination of the source and destination colours, the result you can see in the second ball on the second row in the example picture. This is the whole reason for using the mask to set the pixels we want to colour to black first, so that when we use OR with the colour image, the coloured pixels don't get mixed up with whatever is underneath them.

SRCINVERT

This is the XOR operation used to set the transparent colour in our original image to black (if it isn't black already). Combining a black pixel from the mask with a non-background colour pixel in the destination leaves it untouched, while combining a white pixel from the mask (which remember we generated by setting a particular colour as the "background") with the background colour pixel on the destination cancels it out, and sets it to black.

This is all a little GDI mojo that depends on it's colour vs. monochrome handling, and it hurts my head to think about it too much, but it really makes sense… honest. Example

The example code in the project bmp_two that goes along with this section contains the code for the example picture on this page. It consists of first drawing the mask and the colour image exactly as they are using SRCCOPY, then using each one alone with the SRCAND and SRCPAINT operations respectively, and finally combining them to produce the final product.

The background in this example is set to gray to make the transparency more obvious, as using these operations on a white or black background makes it hard to tell if they're actually working or not.

Timers and Animation

Example: anim_one

Setting up

Before we get things animated, we need to set up a structure to store the position of the ball between updates. This struct will store the current position and size of the ball, as well as the delta values, how much we want it to move each frame.

Once we have the structure type declared, we also declare a global instance of the struct. This is ok since we only have one ball, if were were going to animate a bunch of them, you'd probably want to use an array or other container (such as a linked list in C++) to store them in a more convenient way.

const int BALL_MOVE_DELTA = 2;

typedef struct _BALLINFO {

 int width;

 int height;

 int x;

 int y;

 int dx;

 int dy;

} BALLINFO;

BALLINFO g_ballInfo;

We've also defined a constant BALL_MOVE_DELTA which is how far we want the ball to move on each update. The reason we store deltas in the BALLINFO structure as well is that we want to be able to move the ball left or right and up and down independantly, BALL_MOVE_DELTA is just a handy name to give the value so we can change it later if we want.

Now we need to initialize this structure after we load our bitmaps:

BITMAP bm;

GetObject(g_hbmBall, sizeof(bm), &bm);

ZeroMemory(&g_ballInfo, sizeof(g_ballInfo));

g_ballInfo.width = bm.bmWidth;

g_ballInfo.height = bm.bmHeight;

g_ballInfo.dx = BALL_MOVE_DELTA;

g_ballInfo.dy = BALL_MOVE_DELTA;

The ball starts off in the top left corner, moving to the right and down according to the dx and dy members of BALLINFO.

Setting the Timer

The easiest way to add a simple timer into a window program is with SetTimer(), it's not the best, and it's not recommended for real multimedia or full games, however it's good enough for simple animations like this. When you need something better take a look at timeSetEvent() in MSDN; it's more accurate.

const int ID_TIMER = 1;

ret = SetTimer(hwnd, ID_TIMER, 50, NULL);

if (ret == 0) MessageBox(hwnd, "Could not SetTimer()!", "Error", MB_OK | MB_ICONEXCLAMATION);

Here we've declared a timer id so that we can refer to it later (to kill it) and then set the timer in the WM_CREATE handler of our main window. Each time the timer elapses, it will send a WM_TIMER message to the window, and pass us back the ID in wParam. Since we only have one timer we don't need the ID, but it's useful if you set more than one timer and need to tell them apart.

We've set the timer to elapse every 50 milliseconds, which results in approximately 20 frames per second. Approximately because like I said, SetTimer() is a little inaccurate, but this isn't critical code, and a few milliseconds here or there won't kill us.

Animating in WM_TIMER

Now when we get WM_TIMER we want to calculate the new position for the ball and draw it's updated position.

case WM_TIMER:

 {

  RECT rcClient;

  HDC hdc = GetDC(hwnd);

  GetClientRect(hwnd, &rcClient);

  UpdateBall(&rcClient);

  DrawBall(hdc, &rcClient);

  ReleaseDC(hwnd, hdc);

 }

 break;

I've put the code for updating and drawing the ball in their own functions. This is good practice, and it lets us draw the ball from either WM_TIMER or WM_PAINT without duplicating code, note that the method we use to get the HDC in each case is different, so it's best to leave this code in the message handlers and pass the result into the DrawBall() function.

void UpdateBall(RECT* prc) {

 g_ballInfo.x += g_ballInfo.dx;

 g_ballInfo.y += g_ballInfo.dy;

 if (g_ballInfo.x < 0) {

  g_ballInfo.x = 0;

  g_ballInfo.dx = BALL_MOVE_DELTA;

 } else if(g_ballInfo.x + g_ballInfo.width > prc->right) {

  g_ballInfo.x = prc->right - g_ballInfo.width;

  g_ballInfo.dx = -BALL_MOVE_DELTA;

 }

 if(g_ballInfo.y < 0) {

  g_ballInfo.y = 0;

  g_ballInfo.dy = BALL_MOVE_DELTA;

 } else if(g_ballInfo.y + g_ballInfo.height > prc->bottom) {

  g_ballInfo.y = prc->bottom - g_ballInfo.height;

  g_ballInfo.dy = -BALL_MOVE_DELTA;

 }

}

All this does is some basic math, we add the delta value to the x position to move the ball. If the ball goes outside the client area, move it back in range and change the delta value to the opposite direction so that the ball "bounces" off the sides.

void DrawBall(HDC hdc, RECT* prc) {

 HDC hdcBuffer = CreateCompatibleDC(hdc);

 HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);

 HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

 HDC hdcMem = CreateCompatibleDC(hdc);

 HBITMAP hbmOld = SelectObject(hdcMem, g_hbmMask);

 FillRect(hdcBuffer, prc, GetStockObject(WHITE_BRUSH));

 BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCAND);

 SelectObject(hdcMem, g_hbmBall);

 BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCPAINT);

 BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);

 SelectObject(hdcMem, hbmOld);

 DeleteDC(hdcMem);

 SelectObject(hdcBuffer, hbmOldBuffer);

 DeleteDC(hdcBuffer);

 DeleteObject(hbmBuffer);

}

This is essentially the same drawing code as the past few examples, with the exception that it gets the position and dimentions of the ball from the BALLINFO structure. There is however one important difference…

Double Buffering

When doing your drawing directly to the HDC of the window, it's entirely possible that the screen will get updated before you're done… for example after you draw the mask and before you draw the colour image over top, the user might see a flicker of the back background before your program has a chance to draw over it in colour. The slower your computer and the more drawing operations that you do, the more flicker will be apparent and eventually it will look like a big jumbled mess.

This is terribly distracting, and we can solve it simply by doing all the drawing in memory first, and then copying the completed masterpiece to the screen in a single BitBlt() so that the screen is updated directly from the old image, to the complete new image with none of the individual operations visible.

To do this, we create a temporary HBITMAP in memory that is the exact size of the area we are going to draw to on the screen. We also need an HDC so that we can BitBlt() to the bitmap.

HDC hdcBuffer = CreateCompatibleDC(hdc);

HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);

HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

Now that we have a place to draw to in memory, all of the drawing operations use hdcBuffer instead of hdc (the window) and the results are stored on the bitmap in memory untill we are complete. We can now copy the whole thing over to the window in one shot.

BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);

That's it, and we clean up our HDCs and HBITMAP s as usual.

Faster Double Buffering

In this example I am creating and destroying the bitmap used for double buffering each frame, I did this basically because I wanted to be able to size the window so it's easier to just always create a new buffer than to track when the window position changes and resize the buffer. It would be more efficient to create a global double buffer bitmap and either not allow the window to resize or only resize the bitmap when the window resized, instead of creating it and destroying it all the time. It's up to you to implement this if you want to optimize the drawing for a game or something.

Killing the Timer

When our window is destroyed, it's a good idea to release all resources we used, and in this case that includes the timer we set. To stop it, we simply call KillTimer() and pass in the ID that we used when we created it.

KillTimer(hwnd, ID_TIMER);

Text and Fonts

Example: font_one

Loading Fonts

The Win32 GDI has some remarkable capabilites for dealing with vastly different typefaces, styles, languages and characters sets. One of the drawbacks of this is that dealing with fonts can look rather intimidating to the newcomer. CreateFont(), the primary API when it comes to fonts, has 14 parameters for specifying height, style, weight, family, and various other attributes.

Fortunately, it's not really has hard as it might appear, and a large portion of the work involved is taken care of my sensible default values. All but 2 of the parameters to CreateFont() can be set to 0 or NULL, and the system will simply use a default value giving you a plain ordinary font.

CreateFont() creates an HFONT , a handle to a Logical Font in memory. The data held by this handle can be retreived into a LOGFONT structure using GetObject() just as a BITMAP struct can be filled from an HBITMAP.

The members of the LOGFONT are identical to the parameters to CreateFont() and for convenience you can create a font directly from an existing LOGFONT structure using CreateFontIndirect(). This is very handy, since it makes it simple to create a new font from an existing font handle when you only want to alter certain aspects of it. Use GetObject() to fill a LOGFONT, alter the members that you wish, and create a new font with CreateFontIndirect().

HFONT hf;

HDC hdc;

long lfHeight;

hdc = GetDC(NULL);

lfHeight = –MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72);

ReleaseDC(NULL, hdc);

hf = CreateFont(lfHeight, 0, 0, 0, 0, TRUE, 0, 0, 0, 0, 0, 0, 0, "Times New Roman");

if (hf) {

 DeleteObject(g_hfFont);

 g_hfFont = hf;

} else {

 MessageBox(hwnd, "Font creation failed!", "Error", MB_OK | MB_ICONEXCLAMATION);

}

This is the code used to create the font in the example image. This is Times New Roman at 12 Point with the Italics style set. The italics flag is the 6th parameter to CreateFont() which you can see we have set to TRUE . The name of the font we want to use is the last parameter.

The one bit of trickery in this code is the value used for the size of the font, the lfHeight parameter to CreateFont(). Usually people are used to working with Point sizes, Size 10, Size 12, etc… when dealing with fonts. CreateFont() however doesn't accept point sizes, it wants Logical Units which are different on your screen than they are on your Printer, and even between Printers and screens.

The reason this situation exists is because the resolution of different devices is so vastly different… Printers can easily display 600 to 1200 pixels per inch, while a screen is lucky to get 200… if you used the same sized font on a printer as on a screen, you likely wouldn't even be able to see individual letters.

All we have to do is convert from the point size we want, into the appropriate logical size for the device. In this case the device is the screen, so we get the HDC to the screen, and get the number of logical pixels per inch using GetDeviceCaps() and slap this into the formula so generously provided in MSDN which uses MulDiv() to convert from our pointsize of 12 to the correct logical size that CreateFont() expects. We store this in lfHeight and pass it as the first parameter to CreateFont().

Default Fonts

When you first call GetDC() to get the HDC to your window, the default font that is selected into it is System, which to be honest isn't all that attractive. The simplest way to get a reasonable looking font to work with (without going through the CreateFont() hassle) is to call GetStockObject() and ask for the DEFAULT_GUI_FONT.

This is a system object and you can get it as many times as you want without leaking memory, and you can call DeleteObject() on it which won't do anything, which is good because now you don't need to keep track of whether your font is one from CreateFont() or GetStockObject() before trying to free it.

Drawing Text

Now that we have a handy-dandy font, how do we get some text on the screen? This is assuming that we don't just want to use an Edit or Static control.

Your basic options are TextOut() and DrawText(). TextOut() is simpler, but has less options and doesn't do word wrapping or alignment for you.

char szSize[100];

char szTitle[] = "These are the dimensions of your client area:";

HFONT hfOld = SelectObject(hdc, hf);

SetBkColor(hdc, g_rgbBackground);

SetTextColor(hdc, g_rgbText);

if (g_bOpaque) {

 SetBkMode(hdc, OPAQUE);

} else {

 SetBkMode(hdc, TRANSPARENT);

}

DrawText(hdc, szTitle, –1, prc, DT_WORDBREAK);

wsprintf(szSize, "{%d, %d, %d, %d}", prc->left, prc->top, prc->right, prc->bottom);

DrawText(hdc, szSize, –1, prc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);

SelectObject(hdc, hfOld);

First thing we do is use SelectObject() to get the font we want to use into our HDC and ready for drawing. All future text operations will use this font untill another one is selected in.

Next we set the Text and Background colours. Setting the background colour doesn't actually make the whole background this colour, it only affects certain operations (text being one of them) that use the background colour to draw with. This is also dependant on the current Background Mode. If it is set to OPAQUE (the default) then any text drawn is filled in behing with the background colour. If it is set to TRANSPARENT then text is drawn without a background and whatever is behind will show through and in this case the background colour has no effect.

Now we actually draw the text using DrawText(), we pass in the HDC to use and the string to draw. The 3rd parameter is the length of the string, but we've passed –1 because DrawText() is smart enough that it will figure out how long the text is itself. In the 4th parameter we pass in prc, the pointer to the client RECT. DrawText() will draw inside this rectangle based on the other flags that you give it.

In the first call, we specify DT_WORDBREAK, which defaults to aligned to the top left, and will wrap the text it draws automatically at the edge of the rectangle… very useful.

For the second call, we're only printing a single line without wrapping, and we want it to be centered horizontally as well as vertically (which DrawText() will do only when drawing a single line).

Client Redraw

Just a note about the example program… when the WNDCLASS is registered I have set the CS_VREDRAW and CS_HREDRAW class styles. This causes the entire client area to be redrawn if the window is resized, whereas the default is to only redraw the parts that have changed. That looks really bad since the centered text moves around when you resize and it doesn't update like you'd expect.

Choosing Fonts

In general, any program that deals with fonts will want to let the user choose their own font, as well as the colour and style attribute to use when displaying it.

Like the common dialogs for getting open and save file names, there is a common dialog for choosing a font. This is, oddly enough, called ChooseFont() and it works with the CHOOSEFONT structure for you to set the defaults it should start with as well as returning the final result of the users selection.

HFONT g_hfFont = GetStockObject(DEFAULT_GUI_FONT);

COLORREF g_rgbText = RGB(0, 0, 0);

void DoSelectFont(HWND hwnd) {

 CHOOSEFONT cf = {sizeof(CHOOSEFONT)};

 LOGFONT lf;

 GetObject(g_hfFont, sizeof(LOGFONT), &lf);

 cf.Flags = CF_EFFECTS | CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS;

 cf.hwndOwner = hwnd;

 cf.lpLogFont = &lf;

 cf.rgbColors = g_rgbText;

 if (ChooseFont(&cf)) {

  HFONT hf = CreateFontIndirect(&lf);

  if(hf) {

   g_hfFont = hf;

  } else {

   MessageBox(hwnd, "Font creation failed!", "Error", MB_OK | MB_ICONEXCLAMATION);

  }

  g_rgbText = cf.rgbColors;

 }

}

The hwnd in this call is simply the window you want to use as the parent for the font dialog.

The easiest way to use this dialog is in conjunction with an existing LOGFONT structure, which is most likely from whichever HFONT you are currently using. We set the lpLogFont member of the structure to point to the LOGFONT that we just filled with our current information and also added the CF_INITTOLOGFONTSTRUCT flag so that ChooseFont() knows to use this member. The flag CF_EFFECTS tells ChooseFont() to allow the user to select a colour, as well as Underline and Strikeout attributes.

Oddly enough, the Bold and Italics styles don't count as effects, they are considered part of the font itself and in fact some fonts only come in Bold or Italics. If you want to check or prevent the user from selecting a bold or italic font you can check the lfWeight and lfItalic members of the LOGFONT respectively, after the user has made their selection. You can then prompt the user to make another selection or something change the members before calling CreateFontIndirect().

The colour of a font is not associated with an HFONT, and therefor must be stored seperately, the rgbColors member of the CHOOSEFONT struct is used both to pass in the initial colour and retreive the new colour afterward.

CF_SCREENFONTS indicates that we want fonts designed to work on the screen, as opposed to fonts that are designed for printers. Some support both, some only one or the other. Depending on what you're going to be using the font for, this and many other flags can be found in MSDN to limit exactly which fonts you want the user to be able to select.

Choosing Colours

In order to allow the user to change just the colour of the font, or to let them pick a new colour for anything at all, there is the ChooseColor() common dialog. This is the code used to allow the user to select the background colour in the example program.

COLORREF g_rgbBackground = RGB(255, 255, 255);

COLORREF g_rgbCustom[16] = {0};

void DoSelectColour(HWND hwnd) {

 CHOOSECOLOR cc = {sizeof(CHOOSECOLOR)};

 cc.Flags = CC_RGBINIT | CC_FULLOPEN | CC_ANYCOLOR;

 cc.hwndOwner = hwnd;

 cc.rgbResult = g_rgbBackground;

 cc.lpCustColors = g_rgbCustom;

 if(ChooseColor(&cc)) {

  g_rgbBackground = cc.rgbResult;

 }

}

This is fairly straightforward, again we're using the hwnd parameter as the parent to the dialog. The CC_RGBINIT parameter says to start off with the colour we pass in through the rgbResult member, which is also where we get the colour the user selected when the dialog closes.

The g_rgbCustom array of 16 COLORREF s is required to store any values the user decides to put into the custom colour table on the dialog. You could potentially store these values somewhere like the registry, otherwise they will simply be lost when your program is closed. This parameter is not optional.

Control Fonts

Something else you might want to do at some point is change the font on the controls on your dialog or window. This is usually the case when using CreateWindow() to create controls as we've done in previous examples. Controls like windows use System by default, so we used WM_SETFONT to set a new font handle (from GetStockObject() ) for the control to use. You can use this method with fonts you create from CreateFont() as well. Simply pass the font handle as wParam and set lParam to TRUE to make the control redraw.

I've done this in previous examples, but it makes sense to mention it here because it's relevant and very short:

SendDlgItemMessage(hwnd, IDC_OF_YOUR_CONTROL, WM_SETFONT, (WPARAM)hfFont, TRUE);

Where hfFont is of course the HFONT you want to use, and IDC_OF_YOUR_CONTROL is the ID of whichever control you want to change the font of.