Loading Spinner Animation

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:
spinner


HTML/CSS example:

loading_spinner

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?
loading_spinner

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