user2963623
user2963623

Reputation: 2295

Using multiple threads with wxPython

I am writing a wxpython program. If you open a file in the program, it counts the number of lines in the file and displays it in a staticText Widget.

Here's the relevant code:

In class Frame

def on_select_file(self, event):
    '''Event handler for selecting file'''
    filepath = getFile()  # This is a seperate method which asks the user to select a file and returns the path
    threading.Thread(target = methods.count_lines, args = (filepath, self.staticText)).start()

methods.py

def count_lines(filepath, staticText):
    with open(filepath) as f:
        for i, _ in enumerate(f): pass
    staticText.SetLabel(str(i+1))

Most of the files I'm dealing with are very large (3-4 GB) with around 25 million lines. So if I open a large file, it takes a few tens of seconds to count and display the no. of lines. However if I select a large file and before the staticText widget is updated, open another file which is smaller, the staticText widget shows the new count, but after some time shows the count of the previous file. I understand this is because the previous thread was still running and updated the widget after it ended.

I tried working around it by passing a flag variable as a parameter to the counter function, to check if the widget has been updated. However it does not seem to work. Is there any other way to avoid this?

Upvotes: 1

Views: 1008

Answers (2)

user2963623
user2963623

Reputation: 2295

I modified the code in the Sir Digby Chicken Caesa's answer to suit my need. Rather than creating a custom event and having it fired after some time interval, I set up the line counting thread such that it could be aborted every time a file open event occurs and a previous instance of this thread is still running.

Here's a short example of the implementation:

import threading.Thread

class Line_Counter(threading.Thread):
    def __init__(self, staticText, filename):
        threading.Thread.__init__(self)
        self.staticText = staticText

    def run(self):
        self.exit = False
        with open(self.filename) as f:
            if self.exit:
                return
            for count, _ in enumerate(f):   pass
        self.staticText.SetLabel(str(count + 1))

    def abort(self):
        self.exit = True


class MainFrame(wx.Frame):
    def __init__(self, parent, id, filename):
        wx.Frame.__init__(self, parent, id, 'Threaded File Loader')
        self.line_counter = False      # Set up a dummy thread variable
        #### remaining code
        self.file_open_button.Bind( wx.EVT_BUTTON, self.OnOpenFile ) # Event handler for opening file

    def OnOpenFile(self, event):
        filepath = getFile()  # Creates a file open dialog and returns the file path
        if self.line_counter: # Checks if a counter thread is already running
            self.line_counter.abort() # Stops the thread if True
        self.line_counter = Line_Counter(self.staticText, filename).start()  # Starts a new thread

Upvotes: 0

Sir Digby Chicken Caesar
Sir Digby Chicken Caesar

Reputation: 3123

Essentially, create a custom event type that contains the current line count as a member, and in your worker thread periodically post an event of that type to the class that contains your static text widget using wx.PostEvent(). Then, when the main thread resumes and processes its event loop, you can use the line count reported by the received event(s) to set the text string.

Something like this should work:

import time
from threading import *
import wx
import os.path

EVT_LINE_NUMBER_UPDATE_ID = wx.NewId()

class LineNumberUpdateEvent(wx.PyEvent):
    def __init__(self, lineNum):
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_LINE_NUMBER_UPDATE_ID)
        self.lineNumber = lineNum

class WorkerThread(Thread):
    def __init__(self, notify_window, filename):
        Thread.__init__(self)
        self._notify_window = notify_window
        self._filename = filename
        self.start()

    def run(self):
        with open(this._filename,"r") as file:
            count = 0;
            for line in file:
                 count++
                 if count % 50 == 0: # post an event every 50 lines
                     wx.PostEvent(self._notify_window, LineNumberUpdateEvent(count))
            wx.PostEvent(self._notify_window, LineNumberUpdateEvent(count)) # last event

class MainFrame(wx.Frame):
    def __init__(self, parent, id, filename):
        wx.Frame.__init__(self, parent, id, 'Threaded File Loader')
        self.status = wx.StaticText(self, -1, '', pos=(0,100))
        self.Bind(EVT_LINE_NUMBER_UPDATE_ID, self.OnLineNumberUpdate)
        if (os.path.isfile(filename))
            self.worker = WorkerThread(self,filename)

    def OnLineNumberUpdate(self, event):
        self.status.SetLabel(str(event.lineNumber))

This was adapted from an example posted on the wx Wiki:

http://wiki.wxpython.org/LongRunningTasks

Upvotes: 3

Related Questions