Python Forum
Tkinter GUI question
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Tkinter GUI question
#1
I am having trouble writing a python tkinter GUI. I am trying to create a popup window which has a grid of buttons that then returns a value. In the code example below I have flatted the loop to illustrate the problem, but I plan to use a loop. The problem I am having is that if I pass the routine "setval" a string, then it works fine. If I try to pass the routine "setval" a variable, then it gets the last value of names[kk].

I considered a grid of menu buttons. However, I plan to have over 100 buttons in this project and each with 50 choices. In the past this many radio buttons creates performance issues.

#!/usr/bin/python3
from tkinter import *


root = Tk()
root.title("Root Window")
root.geometry("450x300")

def setval(window,var,value):
    var.set(value)
    print(value)
    print(type(value))
    window.destroy()
    
def open_popup1():
    
    popup1 = Toplevel(root)
    
    popup1.title("choices")
    
    # plan to have 50 choices
    choices  = ("red", "one", "blue", "two", "err")
    kk=0
    Button(popup1, text = choices[kk], command=lambda: setval(popup1,button_text,choices[kk])).grid(row=0,column=0)
    kk=1
    Button(popup1, text = choices[kk], command=lambda: setval(popup1,button_text,choices[kk])).grid(row=0,column=1)
    kk=2
    Button(popup1, text = choices[kk], command=lambda: setval(popup1,button_text,"blue")).grid(row=1,column=0)
    kk=3
    Button(popup1, text = choices[kk], command=lambda: setval(popup1,button_text,"two")).grid(row=1,column=1)
    kk=4
    foo=choices[kk]
    popup1.mainloop()

#    kk=0
#    for rr in range(0,2):
#        for cc in range(0,2):
#            Button(top1, text = choices[kk], command=lambda: setval(top1,button_text,choices[kk])).grid(row=rr,column=cc)
#            kk=kk+1

button_text = StringVar()
button_text.set("a")
# plan to have 100 buttons
button = Button(root, textvariable=button_text, command = open_popup1)
button.pack()

root.mainloop()
ss
Reply
#2
This has to do with how lambda expressions work.

A lambda function creates a closure. A little bubble that maintains the context of when the lambda expression was defined. This can be demonstrated with a simple example:
Output:
>>> x = 2 >>> square = lambda: x**2 >>> print(square()) 4 >>> x = 4 >>> print(square()) 16
The lambda expression not only contains the expression "return x**2", it remembers the variable x. When I change the value referenced by "x", evaluating the lambda expression reflects that change. "square" remembers that it uses the variable "x".

In your example the lambda expression remembers "kk". When you change the value assigned to "kk", evaluating the lambda expression reflects the change. The last value assigned to "kk" was 4.

To solve the problem you can write the lambda expression so it doesn't use "kk", or you can use a partial function.

This is how you write a lambda expression so it doesn't use kk.
command=lambda arg = choices[kk]: setval(popup1, button_text, arg)
"lambda arg = choices[kk]" defines a keyword argument for the lambda expression and assigns a default value of choices[kk]. If kk == 0, the default value for arg is "red". This is kind of like writing a normal function like this.:
def lambda_function(arg="red"):
    return setval(popup1, button_text, arg)
Another way to solve the problem is to use a partial function. partial() does not create a closure. Arguments passed to partial() are evaluated, and the values stored for when the partial is executed.
from functools import partial

command=partial(setval, popup1, button_text, choices[kk])
On a side note, a problem like this is much easier to solve using classes. If tkinter had a button class that let you choose from multiple options using a popup dialog of buttons, yours would be a very short program. You can create a button class like that. The example below creates 100 popup option buttons, each of which has 100 options to choose from.
import tkinter as tk
from functools import partial
import math


