tkinter GUI design: managing variables from multiple widgets/toolbars - python-3.x

{Edit: the answer by Bryan Oakley in the suggested duplicate question enter link description here a) fires a response on change to the array variable (arrayvar.trace mode="w"), and I need it triggered on FocusOut, as described in my original question; b) works for Python 2, but I'm having trouble converting it to work in Python 3.5. I'm currently using his and pyfunc's answers as leads and trying to figure out a similar solution using a FocusOut event.}
I am working on a tkinter GUI that lets a user select a particular type of calculation, using a pair of radio button lists. Based on the selections, a tool bar is populated with multiple modular entry widgets, one for each variable the calculation requires. The goal is to have the numerical entry values passed to the model, which will return data to be graphed on a canvas or matplotlib widget.
My question is: what typical strategy is used for gathering and continually refreshing values from multiple widgets, in order to update displays and to pass them on to the model? The trick here is that there will be a large number of possible calculation types, each with their own toolbar. I'd like the active toolbar to be "aware" of its contents, and ping the model on every change to a widget entry.
I think the widgets and the toolbar would have to be classes, where the toolbar can query each widget for a fresh copy of its entry values when a change is detected, and store them as some collection that is passed to the model. I'm not entirely sure how it can track changes to the widgets. Using a "validate='focusout' " validation on the entry widget (e.g. as in
this validation reference )
suggests itself, but I already use "validate='key' " to limit all entries to numbers. I don't want to use "validate=all" and piggyback onto it because I don't want to continually ask the model to do a lengthy calculation on every keypress.
I'm new to GUI programming, however, so I may be barking up the wrong tree. I'm sure there must be a standard design pattern to address this, but I haven't found it.
Below is a screenshot of a mockup to illustrate what I want the GUI to do. The Task radiobutton controls which secondary button menu appears below. The selection in the second menu populates the top toolbar with the necessary entry widgets.

The following code does (mostly) what I want. The ToolBar frame objects will store the values from its contained widgets, and call the appropriate model as needed. The VarBox objects are Entry widgets with extra functionality. Hitting Tab or Return refreshes the data stored in the ToolBar dictionary, tells the ToolBar to send data to the model, and shifts focus to the next VarBox widget.
from tkinter import *
# Actual model would be imported. "Dummy" model for testing below.
def dummy_model(dic):
"""
A "dummy" model for testing the ability for a toolbar to ping the model.
Argument:
-dic: a dictionary whose values are numbers.
Result:
-prints the sum of dic's values.
"""
total = 0
for value in dic.values():
total += value
print('The total of the entries is: ', total)
class ToolBar(Frame):
"""
A frame object that contains entry widgets, a dictionary of
their current contents, and a function to call the appropriate model.
"""
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.vars = {}
def call_model(self):
print('Sending to dummy_model: ', self.vars)
dummy_model(self.vars)
class VarBox(Frame):
"""
A customized Frame containing a numerical entry box
Arguments:
-name: Name of the variable; appears above the entry box
-default: default value in entry
"""
def __init__(self, parent=None, name='', default=0.00, **options):
Frame.__init__(self, parent, relief=RIDGE, borderwidth=1, **options)
Label(self, text=name).pack(side=TOP)
self.widgetName = name # will be key in dictionary
# Entries will be limited to numerical
ent = Entry(self, validate='key') # check for number on keypress
ent.pack(side=TOP, fill=X)
self.value = StringVar()
ent.config(textvariable=self.value)
self.value.set(str(default))
ent.bind('<Return>', lambda event: self.to_dict(event))
ent.bind('<FocusOut>', lambda event: self.to_dict(event))
# check on each keypress if new result will be a number
ent['validatecommand'] = (self.register(self.is_number), '%P')
# sound 'bell' if bad keypress
ent['invalidcommand'] = 'bell'
#staticmethod
def is_number(entry):
"""
tests to see if entry is acceptable (either empty, or able to be
converted to a float.)
"""
if not entry:
return True # Empty string: OK if entire entry deleted
try:
float(entry)
return True
except ValueError:
return False
def to_dict(self, event):
"""
On event: Records widget's status to the container's dictionary of
values, fills the entry with 0.00 if it was empty, tells the container
to send data to the model, and shifts focus to the next entry box (after
Return or Tab).
"""
if not self.value.get(): # if entry left blank,
self.value.set(0.00) # fill it with zero
# Add the widget's status to the container's dictionary
self.master.vars[self.widgetName] = float(self.value.get())
self.master.call_model()
event.widget.tk_focusNext().focus()
root = Tk() # create app window
BarParentFrame = ToolBar(root) # holds individual toolbar frames
BarParentFrame.pack(side=TOP)
BarParentFrame.widgetName = 'BarParentFrame'
# Pad out rest of window for visual effect
SpaceFiller = Canvas(root, width=800, height=600, bg='beige')
SpaceFiller.pack(expand=YES, fill=BOTH)
Label(BarParentFrame, text='placeholder').pack(expand=NO, fill=X)
A = VarBox(BarParentFrame, name='A', default=5.00)
A.pack(side=LEFT)
B = VarBox(BarParentFrame, name='B', default=3.00)
B.pack(side=LEFT)
root.mainloop()

