Sign in Join
Wiki copytemporary

Graphics - Working with TCanvas: what changed

Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.

Title: Graphics - Working with TCanvas

OldNewLine
1+## Drawing shapes
2+
3+### Drawing a rectangle
4+
5+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.
6+
7+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!
8+
9+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:
10+
11+``` pascal
12+procedure TForm1.FormPaint(Sender: TObject);
13+var
14+ w, h: Integer; // Width and height of the rectangle
15+ cx, cy: Integer; // center of the form
16+ R: TRect; // record containing the coordinates of the rectangle's left, top, right, bottom corners
17+begin
18+ // Calculate form center
19+ cx := Width div 2;
20+ cy := Height div 2;
21+
22+ // Calculate the size of the rectangle
23+ w := Width div 2;
24+ h := Height div 2;
25+
26+ // Calculate the corner points of the rectangle
27+ R.Left := cx - w div 2;
28+ R.Top := cy - h div 2;
29+ R.Right := cx + w div 2;
30+ R.Bottom := cy + h div 2;
31+
32+ // Set the fill color
33+ Canvas.Brush.Color := clRed;
34+ Canvas.Brush.Style := bsSolid;
35+
36+ // Set the border color
37+ Canvas.Pen.Color := clBlue;
38+ Canvas.Pen.Width := 5;
39+ Canvas.Pen.Style := psSolid;
40+
41+ // Draw the rectangle
42+ Canvas.Rectangle(R);
43+end;
44+```
45+
46+### Drawing a circle
47+
48+The canvas does not have a direct method to draw a circle. But there is a method to draw an ellipse. Knowing that a circle is a special case of an ellipse with equal half-axes we can draw a circle as follows:
49+
50+``` pascal
51+procedure TForm1.FormPaint(Sender: TObject);
52+var
53+ radius: Integer; // Radius of the circle
54+ center: TPoint; // Center point of the circle
55+ R: TRect; // Rectangle enclosing the circle
56+begin
57+ // Set the fill color
58+ Canvas.Brush.Color := clYellow;
59+ Canvas.Brush.Style := bsSolid;
60+
61+ // Set the border color
62+ Canvas.Pen.Color := clBlue;
63+ Canvas.Pen.Width := 3;
64+ Canvas.Pen.Style := psSolid;
65+
66+ // We want the circle to be centered in the form
67+ center.X := Width div 2;
68+ center.Y := Height div 2;
69+
70+ // The diameter should be 90% of the width or the height, whichever is smaller.
71+ // The radius, then, is half of this value.
72+ if Width > Height then
73+ radius := round(Height * 0.45)
74+ else
75+ radius := round(Width * 0.45);
76+
77+ // The circle then will be enclosed by the rectangle between center.X +/- radius
78+ // and center.Y +/- radius
79+ R := Rect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius);
80+
81+ // Draw the circle
82+ Canvas.Ellipse(R);
83+end;
84+```
85+
86+### Drawing a polygon
87+
88+#### Simple polygon
89+
90+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).
91+
92+![Simple Polygon: Pentagon](/wiki-images/2/25/Simple_Polygon.png) **Example:** Pentagon
93+
94+``` Pascal
95+procedure TForm1.FormPaint(Sender: TObject);
96+var
97+ P: Array[0..4] of TPoint;
98+ i: Integer;
99+ phi: Double;
100+
101+begin
102+ for i := 0 to 4 do
103+ begin
104+ phi := 2.0 * pi / 5 * i + pi * 0.5;;
105+ P[i].X := round(100 * cos(phi) + 110);
106+ P[i].Y := round(100 * sin(phi) + 110);
107+ end;
108+ Canvas.Brush.Color := clRed;
109+ Canvas.Polygon(P);
110+end;
111+```
112+
113+#### Self-overlapping polygons
114+
115+Here is a modification of the polygon example: Let's rearrange the polygon points so that the first point is connected to the 3rd initial point, the 3rd 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:
116+
117+<figure>
118+<img src="Self-overlapping_Polygon.png" title="Self-overlapping Polygon" />
119+<figcaption>Self-overlapping Polygon</figcaption>
120+</figure>
121+
122+``` Pascal
123+procedure TForm1.FormPaint(Sender: TObject);
124+var
125+ P: Array[0..4] of TPoint;
126+ P1, P2: Array[0..4] of TPoint;
127+ i: Integer;
128+ phi: Double;
129+begin
130+ for i := 0 to 4 do
131+ begin
132+ phi := 2.0 * pi / 5 * i + pi * 0.5;;
133+ P[i].X := round(100 * cos(phi) + 110);
134+ P[i].Y := round(100 * sin(phi) + 110);
135+ end;
136+ P1[0] := P[0];
137+ P1[1] := P[2];
138+ P1[2] := P[4];
139+ P1[3] := P[1];
140+ P1[4] := P[3];
141+ for i:= 0 to 4 do P2[i] := Point(P1[i].X + 200, P1[i].Y); // offset polygon
142+ Canvas.Brush.Color := clRed;
143+ Canvas.Polygon(P1, false); // false --> Even-odd rule
144+ Canvas.Polygon(P2, true); // true ---> Non-zero winding rule
145+end;
146+```
147+
148+#### Polygon with a hole
149+
150+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 ready for this task. However, you need to consider several important points:
151+
152+- 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.
153+- The order of the inner and outer polygon points in the array does not matter.
154+- 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.
155+
156+![Polygon with hole](/wiki-images/b/b2/Polygon_with_hole_1.png) Example:
157+
158+``` Pascal
159+const
160+ P: array of [0..8] of TPoint = (
161+ // outer polygon: a rectangle
162+ (X: 10; Y: 10), // <--- first point of the rectangle
163+ (X:190; Y: 10),
164+ (X:190; Y:190), // (clockwise orientation)
165+ (X: 10; Y:190),
166+ (X: 10; Y: 10), // <--- last point of the rectangle = first point
167+
168+ // inner polygon: a triangle
169+ (X: 20; Y: 20), // <--- first point of the triangle
170+ (X: 40; Y:180), // ( counter-clockwise orientation)
171+ (X: 60; Y: 20),
172+ (X: 20; Y: 20) // <--- last point of the triangle = first point
173+ );
174+
175+procedure TForm1.FormPaint(Sender: TObject);
176+begin
177+ Canvas.Brush.Color := clRed;
178+ Canvas.Polygon(Pts);
179+end;
180+```
181+
182+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. ![Polygon with hole](/wiki-images/1/13/Polygon_with_hole_2.png)
183+
184+``` Pascal
185+procedure TForm1.FormPaint(Sender: TObject);
186+begin
187+ Canvas.Brush.Color := clRed;
188+ Canvas.Pen.Style := psClear;
189+ Canvas.Polygon(Pts);
190+
191+ Canvas.Pen.Style := psSolid;
192+ Canvas.Pen.Color := clBlack;
193+ Canvas.Polyline(Pts, 0, 5); // rectangle starts at index 0 and consists of 5 array elements
194+ Canvas.Polyline(Pts, 5, 4); // triangle starts at index 5 and consists of 4 array elements
195+end;
196+```
197+
198+#### Polygon with several holes
199+
200+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](/wiki-images/9/90/Polygon_with_holes_1.png)
201+
202+``` Pascal
203+const
204+ Pts: array[0..16] of TPoint = (
205+ // outer polygon: a rectangle
206+ (X: 10; Y: 10), // clockwise
207+ (X:190; Y: 10),
208+ (X:190; Y:190),
209+ (X: 10; Y:190),
210+ (X: 10; Y: 10),
211+
212+ // inner polygon: a triangle
213+ (X: 20; Y: 20), // counter-clockwise
214+ (X: 80; Y:180),
215+ (X: 140; Y: 20),
216+ (X: 20; Y: 20),
217+
218+ // 2nd inner triangle
219+ (X: 150; Y: 50), // counter-clockwise
220+ (X: 150; Y:100),
221+ (X: 180; Y: 50),
222+ (X: 150; Y: 50),
223+
224+ // 3rd inner triangle
225+ (X: 180; Y: 80), // counter-clockwise
226+ (X: 160; Y:120),
227+ (X: 180; Y:120),
228+ (X: 180; Y: 80)
229+ );
230+```
231+
232+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 further 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: ![Polygon with holes: no additional points](/wiki-images/7/79/Polygon_with_holes_2.png) ![Polygon with holes: with additional points](/wiki-images/f/f9/Polygon_with_holes_3.png)
233+
234+``` Pascal
235+const
236+ Pts: array[0..18] of TPoint = (
237+ // outer polygon: a rectangle
238+ (X: 10; Y: 10), // clockwise
239+ (X:190; Y: 10),
240+ (X:190; Y:190),
241+ (X: 10; Y:190),
242+ (X: 10; Y: 10),
243+
244+ // 1st inner triangle
245+ (X: 20; Y: 20), // counter-clockwise --> hole
246+ (X: 80; Y:180),
247+ (X: 140; Y: 20),
248+ (X: 20; Y: 20),
249+
250+ // 2nd inner triangle
251+ (X: 150; Y: 50), // counter-clockwise --> hole
252+ (X: 150; Y:100),
253+ (X: 180; Y: 50),
254+ (X: 150; Y: 50),
255+
256+ // 3rd inner triangle
257+ (X: 180; Y: 80), // counter-clockwise --> hole
258+ (X: 160; Y:120),
259+ (X: 180; Y:120),
260+ (X: 180; Y: 80),
261+
262+ (X: 150; Y: 50), // duplicates 1st point of 2nd inner triangle
263+ (X: 20; Y: 20) // duplicates 1st point of 1st inner triangle
264+ );
265+```
266+
267+The last image at the right is drawn again with separate `Polygon()` and `PolyLine()` calls.
268+
269+## Drawing Text
270+
271+### Text painting methods
272+
273+There are two basic methods how to draw text by means of Canvas methods:
274+
275+``` Pascal
276+TCanvas.TextOut(x, y: Integer; const AText: String)
277+```
278+
279+This is the simplest way to draw the given text. Its top/left corner is at the position x/y.
280+
281+``` Pascal
282+TCanvas.TextRect(R: TRect; x, y: Integer; const AText: String; const Style: TTextStyle)
283+```
284+
285+The (optional) `TTextStyle` parameter allows to apply various options to control the text output:
286+
287+- `Alignment: TAlignment = (taLeftJustify, taRightJustify, taCenter)`: horizontal alignment of the text within the rectangle given as parameter R. When `Alignment` is `taLeftJustify` the text begins at \<x\> (measured relative to the canvas); otherwise the text is centered or right-aligned in the rectangle (x is ignored now).
288+- `Layout: TTextLayout = (tlTop, tlCenter, tlBottom)`: Analogous to `Alignment`, but for the vertical direction.
289+- `SingleLine: Boolean`: If WordBreak is false then process \#13, \#10 as standard chars and perform no line breaking.
290+- `Wordbreak: boolean`: If line of text is too long to fit between left and right boundaries, it is attempted to break the text between words into multiple lines. See also `EndEllipsis`.
291+- `EndEllipsis: Boolean`: If line of text is too long to fit between left and right boundaries, the text is truncated the text and an ellipsis (`'...'`) is added. If `Wordbreak` is set as well, `Workbreak` will dominate.
292+- `Clipping: boolean`: Clips the text to the passed rectangle.
293+- `ExpandTabs: boolean`: Replaces \#9 by appropriate amount of spaces (default is usually 8).
294+- `ShowPrefix: boolean`: Processes the first single '&' per line as an underscore and draws '&&' as '&'.
295+- `Opaque: boolean`: Fills background with the current Brush
296+- `SystemFont: Boolean`: Uses the system font instead of Canvas Font
297+- `RightToLeft: Boolean`: For RightToLeft text reading (Text Direction)
298+
299+Alternatively there is also the Windows-like `DrawText` function. Although it follows Windows syntax the procedure in unit `LCLIntf` is cross-platform.
300+
301+``` Pascal
302+function DrawText(DC: HDC; Str: PChar; Count: Integer; var Rect: TRect; Flags: Cardinal): Integer
303+```
304+
305+- `DC`: Handle of the canvas, e.g. `Paintbox1.Canvas.Handle`
306+- `Str`: Text to be written, cast to `PChar`
307+- `Count`: Number of bytes to be sent to the `DrawText` function, use `Length(Str)`
308+- `Rect`: Rectangle within the output should occur. Returns the smallest rectangle occupied by the text.
309+- `Flags`: Can contain a long list of values combined by logical "or" representing options to control the output. Among them `DT_LEFT, DT_CENTER, DT_RIGHT` for horizontal, `DT_TOP, DT_VCENTER, DT_BOTTOM` for vertical alignment, or `DT_SINGLELINE, DT_WORDBREAK, DT_END_ELLIPSIS` to control multiline behaviour. An important option is `DT_CALCRECT` which suppresses painting but returns the size needed for the text output in the `TRect` parameter. See <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtext> for a complete list.
310+
311+### Using the default GUI font
312+
313+This can be done with the following simple code:
314+
315+``` pascal
316+SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));
317+```
318+
319+or:
320+
321+``` pascal
322+Canvas.Font.Name := 'default';
323+```
324+
325+### Drawing text to an exactly fitting width
326+
327+Use the DrawText routine, first with DT_CALCRECT and then without it.
328+
329+``` pascal
330+// First calculate the text size then draw it
331+TextBox := Rect(0, currentPos.Y, Width, High(Integer));
332+DrawText(ACanvas.Handle, PChar(Text), Length(Text),
333+ TextBox, DT_WORDBREAK or DT_INTERNAL or DT_CALCRECT);
334+
335+DrawText(ACanvas.Handle, PChar(Text), Length(Text),
336+ TextBox, DT_WORDBREAK or DT_INTERNAL);
337+```
338+
339+### Drawing text with sharp edges (non antialiased)
340+
341+Some widgetsets support this via
342+
343+``` pascal
344+Canvas.Font.Quality := fqNonAntialiased;
345+```
346+
347+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:
348+
349+``` pascal
350+procedure PaintAliased(Canvas: TCanvas; x, y: integer; const TheText: string);
351+var
352+ w, h, dx, dy: Integer;
353+ IntfImg: TLazIntfImage;
354+ Img: TBitmap;
355+ col: TFPColor;
356+ FontColor, c: TColor;
357+begin
358+ w := 0;
359+ h := 0;
360+ Canvas.GetTextSize(TheText, w, h);
361+ if (w <= 0) or (h <= 0) then exit;
362+ Img := TBitmap.Create;
363+ IntfImg := nil;
364+ try
365+ // paint text to a bitmap
366+ Img.Masked := true;
367+ Img.SetSize(w,h);
368+ Img.Canvas.Brush.Style := bsSolid;
369+ Img.Canvas.Brush.Color := clWhite;
370+ Img.Canvas.FillRect(0, 0, w, h);
371+ Img.Canvas.Font := Canvas.Font;
372+ Img.Canvas.TextOut(0, 0, TheText);
373+ // get memory image
374+ IntfImg := Img.CreateIntfImage;
375+ // replace gray pixels
376+ FontColor := ColorToRGB(Canvas.Font.Color);
377+ for dy := 0 to h - 1 do begin
378+ for dx := 0 to w - 1 do begin
379+ col := IntfImg.Colors[dx, dy];
380+ c := FPColorToTColor(col);
381+ if c <> FontColor then
382+ IntfImg.Colors[dx, dy] := colTransparent;
383+ end;
384+ end;
385+ // create bitmap
386+ Img.LoadFromIntfImage(IntfImg);
387+ // paint
388+ Canvas.Draw(x, y, Img);
389+ finally
390+ IntfImg.Free;
391+ Img.Free;
392+ end;
393+end;
394+```
395+
396+---
397+*From the Free Pascal wiki, page "Graphics - Working with TCanvas", as it stood on 2023-06-27 after a change by Wp, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history