Difference between revisions of "Graphics - Working with TCanvas"

From Free Pascal wiki
Jump to navigationJump to search
(→‎Drawing a polygon with a hole: Several holes in a polygon)
Line 114: Line 114:
 
end;</syntaxhighlight>
 
end;</syntaxhighlight>
  
== Drawing a polygon with a hole ==
+
== Drawing a polygon ==
 +
 
 +
=== Polygon with a hole ===
 
Suppose you must draw the shape of a country with a large lake inside from both of which you have some boundary points. Basically the <tt>Polygon()</tt> method of the LCL canvas is prepared for this task. However, some important facts must be considered:
 
Suppose you must draw the shape of a country with a large lake inside from both of which you have some boundary points. Basically the <tt>Polygon()</tt> method of the LCL canvas is prepared for this task. However, some important facts must be considered:
 
* Prepare the array of polygon vertices such that each polygon is closed (i.e. last point = first point), and that both polygon points are immediately adjacent in the array.  
 
* Prepare the array of polygon vertices such that each polygon is closed (i.e. last point = first point), and that both polygon points are immediately adjacent in the array.  
Line 159: Line 161:
 
   Canvas.Polyline(Pts, 5, 4);  // triangle starts at index 5 and consists of 4 array elements
 
   Canvas.Polyline(Pts, 5, 4);  // triangle starts at index 5 and consists of 4 array elements
 
end; </syntaxhighlight>
 
end; </syntaxhighlight>
 +
 +
=== Polygon with several holes ===
 +
 +
Applying the rules for the single hole in a polygon, we extend the example from the previous section by adding two more triangles inside the outer rectangle. These triangles have the same orientation as the first triangle, opposite to the outer rectangle, and thus should be considered to be holes.
 +
[[Image:Polygon_with_holes_1.png|right|Polygon with holes]]
 +
<syntaxhighlight lang=Pascal>const
 +
  Pts: array[0..16] of TPoint = (
 +
    // outer polygon: a rectangle
 +
    (X: 10; Y: 10),    // clockwise
 +
    (X:190; Y: 10),
 +
    (X:190; Y:190),
 +
    (X: 10; Y:190),
 +
    (X: 10; Y: 10),
 +
 +
    // inner polygon: a triangle
 +
    (X:  20; Y: 20),  // counter-clockwise
 +
    (X:  80; Y:180),
 +
    (X: 140; Y: 20),
 +
    (X:  20; Y: 20),
 +
 +
    // 2nd inner triangle
 +
    (X: 150; Y: 50),  // counter-clockwise
 +
    (X: 150; Y:100),
 +
    (X: 180; Y: 50),
 +
    (X: 150; Y: 50),
 +
 +
    // 3rd inner triangle
 +
    (X: 180; Y: 80),  // counter-clockwise
 +
    (X: 160; Y:120),
 +
    (X: 180; Y:120),
 +
    (X: 180; Y: 80)
 +
  ); </syntaxhighlight>
 +
