¿La mejor manera de estructurar una aplicación tkinter?

136

La siguiente es la estructura general de mi programa típico de python tkinter.

def funA():
    def funA1():
        def funA12():
            # stuff

    def funA2():
        # stuff

def funB():
    def funB1():
        # stuff

    def funB2():
        # stuff

def funC():
    def funC1():
        # stuff

    def funC2():
        # stuff


root = tk.Tk()

button1 = tk.Button(root, command=funA)
button1.pack()
button2 = tk.Button(root, command=funB)
button2.pack()
button3 = tk.Button(root, command=funC)
button3.pack()

funA funBy funCabrirá otras Toplevelventanas con widgets cuando el usuario haga clic en los botones 1, 2, 3.

Me pregunto si esta es la forma correcta de escribir un programa de Python tkinter. Claro, funcionará incluso si escribo de esta manera, pero ¿es la mejor manera? Suena estúpido, pero cuando veo los códigos escritos por otras personas, su código no está desordenado con un montón de funciones y sobre todo tienen clases.

¿Existe alguna estructura específica que debamos seguir como buena práctica? ¿Cómo debo planificar antes de comenzar a escribir un programa de Python?

Sé que no existen las mejores prácticas en programación y tampoco las pido. Solo quiero algunos consejos y explicaciones para mantenerme en la dirección correcta, ya que estoy aprendiendo Python por mí mismo.

Chris Aung
fuente
2
Aquí hay un excelente tutorial sobre el diseño de la GUI de tkinter, con un par de ejemplos: python-textbok.readthedocs.org/en/latest / ... Aquí hay otro ejemplo con un patrón de diseño MVC: sukhbinder.wordpress.com/2014/12/ 25 / ...
Bondolin
12
Esta pregunta puede ser amplia, pero es útil yh como respuesta relativamente popular (en relación con casi todas las demás respuestas [tkinter]). Estoy nominando para reabrir, ya que veo que abrirlo es más útil que cerrarlo.
Bryan Oakley

Respuestas:

271

Abogo por un enfoque orientado a objetos. Esta es la plantilla con la que empiezo:

# Use Tkinter for python 2, tkinter for python 3
import tkinter as tk

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent

        <create the rest of your GUI here>

