Search code examples
pythonwxpythonpython-multithreading

How do I update a wxPython widget from a thread?


I have a wxPython frame and I wish to periodically access a web page and update a widget on the frame with a value.

I know that there is wx.Timer, but I am already using that to update another widget.

My attempt (below) crashes with:

Pango:ERROR:/build/pango1.0-Ne8X8r/pango1.0-1.40.1/./pango/pango-layout.c:3925:pango_layout_check_lines: assertion failed: (!layout->log_attrs)

This is the code that I am using:

import datetime
import wx
import requests
from bs4 import BeautifulSoup
from threading import Thread, Event


class MainFrame(wx.Frame):
    def __init__(self):
        super(MainFrame, self).__init__(None)
        self.Bind(wx.EVT_CLOSE, self.on_quit_click)
        self.Size = (400, 200)

        self.panel = MainPanel(self)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel)
        self.SetSizer(sizer)
        self.Center()
        self.Show()

        self._on_clock_timer(None)
        self.clock_timer = wx.Timer(self, -1)
        self.clock_timer.Start(1000)
        self.Bind(wx.EVT_TIMER, self._on_clock_timer)

        self.stop_flag = Event()
        thread = ScraperThread(self, 1, self.stop_flag)
        thread.start()

    def _on_clock_timer(self, event):
        date_text = datetime.datetime.now().strftime('%a, %d %b %Y, %H:%M:%S')
        self.SetTitle(date_text)

    def on_quit_click(self, event):
        del event
        self.stop_flag.set()
        wx.CallAfter(self.Destroy)


class ScraperThread(Thread):
    def __init__(self, parent, period, event):
        super(ScraperThread, self).__init__()
        self.stopped = event
        self.period = period
        self.parent = parent

    def run(self):
        while not self.stopped.wait(self.period):
            response = requests.get('http://google.com')
            soup = BeautifulSoup(response.text, 'html.parser')
            all_tags = soup.find_all('span')
            for tag in all_tags:
                if 'Advertising' in tag.text:
                    print(tag.text[:50])
                    self.parent.panel.lbl_value.SetLabel(tag.text[:50])


class MainPanel(wx.Panel):
    def __init__(self, parent):
        super(MainPanel, self).__init__(parent)
        self.lbl_value = wx.StaticText(self, label='value')
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.lbl_value, flag=wx.ALL|wx.ALIGN_CENTER, border=5)
        self.SetSizer(sizer)


if __name__ == '__main__':
    wx_app = wx.App()
    MainFrame()
    wx_app.MainLoop()

Solution

  • I'm guessing you will need something like this:

    class MainFrame(wx.Frame):
        def __init__(self):
            # previous code up to self.Show()
    
            self._on_clock_timer(None)
            self.clock_timer = wx.Timer(self, -1)
            self.clock_timer.Start(1000)
            self.Bind(wx.EVT_TIMER, self._on_clock_timer, self.clock_timer)
    
            self.scrape_timer = wx.Timer(self, -1)
            self.scrape_timer.Start(1000)
            self.Bind(wx.EVT_TIMER, self._on_scrape, self.scrape_timer)
    
        def _on_scrape(self, event):
            response = requests.get('http://google.com')
            soup = BeautifulSoup(response.text, 'html.parser')
            all_tags = soup.find_all('span')
            for tag in all_tags:
                if 'Advertising' in tag.text:
                    print(tag.text[:50])
                    self.panel.lbl_value.SetLabel(tag.text[:50])
    

    You can then remove ScraperThread

    The only thing now is that you need to be sure that _on_scrape() finishes quickly else you will build up a backlog of timer events.

    Alternatively you could do: self.scrape_timer.Start(1000, oneShot = True) and have _on_scrape() also finish with this line so that there would be a one second delay between each http request.