Creating classes in Python for Tkinter Labels - python

I built an application with Tkinter that displays statistics to labels based on what team is entered in a textbox. The application works, but my code is 600 lines long, most of which is initializing the labels. The majority of labels share the same format (create a label variable, create a label, position the label with grid), so I figured there should be a way that I can make a constructor class that will return me a new variable with a few parameters. Unfortunately I've so far been unsuccessful due mostly to the fact that I have little OOP design experience. What would be the best way to go about this? The following are the two main sections of code I'd like to replace:
self.team2RecordlabelVariable = Tkinter.StringVar()
team2Recordlabel = Tkinter.Label(self,textvariable=self.team2RecordlabelVariable,anchor='e', fg='white', bg="black")
team2Recordlabel.grid(column = rColLStart, row = 1, columnspan = colRSpan, padx=(2,10), pady=(1,20), sticky = 'E')
and
team2RL = Tkinter.Label(self,text="Record: ",anchor='e', fg='white', bg="black")
team2RL.grid(column = rColStart, row = 1, columnspan = colLSpan, padx=(2,0), pady=(1,20), sticky = 'W')

You can use a class if you just want to create a custom label, but IMO if you also want to call grid it's better to use a function. IMHO a widget should never call grid, pack or place on itself, since that limits how you use it.
Since you want to create the label and call grid at the same time, a simple helper function is all you need. In my example I'm also assuming you don't really need a StringVar for each label since you don't show any code that requires it.
def create_label(parent, text, row, column, colspan):
label = Tkinter.Label(parent, text=text, anchor='e', fg='white', bg="black")
label.grid(column = column, row = row, columnspan = colspan, padx=(2,10), pady=(1,20), sticky = 'E')
return label
...
self.team2Recordlabel = create_label(self, "", 1, rColLStart, colRSpan)

Related

How can I print total clicked number of checkboxes with a function in tkinter? [duplicate]

I made an application with tkinter which creates a list of checkboxes for some data. The checkboxes are created dynamically depending on the size of the dataset. I want to know of a way to get the input of each specific checkbox.
Here is my code, which you should be able to run.
from tkinter import *
root = Tk()
height = 21
width = 5
for i in range(1, height):
placeholder_check_gen = Checkbutton(root)
placeholder_check_gen.grid(row=i, column=3, sticky="nsew", pady=1, padx=1)
for i in range(1, height):
placeholder_scope = Checkbutton(root)
placeholder_scope.grid(row=i, column=4, sticky="nsew", pady=1, padx=1)
root.mainloop()
I looked over other answers and some people got away by defining a variable inside the checkbox settings "variable=x" and then calling that variable with a "show():" function that would have "variable.get()" inside. If anyone could please point me in the right direction or how I could proceed here. Thank you and much appreciated.
Normally you need to create an instance of IntVar or StringVar for each checkbutton. You can store those in a list or dictionary and then retrieve the values in the usual way. If you don't create these variables, they will be automatically created for you. In that case you need to save a reference to each checkbutton.
Here's one way to save a reference:
self.general_checkbuttons = {}
for i in range(1, self.height):
cb = Checkbutton(self.new_window)
cb.grid(row=i, column=3, sticky="nsew", pady=1, padx=1)
self.general_checkbuttons[i] = cb
Then, you can iterate over the same range to get the values out. We do that by first asking the widget for the name of its associated variable, and then using tkinter's getvar method to get the value of that variable.
for i in range(1, self.height):
cb = self.general_checkbuttons[i]
varname = cb.cget("variable")
value = self.root.getvar(varname)
print(f"{i}: {value}")

Second row entry width affect the first row style in tkinter?

I'm trying to create the some sample application.
Which first row is One label then input entry box then submit button.
Then second row has the another entry box.
My problem is when I increase width of the entry box in second row it affect the first row style. I don't know what is the problem.
import Tkinter
tk_obj = Tkinter.Tk()
tk_geo = tk_obj.geometry("1200x800")
Tkinter.Label(tk_obj, text='Enter query ').grid(row=1,column=1)
def callback():
print "hi"
E1 = Tkinter.Entry(tk_obj,bd=3,width=120)
E1.grid(row=1, column=2,ipady=3)
b = Tkinter.Button(tk_obj, text="Check", command=callback)
b.grid(row=1,column=3)
E2 = Tkinter.Entry(tk_obj,bd=3,width=100)
E2.grid(row=2,column=1,ipady=100)
tk_obj.mainloop()
The grid method places widgets in the center of the cell they inhabit. When you have two widgets of different sizes sharing a row or column, this means that there will be blank space around the smaller widget. To make the second Entry widget span the first two columns, use columnspan=2 when you grid() it. To left-align it within those two columns, use sticky='W':
E2.grid(row=2,column=1,ipady=100, columnspan=2, sticky='W')
You can then adjust that Entry widget's width attribute until it looks the way you want it to.

Programatically add and remove tkinter python labels causes IndexError: list index out of range

Sorry for the vague title but I didn't know how to explain myself better. Basically what I try to do in tkinter here is adding and removing labels. The label value gets updated so that I always have an increment of 1 even though I deleted a label in the beginning. If I generate labels and delete them from the bottom up I have no problems but it I delete one from the middle and then try to clean my list I get an error:
Exception in Tkinter callback
Traceback (most recent call last):
File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1536, in __call__
return self.func(*args)
File "/Users/XXXX/Helper/development/dynamicListLabels.py", line 21, in <lambda>
labelList[index].append(ttk.Button(root, text="Remove", command=lambda: removeLabel(labelList[index][0], index)))
IndexError: list index out of range
My python code looks like this:
#!/usr/bin/python
from Tkinter import *
import ttk
def removeLabel(labelToRemove, bla):
labelList[labelToRemove.get()][1].destroy()
labelList[labelToRemove.get()][2].destroy()
del labelList[labelToRemove.get()]
for label in labelList:
index = labelList.index(label)
label[0].set(index)
def addNewLabel():
labelList.append([IntVar()])
index = len(labelList) - 1
labelList[index][0].set(index)
labelList[index].append(ttk.Label(root, textvariable=labelList[index][0]))
labelList[index].append(ttk.Button(root, text="Remove", command=lambda: removeLabel(labelList[index][0], index)))
labelList[index][1].grid(column=0)
labelList[index][2].grid(column=1, row=labelList[index][1].grid_info()['row'])
root = Tk()
labelList = []
ttk.Button(root, text="add label", command=addNewLabel).grid(column=1, row=0)
root.mainloop()
And my GUI looks like this:
Thanks for your help!
Design
The main problem comes when dealing with different indexes. Trying to manipulate them carefully leads to complicated operations resulting in a long and inefficient code. To remedy to this problem, we simply get rid of them and take advantage of the label class variable Tkinter.IntVar() you already are using. This gives us full control of the labels and associated widgets.
An other efficient decision to take that prevents from getting lot of headache is to attach each (label, button) couple widgets to a unique Tkinter.Frame() instance. This offers the advantage of deleting the frame using destroy() method leading automatically to the destruction of the widgets it contains. In the same time, this keeps the look of your GUI and makes your it scalable as it offers you the possibility to add more widgets.
Designing addNewLabel()
There is nothing new here compared to your original code except, as I said in 2. each (label, button) couple will be drawn into a single and unique Tkinter.Frame() instance. Of course, the list frames must be declared global in this method.
Designing removeLabel()
From 1. the only argument we need to pass to removeLabel() is the Tkinter variable (var in the code below) inherent to the label we want to get rid of.
We need then to loop over list of frames (frames in the code below) using winfo_children() to seek for the label which has the text variable we are looking for.
Note that because I draw the label before the button inside individual frames, winfo_children() returns as first widget list element the label
winfo_children():
Returns a list containing the path names of all the children of window. Top-level windows are returned as children of their logical
parents. The list is in stacking order, with the lowest window first,
except for Top-level windows which are not returned in stacking order.
Use the wm stackorder command to query the stacking order of Top-level
windows.
This is why it is correct to write : if frame.winfo_children()[0].var == var and destroy the frame that contains the label which satisfies this condition.
Solution
Here is the program. I commented the lines which I think deserve to be commented:
'''
Created on Jun 25, 2016
#author: billal begueradj
'''
from Tkinter import *
import ttk
def removeLabel(var):
global frames
z = -1
# Loop over the list of rames
for frame in frames:
z = z + 1
# Check the text variable of the label of this frame
if frame.winfo_children()[0].var == var:
# Destroy the related frame
frame.destroy()
# Update the size of the list of frames
frames = frames[:z] + frames[z+1:]
# Do not forget to always rest this flag back to -1
z = -1
# Update the labels' numbers
r = 0
for frame in frames:
frame.winfo_children()[0].var.set(r)
r = r + 1
def addNewLabel():
global frames, i
var = IntVar()
frame = Frame(root)
i = i + 1
frame.grid(row=i, column=0)
var.set(len(frames))
l = ttk.Label(frame, textvariable=var)
l.grid(row=0, column=0)
l.var = var
b = ttk.Button(frame, text="Remove", command=lambda: removeLabel(var))
b.grid(row=0, column=1)
frames.append(frame)
if __name__ == '__main__':
root = Tk()
frames = []
i = 1
ttk.Button(root, text="add label", command=addNewLabel).grid(column=0, row=0)
root.mainloop()
Demo
Let us create 6 labels:
Now let us delete the label number 3. You can see that the numbering of the labels is automatically updated:
Now let us add a new label. You can see the newly added label has a number which is consecutive to the last existing label number in the list:
Note that the length of the list is updated all the time as you wanted.

