Generating Radiobutton grid menu from an Array in Tkinter, Python - python

Edit: Sorry I can't answer my own post since I'm new but I figured it out: If you remove the line "tki.Button(master,..." (2nd to last code line), then the code runs perfectly fine. I guess the grid and the button don't work the way I put it.
sorry to bother but I'm having a little trouble figuring out what's off here. Basically I have an array that I want to loop through and set each of the values as a radiobutton IN A GRID. Later I'm going to loop through several arrays to generate a larger grid menu, but I can probably figure that out once I get this first loop working.
Here is my code:
import Tkinter as tki
master = tki.Tk()
frm = tki.Frame(master, bd = 16, relief = "sunken")
frm.grid()
tType = tki.StringVar()
tColumn = tki.IntVar()
tRow = tki.IntVar()
compType = ["iMac ", "Mac Mini ", "Mac Pro ", "Macbook ", "Macbook Air ", "Macbook Pro "]
tColumn.set(0)
tRow.set(0)
def radioCreate(typeArray):
for t in typeArray:
b = tki.Radiobutton(frm, text = t, variable = tType)
b.config(indicatoron = 0, bd = 4, width = 16, value = t)
b.grid(row = tRow.get(), column = tColumn.get())
tRow.set((tRow.get() + 1)) #increment tRow for next run-through
def p():
print tType.get()
radioCreate(compType)
tki.Button(master, command = p, text = "Display").pack()
master.mainloop()
Now remember, I'm trying to get this working in a grid, because I'm going to populate other columns with other data from different arrays.

The problem is i. These two lines:
frm.grid()
...
tki.Button(...).pack()
While it's perfectly acceptable to use pack and grid in the same application, you can't use them on two widgets that share the same master.
Grid will potentially change the size of a widget or its master depending on its options. Pack will notice the change, and may itself try to resize one or more widgets and/or the master based on its options. Grid will notice the change, and may resize... Pack will notice the change and resize, ...

Related

How to limit the rightest Gtk.TreeViewColumn's width inside a Gtk.ScrolledWindow?

