Книга: Windows API Tutorials

Direct Draw App

Direct Draw App

If you want fast animation (games!), you have to use DirectX. Try filling the screen by drawing each pixel separately using GDI. Compare it with DirectX, which gives you practically direct access to the screen buffer. There's no competition!

The bad news is that DirectX involves dealing with COM interfaces. The good news is that you can encapsulate them nicely inside C++ classes. Let me show you first how to use these classes to write a simple application that displays an animated, rotating, bitmap. We'll use Windows timer to drive the animation. We'll have to:

• Initialize the program, including the DirectX subsystem

• Respond to timer messages

• Respond to window resizing (we'll use the window mode, rather than full screen)

• Respond to paint messages

Let's start with the WM_PAINT message. This message is sent to us when Windows needs to repaint our window. We have to stash somewhere a complete animation frame, so that we can quickly blit it to screen in response to WM_PAINT. The storage for this frame will be called the back surface.

Upon receiving the timer message, we have to paint a new frame on the back surface. Once we're done, we'll invalidate our window, so that WM_PAINT message will be sent to us.

Upon resizing the window, we have to create a new back surface, corresponding to the new window size. Then we'll draw on it and invalidate the window.

The initialization involves creating the timer, creating the World (that's where we keep our bitmap and drive the animation), creating the View, and setting the first timer countdown.

void Controller::Create (CREATESTRUCT * create) {
 _timer.Create (_h, 1);
 _world = new World (create->hInstance, IDB_RS);
 _view.Create (_h, _world);
 _timer.Set (10);
}

Here's our response to WM_SIZE. We let the View know about the new size (it will create a new back surface), we call it's Update method which draws the animation frame, and we invalidate the window.

bool Controller::Size (int width, int height) {
 _view.Size (_h, width, height);
 _view.Update ();
 // force repaint
 _h.Invalidate (false);
 return true;
}

In response to WM_PAINT, we let the View do the painting (blit the back surface to the screen). We also have to tell Windows that the painting is done-validate the uncovered area. This is done simply by constructing the PaintCanvas object.

bool Controller::Paint (HDC hdc) {
 _view.Paint (_h);
 // validate painted area
 PaintCanvas canvas (_h);
 return true;
}

In response to WM_TIMER, we kill the timer, tell the World to move one frame ahead, let the View repaint the frame on the back surface, invalidate the window (so that Paint is called) and set the timeout again.

bool Controller::Timer (int id, TIMERPROC * proc) {
 _timer.Kill ();
 _world->Step ();
 _view.Update ();
 // force repaint
 _h.Invalidate (false);
 _timer.Set (10);
 return true;
}

There is one more thing. DirectX doesn't like when a screen saver kicks in, so we have to preempt it.

bool Controller::SysCommand (int cmd) {
 // disable screen savers
 if (cmd == SC_SCREENSAVE || cmd == SC_MONITORPOWER) return true;
 return false;
}

By the way, this is how SysCommand is called from the window procedure:

case WM_SYSCOMMAND:
 if (pCtrl->SysCommand (wParam & 0xfff0)) return 0;
 break;

Let's now have a closer look at View, which is the class dealing with output to screen. Notice two important data members, the Direct::Draw object and the Direct::Surface object.

class View {
 public: View ();
 void Create (HWND h, World * world);
 void Size (HWND h, int width, int height);
 void Update ();
 void Paint (HWND h);
private:
 Direct::Draw _draw;
 Direct::Surface _backSurface;
 World * _world;
 int _dx;
 int _dy;
};

I have encapsulated all DirectX classes inside the namespace Direct. this trick enforces a very nice naming convention, namely, you have to prefix all directx classes with the prefix Direct::.

The Direct::Draw object's duty is to initialize the DirectX subsystem. It does it in it's constructor, so any time View is constructed (once per program instantiation), DirectX is ready to use. It's destructor, by the way, frees the resources used by DirectX.

Direct::Surface _backSurface will be used by View to draw the animation upon it.

The initialization of DirectX also involves setting up the collaboration level with Windows. Here, we are telling it to work nicely with other windows, rather then taking over in the full-screen mode.

void View::Create (HWND h, World * world) {
 _world = world;
 // Set coopration with Windows
 _draw.SetCoopNormal (h);
}

Back surface is created (and re-created) in response to WM_SIZE message. Surfaces are implemented in such a way that regular assignment operation does the right thing. It deallocates the previous surface and initializes a new one. Notice that the constructor of an OffScreenSurface takes the Direct::Draw object and the dimensions of the surface in pixels.

void View::Size (HWND h, int width, int height) {
 _dx = width;
 _dy = height;
 if (_dx == 0 || _dy == 0) return;
 _backSurface = Direct::OffScreenSurface (_draw, _dx, _dy);
}

Painting is slightly tricky. We have to get access to the primary drawing surface, which is the whole screen buffer. But since we are not using the full-screen mode, we want to draw only inside the rectangular area of our window. That's why we create a Direct::Clipper, to clip anything that would fall outside of that area. The clipper figures out the correct area using the handle to the window that we are passing to it.

Next, we create the primary surface and pass it the clipper. Now we can blit the image directly from our back surface to the screen. Again, since the primary surface represents the whole screen, we have to offset our blit by specifying the target rectangle. The auxillary class Direct::WinRect retrieves the coordinates of that rectangle.

void View::Paint (HWND h) {
 if (_dx == 0 || _dy == 0) return;
 // Clip to window
 Direct::Clipper clipper (_draw);
 clipper.SetHWnd (h);
 // Screen surface
 Direct::PrimarySurface surf (_draw);
 surf.SetClipper (clipper);
 Direct::WinRect rect (h);
 // Blit from back surface to screen
 surf.BltFrom (_backSurface, &rect);
}

Finally, the actual drawing is done using direct access to the back surface buffer. But first, we flood the back surface with white background. Then we construct the Direct::SurfaceBuf. This is the object that let's us set pixels directly into the buffer's memory (depending on implementation, it might be your video card memory or a chunk of your system memory). The details of our drawing algorithm are inessential here. Suffice it to say that the work of setting the pixel is done in the SetPixel method.

void View::Update () {
 if (_dx == 0 || _dy == 0) return;
 try {
  // white background
  _backSurface.Fill (RGB (255, 255, 255));
  {
   // Get direct access to back surface
   Direct::SurfaceBuf buf (_backSurface);
   // draw bitmap within a centered square
   int side = 100;
   if (_dx < side) side = _dx;
   if (_dy < side) side = _dy;
   int xOff = (_dx - side) / 2;
   int yOff = (_dy - side) / 2;
   assert (xOff >= 0);
   assert (yOff >= 0);
   double dxInv = 1.0 / side;
   double dyInv = 1.0 / side;
   for (int i = 0; i < side; ++i) {
    for (int j = 0; j < side; ++j) {
     double u = dxInv * i;
     double v = dyInv * j;
     COLORREF color = _world->GetTexel (u, v);
     // draw pixel directly
     buf.SetPixel (xOff + i, yOff + j, color);
    }
   }
  }
  // Paint will blit the image to screen
 }
 catch (char const * msg) {
  ::MessageBox (0, msg, "Viewer error", MB_OK | MB_ICONERROR);
  throw;
 }
 catch (...) {
  ::MessageBox (0, "Unknown error", "Viewer error", MB_OK | MB_ICONERROR);
  throw;
 }
}

Besides being able to set individual pixels quickly, DirectX also provides ways to efficiently blit bitmaps or even use GDI functions to write to a surface.

Оглавление книги

Оглавление статьи/книги

Генерация: 0.720. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз