2

My code contains two entry widgets, which are supposed to hold the height and width of an image. When the height or the width is adjusted by the user entering a number into one of the two widgets, the value of the other is supposed to be adjusted according to the entered value and a fixed aspect ratio.

My first approach was to bind the update functions to a key-press-event, but I encountered the error that leads to the bound function being executed before the textvariable is changed. The issue is described here: Tkinter, update Entry widget before calling <Key> binding. The proposed solution is to use variable tracing instead of key-press-event binding. However, since in my case the change of one variable executes a function which changes the other variable, I would have expected the functions to execute one another in an infinite loop. This doesn't happen, but the desired outcome is definitely not produced.

import tkinter as tk
import tkinter.ttk as ttk

class App:

    def __init__(self, master, aspect_ratio):
        self.master = master
        self.aspect_ratio = aspect_ratio
        self.shape_x = tk.DoubleVar()
        self.shape_y = tk.DoubleVar()
        self.shape_x.trace('w', self.update_y)
        self.shape_y.trace('w', self.update_x)

        # Entry for x dimension
        self.entry_x = ttk.Entry(self.master, textvariable=self.shape_x, width=5)
        self.entry_x.pack(side='left', fill='x', expand='yes')

        # Label for multiplication sign
        self.entry_label = tk.Label(self.master, text='x', fg='gray')
        self.entry_label.pack(side='left')

        # Entry for y dimension
        self.entry_y = ttk.Entry(self.master, textvariable=self.shape_y, width=5)
        self.entry_y.pack(side='left', fill='x', expand='yes')

    def update_x(self, *args):
        self.shape_x.set(self.shape_y.get() * self.aspect_ratio)

    def update_y(self, *args):
        self.shape_y.set(self.shape_x.get() / self.aspect_ratio)

if __name__ == '__main__':        
    root = tk.Tk()
    app = App(master=root, aspect_ratio=2)
    root.mainloop()

What would be the right way to achieve what I am looking for?

Edit: replaced * in update_y with /

sunnytown
  • 1,844
  • 1
  • 6
  • 13
  • So the problem with this set up is that both functions will always be called. Because you are editing one of the other when one or the other is called. That also triggers another call. – Mike - SMT Oct 14 '19 at 16:15

3 Answers3

2

Arguably, the simplest solution is to bind the <KeyRelease> event, since the modifications happen on a key press. With that, you don't need the overhead of using the textvariable attribute.

self.entry_x.bind("<Any-KeyRelease>", self.update_y)
self.entry_y.bind("<Any-KeyRelease>", self.update_x)
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • This is probably the best solution. My answer only tries to handle the error of the issues with track instead of doing something simple that fixes the issue all together like this will. – Mike - SMT Oct 14 '19 at 18:09
0

Personally I would name my entry fields and just check what entry had focus at the time of edit. Due to both traces being activated we can use the focus to tell us what field to edit.

I had to build a try/except statement to handle the odd behavior of get() not pulling a value from the fields for whatever reason. When I tested with my print statements it appears to get the correct number but maybe due to the double call its failing the get().

Here is the example code:

import tkinter as tk
import tkinter.ttk as ttk


class App(tk.Tk):
    def __init__(self, aspect_ratio):
        super().__init__()
        self.aspect_ratio = aspect_ratio
        self.previous_x = 0
        self.previous_y = 0
        self.shape_x = tk.DoubleVar(self)
        self.shape_y = tk.DoubleVar(self)
        self.shape_x.trace('w', self.update_x_y)
        self.shape_y.trace('w', self.update_x_y)

        ttk.Entry(self, name='x', textvariable=self.shape_x, width=5).pack(side='left', fill='x', expand='yes')
        tk.Label(self, text='x', fg='gray').pack(side='left')
        ttk.Entry(self, name='y', textvariable=self.shape_y, width=5).pack(side='left', fill='x', expand='yes')

    def update_x_y(self, *args):
        name = self.focus_get().winfo_name()
        try:
            x = self.shape_x.get()
            y = self.shape_y.get()
            if name == 'x':
                self.previous_y = x * self.aspect_ratio
                self.shape_y.set(self.previous_y)
            elif name == 'y':
                self.previous_x = y * self.aspect_ratio
                self.shape_x.set(self.previous_x)

            else:
                print('Focus is not one of the 2 entry fields. Do nothing.')
        except:
            print('Handling odd error from the double call to the function returning empty string from get().')


