Python Forum
[Tkinter] Update variable using tkinter entry methon - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: Python Coding (https://python-forum.io/forum-7.html)
+--- Forum: GUI (https://python-forum.io/forum-10.html)
+--- Thread: [Tkinter] Update variable using tkinter entry methon (/thread-35113.html)



Update variable using tkinter entry methon - drSlump - Sep-30-2021

Hallo everyone, I have the following code. The goal is to create a GUI that takes the entry values and update vriables value that I want to use lter in another script that I have already tested. At this stage I am simply printing the values from a button (RUN SIMULATION). It works but I have the following issues:
  • when I start the GUI the predefined value in the entry are like .!frame2.!entry
  • to print the entry value I need to call each dictionary item with the method get() (button RUN SIMULATION). Can I save directly the entry values in the dictianary without the need to use such a method in the button?
def create_geometry_input_dictionary(frame, var):
    temp = {'TITLE': ['Min', 'Step', 'Max'],
            'LABEL1': [StringVar(frame, value='25'),
                       StringVar(frame, value=var),
                       StringVar(frame, value=var)],
            'LABEL2': [StringVar(frame, value='19'),
                       StringVar(frame, value=var),
                       StringVar(frame, value=var)],
            'LABEL3': [StringVar(frame, value='0.8'),
                       StringVar(frame, value=var),
                       StringVar(frame, value=var)],
            'LABEL4': [StringVar(frame, value='2'),
                       StringVar(frame, value=var),
                       StringVar(frame, value=var)],
            'LABEL5': [StringVar(frame, value='0.1'),
                       StringVar(frame, value=var),
                       StringVar(frame, value=var)]}
    return temp

def create_fields(frame, fields_dict):
    for ind, key in enumerate(fields_dict):
        lab_temp = Label(frame, text=key)
        lab_temp.grid(row=ind, column=0, sticky=NS, padx=5, pady=5)
        for i, item in enumerate(fields_dict[key]):
            if ind == 0:
                lab_temp = Label(frame, text=item)
                lab_temp.grid(row=ind, column=i + 1, sticky=NS, padx=5, pady=5)
            else:
                val = Entry(frame, textvariable=item, width=15)
                val.grid(row=ind, column=i + 1, sticky="ns", padx=5, pady=5)
                fields_dict[key][i].set(val)

if __name__ == '__main__':
    root = Tk()
    root.title('Model Definition')
    width = 980
    height = 40
    root.geometry('{}x{}'.format(width, 10 * height))

    width_label = 30

    # create all of the main containers
    top_frame = Frame(root, width=width, height=40, pady=3)
    input_frame_left = Frame(root, width=width / 2, height=40, pady=3)
    input_frame_right = Frame(root, width=width / 2, height=40, pady=3)
    frame_file = Frame(root, width=width, height=40, pady=3)  # include file path
    output_frame_left = Frame(root, width=width / 2, height=40, pady=3)
    output_frame_right = Frame(root, width=width / 2, height=40, pady=3)
    button_frame = Frame(root, width=width, height=40, pady=3)

    # place the container within the main window using grid
    top_frame.grid(row=0, columnspan=2, sticky="n")
    input_frame_left.grid(row=1, column=0, sticky="nw")
    input_frame_right.grid(row=1, column=1, sticky="ne")
    frame_file.grid(row=2, columnspan=2, sticky="ns")
    output_frame_left.grid(row=3, column=0, sticky="w")
    output_frame_right.grid(row=3, column=1, sticky="e")
    button_frame.grid(row=4, columnspan=2, sticky="n")

    # create radio button TYPE of ANALYSIS
    label_top_frame = Label(top_frame, text='TYPE OF ANALYSIS', bd=5, anchor=N).grid(row=0, columnspan=2, sticky=N)
    type_analysis = StringVar(value='1')
    Radiobutton(top_frame, text='One-side heating', value='1', variable=type_analysis).grid(row=1, column=0, sticky=N)
    Radiobutton(top_frame, text='Uniform heating', value='2', variable=type_analysis).grid(row=1, column=1, sticky=N)

    lab = Label(input_frame_left, text='GEOMETRY DATA')
    lab.grid(row=0, column=0, sticky=N, padx=5, pady=5)
    lab = Label(input_frame_left, text='Max')
    lab.grid(row=0, column=1, sticky=N, padx=5, pady=5)
    lab = Label(input_frame_left, text='Step')
    lab.grid(row=0, column=2, sticky=N, padx=5, pady=5)
    lab = Label(input_frame_left, text='Min')
    lab.grid(row=0, column=3, sticky=N, padx=5, pady=5)

    dict_input_geometry = create_geometry_input_dictionary(input_frame_left, '0')
    dict_input_water = create_water_input_dictionary(input_frame_right, '0')

    create_fields(input_frame_left, dict_input_geometry)
    create_fields(input_frame_right, dict_input_water)

 input_path = StringVar(value=path_temp)  
    Label(frame_file, text='path').grid(row=0, columnspan=2)
    Label(frame_file, textvariable=input_path).grid(row=1, column=0, sticky='w') #  textvariable=input_path.get() does not work, why?
    Button(frame_file, text='Open file', command=lambda: load_file(input_path)).grid(row=1, column=1)
    Button(frame_file, text='Run simulation', command=lambda: print(dict_input_geometry['LABEL1'][0].get())).grid(row=1, column=2)
   



RE: Update variable using tkinter entry methon - deanhystad - Oct-07-2021

This is pretty old, but just in case you haven't found answers.

The reason you are getting .!frame2.!entry in your Entry widgets is because you do this:
                val = Entry(frame, textvariable=item, width=15)
                val.grid(row=ind, column=i + 1, sticky="ns", padx=5, pady=5)
                fields_dict[key][i].set(val)
What is val? val is an entry. Remove the fields_dict[key][i].set(val) to fix the problem.

You do need the get, but you can hide it. I would make a class that hides the details of interacting with the "dictionary".
from tkinter import *

class DoubleEntry(Entry):
    '''A special Entry for GeometryMatrix.
    Verifies that input text is a number.
    Can set a command to call when the value changes.
    Set and get numeric value using "value" property.
    '''
    def __init__(self, *args, command=None, justify='right', **kwargs):
        super().__init__(*args, **kwargs)
        self.var = StringVar(self, '')
        self.config(textvariable=self.var, justify=justify)
        self.bind('<Return>', self._accept)
        self.bind('<FocusOut>', self._accept)
        self.value = 0.0
        self.command = command

    def __str__(self):
        '''Return the text value'''
        return self.var.get()

    def _accept(self, event):
        '''Accept value change when return is pressed of losing focus'''
        try:
            value = float(self.var.get())
        except ValueError:
            value = self._value
        changed = value != self._value
        self.value = value
        if changed and self.command:
            self.command(self)

    @property
    def value(self):
        '''Return the numeric value'''
        return self._value

    @value.setter
    def value(self, value):
        '''Set the numeric value'''
        try:
            self._value = float(value)
        except ValueError:
            pass
        self.var.set(str(self._value))

class GeometryMatrix(Frame):
    '''A matrix like thing of Entry controls'''
    def __init__(self, parent, rows, columns, ewidth=10, padding=1, command=None, **kwargs):
        super().__init__(**kwargs)
        self.rows = len(rows)
        self.columns = len(columns)
        self.command = command

        self.column_headers = [Label(self, text=text) for text in columns]
        for column in range(self.columns):
            self.column_headers[column].grid(row=0, column=column+1, padx=padding, pady=padding)

        self.entries = []
        self.row_headers = [Label(self, text=text) for text in rows]
        for row in range(self.rows):
            self.row_headers[row].grid(row=row+1, column=0, padx=padding, pady=padding)
            for column in range(self.columns):
                entry = DoubleEntry(self, width=ewidth, command=self._value_changed)
                entry.grid(row=row+1, column=column+1, padx=padding, pady=padding)
                self.entries.append(entry)

    def __len__(self):
        '''Return how many cells are in my matrix'''
        return self.rows * self.columns

    def _index(self, row, column=0):
        '''Private function.  Compute index for [row, column]'''
        return row * self.columns + column

    def _value_changed(self, cell):
        '''Private function called when a cell value is edited'''
        if (self.command is not None):
            self.command(self)

    def get(self, row, column):
        '''Return value at [row, column]'''
        return self.entries[self._index(row, column)].value

    def set(self, row, column, value):
        '''Set value at [row, column] = value'''
        self.variables[self._index(row, column)].value = value

    @property
    def values(self):
        '''Get list with all values'''
        return [entry.value for entry in self.entries]

    @values.setter
    def values(self, values):
        '''Set all values from list'''
        for entry, value in zip(self.entries, values):
            entry.value = value

    def __str__(self):
        '''Return a pretty string showing my values'''
        w1 = max((len(x['text']) for x in self.row_headers))
        w2 = max(self.entries[0]['width'], max((len(x['text']) for x in self.column_headers)))
        retval = '[' + ' '*w1 + ', '.join((f'{x["text"]:^{w2}}' for x in self.column_headers))
        for row in range(self.rows):
            vals = [f'{str(entry):>{w2}}' for entry in self.entries[self._index(row):self._index(row+1)]]
            retval += '\n ' + f'{self.row_headers[row]["text"]:>{w1}}' + ', '.join(vals)
        return retval + ']'

if __name__ == '__main__':
    root = Tk()
    input_geometry = GeometryMatrix(
        root,
        rows = [f'LABEL{i+1}' for i in range(5)],
        columns = ['Min', 'Step', 'Max'],
        command=print)
    input_geometry.values = [x**2 for x in range(3*5)]
    input_geometry.pack()

    root.mainloop()
With everything wrapped up in a class it is really easy to add things like converting the input to floats, binding a command to call with the values change, etc. It improved the value dictionary idea so much that I decided to add some new features to the Entry widget so it works better with numbers.


RE: Update variable using tkinter entry methon - drSlump - Oct-13-2021

wow! that's great! I will have a look at it as it looks quite complicate. Could you give me some reference so I can understand what have you done?? :/


RE: Update variable using tkinter entry methon - deanhystad - Oct-13-2021

I wrote a class (GeometryMatrix) that makes a bunch of entry widgets and treats them like they are one graphical control that is used to display/enter/edit a list of numbers. Using the class I can write a program that makes a 3x3 matrix of Entry controls with column headers 'A', 'B', and 'C' and row headers '1', '2', '3' with one command. I can also have the GeometryMatrix control call a function when you type a new value into one of the cells. This program prints the sum of values in the matrix.
import tkinter as tk
from geometrymatrix import GeometryMatrix

def sum_the_values(matrix):
    print(sum(matrix.values))

root = tk.Tk()
input_matrix = GeometryMatrix(root, rows=['1', '2', '3'], columns=['A', 'B', 'C'], command=sum_the_values)
input_matrix.pack()
root.mainloop()
The class takes care of setting and getting the values for the entries, and laying out the entries in a grid pattern with column and row headers.

With only a few more lines I can make a program with two of these controls and has them interact. In this example changing a value in the input matrix updates the output matrix to display the squares of the input matrix values.
import tkinter as tk
from geometrymatrix import GeometryMatrix

def square(src, dst):
    dst.values = [value**2 for value in src.values]

root = Tk()
output = GeometryMatrix(root, columns=['A', 'B', 'C'], rows=['1', '2', '3'])
input = GeometryMatrix(root, columns=['A', 'B', 'C'], rows=['1', '2', '3'], command=lambda x:square(x, output))
input.pack()
output.pack()

root.mainloop()
The Entry widget is awkward to use. It doesn't have a command. It doesn't work well with numbers. Getting and setting the value is more complicated than it needs to be. I decided to fix those problems by making an Entry control that is designed to fit into my solution. I gave it a command callback so I can have it notify the matrix control when a cell value is changed. To know when to call the command function I bound the key return event and focus out event to a method. The _accept() method validates the text (makes sure it is a number), gets the numeric value of the text, and calls the command callback function. I gave it a "value" property that gets and sets the numerical value for the control and updates the entry text to show the value.

If you don't like how tkinter (or any Python package) works, you can change it. You could do this with a bunch of convenience functions, but it is often easier and cleaner to modify the behavior of existing code by subclassing. I don't like how the Entry widget works so I wrote a subclass and replaced the parts I don't like. It bugs me that tkinter doesn't have a table class (they do have a tree class that can make table like things) so I created one. If you want to write anything significant in Python you really need to learn how Python classes work.


RE: Update variable using tkinter entry methon - drSlump - Oct-14-2021

Thanks fot the explanation. I will play with it to understand better. Would you suggest me to use tkinter further or to move to Qt to develop my GUI?


RE: Update variable using tkinter entry methon - deanhystad - Oct-14-2021

I don't think it matters much. Until you are more comfortable with classes and inheritance I think you would find Qt intimidating.


RE: Update variable using tkinter entry methon - drSlump - Oct-15-2021

could you suggest me some reference focused on this specific topic? I have some ground knowledge of classes a OOP, but clearly not enough to face this example. Thanks