Related

Connecting comboboxes created in a loop in PyQt5

I'm trying to build a GUI that is generated dynamically based on some input dictionary. I'm using the GridLayout, iterate over the dictionary keys and generate per iteration/grid row the key of the dictionary as a QLineEdit (to give some background color as a visual cue) and two comboboxes next to it. Ideally, the second combobox would change its items based on what is selected in the first combobox. Here's the relevant part of my code:
from PyQt5.QtCore import Qt
from PyQt5.Widgets import QApplication, QMainWindow, QVBoxLayout, QGridLayout, QLineEdit, QComboBox
class GUI(QMainWindow):
"""App View"""
def __init__(self, data):
super().__init__()
self.data = data
self.generalLayout = QVBoxLayout()
self._displayview()
def _displayview(self):
self.layout = QGridLayout()
subdict = self.data
combolist = ['Auto', 'Select value', 'Calculate']
maxwidth = sum([len(key) for key in subdict.keys()])
self.count = 0
for item in subdict.keys():
color = subdict[item]['Color']
# Display tags and bg-color
display = QLineEdit()
display.setMaximumWidth(maxwidth-maxwidth//4)
display.setStyleSheet("QLineEdit"
"{"
f"background : {color}"
"}")
display.setText(item)
display.setAlignment(Qt.AlignLeft)
display.setReadOnly(True)
# Combobox action
self.cb_action = QComboBox()
if color == 'Lightgreen':
self.cb_action.addItem('Auto')
self.cb_action.setEnabled(False)
else:
self.cb_action.addItems(combolist)
# Combobox value
self.cb_value = QComboBox()
self.cb_value.addItem('Text')
self.cb_action.currentIndexChanged.connect(self.react)
self.layout.addWidget(display, self.count, 0)
self.layout.addWidget(self.cb_action, self.count, 1)
self.layout.addWidget(self.cb_value, self.count, 2)
self.count += 1
self.generalLayout.addLayout(self.layout)
def react(self):
... # my humble approaches here
Basically, the first combobox has the three options: Auto, Select value and Calculate and based on that selection, the combobox next to it should present different options (right now, it only has 'Text' for testing purposes).
I tried different approaches in the self.react(), e.g. a simple self.cb_value.addItem('something'), which would however add the item in the last combobox (which makes sense). I also tried to simply build a new combobox with self.layout.addWidget(), however without an index, that won't work. Lastly, I tried to simply create that second column of comboboxes anew in another iteration using self.cb_action.currentText() as help, however, that again returns only the text from the last combobox.
I understand that due to the nature of creating everything while iterating I get these problems. It's not unlikely that I haven't fully understood the concept of widgets. Would anybody be so kind to point me in the right direction how I would do this with variable input data? I'd probably face the same issue when I tried to extract all these information from the comboboxes to get some output I can work with.
Thank you and have a good day.

Python Tkinter: Creating checkbuttons from a list

I am building a gui in tkinter with a list task_list = [].
Tasks are appended to/deleted from the list in the gui.
I want a window with checkboxes for every item in the list.
So if there's 10 items in the list, there should also be 10 checkboxes.
If there's 5 items in the list, there should be 5 corresponding checkboxes.
Can this be done?
I can't find anything on it
Thanks!
Here.
from tkinter import *
task_list=["Call","Work","Help"]
root=Tk()
Label(root,text="My Tasks").place(x=5,y=0)
placement=20
for tasks in task_list:
Checkbutton(root,text=str(tasks)).place(x=5,y=placement)
placement+=20
root.mainloop()
Using grid.
from tkinter import *
task_list=["Call","Work","Help"]
root=Tk()
Label(root,text="My Tasks").grid(row=0,column=0)
placement=3
for tasks in task_list:
Checkbutton(root,text=str(tasks)).grid(row=placement,column=0,sticky="w")
placement+=3
root.mainloop()
Here is my code for this issue:
from tkinter import Tk, Checkbutton, IntVar, Frame, Label
from functools import partial
task_list = ['Task 1', 'Task 2', 'Task 3', 'Work', 'Study']
def choose(index, task):
print(f'Selected task: {task}' if var_list[index].get() == 1 else f'Unselected task: {task}')
root = Tk()
Label(root, text='Tasks').grid(column=0, row=0)
frame = Frame(root)
frame.grid(column=0, row=1)
var_list = []
for index, task in enumerate(task_list):
var_list.append(IntVar(value=0))
Checkbutton(frame, variable=var_list[index],
text=task, command=partial(choose, index, task)).pack()
root.mainloop()
First I would like to mention that it is possible to mix layout manager methods like in this example. The main window uses grid as layout management method and I have gridded a frame to the window, but notice that Checkbuttons are getting packed, that is because frame is a different container so it is possible to use a different layout manager, which in this case makes it easier because pack just puts those checkbuttons one after another.
The other stuff:
There is the task list which would contain the tasks.
Then I have defined a function choose() this function prints out something. It depends on a variable. The comparison happens like this: print out this if value is this else print out this. It is just an if/else statement in one line and all it checks is if the IntVar in that list in that specific index is value 1 so "on". And there are two argument this function takes in: index and task. The index is meant to get the according IntVar from the var_list and the task is meant to display what tasks was chosen or unchosen.
Then the simple root = Tk() and root.mainloop() at the end.
Then is the label that just explains it.
Then the frame and here You can see that both label and frame were gridded using .grid()
Then the var_list = [] just creates an empty list.
Then comes the loop:
It loops over each item in the task_list and extracts the index of that item in the list and the value itself. This is done by using enumerate() function.
Each iteration appends a IntVar(value=0) to the var_list and since this appending happens at the same time as the items are read from the task_list the index of that IntVar in the list is the same as the current item in the task_list so that same index can be used for access.
Then a Checkbutton is created, its master is the frame (so that .pack() can be used) and the text=task so it corresponds to task name, the variable is set as a specific item in the var_list by index and this all has to be done so that a reference to that IntVar is kept. Then comes command=partial(choose, index, task) which may seem confusing but all partial does is basically this function will now execute always with the variables just given so those variables will always be the same for this function for this Checkbutton. And the first argument of partial is the function to be executed and next are arguments this function takes in. Then the Checkbutton gets packed.
If You have any questions ask.
Useful sources:
About partial() (though there are other sources too)
About Checkbutton (other sources about this too)
One line if/else statements

Is there a way I can make buttons in Tkinter with a for loop, while also giving each one a different command?

I'm making a revision system for school, and I want it to be able to use a modular amount of subjects just in case a subject is added to the system, so I need a way to make a number of buttons with different subject names, and be able to differentiate between those buttons using tkinter. So for example, if they were to click the Mathematics button, it would take them to another bit of code specially suited for mathematics(Although, it can't be solely for Mathematics, since then I would need definitions for subjects that haven't even been added yet)
First I simply tried setting the command to "print(subjectnames[subcount-1])", thinking it would print the name of the button, but that just printed both names out straight away without even pressing a button. Then I tried changing the variable name subject to the name of the button, which I didn't expect to work, I was just stumped and desperate
Here I started setting up the definition
def chooseQuiz():
clearWindow()
subjectnames=[]
button=[]
This was probably unimportant, just labels for the title and spacing
Label(mainWindow, text="Which quizzes would you like to take?", bg='purple3', font=('constantia',25,"bold")).grid(row=0, column=0, padx=100, pady=0)
Label(mainWindow, bg='purple3').grid(row=1, column=0, padx=0, pady=15)
Here I extract data from an SQL table to get all subject names from all topics, again probably unimportant but here is where most of the variables are made
c.execute("SELECT Subject_name FROM topics")
for row in c.fetchall():
if row[0] in subjectnames:
pass
elif row[0] not in subjectnames:
subjectnames.append(row[0])
else:
messagebox.showerror("Error", "subjectnames are not appending")
chooseQuiz()
This is the main part of this question, where I tried to form a fluid number of buttons all with different commands, but to no avail
for subcount in range(len(subjectnames)):
button.append(Button(mainWindow, text=str(subjectnames[subcount-1]), bg='grey', fg='black', font=('cambria',15), width=25, command=(subject==subjectnames[subcount-1])))
button[-1].grid(row=subcount+2,column=0, padx=0, pady=15)
I expected the subject variable to be the same as the button I pressed, but it remained at 0(original value). I think this is due to wrong use of the command function in tkinter on my part. The buttons still showed up fine(only 2 subjects currently, Mathematics and Physics, and both showed up fine).
Yes, it is possible.
The following example creates a window with a reset button; upon clicking reset, a frame is populated with buttons corresponding to a random number of buttons chosen randomly from possible subjects. Each button has a command that calls a display function that redirects the call to the proper topic, which, in turn prints the name of its topic to the console, for simplicity of the example. You could easily create functions/classes corresponding to each topic, to encapsulate more sophisticated behavior.
Adding subjects, is as simple as adding a key-value pair in SUBJECTS
Pressing reset again, removes the current button and replaces them with a new set chosen randomly.
import random
import tkinter as tk
from _tkinter import TclError
SUBJECTS = {'Maths': lambda:print('Maths'),
'Physics': lambda:print('Physics'),
'Chemistry': lambda:print('Chemistry'),
'Biology': lambda:print('Biology'),
'Astronomy': lambda:print('Astronomy'),
'Petrology': lambda:print('Petrology'),}
topics = []
def topic_not_implemented():
print('this topic does not exist')
def get_topics():
"""randomly creates a list of topics for this example
"""
global topics
topics = []
for _ in range(random.randrange(1, len(SUBJECTS))):
topics.append(random.choice(list(SUBJECTS.keys())))
return topics
def reset_topics():
global topics_frame
try:
for widget in topics_frame.winfo_children():
widget.destroy()
topics_frame.forget()
topics_frame.destroy()
except UnboundLocalError:
print('error')
finally:
topics_frame = tk.Frame(root)
topics_frame.pack()
for topic in get_topics():
tk.Button(topics_frame, text=topic, command=lambda topic=topic: display(topic)).pack()
def display(topic):
"""redirects the call to the proper topic
"""
SUBJECTS.get(topic, topic_not_implemented)()
root = tk.Tk()
reset = tk.Button(root, text='reset', command=reset_topics)
reset.pack()
topics_frame = tk.Frame(root)
topics_frame.pack()
root.mainloop()

Combining Tkinter Widgets Into Single Class Limits Accessibility?

I am working on a project that involves creating many instances of Tkinter Labels and Entry widgets that will always be aligned next to one another. To try and save myself time, I created a custom class that I am showing below:
class labelEntry(Label,Entry):
def __init__(self,parent,label,row,column,bg_color):
Label.__init__(self,parent)
self['text']=label
self['justify']='right'
self['bg']=bg_color
self.grid(row=row,column=column, sticky=E)
Entry.__init__(self,parent)
self['width']="10"
self.grid(row=row,column=column+1)
This creates the configuration I want and is easy enough to arrange (I have them stored in a frame). The problem is I don't know how to access the Entry widgets that I have created as they are part of this new class.
I have a desire to read and delete the entries from the entry widgets. My best guess at clearing them was with this button that was being fed into the same frame:
class clearAllEntry(Button):
def clearAll(self,targetFrame):
targetFrame.labelEntry.Entry.delete(0,END)
def __init__(self,parent,targetFrame):
Button.__init__(self,parent,text='Clear All Entries',bg='black',fg='white')
self['command']= "clearAll(targetFrame)"
I have also looked at grid_slave as an approach but am having the same issue.
Any advice/help would be greatly appreciated.
First off, if you're creating a new class that contains two objects of different classes, you should not be using inheritance. Instead, use composition.
Second, to be able to access the entry widget, save it to an instance variable.
For example:
class LabelEntry():
def __init__(self, parent, label, row, column, bg_color):
self.label = Label(parent, text=label, justify='right', bg=bg_color)
self.entry = Entry(parent, width=10)
self.label.grid(row=row, column=column, sticky="e")
self.grid(row=row,column=column+1)
Later, you can reference these attributes like you can any other attribute:
le1 = LabelEntry(root)
...
print(le1.entry.get())

Separate user interaction from programmical change: PyQt, QComboBox

I have several QComboBoxes in my PyQt4/Python3 GUI and they are filled with some entries from a database during the initialisation. Initial CurrentIndex is set to 0. There is also a tick box which changes the language of the items in my combo boxes. To preserve current user selection I backup index of the current item and setCurrentIndex to this number after I fill in ComboBox with translated items. All those actions emit currentIndexChanged signal.
Based on the items selected in QComboBoxes some plot is displayed. The idea is to redraw the plot online - as soon as the user changes any of ComboBox current item. And here I have a problem since if I redraw the plot every time signal currentIndexChanged is emited, I redraw it also several times during initialization and if the translation tick box selection was changed.
What is the best way to separate these cases? In principle I need to separate programmical current Index Change from the user, and update the plot only in the later case (during GUI initialisation I can programically call update plot function once). Should I write/rewrite any signal? If so, I never did that before and would welcome any hint or a good example. Use another signal? Or maybe there is a way to temporary block all signals?
There are a few different things you can try.
Firstly, you can make sure you do all your initialization before you connect up the signals.
Secondly, you could use the activated signal, which is only sent whenever the user selects an item. (But note that, unlike currentIndexChanged, this signal is sent even if the index hasn't changed).
Thirdly, you could use blockSignals to temporarily stop any signals being sent while the current index is being changed programmatically.
Here's a script that demonstrates these possibilities:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QVBoxLayout(self)
self.combo = QtGui.QComboBox()
self.combo.setEditable(True)
self.combo.addItems('One Two Three Four Five'.split())
self.buttonOne = QtGui.QPushButton('Change (Default)', self)
self.buttonOne.clicked.connect(self.handleButtonOne)
self.buttonTwo = QtGui.QPushButton('Change (Blocked)', self)
self.buttonTwo.clicked.connect(self.handleButtonTwo)
layout.addWidget(self.combo)
layout.addWidget(self.buttonOne)
layout.addWidget(self.buttonTwo)
self.changeIndex()
self.combo.activated['QString'].connect(self.handleActivated)
self.combo.currentIndexChanged['QString'].connect(self.handleChanged)
self.changeIndex()
def handleButtonOne(self):
self.changeIndex()
def handleButtonTwo(self):
self.combo.blockSignals(True)
self.changeIndex()
self.combo.blockSignals(False)
def changeIndex(self):
index = self.combo.currentIndex()
if index < self.combo.count() - 1:
self.combo.setCurrentIndex(index + 1)
else:
self.combo.setCurrentIndex(0)
def handleActivated(self, text):
print('handleActivated: %s' % text)
def handleChanged(self, text):
print('handleChanged: %s' % text)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

Resources