Rendering this by a simple <tt>Polygon()</tt> fill is disappointing because there are new additional areas with are not expected. The reason is that this model does not return to the starting point in the correct way. The trick is to add two more points (one per shape added to the above single-hole-in-polygon case: The first addtional point duplicates the first point of the 2nd inner triangle, and the second additional point duplicates the first point of the 1st inner triangle. This way, the polygon is closed along the imaginary path the holes were connected initially and not additionaly areas are introduced:
 +
[[Image:Polygon_with_holes_2.png|right|Polygon with holes: no additional points]]
 +
[[Image:Polygon_with_holes_3.png|right|Polygon with holes: with additional points]]
 +
<syntaxhighlight lang="Pascal">const
 +
  Pts: array[0..18] of TPoint = (
 +
    // outer polygon: a rectangle
 +
    (X: 10; Y: 10),    // clockwise
 +
    (X:190; Y: 10),
 +
    (X:190; Y:190),
 +
    (X: 10; Y:190),
 +
    (X: 10; Y: 10),
 +
 +
    // 1st inner triangle
 +
    (X:  20; Y: 20),  // counter-clockwise --> hole
 +
    (X:  80; Y:180),
 +
    (X: 140; Y: 20),
 +
    (X:  20; Y: 20),
 +
 +
    // 2nd inner triangle
 +
    (X: 150; Y: 50),  // counter-clockwise --> hole
 +
    (X: 150; Y:100),
 +
    (X: 180; Y: 50),
 +
    (X: 150; Y: 50),
 +
 +
    // 3rd inner triangle
 +
    (X: 180; Y: 80),  // counter-clockwise --> hole
 +
    (X: 160; Y:120),
 +
    (X: 180; Y:120),
 +
    (X: 180; Y: 80),
 +
 +
    (X: 150; Y: 50),  // duplicates 1st point of 2nd inner triangle
 +
    (X:  20; Y: 20)  // duplicates 1st point of 1st inner triangle
 +
  );
 +
</syntaxhighlight>
 +
The last image at the right is drawn again with separate <tt>Polygon()</tt> and <tt>PolyLine()</tt> calls.

Revision as of 19:03, 24 December 2020

English (en) français (fr) italiano (it) русский (ru)

Drawing a rectangle

Many controls expose their canvas as a public property or in an OnPaint event, e.g. TForm, TPanel and TPaintBox. Let's use TForm as an example to demonstrate how to paint on a canvas.

Suppose we want to draw a red rectangle with a 5-pixel-thick blue border in the center of the form; the size of the rectangle should be half of the size of the form. For this purpose we must add code to the OnPaint event of the form. Don't paint in an OnClick handler because this painting is not persistent and will be erased whenever the operating system requests a repaint, always paint in the OnPaint event!

The TCanvas method for painting a rectangle is called exactly like that: Rectangle(). It gets the coordinates of the rectangle edges either separately or as a TRect record. The fill color is determined by the color of the canvas's Brush, and the border color is given by the color of the canvas's Pen:

procedure TForm1.FormPaint(Sender: TObject);
var
  w, h: Integer;    // Width and height of the rectangle
  cx, cy: Integer;  // center of the form
  R: TRect;         // record containing the coordinates of the rectangle's left, top, right, bottom corners
begin
  // Calculate form center
  cx := Width div 2;
  cy := Height div 2;

  // Calculate the size of the rectangle
  w := Width div 2;
  h := Height div 2;

  // Calculate the corner points of the rectangle
  R.Left := cx - w div 2;
  R.Top := cy - h div 2;
  R.Right := cx + w div 2;
  R.Bottom := cy + h div 2;

  // Set the fill color
  Canvas.Brush.Color := clRed;
  Canvas.Brush.Style := bsSolid;

  // Set the border color
  Canvas.Pen.Color := clBlue;
  Canvas.Pen.Width := 5;
  Canvas.Pen.Style := psSolid;

  // Draw the rectangle
  Canvas.Rectangle(R);
end;

Using the default GUI font

This can be done with the following simple code:

SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));

Drawing a text limited on the width

Use the DrawText routine, first with DT_CALCRECT and then without it.

// First calculate the text size then draw it
TextBox := Rect(0, currentPos.Y, Width, High(Integer));
DrawText(ACanvas.Handle, PChar(Text), Length(Text),
  TextBox, DT_WORDBREAK or DT_INTERNAL or DT_CALCRECT);

DrawText(ACanvas.Handle, PChar(Text), Length(Text),
  TextBox, DT_WORDBREAK or DT_INTERNAL);

Drawing text with sharp edges (non antialiased)

Some widgetsets support this via

Canvas.Font.Quality := fqNonAntialiased;

Some widgetsets like the gtk2 do not support this and always paint antialiased. Here is a simple procedure to draw text with sharp edges under gtk2. It does not consider all cases, but it should give an idea:

procedure PaintAliased(Canvas: TCanvas; x, y: integer; const TheText: string);
var
  w, h, dx, dy: Integer;
  IntfImg:      TLazIntfImage;
  Img:          TBitmap;
  col:          TFPColor;
  FontColor, c: TColor;
begin
  w := 0;
  h := 0;
  Canvas.GetTextSize(TheText, w, h);
  if (w <= 0) or (h <= 0) then exit;
  Img := TBitmap.Create;
  IntfImg := nil;
  try
    // paint text to a bitmap
    Img.Masked := true;
    Img.SetSize(w,h);
    Img.Canvas.Brush.Style := bsSolid;
    Img.Canvas.Brush.Color := clWhite;
    Img.Canvas.FillRect(0, 0, w, h);
    Img.Canvas.Font := Canvas.Font;
    Img.Canvas.TextOut(0, 0, TheText);
    // get memory image
    IntfImg := Img.CreateIntfImage;
    // replace gray pixels
    FontColor := ColorToRGB(Canvas.Font.Color);
    for dy := 0 to h - 1 do begin
      for dx := 0 to w - 1 do begin
        col := IntfImg.Colors[dx, dy];
        c := FPColorToTColor(col);
        if c <> FontColor then
          IntfImg.Colors[dx, dy] := colTransparent;
      end;
    end;
    // create bitmap
    Img.LoadFromIntfImage(IntfImg);
    // paint
    Canvas.Draw(x, y, Img);
  finally
    IntfImg.Free;
    Img.Free;
  end;
end;

Drawing a polygon

Polygon with a hole

Suppose you must draw the shape of a country with a large lake inside from both of which you have some boundary points. Basically the Polygon() method of the LCL canvas is prepared for this task. However, some important facts must be considered:

  • Prepare the array of polygon vertices such that each polygon is closed (i.e. last point = first point), and that both polygon points are immediately adjacent in the array.
  • The order of the inner and outer polygon points in the array does not matter.
  • Make sure that both polygons have opposite orientations, i.e. if the outer polygon has its vertices in clockwise order, then the inner polygon must have the points in counter-clockwise order.
