Delphi drawing, part 3

contents

part1modifying pixels in a bitmap
part2drawing dots and lines: the XBitmap class
part3flicker free painting
part4drawing circles and ellipses


Introduction
This article describes a method for smooth, flicker free, drawing.
Please refer to figure 1. below:
    /fig.1
On a paintbox a line with arrow is drawn.
This is done by moving the mousepointer over the paintbox,
pressing down (and holding) the left mousebutton at point 1,
moving the pointer to point 2, where the mousebutton is released.

General Description
The affected rectangle of the paintbox is marked by a purple line.
Outside this rectangle, no changes took place.
While the mousebutton is down and the mouse is moving, a new line must
painted after each mouse-move. So, at each mouse-move, we have to
    - erase the old line
    - paint the new line
Erasing may be done by repainting the effected rectangle.
But by painting directly in the paintbox, erasing and repainting causes flickering,
which is very unpleasant to the eyes.

A better way is to paint in a Bitmap and copy only the changes to the paintbox.
While painting in this bitmap, during the paint process old images have to be erased
before the new image, representing the new position, can be painted.
So, we add another bitmap, which holds the unchanged image and thus can be used
to restore parts of the bitmap we use for painting.
Restoring is conveniently done by procedure
    bitmap2.canvas.copyrect(pRect,bitmap1.canvas,pRect)
where pRect is the affected area that needs restoring.

See figure 2.
    fig.2
We notice bitmap1 and bitmap2.
Bitmap1 holds the background and original image: the results of previous paint operations.
Trial painting takes place in bitmap2.
During the trial painting in bitmap2, after each mouse-move rectangles are
    - copied from bitmap1 to bitmap2 to restore the paint area
    - copied from bitmap2 to the paintbox to show the (temporary) results
When painting in bitmap2 is finished (on release of the mousebutton), the image may be painted
in bitmap1 to become permanent.

Looking more in detail we note, that the affected rectangle to be copied to the paintbox is the union of
    - the erased area
    - all rectangles associated with paint operations
To control this process, we introduce 2 rectangles (Trect) :
    - drawrect : generated by the paint process, indicating the area of change
    - boxrect : the union of all changes (restores, paints) in bitmap2, to be copied to the paintbox
Also we introduce boolean variable boxflag.
After copying bitmap2 to the paintbox, boxflag is set to false, indicating paintbox1 is updated.
After restoring or painting in bitmap2, boxrect is set to the rectangle of change.
However:
if boxflag is false then the rectangle is simply copied to boxrext and boxflag is set true
if boxflag is true, then the rectangle of change is included with the existing rectangle to
indicate the total area of change.

So, this happens when mouse-move events take place
    - drawrect is copied from bitmap1 to bitmap2, restoring the area of the previous drawing
    - boxrect is set to drawrect
    - new paint operation uses new (x,y) coordinates presented by mouse-move
    - new drawrect is included in boxrect
    - boxrect is copied to the paintbox, which now reflects the new situation
Handling the Mouse-Events
During the paint process following conditions occur:
    - mouse-move events, when no mousebutton is pressed (no action required)
    - mouse-down event (need to store the x,y coordinates)
    - mouse-move events while mousebutton is down (save x,y coordinates, restore, draw in bitmap2 , copy to paintbox)
    - mouse-up event (draw final image in bitmap1)
A counter Pcontrol is used to reflect the above states, Pcontrol =
    0 : mouse-moves : no action
    0 : mouse-down : save (x,y), set Pcontrol = 1
    1 : mouse-move : save (x,y) , draw, show, set Pcontrol = 2
    2 : mouse-move : save (x,y) , restore, draw , show
    2 : mouse-up : final draw in bitmap1, show,set Pcontrol = 0
The Program
Procedure DrawControl receives all events.
For the drawing it selects the appropriate drawing procedure.
Variable pBitmap (TBitmap) is the bitmap to be used by the drawing procedures.
The (x,y) coordinates from the mouse-events are saved in (px1,px2) at first and later in (px2,py2)
Function XY2Rect : Trect , converts px1,py1,px2,py2 to the rectangle format.
Function UniRect(rect1,rect2) : Trect , calculates the union of rect1 , rect2.

mainbtn is a davarrayButton to select various draw operations : line, rectangle, ellipse.
Variable mainbutton reflects the state of mainbtn
For more details, please refer to the comments in the full program listing below:
All procedures are kept simple and straightforward, to show the basic priciples.

