1

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?

0

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, I'll have a look at this
0

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

4 Comments

But this doesn't circumvent the actual problem. The problem is that most wx methods are not thread safe and so you have to post an event for the main thread loop to handle or risk thrashing the state of the gui used in your main thread. You may just be lucky that the call to self.staticText.SetLabel() in your worker thread does not cause fatal / undefined behavior. Check out this article: blog.pythonlibrary.org/2010/05/22/wxpython-and-threads
Actually in this case, the staticText object is accessed by only a single thread at any given time. Even the main thread does not modify this object. It is used to only display the number of lines in a file. Since I'm aborting the previous thread before creating a new one, the issue of thread safety does not crop up. I would have used the wx.CallAfter and PyPubSub if it was accessed by multiple threads.
YOU may not be interacting with the static text widget in your main thread but if the static text widget is onscreen at all while the worker thread is operating (and possibly even if it is not) it definitely can be accessed by the main thread's event loop. For example, if you resized your window the main thread would process this size event by traversing through the widget tree and updating all registered widgets (which your static text must be if you have it onscreen). If this were to happen while the worker thread was interacting with it, this could cause thrashing.
Now that you mention it, my program doesn't support any resizing. Also setting the label of a staticText will hardly cause it to crash. I guess I'll take some liberty in that! If any problem crops up, there's always pyPubSub..

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.