How do I make wx.TextCtrl multi-line text update smoothly? - python

I'm working on an GUI program, and I use AppendText to update status in a multi-line text box(made of wx.TextCtrl). I noticed each time there's a new line written in this box, instead of smoothly adding this line to the end, the whole texts in the box just disappear(not in real, just visually) and I have to click the scroll button to check the newly updated/written status line. Why this happening? Should I add some styles? Hopefully you guys can help me out.
Here's my sample code:
import wx
import thread
import time
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent = None, id = -1, title = "Testing", pos=(350, 110), size=(490,530), style=wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX)
panel = wx.Panel(self)
self.StartButton = wx.Button(parent = panel, id = -1, label = "Start", pos = (110, 17), size = (50, 20))
self.MultiLine = wx.TextCtrl(parent = panel, id = -1, pos = (38, 70), size = (410, 90), style = wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_AUTO_URL)
self.Bind(wx.EVT_BUTTON, self.OnStart, self.StartButton)
def OnStart(self, event):
self.StartButton.Disable()
thread.start_new_thread(self.LongRunning, ())
def LongRunning(self):
Counter = 1
while True:
self.MultiLine.AppendText("Hi," + str(Counter) + "\n")
Counter = Counter + 1
time.sleep(2)
class TestApp(wx.App):
def OnInit(self):
self.TestFrame = TestFrame()
self.TestFrame.Show()
self.SetTopWindow(self.TestFrame)
return True
def main():
App = TestApp(redirect = False)
App.MainLoop()
if __name__ == "__main__":
main()

try this:
self.logs = wx.TextCtrl(self, id=-1, value='', pos=wx.DefaultPosition,
size=(-1,300),
style= wx.TE_MULTILINE | wx.SUNKEN_BORDER)
self.logs.AppendText(text + "\n")

Try calling the Refresh() method on the textCtrl
Update:
A question has already been asked regarding this problem, here is the answer which pretty much solves it, -its not perfect but maybe you can improve on it...
Here is a thread from the wxpython mailing list regarding the problem which may also be of interest to you.

Related

wxPython: ListBox not selectable/clickable when used with ComboCtrl

I am trying to couple wx.ListBox with wx.combo.Comboctrl. A sample code is below. For some reason, the items in the ListBox are not clickable/selectable. I wonder how I can make it work. Thanks!
EDIT: Missing code added
import wx, wx.combo
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, title="", size=(300, 100))
gbs = wx.GridBagSizer()
ComboCtrlBox = wx.combo.ComboCtrl(self)
ComboCtrlPopup = ListBoxComboPopup()
ComboCtrlBox.SetPopupControl(ComboCtrlPopup)
ComboCtrlPopup.ListBox.Append("Apple")
ComboCtrlPopup.ListBox.Append("Banana")
ComboCtrlPopup.ListBox.Append("Orange")
ComboCtrlPopup.ListBox.Bind(wx.EVT_LISTBOX, self.OnSelect) #ADDED
gbs.Add(ComboCtrlBox, pos = (0, 0), span = (1, 1), flag = wx.EXPAND|wx.ALL, border = 10)
gbs.AddGrowableCol(0)
self.SetSizer(gbs)
self.Layout()
def OnSelect(self, evt): #ADDED
print "HAHA"
class ListBoxComboPopup(wx.combo.ComboPopup):
def Init(self):
self.ItemList = []
def Create(self, parent):
self.ListBox = wx.ListBox(parent, -1, size = (-1, 20), choices = self.ItemList)
def GetControl(self):
return self.ListBox
def OnPopup(self):
pass
#-----------------------------------------------------------------------------#
if __name__ == '__main__':
APP = wx.App(False)
FRAME = MainFrame(None)
FRAME.Show()
APP.MainLoop()
You are missing a few things in your ListBoxComboPopup class needed to make it work well with the ComboCtrl. At a minimum you are missing some event binding and handler to catch selection events from the ListBox, and an implementation of the GetStringValue method which the combo will call to get the value to be displayed. Please see the ComboCtrl sample in the wxPython demo for more details and example code.

WxPython using Listbox and other UserInput with a Button

