Example of creating an animated PyQt three-state toggle button like the one used in the Windscribe desktop app
The ToggleButton
is a button that can be in 3 different states:OFF
, TURNING_ON
, and ON
.
- By default, it is
OFF
and just looks like a white filled circle with an upside-down power symbol in the center. - If the button is clicked, it changes state to
TURNING_ON
. The icon rotates by 180° with an animation, so it's not upside-down anymore and two animated spinning half circles appear on the outside of the white circle. - This is where some turning on functionality is executed depending on your application and once that is finished,
you set the state of the button to
ON
. This will turn the spinning half circles to a full circle in a different color to indicate that the button is now turned on. - If the button is clicked again, the button changes state to
OFF
. The full circle fades out and the icon rotates back to its original position with an animation.
The base class of the ToggleButton
is QWidget
. At the top, I define the signals clicked
and stateChanged
that will be emitted later when the button is clicked or its state changes.
I also define static color constants that will be used later for the color of the outer circle.
In the __init__()
method, I start by defining attributes like the state
that is set to OFF
by default.
(The 3 possible states of the button are contained in an enum called ToggleButtonState
).
Then I create multiple instances of QTimeLine
, which will be used to create the different animations:
- Outer circle rotation: Frame range of
0-170
because each half circle has a span of 170° - Outer circle width: Frame range of
0-40
to animate a width from 0.0 to 4.0 with a step of 0.1 - Outer circle opacity: Frame range of
0-255
to animate the alpha value of an RGBA color - Icon rotation: Frame range of
0-180
since the icon rotates by 180°
The timelines call the update()
method every frame change to trigger the paintEvent()
which redraws the button.
They can be started in forward and backward mode depending on if the button is toggled on or off.
Since the outer circle rotation timeline should run infinitely until the button is either turned on or off,
it's set to restart every time it finishes.
EXAMPLE:
If the button is currently turned off and the state changes toTURNING_ON
, the timelines are started in forward mode, so the icon rotation timeline would start at 0 and end at 180 with a step of 1. These values can then be used in thepaintEvent()
to calculate the angle to draw the icon at, which creates an animation.
As mentioned before, all the visual elements are drawn in the overridden paintEvent()
method, where the
QPainter
is initialized and the render hint is set to Antialiasing
for better quality.
- The first element that is drawn is the filled white circle, the main element of the button.
For that, I simply use the
drawEllipse()
method of the painter, passing the center of the button and a radius, in this case 35. To fill the circle, I set aQBrush
instead of aQPen
.
After that, the power icon is drawn onto the white circle. It's made up of a straight line and an arched line that are drawn separately:
-
The straight line is drawn between two points, the center and a second point that depends on the angle at which the icon is to be drawn. If the button is turned off and the timeline value is 0, this is 90°, meaning the second point is directly below the center. If the timeline value is 90 (animation half done), the second point would be directly left of the center at an angle of 180°. The painter method used to draw the line is
drawLine()
and a pen withPenCapStyle.RoundCap
is used to make the line rounded at the ends.
The static methodget_point_on_circle()
from theUtils
class is used to calculate the second point based on the center point, line length, and angle in degrees. -
The arched line is drawn with the
drawArc()
method of the painter withPenCapStyle.FlatCap
for flat line ends. It takes aQRectF
, a start angle, and a span angle as parameters. The rect is simply made up of a width and height that determine the space the arched line will take up, and x and y offsets used to center the rect on the button. The line has a span angle of 326° and an initial angle of -73°, meaning the circle has a gap at the bottom to form the classic power symbol. The timeline value for the icon rotation gets subtracted from the initial angle, so when the timeline value is 180 (animation done), the angle would be -253°, meaning the circle has rotated by 180° and the gap is now at the top.
The last step is drawing either two half circles, one full circle, or no circle, depending on the state of the button:
-
The half circles are also drawn with the
drawArc()
method of the painter. AQRectF
is once again passed along with a start angle and a span angle. The span angle is 170° for both half circles and the start angle is -95° for the first once and 85° for the second one. The value of the outer circle rotation timeline (0-170) is also subtracted which creates an infinite spinning animation if the timeline is running.
The width of the circle is animated by setting the width of theQPen
to the value of the timeline for the outer circle width divided by 10.QTimeLine
can only handle integer values and I wanted an animation from 0.0 to 4.0 with a step of 0.1, so using a frame range of 0-40 and then dividing by 10 is necessary.
The color opacity of the circle is animated by setting the alpha value of the RGBA color to the value of the timeline for the outer circle opacity. So if the animation starts in forward mode, the color will fade in until it has reached full opacity and if the animation starts in backward mode, the color will fade out from full opacity. -
The full circle is once again drawn with the
drawArc()
method of the painter, similarly to the half circles. The only difference is, that start angle is 0° and the span angle is 360° for a full circle.
There is a setState()
method to set the state of the button, which handles starting the timelines
in the right mode to show the correct animations.
This method is also used in the overridden mousePressEvent()
to toggle the state of the button with every left click.
This software is licensed under the MIT license.