if __name__ == '__main__':
    App(aspect_ratio=2).mainloop()
Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
-1

So, the way that I got around this is to have a value store which Entry widget was last updated and then have a "checker" function which checks in tiny intervals to see if either Entry has been updated and then updates the other:

from tkinter import *

class App:
    def __init__(self, root):
        self.root = root

        self.aspect = 2

        self.widthvalue = StringVar(name="width")
        self.widthvalue.set(1)
        self.heightvalue = StringVar(name="height")
        self.heightvalue.set(str(int(self.widthvalue.get())*self.aspect))

        self.widthvalue.trace("w", lambda name, index, mode, width=self.widthvalue: self.entrychange(width))
        self.heightvalue.trace("w", lambda name, index, mode, height=self.heightvalue: self.entrychange(height))

        self.width = Entry(self.root, textvariable=self.widthvalue)
        self.height = Entry(self.root, textvariable=self.heightvalue)

        self.width.pack()
        self.height.pack()

        self.lastupdated = None

        self.root.after(10, self.update)

    def entrychange(self, value):
        self.lastupdated = str(value)

    def update(self):
        if self.lastupdated != None:
            if self.lastupdated == "width" and self.widthvalue.get() != "" and self.widthvalue.get() != "0":
                self.heightvalue.set(str(int(int(self.widthvalue.get())/self.aspect)))
                self.height.update()
                self.lastupdated = None
            elif self.lastupdated == "height" and self.heightvalue.get() != "" and self.heightvalue.get() != "0":
                self.widthvalue.set(str(int(self.heightvalue.get())*self.aspect))
                self.width.update()
                self.lastupdated = None
        self.root.after(10, self.update)

root = Tk()
App(root)
root.mainloop()

Let's break this down.

The first thing we do is set an aspect ratio to keep to. In this case, we're saying that the height will always be double the width:

self.aspect = 2

Next, we create StringVar variables to store the values of the two Entry widgets and set their values to the absolute minimum:

self.widthvalue = StringVar(name="width")
self.widthvalue.set(1)
self.heightvalue = StringVar(name="height")
self.heightvalue.set(str(int(self.widthvalue.get())*self.aspect))

We then set up traces on these variables, so that each time they are updated we call a function entrychange. This function is relatively simple, it just updates another variable called self.lastupdated which stores either width or height depending on which Entry was last updated:

self.widthvalue.trace("w", lambda name, index, mode, width=self.widthvalue: self.entrychange(width))
self.heightvalue.trace("w", lambda name, index, mode, height=self.heightvalue: self.entrychange(height))

We then do some boring bits, draw the widgets, set their textvariable, pack them and set a default value for self.lastupdated:

self.width = Entry(self.root, textvariable=self.widthvalue)
self.height = Entry(self.root, textvariable=self.heightvalue)

self.width.pack()
self.height.pack()

self.lastupdated = None

After this, we kick start a .after() call. Which allows you to have continuous loops in tkinter:

self.root.after(10, self.update)

This starts the process of constantly running our update function.


The update function starts by checking whether or not either Entry has been updated:

if self.lastupdated != None:

It then checks to see which was last updated and ensures that the field isn't blank or equal to 0. This is because both blank values and 0 will mess with our maths (and also aren't valid sizes for an image):

if self.lastupdated == "width" and self.widthvalue.get() != "" and self.widthvalue.get() != "0":

We then set the value of the opposite Entry widget to be either double or half (depending on which Entry was changed), update the widget in the GUI and set the variable self.lastupdated back to its default:

self.heightvalue.set(str(int(int(self.widthvalue.get())/self.aspect)))
self.height.update()
self.lastupdated = None

For the other Entry we do more or less the same.

Ethan Field
  • 4,646
  • 3
  • 22
  • 43