class ButtonDialog(tk.Toplevel):
    """A popup dialog that presents choices as a grid of buttons.
    Required Args
    parent  -- Parent window of dialog.
    choices -- Button labels.

    Optional Args
    width   -- width of the buttons.
    command -- User function called when button is pressed.
    """

    @staticmethod
    def grid_layout(frame, count):
        """Configure grid layout for frame.  Make all "cells" the same size"""
        columns = int(math.ceil(count**0.5))
        rows = int(math.ceil(count / columns))
        for column in range(columns):
            frame.grid_columnconfigure(column, weight=1, uniform="columns")
        for row in range(rows):
            frame.grid_rowconfigure(row, weight=1, uniform="rows")
        return columns

    def __init__(self, parent, choices, width=None, command=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.title("choices")
        self.command = command
        if width is None:
            width = max((len(str(choice)) for choice in choices))

        # Create the buttons
        columns = self.grid_layout(self, len(choices))
        for index, choice in enumerate(choices):
            button = tk.Button(
                self, text=choice, command=partial(self.accept, choice), width=width
            )
            button.grid(row=index // columns, column=index % columns, sticky="news")

    def accept(self, value):
        """Called when button is pressed"""
        if self.command:
            self.command(value)
        self.destroy()


class PopupButton(tk.Button):
    """A combobox/option menu like thing."""

    def __init__(self, parent, choices, width=20, command=None, **kwargs):
        """Init popup button.

        Required Args
        parent  -- Parent widget that contains the button.
        choices -- Choices presented in the popup dialog.

        Optional Args
        width   -- My width in characters
        command -- User function called when selection is made.
        """
        super().__init__(
            parent, text=choices[0], width=width, command=self.click, **kwargs
        )
        self.command = command
        self.choices = choices
        self.value = choices[0]

    def click(self):
        """Called when I am clicked"""
        ButtonDialog(self, self.choices, command=self.select)

    def select(self, value):
        """Called when a selection is made in my popup dialog."""
        self.value = value
        self.configure(text=str(self.value))
        if self.command:
            self.command(value)


class MyWindow(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        frame = tk.Frame(self)
        frame.pack(side=tk.TOP)
        self.sum = tk.IntVar(self, 0)
        tk.Label(frame, text="Sum").pack(side=tk.LEFT, padx=5, pady=5)
        tk.Label(frame, textvariable=self.sum, width=5, justify=tk.RIGHT).pack(
            side=tk.LEFT, padx=(0, 5), pady=5
        )

        choices = list(range(100))
        frame = tk.Frame(self)
        frame.pack(side=tk.TOP)
        self.buttons = [
            PopupButton(frame, choices, command=self.selection_changed, width=5)
            for _ in range(100)
        ]
        for index, button in enumerate(self.buttons):
            button.grid(row=index % 10, column=index // 10, sticky="news")

    def selection_changed(self, *args):
        self.sum.set(sum(b.value for b in self.buttons))


MyWindow().mainloop()
Second side note. Why don't you use a Combobox or Optionmenu? Both let you choose from multiple options and display the currently selected option. Why write a new way to do something that has been solved before (twice!) Is it because of the crazy number of choices are not presented well in the popup list?
Reply
#3
Thank you very much for the detailed answer. I really appreciate it.

To answer your questions:

I had tried something similar in PerlTK using radiomenus. I found it worked fine with a small example. Then when I expanded the code to have 100 buttons then it crashed. I was trying to avoid that issue.

I took a few minutes and coded up a 10 x 10 grid of optionmenus each with 100 values and it worked fine.

However my final implementation will have 90 choices. So a grid is much easier to use than an options menu

Once again, thanks for your help.
Reply
#4
I don't use a radio button with more than 5 choices, but you should be able to make radio button groups as large as you like. I think the crash was you making a programming error. Not saying you are a bad programmer, but Tk has been around a long time and is pretty solid.

This is my example done using radio buttons in the popup. 100 radio buttons, no problem.
import tkinter as tk
from functools import partial
import math
 
 
class ButtonDialog(tk.Toplevel):
    """A popup dialog that presents choices as a grid of radio buttons.
    Required Args
    parent  -- Parent window of dialog.
    choices -- Button labels.
 
    Optional Args
    width   -- width of the buttons.
    command -- User function called when button is pressed.
    """
 
    @staticmethod
    def grid_layout(frame, count):
        """Configure grid layout for frame.  Make all "cells" the same size"""
        columns = int(math.ceil(count**0.5))
        rows = int(math.ceil(count / columns))
        for column in range(columns):
            frame.grid_columnconfigure(column, weight=1, uniform="columns")
        for row in range(rows):
            frame.grid_rowconfigure(row, weight=1, uniform="rows")
        return columns
 
    def __init__(self, parent, choices, width=None, command=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.title("choices")
        self.command = command
        if width is None:
            width = max((len(str(choice)) for choice in choices))
 
        # Create the buttons
        self._value = tk.IntVar(self, choices[0])
        self._value.trace("w", self.accept)
        columns = self.grid_layout(self, len(choices))
        for index, choice in enumerate(choices):
            button = tk.Radiobutton(
                self, text=str(choice), value=choice, variable=self._value, width=width
            )
            button.grid(row=index // columns, column=index % columns, sticky="news")
 
    def accept(self, *args):
        """Called when button is pressed"""
        self.value = self._value.get()
        if self.command:
            self.command(self.value)
        self.destroy()
 
 
class PopupButton(tk.Button):
    """A combobox/option menu like thing."""
 
    def __init__(self, parent, choices, width=20, command=None, **kwargs):
        """Init popup button.
 
        Required Args
        parent  -- Parent widget that contains the button.
        choices -- Choices presented in the popup dialog.
 
        Optional Args
        width   -- My width in characters
        command -- User function called when selection is made.
        """
        super().__init__(
            parent, text=choices[0], width=width, command=self.click, **kwargs
        )
        self.command = command
        self.choices = choices
        self.value = choices[0]
 
    def click(self):
        """Called when I am clicked"""
        ButtonDialog(self, self.choices, command=self.select)
 
    def select(self, value):
        """Called when a selection is made in my popup dialog."""
        self.value = value
        self.configure(text=str(self.value))
        if self.command:
            self.command(value)
 
 
class MyWindow(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        frame = tk.Frame(self)
        frame.pack(side=tk.TOP)
        self.sum = tk.IntVar(self, 0)
        tk.Label(frame, text="Sum").pack(side=tk.LEFT, padx=5, pady=5)
        tk.Label(frame, textvariable=self.sum, width=5, justify=tk.RIGHT).pack(
            side=tk.LEFT, padx=(0, 5), pady=5
        )
 
        choices = list(range(100))
        frame = tk.Frame(self)
        frame.pack(side=tk.TOP)
        self.buttons = [
            PopupButton(frame, choices, command=self.selection_changed, width=5)
            for _ in range(100)
        ]
        for index, button in enumerate(self.buttons):
            button.grid(row=index % 10, column=index // 10, sticky="news")
 
    def selection_changed(self, *args):
        self.sum.set(sum(b.value for b in self.buttons))
 
 
MyWindow().mainloop()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Beginner question re: Tkinter geometry return2sender 3 905 Jun-19-2023, 06:19 PM
Last Post: deanhystad
  simple tkinter question function call not opening image gr3yali3n 5 3,305 Aug-02-2022, 09:13 PM
Last Post: woooee
  [Tkinter] question for a tkinter dialog box RobertAlvarez424 2 2,205 Aug-25-2021, 03:08 PM
Last Post: RobertAlvarez424
  Python tkinter question tablet Nick_tkinter 8 4,927 Mar-04-2021, 10:44 PM
Last Post: Larz60+
  tkinter slider question Nick_tkinter 1 2,434 Feb-22-2021, 01:31 PM
Last Post: deanhystad
  [Tkinter] Noob question:Using pyttsx3 with tkinter causes program to stop and close, any ideas? Osman_P 4 5,233 Nov-14-2020, 10:51 AM
Last Post: Osman_P
  question on tkinter canvas PhotoImage gr3yali3n 1 2,105 Sep-05-2020, 12:18 PM
Last Post: Larz60+
  Tkinter parameter question DPaul 2 2,022 Mar-14-2020, 09:35 AM
Last Post: DPaul
  Newbie question with Tkinter Entry mariolopes 2 2,201 Oct-12-2019, 11:02 PM
Last Post: Larz60+
  tkinter filedialog and pickle - custom icon question. kim07133 0 2,727 Jan-08-2018, 12:10 PM
Last Post: kim07133

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020