Adding a new Canvas class - graemeg/fpGUI GitHub Wiki

About

This will hopefully help anyone in the future implementing a new canvas class in fpGUI. Also this will somewhat explain a bit how the process of painting works and how the different canvases work together.

Steps

  1. Create a new unit that uses fpg_base.
  2. Add a new canvas class i.e. TfpgCairoCanvas derived from TfpgCanvasBase.
  3. Copy/Paste the virtual; abstract; methods to the new type.
  4. Start implementing methods.

Implementation

Canvas Basics / Background

Currently fpGUI uses a single native window surface for each window with it's child widgets. We call these 'alien' widgets because they are completely virtual and the events are managed by the toplevel native window class.

As a result, when a widget/canvas draws, it is actually drawing on the window surface that all widgets share and care must be taken to not draw in another widgets space. Previously, using a window for each widget would do this for us but caused other difficulties not relevant to this topic.

The Soloution

Each fpgWidget has it's own canvas object, however the drawing commands it recieves are performed on a shared window surface or buffer. The canvas acts like a context and has it's own colors/properties/fonts which don't affect other canvases.

Lifecycle of a canvas

Created in the widgets constructor

When a Widget is created it creates a Canvas instance derived from the global variable in fpg_main: DefaultCanvasClass.

FCanvas := DefaultCanvasClass.Create(Self);

A paint event begins

As it's properties are set it's inner implementation is updating it's context for when a paint event is trigger executed. Usually these properties are set or changed in response to a paint event.

DoBeginDraw is called. It has two arguments, the widget and the CanvasTarget. The CanvasTarget is the canvas of the top level widget that has a native window. So in the case of a simple form with a button, the button is drawing on the canvas of the parent form. CanvasTarget for the button would be the same as Button.Parent.Canvas. This is a good place to instantiate the implementation specific resources like a context.

If the canvas is not the toplevel canvas then the base class asks the toplevel canvas to start drawing by calling BeginDraw. This is done for us and need not be done by our child class.

Painting

The virtual; abstract; methods that the canvas class implements recieve commands with coordinates relative to the widget. So if a button is drawing a rectangle around the button, DoDrawRectangle might have the arguments DoDrawRectangle(0,0,Button.Width,Button.Height). Likely the button is not located in the extreme top/left of the form (0,0) so the commands must be translated to the top/left of the widget inside the toplevel canvas.

FDeltaX and FDeltaY are the two variables used to store the deltas of the points the canvas needs to translate where it is actually painted on the toplevel window. They are set in TfpgCanvasBase.BeginDraw and are ready to use before DoBeginDraw is called.

A painting ends

When the painting is done the method DoPutBufferToScreen is called, but only for the toplevel canvas. Since there is only one buffer shared among all the widgets, the painting is completed on the toplevel widget and it only needs to move that buffer to the target window surface once.

DoEndDraw is called in response to FreeResources. FreeResources appears to never be called. A bug? Canvas.EndDraw calls DoPutBufferToScreen.

The canvas is destroyed

In the canvas destructor any remaining allocated implementation specific resources should be freed.

Clipping

With each child sharing a single surface clipping is a little more difficult. If a child widget sets a cliprect that is outside of it's visible area it must be further clipped to prevent unwanted painting. For instance if a button is placed as a child overlapping the bounds of its parent, either partially or fully, it should not draw outside of its parent. The protected method TfpgCanvasBase.GetWidgetWindowRect retrieves the valid visible area in toplevel coordinates of the widget. It should be combined with the argument value of DoSetClipRect for the desired result.

When DoClearCliprect is called a cliprect equal to GetWidgetWindowRect is still required for a non top level canvas.