I have created a custom matplotlib toolbar, and I'm working on the functions associated with the custom toolbar's buttons. One of the buttons functions will (eventually) return a list position that best represents a user's selected position from a plot. I was having a bit of difficulty making this work in my mind, so I made a simple example (trying to avoid using globals) where a label not associated with the toolbar is updated when the toolbar button is pressed.
class TrackPlotToolbar(NavigationToolbar2TkAgg):
toolitems = [t for t in NavigationToolbar2TkAgg.toolitems if
t[0] in ('Home', 'Pan', 'Zoom', 'Save')]
toolitems.append(('Trace', 'Trace Track Position', 'Trace', 'Trace_old'))
def __init__(self, plotCanvas, frame):
self.TraceListOld = []
self.TraceListNew = []
NavigationToolbar2TkAgg.__init__(self, plotCanvas, frame)
def set_message(self, msg):
pass
def Trace_old(self):
gui.infoLabel.text = "abrakadabra"
gui.infoLabel.update()
return 1
class gui(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.grid()
self.parent = parent
self.infoLabel = Label(master = self, text = 'magic')
self.initUI()
def initUI(self):
TestPlot = FigureCanvasTkAgg(TestFig, self)
TestPlot.get_tk_widget().grid(column = 0,\
row = 0, columnspan = 3, rowspan = 5)
TestFrame = Frame(self)
TestFrame.grid(column = 2, row =6, columnspan = 3)
shockToolbar = TrackPlotToolbar(TestPlot,TestFrame)
shockToolbar.update()
self.infoLabel.grid(column = 2, row = 7)
def main():
root = Tk()
app = gui(root)
root.mainloop()
if __name__ == '__main__':
main()
Am I taking the wrong approach? Is it possible to inquire for new data on an event associated with a class inside of the parent class?
Related
I have two scale widgets in my code that are supposed to display values from a list. I want to be able to set individual values for both sliders. For example, for file 1 "2.4" and for file 2 "5.025". But currently I can only update the second (lower) slider without it triggering the first (upper) one. If I use the upper one, the value of the second one is also changed.
I'm stuck implementing the lambda function, so it would be create if some could give me a hint.
Thanks!
import tkinter as tk
class File_Selection():
def __init__(self, frame, text):
self.frame = frame
self.text = text
self.label_file = tk.Label(self.frame, text=text)
self.label_file.pack(side="left", anchor="s")
self.label_test = tk.Label(self.frame, text="| Select value: ")
self.label_test.pack(side="left", anchor="s")
self.scale = tk.Scale(self.frame, orient=tk.HORIZONTAL, from_=0)
self.scale.pack(side="left", anchor="s")
class View:
def __init__(self, view):
self.view = view
self.frame = tk.Frame(self.view)
self.frame.pack()
self.frame_row1 = tk.Frame(self.frame)
self.frame_row1.pack(side="top")
self.frame_row2 = tk.Frame(self.frame)
self.frame_row2.pack(side="top")
self.file_one = File_Selection(self.frame_row1, "File 1")
self.file_two = File_Selection(self.frame_row2, "File 2")
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root)
self.values = [1.01,2.4,3.6,4.89,5.025,6.547]
self.view.file_one.scale.bind('<Enter>', self.update_scale)
self.view.file_two.scale.bind('<Enter>', self.update_scale)
def run(self):
self.root.mainloop()
def update_scale(self, event):
self.active_scales = [self.view.file_one.scale, self.view.file_two.scale]
for scale in self.active_scales:
scale.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=scale: self.set_scale(value, scale))
def set_scale(self, value, scale):
self.newvalue = min(self.values, key=lambda x: abs(x-float(value)))
scale.set(self.newvalue)
if __name__ == "__main__":
c = Controller()
c.run()
The problem is that you have both scales set to active_scales. You can take advantage of the event and do away with active_scales.
You can get the widget associated with an event with event.widget. So, you can change the update_scales function from
def update_scale(self, event):
self.active_scales = [self.view.file_one.scale, self.view.file_two.scale]
for scale in self.active_scales:
scale.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=scale: self.set_scale(value, scale))
to
def update_scale(self, event):
event.widget.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=event.widget: self.set_scale(value, event.widget))
Additionally, since self.values don't appear to change, it does not seem necessary to config each scale to change its bounds and resolution on each event. You can instead pull that out of the event and make you Controller class as follows.
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root)
self.values = [1.01,2.4,3.6,4.89,5.025,6.547]
self.view.file_one.scale.bind('<Enter>', self.update_scale)
self.view.file_two.scale.bind('<Enter>', self.update_scale)
self.view.file_one.scale.config(from_=min(self.values), to=max(self.values), resolution=0.001)
self.view.file_two.scale.config(from_=min(self.values), to=max(self.values), resolution=0.001)
def run(self):
self.root.mainloop()
def update_scale(self, event):
event.widget.config(command=lambda value=event.widget: self.set_scale(value, event.widget))
def set_scale(self, value, scale):
self.newvalue = min(self.values, key=lambda x: abs(x-float(value)))
scale.set(self.newvalue)
I am developing a GUI that imports and plots data (csv file). Data are initially imported in the TabWidget() class using getfile() and on_pushButtonLoad_clicked() and passed to the FirstTab() class, where MRChart() is plotted in the infrastructure() widget. To achieve this, I created the firstTab instance in TabWidget() (apologies if my terminology is incorrect here).
The end goal is to update the plot based on the numerical range selected on a rangeslider. Firstly, I want to pass the value of slider1 to FirstTab() and I should be able to manage from there. To do this, I have attempted to create the tabwidget instance in FirstTab(). However, I get the following: "RecursionError: maximum recursion depth exceeded". I presume this is due to each class having an instance of itself contained in the other.
Any help appreciated.
Code:
class TabWidget(QDialog):
def __init__(self, data):
super(TabWidget, self).__init__()
self.data = data
self.firstTab = FirstTab(self.data)
self.showMaximized()
# create Header
FilterLayout = QHBoxLayout()
FilterLayout.addWidget(self.createHeader1a(), 1)
FilterLayout.addWidget(self.createHeader2a(), 4)
# create Tab
tabwidget = QTabWidget()
tabwidget.addTab(self.firstTab, "Tab 1")
vbox = QVBoxLayout()
vbox.addLayout(FilterLayout)
vbox.addWidget(tabwidget)
self.setLayout(vbox)
def createHeader1a(self): #Import
HeaderBox = QGroupBox("Import Data")
inputfilebtn = QPushButton("Import")
inputfilebtn.clicked.connect(self.on_pushButtonLoad_clicked)
# importrow1
importrow1layout = QHBoxLayout()
importrow1layout.addWidget(inputfilebtn)
HeaderLayout = QVBoxLayout()
HeaderLayout.addLayout(importrow1layout)
HeaderBox.setLayout(HeaderLayout)
HeaderBox.setFlat(True)
return HeaderBox
def createHeader2a(self): #Filter
HeaderBox = QGroupBox("Filter Data")
rightlayout = QHBoxLayout()
# range slider bar to filter column data for plotting
label4 = QLabel(self)
label4.setText("Filter range:")
rightlayout.addWidget(label4)
self.slider1 = QLabeledRangeSlider(Qt.Horizontal)
self.slider1.setRange(5, 500)
self.slider1.setValue((150, 300))
rightlayout.addWidget(self.slider1)
HeaderBox.setLayout(rightlayout)
HeaderBox.setFlat(True) #
return HeaderBox
#import and return file
def getfile(self):
option = QFileDialog.Options()
fname = QFileDialog.getOpenFileName(self, 'Open file',
'c:\\', "CSV files (*.csv)", options=option)
return pd.read_csv(fname[0])
#QtCore.pyqtSlot()
def on_pushButtonLoad_clicked(self):
importedfile = self.getfile()
if importedfile is None:
return
self.firstTab.MRChart(importedfile)
global database
database = importedfile
def getslider1value(self):
return self.slider1.value
class FirstTab(QWidget):
def __init__(self, data):
super(FirstTab, self).__init__()
self.data = data
self.tabwidget = TabWidget(self.data)# issue here. Attempting to # access TabWidget class
# Grid layout of entire tab
layout = QGridLayout()
layout.addWidget(self.infrastructure(self.data), 3, 0)
layout.setRowStretch(4, 3)
layout.setColumnStretch(0, 1)
self.setLayout(layout)
def MRChart(self, importedfile): # pie chart
if self.radioButton1.isChecked():
fig = go.Pie(labels=importedfile[self.radioButton1.label])
elif self.radioButton2.isChecked():
fig = go.Pie(labels=importedfile[self.radioButton2.label])
layout = go.Layout(autosize=True, legend=dict(orientation="h", xanchor='center', x=0.5))
fig = go.Figure(data=fig, layout=layout)
fig.update_layout(margin=dict(t=0, b=0, l=0, r=0))
self.browser.setHtml(fig.to_html(include_plotlyjs='cdn'))
def infrastructure(self, importedfile):
groupBox = QGroupBox("Plot")
self.browser = QtWebEngineWidgets.QWebEngineView(self)
right = QVBoxLayout()
# Change/update plot (MRChart) depending on what Radio button is selected
self.radioButton1 = QRadioButton("Label 1")
self.radioButton1.label = "Column_label_1"
self.radioButton1.toggled.connect(lambda: self.MRChart(database))
right.addWidget(self.radioButton1)
self.radioButton2 = QRadioButton("Label 2")
self.radioButton2.setChecked(True)
self.radioButton2.label = "Column_label_2"
self.radioButton2.toggled.connect(lambda: self.MRChart(database))
right.addWidget(self.radioButton2)
middleright = QHBoxLayout()
middleright.addWidget(self.browser)
middleright.addLayout(right)
groupBox.setLayout(middleright)
groupBox.setFlat(True)
print(self.tabwidget.getslider1value())# attempting to print slider value here
return groupBox
if __name__ == "__main__":
app = QApplication(sys.argv)
tabwidget = TabWidget(data=None)
tabwidget.show()
app.exec()
The recursion error is clear: TabWidget tries to create FirstTab, but there you're trying to create another TabWidget, which will create a further FirstTab and so on.
If you want to keep reference between objects, you must pass references, not create new instances (see the note below).
Also, since FirstTab calls infrastructure in the __init__, at that point the slider in the parent widget has not been created yet.
You must move the creation of FirstTab after the slider is created, and pass the reference in the constructor.
class TabWidget(QDialog):
def __init__(self, data):
super(TabWidget, self).__init__()
self.data = data
self.showMaximized()
# create Header
FilterLayout = QHBoxLayout()
FilterLayout.addWidget(self.createHeader1a(), 1)
FilterLayout.addWidget(self.createHeader2a(), 4)
self.firstTab = FirstTab(self) # <- pass the reference of the parent
# ...
class FirstTab(QWidget):
def __init__(self, tabwidget):
super(FirstTab, self).__init__()
self.tabwidget = tabwidget
self.data = tabwidget.data
# ...
Note that this is the same problem you had in your previous question. I strongly suggest you to review the basics of classes and instances and OOP in general, as knowledge and comprehension of those aspects cannot be ignored.
So my project is about showing information from a database in conjunction with images.
The idea is the following:
I have a database that describes images. One table has general information, like which color is contained in which image and another table has more granular information, like where that color can be found, etc.
When I launch a search, I want two windows to open (ClassTwo and ClassThree).
Instead of the first Window that only contains a Button to launch the search, my full application contains fields that are used to filter a database request. When I launch that search I want e.g. all images with the color green in them, so ClassTwo would list all images with that color, along with additional metadata.
ClassThree would then list all areas that contain the same color, but with a bit more detail, like position in the image and size, etc.
Upon clicking on either of the MultiListbox, I want to open an ImageViewer that shows the image.
In case of ClassThree, I would also directly highlight the area that I clicked on, so both classes would have functions bound to the MultiListbox.
My problem is with the binding of the Listboxes, that does not work properly. When I use e.g. image_parts_info_lb.bind() the function is not triggered at all. When i use image_parts_info_lb.bind_all() only the function of the last MultiListbox is triggered.
You can find the original Python2 version of the MultiListbox in the comment of the class.
Here is my code
import tkinter as tk
class MultiListbox(tk.Frame):
#original Python2 code here:
#https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s05.html
def __init__(self, master, lists):
tk.Frame.__init__(self, master)
self.lists = []
print(lists)
for l,w in lists:
frame = tk.Frame(self)
frame.pack(side='left', expand='yes', fill='both')
tk.Label(frame, text=l, borderwidth=1, relief='raised').pack(fill='x')
lb = tk.Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
relief='flat', exportselection=False, height=16)
lb.pack(expand='yes', fill='both')
self.lists.append(lb)
#commented out functions that were not necessary, as suggested in the comments
#lb.bind('<B1-Motion>', self._select)
lb.bind('<<ListboxSelect>>', self._select)
#lb.bind('<Leave>', lambda e: 'break')
lb.bind('<MouseWheel>', lambda e, s=self: s._scroll_mouse(e))
#lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
frame = tk.Frame(self)
frame.pack(side='left', fill='y')
tk.Label(frame, borderwidth=1, relief='raised').pack(fill='x')
sb = tk.Scrollbar(frame, orient='vertical', command=self._scroll)
sb.pack(expand='yes', fill='y')
self.lists[0]['yscrollcommand']=sb.set
def _select(self, event):
w = event.widget
curselection = w.curselection()
if curselection:
self.selection_clear(0, self.size())
self.selection_set(curselection[0])
def _button2(self, x, y):
for l in self.lists:
l.scan_mark(x, y)
return 'break'
def _b2motion(self, x, y):
for l in self.lists: l.scan_dragto(x, y)
return 'break'
def _scroll(self, *args):
for l in self.lists:
l.yview(*args)
return 'break'
def _scroll_mouse(self, event):
for l in self.lists:
l.yview_scroll(int(-1*(event.delta/120)), 'units')
return 'break'
def curselection(self):
return self.lists[0].curselection()
def delete(self, first, last=None):
for l in self.lists:
l.delete(first, last)
def get(self, first, last=None):
result = []
for l in self.lists:
result.append(l.get(first,last))
if last: return apply(map, [None] + result)
return result
def index(self, index):
self.lists[0].index(index)
def insert(self, index, *elements):
for e in elements:
for i, l in enumerate(self.lists):
l.insert(index, e[i])
def size(self):
return self.lists[0].size()
def see(self, index):
for l in self.lists:
l.see(index)
def selection_anchor(self, index):
for l in self.lists:
l.selection_anchor(index)
def selection_clear(self, first, last=None):
for l in self.lists:
l.selection_clear(first, last)
def selection_includes(self, index):
return self.lists[0].selection_includes(index)
def selection_set(self, first, last=None):
for l in self.lists:
l.selection_set(first, last)
class ClassOne:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master)
self.create_elements()
self.frame.pack()
def create_elements(self):
search_button = tk.Button(self.frame, text = "Launch searches", command = \
self.call_other_classes)
search_button.grid(row = 2, column = 0, padx = 20, pady = 10)
exit_button = tk.Button(self.frame, text = "Exit", command = self.master.quit)
exit_button.grid(row = 2, column = 1, padx = 20, pady = 10)
def call_other_classes(self):
self.classes_list = []
self.classes_list.append(ClassTwo(self.master))
self.classes_list.append(ClassThree(self.master))
class ClassTwo:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(tk.Toplevel(self.master))
self.frame.pack()
self.create_elements()
self.fill_image_listbox()
def create_elements(self):
#placement of the custom MultiListbox
self.available_images_lb = MultiListbox(self.frame, (('stuff1', 0), ('stuff1', 0), \
('stuff1', 0), ('stuff1', 0), ('stuff1', 0) ))
self.available_images_lb.grid(row = 1, column = 1)
self.available_images_lb.bind('<<ListboxSelect>>', self.print_stuff_two)
#Button
exit_button = tk.Button(self.frame, text = "Quit", command = self.frame.quit)
exit_button.grid(row = 2, column = 1, padx = 20, pady = 10)
def fill_image_listbox(self):
image_info = [5*['ABCD'],5*['EFGH'],5*['JKLM'],5*['NOPQ'],5*['RSTU'], 5*['VWXY']]
for item in image_info:
self.available_images_lb.insert('end', item)
def print_stuff_two(self, event):
print('Class Two active')
class ClassThree:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(tk.Toplevel(self.master))
self.create_elements()
self.frame.pack()
self.fill_imageparts_listbox()
def create_elements(self):
self.image_parts_info_lb = MultiListbox(self.frame, (('stuff1', 0), ('stuff1', 0), \
('stuff1', 0), ('stuff1', 0), ('stuff1', 0) ))
self.image_parts_info_lb.grid(row = 1, column = 1)
self.image_parts_info_lb.bind('<<ListboxSelect>>', self.print_stuff_three)
def fill_imageparts_listbox(self):
self.image_parts_info_lb.delete(0, 'end')
image_part_info = [5*['1234'],5*['5678'],5*['91011']]
for item in image_part_info:
self.image_parts_info_lb.insert('end', item)
def print_stuff_three(self, event):
print('Class Three active')
def main():
root = tk.Tk()
root.title('Image Viewer')
root.geometry('500x150+300+300')
my_class_one = ClassOne(root)
root.mainloop()
if __name__ == "__main__":
main()
Instead of the printing functions I would launch a simple Image Viewer and use Pillow to highlight areas in the image. That functionality is implemented and works well, only the function binding to the custom Listbox is not working as it should.
I am open to any input. Also, if you have any recommendations for coding patterns, feel free to let me know.
Question: MultiListbox custom event binding problem, click on any of the list elements, trigger the print_stuff_x function
Implement a custom event '<<MultiListboxSelect>>' which get fired on processed a '<<ListboxSelect>>' event and .bind(... at it.
Reference:
Tkinter.Widget.event_generate-method
event generate
Generates a window event and arranges for it to be processed just as if it had come from the window system. If the -when option is specified then it determines when the event is processed.
Crore Point:
self.event_generate('<<MultiListboxSelect>>', when='tail')
class MultiListbox(tk.Frame):
def __init__(self, master, lists):
...
for l,w in lists:
...
lb = tk.Listbox(frame, ...
lb.bind('<<ListboxSelect>>', self._select)
def _select(self, event):
w = event.widget
curselection = w.curselection()
if curselection:
self.event_generate('<<MultiListboxSelect>>', when='tail')
...
class ClassTwo:
...
def create_elements(self):
self.available_images_lb = MultiListbox(self.frame, ...
...
self.available_images_lb.bind('<<MultiListboxSelect>>', self.print_stuff_two)
...
class ClassThree:
...
def create_elements(self):
self.image_parts_info_lb = MultiListbox(self.frame, ...
...
self.image_parts_info_lb.bind('<<MultiListboxSelect>>', self.print_stuff_three)
I am currently building a user based system, where I have a class for a login screen in pythons tkinter. Once the user information is correct it will initiate a class for the menu. I'm using an SQL database which python reads and checks user information with. I'm going to need the username of the user to be passed to the menu class so the class knows what information to display on that menu.
from tkinter import *
class Login:
def __init__(self, master):
self.master = master
self.label = Label(self.master, text = "enter name")
self.entry = Entry(self.master)
self.button = Button(self.master, text = "submit", command = self.loadMenu)
self.label.grid(row = 0, column = 0)
self.entry.grid(row = 0, column = 1)
self.button.grid(row = 1, column = 0, columnspan = 2)
self.name = self.entry.get()
def loadMenu(self):
self.menu = Toplevel(self.master)
self.app = Menu(self.menu)
class Menu:
def __init__(self, master):
self.master = master
self.label = Label(self.master, text = "dave")
self.label.grid(row = 0, column = 0)
def main():
root = Tk()
run = Login(root)
root.mainloop()
if __name__ == '__main__':
main()
In the above example code, what method could be used to pass the variable 'self.name' to class menu so that it can be displayed in a label? I am trying not to use a global variable. Many Thanks.
Your class Menu, needs a method to set the name. e.g.:
class myMenu:
def __init__(self,...):
....
def set_Me(self,some_name):
self.some_name = some_name
# or
self.label.setName(some_name)
in the main program you call:
self.app.set_Me(self.name)
The above is pseudo code. But you should get the idea.
This technique you can use to pass variables to any class at any time.
If you need to pass variable only once:
class my_class:
def __init__(self,my_variable):
self.my_variable = my_variable
I'm trying to update a label in a python tkinter application when another widget is selected.
I created a small tool, that demonstrates the issue. The tool will create a small GUI with a label on top. This label should show the number of the box that was selected.
Problem is that when a box number is selected by a mouse click, the number is not shown in the top label. Clicking the box number should call setSelected, which should call app.setLabel(string). However, I get the error "global name 'app' is not defined"
How do I make the object 'app' global?
#!/usr/bin/env python
import Tkinter
def setSelected(string):
app.setLabel(string)
class Gui():
Boxes = []
def __init__(self):
self._root = Tkinter.Tk()
self._root.protocol("WM_DELETE_WINDOW", self._applicationExit)
self._string = Tkinter.StringVar()
self.setLabel('None')
self._label = Tkinter.Label(self._root, textvariable = self._string,
width = 10)
self._label.grid(row = 0, column = 0, padx = 5, pady = 5)
self._createBoxOverview()
self._root.mainloop()
def _applicationExit(self, event = None):
self._root.destroy()
def _createBoxOverview(self):
_frame = Tkinter.LabelFrame(self._root, text = 'Boxes')
for _id in range(4):
self.Boxes.append(Box(_frame, _id))
self.Boxes[_id].grid(row = 0, column = _id)
_frame.grid(row = 1, column = 0, padx = 5, pady = 5)
def setLabel(self, string):
self._string.set(string)
class Box(Tkinter.Label):
def __init__(self, master, id):
Tkinter.Label.__init__(self, master)
self._id = str(id)
self._text = Tkinter.StringVar()
self._text.set(self._id)
self.config(textvariable = self._text, width = 3)
self.bind("<Button-1>", self._onSelect)
def _onSelect(self, event):
setSelected(self._id)
if __name__ == '__main__':
app = Gui()
The issue is that you are creating root (Tk() App) and calling root.mainloop() , inside __init__() itself, so app does not completely get created, since it would only get created if you return from __init__() , but you do not do that until you close the app.
The easiest solution for your case would be to move the root object outisde Gui() class. Example -
import Tkinter
def setSelected(string):
app.setLabel(string)
class Gui():
Boxes = []
def __init__(self, root):
self._root = root
self._root.protocol("WM_DELETE_WINDOW", self._applicationExit)
self._string = Tkinter.StringVar()
self.setLabel('None')
self._label = Tkinter.Label(self._root, textvariable = self._string,
width = 10)
self._label.grid(row = 0, column = 0, padx = 5, pady = 5)
self._createBoxOverview()
def _applicationExit(self, event = None):
self._root.destroy()
def _createBoxOverview(self):
_frame = Tkinter.LabelFrame(self._root, text = 'Boxes')
for _id in range(4):
self.Boxes.append(Box(_frame, _id))
self.Boxes[_id].grid(row = 0, column = _id)
_frame.grid(row = 1, column = 0, padx = 5, pady = 5)
def setLabel(self, string):
self._string.set(string)
class Box(Tkinter.Label):
def __init__(self, master, id):
Tkinter.Label.__init__(self, master)
self._id = str(id)
self._text = Tkinter.StringVar()
self._text.set(self._id)
self.config(textvariable = self._text, width = 3)
self.bind("<Button-1>", self._onSelect)
def _onSelect(self, event):
setSelected(self._id)
if __name__ == '__main__':
root = Tkinter.Tk()
app = Gui(root)
root.mainloop()