Python - arrays of tkinter widgets changing with radiobutton clicks

I'm working on a GUI in Python using tkinter. I'm reading a text file in and creating GUI elements dynamically based on the lines in the text file. I have an array for each of my element types, which include labels, radiobutton variables (StringVars), and colored circles (drawn with create_oval). My goal is that when the user changes a radiobutton from "not assigned" to "in" or "out", the colored circle on that line will change from yellow to green. Here's how the GUI looks after the text file has been read in:
Item 1: (o) in () out () not assigned (G)
Item 2: () in () out (o) not assigned (Y)
Currently, I have a trace on the radiobutton StringVars so that I can call a method whenever one of the buttons is changed. My problem is figuring out which radiobutton was changed so that I can change the color of the circle on that line...
I'm currently going the route of duplicating the whole radiobutton StringVar array into a temp global array. When the trace function is called, I compare the temp array with what's currently in the array to figure out where the change is. I duplicate the array with: temp_radiobutton_vars = list(radiobutton_vars), but I'm not sure if this is the right route. My temp list and the current list always show the same results when I get() the StringVar, even after I changed the button. Any ideas on how to fix this, or maybe there's a better method to do what I'm looking to do...
Sorry for the long and not great explanation. If anyone needs more info or snippets of code, let me know. Thanks!
There are many ways to solve this problem. Since you are already using variable traces, perhaps the simplest solution is to pass the index of the canvas item to the callback. You can use lambda or functools.partial for this task. You could also not use variable traces, but instead, associate a command with each radiobutton. In both cases you simply need to tell the callback which index to operate on.
In the following example, the callback takes a reference to a variable and the index to the canvas item. It fetches the value, looks up the color in a table, and then configures the canvas item:
def on_radiobutton(var, index):
value = var.get()
color = {"in": "green", "out": "red", "unassigned": "yellow"}
self.canvas.itemconfigure(index, fill=color[value])
This is how the trace is set up using lambda (note that name1, name2 and op are automatically sent by tkinter for every trace):
var = tk.StringVar()
rb0 = tk.Radiobutton(..., variable=var, value="in", text="in")
rb1 = tk.Radiobutton(..., variable=var, value="out", text="out")
rb2 = tk.Radiobutton(..., variable=var, value="unassigned", text="not assigned")
var.trace("w", lambda name1, name2, op, index=i, var=var:
on_radiobutton(var, index))
It sounds like you have the wrong idea with Radiobuttons. All "connected" Radiobuttons should have the same variable value; in this way, you can call theVariable.get() and compare that with the value of each Radiobutton; you shouldn't need a reference to every Radiobutton; nor should you have a StringVar for each Radiobutton, only each line.
Edit: I've expanded my example to show how this would work for more than one line. All that changed is now I check which line I have passed in my callback, and using that I know which line to update (in your case, which canvas to color). It's just some 2D list processing to check which Radiobutton is selected based upon which line is issuing the callback.
from Tkinter import *
root = Tk()
root.geometry("300x200+500+400")
lines = [StringVar(), StringVar()]
strings = [["Hello", "Stack", "Overflow"], ["Whats", "Going", "On"]]
buttons = [[],[]]
l1 = Label(root, text = "Selection: ", justify = LEFT)
l1.grid(column = 0, row = 0, sticky = NW, padx = (0, 250))
l1.grid_propagate(False)
l2 = Label(root, text = "Selection: ", justify = LEFT)
l2.grid(column = 0, row = 4, sticky = NW, padx = (0, 250))
l2.grid_propagate(False)
def process(line):
global l1, l2, strings, lines
if line == lines[0]:
# Since lines[0] was passed in to the callback, we know to update line 0;
# take that line's label (or canvas in your case)
updateLine = 0
updateLabel = l1
else:
# Otherwise take the other line
updateLine = 1
updateLabel = l2
# These operations are performed within if/elif/else to show how you coul
# choose a different process for each Radiobutton: example, coloring a canvas differently
if lines[updateLine].get() == strings[updateLine][0]:
# This means the first button of whatever line was selected
updateLabel.config(text = "Selection: %s" %strings[updateLine][0])
elif lines[updateLine].get() == strings[updateLine][1]:
# This means the second button of whatever line was selected
updateLabel.config(text = "Selection: %s" %strings[updateLine][1])
else:
# You get the idea
updateLabel.config(text = "Selection: Bet you thought I'd say %s" %strings[updateLine][2])
# Must have a seperate row number because with multiple lines, we can't simply use 'i' or 'j'
rowNum = 1
for i in range(len(lines)):
for j in range(len(strings[i])):
buttons[i].append(Radiobutton(root, text = strings[i][j], variable = lines[i], value = strings[i][j], command = lambda line = lines[i]: process(line)))
buttons[i][j].grid(column = 0, row = rowNum, sticky = NW)
rowNum +=1
rowNum += 2
root.mainloop()

