基于另一个条目小部件的内容更新条目小部件

Update entry widget based contents of another entry widget

我的代码包含两个条目小部件,它们应该保存图像的高度和宽度。 当用户在两个小部件之一中输入数字调整高度或宽度时,另一个的值应该根据输入的值和固定的纵横比进行调整。

我的第一种方法是将更新函数绑定到按键事件,但我遇到了导致在更改文本变量之前执行绑定函数的错误。该问题在此处描述:Tkinter, update Entry widget before calling <Key> binding。建议的解决方案是使用变量跟踪而不是按键事件绑定。但是,由于在我的例子中,一个变量的更改会执行一个更改另一个变量的函数,因此我希望这些函数在无限循环中相互执行。这并没有发生,但肯定没有产生想要的结果。

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

实现我所寻找的目标的正确方法是什么?

编辑:将 update_y 中的 * 替换为 /

所以,我解决这个问题的方法是有一个值存储,其中 Entry 小部件是最后更新的,然后有一个 "checker" 函数,它以微小的间隔检查是否 Entry已更新再更新其他:

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

我们来分解一下。

我们要做的第一件事是设置要保持的纵横比。在这种情况下,我们说高度将始终是宽度的两倍:

self.aspect = 2

接下来,我们创建 StringVar 变量来存储两个 Entry 小部件的值,并将它们的值设置为绝对最小值:

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

然后我们在这些变量上设置跟踪,以便每次更新它们时我们调用一个函数 entrychange。这个函数相对简单,它只是更新另一个名为 self.lastupdated 的变量,该变量存储 widthheight,具体取决于上次更新的 Entry

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

然后我们做一些无聊的事情,绘制小部件,设置它们的 textvariable,打包它们并设置默认值 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() 调用。这允许您在 tkinter:

中连续循环
self.root.after(10, self.update)

这将启动不断 运行 我们的 update 函数的过程。


更新函数首先检查 Entry 是否已更新:

if self.lastupdated != None:

然后它会检查哪个是最后更新的,并确保该字段不为空或不等于 0。这是因为空白值和 0 都会扰乱我们的数学计算(并且对于图片):

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

然后我们将相反的 Entry 小部件的值设置为双倍或一半(取决于更改了哪个 Entry),更新 GUI 中的小部件并设置变量 self.lastupdated 恢复为默认值:

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

对于另一个 Entry 我们做的差不多。

就我个人而言,我会命名我的条目字段,并在编辑时检查焦点所在的条目。由于两个跟踪都被激活,我们可以使用焦点来告诉我们要编辑的字段。

我必须构建一个 try/except 语句来处理 get() 出于某种原因不从字段中提取值的奇怪行为。当我用我的打印语句进行测试时,它似乎得到了正确的数字,但可能是由于两次调用导致 get() 失败。

示例代码如下:

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

可以说,最简单的解决方案是绑定 <KeyRelease> 事件,因为修改发生在 press 键上。这样,您就不需要使用 textvariable 属性的开销。

self.entry_x.bind("<Any-KeyRelease>", self.update_y)
self.entry_y.bind("<Any-KeyRelease>", self.update_x)