if __name__ == "__main__":
    root = tk.Tk()
    MainApplication(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

Las cosas importantes a tener en cuenta son:

  • No uso una importación de comodines. Importo el paquete como "tk", lo que requiere que prefijo todos los comandos con tk.. Esto evita la contaminación del espacio de nombres global, además hace que el código sea completamente obvio cuando está utilizando clases Tkinter, clases ttk o algunas propias.

  • La aplicación principal es una clase . Esto le brinda un espacio de nombres privado para todas sus devoluciones de llamada y funciones privadas, y generalmente facilita la organización de su código. En un estilo de procedimiento, debe codificar de arriba hacia abajo, definiendo funciones antes de usarlas, etc. Con este método no lo hace, ya que en realidad no crea la ventana principal hasta el último paso. Prefiero heredar tk.Framesolo porque normalmente empiezo creando un marco, pero de ninguna manera es necesario.

Si su aplicación tiene ventanas de nivel superior adicionales, le recomiendo hacer de cada una de ellas una clase separada, heredando de tk.Toplevel. Esto le brinda las mismas ventajas mencionadas anteriormente: las ventanas son atómicas, tienen su propio espacio de nombres y el código está bien organizado. Además, facilita poner cada uno en su propio módulo una vez que el código comienza a crecer.

Finalmente, es posible que desee considerar el uso de clases para cada parte importante de su interfaz. Por ejemplo, si está creando una aplicación con una barra de herramientas, un panel de navegación, una barra de estado y un área principal, puede crear cada una de esas clases. Esto hace que su código principal sea bastante pequeño y fácil de entender:

class Navbar(tk.Frame): ...
class Toolbar(tk.Frame): ...
class Statusbar(tk.Frame): ...
class Main(tk.Frame): ...

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.statusbar = Statusbar(self, ...)
        self.toolbar = Toolbar(self, ...)
        self.navbar = Navbar(self, ...)
        self.main = Main(self, ...)

        self.statusbar.pack(side="bottom", fill="x")
        self.toolbar.pack(side="top", fill="x")
        self.navbar.pack(side="left", fill="y")
        self.main.pack(side="right", fill="both", expand=True)

Dado que todas esas instancias comparten un padre común, el padre efectivamente se convierte en la parte "controlador" de una arquitectura modelo-vista-controlador. Entonces, por ejemplo, la ventana principal podría colocar algo en la barra de estado llamando self.parent.statusbar.set("Hello, world"). Esto le permite definir una interfaz simple entre los componentes, lo que ayuda a mantener el acoplamiento al mínimo.

Bryan Oakley
fuente
22
@Bryan Oakley, ¿conoces algún buen código de muestra en Internet que pueda estudiar su estructura?
Chris Aung
2
Secundo el enfoque orientado a objetos. Sin embargo, abstenerse de usar la herencia en su clase que llama a la GUI es una buena idea, en mi experiencia. Le ofrece más flexibilidad si los objetos Tk y Frame son atributos de una clase que no hereda de nada. De esta manera, puede acceder a los objetos Tk y Frame más fácilmente (y de manera menos ambigua), y destruir uno no destruirá todo en su clase si no lo desea. Olvidé la razón exacta por la que esto es vital en algunos programas, pero te permite hacer más cosas.
Brōtsyorfuzthrāx
1
¿No simplemente tener una clase te dará un espacio de nombres privado? ¿Por qué subclasificar el Marco mejora en eso?
gcb
3
@gcb: sí, cualquier clase te dará un espacio de nombres privado. ¿Por qué subclase de un marco? Normalmente voy a crear un marco de todos modos, por lo que es una clase menos para administrar (subclase de Marco, frente a una clase que hereda del objeto, con un marco como atributo). He reformulado la respuesta un poco para aclararlo. Gracias por la respuesta.
Bryan Oakley
2
@madtyn: no es necesario guardar una referencia parent, a menos que la vaya a usar más tarde. No lo guardé porque ninguno de los códigos en mi ejemplo requería que se guardara.
Bryan Oakley
39

Poner cada una de sus ventanas de nivel superior en su propia clase separada le permite reutilizar el código y una mejor organización del código. Cualquier botón y método relevante que esté presente en la ventana debe definirse dentro de esta clase. Aquí hay un ejemplo (tomado de aquí ):

import tkinter as tk

class Demo1:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.button1 = tk.Button(self.frame, text = 'New Window', width = 25, command = self.new_window)
        self.button1.pack()
        self.frame.pack()
    def new_window(self):
        self.newWindow = tk.Toplevel(self.master)
        self.app = Demo2(self.newWindow)

class Demo2:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.quitButton = tk.Button(self.frame, text = 'Quit', width = 25, command = self.close_windows)
        self.quitButton.pack()
        self.frame.pack()
    def close_windows(self):
        self.master.destroy()

def main(): 
    root = tk.Tk()
    app = Demo1(root)
    root.mainloop()

if __name__ == '__main__':
    main()

Ver también:

Espero que ayude.

alecxe
fuente
6

Esta no es una mala estructura; Funcionará bien. Sin embargo, debe tener funciones en una función para hacer comandos cuando alguien hace clic en un botón o algo

Entonces, lo que podría hacer es escribir clases para estos y luego tener métodos en la clase que manejen los comandos para los clics de los botones y tal.

Aquí hay un ejemplo:

import tkinter as tk

class Window1:
    def __init__(self, master):
        pass
        # Create labels, entries,buttons
    def button_click(self):
        pass
        # If button is clicked, run this method and open window 2


class Window2:
    def __init__(self, master):
        #create buttons,entries,etc

    def button_method(self):
        #run this when button click to close window
        self.master.destroy()

def main(): #run mianloop 
    root = tk.Tk()
    app = Window1(root)
    root.mainloop()

if __name__ == '__main__':
    main()

Por lo general, los programas tk con múltiples ventanas son múltiples clases grandes y en __init__todas las entradas, etiquetas, etc. se crean y luego cada método es manejar eventos de clic de botón

Realmente no hay una forma correcta de hacerlo, lo que sea que funcione para usted y haga el trabajo siempre que sea legible y puede explicarlo fácilmente porque si no puede explicar fácilmente su programa, probablemente haya una mejor manera de hacerlo .

Echa un vistazo a Pensar en Tkinter .

De serie
fuente
3
"Pensar en Tkinter" aboga por las importaciones mundiales, lo que creo que es un mal consejo.
Bryan Oakley el
1
Es cierto, no te sugiero que uses globales solo parte de la estructura de métodos de clase principal, tienes razón :)
Serie
2

