¡Hola! Bienvenidos a este tutorial para crear una interfaz gráfica de usuario para nuestros proyectos de Python.
Este tutorial te guiará en el uso de PyQt5, una librería de Python para crear interfaces gráficas de manera sencilla y práctica usando algunas líneas de código y los elementos visuales a los que estamos acostumbrados en los sistemas operativos.
Al final de este tutorial serás capaz de crear una aplicación de escritoria básica con una interfaz gráfica compuesta de ventanas, botones, etiquetas y campos de texto, que sea completamente funcional y que nos permita interactuar con la lógica de nuestro programa.
Para el siguiente tutorial es necesario tener Python 3 y pip instalado. Se recomienda un editor de código como Visual Studio Code o PyCharm.
Como se menciona en la introducción, el propósito de este tutorial es crear una interfaz gráfica para una aplicación sencilla.
Para este tutorial, queremos crear una aplicación que lleve el registro de las materias que hemos visto. Para ello, requerimos saber su nombre, el semestre en el que la vimos, el profesor que la dictó y la nota que obtuvimos. La idea es que la aplicación nos permita revisar todas las materias usando botones para navegar por cada pedazo de información.
La interfaz gráfica debería verse algo así:
Tenemos etiquetas, campos de texto no editables y dos botones, el botón "<<" para navegar hacia atrás y ">>" para navegar hacia adelante en la lista de materias.
En anteriores tutoriales hemos aprendido como crear la estructura inicial de directorios y archivos para un proyecto en Python. Utilizando PyScaffold empezaremos con una estructura de directorios así:
Si bien esto funciona para aplicaciones autocontenidas, necesitamos tomar una decisión con relación a la división de los componentes. Para este proyecto, haremos la división entre el paquete de la interfaz gráfica (gui
) y el paquete de la lógica (logica
) para nuestra aplicación en Python. Ambos paquetes tendrán su carpeta dentro de la estructura de archivos así:
Creamos entonces las carpeta gui
y logica
en src/mi_aplicacion
. Ahora, es necesario crear en cada paquete el archivo __init__.py
para indicarle a Python que se trata de paquetes de nuestra aplicación. Adicionalmente debemos crear fuera de ambas carpetas, pero en la raíz (mi_aplicacion
) el archivo __main__.py
que será el punto de entrada de nuestro programa.
Cada carpeta dentro de src
corresponde a un paquete en Python, y cada archivo dentro de ese paquete es un módulo. Para poder emplear nuestra interfaz gráfica, debemos crear el archivo __init__.py
en la carpeta gui con el siguiente contenido:
# -*- coding: utf-8 -*-
from pkg_resources import get_distribution, DistributionNotFound
try:
# Change here if project is renamed and does not equal the package name
dist_name = __name__
__version__ = get_distribution(dist_name).version
except DistributionNotFound:
__version__ = 'unknown'
finally:
del get_distribution, DistributionNotFound
Nuestro punto de entrada, __main__.py
, por el momento, no tiene ningún código funcional, pero esto lo iremos arreglando:
import sys
if __name__ == '__main__':
pass
Ahora debemos instalar PyQt5. Para esto, y habiendo activado previamente nuestro ambiente virtual, sólo debemos usar la instrucción:
pip install pyqt5
Una vez instalada, estamos listos para crear nuestra interfaz gráfica.
Repasemos la lógica de nuestra aplicación. El objetivo de este tutorial es concentrarnos en la interfaz gráfica, así que iremos al paquete logica
y modificaremos el módulo organizador_materias
para añadir la clase que modele las materias y que contenga la información inicial que queremos mostrar:
class Organizador_Materias():
def __init__(self):
self.lista_materias = []
self.lista_materias.append({"Nombre":"Introducción a la programación", "Semestre":"2019-1", "Profesor":"Antonio Andrade", "Nota":4.5})
self.lista_materias.append({"Nombre":"Estructuras de datos", "Semestre":"2019-2", "Profesor":"Hamid Abdallah", "Nota":3.5})
self.lista_materias.append({"Nombre":"Desarrollo de software", "Semestre":"2020-1", "Profesor":"Rubby Casallas", "Nota":2.0})
self.lista_materias.append({"Nombre":"Arquitectura de software", "Semestre":"2020-2", "Profesor":"Xiao Lihua", "Nota":4.0})
self.actual = 0
Hemos modelado una lista de diccionarios que contiene la información . Ahora completamos nuestra lógica con las funciones para retornar el valor actual de la lista, y avanzar y retroceder en la misma de forma circular (es decir, al llegar al final de la lista, volver al principio). Creamos entonces tres funciones de la lógica en la clase Organizador_Materias
:
def dar_materia_actual(self):
return self.lista_materias[self.actual]
def avanzar(self):
self.actual += 1
self.actual = self.actual % len(self.lista_materias)
def retroceder(self):
self.actual -= 1
self.actual = self.actual % len(self.lista_materias)
Podemos entonces pasar a crear la interfaz gráfica de nuestra aplicación.
Primero, debemos garantizar que nuestra aplicación correrá inicialmente como una aplicación con interfaz de PyQt5. Debemos entonces ir a nuestro punto de entrada __main__.py
y añadir las siguientes líneas:
import sys
from gui.visor_materias import Aplicacion_Gui
from PyQt5.QtWidgets import QApplication
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.gui = Aplicacion_Gui()
if __name__ == '__main__':
app = App(sys.argv)
sys.exit(app.exec_())
Con la creación de la clase App
, que hereda de QApplication
, garantizamos que se instancie la interfaz gráfica y pueda ser lanzada como tal. Y al modificar la línea después de if __name__ == '__main__':
estamos lanzando nuestra aplicación con los argumentos de la consola y ejecutándola.
Ahora vamos a nuestro módulo visor_materias.py en el paquete gui. Lo primero será importar los elementos que necesitaremos:
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGroupBox, QGridLayout, QLabel, QLineEdit, QVBoxLayout
Y creamos la clase que contendrá nuestra ventana principal, que hereda de QWidget, la clase que contendrá todos los elementos de nuestra interfaz. También necesitamos declarar los atributos iniciales que representan el título de la ventana (title), la posición inicial de la esquina superior izquierda de la ventana (left, top), y las dimensiones de la misma (width, height).
class Aplicacion_Gui(QWidget):
def __init__(self):
super().__init__()
#Se establecen las características de la ventana
self.title = 'Mi aplicación'
self.left = 80
self.top = 80
self.width = 300
self.height = 320
Para inicializar entonces creamos un método en donde definiremos la geometría inicial de la ventana y el título; para finalmente hacer visible la ventana (self.show()
). Invocamos la inicialización en el método __init__
.
class Aplicacion_Gui(QWidget):
def __init__(self, logic):
super().__init__()
#Se establecen las características de la ventana
self.title = 'Mi aplicación'
self.left = 80
self.top = 80
self.width = 300
self.height = 320
self.inicializar_GUI()
def inicializar_GUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.show()
Si corremos nuestra aplicación ahora, veremos entonces la ventana inicial en las dimensiones indicadas sin nada más:
Ya que tenemos la ventana inicial de la aplicación, nuestro objetivo ahora será definir el distribuidor gráfico (layout) de la ventana y de sus componentes. Veremos tres distribuidores básicos de PyQt5:
QHBoxLayout: Organiza los elementos de forma horizontal, de izquierda a derecha.
QVBoxLayout: Organiza los elementos de forma vertical, de arriba hacia abajo.
QGridLayout: Organiza los elementos en una cuadrícula indexable.
Para nuestra aplicación, usaremos QVBoxLayout
para los diferentes componentes o cajas que añadiremos al QWidget
principal.
def inicializar_GUI(self):
#inicializamos la ventana
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
#Creamos el distribuidor gráfico principal
self.distr_vertical = QVBoxLayout()
self.setLayout(self.distr_vertical)
#Hacemos la ventana visible
self.show()
Para nuestra aplicación final tendremos dos cajas (boxes) o agrupaciones para contener los demás elementos. Para esto usamos el elemento QGroupBox
. Si mandamos como parámetro un texto al constructor, estos grupos quedarán con un título definido. La caja superior que contendrá la información de la materias irá arriba, y la caja de los botones irá en la parte superior.
Cada una de estas cajas debe ser definida con su propio distribuidor gráfico, para la caja de las materias usaremos un QGridLayout
y para la caja de los botones un QHBoxLayout
.
def inicializar_GUI(self):
#inicializamos la ventana
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
#Creamos el distribuidor gráfico principal
self.distr_vertical = QVBoxLayout()
#Creamos la caja de materias
self.caja_materias = QGroupBox("Materia")
distr_caja_materias = QGridLayout()
self.caja_materias.setLayout(distr_caja_materias)
#Creamos la caja de botones
self.caja_botones = QGroupBox()
distr_caja_botones = QHBoxLayout()
self.caja_botones.setLayout(distr_caja_botones)
#Agregamos las cajas a nuestra aplicación
self.distr_vertical.addWidget(self.caja_materias)
self.distr_vertical.addWidget(self.caja_botones)
#Definimos el distribuidor principal de la ventana.
self.setLayout(self.distr_vertical)
#Hacemos la ventana visible
self.show()
Para definir el distribuidor de cada caja usamos la función setLayout
. Y para añadir cada caja al distribuidor principal, usamos la instrucción addWidget
, enviando como parámetro nuestras cajas. Si corremos nuestra aplicación veremos que la ventana comienza a tomar forma:
La primera caja quedó en la parte superior, y la caja de botones (a la que intencionalmente no pusimos título) quedó en la parte superior. Todavía no podemos ver los distribuidores de cada caja, pero al agregar elementos en el siguiente paso del tutorial veremos su impacto.
Vamos entonces a visitar los elementos necesarios para terminar de construir nuestra aplicación. Para visualizar la información de materias necesitamos etiquetas y campos textos. Ambos elementos se llaman QLabel
y QLineEdit
en PyQt, respectivamente. Ahora, será cuestión de crearlos y agregarlos a la caja de materias. Empecemos creándolos:
def inicializar_GUI(self):
#inicializamos la ventana
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
#Creamos el distribuidor gráfico principal
self.distr_vertical = QVBoxLayout()
#Creamos la caja de materias
self.caja_materias = QGroupBox("Materia")
distr_caja_materias = QGridLayout()
self.caja_materias.setLayout(distr_caja_materias)
#Creamos las etiquetas y campos de texto de la materia la caja de materias
self.etiqueta_nombre = QLabel('Nombre')
self.txt_nombre = QLineEdit()
self.etiqueta_semestre = QLabel('Semestre')
self.txt_semestre = QLineEdit()
self.etiqueta_profesor = QLabel('Profesor')
self.txt_profesor = QLineEdit()
self.etiqueta_nota = QLabel('Nota')
self.txt_nota = QLineEdit()
#Creamos la caja de botones
self.caja_botones = QGroupBox()
distr_caja_botones = QHBoxLayout()
self.caja_botones.setLayout(distr_caja_botones)
#Agregamos las cajas a nuestra aplicación
self.distr_vertical.addWidget(self.caja_materias)
self.distr_vertical.addWidget(self.caja_botones)
#Definimos el distribuidor principal de la ventana
self.setLayout(self.distr_vertical)
#Hacemos la ventana visible
self.show()
El constructor de cada etiqueta recibe como parámetro el texto que se mostrará. Lo mismo aplica para los campos de texto, pero por el momento los dejaremos vacíos. Ahora debemos agregar cada elemento a la caja de materias. Dado que la caja de materias tiene un distribuidor QGridLayout
, debemos indicarle las coordenadas en las que se debe agregar cada elemento, teniendo en cuenta que la esquina superior izquierda de la caja siempre será la coordenada 0,0. La primera coordenada representa la fila y la segunda la columna.
def inicializar_GUI(self):
#inicializamos la ventana
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
#Creamos el distribuidor gráfico principal
self.distr_vertical = QVBoxLayout()
#Creamos la caja de materias
self.caja_materias = QGroupBox("Materia")
distr_caja_materias = QGridLayout()
self.caja_materias.setLayout(distr_caja_materias)
#Creamos las etiquetas y campos de texto de la materia la caja de materias
self.etiqueta_nombre = QLabel('Nombre')
self.txt_nombre = QLineEdit()
self.etiqueta_semestre = QLabel('Semestre')
self.txt_semestre = QLineEdit()
self.etiqueta_profesor = QLabel('Profesor')
self.txt_profesor = QLineEdit()
self.etiqueta_nota = QLabel('Nota')
self.txt_nota = QLineEdit()
#Agregamos a la caja de materias las etiquetas
distr_caja_materias.addWidget(self.etiqueta_nombre, 0,0)
distr_caja_materias.addWidget(self.etiqueta_semestre, 1,0)
distr_caja_materias.addWidget(self.etiqueta_profesor, 2,0)
distr_caja_materias.addWidget(self.etiqueta_nota, 3,0)
#Agregamos a la caja de materias los campos de texto
distr_caja_materias.addWidget(self.txt_nombre, 0,1)
distr_caja_materias.addWidget(self.txt_semestre, 1,1)
distr_caja_materias.addWidget(self.txt_profesor, 2,1)
distr_caja_materias.addWidget(self.txt_nota, 3,1)
#Creamos la caja de botones
self.caja_botones = QGroupBox()
distr_caja_botones = QHBoxLayout()
self.caja_botones.setLayout(distr_caja_botones)
#Agregamos las cajas a nuestra aplicación
self.distr_vertical.addWidget(self.caja_materias)
self.distr_vertical.addWidget(self.caja_botones)
#Definimos el distribuidor principal de la ventana
self.setLayout(self.distr_vertical)
#Hacemos la ventana visible
self.show()
Miremos el resultado en la ventana de nuestra aplicación:
¡Fantástico! Algo que recordar es que el método addWidget asociado a un QGridLayout también puede recibir como parámetro el tamaño en filas y columnas que tiene un elemento. Si no especificamos el parámetro, PyQt asume 1 por defecto, como si hiciéramos:
addWidget(elemento, 0,1,1,1)
Pero si usáramos la instrucción:
addWidget(elemento, 0,1,2,1)
Esto indicaría que el elemento a insertar ocupa dos filas de alto y una columna de ancho.
Pasemos ahora a la caja de los botones. En este caso, para crear botones usamos el elemento QPushButton
de PyQt, que recibe como parámetro la etiqueta de los mismos. Y dado que la caja de los botones tiene un distribuidor simple que organiza los elementos horizontalmente en el orden de adición, podemos agregarlos sin indicar las coordenadas pero teniendo cuidado del orden de cada uno.
def inicializar_GUI(self):
#inicializamos la ventana
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
#Creamos el distribuidor gráfico principal
self.distr_vertical = QVBoxLayout()
#Creamos la caja de materias
self.caja_materias = QGroupBox("Materia")
distr_caja_materias = QGridLayout()
self.caja_materias.setLayout(distr_caja_materias)
#Creamos las etiquetas y campos de texto de la materia la caja de materias
self.etiqueta_nombre = QLabel('Nombre')
self.txt_nombre = QLineEdit()
self.etiqueta_semestre = QLabel('Semestre')
self.txt_semestre = QLineEdit()
self.etiqueta_profesor = QLabel('Profesor')
self.txt_profesor = QLineEdit()
self.etiqueta_nota = QLabel('Nota')
self.txt_nota = QLineEdit()
#Agregamos a la caja de materias las etiquetas
distr_caja_materias.addWidget(self.etiqueta_nombre, 0,0)
distr_caja_materias.addWidget(self.etiqueta_semestre, 1,0)
distr_caja_materias.addWidget(self.etiqueta_profesor, 2,0)
distr_caja_materias.addWidget(self.etiqueta_nota, 3,0)
#Agregamos a la caja de materias los campos de texto
distr_caja_materias.addWidget(self.txt_nombre, 0,1)
distr_caja_materias.addWidget(self.txt_semestre, 1,1)
distr_caja_materias.addWidget(self.txt_profesor, 2,1)
distr_caja_materias.addWidget(self.txt_nota, 3,1)
#Creamos la caja de botones
self.caja_botones = QGroupBox()
distr_caja_botones = QHBoxLayout()
self.caja_botones.setLayout(distr_caja_botones)
#Creamos los botones para la caja de botones
self.boton_retroceder = QPushButton("<<")
self.boton_avanzar = QPushButton(">>")
#Agregamos los botones a la caja de botones
distr_caja_botones.addWidget(self.boton_retroceder)
distr_caja_botones.addWidget(self.boton_avanzar)
#Agregamos las cajas a nuestra aplicación
self.distr_vertical.addWidget(self.caja_materias)
self.distr_vertical.addWidget(self.caja_botones)
#Definimos el distribuidor principal de la ventana
self.setLayout(self.distr_vertical)
#Hacemos la ventana visible
self.show()
Es importante recordar que en el QHBoxLayout
añade los elementos de izquierda a derecha. Miremos el resultado de nuestra aplicación:
¡Perfecto! Tenemos la estructura deseada. Podemos incluso jugar con los tamaños de las cajas si queremos mejorar la distribución de los elementos. Por ejemplo, sobre la caja de botones podemos invocar la siguiente instrucción:
self.caja_botones.setFixedHeight(50)
Para reducir su tamaño a 50 pixeles.
También, previendo que los campos de texto no deberían ser editables por el usuario podemos hacer que sean sólo de lectura con la siguiente instrucción:
self.txt_nombre.setReadOnly(True)
Puedes revisar la documentación de PyQt5 si quieres buscar otras propiedades útiles al momento de diseñar aplicaciones gráficas.
Bien, tenemos nuestro cascarón, aunque no es funcional. Para esto necesitamos establecer la comunicación con la lógica. En nuestro módulo de la interfaz entonces debemos modificar la función __init__
para que reciba como parámetro la lógica y la asigne en un atributo de nuestra clase Aplicacion_Gui
.
def __init__(self, logica):
super().__init__()
#Se establecen las características de la ventana
self.title = 'Mi aplicación'
self.left = 80
self.top = 80
self.width = 300
self.height = 320
#Inicializamos la ventana principal
self.inicializar_GUI()
#Asignamos el valor de la lógica
self.logica = logica
Y la creación de los objetos, junto con el paso de la lógica a la interfaz ocurrirá en nuestro módulo __main__.py
, nuestro punto de entrada. Así que en este archivo debemos añadir las líneas para instanciar Organizador_Materias
y Aplicacion_Gui
pasando como parámetro la lógica
import sys
from logica.organizador_materias import Organizador_Materias
from gui.visor_materias import Aplicacion_Gui
from PyQt5.QtWidgets import QApplication
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.logica = Organizador_Materias()
self.gui = Aplicacion_Gui(self.logica)
if __name__ == '__main__':
app = App(sys.argv)
sys.exit(app.exec_())
Y listo. Ahora podemos ir a la interfaz y crear un método que actualice los valores de la interfaz por nosotros:
class Aplicacion_Gui(QWidget):
def __init__(self, logica):
super().__init__()
#Se establecen las características de la ventana
self.title = 'Mi aplicación'
self.left = 80
self.top = 80
self.width = 300
self.height = 320
#Inicializamos la ventana principal
self.inicializar_GUI()
#Asignamos el valor de la lógica
self.logica = logica
self.actualizar_materia()
def actualizar_materia(self):
actual = self.logica.dar_materia_actual()
self.txt_nombre.setText(actual["Nombre"])
self.txt_semestre.setText(actual["Semestre"])
self.txt_profesor.setText(actual["Profesor"])
self.txt_nota.setText(str(actual["Nota"]))
Y listo. Al iniciar nuestra aplicación entonces veremos los datos de la primera materia tan sólo al correrla:
Todavía necesitamos hacer que los botones nos permitan navegar por las materias, no hemos terminado aún.
Finalmente necesitamos manejar los eventos de navegación de los botones. Para esto crearemos dos nuevas funciones en nuestro módulo visor_materias
, en la clase Aplicacion_Gui
:
class Aplicacion_Gui(QWidget):
def actualizar_materia(self):
actual = self.logica.dar_materia_actual()
self.txt_nombre.setText(actual["Nombre"])
self.txt_semestre.setText(actual["Semestre"])
self.txt_profesor.setText(actual["Profesor"])
self.txt_nota.setText(str(actual["Nota"]))
def avanzar_materia(self):
self.logica.avanzar()
self.actualizar_materia()
def retroceder_materia(self):
self.logica.retroceder()
self.actualizar_materia()
Estas funciones llaman a la lógica para avanzar o retroceder la materia actual y luego actualizan la interfaz para que se muestre la nueva materia actual.
Para conectar los eventos con los botones sólo necesitamos entonces usar la funcion clicked.connect
de la clase QPushButton
que recibe como parámetro la función a la que conectaremos el botón. Es decir que al momento de crear los botones debemos añadir dos líneas:
def inicializar_GUI(self):
...
#Creamos la caja de botones
self.caja_botones = QGroupBox()
distr_caja_botones = QHBoxLayout()
self.caja_botones.setFixedHeight(50)
self.caja_botones.setLayout(distr_caja_botones)
#Creamos los botones para la caja de botones
self.boton_retroceder = QPushButton("<<")
self.boton_retroceder.clicked.connect(self.retroceder_materia)
self.boton_avanzar = QPushButton(">>")
self.boton_avanzar.clicked.connect(self.avanzar_materia)
#Agregamos los botones a la caja de botones
distr_caja_botones.addWidget(self.boton_retroceder)
distr_caja_botones.addWidget(self.boton_avanzar)
...
#Hacemos la ventana visible
self.show()
¡Y listo! Con eso hemos terminado nuestra aplicación.
Con esto hemos terminado, si corremos nuestra aplicación desde nuestro editor podemos ver su funcionamiento.
Con eso ya estas listo para crear tus propias interfaces gráficas en PyQt5. ¡Muchos éxitos!
Para saber más sobre los elementos de qt, puedes visitar la documentación general en este enlace.