Sign in Join
Wiki copytemporary

Graphics - Working with TCanvas: what changed

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

OldNewLine
11## Drawing shapes
22
33### Drawing a rectangle
44
55Many 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.
66
77Suppose 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!
88
99The 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:
1010
1111``` pascal
1212procedure TForm1.FormPaint(Sender: TObject);
1313var
1414 w, h: Integer; // Width and height of the rectangle
1515 cx, cy: Integer; // center of the form
1616 R: TRect; // record containing the coordinates of the rectangle's left, top, right, bottom corners
1717begin
1818 // Calculate form center
1919 cx := Width div 2;
2020 cy := Height div 2;
2121
2222 // Calculate the size of the rectangle
2323 w := Width div 2;
2424 h := Height div 2;
2525
2626 // Calculate the corner points of the rectangle
2727 R.Left := cx - w div 2;
2828 R.Top := cy - h div 2;
2929 R.Right := cx + w div 2;
3030 R.Bottom := cy + h div 2;
3131
3232 // Set the fill color
3333 Canvas.Brush.Color := clRed;
3434 Canvas.Brush.Style := bsSolid;
3535
3636 // Set the border color
3737 Canvas.Pen.Color := clBlue;
3838 Canvas.Pen.Width := 5;
3939 Canvas.Pen.Style := psSolid;
4040
4141 // Draw the rectangle
4242 Canvas.Rectangle(R);
4343end;
4444```
4545
4646### Drawing a circle
4747
4848The 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:
4949
5050``` pascal
5151procedure TForm1.FormPaint(Sender: TObject);
5252var
5353 radius: Integer; // Radius of the circle
5454 center: TPoint; // Center point of the circle
5555 R: TRect; // Rectangle enclosing the circle
5656begin
5757 // Set the fill color
5858 Canvas.Brush.Color := clYellow;
5959 Canvas.Brush.Style := bsSolid;
6060
6161 // Set the border color
6262 Canvas.Pen.Color := clBlue;
6363 Canvas.Pen.Width := 3;
6464 Canvas.Pen.Style := psSolid;
6565
6666 // We want the circle to be centered in the form
6767 center.X := Width div 2;
6868 center.Y := Height div 2;
6969
7070 // The diameter should be 90% of the width or the height, whichever is smaller.
7171 // The radius, then, is half of this value.
7272 if Width > Height then
7373 radius := round(Height * 0.45)
7474 else
7575 radius := round(Width * 0.45);
7676
7777 // The circle then will be enclosed by the rectangle between center.X +/- radius
7878 // and center.Y +/- radius
7979 R := Rect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius);
8080
8181 // Draw the circle
8282 Canvas.Ellipse(R);
8383end;
8484```
8585
8686### Drawing a polygon
8787
8888#### Simple polygon
8989
9090A 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).
9191
9292![Simple Polygon: Pentagon](/wiki-images/2/25/Simple_Polygon.png) **Example:** Pentagon
9393
9494``` Pascal
9595procedure TForm1.FormPaint(Sender: TObject);
9696var
9797 P: Array[0..4] of TPoint;
9898 i: Integer;
9999 phi: Double;
100100
101101begin
102102 for i := 0 to 4 do
103103 begin
104104 phi := 2.0 * pi / 5 * i + pi * 0.5;;
105105 P[i].X := round(100 * cos(phi) + 110);
106106 P[i].Y := round(100 * sin(phi) + 110);
107107 end;
108108 Canvas.Brush.Color := clRed;
109109 Canvas.Polygon(P);
110110end;
111111```
112112
113113#### Self-overlapping polygons
114114
115115Here 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:
116116
117-<figure>
118-<img src="Self-overlapping_Polygon.png" title="Self-overlapping Polygon" />
119-<figcaption>Self-overlapping Polygon</figcaption>
120-</figure>
117+![](/wiki-images/2/25/Self-overlapping_Polygon.png)
121118
122119``` Pascal
123120procedure TForm1.FormPaint(Sender: TObject);
124121var
125122 P: Array[0..4] of TPoint;
126123 P1, P2: Array[0..4] of TPoint;
127124 i: Integer;
128125 phi: Double;
129126begin
130127 for i := 0 to 4 do
131128 begin
132129 phi := 2.0 * pi / 5 * i + pi * 0.5;;
133130 P[i].X := round(100 * cos(phi) + 110);
134131 P[i].Y := round(100 * sin(phi) + 110);
135132 end;
136133 P1[0] := P[0];
137134 P1[1] := P[2];
138135 P1[2] := P[4];
139136 P1[3] := P[1];
140137 P1[4] := P[3];
141138 for i:= 0 to 4 do P2[i] := Point(P1[i].X + 200, P1[i].Y); // offset polygon
142139 Canvas.Brush.Color := clRed;
143140 Canvas.Polygon(P1, false); // false --> Even-odd rule
144141 Canvas.Polygon(P2, true); // true ---> Non-zero winding rule
145142end;
146143```
147144
148145#### Polygon with a hole
149146
150147Suppose 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:
151148
152149- 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.
153150- The order of the inner and outer polygon points in the array does not matter.
154151- 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.
155152
156153![Polygon with hole](/wiki-images/b/b2/Polygon_with_hole_1.png) Example:
157154
158155``` Pascal
159156const
160157 P: array of [0..8] of TPoint = (
161158 // outer polygon: a rectangle
162159 (X: 10; Y: 10), // <--- first point of the rectangle
163160 (X:190; Y: 10),
164161 (X:190; Y:190), // (clockwise orientation)
165162 (X: 10; Y:190),
166163 (X: 10; Y: 10), // <--- last point of the rectangle = first point
167164
168165 // inner polygon: a triangle
169166 (X: 20; Y: 20), // <--- first point of the triangle
170167 (X: 40; Y:180), // ( counter-clockwise orientation)
171168 (X: 60; Y: 20),
172169 (X: 20; Y: 20) // <--- last point of the triangle = first point
173170 );
174171
175172procedure TForm1.FormPaint(Sender: TObject);
176173begin
177174 Canvas.Brush.Color := clRed;
178175 Canvas.Polygon(Pts);
179176end;
180177```
181178
182179You 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)
183180
184181``` Pascal
185182procedure TForm1.FormPaint(Sender: TObject);
186183begin
187184 Canvas.Brush.Color := clRed;
188185 Canvas.Pen.Style := psClear;
189186 Canvas.Polygon(Pts);
190187
191188 Canvas.Pen.Style := psSolid;
192189 Canvas.Pen.Color := clBlack;
193190 Canvas.Polyline(Pts, 0, 5); // rectangle starts at index 0 and consists of 5 array elements
194191 Canvas.Polyline(Pts, 5, 4); // triangle starts at index 5 and consists of 4 array elements
195192end;
196193```
197194
198195#### Polygon with several holes
199196
200197Applying 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)
201198
202199``` Pascal
203200const
204201 Pts: array[0..16] of TPoint = (
205202 // outer polygon: a rectangle
206203 (X: 10; Y: 10), // clockwise
207204 (X:190; Y: 10),
208205 (X:190; Y:190),
209206 (X: 10; Y:190),
210207 (X: 10; Y: 10),
211208
212209 // inner polygon: a triangle
213210 (X: 20; Y: 20), // counter-clockwise
214211 (X: 80; Y:180),
215212 (X: 140; Y: 20),
216213 (X: 20; Y: 20),
217214
218215 // 2nd inner triangle
219216 (X: 150; Y: 50), // counter-clockwise
220217 (X: 150; Y:100),
221218 (X: 180; Y: 50),
222219 (X: 150; Y: 50),
223220
224221 // 3rd inner triangle
225222 (X: 180; Y: 80), // counter-clockwise
226223 (X: 160; Y:120),
227224 (X: 180; Y:120),
228225 (X: 180; Y: 80)
229226 );
230227```
231228
232229Rendering 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)
233230
234231``` Pascal
235232const
236233 Pts: array[0..18] of TPoint = (
237234 // outer polygon: a rectangle
238235 (X: 10; Y: 10), // clockwise
239236 (X:190; Y: 10),
240237 (X:190; Y:190),
241238 (X: 10; Y:190),
242239 (X: 10; Y: 10),
243240
244241 // 1st inner triangle
245242 (X: 20; Y: 20), // counter-clockwise --> hole
246243 (X: 80; Y:180),
247244 (X: 140; Y: 20),
248245 (X: 20; Y: 20),
249246
250247 // 2nd inner triangle
251248 (X: 150; Y: 50), // counter-clockwise --> hole
252249 (X: 150; Y:100),
253250 (X: 180; Y: 50),
254251 (X: 150; Y: 50),
255252
256253 // 3rd inner triangle
257254 (X: 180; Y: 80), // counter-clockwise --> hole
258255 (X: 160; Y:120),
259256 (X: 180; Y:120),
260257 (X: 180; Y: 80),
261258
262259 (X: 150; Y: 50), // duplicates 1st point of 2nd inner triangle
263260 (X: 20; Y: 20) // duplicates 1st point of 1st inner triangle
264261 );
265262```
266263
267264The last image at the right is drawn again with separate `Polygon()` and `PolyLine()` calls.
268265
269266## Drawing Text
270267
271268### Text painting methods
272269
273270There are two basic methods how to draw text by means of Canvas methods:
274271
275272``` Pascal
276273TCanvas.TextOut(x, y: Integer; const AText: String)
277274```
278275
279276This is the simplest way to draw the given text. Its top/left corner is at the position x/y.
280277
281278``` Pascal
282279TCanvas.TextRect(R: TRect; x, y: Integer; const AText: String; const Style: TTextStyle)
283280```
284281
285282The (optional) `TTextStyle` parameter allows to apply various options to control the text output:
286283
287284- `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).
288285- `Layout: TTextLayout = (tlTop, tlCenter, tlBottom)`: Analogous to `Alignment`, but for the vertical direction.
289286- `SingleLine: Boolean`: If WordBreak is false then process \#13, \#10 as standard chars and perform no line breaking.
290287- `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`.
291288- `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.
292289- `Clipping: boolean`: Clips the text to the passed rectangle.
293290- `ExpandTabs: boolean`: Replaces \#9 by appropriate amount of spaces (default is usually 8).
294291- `ShowPrefix: boolean`: Processes the first single '&' per line as an underscore and draws '&&' as '&'.
295292- `Opaque: boolean`: Fills background with the current Brush
296293- `SystemFont: Boolean`: Uses the system font instead of Canvas Font
297294- `RightToLeft: Boolean`: For RightToLeft text reading (Text Direction)
298295
299296Alternatively there is also the Windows-like `DrawText` function. Although it follows Windows syntax the procedure in unit `LCLIntf` is cross-platform.
300297
301298``` Pascal
302299function DrawText(DC: HDC; Str: PChar; Count: Integer; var Rect: TRect; Flags: Cardinal): Integer
303300```
304301
305302- `DC`: Handle of the canvas, e.g. `Paintbox1.Canvas.Handle`
306303- `Str`: Text to be written, cast to `PChar`
307304- `Count`: Number of bytes to be sent to the `DrawText` function, use `Length(Str)`
308305- `Rect`: Rectangle within the output should occur. Returns the smallest rectangle occupied by the text.
309306- `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.
310307
311308### Using the default GUI font
312309
313310This can be done with the following simple code:
314311
315312``` pascal
316313SelectObject(Canvas.Handle, GetStockObject(DEFAULT_GUI_FONT));
317314```
318315
319316or:
320317
321318``` pascal
322319Canvas.Font.Name := 'default';
323320```
324321
325322### Drawing text to an exactly fitting width
326323
327324Use the DrawText routine, first with DT_CALCRECT and then without it.
328325
329326``` pascal
330327// First calculate the text size then draw it
331328TextBox := Rect(0, currentPos.Y, Width, High(Integer));
332329DrawText(ACanvas.Handle, PChar(Text), Length(Text),
333330 TextBox, DT_WORDBREAK or DT_INTERNAL or DT_CALCRECT);
334331
335332DrawText(ACanvas.Handle, PChar(Text), Length(Text),
336333 TextBox, DT_WORDBREAK or DT_INTERNAL);
337334```
338335
339336### Drawing text with sharp edges (non antialiased)
340337
341338Some widgetsets support this via
342339
343340``` pascal
344341Canvas.Font.Quality := fqNonAntialiased;
345342```
346343
347344Some 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:
348345
349346``` pascal
350347procedure PaintAliased(Canvas: TCanvas; x, y: integer; const TheText: string);
351348var
352349 w, h, dx, dy: Integer;
353350 IntfImg: TLazIntfImage;
354351 Img: TBitmap;
355352 col: TFPColor;
356353 FontColor, c: TColor;
357354begin
358355 w := 0;
359356 h := 0;
360357 Canvas.GetTextSize(TheText, w, h);
361358 if (w <= 0) or (h <= 0) then exit;
362359 Img := TBitmap.Create;
363360 IntfImg := nil;
364361 try
365362 // paint text to a bitmap
366363 Img.Masked := true;
367364 Img.SetSize(w,h);
368365 Img.Canvas.Brush.Style := bsSolid;
369366 Img.Canvas.Brush.Color := clWhite;
370367 Img.Canvas.FillRect(0, 0, w, h);
371368 Img.Canvas.Font := Canvas.Font;
372369 Img.Canvas.TextOut(0, 0, TheText);
373370 // get memory image
374371 IntfImg := Img.CreateIntfImage;
375372 // replace gray pixels
376373 FontColor := ColorToRGB(Canvas.Font.Color);
377374 for dy := 0 to h - 1 do begin
378375 for dx := 0 to w - 1 do begin
379376 col := IntfImg.Colors[dx, dy];
380377 c := FPColorToTColor(col);
381378 if c <> FontColor then
382379 IntfImg.Colors[dx, dy] := colTransparent;
383380 end;
384381 end;
385382 // create bitmap
386383 Img.LoadFromIntfImage(IntfImg);
387384 // paint
388385 Canvas.Draw(x, y, Img);
389386 finally
390387 IntfImg.Free;
391388 Img.Free;
392389 end;
393390end;
394391```
395392
396393---
397394*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