FPReport Custom elements: what changed
Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.
Title: FPReport Custom elements
| Old | New | Line | |
|---|---|---|---|
| 1 | + | FPReport contains support for four basic reporting elements: | |
| 2 | + | ||
| 3 | + | - Memo | |
| 4 | + | - Shape | |
| 5 | + | - Image | |
| 6 | + | - Checkbox | |
| 7 | + | ||
| 8 | + | While these will get you a long way, sometimes something extra is needed. | |
| 9 | + | ||
| 10 | + | On this page we explain how to create a new element. | |
| 11 | + | ||
| 12 | + | We'll take the sample TReportPolygon as an example. | |
| 13 | + | ||
| 14 | + | FPReport by default contains 2 custom reporting elements: | |
| 15 | + | ||
| 16 | + | - BarCode | |
| 17 | + | - QRCode (based on [Nayuki's QR code generator](https://www.nayuki.io/page/qr-code-generator-library)) | |
| 18 | + | ||
| 19 | + | Both elements are developed using the technique explained on this page. | |
| 20 | + | ||
| 21 | + | ## Create a new element class | |
| 22 | + | ||
| 23 | + | Any printable element must descend from TFPReportElement. It must expose published properties which allow the user to control behaviour of the element. | |
| 24 | + | ||
| 25 | + | ``` pascal | |
| 26 | + | TReportPolygon = class(TFPReportElement) | |
| 27 | + | Published | |
| 28 | + | Property Corners : Cardinal Read FCorners Write SetCorners; | |
| 29 | + | Property LineWidth : Cardinal Read FLineWidth Write SetCLineWidth; | |
| 30 | + | Property Color : TFPReportColor Read FColor Write FColor; | |
| 31 | + | // In degrees | |
| 32 | + | Property RotateAngle : Double Read FRotateAngle Write FRotateAngle; | |
| 33 | + | end; | |
| 34 | + | ``` | |
| 35 | + | ||
| 36 | + | The element itself must only know how to read/write its properties from/to a report streamer, and how to copy properties to another instance. | |
| 37 | + | ||
| 38 | + | ### Copying properties for cloning | |
| 39 | + | ||
| 40 | + | Copying properties to another instance is done in the usual fashion, using the **Assign** method: | |
| 41 | + | ||
| 42 | + | ``` pascal | |
| 43 | + | procedure TReportPolygon.Assign(Source: TPersistent); | |
| 44 | + | ||
| 45 | + | Var | |
| 46 | + | P : TReportPolygon; | |
| 47 | + | ||
| 48 | + | begin | |
| 49 | + | if (Source is TReportPolygon) then | |
| 50 | + | begin | |
| 51 | + | P:=Source as TReportPolygon; | |
| 52 | + | Corners:=P.Corners; | |
| 53 | + | Color:=P.Color; | |
| 54 | + | LineWidth:=P.LineWidth; | |
| 55 | + | RotateAngle:=P.RotateAngle; | |
| 56 | + | end; | |
| 57 | + | inherited Assign(Source); | |
| 58 | + | end; | |
| 59 | + | ``` | |
| 60 | + | ||
| 61 | + | This is important because during layouting, the assign method is used to clone the design elements before layouting them on the page. | |
| 62 | + | ||
| 63 | + | ### Writing properties to a stream | |
| 64 | + | ||
| 65 | + | When saving a report to file, all elements are written to a stream using a **TFPReportStreamer** class. (unit *fpreportstreamer*) | |
| 66 | + | ||
| 67 | + | Writing properties to the streamer must be implemented in the **WriteElement** method, or the **DoWriteLocalProperties** method: | |
| 68 | + | ||
| 69 | + | ``` pascal | |
| 70 | + | procedure TReportPolygon.DoWriteLocalProperties(AWriter: TFPReportStreamer; | |
| 71 | + | AOriginal: TFPReportElement); | |
| 72 | + | ||
| 73 | + | Var | |
| 74 | + | P : TReportPolygon; | |
| 75 | + | ||
| 76 | + | begin | |
| 77 | + | inherited DoWriteLocalProperties(AWriter, AOriginal); | |
| 78 | + | if AOriginal is TReportPolygon then | |
| 79 | + | begin | |
| 80 | + | P:=AOriginal as TReportPolygon; | |
| 81 | + | AWriter.WriteIntegerDiff('Color', Color, P.Color); | |
| 82 | + | AWriter.WriteIntegerDiff('Corners',Corners,P.Color); | |
| 83 | + | AWriter.WriteIntegerDiff('LineWidth',LineWidth,P.LineWidth); | |
| 84 | + | AWriter.WriteFloatDiff('RotateAngle',RotateAngle,P.RotateAngle); | |
| 85 | + | end | |
| 86 | + | else | |
| 87 | + | begin | |
| 88 | + | AWriter.WriteInteger('Color', Color); | |
| 89 | + | AWriter.WriteInteger('Corners',Corners); | |
| 90 | + | AWriter.WriteInteger('LineWidth',LineWidth); | |
| 91 | + | AWriter.WriteFloat('RotateAngle',RotateAngle); | |
| 92 | + | end; | |
| 93 | + | end; | |
| 94 | + | ``` | |
| 95 | + | ||
| 96 | + | The WriteElement can be called during layouting. During layouting, a copy is made of each element, and the propertie values can differ from the values set during design. To minimize the size of the stream, only a diff may be written: **AOriginal** is the element as designed, and element, do be able to write only properties that have changed. | |
| 97 | + | ||
| 98 | + | ### Reading properties from a stream | |
| 99 | + | ||
| 100 | + | When the report design is read from a stream, every element is read from stream. This happens using the same **TFPReportStreamer** class (unit *fpreportstreamer*) as is used to write the element. | |
| 101 | + | ||
| 102 | + | The ReadElement method is responsible for reading the element's design from stream: | |
| 103 | + | ||
| 104 | + | ``` pascal | |
| 105 | + | procedure TReportPolygon.ReadElement(AReader: TFPReportStreamer); | |
| 106 | + | begin | |
| 107 | + | inherited ReadElement(AReader); | |
| 108 | + | Color:= AReader.ReadInteger('Color', clBlack); | |
| 109 | + | Corners:=AReader.ReadInteger('Corners',3); | |
| 110 | + | LineWidth:=AReader.ReadInteger('LineWidth',1); | |
| 111 | + | RotateAngle:=AReader.ReadFloat('RotateAngle',0); | |
| 112 | + | end; | |
| 113 | + | ``` | |
| 114 | + | ||
| 115 | + | ## Registering the new element | |
| 116 | + | ||
| 117 | + | Before the element can be used in a stream or rendered (in PDF, image, HTML or on screen) it must be registered. This is done using the element factory, gElementFactory= | |
| 118 | + | ||
| 119 | + | ``` pascal | |
| 120 | + | gElementFactory.RegisterClass('Polygon',TReportPolygon); | |
| 121 | + | ``` | |
| 122 | + | ||
| 123 | + | When reading a report design from stream, this tells the streaming engine that whenever it encounters a 'Polygon' element type, it must create an instance of **TReportPolygon**, and read it from stream. | |
| 124 | + | ||
| 125 | + | The element factory also keeps the various hooks for rendering the element. | |
| 126 | + | ||
| 127 | + | ## Rendering the new element | |
| 128 | + | ||
| 129 | + | An element by itself does not know how to render: it is the task of a renderer to know this. The renderers know how to render the standard fpreport elements. | |
| 130 | + | ||
| 131 | + | For all unknown elements, the renderer engine will look in the element factory for a handler to actually render the list. It has already rendered the frame (if any) There are 2 types of render callback: | |
| 132 | + | ||
| 133 | + | 1. Renderer-Specific handler. | |
| 134 | + | ||
| 135 | + | ||
| 136 | + | ||
| 137 | + | This render callback is specific to the current renderer. It's parameters are: | |
| 138 | + | ||
| 139 | + | - aPos: Offset of the position on the page. | |
| 140 | + | - aElement: the element to render | |
| 141 | + | - aExporter: the renderer which is currently rendering | |
| 142 | + | - aDPI : the DPI the renderer is using. | |
| 143 | + | ||
| 144 | + | ``` pascal | |
| 145 | + | TFPReportElementExporterCallBack = Procedure(aPos : TFPReportPoint; aElement : TFPReportElement; AExporter : TFPReportExporter; ADPI: Integer); | |
| 146 | + | ``` | |
| 147 | + | ||
| 148 | + | 1. Fallback Image renderer: | |
| 149 | + | ||
| 150 | + | ||
| 151 | + | ||
| 152 | + | This handler must render the element to a TFPCustomImage instance. The image has the expected size. | |
| 153 | + | ||
| 154 | + | The image will then be placed correctly on the page by the renderer. | |
| 155 | + | ||
| 156 | + | ``` pascal | |
| 157 | + | TFPReportImageRenderCallBack = Procedure(aElement : TFPReportElement; aImage: TFPCustomImage); | |
| 158 | + | ``` | |
| 159 | + | ||
| 160 | + | It is sufficient to have a fallback renderer, but it can be advantageous to have renderer-specific callbacks. | |
| 161 | + | ||
| 162 | + | For the HTML renderer, complex graphics can be rendered using the fallback renderer. | |
| 163 | + | ||
| 164 | + | ### Image fallback renderer | |
| 165 | + | ||
| 166 | + | To render an element on a **TFPCustomImage** descendent, the **TFPImageCanvas** canvas class can be used: | |
| 167 | + | ||
| 168 | + | ``` pascal | |
| 169 | + | Procedure RenderPolygonToImage(aElement : TFPReportElement;AImage : TFPCustomImage); | |
| 170 | + | ||
| 171 | + | Var | |
| 172 | + | C : TFPImageCanvas; | |
| 173 | + | P : TReportPolygon; | |
| 174 | + | ||
| 175 | + | begin | |
| 176 | + | P:=AElement as TReportPolygon; | |
| 177 | + | C:=TFPImageCanvas.Create(AImage); | |
| 178 | + | try | |
| 179 | + | if (AElement.Frame.BackgroundColor<>fpreport.clNone) then | |
| 180 | + | begin | |
| 181 | + | C.Brush.FPColor:=ColorToRGBTriple(AElement.Frame.BackgroundColor); | |
| 182 | + | C.Brush.Style:=bsSolid; | |
| 183 | + | C.FillRect(0,0,AImage.Width-1,AImage.Height-1); | |
| 184 | + | end; | |
| 185 | + | PaintPolygon(C,Point(0,0),AImage.Width,AImage.Height,P.Corners,P.RotateAngle,P.linewidth,ColorToRGBTriple(P.Color)); | |
| 186 | + | finally | |
| 187 | + | C.Free; | |
| 188 | + | end; | |
| 189 | + | end; | |
| 190 | + | ``` | |
| 191 | + | ||
| 192 | + | The actual paint is then done in PaintPolygon: | |
| 193 | + | ||
| 194 | + | ``` pascal | |
| 195 | + | Procedure PaintPolygon(Canvas : TFPCustomCanvas; AOffset : TPoint; AWidth,AHeight : Integer; ANumber : Integer; AStartAngle : Double; ALineWidth : Integer; AColor : TFPColor); | |
| 196 | + | ||
| 197 | + | Var | |
| 198 | + | CX,CY,R,I : Integer; | |
| 199 | + | P : Array of TPoint; | |
| 200 | + | A,Step : Double; | |
| 201 | + | ||
| 202 | + | begin | |
| 203 | + | Canvas.Pen.FPColor:=AColor; | |
| 204 | + | Canvas.Pen.Width:=aLineWidth; | |
| 205 | + | Canvas.Pen.Style:=psSolid; | |
| 206 | + | if ANumber<3 then | |
| 207 | + | exit; | |
| 208 | + | CX:=AOffset.x+AWidth div 2; | |
| 209 | + | CY:=AOffset.y+AHeight div 2; | |
| 210 | + | if aWidth<aHeight then | |
| 211 | + | R:=AWidth div 2 | |
| 212 | + | else | |
| 213 | + | R:=AHeight div 2; | |
| 214 | + | SetLength(P,ANumber); | |
| 215 | + | A:=AStartAngle; | |
| 216 | + | Step:=(2*Pi)/ANumber; | |
| 217 | + | For I:=0 to ANumber-1 do | |
| 218 | + | begin | |
| 219 | + | P[i].X:=CX+Round(R*Cos(a)); | |
| 220 | + | P[i].Y:=CY-Round(R*Sin(a)); | |
| 221 | + | A:=A+Step; | |
| 222 | + | end; | |
| 223 | + | For I:=0 to ANumber-2 do | |
| 224 | + | Canvas.Line(P[I],P[I+1]); | |
| 225 | + | Canvas.Line(P[ANumber-1],P[0]); | |
| 226 | + | SetLength(P,0); | |
| 227 | + | end; | |
| 228 | + | ``` | |
| 229 | + | ||
| 230 | + | This routine will be more or less verbatim copied for all other renderers. | |
| 231 | + | ||
| 232 | + | To tell the rendering engin about this routine, it must be registered in the element factory. A good place for this is right after the registration of the new element: | |
| 233 | + | ||
| 234 | + | ``` pascal | |
| 235 | + | begin | |
| 236 | + | gElementFactory.RegisterClass('Polygon',TReportPolygon); | |
| 237 | + | // Fallback renderer | |
| 238 | + | gElementFactory.RegisterImageRenderer(TReportPolygon,@RenderPolygonToImage); | |
| 239 | + | ``` | |
| 240 | + | ||
| 241 | + | ### Renderer-specific callback: PDF | |
| 242 | + | ||
| 243 | + | Converting an element to image and then rendering the image on the page may not always be the most efficient or accurate method. | |
| 244 | + | ||
| 245 | + | For example, the PDF creator has built in support for rendering polygons. This can be used to substantually speed up the drawing of a polygon when rendering to PDF (and at the same time makes the PDF file much smaller): | |
| 246 | + | ||
| 247 | + | ``` pascal | |
| 248 | + | Procedure RenderPolygonInPDF(AOffset : TFPReportPoint; E: TFPReportElement; RE : TFPReportExporter; ADPI : Integer); | |
| 249 | + | ||
| 250 | + | Var | |
| 251 | + | PR : TFPReportExportPDF; | |
| 252 | + | PG : TReportPolygon; | |
| 253 | + | C : TPDFCoord; | |
| 254 | + | I,ANumber : Integer; | |
| 255 | + | P : Array of TPDFCoord; | |
| 256 | + | R,A,Step : Double; | |
| 257 | + | APage: TPDFPage; | |
| 258 | + | ||
| 259 | + | begin | |
| 260 | + | PR:=RE as TFPReportExportPDF; | |
| 261 | + | PG:=E as TReportPolygon; | |
| 262 | + | APage:=PR.CurrentPage; | |
| 263 | + | ANumber:=PG.Corners; | |
| 264 | + | if ANumber<3 then | |
| 265 | + | exit; | |
| 266 | + | C.X:=AOffset.Left+E.RTLayout.Left+E.RTLayout.Width / 2; | |
| 267 | + | C.Y:=AOffset.Top+E.RTLayout.Top+E.RTLayout.Height / 2; | |
| 268 | + | if E.RTLayout.Width<E.RTLayout.Height then | |
| 269 | + | R:=E.RTLayout.Width / 2 | |
| 270 | + | else | |
| 271 | + | R:=E.RTLayout.Height / 2; | |
| 272 | + | SetLength(P,ANumber); | |
| 273 | + | A:=PG.RotateAngle; | |
| 274 | + | Step:=(2*Pi)/ANumber; | |
| 275 | + | For I:=0 to ANumber-1 do | |
| 276 | + | begin | |
| 277 | + | P[i].X:=C.X+R*Cos(a); | |
| 278 | + | P[i].Y:=C.Y-R*Sin(a); | |
| 279 | + | A:=A+Step; | |
| 280 | + | end; | |
| 281 | + | APage.SetColor(PG.Color,True); | |
| 282 | + | APage.DrawPolyGon(P,PG.LineWidth); | |
| 283 | + | APage.StrokePath; | |
| 284 | + | end; | |
| 285 | + | ``` | |
| 286 | + | ||
| 287 | + | Registering this callback is again done on the element factory. | |
| 288 | + | ||
| 289 | + | In this case in addition to the element class and element class, the renderer class must be specified as well. | |
| 290 | + | ||
| 291 | + | ``` pascal | |
| 292 | + | gElementFactory.RegisterElementRenderer(TReportPolygon,TFPReportExportPDF,@RenderPolygonInPDF); | |
| 293 | + | ``` | |
| 294 | + | ||
| 295 | + | ### Renderer-specific callback: LCL | |
| 296 | + | ||
| 297 | + | For completeness' sake we'll also show how to render in the LCL renderer: this renderer is used in the designer tool to design and preview the report. It is therefor a good idea to always idea to register an LCL renderer. | |
| 298 | + | ||
| 299 | + | It resembles the FPImage fallback renderer: | |
| 300 | + | ||
| 301 | + | ``` pascal | |
| 302 | + | Procedure RenderPolygonInLCL(AOffset : TFPReportPoint; E: TFPReportElement; RE : TFPReportExporter; ADPI : Integer); | |
| 303 | + | var | |
| 304 | + | PR : TFPReportExportCanvas; | |
| 305 | + | PG : TReportPolygon; | |
| 306 | + | R : Trect; | |
| 307 | + | rPt : TFPReportPoint; | |
| 308 | + | pt : TPoint; | |
| 309 | + | ||
| 310 | + | begin | |
| 311 | + | PR:=RE as TFPReportExportCanvas; | |
| 312 | + | PG:=E as TReportPolygon; | |
| 313 | + | rpt.Left:=AOffset.Left+E.RTLayout.Left; | |
| 314 | + | rpt.Top:=AOffset.Top+E.RTLayout.top; | |
| 315 | + | Pt:=PR.CoordToPoint(rpt,0,0); | |
| 316 | + | R.TopLeft:=pt; | |
| 317 | + | R.Right:=R.Left+PR.HmmToPixels(E.RTLayout.Width); | |
| 318 | + | R.Bottom:= R.Top+PR.VmmToPixels(E.RTLayout.Height); | |
| 319 | + | PR.Canvas.Brush.Color:=e.Frame.BackgroundColor; | |
| 320 | + | PR.Canvas.Brush.Style:=bsSolid; | |
| 321 | + | PR.Canvas.FillRect(R); | |
| 322 | + | LCLPaintPolygon(PR.Canvas,R,PG.Corners,PG.RotateAngle,PG.LineWidth,PR.RGBToBGR(PG.Color)); | |
| 323 | + | end; | |
| 324 | + | ``` | |
| 325 | + | ||
| 326 | + | The paint procedure is a copy of the one for the image renderer: | |
| 327 | + | ||
| 328 | + | ``` pascal | |
| 329 | + | Procedure LCLPaintPolygon(Canvas : TCanvas; ARect : Trect; ANumber : Integer; AStartAngle : Double; ALineWidth : Integer; AColor : TColor); | |
| 330 | + | ||
| 331 | + | Var | |
| 332 | + | CX,CY,R,I : Integer; | |
| 333 | + | P : Array of TPoint; | |
| 334 | + | A,Step : Double; | |
| 335 | + | AWidth,AHeight : Integer; | |
| 336 | + | ||
| 337 | + | begin | |
| 338 | + | AWidth:=ARect.Right-ARect.Left+1; | |
| 339 | + | AHeight:=ARect.Bottom-ARect.Top+1; | |
| 340 | + | Canvas.Pen.Color:=AColor; | |
| 341 | + | Canvas.Pen.style:=psSolid; | |
| 342 | + | Canvas.Pen.Width:=aLineWidth; | |
| 343 | + | if ANumber<3 then | |
| 344 | + | exit; | |
| 345 | + | CX:=ARect.Left+AWidth div 2; | |
| 346 | + | CY:=ARect.Top+AHeight div 2; | |
| 347 | + | if aWidth<aHeight then | |
| 348 | + | R:=AWidth div 2 | |
| 349 | + | else | |
| 350 | + | R:=AHeight div 2; | |
| 351 | + | SetLength(P,ANumber); | |
| 352 | + | A:=AStartAngle; | |
| 353 | + | Step:=(2*Pi)/ANumber; | |
| 354 | + | For I:=0 to ANumber-1 do | |
| 355 | + | begin | |
| 356 | + | P[i].X:=CX+Round(R*Cos(a)); | |
| 357 | + | P[i].Y:=CY-Round(R*Sin(a)); | |
| 358 | + | A:=A+Step; | |
| 359 | + | end; | |
| 360 | + | For I:=0 to ANumber-2 do | |
| 361 | + | Canvas.Line(P[I],P[I+1]); | |
| 362 | + | Canvas.Line(P[ANumber-1],P[0]); | |
| 363 | + | end; | |
| 364 | + | ``` | |
| 365 | + | ||
| 366 | + | Again, the render callback must be registered: | |
| 367 | + | ||
| 368 | + | ``` pascal | |
| 369 | + | gElementFactory.RegisterElementRenderer(TReportPolygon,TFPReportExportCanvas,@RenderPolygonInLCL); | |
| 370 | + | ``` | |
| 371 | + | ||
| 372 | + | After this, the rendering in LCL will be done directly, without copying bitmaps. | |
| 373 | + | ||
| 374 | + | ## More info | |
| 375 | + | ||
| 376 | + | See the **reportpolygon.pas** unit in the demo applications, it contains renderers for the 5 known renderers. | |
| 377 | + | ||
| 378 | + | --- | |
| 379 | + | *From the Free Pascal wiki, page "FPReport Custom elements", as it stood on 2020-02-16 after a change by Trev, 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).* |