OOP debería ser el enfoque y framedebería ser una variable de clase en lugar de una variable de instancia .

from Tkinter import *
class App:
  def __init__(self, master):
    frame = Frame(master)
    frame.pack()
    self.button = Button(frame, 
                         text="QUIT", fg="red",
                         command=frame.quit)
    self.button.pack(side=LEFT)
    self.slogan = Button(frame,
                         text="Hello",
                         command=self.write_slogan)
    self.slogan.pack(side=LEFT)
  def write_slogan(self):
    print "Tkinter is easy to use!"

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

ingrese la descripción de la imagen aquí

Referencia: http://www.python-course.eu/tkinter_buttons.php

Trevor
fuente
2
Solo se puede usar TKinteren Python 2. Recomendaría usar tkinterPython 3. También colocaría las últimas tres líneas de código debajo de una main()función y llamaría al final del programa. Yo definitivamente evitar el uso from module_name import *, ya que contamina el espacio de nombres global y puede reducir la legibilidad.
Zac
1
¿Cómo podría saber la diferencia entre button1 = tk.Button(root, command=funA)y button1 = ttk.Button(root, command=funA)si el tkintermódulo de extensión también se estaba importando? Con la *sintaxis, ambas líneas de código parecerían serlo button1 = Button(root, command=funA). No recomendaría usar esa sintaxis.
Zac
0

Organizar su aplicación usando la clase le facilita a usted y a otras personas que trabajan con usted depurar problemas y mejorar la aplicación fácilmente.

Puede organizar fácilmente su aplicación de esta manera:

class hello(Tk):
    def __init__(self):
        super(hello, self).__init__()
        self.btn = Button(text = "Click me", command=close)
        self.btn.pack()
    def close():
        self.destroy()

app = hello()
app.mainloop()
muyustan
fuente
-2

Probablemente, la mejor manera de aprender cómo estructurar su programa es leyendo el código de otras personas, especialmente si se trata de un gran programa al que han contribuido muchas personas. Después de mirar el código de muchos proyectos, debe tener una idea de cuál debería ser el estilo de consenso.

Python, como lenguaje, es especial porque hay algunas pautas sólidas sobre cómo debe formatear su código. El primero es el llamado "Zen de Python":

  • Hermoso es mejor que feo.
  • Explícito es mejor que implícito.
  • Simple es mejor que complejo.
  • Complejo es mejor que complicado.
  • Plano es mejor que anidado.
  • Escaso es mejor que denso.
  • La legibilidad cuenta.
  • Los casos especiales no son lo suficientemente especiales como para romper las reglas.
  • Aunque la practicidad supera la pureza.
  • Los errores nunca deben pasar en silencio.
  • A menos que sea silenciado explícitamente.
  • Ante la ambigüedad, rechaza la tentación de adivinar.
  • Debe haber una, y preferiblemente solo una, forma obvia de hacerlo.
  • Aunque esa manera puede no ser obvia al principio a menos que seas holandés.
  • Ahora es mejor que nunca.
  • Aunque no es a menudo mejor que la derecha ahora.
  • Si la implementación es difícil de explicar, es una mala idea.
  • Si la implementación es fácil de explicar, puede ser una buena idea.
  • Los espacios de nombres son una gran idea, ¡hagamos más de eso!

En un nivel más práctico, está PEP8 , la guía de estilo para Python.

Con eso en mente, diría que su estilo de código realmente no encaja, particularmente las funciones anidadas. Encuentre una manera de aplanarlos, ya sea utilizando clases o moviéndolos a módulos separados. Esto hará que la estructura de su programa sea mucho más fácil de entender.

