OWL Tutorial - Step 3

Painting the Window

Date: 29 March 1997
Author: Christopher Kohlhoff

OK, so now we have an application that creates a window and responds to some menu selections. What next? Well, many applications (though not all) involve some sort of custom graphic drawing. So that is what we are going to to do here.

The Shape Classes

Before we get into any drawing, we'll create some shape classes for storing our data. To begin with, let's create a class called TShape. This will be an abstract base class, and will thus provide an "interface". The primary functions it contains will be:

Let's take a look at the class declaration:

class TShape
{
  public:
    virtual void Draw(TDC& dc) = 0;
    virtual void Update(TPoint& p) = 0;
    bool operator==(const TShape& other) { return false; }
};

And so that we can store shape objects, we will use one of Borland's container templates:

typedef TIArrayAsVector<TShape> TShapeArray;

TIArrayAsVector is a template class that holds pointers to objects (in our case TShape and its descendants). It will automatically resize to make room for added objects, and will therefore save us from a lot of memory management hassles. (There are versions of these templates that store copies of objects rather than pointers to them. However TShape is an abstract base class - it cannot be instantiated - and we want to store derived classes, so we must use pointers.)

From TShape we will derive three classes: TLine, TRectangle and TEllipse. We will show how TLine is implemented here (you can look at the implementation for the others by downloading the tutorial source).

class TLine : public TShape
{
  public:
    TLine() {}
    TLine(const TPoint& p1, const TPoint& p2) : P1(p1), P2(p2) {}
    virtual void Draw(TDC& dc);
    virtual void Update(TPoint& p) { P2 = p; }
  protected:
    TPoint P1, P2;
};


void TLine::Draw(TDC& dc)
{
  // Position the graphics "pointer" at the
  // beginning of the line.
  //
  dc.MoveTo(P1);

  // Draw a line from the current position to
  // the other end.
  //
  dc.LineTo(P2);
}

This introduces us to the heart and soul of Windows graphics programming: the device context. In OWL this is encapsulated in the class TDC. TDC includes functions for drawing lines (as above) and also for drawing rectangles, ellipses, text and more.

The Paint Method

If you are already familiar with Windows programming, you may know that a window gets sent a WM_PAINT message when it needs to be painted. You may also know that you have to call BeginPaint before you start painting and EndPaint when you are done. All these things to remember! It's good to know that all you have to do to paint in a window in OWL is to override the virtual function TWindow::Paint. (You may recall from Step 1 that our client window was derived from TWindow - in fact all window classes are derived from this, including TFrameWindow.) So all we have to do is:

class TTutorialClientWindow : public TWindow
{
  // ...
  protected:
    virtual void Paint(TDC& dc, bool erase, TRect& rect);
  // ...
};

void TTutorialClientWindow::Paint(TDC& dc, bool /*erase*/, TRect& /*rect*/)
{
  for (int i = 0, iMax = Shapes.GetItemsInContainer(); i < iMax; i++)
    Shapes[i]->Draw(dc);
}

The first parameter, dc, is the device context we mentioned above. The second parameter erase says whether the paint area needs to be erased first, and the rect is the area that requires painting. You will notice that in this example we are ignoring these last two, however if you were trying to write an application with as smooth painting as possible, they should be taken into account.

Painting On The Fly

There is no reason why all your painting must take place in the above Paint function. In fact, the only requirement is that you are able to reproduce the entire contents of a window there. Otherwise there are no restrictions (or very few anyway) on where you do your painting. To illustrate this, we will make it so that when you add a shape, you drag until it is the size you want.

The order of the steps used in the drag operation will be:

In the Paint method the device context was provided as an argument. To paint at other times we need to create one explicitly. For this, we use the class TClientDC which is derived from TDC:

DragDC = new TClientDC(*this);

The TClientDC constructor is defined to take an HWND (handle to a window). OWL elegantly provides a casting operator from TWindow to HWND.

The full code for the drag operation is shown below.

void TTutorialClientWindow::EvLButtonDown(uint /*modKeys*/, TPoint& point)
{
  // Begin the drag operation.
  //
  // We select a null brush into the DC because we
  // do not want our drag objects to be filled.
  //
  // SetCapture causes all mouse input to be directed
  // to this window. This ensures that the drag
  // operation is not interrupted.
  //
  DragDC = new TClientDC(*this);
  DragDC->SelectObject((HBRUSH) GetStockObject(NULL_BRUSH));
  SetCapture();

  // Add a new shape to the array of shapes.
  //
  switch (ShapeType)
  {
    case Line:
      DragShape = new TLine(point, point);
      break;
    case Rectangle:
      DragShape = new TRectangle(TRect(point, point));
      break;
    case Ellipse:
      DragShape = new TEllipse(TRect(point, point));
      break;
  }
  Shapes.Add(DragShape);

  // Draw an (inverted) image of the shape.
  //
  int mode = DragDC->SetROP2(R2_NOT);
  DragShape->Draw(*DragDC);
  DragDC->SetROP2(mode);
}

void TTutorialClientWindow::EvMouseMove(uint /*modKeys*/, TPoint& point)
{
  if (DragDC) // We are in the middle of a drag operation.
  {
    // Draw an (inverted) image of the shape. (This
    // is to remove any previously drawn image).
    //
    int mode = DragDC->SetROP2(R2_NOT);
    DragShape->Draw(*DragDC);
    DragDC->SetROP2(mode);

    // Update the shape to its new size
    //
    DragShape->Update(point);

    // Draw an new (inverted) image of the shape.
    //
    mode = DragDC->SetROP2(R2_NOT);
    DragShape->Draw(*DragDC);
    DragDC->SetROP2(mode);
  }
}

void TTutorialClientWindow::EvLButtonUp(uint /*modKeys*/, TPoint& point)
{
  if (DragDC)
  {
    // Draw an (inverted) image of the shape. (This
    // is to remove any previously drawn image).
    //
    int mode = DragDC->SetROP2(R2_NOT);
    DragShape->Draw(*DragDC);
    DragDC->SetROP2(mode);

    // End the drag operation.
    //
    delete DragDC;
    DragDC = 0;
    ReleaseCapture();

    // Update the shape to its final size.
    //
    DragShape->Update(point);

    // Force the window to be repainted
    //
    Invalidate(true);
  }
}

Selecting Into the DC

The OWL classes TBrush, TPen and TFont encapsulate the functionality of the Windows objects HBRUSH, HPEN and HFONT respectively. These are used, for example, in conjunction with the TColor class to change the colours which are currently being used for painting. By calling the method SelectObject of TDC you can select and object of one of these types into the device context. This causes the previously selected object of the same type to be replaced. We use this in the code above to specify that the background colour of the shape being resized should be transparent.

You may like to experiment with the effects of selecting these objects. For example, selecting TPen(TColor::LtRed, 5) into the device context causes the shapes to be drawn with a thick red border (the colour will only work in the Paint method since we are drawing our drag shapes as an inversion).

 

Previous Step | Next Step | Tutorial Contents