Advanced PlotLab Techniques: Shared Code for FireMonkey & VCLPlotLab is a powerful plotting library for Delphi that can be used in both FireMonkey (FMX) and Visual Component Library (VCL) applications. When building cross-platform or multi-UI-targeted applications, maintaining a single codebase that serves both frameworks saves time, reduces bugs, and streamlines feature development. This article explores advanced techniques for sharing PlotLab code between FireMonkey and VCL, covering architecture patterns, component abstraction, rendering differences, input handling, performance tuning, and practical examples that demonstrate how to create maintainable, high-performance plotting code usable in both frameworks.
Why share code between FMX and VCL?
Maintaining separate plotting logic for VCL (Windows desktop) and FMX (cross-platform: Windows, macOS, iOS, Android) can lead to duplicated effort and divergent behavior. Shared code makes it easier to:
- Implement features once and run them everywhere.
- Ensure consistent appearance and behavior across platforms.
- Simplify testing and debugging.
- Reduce the maintenance burden when PlotLab updates or when you refactor code.
Key idea: separate UI-specific glue from UI-agnostic plotting logic.
High-level architecture for shared PlotLab code
A clean separation of concerns is essential. Consider a layered architecture:
- Core plotting layer (UI-agnostic)
- Data models, scales, axes, series logic, math, and layout calculations.
- Rendering abstraction layer
- Small interface that encapsulates drawing operations (lines, text, fills, transforms).
- UI glue layer (FMX-specific and VCL-specific adapters)
- Implement the rendering interface using FMX.Canvas or VCL.Canvas.
- Integration components
- Lightweight components that host PlotLab in FMX and VCL forms, handling events and lifecycle.
This architecture lets you place the core plotting logic into a Delphi unit (or a set of units) that contains no FMX or VCL uses clauses. Only the rendering adapters live in platform-specific units.
Designing the rendering abstraction
Create a minimal, well-documented rendering interface that covers the drawing needs of PlotLab: lines, polylines, bezier paths (if used), rectangles, ellipses, images, text, clipping, transforms, stroke and fill styles, and text metrics.
Example interface (conceptual; adapt names to your project):
- BeginDraw(Rect)
- EndDraw
- SetTransform(Matrix)
- PushClip(Rect)
- PopClip
- DrawLine(x1, y1, x2, y2, StrokeStyle)
- DrawPolyline(PointArray, StrokeStyle)
- DrawRect(Rect, StrokeStyle, FillStyle)
- DrawEllipse(Rect, StrokeStyle, FillStyle)
- DrawText(Text, Rect, TextStyle)
- MeasureText(Text, TextStyle): Size
- DrawBitmap(Bitmap, SrcRect, DestRect, Opacity)
- SaveState / RestoreState
Keep the interface small. If PlotLab uses more advanced GPU features in FMX, add optional capabilities flags (e.g., SupportsAntialiasing, SupportsPaths).
Implementing adapters: FMX vs VCL
FMX and VCL have different graphics models. FMX uses a hardware-accelerated, platform-abstracted canvas that supports high-DPI scaling and GPU acceleration; VCL uses GDI/GDI+ (or Direct2D) on Windows. Adapters translate the rendering interface calls to each framework’s APIs.
FMX adapter notes:
- Use TCanvas.BeginScene / EndScene.
- Use TCanvas.FillRect, Stroke.Path for vector shapes, and TTextLayout for text measurement and layout.
- Consider TBitmapCanvas for offscreen rendering, and GPU-backed bitmaps for better performance on supported platforms.
VCL adapter notes:
- Use TCanvas onpaint and BeginPaint/EndPaint semantics.
- For anti-aliased vector drawing, prefer GDI+ (TGPGraphics) or Direct2D if available; otherwise, GDI lines may be enough for simple plots.
- Use Windows font metrics (Canvas.TextWidth/Height) for measurement, or GDI+ text measurement for consistency with FMX.
Example structural approach:
- Place IPlotRenderer in a core unit.
- Create PlotRendererFMX.pas and PlotRendererVCL.pas that implement IPlotRenderer.
- At runtime, the hosting component instantiates the correct adapter based on the framework.
Handling coordinate systems and transforms
Coordinate handling is a common source of bugs when sharing plotting code.
- Normalize plot coordinates in the core layer (logical coordinates). The renderer should map logical coordinates to device coordinates.
- Use a consistent matrix representation (affine 3×3 or 2×3) in the core layer and provide conversion utilities in adapters.
- Remember that FMX has device-independent pixel scaling (StyleBook, Form.Scale), and VCL may need manual DPI-awareness (Scaled property, PerMonitorV2) — ensure your adapter applies the correct DPI transform.
Tip: encapsulate pixel density and transform logic in the adapter, exposing a function to the core like DeviceToLogicalScale or LogicalToDeviceMatrix.
Text rendering and measurement
Text is often the trickiest part to make identical across frameworks.
- Use a TextStyle record in the core layer that includes font family, size (in points), weight, style, alignment, and wrapping rules.
- Implement MeasureText in both adapters with high fidelity. FMX’s TTextLayout offers advanced layout features; in VCL, use either Canvas.TextExtent (simple) or GDI+/DirectWrite for more accurate measurement.
- For multilingual or complex scripts (Arabic, Indic), prefer platform-native layout engines (FMX/DirectWrite) and ensure the adapter selects them when available.
Input handling, interaction, and hit testing
Keep interaction logic (pan, zoom, selection) framework-agnostic:
- Core layer exposes methods: OnPanStart(x,y), OnPanMove(dx,dy), OnZoomAt(x,y,scaleFactor), OnHitTest(x,y).
- UI layer converts pointer/mouse/touch events to these calls.
- Implement a simple pointer-event adapter in FMX and VCL to translate multi-touch gestures (pinch/rotate) to zoom/pan commands. FMX provides multitouch events; in VCL you may need to handle WM_GESTURE or use FireMonkey-style gesture processing libraries.
Hit testing:
- Core plotting objects should expose bounding boxes in logical coordinates. The adapter transforms input point into logical coordinates and invokes core hit testing.
- For complex shapes, precompute simplified hit regions (bounding boxes or convex hulls) to speed up tests.
Shared component example
High-level flow:
- Shared unit: PlotCore.pas
- Types: TPlotModel, TPlotSeries, IPlotRenderer (no FMX/VCL uses)
- Logic: layout, axis calculation, series data handling, hit-testing
- FMX unit: PlotHostFMX.pas
- Uses FMX.Types, FMX.Graphics
- Implements TPlotRendererFMX = class(TInterfacedObject, IPlotRenderer)
- Exposes TPlotControlFMX = class(TControl) that owns a TPlotModel and draws via the adapter
- VCL unit: PlotHostVCL.pas
- Uses VCL.Graphics
- Implements TPlotRendererVCL
- Exposes TPlotControlVCL = class(TCustomControl)
Initialization pattern:
- The host component assigns renderer := TPlotRendererFMX.Create(Canvas, Scale) in Paint/OnPaint.
- Core drawing routine: PlotModel.Render(renderer).
Dependency management:
- Place PlotCore in a runtime package or library used by both FMX and VCL projects.
- Keep no FMX/VCL units in PlotCore to keep it reusable.
Performance optimization
Plotting can be heavy with many series or points. Techniques:
- Decimation / level-of-detail (LOD): reduce point-count when zoomed out. Implement down-sampling in core using algorithms like largest-triangle-three-buckets (LTTB) for preserving shape.
- Caching / backbuffers: render static layers (axes, gridlines) to an offscreen bitmap and reuse until invalidated.
- Incremental rendering: update only dirty regions or newly added series points.
- Use GPU acceleration on FMX: prefer GPU bitmaps and hardware strokes when available.
- Multi-threaded data preparation: compute decimation, statistics, and axis ticks on background threads but keep rendering on the main thread.
- Avoid per-point allocations in tight drawing loops; reuse buffers and pre-allocated arrays.
Example: implement a TPlotCache that stores rendered bitmaps at various scales. The renderer checks cache validity and composite caches instead of redrawing everything.
Styling and theming
Provide a theme abstraction:
- Core theme record: colors, grid visibility, axis styles, series palettes.
- Implement theme-to-renderer mapping in adapters: convert theme color records into TAlphaColor (FMX) or TColor (VCL) and apply gamma/correct blending according to platform capabilities.
- Support per-platform default themes (e.g., native light/dark). Allow user override via theme injection into the PlotModel.
Dealing with platform-specific features
When a platform offers unique capabilities, expose them as optional features so core logic can use them when available but degrade gracefully:
- FMX: shaders, GPU blending modes, subpixel text positioning.
- VCL: Direct2D/DirectWrite, GDI+ advanced smoothing.
Pattern:
- IPlotRenderer.Capabilities: set of flags.
- Core code queries capabilities and adapts (e.g., use path stroking if SupportsPaths).
Practical code snippets
Note: the following are conceptual snippets — adapt types/names to your codebase.
IPlotRenderer interface (conceptual):
type TPointFArr = array of TPointF; IPlotRenderer = interface procedure BeginDraw(const ADest: TRectF); procedure EndDraw; procedure SetTransform(const AMatrix: TMatrix); procedure DrawLine(const AFrom, ATo: TPointF; const AStrokeWidth: Single; const AColor: TAlphaColor); procedure DrawPolyline(const APoints: TPointFArr; const AStrokeWidth: Single; const AColor: TAlphaColor); procedure DrawText(const AText: string; const ARect: TRectF; const AFont: TFont; const AColor: TAlphaColor); function MeasureText(const AText: string; const AFont: TFont): TSizeF; procedure PushClip(const ARect: TRectF); procedure PopClip; end;
FMX adapter sketch:
type TPlotRendererFMX = class(TInterfacedObject, IPlotRenderer) private FCanvas: TCanvas; FScale: Single; public constructor Create(ACanvas: TCanvas; AScale: Single); procedure BeginDraw(const ADest: TRectF); procedure EndDraw; procedure SetTransform(const AMatrix: TMatrix); procedure DrawLine(const AFrom, ATo: TPointF; const AStrokeWidth: Single; const AColor: TAlphaColor); ... end;
VCL adapter sketch:
type TPlotRendererVCL = class(TInterfacedObject, IPlotRenderer) private FCanvas: TCanvas; FDPI: Integer; public constructor Create(ACanvas: TCanvas; ADPI: Integer); procedure BeginDraw(const ADest: TRectF); procedure EndDraw; procedure DrawLine(const AFrom, ATo: TPointF; const AStrokeWidth: Single; const AColor: TColor); ... end;
Testing and QA
- Unit test the core logic (axis calculations, tick generation, decimation) independent of UI.
- Create smoke tests for rendering by drawing to bitmaps in both adapters and comparing against golden images (tolerant pixel difference).
- Create interactive tests on target platforms (Windows, macOS, Android, iOS) to validate input handling, DPI scaling, and performance.
- Automate performance benchmarks with varying series sizes and measure frame times.
Migration and incremental adoption
If you already have a VCL or FMX PlotLab integration:
- Start by extracting core plotting logic into PlotCore units.
- Implement an adapter for the other framework and port one feature at a time.
- Use feature toggles to switch between native drawing paths during the migration.
- Keep user-facing APIs stable so forms and existing code require minimal changes.
Example: Implementing shared pan & zoom
Core interface:
- BeginDrag(x,y)
- DragTo(x,y)
- EndDrag
- ZoomAt(x,y,scale)
UI glue:
- FMX OnMouseDown / OnTouchBegin -> Core.BeginDrag with logical coords
- VCL MouseDown -> same
Coordinate conversion:
- Input device point -> adapter transforms to logical using inverse of current transform matrix.
Troubleshooting checklist
- Text looks different between platforms: check font substitution, DPI scaling, and text measurement routines.
- Slow redraws: enable caching for static layers and implement LOD.
- Hit testing inaccurate: verify transform math and ensure input coordinates use the same logical space as object bounding boxes.
- Flicker on VCL: use double buffering or offscreen bitmaps.
- Inconsistent anti-aliasing: check renderer capabilities and choose consistent smoothing settings.
Conclusion
Sharing PlotLab code between FireMonkey and VCL is a practical, maintainable strategy that pays off as applications grow. The core principles are separation of concerns, a small rendering abstraction, faithful adapter implementations, and performance-conscious rendering techniques. With a compact IPlotRenderer interface, careful handling of text and transforms, and LOD/caching strategies, you can deliver a single, robust plotting core that behaves consistently across platforms while taking advantage of each framework’s strengths where appropriate.
Leave a Reply