Как сделать кадры tkinter в цикле и обновить значения объекта? - PullRequest
5 голосов
/ 11 октября 2019

У меня есть класс с именем Bones В моем словаре skeleton есть 5 Bones. Однако в моей реальной реализации есть более 300 костей, поэтому я задаю этот вопрос сегодня о stackoverflow.

Каждый Bone имеет:

  • ID: от Int доидентифицировать кость
  • w: позиция w (с плавающей точкой от -1 до 1)
  • x: позиция x (с плавающей точкой от -1 до 1)
  • y: позиция y (с плавающей точкой от -1 до 1)
  • z: позиция z (с плавающей точкой от -1 до 1)

Bone.py

INCREMENT = 0.01

class Bone:
    def __init__(self, boneId, w, x, y, z):
        self.id = boneId
        self.w = w
        self.x = x
        self.y = y
        self.z = z

    def shouldChangePos(self, num):
        if (num >= 1 or num <= -1):
            return False
        return True

    def incrW(self):
        if(self.shouldChangePos(self.w)):
            self.w = self.w + INCREMENT

    def decrW(self):
        if(self.shouldChangePos(self.w)):
            self.w = self.w - INCREMENT

    def incrX(self):
        if(self.shouldChangePos(self.x)):
            self.x = self.x + INCREMENT

    def decrX(self):
        if(self.shouldChangePos(self.x)):
            self.x = self.x - INCREMENT

    def incrY(self):
        if(self.shouldChangePos(self.y)):
            self.y = self.y + INCREMENT

    def decrY(self):
        if(self.shouldChangePos(self.y)):
            self.y = self.y - INCREMENT

    def incrZ(self):
        if(self.shouldChangePos(self.z)):
            self.z = self.z + INCREMENT

    def decrZ(self):
        if(self.shouldChangePos(self.z)):
            self.z = self.z - INCREMENT

Объяснение проблемы

Я пытаюсь создать графический интерфейс tkinter, который выглядит примерно так:

mock-up drawing of gui

Обозначения:

  • Зеленый - представляет Frame (только моя аннотация, чтобы объяснить)
  • Красный - естьатрибуты объекта (только моя аннотация для объяснения)
  • черный - это методы объекта (просто моя аннотация для объяснения)
  • синий - текст и кнопки, отображаемые мне

Как видите, это шдолжен ID, w, x, y, z. И под ним есть кнопка + и кнопка - . При каждом нажатии этих кнопок я хочу уменьшить соответствующее значение в объекте и обновить отображаемое число tkinter. Я знаю, как сделать это вручную, но по моему требованию у меня 300+ Bones. Я не могу сделать эти кадры вручную.

Как я могу создать эти кадры в цикле и обновить значение, отображаемое в графическом интерфейсе и объекте, когда + или - нажата кнопка?


main.py

from tkinter import *
from tkinter import ttk
from Bone import *

skeleton = {
    1: Bone(-0.42, 0.1, 0.02, 0.002, 0.234),
    4: Bone(4, 0.042, 0.32, 0.23, -0.32),
    11: Bone(11, 1, -0.23, -0.42, 0.42),
    95: Bone(95, -0.93, 0.32, 0.346, 0.31),
}


root = Tk()
root.geometry('400x600')

boneID = Label(root, text="ID: 1")
boneID.grid(row=1, column=1, sticky=W, padx=(0, 15))

w = Label(root, text="-0.42")
w.grid(row=1, column=2, sticky=W)

x = Label(root, text="0.02")
x.grid(row=1, column=4, sticky=W)

y = Label(root, text="0.002")
y.grid(row=1, column=6, sticky=W)

z = Label(root, text="0.234")
z.grid(row=1, column=8, sticky=W)

wPlusBtn = Button(root, text="+")
wPlusBtn.grid(row=2, column=2)
wMinusBtn = Button(root, text="-")
wMinusBtn.grid(row=2, column=3, padx=(0, 15))

xPlusBtn = Button(root, text="+")
xPlusBtn.grid(row=2, column=4)
xMinusBtn = Button(root, text="-")
xMinusBtn.grid(row=2, column=5, padx=(0, 15))

yPlusBtn = Button(root, text="+")
yPlusBtn.grid(row=2, column=6)
yMinusBtn = Button(root, text="-")
yMinusBtn.grid(row=2, column=7, padx=(0, 15))

zPlusBtn = Button(root, text="+")
zPlusBtn.grid(row=2, column=8)
zMinusBtn = Button(root, text="-")
zMinusBtn.grid(row=2, column=9, padx=(0, 15))

root.mainloop()

Ответы [ 3 ]

1 голос
/ 11 октября 2019

TL; DR - разбейте одну большую проблему на несколько более мелких, а затем решите каждую проблему отдельно.


Главное окно

Начните сглядя на общий дизайн интерфейса. У вас есть два раздела: панель с костями и панель с произвольным текстом. Поэтому первое, что я хотел бы сделать, это создать эти панели в виде фреймов:

