Search code examples
pythonwxpython

placing values on TextCtrl in subthread cause doesn't always work and cause random segmentation faults


I'm trying to create a gui application using wxpython and I got some issues with the TextCtrl element. The effect that I am trying to achieve is that the user will enter a command to a text field (command) and the command might pop a message that appears in the `(out) field. After some time (0.7 seconds on this example) the message will get back to a default message ("OutPut"). I have two problems:

  1. The message doesn't always appear.
  2. The program sometime crashes due to a segmentation fault and I don't get any Error message to handle that.

I guess that the two related in some way, but I don't know why. In the following example, I only type "test" and wait until the original message appear. Both problems happen on that scenario.

I post here two files that serve as smallest working example. File number 1, creates the GUI,

import wx
import os.path

import os

from threading import Thread
from time import sleep
from MsgEvent import *

class MainWindow(wx.Frame):
    def __init__(self):
        super(MainWindow, self).__init__(None, size=(400,200),)
#style=wx.MAXIMIZE)
        self.CreateInteriorWindowComponents()
        self.CreateKeyBinding()
        self.command.SetFocus()
        self.Layout()

    def Test(self):
        self.command.SetValue('open')
        self.ParseCommand(None)


    def PostMessage(self,msg):
        '''For its some reason, this function is called twice, 
        the second time without any input. I could'nt understand why. 
        For that, the test :if msg == None'''
        if msg == None: return 
        worker = MessageThread(self,msg,0.7,'OutPut')
        worker.start()

    def CreateKeyBinding(self):
        self.command.Bind(wx.EVT_CHAR,self.KeyPressed)

    def KeyPressed(self,event):
        char = event.GetUniChar()
        if char == 13 and not event.ControlDown(): #Enter
            if wx.Window.FindFocus() == self.command:
                self.ParseCommand(event)
        else:
            event.Skip()

    def ParseCommand(self,event):
        com = self.command.GetValue().lower() #The input in the command field

        self.PostMessage(com)

    def CreateInteriorWindowComponents(self):
        ''' Create "interior" window components. In this case it is just a
            simple multiline text control. '''
        self.panel = wx.Panel(self)

        font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
        font.SetPointSize(12)

        self.vbox = wx.BoxSizer(wx.VERTICAL)

        #Out put field
        self.outBox = wx.BoxSizer(wx.HORIZONTAL)
        self.out = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.BORDER_NONE)
        self.out.SetValue('OutPut')
        self.out.SetFont(font)
        self.outBox.Add(self.out,proportion=1,flag=wx.EXPAND,border=0)
        self.vbox.Add(self.outBox,proportion=0,flag=wx.LEFT|wx.RIGHT|wx.EXPAND,border=0)
        #Setting the backgroudn colour to window colour
        self.out.SetBackgroundColour(self.GetBackgroundColour())


        #Commands field
        self.commandBox = wx.BoxSizer(wx.HORIZONTAL)
        self.command = wx.TextCtrl(self.panel, style=wx.TE_PROCESS_ENTER)
        self.command.SetFont(font)
        self.commandBox.Add(self.command, proportion=1, flag=wx.EXPAND)
        self.vbox.Add(self.commandBox, proportion=0, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=0)

        self.panel.SetSizer(self.vbox)
        return




    #Close the window
    def OnExit(self, event):
        self.Close()  # Close the main window.





app = wx.App()
frame = MainWindow()
frame.Center()
frame.Show()
app.MainLoop()

And file number 2, called MsgThread.py handle the events.

import wx
import threading 
import time

myEVT_MSG = wx.NewEventType()
EVT_MSG = wx.PyEventBinder(myEVT_MSG,1)

class MsgEvent(wx.PyCommandEvent):
    """ event to signal that a message is ready  """
    def __init__(self,etype,eid,msg='',wait=0,msg0=''):
        """ create the event object """
        wx.PyCommandEvent.__init__(self,etype,eid)
        self._msg = unicode(msg)
        self._wait_time = wait
        self._reset_message = unicode(msg0)

    def GetValue(self):
        """ return the value from the event """
        return self._msg



class MessageThread(threading.Thread):
    def __init__(self,parent,msg='',wait=0,msg0=''):
        """ 
        parent - The gui object that shuold recive the value
        value - value to handle
        """
        threading.Thread.__init__(self)
        if type(msg) == int:
            msg = unicode(msg)
        self._msg = msg
        self._wait_time = wait
        self._reset_message = msg0
        self._parent = parent
        print self._msg
    def run(self):
        """ overide thread.run Don't call this directly, its called internally when you call Thread.start()"""
        self._parent.out.SetValue(unicode(self._msg))
        time.sleep(self._wait_time)
        self._parent.out.SetValue(self._reset_message)
        self._parent.MessageFlag = False
        event = MsgEvent(myEVT_MSG,-1,self._msg)
        wx.PostEvent(self._parent,event)

What is faulty?


Solution

  • WX Python is not thread safe except for 3 functions (wx.CallAfter, wx.CallLater, wx.PostEvent)

    So basically what you have to do is ensure you never call any widget directly from within the subthread. You may use events or CallAfter.

    You also might want to check this:

    Nice writeup about threads and wxpython

    This might help as well

    Lokla