Thursday, January 28, 2021

Quick and Dirty FireMonkey Donut Gauge

Someone was asking where they could find gauge style chart components for FireMonkey like the ones you might see on a dashboard. They even helpfully posted some pictures.

At least one of these looks like it would be easy to reproduce without any third party components.




The requirements are straight forward. Create a donut gauge chart similar to the one pictured here. It will operate like a TProgressBar, only rounder, and have Min, Max and Value properties. The value property is displayed in the middle and changes to any of these properties affects the size of the curved progress bar (arc) accordingly.

This can be accomplished with two TArc components and a TLabel.

Since it's a proof of concept for a visual control, it makes sense to use a frame. This separates the gauge specific code into a separate unit, lets you declare private fields and publish properties. Things like testing and making multiple instances become easier, too.

Source code for the sample project is available here and can be built with recent versions of Delphi, including the Community Edition.


Create the application


Create a new FireMonkey application:
- Click File|New|Multi-Device Application - Delphi from the menu and select Blank Application.

Add a FireMonkey frame:
- Click File|New|Other|Individual Files and select FireMonkey Frame.
- Set the Height and Width properties of the frame to 200. The gauge looks better if the dimensions are the same.
- Give it a meaningful name. This is important. I used fraDonutGauge.

Add the following to the frame:
- Two TArc components.
- One TLabel component.

Configure Arc1. This acts as the background and doesn't change:
- Set Align to Client.
- Set Stroke.Cap to Round.
- Set Stroke.Color to Blue.
- Set Stroke.Thickness to 15.
- In Margins, set Left, Right and Top all to 1 pixel. 
- Set StartAngle to 135.
- Set EndAngle to 270.

Set Configure Arc2. This updates when the value changes:
- Set Align to Client.
- Set Stroke.Cap to Round.
- Set Stroke.Color to Lightgray.
- Set Stroke.Thickness to 17.
- Set StartAngle to 135.
- Set EndAngle to 0.

Configure Label1:
- Set Align to Client.
- In TextSettings, set HorizAlign to Center.
- In TextSettings, set Font Family to Arial.
- In TextSettings, set Font Size to 28.
- Set Text to 0.


In case it isn't obvious from the property names, TArc's StartAngle is measured from 0 degrees starting at the right side of the circle and goes clockwise. EndAngle measures from StartAngle instead of from that same 0 degree origin and also goes clockwise.




The Code


The code to set properties and call the drawing code is mostly just boilerplate. The interesting parts are in the Draw procedure where the arc's angle is calculated and updated.

procedure TfraDonutGauge.Draw;
var
  LSweepAngle: Single;
begin
  Label1.Text := Format('%.0n', [FValue + 0.0]);
  LSweepAngle := ((FValue - FMin) / (FMax - FMin)) * 270;

  // Arc2.EndAngle := LSweepAngle;
  TAnimator.AnimateFloat(Arc2, 'EndAngle', LSweepAngle, 0.1,
    TAnimationType.InOut, TInterpolationType.Quadratic);
end;


There isn't any range or error checking to speak of, but it's sample code, so I'm allowed to be lazy like that. In production, it pays to not scrimp here. What happens if a value is out of range or if min is larger than max, etc? The more robust you make your components, the more reliable your applications will be. Getting this right will likely take as much or more time than the rest of the code.

The Format function uses %n so that the thousands separator is displayed. It requires a floating point value, so the code adds 0.0 to convert from Integer and the %.0n in the format string keeps it from displaying two (by default) decimal places.

Calculating LSweepAngle looks a little more complicated than you might expect, but Min can be some value other than zero and that needs to be taken into account. If this were a more serious project, I would put that code into a separate function and write some unit tests to make sure it works as expected. 

When you set the arc's EndAngle (commented line), the display updates immediately. By using AnimateFloat, that transition is less abrupt and, I think, adds some visual flair. In this case, the bar starts moving slowly, speeds up until the middle and slows down as it approaches the final value. This all happens over the span of 0.1 seconds, which is pretty quick, but any longer seemed laggy.


Using the Gauge


On the main form, select Frames from the component palette. You will see a list of all of the frames used in the project. There should only be one. Select it and click OK and the frame will be added to the form.

If you click on the frame, the topmost component on the frame is selected. If you want to drag the frame, there's a trick to it. Click on the frame and hold the mouse button down. In the structure pane, you will see that the label has focus. While still holding the mouse button down, press escape once. Focus will change to the parent, which is the frame itself. Now you can drag the frame where you like. This works on any control with children, like a panel or layout and is useful if the child components obscure the parent.

Add some buttons to the form to test setting values. Here's the code I used:

procedure TfrmMain.Button1Click(Sender: TObject);
begin
  fraDonutGauge1.Value := 0;
end;

procedure TfrmMain.Button2Click(Sender: TObject);
begin
  fraDonutGauge1.Value := 500;
end;

procedure TfrmMain.Button3Click(Sender: TObject);
begin
  fraDonutGauge1.Value := 1280;
end;

procedure TfrmMain.Button4Click(Sender: TObject);
begin
  fraDonutGauge1.Value := 2000;
end;


And there you have it - A functional, but still rough around the edges, donut gauge chart.


Next Steps


If someone wanted to dig a little deeper, here are some suggestions:

Automatically change the stroke width and font size when the gauge is resized.

Instead of hosting FireMonkey components, you could do some or all of the drawing directly using canvas methods or TPathData. Look at the source code for other components, especially under Shapes, to see how they are done. The QuarkCube YouTube channel has lots of videos using FireMonkey in interesting and creative ways. Just whatever you do, don't adopt their code formatting style. I mean, yikes!

Create a component that can be installed into the IDE and used at design time. Ray Konopka gave a great talk about creating custom FireMonkey controls at ITDevCon. [Part 1]  [Part 2]

Reproduce some of the other gauge styles that were being asked for. A lot of the logic is the same and can be implemented in a common parent component and then create descendants that only need to care about any differences between them.



Conclusion


Third party component vendors are great, but if you need something simple, if you already have a component that does most of what you need and can be extended or if you need something that just doesn't exist, it might be worth trying to build it yourself. What have you got to lose?

FireMonkey, in particular, is a rich, cross-platform, hardware accelerated graphics framework. I like it a lot for data visualisation. Especially in combination with Delphi's database features.

1 comment:

lim electronics said...

Blue color should be reversed
0 no color 360 blue. Now it is wrong