Drawing of a beautiful speedmeter

Hello all,

I would to create a speedmeter custom widget with wxPython for a GUI like the presented on the picture below:
image

I would like to know if it is possible to draw the widget with wxPython including gradient and light effect or the best way to create it.
Thank you for your help.

Hi and welcome to Discuss wxPython,

wxPython has the wx.lib.agw.speedmeter.py module which can do something similar. If it is not suitable itself, you may be able to create your own module based on how it works. See: wx.lib.agw.speedmeter — wxPython Phoenix 4.2.1 documentation

Here are some examples from the wxPython demo that demonstrate it’s capabilities:

Thank you for your reply.
I have already seen this module and I think you are right and I can use it as base of my own module. However, I want to have a more beautiful widget but I don’t now how I have to draw the widget in wxPyhton.
Do you know if some tutorials exist to draw the widget with gradient and light effect in wxPython?

The wx.lib.agw.artmanager.py provides the ArtManager class which contain several methods for drawing gradients. See: wx.lib.agw.artmanager.ArtManager — wxPython Phoenix 4.2.1 documentation

Perhaps it might provide some ideas?

In your example image the outer ring of the speedmeter gets lighter at the left and right edges. I had a play with the GraphicsContext class to see if I could create something similar.

The code below is a quick hack that just draws the outer ring that fades in a similar manner. It might provide some ideas.

I have only tested it using Python 3.10.12 + wxPython 4.2.1 gtk3 (phoenix) wxWidgets 3.2.2.1 on Linux Mint 21.2

Screenshot at 2023-10-20 16-32-08

import math
import wx

class GradientRing(wx.Control):
    def __init__(self, parent, inner_radius, outer_radius, inner_colour, outer_colour):
        super(GradientRing, self).__init__(parent, style=wx.NO_BORDER)

        self.inner_radius = inner_radius
        self.outer_radius = outer_radius
        self.inner_colour = inner_colour
        self.outer_colour = outer_colour

        self.Bind(wx.EVT_PAINT, self.OnPaint)


    def DoGetBestSize(self):
        return wx.Size(self.outer_radius*2, self.outer_radius*2)


    def OnPaint(self, _event):
        gcdc = wx.GCDC(wx.PaintDC(self))
        gc = gcdc.GetGraphicsContext()

        pen = gc.CreatePen(wx.TRANSPARENT_PEN)
        gc.SetPen(pen)

        x1 = self.outer_radius
        y1 = self.outer_radius

        # Draw right segment
        grad_brush = gc.CreateLinearGradientBrush(x1, y1,
                                                  x1*2, y1,
                                                  self.inner_colour,
                                                  self.outer_colour)
        gc.SetBrush(grad_brush)

        path = gc.CreatePath()
        path.AddArc(x1, y1, self.outer_radius, math.radians(270), math.radians(90), True)
        path.AddArc(x1, y1, self.inner_radius, math.radians(270), math.radians(90), True)
        gc.DrawPath(path)

        # Draw left segment
        grad_brush = gc.CreateLinearGradientBrush(x1, y1,
                                                  0, y1,
                                                  self.inner_colour,
                                                  self.outer_colour)
        gc.SetBrush(grad_brush)

        path = gc.CreatePath()
        path.AddArc(x1, y1, self.outer_radius, math.radians(270), math.radians(90), False)
        path.AddArc(x1, y1, self.inner_radius, math.radians(270), math.radians(90), False)
        gc.DrawPath(path)


class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)

        vsizer = wx.BoxSizer(wx.VERTICAL)
        colour_1 = wx.Colour(0, 0, 0)
        colour_2 = wx.Colour(120, 120, 120)
        ring = GradientRing(self, 90, 100, colour_1, colour_2)
        vsizer.Add(ring, 0, 0, 0)
        self.SetSizer(vsizer)


class MyFrame(wx.Frame):
    def __init__(self, parent):
        super(MyFrame, self).__init__(parent)
        self.SetSize((250, 250))
        self.panel = MyPanel(self)


if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame(None)
    frame.Show()
    app.MainLoop()
1 Like

I’ve done this sort of a thing in OpenGL before. I had one bitmap for the gauge and one for the little tick marker that spins around. The marker had an alpha channel so it could be drawn on top.

So, all you really need are two pretty bitmaps (or one bitmap like the one you posted above, that you split in two with PhotoShop / GIMP) and the ability to add a rotation to the top image. If you don’t want OpenGL, a wx.GraphicsContext probably has some rotation support.

2 Likes

Thanks for the excellent sample code, Richard.
It is surprisingly easy to draw a gradient arc line.

You’re drawing the left and right lines separately, but I noticed that it can simply be done by one code:

        path = gc.CreatePath()
        path.AddArc(x1, y1, self.outer_radius, 0, math.pi*2, True)
        path.AddArc(x1, y1, self.inner_radius, 0, math.pi*2, True)
        gc.DrawPath(path)

The first thing that came to mind was the speedometer background and a needle drawn on the bitmap DC. But, I think @avose idea of using OpenGL would be great if you could make a 3D needle model. :wink:

1 Like

This might help. Conical gradient base code.

Everything is a curve or conical in the sense it is a curve…

1 Like

Everything you will need to know about how to make your speedometer is going to be in the code located in this repository.

It produces the same knob seen in this video.

The video is of a different graphics library called LVGL which is made for microcontrollers but the results are the same. You have to excuse the blurriness in the video as that is being caused by the screen capture program.

2 Likes

@kdschlosser I do hope you don’t mind, I’ve taken your excellent VolumeKnob and added a few bells and whistles. You know what they say about the Devil and idle hands and in my defence, it’s been raining for 2 days.
This amended code now provides a volume knob or a speedometer, with a few extra options.
I’ve added your copyright, if there’s an issue let me know.

knobdemo_speedo.py (4.0 KB) knob.py (45.3 KB)

Bug Fix Update:
Amended Version 1.1
Bug fix for incorrect Unbind of the odometer update timer
Addition of a pointer spine
Addition of variable ShowMinMax values
Addition of variable OdometerColour

knob.py (47.1 KB) knobdemo_speedo.py (5.2 KB)

1 Like