Tengo una clase llamada Bones Tengo 5 Bones en mi diccionario skeleton. Sin embargo, en mi implementación real hay más de 300 huesos, por eso estoy haciendo esta pregunta hoy en stackoverflow.

Cada Bone tiene:

  • ID: Una int para identificar un hueso
  • w: posición w (flotante entre -1 y 1)
  • x: posición x (flota entre -1 y 1)
  • y: posición y (flota entre -1 y 1)
  • z: posición z (flotante entre -1 y 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

Explicación del problema.

Estoy tratando de hacer una GUI tkinter que se vea así:

mock-up drawing of gui

Leyenda:

  • Verde: representa un Frame (solo mi anotación para explicar)
  • Rojo: son atributos del objeto (solo mi anotación para explicar)
  • Negro: son métodos del objeto (solo mi anotación para explicar)
  • Azul: me aparecen texto y botones

Como puede ver, muestra ID, w, x, y, z. Y debajo de él, hay un botón + y un botón - . Cada vez que se hace clic en estos botones, quiero disminuir el valor correspondiente en el objeto y actualizar el número tkinter que se muestra. Sé cómo hacer esto manualmente, pero según mis requisitos, tengo más de 300 Bones. No puedo hacer estos cuadros manualmente.

¿Cómo puedo crear estos marcos en un bucle y actualizar el valor que se muestra en la GUI y el objeto cuando se hace clic en un botón + o - ?


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()
5
Khloe 11 oct. 2019 a las 01:25

3 respuestas

La mejor respuesta

Aquí hay dos problemas: crear los cuadros en un bucle y actualizar los valores al presionar los botones +/-.

Para manejar el problema del marco, le sugiero que cree una clase BoneFrame que contenga todos los widgets (botones y etiquetas) relacionados con una instancia Bone. Allí, también puede vincular los botones a los métodos Bone para actuar sobre los valores. Algo así: estoy seguro de que sabrá cómo completar esto con las otras variables y las coordenadas de cuadrícula que desee

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

        # Create your widgets
        self.x_label = tk.Label(self, text=bone.x)
        self.x_decr_button = tk.Button(self, text="-", action=bone.decr_x)
        self.x_incr_button = tk.Button(self, text="+", action=bone.incr_x)
        ...

        # Then grid all the widgets as you want
        self.x_label.grid()
        ...

Luego, puede iterar fácilmente sobre su dict de Bone s, instanciar BoneFrame cada vez, y pack o grid esa instancia en un contenedor principal. Tal vez desee agregar un bone_id a los parámetros de BoneFrame.__init__ y pasarlo al bucle.

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

Por ahora, los valores en la etiqueta nunca se actualizan. Esto se debe a que solo configuramos su texto una vez y luego nunca los actualizamos. En lugar de vincular los botones directamente a los métodos de Bone, podemos definir métodos más complejos en BoneFrame que logran más lógica, incluida la actualización de los valores y también la actualización de los widgets. Aquí hay una forma de hacerlo:

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)

Por lo tanto, necesitamos un StringVar para actualizar el contenido de la etiqueta. Para resumir, en lugar de vincular el botón a bone.incr_x, lo vinculamos a self.incr_x, lo que nos permite hacer lo que queramos al presionar un botón, es decir, 1. cambiar el valor en el { {X3}}, y 2. actualice el valor que muestra la etiqueta.

1
Right leg 10 oct. 2019 a las 23:52

Una forma habitual de abordar este tipo de problema es crear funciones (o métodos de clase) para realizar los bits repetitivos del código (es decir, el DRY principio de ingeniería de software).

Irónicamente, hacer esto en sí mismo puede ser un poco tedioso, ya que rápidamente descubrí que tratar de refactorizar su código existente para que sea así, pero a continuación se muestra el resultado que debería darle una buena idea de cómo se puede hacer.

