I was creating my application using GTK but I was getting some issues so I decided to change to wxPython. The thing is that in GTK there was a Spinner widget (window) and now I want to have a similar spinner loading animation in wxPython.
I tried using a GIF but the GIF I found looked pixelated and I wasn’t able to resize it. Maybe I could use a wx.Bitmap or some HTML/CSS?
GIF example:
HTML/CSS example:
Using wx.html2.WebView
I got this working:
import wx
import wx.html2
LOADING_SPINNER_PAGE = """
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
.loading-spinner {{
width: {width}px;
height: {height}px;
border: {border_width}px solid {border_color};
border-radius: 50%;
border-top-color: {spinner_color};
animation: spin {spinner_speed}s ease infinite;
}}
@keyframes spin {{
to {{ transform: rotate(360deg); }}
}}
</style>
</head>
<body style='width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 999; background-color: {background_color};'>
<div class="loading-spinner"></div>
</body>
</html>
"""
def create_spinner_web_view(
width: int=None,
height: int=None,
border_width: int=7,
border_color: str="rgba(230, 230, 230, .3)",
spinner_color: str="#3dc9e5",
spinner_speed: int=1,
background_color: str="#ffffff",
**kwargs
):
"""Spinner loading animation made with HTML and CSS in a wx.html2.WebView.
Parameters:
width (int=None): The width of the spinner.
height (int=None): The height of the spinner.
border_width (int=None): The width of the border.
border_color (str=None): The color of the border.
spinner_color (str=None): The color of the spinner.
spinner_speed (int=None): The speed of the spinner.
background_color (str=None): The backgound color of the whole page.
Notes:
If width is passed but not height, height will be equal to width (same with height).
By default both, widht and height, are 45.
Return:
A wx.html2.WebView.
"""
def on_show(event):
# Resize the parent (in this case the frame) to adjust the spinner
# This was just tested when the direct parent of the spinner is a wx.Frame
# In other cases this may not work.
parent = web_view.GetParent()
parent.SetSize(parent.GetSize().Width + 1, parent.GetSize().Height)
parent.SetSize(parent.GetSize().Width - 1, parent.GetSize().Height)
if width is not None and height is None:
height = width
elif height is not None and width is None:
width = height
else:
if width is None:
width = 45
if height is None:
height = 45
web_view = wx.html2.WebView.New(size=(width + 40, height + 40), **kwargs)
web_view.Bind(wx.EVT_SHOW, on_show)
web_view.SetPage(
html=LOADING_SPINNER_PAGE.format(
width=width,
height=height,
border_width=border_width,
border_color=border_color,
spinner_color=spinner_color,
spinner_speed=spinner_speed,
background_color=background_color
),
baseUrl="" # Emtpy baseUrl
)
return web_view
class MyFrame(wx.Frame):
def __init__(self, **kwargs):
super().__init__(**kwargs)
sizer = wx.BoxSizer(wx.VERTICAL)
self.button = wx.Button(self, label="Start loading")
self.button.Bind(wx.EVT_BUTTON, self.on_click)
self.spinner = create_spinner_web_view(
parent=self,
background_color=self.GetBackgroundColour().GetAsString(wx.C2S_HTML_SYNTAX),
width=40,
border_width=7 # The example GIF uses border_width 10
)
self.spinner.Hide()
sizer.Add(self.button)
sizer.Add(self.spinner)
self.SetSizer(sizer)
self.Show()
def on_click(self, event):
if self.spinner.IsShown():
self.button.SetLabel("Start Loading")
self.spinner.Hide()
else:
self.button.SetLabel("Stop Loading")
self.spinner.Show()
if __name__ == "__main__":
app = wx.App()
frame = MyFrame(parent=None, title="Spinner example")
app.MainLoop()
Realize that setting background_color
parameter in create_spinner_web_view
as your wx.Frame
background color is very important because it simulates transparency, otherwise you will get a white square with the spinner inside.
Limitations:
- It takes a few milliseconds to load when you open the window.
1 Like
I don’t know if this will work in a web app but in a .py script I use
wait = wx.BusyCursor()
.
.
.
del wait
I added a wx.ActivityIndicator control to a dialog recently to achieve a similar effect. One problem I encountered was that doing the processing required in the main thread stopped the indicator from rotating. When I moved the processing to a child thread it worked as expected.
1 Like
Who would know that it was called activity indicator?
Here's the previous example upgraded by using wx.ActivityIndicatior
import wx
class MyFrame(wx.Frame):
def __init__(self, **kwargs):
super().__init__(**kwargs)
sizer = wx.BoxSizer(wx.VERTICAL)
self.button = wx.Button(self, label="Start loading")
self.button.Bind(wx.EVT_BUTTON, self.on_click)
self.spinner = wx.ActivityIndicator(self, size=(100, 100))
sizer.Add(self.button)
sizer.Add(self.spinner)
self.SetSizer(sizer)
self.Show()
def on_click(self, event):
if self.spinner.IsRunning():
self.button.SetLabel("Start Loading")
self.spinner.Stop()
else:
self.button.SetLabel("Stop Loading")
self.spinner.Start()
if __name__ == "__main__":
app = wx.App()
frame = MyFrame(parent=None, title="Spinner example")
app.MainLoop()
2 Likes
Indeed! I only came across it because I was going through the wxPython Demo, looking for something else!
1 Like