Polygon with hole

Example:

const
  P: array of [0..8] of TPoint = (
    // outer polygon: a rectangle 
    (X: 10; Y: 10),   // <--- first point of the rectangle
    (X:190; Y: 10),
    (X:190; Y:190),   //      (clockwise orientation)
    (X: 10; Y:190),
    (X: 10; Y: 10),   // <--- last point of the rectangle = first point

    // inner polygon: a triangle
    (X: 20; Y: 20),   // <--- first point of the triangle
    (X: 40; Y:180),   //      ( counter-clockwise orientation) 
    (X: 60; Y: 20),
    (X: 20; Y: 20)    // <--- last point of the triangle = first point
  );

procedure TForm1.FormPaint(Sender: TObject);
begin
  Canvas.Brush.Color := clRed;
  Canvas.Polygon(Pts);
end;

You may notice that there is a connection line from the starting point of the inner triangle back to the starting point of the outer rectangle (marked by a blue circle in the screenshot). This is because the Polygon() method closes the entire polygon, i.e. connects the very first with the very last array point. It can be avoided by drawing the polygon and the border separately. For drawing the fill the Pen.Style should be set to psClear to hide the outline. The PolyLine() method can be used to draw the border; this methods accepts arguments for the starting point index and count of the points in the array to be drawn.

Polygon with hole
procedure TForm1.FormPaint(Sender: TObject);
begin
  Canvas.Brush.Color := clRed;
  Canvas.Pen.Style := psClear;
  Canvas.Polygon(Pts);

  Canvas.Pen.Style := psSolid;
  Canvas.Pen.Color := clBlack;
  Canvas.Polyline(Pts, 0, 5);  // rectangle starts at index 0 and consists of 5 array elements
  Canvas.Polyline(Pts, 5, 4);  // triangle starts at index 5 and consists of 4 array elements
end;

Polygon with several holes

Applying the rules for the single hole in a polygon, we extend the example from the previous section by adding two more triangles inside the outer rectangle. These triangles have the same orientation as the first triangle, opposite to the outer rectangle, and thus should be considered to be holes.

Polygon with holes
const
  Pts: array[0..16] of TPoint = (
    // outer polygon: a rectangle
    (X: 10; Y: 10),    // clockwise
    (X:190; Y: 10),
    (X:190; Y:190),
    (X: 10; Y:190),
    (X: 10; Y: 10),

    // inner polygon: a triangle
    (X:  20; Y: 20),   // counter-clockwise
    (X:  80; Y:180),
    (X: 140; Y: 20),
    (X:  20; Y: 20),

    // 2nd inner triangle
    (X: 150; Y: 50),   // counter-clockwise
    (X: 150; Y:100),
    (X: 180; Y: 50),
    (X: 150; Y: 50),

    // 3rd inner triangle
    (X: 180; Y: 80),  // counter-clockwise
    (X: 160; Y:120),
    (X: 180; Y:120),
    (X: 180; Y: 80)
  );

Rendering this by a simple Polygon() fill is disappointing because there are new additional areas with are not expected. The reason is that this model does not return to the starting point in the correct way. The trick is to add two more points (one per shape added to the above single-hole-in-polygon case: The first addtional point duplicates the first point of the 2nd inner triangle, and the second additional point duplicates the first point of the 1st inner triangle. This way, the polygon is closed along the imaginary path the holes were connected initially and not additionaly areas are introduced:

Polygon with holes: no additional points
Polygon with holes: with additional points
const
  Pts: array[0..18] of TPoint = (
    // outer polygon: a rectangle
    (X: 10; Y: 10),    // clockwise
    (X:190; Y: 10),
    (X:190; Y:190),
    (X: 10; Y:190),
    (X: 10; Y: 10),

    // 1st inner triangle
    (X:  20; Y: 20),   // counter-clockwise --> hole
    (X:  80; Y:180),
    (X: 140; Y: 20),
    (X:  20; Y: 20),

    // 2nd inner triangle
    (X: 150; Y: 50),   // counter-clockwise --> hole
    (X: 150; Y:100),
    (X: 180; Y: 50),
    (X: 150; Y: 50),

    // 3rd inner triangle
    (X: 180; Y: 80),  // counter-clockwise --> hole
    (X: 160; Y:120),
    (X: 180; Y:120),
    (X: 180; Y: 80),

    (X: 150; Y: 50),  // duplicates 1st point of 2nd inner triangle
    (X:  20; Y: 20)   // duplicates 1st point of 1st inner triangle
  );

The last image at the right is drawn again with separate Polygon() and PolyLine() calls.