Scalable Vector Graphics (SVG) are awesome. Not only because they are scaleable, but perhaps even more importantly, because they are editable. When you download a bitmap or png from the web, your options for editing it are limited. With a lot of work and expertise in Photoshop, you can perhaps edit them, but usually not perfectly and changing them on the fly is just not possible.
SVGs are different. Each piece of the graphic is editable. You can change colors, rotations, visibility, etc. With the RiverSoftAVG SVG Component Library (RSCL), you get access to a whole world wide web of customizable graphics for your applications.
In this blog post, I am going to show you how to download, prepare, and use a SVG to create incredible customizable graphics for your applications. I will take you through the exact steps in my journey, the easy and the hard, to find and customize an SVG to provide a great new gauge, that can be used in our applications. For those who don’t own the RSCL, there is an evaluation version available from the RiverSoftAVG Download Page that you can use to follow along.
Finding the SVG
Some say that the first step on a journey is the hardest; in this case, they are definitely right. The hardest part about this whole process is finding the right SVG. Finding good SVGs is easy. Even finding good, free SVGs is easy. But finding good, free SVGs that can be easily customized (whether on purpose or by accident) is not quite as easy.
The first thing I did was go to www.openclipart.org. OpenClipArt.org is a great repository for clip art that have been released into the public domain and licensed for unlimited commercial use.
I had no idea what I was going to create for this blog post, but I knew I wanted a gauge, so I typed “gauge” into their search box and browsed the results. I finally settled on the speedometer https://openclipart.org/detail/99937/speedometer created and uploaded by rg1024 back in 2010.
It had a clean look and it looked like it had a nice discrete object for the needle (at least I hoped so). My idea was that by rotating the needle I would have a nice, dynamic speedometer gauge.
I downloaded the SVG, which was called velocimetro.svg. First, I opened the SVG using the SVGPaintBoxVCL demo and it looked great, but I needed to do some pre-processing work to make the gauge useful as a dynamic object. I started the SVGEditorFMX demo application provided with the RSCL and opened the velocimetro.svg file.
Clicking on the needle in the SVG selected the glass shine in front of the needle, but it got me in the right area. By clicking on elements in the left tree view, I finally found 3 paths that made up the needle: the needle itself, its shadow, and the highlight for the needle. I changed the names of the elements to Needle, NeedleShadow, and NeedleHighlight to make them easier to access in code and saved the SVG. I would need to group the three paths to make it easier to rotate, but the SVGEditorFMX does not have a way to add a svg group yet so I loaded the SVG in NotePad++. I searched for “Needle” and quickly created a svg group around the three paths.
<g id="NeedleGroup"> <path id="Needle" style="fill:white" d="M391.31,343.02 L392.646,343.378 L363.659,465.898 L358.572,474.673 L355.17,473.761 L355.152,463.618 L391.31,343.018 Z "/> <path id="NeedleShadow" style="fill:black" d="M394.22,344.12 L359.114,475.14 L360.922,475.625 L366.004,466.866 L394.995,344.326 L394.219,344.118 Z "/> <path id="NeedleHighlight" style="fill:#babdb6" d="M392.22,342.62 L357.114,473.64 L358.922,474.125 L364.004,465.366 L392.995,342.826 L392.219,342.618 Z "/> </g>
Saving my work, my speedometer was almost ready for its first use…
Rotating the Speedometer Needle at Run-Time
I quickly created a test project to display and hopefully edit my SVG. I performed the following steps:
- Created a New VCL Project
- Dropped a TRSSVGDocument component on the form
- Set the RSSVGDocument1.Filename property to the velocimetro.svg file
- Dropped a TRSSVGImage component on the form
- Set the RSSVGImage1.SVGDocument to RSSVGDocument1
I now had my SVG displaying in a project. Now, I needed to edit the needle. For testing, I decided to use a TTrackBar to change the speed value. From looking at the SVG output, the author seemed to want the speed to go from -5 (?!) to 245. I needed to convert that value into a rotation angle for the needle group. Dropping a TTrackBar onto the form, I set its min and max, and created a OnChange event handler to find the needle group by its name and then apply a rotation matrix to it.
However, there were two problems. Firstly, rotation usually occurs around the origin which is most definitely not what we wanted here. I needed the pivot point or base of the needle. When I had examined the needle, I realized the bottom of the needle is in the bottom left. Thankfully, that is really easy to get. Every graphical element in the RSCL has a BoundsRect property. I would use the BoundsRect.BottomLeft as the pivot point. Secondly, by default the needle was not placed pointing left or right or top or bottom. It was at a slight angle, which I had to account for in the rotation angle. Also, I needed to constrain the angle so that the needle stopped at the bounds of the SVG. By experimentation, I figured out that the needle needed to be rotated counter-clockwise at the most to -109. Clockwise, the angle could not be greater than 78.
Finding the needle itself is very easy with the RSCL since I changed its ID. I just called the TSVGDocument.AllItems property with the name of the needle. Here is the final event handler:
procedure TfrmSpeedometer.TrackBar1Change(Sender: TObject); const LowGauge = -109; HiGauge = 187-LowGauge; var Needle: TSVGGraphicElement; NeedleAngle: Single; aRect: TSVGRect; begin Label1.Caption := TTrackBar(Sender).Position.ToString; // Get Needle Group to manipulate Needle := RSSVGDocument1.SVG.AllItems['NeedleGroup'] as TSVGGraphicElement; aRect := Needle.BoundsRect; // scale trackbar value into between Min and Max NeedleAngle := TTrackBar(Sender).Position; NeedleAngle := ((NeedleAngle-TTrackBar(Sender).Min)/(TTrackBar(Sender).Max-TTrackBar(Sender).Min))*(HiGauge-LowGauge)+LowGauge; // rotate around bottom left Needle.Matrix := CreateRotationRSMatrix(RSPoint(aRect.Left, aRect.Bottom), DegToRad(NeedleAngle)); end;
Running the application, it worked but there was horrible flicker. I set the form’s DoubleBuffered property to True and that fixed that issue.
However, there was one issue left. Annoyingly, it turned out that the SVG was not created with the needle properly centered. When the needle was a low number like 40, it would overlap the markers. However, when the needle was a high number like 220, the needle wouldn’t reach the markers. To fix this issue, I decided to create another group around our NeedleGroup, this group would translate (move in X and Y) the needle group over so everything would line up just so.
<g id="OuterNeedleGroup" transform="translate(5 -10)"> <g id="NeedleGroup"> <path id="Needle" style="fill:white" d="M391.31,343.02 L392.646,343.378 L363.659,465.898 L358.572,474.673 L355.17,473.761 L355.152,463.618 L391.31,343.018 Z "/> <path id="NeedleShadow" style="fill:black" d="M394.22,344.12 L359.114,475.14 L360.922,475.625 L366.004,466.866 L394.995,344.326 L394.219,344.118 Z "/> <path id="NeedleHighlight" style="fill:#babdb6" d="M392.22,342.62 L357.114,473.64 L358.922,474.125 L364.004,465.366 L392.995,342.826 L392.219,342.618 Z "/> </g> </g>
I ran the project, and moved the Track bar left and right. The needle smoothly updated from -5 to 245 and nicely centered. Fantastic! The hard part was done.
Customizing the Speedometer
Besides the problems with the needle, this SVG turned out to be a very good SVG for customization. Everything was nicely ordered if not identified well. By spelunking around with the SVG Editor, I identified and labeled the other parts of the speedometer such as the frame, the backface and the markers. About the only limitation with this SVG is that the author didn’t use text elements for the speed labels (40, 80, 120, etc). Instead, the author used paths so I cannot change those labels easily. I also ended up changing the IDs for the gradients (e.g., linearGradientFrame) to more easily identify and find them later.
After I finished identifying all the parts of the speedometer, I wanted to test it out. I dropped a TColorBox on my form and set up an event handler to change the color of the needle. The only trick is that I needed to change the needle’s color and the needle’s highlight:
procedure TfrmSpeedometer.ColorBox2Change(Sender: TObject); var Needle: TSVGGraphicElement; NewColor: TSVGColorRec; begin NewColor := (Sender as TColorBox).Selected; RSSVGDocument1.SVG.BeginUpdate; try // get needle Needle := RSSVGDocument1.SVG.AllItems['Needle'] as TSVGGraphicElement; // change its color Needle.Brush.Color := NewColor; // dim colors NewColor.R := trunc(NewColor.R * 0.5); NewColor.G := trunc(NewColor.G * 0.5); NewColor.B := trunc(NewColor.B * 0.5); // get highlight or shaded portion Needle := RSSVGDocument1.SVG.AllItems['NeedleHighlight'] as TSVGGraphicElement; // change its color Needle.Brush.Color := NewColor; finally RSSVGDocument1.SVG.EndUpdate; end; end;
One important note about the above code. By default, SVG elements inherit their color from their parent element. Usually, when you write Needle.Brush, what you are getting is a customized copy of the parent’s Brush. Setting the brush’s color will not actually change anything as the value is still inherited. In that case, you need to turn off the inheritance of the fill value before setting the fill value, like so: Needle.Inherits.Fill := False; However, in this case, the needle’s color is not inherited and we can just set the Needle.Brush without worry.
Running the project again, I could now change the needle’s color.
Changing the backface or the frame turned out to be slightly more challenging. For the backface, the fill color is a gradient (e.g., <path id=”backface” style=”fill:url(#linearGradientBackface)”…). You have to change the color of the gradient (more specifically, the gradient stops) in order to change the color of the backface. After doing that, you need to force the backface to update its color. Calling the Reset method will mark every resource (i.e., brush, pen, and font as dirty and make the SVG element regenerate the resources when needed.
Dropping another TColorBox on the form, here is the backface OnChange event handler:
var Backface: TSVGLinearGradient; NewColor: TSVGColorRec; i: Integer; begin NewColor := (Sender as TColorBox).Selected; RSSVGDocument1.SVG.BeginUpdate; try // get Backface gradient highlight Backface := RSSVGDocument1.SVG.AllItems['linearGradientBackfaceHighlight'] as TSVGLinearGradient; for i := 0 to Backface.Items.Count - 1 do (Backface.Items[i] as TSVGGradientStop).StopBrush.Color := NewColor; // get Backface gradient Backface := RSSVGDocument1.SVG.AllItems['linearGradientBackface'] as TSVGLinearGradient; (Backface.Items as TSVGGradientStop).StopBrush.Color := NewColor; // dim colors NewColor.R := trunc(NewColor.R * 0.5); NewColor.G := trunc(NewColor.G * 0.5); NewColor.B := trunc(NewColor.B * 0.5); (Backface.Items as TSVGGradientStop).StopBrush.Color := NewColor; // rebuild all brushes so that they ask for new gradients RSSVGDocument1.SVG.Reset; finally RSSVGDocument1.SVG.EndUpdate; end;
I did the same thing for the frame, the markers and text, and the glassy shine.
Finally, to show how easy it is to change other properties, I added checkboxes to turn off the visibility of the different elements. The check box handlers look like this:
procedure TfrmSpeedometer.CheckBox5Click(Sender: TObject); begin RSSVGDocument1.SVG.BeginUpdate; try (RSSVGDocument1.SVG.AllItems['backface'] as TSVGGraphicElement).Visible := (Sender as TCheckBox).Checked; (RSSVGDocument1.SVG.AllItems['backfaceHighlight'] as TSVGGraphicElement).Visible := (Sender as TCheckBox).Checked; finally RSSVGDocument1.SVG.EndUpdate; end; end;
The source for the project is here. The project includes a compiled version to play around with. If you clone the TSVGDocument, you can easily create multiple speedometers, each with their own color scheme changeable at run-time. And with a little more work to replace the path elements with true text elements (or a little more luck in finding a more customizable SVG), you could change the scale of the speedometer so it could be any analog gauge.
Well, that is it for today. I hope this blog post helps you more easily see the awesome possibilities of SVGs. There is a whole world wide web of free, customizable, scalable, and high-quality content out there. With the RiverSoftAVG SVG Component Library and a little prep work, you can easily use this content to really make your applications stand out.
Until next time, Happy CodeSmithing!