I cannot prevent the rightest column of a Gtk.TreeView to expand.
As the real Gtk.TreeView may display a greater number of rows, making it usually somewhat greater than the screen's height, it is embedded in a Gtk.ScrolledWindow. This is required. Without it, attaching an empty grid at the right of the treeview, expanding itself horizontally, would fix the problem. Based on this idea, I've tried a workaround that introduces another difficulty (see below).
I have built a minimal working example from the example from https://python-gtk-3-tutorial.readthedocs.io/en/latest/treeview.html#filtering, without filtering nor buttons; and the columns are 80 px wide at least (this works) and their content is horizontally centered. This last detail makes the horizontal expansion of the rightest column visible. In the original example, it does expand too, but as everything is left aligned, this is not really visible. I'd liked to keep the columns' content centered, without seeing the rightest expanded.
This example is minimal, but contains some helping features: you'll find clickable column titles, that will display some information about the clicked column in the console; a remove button (works fine, remove the selected rows) and a paste button that allows to paste new rows from a selection (e.g. from selected lines from a spreadsheet, but there's nothing to check the data are correct, if you paste something that does not convert to int, it will simply crash).
Workaround
A workaround I've tried consist of gathering both the treeview and a horizontally expanding empty right grid at its right inside a grid that would be put inside the Gtk.ScrolledWindow. It works, but causes other subtle problems: in some situations, the treeview does not get refreshed (it happens after a while), yet nothing prevents the main loop to refresh the view (there's no other processing in the background, for instance). To experiment this workaround: comment and uncomment the lines as described in the code below; run the program via python script.py (if you need to install pygobject in a venv, see here), notice the rightest column does not expand to the right any longer, select the 3 first rows and press "remove", then from a spread sheet, select 3 lines of dummy integers as shown below and then press "paste". Scroll down to the last rows: you'll see most of the time that the 3 pasted lines do not show up, even if it is possible to scroll over the last row. Maybe one of them will show up after some time, then another... (or simply select a row, and they'll show up). Strangely, it happens if one has just removed as many lines as one wants to paste after the removal (3 removed, 3 pasted; or 4 removed, 4 pasted etc.).
Example spreadsheet selection:
Question
So, I'd prefer to avoid the workaround (I'm afraid I may find other situations triggering a bad refreshing of the treeview), that I could not fix itself (for instance, setting self.scrollable_treelist.set_propagate_natural_height(True) proved useless, maybe I'm not using it correctly though?) and only attach the treeview itself directly in the Gtk.ScrolledWindow. How to prevent the rightest column to expand, then?
(I've tried to use a fair amount of setters and properties of the cell renderers, the treeview, the treeview columns, the scrolled window, to no avail. Some of them are still in the code below.)
Any solution using and fixing the workaround above would be accepted though.
In any case, the treeview may be scrolled, and lines may be added and removed from it without any refreshing problem.
Source Code
import gi
try:
gi.require_version('Gtk', '3.0')
except ValueError:
raise
else:
from gi.repository import Gtk, Gdk
# ints to feed the store
data_list = [(i, 2 * i, 3 * i, 4 * i, 5 * i) for i in range(40)]
class AppWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Treeview Columns Size Demo")
self.set_border_width(10)
# Setting up the self.grid in which the elements are to be positioned
self.grid = Gtk.Grid()
self.grid.set_column_homogeneous(True)
self.grid.set_row_homogeneous(True)
self.add(self.grid)
# Creating the ListStore model
self.store = Gtk.ListStore(int, int, int, int, int)
for data_ref in data_list:
self.store.append(list(data_ref))
# creating the treeview and adding the columns
self.treeview = Gtk.TreeView(model=self.store)
rend = Gtk.CellRendererText()
rend.set_alignment(0.5, 0.5)
for i, column_title in enumerate([f'nĂ—{p}' for p in [1, 2, 3, 4, 5]]):
column = Gtk.TreeViewColumn(column_title, rend, text=i)
column.set_min_width(80)
# column.set_max_width(80)
# column.set_fixed_width(80)
# column.set_sizing(Gtk.TreeViewColumnSizing(1))
column.set_alignment(0.5)
column.set_clickable(True)
column.connect('clicked', self.on_column_clicked)
self.treeview.append_column(column)
self.treeview.set_hexpand(False)
self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
# Put the treeview in a scrolled window
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
self.scrollable_treelist.add(self.treeview)
# WORKAROUND
# Alternatively, embed the treeview inside a grid containing an
# empty grid to the right of the treeview
# To try it: comment out the previous line; uncomment next lines
# scrolled_grid = Gtk.Grid()
# empty_grid = Gtk.Grid()
# empty_grid.set_hexpand(True)
# scrolled_grid.attach(self.treeview, 0, 0, 8, 10)
# scrolled_grid.attach_next_to(empty_grid, self.treeview,
# Gtk.PositionType.RIGHT, 1, 1)
# self.scrollable_treelist.add(scrolled_grid)
# self.scrollable_treelist.set_propagate_natural_height(True)
# Buttons
self.remove_button = Gtk.Button(label='Remove')
self.remove_button.connect('clicked', self.on_remove_clicked)
self.paste_button = Gtk.Button(label='Paste')
self.paste_button.connect('clicked', self.on_paste_clicked)
self.grid.attach_next_to(self.remove_button, self.scrollable_treelist,
Gtk.PositionType.TOP, 1, 1)
self.grid.attach_next_to(self.paste_button, self.remove_button,
Gtk.PositionType.RIGHT, 1, 1)
self.set_default_size(800, 500)
self.show_all()
# Clipboard (to insert several rows)
self.clip = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
self.clip2 = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
def on_column_clicked(self, col):
print(f'col.get_sizing()={col.get_sizing()}')
print(f'col.get_expand()={col.get_expand()}')
print(f'col.get_width()={col.get_width()}')
print(f'col.get_min_width()={col.get_min_width()}')
print(f'col.get_max_width()={col.get_max_width()}')
print(f'col.get_fixed_width()={col.get_fixed_width()}')
def on_remove_clicked(self, widget):
model, paths = self.treeview.get_selection().get_selected_rows()
refs = []
for path in paths:
refs.append(Gtk.TreeRowReference.new(model, path))
for ref in refs:
path = ref.get_path()
treeiter = model.get_iter(path)
model.remove(treeiter)
# print(f'AFTER REMOVAL, REMAINING ROWS={[str(r[0]) for r in model]}')
def on_paste_clicked(self, widget):
text = self.clip.wait_for_text()
if text is None:
text = self.clip2.wait_for_text()
if text is not None:
lines = text.split('\n') # separate the lines
lines = [tuple(L.split('\t')) for L in lines] # convert to tuples
print(f'PASTE LINES={lines}')
for line in lines:
if len(line) == 5:
line = tuple(int(value) for value in line)
self.store.append(line)
win = AppWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

Tkinter Checkbutton variable

I'm not great at tkinter or even python so I've run into what should be a simple problem.
I have something like this in the middle of my project:
visible = numLevels * [IntVar(value=1)]
top = Toplevel()
settingslabel = Label(top, text='Settings', height=0, width=100)
for i in range(0, numLevels ):
check = ttk.Checkbutton(settingslabel, text='Level ' + str(i), variable=visible[i])
check.grid(column = 0, row = i)
check.var = visible[i]
settingslabel.grid(column = 0, row=0)
I want to have settings screen with a checkbox for every level, while maintaining an array of integers that represent the status of each button.
However, all checkboxes are synchronized. Meaning, when I check a box, all other boxes also become checked. I believe that this is because of the 'variable' field of the checkbutton. As the loop continues, i is updated, and as a result, visible[i] changes as well. I want to preserve the variable when I created the checkbutton. I don't understand how tkinter/python work well enough to know.
The number is levels can be any integer > 0 and is determined at runtime so I can't just unroll the loop.
Is there a better way to do this? Thanks in advance.
This is a duplicate of this SO post, but explanation below.
Its because all your boxes are sharing the same tkinter.Intvar() object:
numlevels = 5
visible = numlevels * [IntVar(value = 1)]
for i in range(len(visible)):
print (hex(id(visible[i]))
# Outputs:
'0x67f8190'
'0x67f8190'
'0x67f8190'
'0x67f8190'
'0x67f8190'
To solve: visible = [IntVar(value = 1) for i in range(numlevels)]

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()

Make text in a tkinter Listbox fit inside?

I've looked everywhere for a fix to this. I stumbled across this:
How to fit Tkinter listbox to contents
But this question is asking the reverse of what I want. I want the box to remain the size I've set it to, but the text runs off of the side like in the screenshot from the above linked question. Is there anyway to force a \n to be added to the string once its character count reaches the length of the listbox?
Also, I apologize if something is wrong with the format of my question, I've never posted here before.
class Stars(Tk):
def __init__(self):
Tk.__init__(self)
self.feed = Listbox(self, width = 55 , height = 31, relief = SUNKEN, borderwidth = 3)
self.feed.grid(row = 1, column = 2, columnspan = 2)
def simulate(self):
self.mass = eval(self.massEntry.get())
self.feed.insert(END, 'Star to be created with mass of {} * 10^30 kg; {} solar masses.'.format(1.98855 * self.mass, self.mass))
self.feed.insert(END, '0 years: Protostar formed in an interstellar gas cloud, and begins to compress due to gravity.')
This is all of the relevant code (trying to make a stellar evolution simulation). This is what it looks like when run, with the problem circled in red:
http://imgur.com/dZCYe6s
No, there is no way to have a Listbox wrap the text. If you want to support wrapping, use a Text widget. If you want to select items like in a listbox, you can add some custom bindings to do that.

Categories