Aligning widgets using grid between multiple Tkinter LabelFrames

I'm trying to create a Tkinter layout that has labels and entry fields vertically aligned across multiple LabelFrame boxes.
Here's some simplified code:
#!/usr/bin/python
from Tkinter import *
win = Frame()
win.grid(sticky=N+S+E+W)
frame_a = LabelFrame(win, text='Top frame', padx=5, pady=5)
frame_b = LabelFrame(win, text='Bottom frame', padx=5, pady=5)
frame_a.grid(sticky=E+W)
frame_b.grid(sticky=E+W)
for frame in frame_a, frame_b:
for col in 0, 1, 2:
frame.columnconfigure(col, weight=1)
Label(win, text='Hi').grid(in_=frame_a, sticky=W)
Label(win, text='Longer label, shorter box').grid(in_=frame_b, sticky=W)
Entry(win).grid(in_=frame_a, row=0, column=1, sticky=W)
Entry(win, width=5).grid(in_=frame_b, row=0, column=1, sticky=W)
win.mainloop()
The above code produces a window that looks like the below:
Whereas I'm looking to find some way of aligning the fields so the window looks more like this (with thanks to MS Paint):
I've played around with in_ arguments to grid(), but not achieved very much, and I can't think of anything else to experiment with.
The short answer is: you can't do what you want. grid does not manage its rows and columns across multiple containers.
However, there are at least a couple ways to achieve the effect you are wanting. One way is to give the first column in each container an explicit, identical width. To do that you can use the grid_columnconfigure method to give each column a minimum width.
Another solution is to give each label an identical width, which effectively will set the width of the first column to be the same (assuming all columns in each container have the same weight).
The easiest way I know to do what you want, is to change the make the Label text variables, and then, check the len(txt1) against len(txt2), and, set the width variable of both to the longest one. The following code is close. My knowledge is too limited to figure out where the extra space is coming from.
txt1 = StringVar()
txt2 = StringVar()
lblWidth = IntVar()
txt1 = "Hi"
txt2 = "Longer label, shorter box"
if (len(txt1) > len(txt2)):
lblWidth = len(txt1)
else:
lblWidth = len(txt2)
Label(win, text=txt1, width=lblWidth, anchor=W).grid(in_=frame_a)
Label(win, text=txt2, width=lblWidth, anchor=W).grid(in_=frame_b, sticky=W)

Categories