I am trying to create a web crawler based on specific user input. For example, the User Input I am trying to receive is from a ListBox and a text field. Once I have that information, I would like the user to click a button to start the search with the information collected.
This is where I have been getting problems. The EVT function doesn't recognize the listbox since its been linked to the Button evt. Is there a way to solve the problem? Can EVT information be shared with other EVTs?
Here is what I have so far:
import wx # for gui
class MyFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Title', size=(300,200))
tournLevel = ['$10,000', '$15,000', '$20,000', '$50,000','$75,000','$100,000']
levelBox = wx.ListBox(panel, -1, (40, 50), (90, 90), tournLevel)
levelBox.SetSelection(1) # default selection
checkButton = wx.Button(panel, label= "Check Now", pos = (150, 50), size = (90, 40))
self.Bind(wx.EVT_BUTTON, self.OnClick, checkButton)
def OnClick(self, event):
currLevel = event.GetSelection()
print(currLevel) # to test if GetSelection is working
if __name__ == '__main__':
app = wx.App()
frame = MyFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
I would be very happy if I could just get the button to recognize the ListBox results.
Thank you for your time!
You can just grab it from the listbox, you don't need it from the event. See below:
import wx # for gui
class MyFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Title', size=(300,200))
tournLevel = ['$10,000', '$15,000', '$20,000', '$50,000','$75,000','$100,000']
self.levelBox = wx.ListBox(panel, -1, (40, 50), (90, 90), tournLevel)
self.levelBox.SetSelection(1) # default selection
self.checkButton = wx.Button(panel, label= "Check Now", pos = (150, 50), size = (90, 40))
self.Bind(wx.EVT_BUTTON, self.OnClick, self.checkButton)
def OnClick(self, event):
currLevel = self.levelBox.GetSelection()
print(currLevel) # to test if GetSelection is working
if __name__ == '__main__':
app = wx.App()
frame = MyFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
More specifically, if you store levelBox as self.levelBox, it will be accessible inside the OnClick method as a MyFrame attribute. You can then use the GetSelection method for this object (not the event), which will get the current selection.
You can make levelBox into a property of the class by turning it into self.levelBox and accessing it that way as #brettb mentioned. However you can get a bit sneakier and do it using a lambda for your callback to pass the Listbox widget to the event handler:
import wx # for gui
class MyFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Title', size=(300,200))
panel = wx.Panel(self)
tournLevel = ['$10,000', '$15,000', '$20,000', '$50,000','$75,000','$100,000']
levelBox = wx.ListBox(panel, -1, (40, 50), (90, 90), tournLevel)
levelBox.SetSelection(1) # default selection
checkButton = wx.Button(panel, label= "Check Now", pos = (150, 50), size = (90, 40))
evt = lambda caller, widget=levelBox: self.OnClick(caller, widget)
checkButton.Bind(wx.EVT_BUTTON, evt)
def OnClick(self, event, widget):
currLevel = widget.GetSelection()
print(currLevel) # to test if GetSelection is working
print widget.GetString(currLevel)
if __name__ == '__main__':
app = wx.App()
frame = MyFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
Also note that you didn't have panel defined, so your original code doesn't work. See the following for more information:
http://wiki.wxpython.org/Passing%20Arguments%20to%20Callbacks

wxPython: show number repeatedly

I'm a very beginner of Python Programming, and now I'm wondering why my widgets cannot show number repeatedly.
That is, I'd like to let the text show numbers from 1 to 9, but in the while loop, it only shows 9....
Any suggestions?
Here is my code(Python version: 2.6):
#!/user/bin/python
import wx
class Frame(wx.Frame):
def __init__(self,parent,id):
wx.Frame.__init__(self, parent, id,
'Show Number',
size = (200,150),
style=wx.MINIMIZE_BOX | wx.RESIZE_BORDER
| wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX)
self.initUI()
def initUI(self):
widgetPanel=wx.Panel(self, -1)
widgetPanel.SetBackgroundColour('white')
# Buttons for play the simulation
playButton = wx.Button(widgetPanel, -1, "Play", pos=(10,10), size=(30,30))
self.Bind(wx.EVT_BUTTON, self.play, playButton)
playButton.SetDefault()
# Time
self.timeText = wx.TextCtrl(widgetPanel, -1, "", pos=(10, 50),
size =(100,30), style=wx.TE_CENTER)
self.timeText.SetBackgroundColour('white')
self.timeText.SetFont(wx.Font(20, wx.DECORATIVE, wx.NORMAL, wx.NORMAL))
def play(self, event):
#self.timeText.SetValue("19:32")
self.show_num()
def show_num(self):
x = 0
while(x < 10):
self.timeText.SetValue(str(x))
x += 1
if __name__ == "__main__":
app = wx.App(False)
frame = Frame(parent=None,id=-1)
frame.Show()
app.MainLoop()
you need to give your app the chance to update .... the easiest and most correct way of doing this is to use timers instead of a loop ... I have included a minimal example
import wx
app = wx.App(redirect=False)
frame = wx.Frame(None,-1,"Counter")
btn = wx.Button(f,-1,"Click Me!")
timer = wx.Timer() # this will update the count
def onUpdateButton(evt):
next_int = int(btn.GetLabel())+1
btn.SetLabel(str(next_int))
if next_int > 10:
timer.Stop()
btn.Enable(True)
def onButtonClick(event):
btn.SetLabel("0")
btn.Enable(False)
timer.Start(1000) # 1 second updates
timer.Bind(wx.EVT_TIMER,onUpdateButton)
btn.Bind(wx.EVT_BUTTON,onButtonClick)
frame.Show()
app.MainLoop()
it is probably obvious to most users, however may be worth mentioning that instead of keeping a variable with the current count i am just using the label string
Like the other answer said, you need to give time to your app to update.
I have made a few additions to your code to make it work:
import wx
import time
import thread
class Frame(wx.Frame):
def __init__(self,parent,id):
wx.Frame.__init__(self, parent, id,
'Show Number',
size = (200,150),
style=wx.MINIMIZE_BOX | wx.RESIZE_BORDER
| wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX)
self.initUI()
def initUI(self):
widgetPanel=wx.Panel(self, -1)
widgetPanel.SetBackgroundColour('white')
# Buttons for play the simulation
playButton = wx.Button(widgetPanel, -1, "Play", pos=(10,10), size=(30,30))
self.Bind(wx.EVT_BUTTON, self.play, playButton)
playButton.SetDefault()
# Time
self.timeText = wx.TextCtrl(widgetPanel, -1, "", pos=(10, 50),
size =(100,30), style=wx.TE_CENTER)
self.timeText.SetBackgroundColour('white')
self.timeText.SetFont(wx.Font(20, wx.DECORATIVE, wx.NORMAL, wx.NORMAL))
def play(self, event):
#self.timeText.SetValue("19:32")
#self.show_num()
thread.start_new_thread(self.show_num,())
def show_num(self):
x = 0
while(x < 10):
self.timeText.SetValue(str(x))
time.sleep(1)
x += 1
if __name__ == "__main__":
app = wx.App(False)
frame = Frame(parent=None,id=-1)
frame.Show()
app.MainLoop()
Notice that time.sleep(1) pauses the while loop for 1 second so that the display can be updated. You can reduce or increase the pause time as you desire.
I experienced an update issue with:
self.timeText.SetValue(str(x))
time.sleep(1)
x += 1
I added a wx.YieldIfNeeded() before the sleep:
self.timeText.SetValue(str(x))
wx.YieldIfNeeded()
wx.sleep(1)
x += 1
This makes sure the GUI is updated and other events are handled.
I also would use the wx.Sleep(). This prevents using an other library...

How to refresh listctrl simultaneously when using setstringitem function

