I want my wxPython application to trigger an event when a text entry widget loses focus. I've followed the tutorial here, which describes using wx.EVT_KILL_FOCUS. However, I'm getting unexpected behavior.
The following code works fine:
import wx
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Focus Tutorial 1a")
panel = wx.Panel(self, wx.ID_ANY)
txt = wx.TextCtrl(panel, wx.ID_ANY, "")
txt.Bind(wx.EVT_KILL_FOCUS, self.onTextKillFocus)
"""
This next line seems to be important for working correctly,
but I don't understand why:
"""
txt.Bind(wx.EVT_SET_FOCUS, self.onTextFocus)
btn = wx.Button(panel, wx.ID_ANY, "Test")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(txt, 0, wx.ALL, 5)
sizer.Add(btn, 0, wx.ALL, 5)
panel.SetSizer(sizer)
def onTextFocus(self, event):
print "text received focus!"
def onTextKillFocus(self, event):
print "text lost focus!"
if __name__ == '__main__':
app = wx.App()
frame = MyForm().Show()
app.MainLoop()
When I tab back and forth from the text control to the button, or click in or out of the text control, I get the focus messages I expect.
However, when I make the following (reasonable?) edit, things go south:
import wx
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Focus Tutorial 1a")
panel = wx.Panel(self, wx.ID_ANY)
txt = wx.TextCtrl(panel, wx.ID_ANY, "")
txt.Bind(wx.EVT_KILL_FOCUS, self.onTextKillFocus)
"""
This next line seems to be important for working correctly,
but I don't understand why:
"""
## txt.Bind(wx.EVT_SET_FOCUS, self.onTextFocus)
btn = wx.Button(panel, wx.ID_ANY, "Test")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(txt, 0, wx.ALL, 5)
sizer.Add(btn, 0, wx.ALL, 5)
panel.SetSizer(sizer)
def onTextFocus(self, event):
print "text received focus!"
def onTextKillFocus(self, event):
print "text lost focus!"
if __name__ == '__main__':
app = wx.App()
frame = MyForm().Show()
app.MainLoop()
The unexpected behavior is that when I try to change focus from the text box to anything else (tabbing or mouse clicks), the 'text lost focus!' message prints once, and never again, and I can no longer edit the contents of the text control.
Is this expected behavior? If not, what am I doing wrong?
Python version 2.7, wxPython version 3.0.0.0, Windows 7 64-bit
As explained in wxEvent::Skip() documentation, you should almost invariably call it for non-command events as you don't want to prevent the default handling from taking place.
Related
I would like to make a wxpython program that has a notification center just like the one on windows or mac. Whenever I have a message, the message will show inside the the notification panel, and the user could close that message afterwards.
I have a sample code for illustration as follows:
import wx
import wx.lib.scrolledpanel as scrolled
class MyFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)
topPanel = wx.Panel(self)
panel1 = wx.Panel(topPanel, -1)
button1 = wx.Button(panel1, -1, label="generate message")
self.panel2 = scrolled.ScrolledPanel(
topPanel, -1, style=wx.SIMPLE_BORDER)
self.panel2.SetAutoLayout(1)
self.panel2.SetupScrolling()
button1.Bind(wx.EVT_BUTTON, self.onAdd)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(panel1,-1,wx.EXPAND|wx.ALL,border=10)
sizer.Add(self.panel2,-1,wx.EXPAND|wx.ALL,border=10)
self.sizer2 = wx.BoxSizer(wx.VERTICAL)
topPanel.SetSizer(sizer)
self.panel2.SetSizer(self.sizer2)
def onAdd(self, event):
new_text = wx.TextCtrl(self.panel2, value="New Message")
self.sizer2.Add(new_text,0,wx.EXPAND|wx.ALL,border=1)
self.panel2.Layout()
self.panel2.SetupScrolling()
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, 'frame')
frame.Show(True)
return True
app = MyApp(0)
app.MainLoop()
In the above I code, the right panel (i.e. panel2) serves as a notification center that all the messages should shown inside it. On the left panel (i.e. panel1) I have a button to generate message just to mimic the notification behavior. Ideally the message on the right panel should be a message box that you could close (maybe a frame? Or a MessageDialog?)
Any hint or advice is much appreciated, and an example would be the best!
Thanks!
Finally figured out myself, it was easier than I initially thought.
Here is the code:
import wx
import wx.lib.scrolledpanel as scrolled
class MyFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)
self.number_of_panels = 0
topPanel = wx.Panel(self)
panel1 = wx.Panel(topPanel, -1)
button1 = wx.Button(panel1, -1, label="generate message")
self.panel2 = scrolled.ScrolledPanel(
topPanel, -1, style=wx.SIMPLE_BORDER)
self.panel2.SetAutoLayout(1)
self.panel2.SetupScrolling()
button1.Bind(wx.EVT_BUTTON, self.onAdd)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(panel1,0,wx.EXPAND|wx.ALL,border=5)
sizer.Add(self.panel2,1,wx.EXPAND|wx.ALL,border=5)
self.sizer2 = wx.BoxSizer(wx.VERTICAL)
topPanel.SetSizer(sizer)
self.panel2.SetSizer(self.sizer2)
def onAdd(self, event):
self.number_of_panels += 1
panel_label = "Panel %s" % self.number_of_panels
panel_name = "panel%s" % self.number_of_panels
new_panel = wx.Panel(self.panel2, name=panel_name, style=wx.SIMPLE_BORDER)
self.closeButton = wx.Button(new_panel, label='Close %s' % self.number_of_panels)
self.closeButton.panel_number = self.number_of_panels
self.closeButton.Bind(wx.EVT_BUTTON, self.OnClose)
self.sizer2.Add(new_panel,0,wx.EXPAND|wx.ALL,border=1)
self.panel2.Layout()
self.panel2.SetupScrolling()
def OnClose(self, e):
if self.panel2.GetChildren():
e.GetEventObject().GetParent().Destroy()
self.number_of_panels -= 1
self.panel2.Layout() # Reset layout after destroy the panel
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, 'frame')
frame.Show(True)
return True
app = MyApp(0)
app.MainLoop()
Basically I can destroy the newly created panel. I just need to know which panel it is when I click the close button. This should work very similar to the Notification Center.
I have a WXnotebook that has different number of tabs depending on the amount of information that the program pulls. My goal is to take a screenshot of the information displayed by each tab and store those images. Im having a problem with the program going through the tabs. I was thinking maybe something like
for i in range(numOfTabs):
self.waferTab.ChangeSelection(i)
time.sleep(3)
but this only shows me the last tab in the wxnotebook. If anybody knows anyway of getting this i'd really appreciate it.
EDIT
so I tried the following like suggested below but the GUI shows up but when it shows up It looks like it already iterated through the whole loop and displays the selection is the last tab I still cant see the screen actually going through the tabs
for i in range(numOfTabs):
self.waferTab.SetSelection(i)
Refresh
wx.SafeYield()
time.sleep(10)
I don't know why you would want to do this as it seems like a confusing interface for a user to use, but here's an example using a wx.Timer:
import random
import wx
class TabPanel(wx.Panel):
def __init__(self, parent):
""""""
wx.Panel.__init__(self, parent=parent)
colors = ["red", "blue", "gray", "yellow", "green"]
self.SetBackgroundColour(random.choice(colors))
btn = wx.Button(self, label="Press Me")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(btn, 0, wx.ALL, 10)
self.SetSizer(sizer)
class DemoFrame(wx.Frame):
"""
Frame that holds all other widgets
"""
def __init__(self):
"""Constructor"""
wx.Frame.__init__(self, None, wx.ID_ANY,
"Notebook Tutorial",
size=(600,400)
)
panel = wx.Panel(self)
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.change_tabs, self.timer)
self.timer.Start(1000)
self.notebook = wx.Notebook(panel)
tabOne = TabPanel(self.notebook)
self.notebook.AddPage(tabOne, "Tab 1")
tabTwo = TabPanel(self.notebook)
self.notebook.AddPage(tabTwo, "Tab 2")
tabThree = TabPanel(self.notebook)
self.notebook.AddPage(tabThree, 'Tab 3')
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
panel.SetSizer(sizer)
self.Layout()
self.Show()
def change_tabs(self, event):
current_selection = self.notebook.GetSelection()
print(current_selection)
pages = self.notebook.GetPageCount()
if current_selection + 1 == pages:
self.notebook.ChangeSelection(0)
else:
self.notebook.ChangeSelection(current_selection + 1)
if __name__ == "__main__":
app = wx.App(True)
frame = DemoFrame()
app.MainLoop()
You could also use a Thread and use something like wx.CallAfter to update your UI, but I think a timer makes more sense in this case.
I'm looking for a way to get a list of all sizerItems which correspond to widgets. To demonstrate, I created the following example:
import wx
from wx.lib.scrolledpanel import ScrolledPanel
class Tester(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Window")
self.panel = ScrolledPanel(parent=self, id=-1)
self.panel.SetupScrolling()
self.sizerItems = []
Btn1 = wx.Button(self.panel, -1, "I'm a Button!")
Btn2 = wx.Button(self.panel, -1, "Me Too!")
Btn3 = wx.Button(self.panel, -1, "Hey! Quiet up there!")
Btn4 = wx.Button(self.panel, -1, "Jeez Frank, relax")
vbox = wx.BoxSizer(wx.VERTICAL)
hbox1 = wx.BoxSizer(wx.HORIZONTAL)
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
hbox1.Add(Btn1, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
hbox1.Add(Btn2, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
hbox2.Add(Btn3, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
hbox2.Add(Btn4, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
vbox.Add(hbox1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=25)
vbox.Add(hbox2, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=25)
self.panel.SetSizer(vbox)
self.panel.Layout()
self.GetAllChildren(self.panel.GetSizer())
print self.sizerItems
def GetAllChildren(self, item):
try:
for sizerItem in item.GetChildren():
self.GetAllChildren(sizerItem)
except:
try:
item.GetSizer()
self.GetAllChildren(item.GetSizer())
except:
self.sizerItems.append(item)
return
app = wx.PySimpleApp()
Tester().Show()
app.MainLoop()
This code makes a GUI with 4 buttons. I want GetAllChildren() to return a list (self.sizerItems) of all the sizerItems which are not sizers. For this example, there should be 4 elements in the list corresponding to the four buttons. GetAllChildren() should recursively run down the tree of sizers, so vbox first, then hbox1 and all its children, then hbox2 and all its children.
Currently though, I get [None,None,None,None] as the result. I realize the try...except is where it is breaking, but I don't know how else to decide whether a particular item is what I want. Any suggestions on how to fix this or alternative methods? Thanks in advance
I played around with the code a bit and changed it so that it actually returned the widget instances instead of the sizerItem instances:
import wx
from wx.lib.scrolledpanel import ScrolledPanel
class Tester(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Window")
self.panel = ScrolledPanel(parent=self, id=-1)
self.panel.SetupScrolling()
self.sizerItems = []
Btn1 = wx.Button(self.panel, -1, "I'm a Button!")
Btn2 = wx.Button(self.panel, -1, "Me Too!")
Btn3 = wx.Button(self.panel, -1, "Hey! Quiet up there!")
Btn4 = wx.Button(self.panel, -1, "Jeez Frank, relax")
vbox = wx.BoxSizer(wx.VERTICAL)
hbox1 = wx.BoxSizer(wx.HORIZONTAL)
hbox2 = wx.BoxSizer(wx.HORIZONTAL)
hbox1.Add(Btn1, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
hbox1.Add(Btn2, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
hbox2.Add(Btn3, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
hbox2.Add(Btn4, flag=wx.LEFT|wx.RIGHT|wx.TOP, border=10)
vbox.Add(hbox1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=25)
vbox.Add(hbox2, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=25)
self.panel.SetSizer(vbox)
self.panel.Layout()
self.GetAllChildren(self.panel.GetSizer())
print self.sizerItems
def GetAllChildren(self, item):
for sizerItem in item.GetChildren():
widget = sizerItem.GetWindow()
if not widget:
# then it's probably a sizer
sizer = sizerItem.GetSizer()
if isinstance(sizer, wx.Sizer):
self.GetAllChildren(sizer)
else:
self.sizerItems.append(widget)
#----------------------------------------------------------------------
if __name__ == "__main__":
app = wx.App(False)
Tester().Show()
app.MainLoop()
You can read more about this method in one of my old tutorials:
http://www.blog.pythonlibrary.org/2012/08/24/wxpython-how-to-get-children-widgets-from-a-sizer/
Which, oddly enough, is based on another StackOverflow question:
wxPython: How to get sizer from wx.StaticText?
Anyway, when I ran that code above, I got back a list of four buttons. To check and make sure they aren't the same button added 4 times, I did this:
for item in self.sizerItems:
print item.GetLabel()
You might also want to take a look at the Widget Inspection Tool which can visually show you which widgets are in which sizers.
the path you are going down leads to the darkside! turn back!
you should just do something like
def __init__(self):
...
self.widgets = {
'text_input':textBox,
'btn1':btn1,
....
}
then just access them later through
my_instance.widgets['text_input'].GetValue()
or
def validate(self):
self.widgets['text_input'].GetValue()
I have a GUI with two wxNotebook-elements like this:
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY,
"My App",size=(800,600),pos=((wx.DisplaySize()[0]-800)/2,(wx.DisplaySize()[1]-600)/2),style= wx.SYSTEM_MENU | wx.CAPTION | wx.MINIMIZE_BOX | wx.CLOSE_BOX)
self.SetBackgroundColour((232,232,232))
self.p = wx.Panel(self,size=(800,6300),pos=(0,0))
self.SetPages()
def SetPages(self):
self.nb = wx.Notebook(self.p,style=wx.NB_BOTTOM)
page1 = PageOne(self.nb)
page2 = PageTwo(self.nb)
self.nb.AddPage(page1, "page1")
self.nb.AddPage(page2, "page2")
self.sizer = wx.BoxSizer()
self.sizer.Add(self.nb, 1, wx.EXPAND)
self.p.SetSizer(self.sizer)
Now I want to create a third Notebook-page & set focus on it at a certain event. But this does not work:
def CreateNewPageEvent(self, event):
self.CreateNewPage()
def CreateNewPage(self):
page3 = PageThree(self.nb)
self.nb.AddPage(page3, "page3")
I must admit that I'm not sure what a "BoxSizer" does =/
Any ideas to get this working?
Edit: OK, this works for an event inside my MainFrame-class. But I also want to create a new nb-page from an event of another class:
class ContinueApp(MainFrame):
def foo(self):
super(ContinueApp, self).CreateNewPage()
def continueapp(event):
cont = ContinueApp()
cont.foo()
The BoxSizer (and other sizers) are for laying out widgets so you don't have to position them yourself. They also help control which widgets expand or stretch when you make your application window larger or smaller. In your case, you should NOT add the same widget to the same sizer twice. You shouldn't add one widget to two different sizers either.
You need to remove this:
self.nb.AddPage(page1, "page3")
self.sizer.Add(self.nb, 1, wx.EXPAND)
self.p.SetSizer(self.sizer)
Also note that you are adding page1 to the notebook again when you should be adding page3:
page3 = PageThree(self.nb)
self.nb.AddPage(page3, "page3")
If you want to switch between tabs programmatically, you should use the notebook's SetSelection method. I have an example app you can look at in the following tutorial (or the answer below it):
http://www.blog.pythonlibrary.org/2012/07/18/wxpython-how-to-programmatically-change-wx-notebook-pages/
wxpython: How to make a tab active once it is opened via an event handler?
Once you have switched tabs, you may want to set the focus on a widget within that tab. I find that using pubsub to send events is probably the cleanest way to communicate between classes. I have a couple of tutorials on that subject:
For early versions of wxPython 2.8 - http://www.blog.pythonlibrary.org/2010/06/27/wxpython-and-pubsub-a-simple-tutorial/
For later versions of wxPython 2.8 and all of 2.9 - http://www.blog.pythonlibrary.org/2013/09/05/wxpython-2-9-and-the-newer-pubsub-api-a-simple-tutorial/
This should help you, just click "GoTo Blue Panel" button.
import wx
import wx.lib
import wx.lib.flatnotebook as FNB
class MyFlatNotebook(FNB.FlatNotebook):
def __init__(self, parent):
mystyle = FNB.FNB_DROPDOWN_TABS_LIST|\
FNB.FNB_FF2|\
FNB.FNB_SMART_TABS|\
FNB.FNB_X_ON_TAB
super(MyFlatNotebook, self).__init__(parent, style=mystyle)
# Attributes
self.textctrl = wx.TextCtrl(self, value="edit me", style=wx.TE_MULTILINE)
self.blue = wx.Panel(self)
self.blue.SetBackgroundColour(wx.BLUE)
# Setup
self.AddPage(self.textctrl, "Text Editor")
self.AddPage(self.blue, "Blue Panel")
class MyFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)
# Make some buttons
vbox = wx.BoxSizer(wx.VERTICAL)
hbox = wx.BoxSizer(wx.HORIZONTAL)
button = wx.Button(self, wx.ID_OK, "GoTo Blue Panel")
self.Bind(wx.EVT_BUTTON, self.OnButton, button)
hbox.Add(button, 0, wx.ALL, 5)
self.nb = MyFlatNotebook(self)
vbox.Add(hbox, 0, wx.EXPAND)
vbox.Add(self.nb, 1, wx.EXPAND)
self.SetSizer(vbox)
def OnButton(self, event):
self.nb.SetSelection(1)
if __name__=='__main__':
app = wx.App(False)
frame = MyFrame(None, -1, "NoteTest")
frame.Show()
app.MainLoop()
Ok I have an application I am coding and am trying to get a layout simpler to this:
Notice how the text is left justified and the input boxes are all aligned, I see this in the wxPython demo code, but they all use the flexgrid sizer and I am trying to only use BoxSizers (due to them being simpler and because I only understand a little of sizers and even struggle with using BoxSizers, in 6 months I would have an even harder time)
I have tried having the input and text in two vertical sizers and then putting those in a horizontal sizer, didn't work because the text was not aligned with the inputs. I also tried doing that and also having each text, input pairing in a sizer, even worse. Any suggestions?
Here's a simple example using just BoxSizers:
import wx
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
# Add a panel so it looks the correct on all platforms
panel = wx.Panel(self, wx.ID_ANY)
# create the labels
lblOne = wx.StaticText(panel, label="labelOne", size=(60,-1))
lblTwo = wx.StaticText(panel, label="lblTwo", size=(60,-1))
lblThree = wx.StaticText(panel, label="lblThree", size=(60,-1))
# create the text controls
txtOne = wx.TextCtrl(panel)
txtTwo = wx.TextCtrl(panel)
txtThree = wx.TextCtrl(panel)
# create some sizers
mainSizer = wx.BoxSizer(wx.VERTICAL)
lineOneSizer = wx.BoxSizer(wx.HORIZONTAL)
lineTwoSizer = wx.BoxSizer(wx.HORIZONTAL)
lineThreeSizer = wx.BoxSizer(wx.HORIZONTAL)
# add widgets to sizers
lineOneSizer.Add(lblOne, 0, wx.ALL|wx.ALIGN_LEFT, 5)
lineOneSizer.Add(txtOne, 0, wx.ALL, 5)
lineTwoSizer.Add(lblTwo, 0, wx.ALL|wx.ALIGN_LEFT, 5)
lineTwoSizer.Add(txtTwo, 0, wx.ALL, 5)
lineThreeSizer.Add(lblThree, 0, wx.ALL|wx.ALIGN_LEFT, 5)
lineThreeSizer.Add(txtThree, 0, wx.ALL, 5)
mainSizer.Add(lineOneSizer)
mainSizer.Add(lineTwoSizer)
mainSizer.Add(lineThreeSizer)
panel.SetSizer(mainSizer)
# Run the program
if __name__ == "__main__":
app = wx.App(False)
frame = MyForm()
frame.Show()
app.MainLoop()
But this is kind of messy, so here's a refactored version:
import wx
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
# create the main sizer
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
# Add a panel so it looks the correct on all platforms
self.panel = wx.Panel(self, wx.ID_ANY)
lbls = ["labelOne", "lblTwo", "lblThree"]
for lbl in lbls:
self.buildLayout(lbl)
self.panel.SetSizer(self.mainSizer)
#----------------------------------------------------------------------
def buildLayout(self, text):
""""""
lblSize = (60,-1)
lbl = wx.StaticText(self.panel, label=text, size=lblSize)
txt = wx.TextCtrl(self.panel)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(lbl, 0, wx.ALL|wx.ALIGN_LEFT, 5)
sizer.Add(txt, 0, wx.ALL, 5)
self.mainSizer.Add(sizer)
# Run the program
if __name__ == "__main__":
app = wx.App(False)
frame = MyForm()
frame.Show()
app.MainLoop()
For most layouts other than the most basic you usually can't escape using a number of different types of sizers in order to realize your design.
Here is a good tutorial on sizers.