root = tk.Tk()
bonePanel = tk.Frame(root, background="forestgreen", bd=2, relief="groove")
textPanel = tk.Frame(root, background="forestgreen", bd=2, relief="groove")

Конечно, вам также нужно использовать pack или grid, чтобы расположить их на окне. Я рекомендую pack, поскольку имеется только две рамки, и они расположены рядом.

Отображение костей

Для панели с костями у вас, кажется, есть один ряд для каждой кости. Итак, я рекомендую создать класс для представления каждой строки. Он может наследовать от Frame и отвечать за все, что происходит внутри этого ряда. Унаследовав от Frame, вы можете обращаться с ним как с пользовательским виджетом в отношении размещения его на экране.

Цель состоит в том, чтобы код вашего пользовательского интерфейса выглядел примерно так:

bones = (
    Bone(boneId=1,  w=-0.42, x=0.02,  y=0.002, z=0.234),
    Bone(boneId=4,  w=0.042, x=0.32,  y=0.23,  z=-0.32),
    Bone(boneId=11, w=1,     x=-0.23, y=-0.42, z=0.42),
    ...
)

bonePanel = tk.Frame(root)
for bone in bones:
    bf = BoneFrame(bonePanel, bone)
    bf.pack(side="top", fill="x", expand=True)

Опять же, вы можете использовать grid, если хотите, но pack кажется естественным выбором, поскольку ряды располагаются сверху вниз.

Отображение одной кости

Теперь нам нужно решить, что делает каждый BoneFrame. Похоже, он состоит из пяти разделов: раздел для отображения идентификатора, а затем четыре почти идентичные разделы для атрибутов. Поскольку единственное различие между этими разделами - это атрибут, который они представляют, имеет смысл представлять каждый раздел как экземпляр класса. Опять же, если класс наследует от Frame, мы можем рассматривать его так, как будто это был пользовательский виджет.

На этот раз мы должны передать кость и, возможно, строку, сообщающую ей, какой идентификатор обновлять.

Итак, может показаться, что выглядело бы примерно так:

class BoneFrame(tk.Frame):
    def __init__(self, master, bone):
        tk.Frame.__init__(self, master)

        self.bone = bone

        idlabel = tk.Label(self, text="ID: {}".format(bone.id))
        attr_w = BoneAttribute(self, self.bone, "w")
        attr_x = BoneAttribute(self, self.bone, "x")
        attr_y = BoneAttribute(self, self.bone, "y")
        attr_z = BoneAttribute(self, self.bone, "z")

pack - хороший выбор, поскольку все эти разделы выстроены слева направо, но вы могли быиспользуйте grid, если хотите. Единственное реальное отличие состоит в том, что использование grid занимает еще пару строк кода для настройки веса строк и столбцов.

Виджеты для кнопок и меток атрибутов

Наконец, мы должны занятьсяBoneAttribute класс. Здесь мы наконец добавляем кнопки.

Это довольно просто и следует той же схеме: создайте виджеты, а затем разместите их. Там немного больше, хотя. Нам нужно подключить кнопки, чтобы обновить кость, а также обновлять метку всякий раз, когда кость меняется.

Я не буду вдаваться во все детали. Все, что вам нужно сделать, это создать ярлык, пару кнопок и функции для вызова кнопок. Кроме того, мы хотим, чтобы функция обновляла метку при изменении значения.

Давайте начнем с функции обновления метки. Поскольку мы знаем имя атрибута, мы можем выполнить простой поиск, чтобы получить текущее значение и изменить метку:

class BoneAttribute(tk.Frame):
    ...
    def refresh(self):
        value = "{0:.4f}".format(getattr(self.bone, self.attr))
        self.value.configure(text=value)

С этим мы можем обновить метку, когда захотим.

Теперь нужно просто определить, что делают кнопки. Есть лучшие способы сделать это, но простой, прямой способ - это просто иметь несколько операторов if. Вот как может выглядеть функция приращения:

...
plus_button = tk.Button(self, text="+", command=self.do_incr)
...

def do_incr(self):
    if self.attr == "w":
        self.bone.incrW()
    elif self.attr == "x":
        self.bone.incrX()
    elif self.attr == "y":
        self.bone.incrY()
    elif self.attr == "z":
        self.bone.incrZ()

    self.refresh()

Функция do_decr идентична, за исключением того, что она вызывает один раз из функций декремента.

И это все. Ключевой момент здесь состоит в том, чтобы разбить вашу более крупную проблему на более мелкие, а затем решать каждую более мелкую проблему по одной. Независимо от того, есть ли у вас три кости или 300, единственный дополнительный код, который вам нужно написать, - это то, где вы изначально создаете объекты кости. Код пользовательского интерфейса остается точно таким же.

1 голос
/ 11 октября 2019

Здесь есть две проблемы: создание рамок в цикле и обновление значений после нажатия кнопок +/-.

