Skip to content

Paint a chart on your own canvas with TChartPainter - #4

Open
JaapBaak wants to merge 2 commits into
GDKsoftware:mainfrom
JaapBaak:feature/chart-painter
Open

JaapBaak wants to merge 2 commits into
GDKsoftware:mainfrom
JaapBaak:feature/chart-painter

Conversation

@JaapBaak

Copy link
Copy Markdown
Contributor

Until now, the only way to show a Chart4D plot on screen, with its cached render, hover highlight and tooltip, was the TChart4D control. An application that already owns a canvas had to either reimplement that logic or give up hover and tooltips. A typical case is a VCL TPaintBox in an existing viewer.

This PR adds TChartPainter to both Chart4D.VCL and Chart4D.FMX. It paints any TChartPlot onto a canvas the caller supplies, with the same buffering, hover tracking and tooltip as the control:

FPainter := TChartPainter.Create(FPlot);
FPainter.View.OnRepaintRequest := PainterRepaintRequest;   // -> PaintBox.Invalidate

procedure TFormViewer.PaintBoxPaint(Sender: TObject);
begin
  FPainter.Paint(PaintBox.Canvas, PaintBox.Width, PaintBox.Height);
end;

When the chart shares the canvas with other content, it can be painted into a rectangle instead. It is laid out for that rectangle's size, and nothing is drawn outside it:

FPainter.Paint(PaintBox.Canvas, TRect.Create(200, 0, PaintBox.Width, PaintBox.Height));

Pointer moves and leaves go to FPainter.MouseMove and FPainter.MouseLeave. MouseMove takes canvas coordinates, and a position outside the painted rectangle counts as leaving the chart. FPainter.View carries ShowTooltips and OnDataPointHover.

Design

  • The framework-neutral part is in the new RTL-only unit Chart4D.View.pas, as TChartView. It references a plot without owning it and subscribes to OnChanged. It tracks whether the last render is still valid, including its size, and owns the TChartHoverState. It renders only when needed, draws the tooltip overlay, and fires OnDataPointHover and OnRepaintRequest when the hovered point changes. It works in chart-local coordinates and holds no pixels, because the RTL has no bitmap class and IChartCanvas cannot draw bitmaps.

  • Each framework's TChartPainter owns a view and the back buffer: a Vcl.Graphics.TBitmap drawn through TGdiPlusChartCanvas, or an FMX.Graphics.TBitmap drawn through TFmxChartCanvas. It has two Paint overloads:

    • Paint(Canvas, Bounds), with a TRect in the VCL and a TRectF in FMX. It resizes the buffer to the size of Bounds, renders when needed, copies the buffer to Bounds and then draws the overlay, shifted to Bounds.TopLeft and clipped to Bounds. The clip matters: a tooltip pushed against the chart edge strokes its border across that edge, which would otherwise mark the neighbouring content.
    • Paint(Canvas, Width, Height), which is the same as Paint with bounds (0, 0, Width, Height).

    Both overloads raise EChart4DException for a negative width or height.

  • The painter remembers the painted bounds, so MouseMove translates canvas coordinates into chart coordinates. Outside the bounds it reports leaving, since a hit target with a radius could otherwise be hit from just outside the chart.

  • The painter does not own the plot and takes over Plot.OnChanged. When destroyed, it clears that handler only if it hasn't been reassigned since.

TChart4D now uses the painter

TChart4D in both frameworks is now built on its painter, painting at its own origin. It still owns its plot, and passes Paint, Resize and the mouse events on to the painter. The two controls were near line-for-line duplicates (FHover, FBackBuffer, EnsureBackBuffer, DrawTooltipOverlay, PlotChanged, HoverChanged and the mouse handlers). That logic now lives once in the core, as TChartHoverState already does (SPEC 4.11), so the controls cannot drift apart. Only the back buffer, the canvas adapter and Invalidate versus Repaint still differ.

Compatibility

The public and protected API of TChart4D is unchanged: Create, Destroy, SaveToPng, Plot, ShowTooltips, OnDataPointHover, the published properties, FMX's Paint, and RenderChartToBackBuffer. So is its behaviour: repaint on Plot.OnChanged, a resize invalidates the buffer, OnDataPointHover passes the control as Sender, and SaveToPng (untouched) never draws a tooltip and raises on an empty plot.

Docs

  • README.md: new section "Painting on your own canvas", including painting into a rectangle.
  • SPEC.md: new section 4.25 for the view and painters; sections 2, 4.2 (the new SPaintBoundsNegativeSize message), 4.9, 4.10, 4.11 and the test list in section 5 are updated.

Tests

  • New fixture Chart4D.View.Tests with 19 tests: render skipping and re-rendering after a plot change, size change or Invalidate; hover events and repaint requests; overlay drawn or not; unsubscribe on destroy.

  • VclCheck and FmxCheck each have a new VerifyPainterBounds check. The same painter paints once at the origin and once into offset bounds on a larger canvas filled with a marker color. The check requires that:

    • the offset region matches the origin paint, both at rest and with the tooltip showing (FMX allows a rounding step of 2 per color channel, because it antialiases the tooltip slightly differently when it is shifted);
    • no pixel outside the bounds changes;
    • hovering at the offset reports the data point, and moving outside the bounds reports leaving;
    • negative bounds raise.

    The hovered point is the last one of a line chart, so its tooltip is pushed against the edge. Removing the overlay shift or the clip from either painter makes the check fail.

  • Chart4D_R (RAD Studio 12.0 and 13.0) and CoreCheck, VclCheck and FmxCheck include the new unit, and CoreCheck exercises the view.

  • The suite passes on Win32 and Win64 (241/241 each).

  • The packages, demos and tools build with zero warnings and hints, and CoreCheck, VclCheck and FmxCheck pass. VclCheck and FmxCheck drive the controls through the new paint path.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant