wxPython: Class attribute shared among several instances accidentally - python

This bit of code has me scratching my head.
I'm trying to write wxPython GUI, with a frame that contains several 'PulseBox' objects (code below). Each pulse box is basically 4 wx.TextCtrls with 4 wx.StaticTexts positioned next to them as labels. The object has a single attribute called 'data' which is a dictionary that I would like to access later.
I would like the TextCtrls to change the values of 'data' for their own pulse box when text is entered. Unfortunatley, when I enter text into a single pulse box, it is now changing 'data' for all the pulseboxes.
import wx
class MainGUI(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, '')
self.panel = wx.Panel(self)
#Create Controls
#All Contained in a Static Box
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
#A Row of Pulseboxes
self.setpulse = PulseBox(self.panel, 'Set')
self.resetpulse = PulseBox(self.panel, 'Reset')
self.gatepulse = PulseBox(self.panel, 'Gate')
#Generate Layout
#Use a FlexGrid Sizer
self.fgs = wx.FlexGridSizer(rows=1, cols=3, vgap=15, hgap=15)
self.fgs.AddMany([(self.setpulse, 1, wx.EXPAND),
(self.resetpulse, 1, wx.EXPAND),
(self.gatepulse, 1, wx.EXPAND)])
#Add the FlexGridSizer to the StaticBoxSizer
self.sizer.Add(self.fgs, proportion=1, flag=wx.ALL, border=15)
#FitTheSizer
self.panel.SetSizer(self.sizer)
self.sizer.Fit(self)
class PulseBox(wx.Panel):
def __init__(self, parent, name='Set',
data=dict(Leading='1.0e-6', Width='10.0e-6',
Trailing='1.0e-6', Delay='1.0e-3') ):
wx.Panel.__init__(self, parent)
self.data = data
self.name = name
#Create Controls
#All Contained in a Static Box
self.box = wx.StaticBox(self, label=name + ' Pulse (seconds)')
self.sizer = wx.StaticBoxSizer(self.box, wx.VERTICAL)
#A row of labels
self.label_lead = wx.StaticText(self, label='Leading')
self.label_width = wx.StaticText(self, label='Width')
self.label_trail = wx.StaticText(self, label='Trailing')
self.label_delay = wx.StaticText(self, label='Delay')
#A row of textctrls
self.textctrl_lead = wx.TextCtrl(self, value=data['Leading'], style=wx.TE_PROCESS_ENTER, name='Leading')
self.textctrl_width = wx.TextCtrl(self, value=data['Width'], style=wx.TE_PROCESS_ENTER, name='Width')
self.textctrl_trail = wx.TextCtrl(self, value=data['Trailing'], style=wx.TE_PROCESS_ENTER, name='Trailing')
self.textctrl_delay = wx.TextCtrl(self, value=data['Delay'], style=wx.TE_PROCESS_ENTER, name='Delay')
#Send back their data
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_width)
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_lead)
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_trail)
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_delay)
#Generate Layout
#Use a FlexGrid Sizer
self.fgs = wx.FlexGridSizer(rows=4, cols=2, vgap=9, hgap=25)
self.fgs.AddMany([(self.label_lead), (self.textctrl_lead, 1, wx.EXPAND),
(self.label_width), (self.textctrl_width, 1, wx.EXPAND),
(self.label_trail), (self.textctrl_trail, 1, wx.EXPAND),
(self.label_delay), (self.textctrl_delay, 1, wx.EXPAND)])
#Expand the TextCtrl boxes to fill panel
self.fgs.AddGrowableCol(1, 1)
#Add the FlexGridSizer to the StaticBoxSizer
self.sizer.Add(self.fgs, proportion=1, flag=wx.ALL|wx.EXPAND, border=15)
#FitTheSizer
self.SetSizer(self.sizer)
self.sizer.Fit(self)
def on_change_pulses(self, event):
textctrl = event.GetEventObject()
name = textctrl.GetName()
value = textctrl.GetValue()
self.data[name] = value
print self.name, self.data
if __name__ == '__main__':
app = wx.PySimpleApp()
app.frame = MainGUI()
app.frame.Show()
app.MainLoop()
As an example, when I change the 'Leading' TextCtrl of the 'Set' box
to 1, I get printed: Set {'Delay': '1.0e-3', 'Width': '10.0e-6',
'Trailing': '1.0e-6', 'Leading': u'1'}
And then when I change the 'Width' TextCtrl of the 'Reset' box to 2, I get printed:
Reset {'Delay': '1.0e-3', 'Width': u'2', 'Trailing': '1.0e-6',
'Leading': u'1'}
Even though I never set the 'Leading' for the Reset box.

You are processing the message EVT_TEXT_ENTER and on_change_pulses is only updating self.data with the value of TextCtrl that revised the event.
You can fix it either by processing EVT_TEXT so the values are updated as soon as the user modifies some TextCtrl, you can add:
....
#Send back their data
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_width)
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_lead)
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_trail)
self.Bind(wx.EVT_TEXT_ENTER, self.on_change_pulses, self.textctrl_delay)
# ADDED: listen for EVT_TEXT event
self.Bind(wx.EVT_TEXT, self.on_change_pulses_update, self.textctrl_width)
self.Bind(wx.EVT_TEXT, self.on_change_pulses_update, self.textctrl_lead)
self.Bind(wx.EVT_TEXT, self.on_change_pulses_update, self.textctrl_trail)
self.Bind(wx.EVT_TEXT, self.on_change_pulses_update, self.textctrl_delay)
#Generate Layout
#Use a FlexGrid Sizer
self.fgs = wx.FlexGridSizer(rows=4, cols=2, vgap=9, hgap=25)
....
and add a handler to the event:
def on_change_pulses_update(self, event):
textctrl = event.GetEventObject()
name = textctrl.GetName()
value = textctrl.GetValue()
self.data[name] = value
or you can re-read the values of all TextCtrl if anyone of them revised EVT_TEXT_ENTER:
def on_change_pulses(self, event):
textctrl = event.GetEventObject()
name = textctrl.GetName()
#value = textctrl.GetValue()
self.data['Width'] = self.textctrl_width.GetValue()
self.data['Leading'] = self.textctrl_lead.GetValue()
self.data['Trailing'] = self.textctrl_trail.GetValue()
self.data['Delay'] = self.textctrl_delay.GetValue()
print self.name, self.data

Related

Can't insert graph in a wxPython GUI

I am trying to make a telemetry software for a college project, that's why I'm using wxPython to make a GUI. I'm currently trying to make a function that displays a graph of brake_p and throttle in function of the time after I opened the entry file.
The code looks good to me, however it looks like there is an error inside the module that blocks me from succeeding.
Here is the error I get when I open the entry file :
> PS C:\Users\Adrie\OneDrive\Bureau\telemetry python> &
> 'C:\Users\Adrie\AppData\Local\Microsoft\WindowsApps\python3.10.exe'
> 'c:\Users\Adrie\.vscode\extensions\ms-python.python-2022.20.1\pythonFiles\lib\python\debugpy\adapter/../..\debugpy\launcher' '54993' '--' 'c:\Users\Adrie\OneDrive\Bureau\telemetry
> python\v0601.py' Traceback (most recent call last): File
> "c:\Users\Adrie\OneDrive\Bureau\telemetry python\v0601.py", line 105,
> in OnOpen self.OpenData(file) File
> "c:\Users\Adrie\OneDrive\Bureau\telemetry python\v0601.py", line 144,
> in OpenData self.canvas.Draw(self.drawBrakeThrottlePlot) File
> "C:\Users\Adrie\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\wx\lib\plot\plotcanvas.py",
> line 1750, in Drawgraphics.logScale = self.logScale AttributeError:
> 'method' object has no attribute 'logScale' PS
> C:\Users\Adrie\OneDrive\Bureau\telemetry python>
The error I can't understand is the part in bold caracters,as it is nowhere in my code :
Drawgraphics.logScale = self.logScale
AttributeError: 'method' object has no attribute 'logScale'
I 'd like to add I plan to add a bar marker on the graph that would be either automatically move at real-time, or either movable by the user so the time at the marker would be used to display in other widget data corresponding to the exact instant (like in the matlab simulink graph/scopes), which means I can't store the graph as a bitmap but really as a graph/plot.
I tried to switch to another library to import the plot into the wxFrame but I just can't make it work as they look more complicated to me, so I got back to this method.
Here is the code :
import wx
import numpy as np
import matplotlib.pyplot as plt
from wx.lib.plot import PlotCanvas, PlotGraphics, PolyLine, PolyMarker
import wxmplot.interactive as wi
# Variables initialization
time, speed, distance, brake_p, throttle, g_x, g_y, g_z, gps_x, gps_y, omega_fl, omega_fr = ([] for i in range(12))
r=1 # Wheel radius, will implement a feature to make it editable by the user later
class MainWindow(wx.Frame):
def __init__(self, *args, **kw):
super(MainWindow, self).__init__(*args, **kw)
# Create a panel and add it to the main window
panel = wx.Panel(self)
# Set the panel as the main window's sizer
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(panel, 1, wx.EXPAND)
self.SetSizer(sizer)
# Set the window's min size
self.SetMinSize((1280,720))
# create a menu bar
self.makeMenuBar()
# and a status bar
self.CreateStatusBar()
self.SetStatusText("v 0.1")
# put some text with a larger bold font on it
st = wx.StaticText(panel, label="Welcome on the EFT's Telemetry software !")
font = st.GetFont()
font.PointSize += 10
font = font.Bold()
st.SetFont(font)
# and create a sizer to manage the layout of child widgets
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(st, wx.SizerFlags().Border(wx.TOP|wx.LEFT, 25))
panel.SetSizer(sizer)
def makeMenuBar(self):
# Make a file menu with Hello and Exit items
fileMenu = wx.Menu()
# The "\t..." syntax defines an accelerator key that also triggers
# the same event
openItem = fileMenu.Append(-1, "&Open...\tCtrl-O",
"Open a data file")
fileMenu.AppendSeparator()
# When using a stock ID we don't need to specify the menu item's
# label
exitItem = fileMenu.Append(wx.ID_EXIT)
# Now a help menu for the about item
helpMenu = wx.Menu()
aboutItem = helpMenu.Append(wx.ID_ABOUT)
# Now a view menu for the view item
# viewMenu = wx.Menu()
# aboutItem = helpMenu.Append(wx.ID_VIEW)
# Make the menu bar and add the two menus to it. The '&' defines
# that the next letter is the "mnemonic" for the menu item. On the
# platforms that support it those letters are underlined and can be
# triggered from the keyboard.
menuBar = wx.MenuBar()
menuBar.Append(fileMenu, "&File")
menuBar.Append(helpMenu, "&Help")
# menuBar.Append(viewMenu, "&View")
# Give the menu bar to the frame
self.SetMenuBar(menuBar)
# Finally, associate a handler function with the EVT_MENU event for
# each of the menu items. That means that when that menu item is
# activated then the associated handler function will be called.
self.Bind(wx.EVT_MENU, self.OnOpen, openItem)
self.Bind(wx.EVT_MENU, self.OnExit, exitItem)
self.Bind(wx.EVT_MENU, self.OnAbout, aboutItem)
def OnExit(self, event):
self.Close(True)
def OnAbout(self, event):
wx.MessageBox("This is a simple Telemetry software project.",
wx.OK|wx.ICON_INFORMATION)
def drawBrakeThrottlePlot():
data1 = PolyLine([time, brake_p], legend='Brake Pressure (%)', colour='red', width='1')
data2 = PolyLine([time, throttle], legend='Throttle (%)', colour='green', width='1')
return PlotGraphics([data1, data2], "Real Time Brake Pressure and Throttle", "Time (ms)", "Relative Value (%)")
def OnOpen(self, event):
# Create the file dialog
with wx.FileDialog(self, "Open Data file", wildcard="text files (*.txt)|*.txt",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Proceed loading the file chosen by the user
pathname = fileDialog.GetPath()
try:
with open(pathname, 'r') as file:
self.OpenData(file)
except IOError:
wx.LogError("Cannot open file '%s'." % pathname)
def OpenData(self, file):
data = file.readlines() # On assigne à L toutes les lignes du fichier
nb_lignes = len(data) # On assigne à nb_lignes la longueur de L
file.close()
for i in range(nb_lignes):
data[i] = data[i].strip('\n')
temp = data[i].split()
time.append(float(temp[0]))
speed.append((float(temp[8])+float(temp[9]))*r/2)
distance.append(speed[i]*1/1000) # Ici 1/1000 correspond à la fréquence d'échantillonage, modifiable en fx du capteur
brake_p.append(float(temp[1]))
throttle.append(float(temp[2]))
g_x.append(float(temp[3]))
g_y.append(float(temp[4]))
g_z.append(float(temp[5]))
gps_x.append(float(temp[6]))
gps_y.append(float(temp[7]))
omega_fl.append(float(temp[8]))
omega_fr.append(float(temp[9]))
# Remove the text on the panel
for child in self.GetChildren():
child.Destroy()
# create a panel and add it to the main window
panel = wx.Panel(self)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(panel, 1, wx.EXPAND)
self.SetSizer(self.sizer)
# Create the plot
self.canvas = PlotCanvas(panel)
self.canvas.Draw(self.drawBrakeThrottlePlot)
self.sizer.Add(self.canvas, 1, wx.EXPAND)
self.sizer.Add(self.createControls(panel), 0, wx.EXPAND)
self.sizer.Add((-1, 10))
self.sizer.Add(self.createButtonBar(panel), 0, wx.EXPAND)
self.SetSizer(self.sizer)
self.sizer.Fit(self)
def createControls(self, panel):
box = wx.StaticBox(panel, -1, 'Data')
sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
grid = wx.GridBagSizer(5, 5)
txt1 = wx.StaticText(panel, -1, 'Time (s)')
grid.Add(txt1, (1, 0))
self.tc1 = wx.TextCtrl(panel, -1, '', size=(80, -1))
grid.Add(self.tc1, (1, 1))
txt2 = wx.StaticText(panel, -1, 'Speed (km/h)')
grid.Add(txt2, (2, 0))
self.tc2 = wx.TextCtrl(panel, -1, '', size=(80, -1))
grid.Add(self.tc2, (2, 1))
txt3 = wx.StaticText(panel, -1, 'RPM')
grid.Add(txt3, (3, 0))
self.tc3 = wx.TextCtrl(panel, -1, '', size=(80, -1))
grid.Add(self.tc3, (3, 1))
txt4 = wx.StaticText(panel, -1, 'Brake pressure')
grid.Add(txt4, (4, 0))
self.tc4 = wx.TextCtrl(panel, -1, '', size=(80, -1))
grid.Add(self.tc4, (4, 1))
txt5 = wx.StaticText(panel, -1, 'Throttle position')
grid.Add(txt5, (5, 0))
self.tc5 = wx.TextCtrl(panel, -1, '', size=(80, -1))
grid.Add(self.tc5, (5, 1))
sizer.Add(grid, 0, wx.ALL, 10)
return sizer
def createButtonBar(self, panel):
"""
Create the buttons at the bottom of the panel
"""
sizer = wx.BoxSizer(wx.HORIZONTAL)
self.zoomInButton = wx.Button(panel, -1, "Zoom In")
self.Bind(wx.EVT_BUTTON, self.OnZoomIn, self.zoomInButton)
sizer.Add(self.zoomInButton, 0, wx.LEFT | wx.RIGHT, 10)
self.zoomOutButton = wx.Button(panel, -1, "Zoom Out")
self.Bind(wx.EVT_BUTTON, self.OnZoomOut, self.zoomOutButton)
sizer.Add(self.zoomOutButton, 0, wx.LEFT | wx.RIGHT, 10)
self.panLeftButton = wx.Button(panel, -1, "Pan Left")
self.Bind(wx.EVT_BUTTON, self.OnPanLeft, self.panLeftButton)
sizer.Add(self.panLeftButton, 0, wx.LEFT | wx.RIGHT, 10)
self.panRightButton = wx.Button(panel, -1, "Pan Right")
self.Bind(wx.EVT_BUTTON, self.OnPanRight, self.panRightButton)
sizer.Add(self.panRightButton, 0, wx.LEFT | wx.RIGHT, 10)
self.resetButton = wx.Button(panel, -1, "Reset")
self.Bind(wx.EVT_BUTTON, self.OnReset, self.resetButton)
sizer.Add(self.resetButton, 0, wx.LEFT | wx.RIGHT, 10)
self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, self.resetButton)
return sizer
def OnZoomIn(self, event):
self.canvas.Zoom(1.2)
event.Skip()
def OnZoomOut(self, event):
self.canvas.Zoom(1/1.2)
event.Skip()
def OnPanLeft(self, event):
self.canvas.Move(-0.1, 0)
event.Skip()
def OnPanRight(self, event):
self.canvas.Move(0.1, 0)
event.Skip()
def OnReset(self, event):
self.canvas.Reset()
event.Skip()
def OnUpdateUI(self, event):
if self.canvas.CanReset():
self.resetButton.Enable(True)
else:
self.resetButton.Enable(False)
if __name__ == '__main__':
app = wx.App()
frame = MainWindow(None, title='Telemetry EFT')
frame.Show()
app.MainLoop()
And here is a link to the example data file : data file

Continuously check for radio button status [WxPython]

I have a listbox with a set of strings. The set of strings I want to display depends on which radio button is selected. I would like it such that while the user is interacting with the Form, if they ever change the radio button it will update the list box.
Here is my code (I'm leaving the array for t87 and t89 out because they are very long (assume they exist):
def OnBtnSuperTesting(self, event):
class MainWindow(wx.Frame):
def __init__(self, parent, title):
self.dirname=''
wx.Frame.__init__(self, parent, title=title, size=(320,440))
self.SetBackgroundColour(wx.WHITE)
self.CenterOnScreen()
self.CreateStatusBar()
self.radioT89 = wx.RadioButton(self, -1, 'T89 only', pos = (2,0), style = wx.RB_GROUP)
self.radioT87 = wx.RadioButton(self, -1, 'T87 only', pos = (154, 0))
self.radioKeySort = wx.RadioButton(self, -1, 'Sort by Key', pos = (2,40), style = wx.RB_GROUP)
self.radioAtoZ = wx.RadioButton(self, -1, 'Sort Name A-Z', pos = (2,60))
self.radioZtoA = wx.RadioButton(self, -1, 'Sort Name Z-A', pos = (2,80))
self.checkCode = wx.CheckBox(self, -1, 'Generate Code', pos = (154,40))
self.checkBuild = wx.CheckBox(self, -1, 'Generate Build Report', pos = (154, 60))
self.ln = wx.StaticLine(self, -1, pos = (0,15), size = (300,3), style = wx.LI_HORIZONTAL)
self.ln2 = wx.StaticLine(self, -1, pos = (150,15), size = (3,100), style = wx.LI_VERTICAL)
self.radioT87.Bind(wx.EVT_RADIOBUTTON, self.updateList)
#self.Bind(wx.EVT_RADIOBUTTON, self.radioT89, self.updateList())
self.listbox = wx.ListBox(self, -1, pos = (0,120), size = (300,200), choices = T89, style = (wx.LB_SINGLE|wx.LB_HSCROLL))
self.go = wx.Button(self,-1, label = 'Go!', pos = (110, 325))
# Setting up the menu.
filemenu= wx.Menu()
menuAbout= filemenu.Append(wx.ID_ABOUT, "&About"," Information about this program")
menuExit = filemenu.Append(wx.ID_EXIT,"E&xit"," Terminate the program")
# Creating the menubar.
menuBar = wx.MenuBar()
menuBar.Append(filemenu,"&File")
self.SetMenuBar(menuBar)
# Events.
self.Bind(wx.EVT_MENU, self.OnExit, menuExit)
self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout)
self.SetAutoLayout(1)
self.Show()
def OnExit(self,e):
self.Close(True) # Close the frame.
def updateList(self):
if self.radioT87.GetValue() == True:
choices = T87
self.listbox.Set(choices)
else:
choices = T89
self.listbox.Set(choices)
app = wx.App(False)
frame = MainWindow(None, "Supervisory Testing")
app.MainLoop()
When you create each radiobutton you can create a bind event. What this does (as you have implemented later on in your code) is execute a command function when the bind event occurs. In your case it would look like this:
self.Bind(wx.EVT_RADIOBUTTON,self.RadioButton,self.DoSomething)
Explanation:
wx.EVT_RADIOBUTTON
This is the event that is triggered when the user changes the Radiobutton's status. It may or may not have attributes.
self.RadioButton
This is the radiobutton which you would like to bind. In your case "self.radioAtoZ" or similar.
self.DoSomething
THis is the callback function. You can make it whatever you want such as:
def DoSomething(self):
if self.radioAtoZ.getStatus():
rearrangeNumbersFromAtoZ
print 'Re-arranged numbers from A to Z'
else:
etc.
EDIT:
self.RadioButton.Bind(EVT_RADIOBUTTON, self.DoSomething)
Your structure for self.DoSomething should be like this:
Class MainWindow:
def __init_(self, parent):
def DoSomething(self):
#dostuff
Also in response to your other comment:
when a function is called within a Bind event, it passes the event to the function by default. In addition, all functions have the "self" arg, thus 2 given args. You need to change the following:
def DoSomething(self, event):
#dostuff
I decided to rewrite the OP's code to demonstrate how to bind 2 RadioButton's to 2 different event handlers and update the ListBox:
import wx
########################################################################
class MyFrame(wx.Frame):
""""""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
wx.Frame.__init__(self, None, title="Radios!")
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.radioAtoZ = wx.RadioButton(panel, label='Sort Name A-Z',
style = wx.RB_GROUP)
self.radioAtoZ.Bind(wx.EVT_RADIOBUTTON, self.sortAZ)
sizer.Add(self.radioAtoZ, 0, wx.ALL|wx.EXPAND, 5)
self.radioZtoA = wx.RadioButton(panel, label='Sort Name Z-A')
self.radioZtoA.Bind(wx.EVT_RADIOBUTTON, self.sortZA)
sizer.Add(self.radioZtoA, 0, wx.ALL|wx.EXPAND, 5)
choices = ["aardvark", "zebra", "bat", "giraffe"]
self.listbox = wx.ListBox(panel, choices=choices)
sizer.Add(self.listbox, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.Show()
#----------------------------------------------------------------------
def sortAZ(self, event):
""""""
choices = self.listbox.GetStrings()
choices.sort()
self.listbox.SetItems(choices)
#----------------------------------------------------------------------
def sortZA(self, event):
""""""
choices = self.listbox.GetStrings()
choices.sort()
choices.reverse()
self.listbox.SetItems(choices)
if __name__ == "__main__":
app = wx.App(False)
frame = MyFrame()
app.MainLoop()
You will want to take a look at the following wiki article on the differences of binding it this way versus the other:
http://wiki.wxpython.org/self.Bind%20vs.%20self.button.Bind
Most of the time when you create a group of widgets that do different things, you bind them to different event handlers. If you want to bind all the RadioButtons to the same handler, then you'll probably need to name each widget with a unique name so that when they come to the handler, you can tell which button was pressed. Then you can use a series of if statements to decide what to do to the list box. Here's a tutorial that talks about that sort of thing:
http://www.blog.pythonlibrary.org/2011/09/20/wxpython-binding-multiple-widgets-to-the-same-handler/

WxPython dynamically added sizers mis-behaving

I'm trying to set-up a display to show the gamertag and avatar of users added to a text file, it most of the way there but I can't get them to position properly.
A quick mock-up of what I want: here.
Here is what I currently have on start: here
EDIT: I've switched from using a BoxSizer to using a GridSizer and that seems to have fixed the position issue, they no longer overlap, the shifting problem is still present however.
The sizer containing the users shouldn't be overlapping with the input sizer at the top, I don't know what is causing this.
And what happens when it updates to check for new users: here
Might not be that easy to see but in the second image the lowest user is shifted down, it gets further and further down as the program runs, each time it is moved down by it's own height.
The relevant code areas:
Creating the starting sizers
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
self.widget_sizer = wx.BoxSizer(wx.VERTICAL)
#Holds input for gamertags and addition
self.input_sizer = wx.BoxSizer(wx.HORIZONTAL)
#Content to be added immediately.
self.gamer_tag_textbox = wx.TextCtrl(self, -1)
self.gamer_tag_textbox.SetFocus()
self.add_gamer_tag = wx.Button(self, -1, 'Add Friend')
#Contains the displayed content
self.user_sizer = wx.BoxSizer(wx.VERTICAL)
#Add objects to sizers
self.input_sizer.Add(self.gamer_tag_textbox, 0)
self.input_sizer.Add(self.add_gamer_tag, 0)
#Set up the sizers
self.widget_sizer.Add(self.input_sizer, 0)
self.widget_sizer.Add(self.user_sizer, 0)
self.main_sizer.Add(self.widget_sizer, 0)
self.SetSizer(self.main_sizer)
Adding sizers created for each user to the main user_sizer.
def display_user_content(self, details):
self.user_sizer.Clear(True)
#This is different to the original code, it originally used boxsizers in the for each loop.
self.single_user_sizer = wx.GridSizer(cols=2)
for each in details:
#Create sizer to contain user information
#Get username
username = each[0]
#Get location of image file
location = each[-1]
#Create static text to contain username
stat = wx.StaticText(self, -1, 'username')
#Load image from location and convert to bitmap.
png = wx.Image(location, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
#Create bitmap
avatar = wx.StaticBitmap(self, -1, png)
#Add to sizer
self.single_user_sizer.Add(avatar, 1)
self.single_user_sizer.Add(stat, 1)
#Add each users sizer to main user sizer
self.user_sizer.Add(self.single_user_sizer, 1)
#Add main user sizer to widget sizer
self.widget_sizer.Add(self.user_sizer, 0)
self.frame.Fit()
Full code (minus classes): here
Maybe this is similar to what you would like to achieve?
import wx
NUMBER = 3
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self)
self.windowSizer = wx.BoxSizer()
self.windowSizer.Add(self.panel, 1, wx.ALL | wx.EXPAND)
self.sizer = wx.GridBagSizer(vgap=5, hgap=5)
self.text = wx.TextCtrl(self.panel, size=(0, 0))
self.button = wx.Button(self.panel)
self.sizer.Add(self.text, (0, 0), flag=wx.EXPAND)
self.sizer.Add(self.button, (0, 1))
self.icons = []
self.stats = []
for i in range(NUMBER):
icon = wx.Panel(self.panel, size=(50, 50))
icon.SetBackgroundColour(wx.RED)
stat = wx.Panel(self.panel, size=(200, -1))
stat.SetBackgroundColour(wx.BLUE)
self.sizer.Add(icon, (i+1, 0))
self.sizer.Add(stat, (i+1, 1), flag=wx.EXPAND)
self.icons.append(icon)
self.stats.append(stat)
self.sizer.AddGrowableCol(1)
self.border = wx.BoxSizer()
self.border.Add(self.sizer, 1, wx.ALL | wx.EXPAND, 5)
self.panel.SetSizerAndFit(self.border)
self.SetSizerAndFit(self.windowSizer)
self.Show()
app = wx.App(False)
win1 = MainWindow(None)
app.MainLoop()
Or maybe more like this?
import wx
NUMBER = 3
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self)
self.windowSizer = wx.BoxSizer()
self.windowSizer.Add(self.panel, 1, wx.ALL | wx.EXPAND)
self.sizer = wx.GridBagSizer(vgap=5, hgap=5)
self.toolbar_sizer = wx.BoxSizer()
self.text = wx.TextCtrl(self.panel)
self.button = wx.Button(self.panel)
self.toolbar_sizer.Add(self.text, 0, flag=wx.CENTER)
self.toolbar_sizer.Add(self.button, 0)
self.sizer.Add(self.toolbar_sizer, (0, 0), span=(1, 2), flag=wx.EXPAND)
self.icons = []
self.stats = []
for i in range(NUMBER):
icon = wx.Panel(self.panel, size=(50, 50))
icon.SetBackgroundColour(wx.RED)
stat = wx.Panel(self.panel, size=(200, -1))
stat.SetBackgroundColour(wx.BLUE)
self.sizer.Add(icon, (i+1, 0))
self.sizer.Add(stat, (i+1, 1), flag=wx.EXPAND)
self.icons.append(icon)
self.stats.append(stat)
self.sizer.AddGrowableCol(1)
self.border = wx.BoxSizer()
self.border.Add(self.sizer, 1, wx.ALL | wx.EXPAND, 5)
self.panel.SetSizerAndFit(self.border)
self.SetSizerAndFit(self.windowSizer)
self.Show()
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()
At the end of the display_user_content function I was adding the user_sizer to the widget_sizer each time, this was unnecessary and was causing a doubling of the number of results, I have removed that line and my code now works.
The fixed code:
def display_user_content(self, details):
self.user_sizer.Clear(True)
self.single_user_sizer = wx.GridSizer(cols=2, hgap=5, vgap=5)
for each in details:
#Get username
self.username_sizer = wx.BoxSizer(wx.HORIZONTAL)
username = each[0]
#Get location of image file
location = each[-1]
#Create static text to contain username
stat = wx.StaticText(self, -1, 'username')
#Load image from location and convert to bitmap.
png = wx.Image(location, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
#Create bitmap
avatar = wx.StaticBitmap(self, -1, png)
#Add to sizer
self.single_user_sizer.Add(avatar, 0)
self.username_sizer.Add(stat, 0)
self.single_user_sizer.Add(self.username_sizer, 0)
#Add each users sizer to main user sizer
self.user_sizer.Add(self.single_user_sizer, 0)
#Add main user sizer to widget sizer
#self.widget_sizer.Add(self.user_sizer, 0)
self.Fit()

Set String of the Current Selected Item with wxListbox

I have a listbox,
How can I change the string of current selected item of the listbox to another string?
I cant really find how to do this on Google.
Just delete selected string and insert a new one, like it is done in this example for a single-choice listbox:
import wx
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
wx.Frame.__init__(self, *args, style=wx.DEFAULT_FRAME_STYLE)
self.button = wx.Button(self, -1, "Change")
self.Bind(wx.EVT_BUTTON, self.ButtonPress, self.button)
self.tc = wx.TextCtrl(self, -1)
self.lb = wx.ListBox(self, -1, choices = ('One', 'Two'))
box = wx.BoxSizer(wx.VERTICAL)
box.Add(self.lb, 0, wx.EXPAND, 0)
box.Add(self.tc, 0, wx.EXPAND, 0)
box.Add(self.button, 0, wx.ADJUST_MINSIZE, 0)
self.SetSizer(box)
box.Fit(self)
self.Layout()
def ButtonPress(self, evt):
txt = self.tc.GetValue()
pos = self.lb.GetSelection()
self.lb.Delete(pos)
self.lb.Insert(txt, pos)
if __name__ == "__main__":
app = wx.PySimpleApp(0)
frame = MyFrame(None, -1, "")
frame.Show()
app.MainLoop()
If you need multiple-selection listbox, then you should create it with style=wx.LB_MULTIPLE:
self.lb = wx.ListBox(self, -1, choices = ('One', 'Two'), style=wx.LB_MULTIPLE)
Now you're able to change multiple strings at once:
def ButtonPress(self, evt):
txt = self.tc.GetValue()
for pos in self.lb.GetSelections():
self.lb.Delete(pos)
self.lb.Insert(txt, pos)

Destroy() wxpython simple error?

i am having an interesting problem,
This program is a simple image viewer and it can contain different images in a listbox. The listbox contains the names of the images. You can load an image to an item in the listbox. You can click any item on the listbox to see its image. For some reason Destroy() is not functioning properly. Please run the following code if you are unable to understand me,
IMAGE_NAME=[]
IMAGE_DATA=[]
IMAGE_LISTSEL=[]
import sys
import wx
def deletepic(self,parent):
try:
bitmap1.Destroy()
bmp1.Destroy()
except:
print sys.exc_info()
def sendnewpic(self,parent):
global scroll_img
deletepic(self,parent)
print IMAGE_DATA[IMAGE_LISTSEL[0]]
if IMAGE_DATA[IMAGE_LISTSEL[0]]!='':
try:
print IMAGE_DATA[IMAGE_LISTSEL[0]]
bmp1 = wx.Image(IMAGE_DATA[IMAGE_LISTSEL[0]], wx.BITMAP_TYPE_ANY).ConvertToBitmap()
bitmap1 = wx.StaticBitmap(scroll_img, -1, bmp1, (0, 0))
except:
pass
def areachange(self,pg):
print pg
try:
if IMAGE_DATA[IMAGE_LISTSEL[0]]=='':
deletepic(self,parent)
except:
pass
if pg=="Images":
self.images_area.Show()
else:
self.images_area.Hide()
class imageMax(wx.Panel):
pass
class imageTab(imageMax):
def imagesel(self,parent):
IMAGE_LISTSEL[:] = []
IMAGE_LISTSEL.append(self.listBox.GetSelection())
sendnewpic(self,parent)
def newAddImage(self,parent):
IMAGE_NAME.append('hi');
IMAGE_DATA.append('');
self.listBox.Set(IMAGE_NAME)
self.listBox.SetSelection(len(IMAGE_NAME)-1)
self.imagesel(self) #making it a selected image, globally
def reName(self,parent):
sel = self.listBox.GetSelection()
text = self.listBox.GetString(sel)
renamed = wx.GetTextFromUser('Rename item', 'Rename dialog', text)
if renamed != '':
IMAGE_NAME.pop(sel)
IMAGE_NAME.insert(sel,renamed)
self.listBox.Set(IMAGE_NAME)
self.listBox.SetSelection(sel)
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.listBox = wx.ListBox(self, size=(200, -1), choices=IMAGE_NAME, style=wx.LB_SINGLE)
self.sizer = wx.BoxSizer(wx.VERTICAL)
btnSizer = wx.BoxSizer(wx.VERTICAL) #change to horizontal for side by side
self.sizerMain = wx.BoxSizer()
self.listBox.Bind(wx.EVT_LISTBOX_DCLICK, self.reName)
self.listBox.Bind(wx.EVT_LISTBOX, self.imagesel)
btn = wx.Button(self, label="Create New",size=(200, 40))
btnTwo = wx.Button(self, label="Test 2",size=(200, 40))
btn.Bind(wx.EVT_BUTTON, self.newAddImage)
self.sizer.Add(self.listBox, proportion=1, flag=wx.TOP | wx.EXPAND | wx.LEFT, border=5)
btnSizer.Add(btn, 0, wx.ALL, 5)
btnSizer.Add(btnTwo, 0, wx.ALL, 5)
self.sizer.Add(btnSizer)
self.sizerMain.Add(self.sizer, proportion=0, flag=wx.BOTTOM | wx.EXPAND, border=0)
self.SetSizer(self.sizerMain)
class MyNotebook(wx.Notebook):
def __init__(self, *args, **kwargs):
wx.Notebook.__init__(self, *args, **kwargs)
class MyPanel(imageTab):
def OnClickTop(self, event):
scroll_img.Scroll(600, 400)
def OnClickBottom(self, event):
scroll_img.Scroll(1, 1)
def OnPageChanged(self, event):
new = event.GetSelection()
areachange(self,self.notebook.GetPageText(new))
event.Skip()
def OnPageChanging(self, event):
event.Skip()
def onOpenFile(self,parent):
""" Open a file"""
filename = wx.FileSelector()
if (filename!=''):
global bitmap1,bmp1,scroll_img
if IMAGE_DATA[IMAGE_LISTSEL[0]]!='':
deletepic(self,parent)
bmp1 = wx.Image(filename, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
bitmap1 = wx.StaticBitmap(scroll_img, -1, bmp1, (0, 0))
scroll_img.SetScrollbars(1, 1, bmp1.GetWidth(), bmp1.GetHeight())
IMAGE_DATA[IMAGE_LISTSEL[0]]=filename
print IMAGE_DATA
def __init__(self, *args, **kwargs):
global bitmap1,bmp1,scroll_img
wx.Panel.__init__(self, *args, **kwargs)
self.notebook = MyNotebook(self, size=(225, -1))
# self.button = wx.Button(self, label="Something else here? Maybe!")
tab_images = imageTab(self.notebook)
# add the pages to the notebook with the label to show on the tab
self.notebook.AddPage(tab_images, "Pics",select=True)
scroll_img = wx.ScrolledWindow(self, -1)
scroll_img.SetScrollbars(1, 1, 600, 400)
#self.button = wx.Button(scroll_img, -1, "Scroll Me", pos=(50, 20))
#self.Bind(wx.EVT_BUTTON, self.OnClickTop, self.button)
#self.button2 = wx.Button(scroll_img, -1, "Scroll Back", pos=(500, 350))
#self.Bind(wx.EVT_BUTTON, self.OnClickBottom, self.button2)
self.images_area=wx.StaticBox(self, -1, '')
self.sizerBox = wx.StaticBoxSizer(self.images_area,wx.HORIZONTAL)
#self.load_file=wx.Button(self, label='Load File')
#self.sizerBox.Add(self.load_file,0,wx.ALL,5)
self.sizerBox2 = wx.BoxSizer()
self.sizerBox.Add(scroll_img, 1, wx.EXPAND|wx.ALL, 10)
self.sizerBox2.Add(self.sizerBox, 1, wx.EXPAND|wx.ALL, 10)
self.sizer = wx.BoxSizer()
self.sizer.Add(self.notebook, proportion=0, flag=wx.EXPAND)
# self.sizer.Add(self.button, proportion=0)
btnSizer = wx.BoxSizer() #change to horizontal for side by side
btnTwo = wx.Button(self, label="Load File",size=(200, 40))
btnTwo.Bind(wx.EVT_BUTTON,self.onOpenFile)
bmp1 = None
bitmap1 = None
btnSizer.Add(btnTwo, 0, wx.TOP, 15)
self.sizerBox2.Add(btnSizer)
#self.sizerBox.Add(self.bitmap1)
self.sizer.Add(self.sizerBox2, proportion=1, flag=wx.EXPAND)
self.SetSizer(self.sizer)
self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
areachange(self,self.notebook.GetPageText(0))
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = MyPanel(self)
self.Show()
app = wx.App(False)
win = MainWindow(None, size=(600, 400))
app.MainLoop()
Try this to see what the error is,
1.you press create new (any amount of times)
2.press, load file
3. click any item on the listbox (except for the one that you are in)
4. Then go back to the orginal item that you were in,
5. Then click any item, other than the one you are currently in
There will be some sort of problem, the image does not destroy itself and returns an error like the following:
(, PyDeadObjectError('The C++ part of the StaticBitmap object has been deleted, attribute access no longer allowed.',), )
I am still able to load images but the previous images do not delete.
It is hard to word this problem, if anyone can help me with this situation, it would be greatly appreciated. If you need further explanation please comment. I thank you greatly for viewing.
Here you have your code fixed to clear your current image when loading another one.
This is done basically using self.parent.bitmap.Destroy().
I modified some few things without changing the structure of your code, in order for you to recognize changes. I eliminated globals calls. Look how I also eliminated the global IMAGE_LISTSEL variable and converted it in a class attribute. That is what Robin and Fenikso were telling you. Try to do the same with IMAGE_NAME and IMAGE_DATA.
Although the code is working, it is still far from being acceptable wxpython code. You can get many examples of correctly written wxpython code in the web. If you can afford it I recommend to you wxPython in Action from Noel Rappin and Robin Dunn.
IMAGE_NAME = []
IMAGE_DATA = []
import sys
import wx
def deletepic(self):
try:
self.parent.bitmap.Destroy()
except:
print sys.exc_info()
def sendnewpic(self):
if self.parent.bitmap: deletepic(self)
if IMAGE_DATA[self.image_listsel] != '':
try:
print IMAGE_DATA[self.image_listsel]
bmp = wx.Image(IMAGE_DATA[self.image_listsel], wx.BITMAP_TYPE_ANY).ConvertToBitmap()
self.parent.scroll_img.SetScrollbars(1, 1, bmp.GetWidth(), bmp.GetHeight())
self.parent.bitmap = wx.StaticBitmap(self.parent.scroll_img, -1, bmp, (0, 0))
self.parent.Refresh()
except:
pass
def areachange(self, pg):
print pg
try:
if IMAGE_DATA[self.image_listsel] == '':
deletepic(self)
except:
pass
if pg == "Images":
self.images_area.Show()
else:
self.images_area.Hide()
class imageTab(wx.Panel):
def __init__(self, parent, grandparent):
wx.Panel.__init__(self, parent)
self.parent = grandparent
self.image_listsel = 0
self.listBox = wx.ListBox(self, size=(200, -1), choices=IMAGE_NAME, style=wx.LB_SINGLE)
self.sizer = wx.BoxSizer(wx.VERTICAL)
btnSizer = wx.BoxSizer(wx.VERTICAL) #change to horizontal for side by side
self.sizerMain = wx.BoxSizer()
self.listBox.Bind(wx.EVT_LISTBOX_DCLICK, self.reName)
self.listBox.Bind(wx.EVT_LISTBOX, self.imagesel)
btn = wx.Button(self, label="Create New",size=(200, 40))
btnTwo = wx.Button(self, label="Test 2",size=(200, 40))
btn.Bind(wx.EVT_BUTTON, self.newAddImage)
self.sizer.Add(self.listBox, proportion=1, flag=wx.TOP | wx.EXPAND | wx.LEFT, border=5)
btnSizer.Add(btn, 0, wx.ALL, 5)
btnSizer.Add(btnTwo, 0, wx.ALL, 5)
self.sizer.Add(btnSizer)
self.sizerMain.Add(self.sizer, proportion=0, flag=wx.BOTTOM | wx.EXPAND, border=0)
self.SetSizer(self.sizerMain)
def imagesel(self, evt):
self.image_listsel = self.listBox.GetSelection()
sendnewpic(self)
def newAddImage(self, evt):
IMAGE_NAME.append('hi')
IMAGE_DATA.append('')
self.listBox.Set(IMAGE_NAME)
self.listBox.SetSelection(len(IMAGE_NAME)-1)
self.imagesel(None) #making it a selected image, globally
def reName(self,parent):
sel = self.listBox.GetSelection()
text = self.listBox.GetString(sel)
renamed = wx.GetTextFromUser('Rename item', 'Rename dialog', text)
if renamed != '':
IMAGE_NAME.pop(sel)
IMAGE_NAME.insert(sel,renamed)
self.listBox.Set(IMAGE_NAME)
self.listBox.SetSelection(sel)
class MyPanel(wx.Panel):
def __init__(self, *args, **kwargs):
wx.Panel.__init__(self, *args, **kwargs)
self.notebook = wx.Notebook(self, size=(225, -1))
#
self.tab_images = imageTab(self.notebook, self)
# add the pages to the notebook with the label to show on the tab
self.notebook.AddPage(self.tab_images, "Pics", select=True)
self.scroll_img = wx.ScrolledWindow(self, -1)
self.scroll_img.SetScrollbars(1, 1, 600, 400)
self.images_area = wx.StaticBox(self, -1, '')
self.sizerBox = wx.StaticBoxSizer(self.images_area, wx.HORIZONTAL)
self.sizerBox2 = wx.BoxSizer()
self.sizerBox.Add(self.scroll_img, 1, wx.EXPAND|wx.ALL, 10)
self.sizerBox2.Add(self.sizerBox, 1, wx.EXPAND|wx.ALL, 10)
self.sizer = wx.BoxSizer()
self.sizer.Add(self.notebook, proportion=0, flag=wx.EXPAND)
#
btnSizer = wx.BoxSizer() #change to horizontal for side by side
btnTwo = wx.Button(self, label="Load File", size=(200, 40))
btnTwo.Bind(wx.EVT_BUTTON, self.onOpenFile)
self.bmp = None
self.bitmap = None
btnSizer.Add(btnTwo, 0, wx.TOP, 15)
self.sizerBox2.Add(btnSizer)
#
self.sizer.Add(self.sizerBox2, proportion=1, flag=wx.EXPAND)
self.SetSizer(self.sizer)
self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged)
areachange(self, self.notebook.GetPageText(0))
def OnClickTop(self, event):
self.scroll_img.Scroll(600, 400)
def OnClickBottom(self, event):
self.scroll_img.Scroll(1, 1)
def OnPageChanged(self, event):
new = event.GetSelection()
areachange(self, self.notebook.GetPageText(new))
event.Skip()
def OnPageChanging(self, event):
event.Skip()
def onOpenFile(self, evt):
""" Open a file"""
filename = wx.FileSelector()
if filename != '':
IMAGE_DATA[ self.tab_images.image_listsel] = filename
self.tab_images.imagesel(None)
print IMAGE_DATA
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = MyPanel(self)
self.Show()
app = wx.App(False)
win = MainWindow(None, size=(600, 400))
app.MainLoop()
Sometimes you are using bmp1 and bitmap1 as local variables and sometimes as globals. Since you are making multiple instances of them without saving the prior references anywhere then you are losing your references to the already existing objects. When you Destroy() them then you are only destroying the most recently created instances.
Try adding them to some sort of collection (like a list) instead and then you can access any of the items from the list when you need them later. Also try to avoid using global variables. Store your variables in the object instances that they belong to.

Categories