Además de reducir la cantidad de código que tiene que escribir, también simplifica hacer cambios o agregar mejoras porque solo se han hecho en un solo lugar. A menudo, lo más complicado es determinar qué argumentos pasar las funciones para que puedan hacer lo que se necesita hacer de forma genérica y evitar valores codificados.

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

Captura de pantalla de ella corriendo:

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

Por cierto, noté mucha repetición en el código del módulo Bone.py, que probablemente podría reducirse de manera similar.

0
martineau 11 oct. 2019 a las 03:06

TL; DR : divida su gran problema en varios problemas más pequeños y luego resuelva cada problema por separado.


La ventana principal

Comience mirando el diseño general de la interfaz de usuario. Tiene dos secciones: un panel que contiene huesos y un panel que contiene texto aleatorio. Entonces, lo primero que haría es crear estos paneles como marcos:

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

Por supuesto, también debe usar pack o grid para colocarlos en la ventana. Recomiendo pack ya que solo hay dos cuadros y están uno al lado del otro.

Mostrar huesos

Para el panel de hueso, parece tener una sola fila para cada hueso. Por lo tanto, recomiendo crear una clase para representar cada fila. Puede heredar de Frame y ser responsable de todo lo que sucede dentro de esa fila. Al heredar de Frame, puede tratarlo como un widget personalizado con respecto al diseño en la pantalla.

El objetivo es que su código de UI se vea así:

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)

Nuevamente, puede usar grid si lo desea, pero pack parece ser la opción natural ya que las filas se apilan de arriba a abajo.

Mostrar un solo hueso

Ahora, necesitamos abordar lo que cada BoneFrame hace. Parece estar compuesto por cinco secciones: una sección para mostrar la identificación y luego cuatro secciones casi idénticas para los atributos. Dado que la única diferencia entre estas secciones es el atributo que representan, tiene sentido representar cada sección como una instancia de una clase. Nuevamente, si la clase hereda de Frame podemos tratarla como si fuera un widget personalizado.

Esta vez, deberíamos pasar el hueso, y tal vez una cadena que le diga qué ID actualizar.

Entonces, podría comenzar luciendo así:

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 es una buena opción aquí ya que estas secciones están todas alineadas de izquierda a derecha, pero puede usar grid si lo prefiere. La única diferencia real es que usar grid requiere un par de líneas de código más para configurar los pesos de las filas y columnas.

Widgets para los botones y etiquetas de atributos

Finalmente, tenemos que abordar la clase BoneAttribute. Aquí es donde finalmente agregamos los botones.

Es bastante sencillo y sigue el mismo patrón: crea los widgets y luego colócalos. Sin embargo, hay un poco más. Necesitamos conectar los botones para actualizar el hueso, y también debemos actualizar la etiqueta cada vez que cambie el hueso.

No entraré en todos los detalles. Todo lo que necesita hacer es crear una etiqueta, un par de botones y funciones para que los botones llamen. Además, queremos una función para actualizar la etiqueta cuando cambia el valor.

Comencemos con la función para actualizar la etiqueta. Como conocemos el nombre del atributo, podemos hacer una búsqueda simple para obtener el valor actual y cambiar la etiqueta:

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

Con eso, podemos actualizar la etiqueta cuando queramos.

Ahora solo es cuestión de definir qué hacen los botones. Hay mejores formas de hacerlo, pero una forma simple y directa es tener algunas declaraciones if. Así es como se vería la función de incremento:

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

La función do_decr es idéntica, excepto que llama una vez a las funciones de disminución.

Y eso es todo. El punto clave aquí es dividir su problema más grande en problemas más pequeños, y luego abordar cada problema más pequeño uno a la vez. Ya sea que tenga tres huesos o 300, el único código adicional que tiene que escribir es donde inicialmente crea los objetos de hueso. El código de la interfaz de usuario permanece exactamente igual.

1
Bryan Oakley 10 oct. 2019 a las 23:46
58331886