Выбрать главу

 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.