Для решения проблемы с рамкой я предлагаю вам создать BoneFrame класс, который содержит все виджеты (кнопки и метки), связанные с одним Bone экземпляром. Там вы также можете привязать кнопки к методам Bone, чтобы воздействовать на значения. Примерно так - я уверен, что вы знаете, как завершить это с другими переменными и координатами сетки, которые вам нужныBoneFrame каждый раз и pack или grid этого экземпляра в родительский контейнер. Возможно, вы захотите добавить bone_id к параметрам BoneFrame.__init__ и передать его в цикле.

# In your main script
for bone_id, bone in skeleton.items():
    frame = BoneFrame(root, bone)
    frame.pack()

Пока значения в метке никогда не обновляются. Это потому, что мы просто устанавливаем их текст один раз, а потом никогда не обновляем их. Вместо того, чтобы связывать кнопки непосредственно с методами Bone, мы можем определить в BoneFrame более сложные методы, которые достигают большей логики, включая обновление значений, а также обновление виджетов. Вот один из способов сделать это:

class BoneFrame(tk.Frame):
    def __init__(self, parent, bone):
        super().__init__(parent)

        # Store the bone to update it later on
        self.bone = bone

        # Instantiate a StringVar in order to be able to update the label's text
        self.x_var = tk.StringVar()
        self.x_var.set(self.bone.x)

        self.x_label = tk.Label(self, textvariable=self.x_var)
        self.x_incr_button = tk.Button(self, text="+", action=self.incr_x)

        ...

    def incr_x(self):
        self.bone.incr_x()
        self.x_var.set(self.bone.x)

Итак, нам нужно StringVar, чтобы обновить содержимое метки. Подводя итог, вместо привязки кнопки к bone.incr_x, мы привязываем ее к self.incr_x, что позволяет нам делать все, что мы хотим при нажатии кнопки, то есть 1. изменять значение в экземпляре Bone,и 2. обновить значение, отображаемое на ярлыке.

0 голосов
/ 11 октября 2019

Обычным способом решения этой проблемы является создание функций (или методов класса) для выполнения повторяющихся битов кода (т. Е. Принцип DRY разработки программного обеспечения).

По иронии судьбы, выполнение этого само по себе может быть немного утомительным, так как я быстро обнаружил, что попытаться реорганизовать существующий код таким образом - но ниже приведен результат, который должен дать вам хорошее представление о том, как это можно сделать.

Помимо сокращения объема кода, который вы должны написать, это также упрощает внесение изменений или добавление улучшений, поскольку они выполняются только в одном месте. Часто самое сложное - определить, какие аргументы передать функциям, чтобы они могли делать то, что нужно, общим способом и избежать жестко закодированных значений.

from tkinter import *
from tkinter import ttk
from Bone import *

skeleton = {
    1: Bone(1, -0.42, 0.02, 0.002, 0.234),
    4: Bone(4, 0.042, 0.32, 0.23, -0.32),
    11: Bone(11, 1, -0.23, -0.42, 0.42),
    95: Bone(95, -0.93, 0.32, 0.346, 0.31),
}


def make_widget_group(parent, col, bone, attr_name, variable, incr_cmd, decr_cmd):
    label = Label(parent, textvariable=variable)
    label.grid(row=1, column=col, sticky=W)

    def incr_callback():
        incr_cmd()
        value = round(getattr(bone, attr_name), 3)
        variable.set(value)

    plus_btn = Button(parent, text='+', command=incr_callback)
    plus_btn.grid(row=2, column=col)

    def decr_callback():
        decr_cmd()
        value = round(getattr(bone, attr_name), 3)
        variable.set(value)

    minus_btn = Button(parent, text='-', command=decr_callback)
    minus_btn.grid(row=2, column=col+1, padx=(0, 15))


def make_frame(parent, bone):
    container = Frame(parent)

    boneID = Label(container, text='ID: {}'.format(bone.id))
    boneID.grid(row=1, column=1, sticky=W, padx=(0, 15))

    parent.varW = DoubleVar(value=bone.w)
    make_widget_group(container, 2, bone, 'w', parent.varW, bone.incrW, bone.decrW)

    parent.varX = DoubleVar(value=bone.x)
    make_widget_group(container, 4, bone, 'x', parent.varX, bone.incrX, bone.decrX)

    parent.varY = DoubleVar(value=bone.y)
    make_widget_group(container, 6, bone, 'y', parent.varY, bone.incrY, bone.decrY)

    parent.varZ = DoubleVar(value=bone.z)
    make_widget_group(container, 8, bone, 'z', parent.varZ, bone.incrZ, bone.decrZ)

    container.pack()


if __name__ == '__main__':

    root = Tk()
    root.geometry('400x600')

    for bone in skeleton.values():
        make_frame(root, bone)

    root.mainloop()

Снимок экрана: он работает:

Screenshot of it running show multiple rows create in a for loop

Кстати, я заметил много повторений в коде модуля Bone.py, которые, вероятно, можно уменьшить аналогичным образом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...