I'm so troubled by this problem:
I've create a ListCtrl object, a TextCtrl object, and a button. First I fill some data into the ListCtrl object, when I press the button, it will append some strings into TextCtrl object and use SetStringItem to modify ListCtrl object.
As you can see in the button function, I've added time.sleep(2) in each loop. When I've got is when the button is pressed, TextCtrl will be refreshed every time the strings is inserted, but ListCtrl just freeze until the LOOP IS FINISHED, then it will display the correct strings.
I want to know how to refresh the ListCtrl object as soon as SetStringItem is called.
Any help is deeply appreciated.
Here is the code:
import wx
import sys
import time
class Frame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, size=(450, 450))
self.panel = wx.Panel(self)
self.dl = wx.ListCtrl(self,-1,size=(300,100),style=wx.LC_REPORT)
self.dl.InsertColumn(0, 'File')
self.dl.InsertColumn(1, 'Progress')
self.dl.InsertColumn(2, 'State')
for row in range(3):
labels = [l+str(row) for l in "FILE PERCENT STATE".split()]
# sys.maxint inserts at the end of the list
index = self.dl.InsertStringItem(sys.maxint, labels[0])
self.dl.SetStringItem(index, 1, labels[1])
self.dl.SetStringItem(index, 2, labels[2])
self.Show(True)
button2 = wx.Button(self, label=u"test", pos=(15, 200), size=(60, 25))
self.Bind(wx.EVT_BUTTON, self.test, button2)
self.text = wx.TextCtrl(self, -1, pos=(80, 200), size=(200, 175), style=wx.TE_MULTILINE)
def test(self,event):
for i in range(3):
self.dl.SetStringItem(i,1,"HELLO")
self.text.AppendText("HELLO")
time.sleep(2)
app = wx.App()
Frame(None)
app.MainLoop()
The problem is that time.sleep blocks your GUI, what you will need to do to get the effect that you are trying for is:
On your button press add the first item & start a 2 second wx.Timer with an event handler/
In the event handler add the next string or if there are no more to add cancel the timer.
I've changed my code to this, and it works, thanks steve
import wx
import sys
import time
class Frame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, size=(450, 450))
self.panel = wx.Panel(self)
self.dl = wx.ListCtrl(self,-1,size=(300,100),style=wx.LC_REPORT)
self.dl.InsertColumn(0, 'File')
self.dl.InsertColumn(1, 'Progress')
self.dl.InsertColumn(2, 'State')
for row in range(3):
labels = [l+str(row) for l in "FILE PERCENT STATE".split()]
# sys.maxint inserts at the end of the list
index = self.dl.InsertStringItem(sys.maxint, labels[0])
self.dl.SetStringItem(index, 1, labels[1])
self.dl.SetStringItem(index, 2, labels[2])
self.Show(True)
self.timer = wx.Timer(self,-1)
#self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
self.Bind(wx.EVT_TIMER, self.test1, self.timer)
button2 = wx.Button(self, label=u"test", pos=(15, 200), size=(60, 25))
self.Bind(wx.EVT_BUTTON, self.test, button2)
self.text = wx.TextCtrl(self, -1, pos=(80, 200), size=(200, 175), style=wx.TE_MULTILINE)
self.z=0
def test(self,event):
self.timer.Start(3000)
def test1(self,event):
for i in range(1):
self.dl.SetStringItem(self.z,1,"HELLO")
self.text.AppendText("HELLO")
self.z+=1
if self.z >2 :
self.timer.Stop()
app = wx.App()
Frame(None)
app.MainLoop()

How come this way of ending a thread is not working?

I just came out with my noob way of ending a thread, but I don't know why it's not working. Would somebody please help me out?
Here's my sample code:
import wx
import thread
import time
import threading
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent = None, id = -1, title = "Testing", pos=(350, 110), size=(490, 200), style=wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX)
self.panel = wx.Panel(self)
self.stop = False
self.StartButton = wx.Button(parent = self.panel, id = -1, label = "Start", pos = (110, 17), size = (50, 20))
self.MultiLine = wx.TextCtrl(parent = self.panel, id = -1, pos = (38, 70), size = (410, 90), style = wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_AUTO_URL)
self.Bind(wx.EVT_BUTTON, self.OnStart, self.StartButton)
self.Bind(wx.EVT_CLOSE, self.OnClose)
def OnStart(self, event):
self.StartButton.Disable()
self.NewThread = threading.Thread(target = self.LongRunning)
self.NewThread.start()
def OnClose(self, event):
self.stop = True
BusyBox = wx.BusyInfo("Just a moment please!", self)
wx.Yield()
while True:
try:
if not self.NewThread.isAlive():
self.Destroy()
break
time.sleep(0.5)
except:
self.Destroy()
break
def LongRunning(self):
Counter = 1
while True:
time.sleep(2)
print "Hello, ", Counter
self.MultiLine.AppendText("hello, " + str(Counter) + "\n") #If you comment out this line, everything works fine. Why can't I update the fame after I hit the close button?
Counter = Counter + 1
if self.stop:
break
class TestApp(wx.App):
def OnInit(self):
self.TestFrame = TestFrame()
self.TestFrame.Show()
self.SetTopWindow(self.TestFrame)
return True
def main():
App = TestApp(redirect = False)
App.MainLoop()
if __name__ == "__main__":
main()
As you can see in my code, there's a infinite loop in the thread, what I tell the thread to do is break out of the loop once I click the close button. But the problem is, every time when I hit the close button, it seems the code stuck at self.MultiLine.AppendText("hello, " + str(Counter) + "\n") line, I don't know why. Anybody can help?
Try using a thread safe method such as wx.CallAfter when updating your multiline.
def LongRunning(self):
Counter = 1
while True:
time.sleep(2)
print "Hello, ", Counter
wx.CallAfter(self.updateMultiLine, "hello, " + str(Counter) + "\n")
Counter = Counter + 1
if self.stop:
break
def updateMultiLine(self, data):
self.MultiLine.AppendText(data)
In general with GUI toolkits, only one thread should access GUI functions. An exception is wx.CallAfter
As you (should) know, software defects can be classified into three groups:
Your bugs.
Their bugs.
Threads.
;)

Categories