Program Listing
    Untitled
    unit Unit1;
    {
     smooth, flicker free, drawing demo
    }
    
    interface
    
    uses
      Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
      davArrayBtn, ExtCtrls;
    
    type
      TForm1 = class(TForm)
        DavArrayBtn1: TDavArrayBtn;
        PaintBox1: TPaintBox;
        procedure DavArrayBtn1BtnPaint(sender: TObject; BtnNr: Byte;
          status: TBtnStatus);
        procedure FormCreate(Sender: TObject);
        procedure PaintBox1Paint(Sender: TObject);
        procedure FormPaint(Sender: TObject);
        procedure DavArrayBtn1BtnChange(sender: TObject; BtnNr: Byte;
          status: TBtnStatus; button: TMouseButton);
        procedure PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
          Shift: TShiftState; X, Y: Integer);
        procedure PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X,
          Y: Integer);
        procedure PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;
          Shift: TShiftState; X, Y: Integer);
      private
        { Private declarations }
      public
        { Public declarations }
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.DFM}
    
    procedure restore(r : Trect);forward;
    procedure drawswitch;forward;
    function UniRect(r1,r2 : Trect) : Trect;forward;
    procedure map2box;forward;
    procedure initmaps;forward;
    function XY2Rect(x1,y1,x2,y2 : integer) : Trect;forward;
    procedure UpdateBoxrect(r : Trect);forward;
    
    type TMain = (mbClear,mbLine,mbRectangle,mbEllipse,mbOff);//main menu states
         TmsEvent = (msDown,msMove,msUp);  //mouse events
    
    var bitmap1,bitmap2,pMap : TBitmap;
        mainbutton : Tmain = mbOff;     //select drawing operation
        Pcontrol : byte;                //drawing control counter
        boxrect,drawrect : Trect;
        boxflag : boolean = false;
        px1,py1,px2,py2 : integer;      //coordinates from mouse events
    
    procedure DrawControl(ms : TmsEvent; x,y : integer);
    //receives mouse events, controls drawing
    begin
     case ms of 
       msDown : if Pcontrol = 0 then
                 begin
                  Pcontrol := 1;
                  Pmap := bitmap2;     //paint in bitmap2
                  px1 := x; py1 := y;  //save coordinates
                 end;
       msMove : begin
                 if Pcontrol = 2 then restore(drawrect);  //restore previous drawing
                 if Pcontrol = 1 then Pcontrol := 2;
                 if Pcontrol <> 0 then
                  begin
                   px2 := x; py2 := y;
                   drawswitch;        //select drawing proc, make drawrect
                   UpdateBoxRect(drawrect);
                   map2box;           //show drawing changes in boxrect
                  end;
                end;
       msUp   : begin
                 if Pcontrol = 2 then
                  begin
                   Pmap := bitmap1;
                   drawswitch;
                   restore(drawrect);
                   map2box;
                  end;
                 Pcontrol := 0;
                end;
     end;//case
    end;
    
    procedure lineproc;
    //paint a red line
    begin
     with Pmap do with canvas do
      begin
       pen.color := $0000ff;
       moveto(px1,py1);
       lineto(px2,py2);
      end;
    end;
    
    procedure rectproc;
    //paint a green rectangle
    begin
     with Pmap do with canvas do
      begin
       pen.color := $00c000;
       brush.style := bsClear;
       rectangle(px1,py1,px2,py2);
      end;
    end;
    
    procedure ellipseproc;
    //paint a blue ellipse
    begin
     with Pmap do with canvas do
      begin
       pen.color := $ff0000;
       brush.style := bsclear;
       ellipse(px1,py1,px2,py2);
      end;
    end;
    
    procedure TForm1.DavArrayBtn1BtnChange(sender: TObject; BtnNr: Byte;
      status: TBtnStatus; button: TMouseButton);
    //main menu button change
    begin
     with davarrayBtn1 do
      begin
       if status <> stDown then mainbutton := mbOff
        else
         begin
          mainbutton := Tmain(BtnNr);
          case mainbutton of
           mbClear : begin
                      initmaps;
                      Pcontrol := 0;
                      boxflag := false;
                      BtnRelease(byte(mainbutton));
                     end;
           mbLine  : ;
           mbRectangle : ;
           mbEllipse : ;
          end;//case
         end;
      end;//with
    end;
    
    procedure drawswitch;
    //select proper drawing procedure, make drawrect
    begin
     drawrect := XY2Rect(px1,py1,px2,py2);
     with drawrect do
      begin
       left := left -1;
       right := right + 1;
       top := top - 1;
       bottom := bottom + 1;
      end;
     if boxflag then boxrect := UniRect(boxrect,drawrect)
      else begin
            boxrect := drawrect;
            boxflag := true;
           end;  
     case mainbutton of
      mbLine      : lineproc;
      mbRectangle : rectproc;
      mbEllipse   : EllipseProc;
     end;
    end;
    
    procedure UpdateBoxrect(r : Trect);
    begin
     if boxflag then boxrect := UniRect(boxrect,r)  //union of rectangles
      else begin
            boxrect := r;
            boxflag := true;
           end;
    end;
    
    procedure restore(r : Trect);
    //restore r area of bitmap2
    begin
     bitmap2.canvas.CopyRect(r,bitmap1.canvas,r);
     UpdateBoxRect(r);
    end;
    
    procedure map2box;
    begin
     form1.paintbox1.canvas.copyrect(boxrect,bitmap2.canvas,boxrect);
     boxflag := false; //paintbox update done
    end;
    
    function UniRect(r1,r2 : Trect) : Trect;
    //union of rectangles
    begin
     with result do
      begin
       if r1.left   < r2.left   then left   := r1.left   else left   := r2.Left;
       if r1.top    < r2.top    then top    := r1.top    else top    := r2.top;
       if r1.right  > r2.right  then right  := r1.right  else right  := r2.right;
       if r1.bottom > r2.bottom then bottom := r1.bottom else bottom := r2.bottom;
      end;
    end;
    
    function XY2Rect(x1,y1,x2,y2 : integer) : Trect;
    //make rectangle out of coordinates
    begin
     with result do
      begin
       if x1 < x2 then begin
                        left := x1; right := x2;
                       end
       else begin
             left := x2; right := x1;
            end;
       if y1 < y2 then begin
                        top := y1; bottom := y2;
                       end
       else begin
             top := y2; bottom := y1;
            end;
      end;//with
    end;
    
    procedure TForm1.PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    begin
     drawControl(msDown,x,y);
    end;
    
    procedure TForm1.PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    begin
     drawControl(msMove,x,y);
    end;
    
    procedure TForm1.PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    begin
     drawControl(msUp,x,y);
    end;
    
    procedure TForm1.DavArrayBtn1BtnPaint(sender: TObject; BtnNr: Byte;
      status: TBtnStatus);
    const dtext : array[0..3] of string =
                  ('clear','line','rectangle','ellipse');
    var r : Trect;
        x,y : integer;
    begin
     with davarrayBtn1 do with canvas do
      begin
       if status = stHI then font.style := [fsBold] else font.style := [];
       r := getBtnRect(BtnNr);
       x := r.left + (btnWidth - textwidth(dtext[btnNr])) div 2;
       y := r.top + (btnheight - textheight(dtext[btnNr])) div 2;
       textout(x,y,dtext[BtnNr]);
      end;
    end;
    
    procedure initmaps;
    //paint grid in bitmap1, copy to bitmap2
    var i : integer;
    begin
     with bitmap1 do with canvas do
      begin
       brush.color := $e0ffff;
       brush.style := bsSolid;
       pen.color := $fff0f0;
       fillrect(rect(0,0,width,height));
       pen.Width := 1;
       i := 0;
       while i < width do
        begin
         moveto(i,0); lineto(i,height);
         inc(i,20);
        end;
       i := 0;
       while i < height do
        begin
         moveto(0,i); lineto(width,i);
         inc(i,20);
        end;
      end;
     bitmap2.canvas.draw(0,0,bitmap1);
     form1.paintbox1.invalidate;
    end;
      
    procedure TForm1.FormCreate(Sender: TObject);
    begin
     with davArrayBtn1.canvas do
      begin
       font.height := 20;
      end;
     bitmap1 := TBitmap.create;
     with bitmap1 do
      begin
       width := paintbox1.width;
       height := paintbox1.height;
       pixelformat := pf32bit;
      end;
     bitmap2 := TBitmap.create;
     with bitmap2 do
      begin
       width := paintbox1.width;
       height := paintbox1.height;
       pixelformat := pf32bit;
      end;
     pMap := nil;
     initmaps;
    end;
    
    procedure TForm1.PaintBox1Paint(Sender: TObject);
    begin
     paintbox1.canvas.draw(0,0,bitmap2);
    end;
    
    procedure TForm1.FormPaint(Sender: TObject);
    //paint edge around paintbox
    var x1,y1,x2,y2 : integer;
    begin
     with paintbox1 do
      begin
       x1 := left-1;
       y1 := top-1;
       x2 := left+width;
       y2 := top + height;
      end;
     with canvas do
      begin
       pen.color := $303030;
       pen.width := 1;
       moveto(x1,y1); lineto(x1,y2);
       lineto(x2,y2); lineto(x2,y1); lineto(x1,y1);
      end; 
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
     bitmap1.free;
     bitmap2.free;
    end;
    
    end.