# Difference between revisions of "Graphics - Working with TCanvas"

(→Drawing a polygon: Self-overlapping polygons) |
m (improved minor glitches in spelling etc.) |
||

Line 2: | Line 2: | ||

== Drawing a rectangle == | == Drawing a rectangle == | ||

− | Many controls expose their canvas as a public property or | + | Many controls expose their canvas as a public Canvas property (or via an OnPaint event. Such controls include [[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 | + | Suppose we want to draw a red rectangle with a 5-pixel-thick blue border in the center of the form, and the the rectangle should be half the size of the form. For this purpose we must add code to the OnPaint event of the form. Never 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 | + | The TCanvas method for painting a rectangle is named very logically: <tt>Rectangle()</tt>. You can pass rectangle's edge coordinates to the method either as four separate x/y values, or as a single <tt>TRect</tt> 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: |

<syntaxhighlight lang=pascal> | <syntaxhighlight lang=pascal> | ||

Line 117: | Line 117: | ||

=== Simple polygon === | === Simple polygon === | ||

− | A polygon is drawn by the <tt>Polygon</tt> method of the canvas. The polygon is defined by an array of points (<tt>TPoint</tt>) which are connected by straight lines drawn with the current <tt>Pen</tt>, and the inner area is filled by the | + | A polygon is drawn by the <tt>Polygon</tt> method of the canvas. The polygon is defined by an array of points (<tt>TPoint</tt>) which are connected by straight lines drawn with the current <tt>Pen</tt>, and the inner area is filled by the current <tt>Brush</tt>. The polygon is closed automatically, i.e. the last array point does not necessarily need to coincide with the first point (although there are cases where this is required -- see below). |

[[Image:Simple_Polygon.png|right|Simple Polygon: Pentagon]] | [[Image:Simple_Polygon.png|right|Simple Polygon: Pentagon]] | ||

Line 141: | Line 141: | ||

=== Self-overlapping polygons === | === Self-overlapping polygons === | ||

− | Here is a modification of the polygon example: Let's rearrange the polygon points so that the first point is | + | Here is a modification of the polygon example: Let's rearrange the polygon points so that the first point is connected to the third initial point, the point is connected to the 5th point, the 5th point to the 2nd point and the 2nd point to to 4th point. This is a self-overlapping polygon and results in a star-shape. However, owing to the overlapping, different effects can be obtained which depend on the optional <tt>Winding</tt> parameter of the <tt>Polygon()(</tt> method. When <tt>Winding</tt> is <tt>false</tt> an area is filled by the "even-odd rule" (https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), otherwise by the "non-zero winding rule" (https://en.wikipedia.org/wiki/Nonzero-rule). The following code example compares both cases: |

[[Image:Self-overlapping_Polygon.png|right|Self-overlapping Polygon]] | [[Image:Self-overlapping_Polygon.png|right|Self-overlapping Polygon]] | ||

Line 170: | Line 170: | ||

=== Polygon with a hole === | === Polygon with a hole === | ||

− | Suppose you | + | Suppose you want to 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, you need to consider several important points: |

− | * | + | * You must prepare the array of polygon vertices such that each polygon is closed (i.e. last point = first point), and that both first and last polygon points are immediately adjacent in the array. |

* The order of the inner and outer polygon points in the array does not matter. | * 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. | * 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. | ||

Line 200: | Line 200: | ||

end; </syntaxhighlight> | end; </syntaxhighlight> | ||

− | You may notice that there is a | + | You may notice that there is a line connecting 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 <tt>Polygon()</tt> method closes the entire polygon, i.e. it connects the very first with the very last array point. You can avoid this by drawing the polygon and the border separately. To draw the fill the <tt>Pen.Style</tt> should be set to <tt>psClear</tt> to hide the outline. The <tt>PolyLine()</tt> method can be used to draw the border; this method needs arguments for the starting point index and also a count of the array points to be drawn. |

[[Image:Polygon_with_hole_2.png|right|Polygon with hole]] | [[Image:Polygon_with_hole_2.png|right|Polygon with hole]] | ||

<syntaxhighlight lang=Pascal> | <syntaxhighlight lang=Pascal> | ||

Line 246: | Line 246: | ||

(X: 180; Y: 80) | (X: 180; Y: 80) | ||

); </syntaxhighlight> | ); </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 | + | 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 correctly. The trick is to add two furthre points (one per shape). These are added to the above single-hole-in-polygon case: the first additional point duplicates the first point of the 2nd inner triangle, and the second additional point duplicates the first point of the 1st inner triangle. By so doing, the polygon is closed along the imaginary path the holes were connected by initially, and no additional areas are introduced: |

[[Image:Polygon_with_holes_2.png|right|Polygon with holes: no additional points]] | [[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]] | [[Image:Polygon_with_holes_3.png|right|Polygon with holes: with additional points]] |

## Revision as of 20:58, 25 December 2020

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

## Contents

## Drawing a rectangle

Many controls expose their canvas as a public Canvas property (or via an OnPaint event. Such controls include 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, and the the rectangle should be half the size of the form. For this purpose we must add code to the OnPaint event of the form. Never 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 named very logically: `Rectangle()`. You can pass rectangle's edge coordinates to the method either as four separate x/y values, or as a single `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

### Simple polygon

A polygon is drawn by the `Polygon` method of the canvas. The polygon is defined by an array of points (`TPoint`) which are connected by straight lines drawn with the current `Pen`, and the inner area is filled by the current `Brush`. The polygon is closed automatically, i.e. the last array point does not necessarily need to coincide with the first point (although there are cases where this is required -- see below).

Example: Pentagon

```
procedure TForm1.FormPaint(Sender: TObject);
var
P: Array[0..4] of TPoint;
i: Integer;
phi: Double;
begin
for i := 0 to 4 do
begin
phi := 2.0 * pi / 5 * i + pi * 0.5;;
P[i].X := round(100 * cos(phi) + 110);
P[i].Y := round(100 * sin(phi) + 110);
end;
Canvas.Brush.Color := clRed;
Canvas.Polygon(P);
end;
```

### Self-overlapping polygons

Here is a modification of the polygon example: Let's rearrange the polygon points so that the first point is connected to the third initial point, the point is connected to the 5th point, the 5th point to the 2nd point and the 2nd point to to 4th point. This is a self-overlapping polygon and results in a star-shape. However, owing to the overlapping, different effects can be obtained which depend on the optional `Winding` parameter of the `Polygon()(` method. When `Winding` is `false` an area is filled by the "even-odd rule" (https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule), otherwise by the "non-zero winding rule" (https://en.wikipedia.org/wiki/Nonzero-rule). The following code example compares both cases:

```
procedure TForm1.FormPaint(Sender: TObject);
var
P: Array[0..4] of TPoint;
P1, P2: Array[0..4] of TPoint;
i: Integer;
phi: Double;
begin
for i := 0 to 4 do
begin
phi := 2.0 * pi / 5 * i + pi * 0.5;;
P[i].X := round(100 * cos(phi) + 110);
P[i].Y := round(100 * sin(phi) + 110);
end;
P1[0] := P[0];
P1[1] := P[2];
P1[2] := P[4];
P1[3] := P[1];
P1[4] := P[3];
for i:= 0 to 4 do P2[i] := Point(P1[i].X + 200, P1[i].Y); // offset polygon
Canvas.Brush.Color := clRed;
Canvas.Polygon(P1, false);
Canvas.Polygon(P2, true);
end;
```

### Polygon with a hole

Suppose you want to 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, you need to consider several important points:

- You must prepare the array of polygon vertices such that each polygon is closed (i.e. last point = first point), and that both first and last 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.

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 line connecting 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. it connects the very first with the very last array point. You can avoid this by drawing the polygon and the border separately. To draw 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 method needs arguments for the starting point index and also a count of the array points to be drawn.

```
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.

```
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 correctly. The trick is to add two furthre points (one per shape). These are added to the above single-hole-in-polygon case: the first additional point duplicates the first point of the 2nd inner triangle, and the second additional point duplicates the first point of the 1st inner triangle. By so doing, the polygon is closed along the imaginary path the holes were connected by initially, and no additional areas are introduced:

```
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.