Inbar Rose
fuente
12
-1 por usar el Zen de Python. Si bien todo es un buen consejo, no aborda directamente la pregunta que se hizo. Elimine el último párrafo y esta respuesta podría aplicarse a casi todas las preguntas de Python en este sitio. Es un buen consejo positivo, pero no responde la pregunta.
Bryan Oakley
1
@BryanOakley No estoy de acuerdo contigo en eso. Sí, el Zen de Python es amplio y puede usarse para abordar muchas preguntas. Mencionó en el párrafo final optar por clases o colocar las funciones en módulos separados. También mencionó PEP8, una guía de estilo para Python, con referencias a ella. Aunque no es una respuesta directa, creo que esta respuesta es creíble en el hecho de que menciona muchas rutas diferentes que se pueden tomar. Esa es solo mi opinión
Zac
1
Vine aquí buscando respuestas a esta pregunta específica. Incluso para una pregunta abierta, no puedo hacer nada con esta respuesta. Yo también de mí.
Jonathan
De ninguna manera, la pregunta es sobre estructurar una aplicación tkinter , nada sobre las pautas de estilo / codificación / zen. Fácil como citar a @Arbiter "Aunque no es una respuesta directa", entonces, NO es una respuesta. Esto es como "tal vez sí y tal vez no", con zen antepuesto.
m3nda
-7

Yo personalmente no uso el enfoque orientado a objeciones, principalmente porque a) solo se interpone en el camino; b) nunca lo reutilizará como módulo.

pero algo que no se discute aquí es que debe usar subprocesos o multiprocesamiento. Siempre. de lo contrario, su aplicación será horrible.

simplemente haga una prueba simple: inicie una ventana y luego busque alguna URL o cualquier otra cosa. los cambios son su interfaz de usuario no se actualizará mientras se realiza la solicitud de red. Es decir, su ventana de aplicación se romperá. depende del sistema operativo en el que se encuentre, pero la mayoría de las veces, no se volverá a dibujar, todo lo que arrastre sobre la ventana estará pegado en él, hasta que el proceso vuelva al circuito principal de TK.

gcb
fuente
44
Lo que dices simplemente no es cierto. He escrito cientos de aplicaciones basadas en tk, tanto personales como comerciales, y casi nunca he tenido que usar hilos. Los hilos tienen su lugar, pero simplemente no es cierto que debe usarlos al escribir programas tkinter. Si tiene funciones de larga ejecución, es posible que necesite hilos o multiprocesamiento, pero hay muchos, muchos tipos de programas que puede escribir que no necesitan hilos.
Bryan Oakley
Creo que si reformularas tu respuesta para ser un poco más claro al respecto, sería una mejor respuesta. También sería realmente útil tener un ejemplo canónico del uso de hilos con tkinter.
Bryan Oakley
no me importó ser la mejor respuesta aquí porque está un poco fuera de tema. pero tenga en cuenta que comenzar con threading / multip es muy fácil. si tienes que agregar más tarde, es una batalla perdida. y hoy en día, no hay absolutamente ninguna aplicación que nunca hable con la red. e incluso si ignora y piensa 'solo tengo un pequeño disco IO', mañana su cliente decide que el archivo vivirá en NFS y está esperando el IO de la red y su aplicación parece inactiva.
gcb
2
@ erm3nda: "todas las aplicaciones conectadas a la red o que escriban IO serán mucho más rápidas utilizando subprocesos o subprocesos" , eso simplemente no es cierto. Subprocesar no necesariamente hará que su programa sea más rápido, y en algunos casos lo hará más lento. En la programación de la GUI, la razón principal para usar hilos es poder ejecutar algún código que de otro modo bloquearía la GUI.
Bryan Oakley
2
@ erm3nda: No, yo soy no diciendo las discusiones no son necesarios en absoluto . Definitivamente son necesarios (bueno, hilos o multiprocesamiento) para muchas cosas. Es solo que hay una clase muy grande de aplicaciones GUI donde tkinter es adecuado pero donde los hilos simplemente no son necesarios. Y sí, "instaladores, blocs de notas y otras herramientas fáciles" entran en esa categoría. El mundo está compuesto de más de estas "herramientas fáciles" que de cosas como Word, Excel, Photoshop, etc. Además, recuerde que el contexto aquí es tkinter . Tkinter generalmente no se usa para aplicaciones muy grandes y complejas.
Bryan Oakley