¡Hola! Bienvenidos a este tutorial para profundizar en otros elementos y componentes que se pueden crear en una interfaz gráfica usando PyQt5. PyQt5 es 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 podrás añadir otros tipos de componentes a tus interfaces como diálogos personalizados, mensajes de error y componentes de interacción con el usuario más allá de los campos de texto.

Objetivos

  1. Complementar interfaces gráficas con más elementos de personalización a nuestra disposición a través de PyQt5.

Requerimientos Técnicos

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.

Instalación de Librerías

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.

Este tutorial es complementario al tutorial Creación de interfaces gráficas con PyQt5 y requiere que hayas hecho dicho tutorial y hayas construido una interfaz gráfica basada en dicho enunciado. Si no has completado ese tutorial, lo mejor es que lo revises antes de continuar aquí.

Elementos presentados: QDialog, QButtonBox , QComboBox

QDialog

Una de las opciones disponibles al momento de crear interfaces gráficas consiste en la creación de diálogos personalizados. Los diálogos ofrecen la posibilidad de incluir una lista de botones básicos por defecto y de personalizar de acuerdo con las necesidades.

Para ilustrar esto, vamos a crear un diálogo simple que le permita al usuario añadir una materia introduciendo los diferentes datos necesarios.

Lo primero es crear un nuevo módulo de nombre dialogo_nueva_materia.py y allí crearemos nuestra clase básica:

class Dialogo_nueva_materia(QDialog):

    def __init__(self, *args, **kwargs):
        super(Dialogo_nueva_materia, self).__init__(*args, **kwargs)
        self.setWindowTitle("Agregar nueva materia")

Heredamos directamente de QDialog, y a partir de aquí podemos definir la estructura básica. Vamos a indicar el título de la ventana y en nuestro módulo visor_materias.py que trabajamos en el anterior tutorial para añadir un nuevo botón e inicializar el diálogo que hemos creado, y l conectamos con una función que ejecute el diálogo en cuestión.

from .dialogo_nueva_materia import Dialogo_nueva_materia

class Aplicacion_Gui(QWidget):

...


    def inicializar_GUI(self):
       ...
        #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)
        self.boton_nueva_materia = QPushButton("Nueva Materia")
        self.boton_nueva_materia.clicked.connect(self.dialogo_nueva_materia)


        #Agregamos los botones a la caja de botones
        distr_caja_botones.addWidget(self.boton_retroceder)
        distr_caja_botones.addWidget(self.boton_avanzar)
        distr_caja_botones.addWidget(self.boton_nueva_materia)
        
       ...
        
        self.show()


      def dialogo_nueva_materia(self):
        dialogo =Dialogo_nueva_materia()
        dialogo.exec_()

Nuestra interfaz principal ahora se verá así:

Y al presionar el botón abriremos un diálogo vacío:

Ahora podemos empezar a construir el diálogo de acuerdo a nuestras necesidades.

QDialogButtonBox

Uno de los elementos que podemos usar para construir diálogos rápidamente en PyQt5 son los QDialogButtonBox, widgets que permiten usar una serie de botones pre-definidos para las interfaces gráficas en pocas líneas. Los siguientes botones están disponibles para usar:

QDialogButtonBox.Ok

QDialogButtonBox.Open

QDialogButtonBox.Save

QDialogButtonBox.Cancel

QDialogButtonBox.Close

QDialogButtonBox.Discard

QDialogButtonBox.Apply

QDialogButtonBox.Reset

QDialogButtonBox.RestoreDefaults

QDialogButtonBox.Help

QDialogButtonBox.SaveAll

QDialogButtonBox.Yes

QDialogButtonBox.YesToAll

QDialogButtonBox.No

QDialogButtonBox.NoToAll

QDialogButtonBox.Abort

QDialogButtonBox.Retry

QDialogButtonBox.Ignore

Para nuestro diálogo vamos a usar el botón Save y el botón Cancel. Entonces podemos agregarlos y dado que funcionan como flags, estos pueden concatenarse usando el operador |. Creamos entonces la distribución y nos aseguramos de que la señal de accepted o rejected de la caja de botones asociada a los botones correspondientes esté conectada con las funciones correspondientes del diálogo. Este es nuestro diálogo por el momento:

class Dialogo_nueva_materia(QDialog):

    def __init__(self, *args, **kwargs):
        super(Dialogo_nueva_materia, self).__init__(*args, **kwargs)
        self.setWindowTitle("Agregar nueva materia")

        #Creamos el distribuidor gráfico del diálogo
        self.distribuidor = QVBoxLayout()     

        #Creamos la caja de botones para Save y Cancel
        Q_Botones = QDialogButtonBox.Save | QDialogButtonBox.Cancel
        self.caja_botones = QDialogButtonBox(Q_Botones)
        
        #Conectamos las señales de los botones con las funciones del diálogo
        self.caja_botones.accepted.connect(self.accept)
        self.caja_botones.rejected.connect(self.reject)

        #Añadimos al distribuidor la caja de botones
        self.distribuidor.addWidget(self.caja_botones)

        #Añadimos el distribuidor al diálogo
        self.setLayout(self.distribuidor)   

Las funciones accept y reject son funciones de la súperclase QDialog. Con esto ya tenemos una ventana inicial con botones que proveen un nivel básico de interacción:

Por el momento no hay mucha diferencia entre salir del diálogo presionando Save o Cancel, pero por detrás, al presionar Save, el diálogo termina con la función accept, y al presionar Cancel con la función reject. Esto nos va a permitir personalizar nuestras funciones.

QComboBox

Vamos entonces a inicializar el resto de componentes del diálogo, usando un QGridLayout y lo que hemos aprendido del anterior tutorial

class Dialogo_nueva_materia(QDialog):

    def __init__(self, *args, **kwargs):
        super(Dialogo_nueva_materia, self).__init__(*args, **kwargs)
        self.setWindowTitle("Agregar nueva materia")

        #Creamos el distribuidor gráfico del diálogo
        self.distribuidor = QVBoxLayout()     

        #Creamos la caja de botones para Save y Cancel
        Q_Botones = QDialogButtonBox.Save | QDialogButtonBox.Cancel
        self.caja_botones = QDialogButtonBox(Q_Botones)

        #Conectamos las señales de los botones con las funciones del diálogo
        self.caja_botones.accepted.connect(self.accept)
        self.caja_botones.rejected.connect(self.reject)

        #Inicializamos y añadimos los otros componentes del diálogo
        self.inicializar_dialogo()

        #Añadimos al distribuidor la caja de botones
        self.distribuidor.addWidget(self.caja_botones)

        #Añadimos el distribuidor al diálogo
        self.setLayout(self.distribuidor)   

        
        
    
    def inicializar_dialogo(self):
        
        caja_campos =  QGroupBox("Ingrese los datos")
        distr_campos = QGridLayout()

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

        distr_campos.addWidget(self.etiqueta_nombre, 0,0)
        distr_campos.addWidget(self.txt_nombre, 0,1)
        distr_campos.addWidget(self.etiqueta_semestre, 1,0)
        distr_campos.addWidget(self.txt_semestre, 1,1)
        distr_campos.addWidget(self.etiqueta_profesor, 2,0)
        distr_campos.addWidget(self.txt_profesor, 2,1)
        distr_campos.addWidget(self.etiqueta_nota, 3,0)
        distr_campos.addWidget(self.txt_nota, 3,1)

        caja_campos.setLayout(distr_campos)
        self.distribuidor.addWidget(caja_campos)

El resultado es un diálogo como el que vemos a continuación:

Recordemos que debemos ser cuidadosos con el orden en el que agregamos los elementos. Si agregamos los botones antes que los campos, nuestro diálogo tendría los botones en la parte superior.


Aprovechemos este diálogo para usar un componente algo diferente: un QComboBox. Este elemento nos permite darle al usuario una lista de opciones de las que debe elegir una, y para ello debemos pasarle dichas opciones. Hagamos un par de cambios en nuestra función de inicialización:

def inicializar_dialogo(self):
        
        caja_campos =  QGroupBox("Ingrese los datos")
        distr_campos = QGridLayout()

        self.etiqueta_nombre = QLabel("Nombre")
        self.txt_nombre = QLineEdit()
        self.etiqueta_semestre = QLabel("Semestre")
        self.txt_semestre = QComboBox()
        self.txt_semestre.addItems(["2019-1","2019-2","2020-1"])
        self.etiqueta_profesor = QLabel("Profesor")
        self.txt_profesor = QLineEdit()
        self.etiqueta_nota = QLabel("Nota")
        self.txt_nota = QLineEdit()

        distr_campos.addWidget(self.etiqueta_nombre, 0,0)
        distr_campos.addWidget(self.txt_nombre, 0,1)
        distr_campos.addWidget(self.etiqueta_semestre, 1,0)
        distr_campos.addWidget(self.txt_semestre, 1,1)
        distr_campos.addWidget(self.etiqueta_profesor, 2,0)
        distr_campos.addWidget(self.txt_profesor, 2,1)
        distr_campos.addWidget(self.etiqueta_nota, 3,0)
        distr_campos.addWidget(self.txt_nota, 3,1)

        caja_campos.setLayout(distr_campos)
        self.distribuidor.addWidget(caja_campos)

Esto nos generará una lista desplegable en nuestro diálogo:

Si revisamos la documentación, debemos tener en cuenta que para obtener la opción elegida por el usuario, en lugar de usar la función text() como en el componente QLineEdit, debemos usar la función currentText()

Manejo de diálogos en la interfaz

Bien ahora sólo resta conectar nuestro diálogo con la interfaz gráfica para añadir la materia que cree el usuario. Primero, debemos ir al módulo organizador_materias para crear una función en la clase principal que agregue a la lista de materias los elementos:

def aniadir_materia(self, nueva_materia):
      self.lista_materias.append(nueva_materia)

Ahora en nuestro módulo dialogo_nueva_materia creamos una función en nuestra clase principal del diálogo para obtener los valores de entrada para la nueva materia así:

def dar_valores(self):
        return {"Nombre":self.txt_nombre.text(), "Semestre": self.txt_semestre.currentText(), "Profesor":self.txt_profesor.text(), "Nota":float(self.txt_nota.text())}

Y finalmente modificamos la función dialogo_nueva_materia en nuestro módulo de la interfaz gráfica para responder en caso de que el usuario grabe los valores:

def dialogo_nueva_materia(self):
        dialogo = Dialogo_nueva_materia()     
        if dialogo.exec_():
            self.logica.aniadir_materia(dialogo.dar_valores())

El resultado es un diálogo funcional que nos permite agregar materias (aunque no verifica los valores de las mismas). Este es el resultado final:

PyQt5 también nos permite crear mensajes predefinidos de forma ágil, sin necesidad de construir una clase para personalizarlos. PyQt ofrece por defecto los siguientes tipos de diálogos:

Question Question

Information Information

Warning Warning

Critical Critical

Y los botones corresponden a los que presentamos en la sección anterior.

Primero, vamos a crear un mensaje de confirmación para preguntarle al usuario si está seguro de guardar la nueva materia. Para esto vamos a sobre-escribir el método accept de nuestra clase Dialogo_nueva_materia así:

def accept(self):
        dialogo_confirmacion = QMessageBox()
        super().accept()

Con esto hemos creado solamente el diálogo base y luego llamamos al método accept de la súperclase QDialog. Para este diálogo usaremos un mensaje de tipo pregunta, con los botones Yes y No.

def accept(self):
        dialogo_confirmacion = QMessageBox()
        dialogo_confirmacion.setIcon(QMessageBox.Question)
        dialogo_confirmacion.setText("¿Está seguro que desea guardar los datos como una nueva materia?")
        dialogo_confirmacion.setWindowTitle("Confirmación")
        dialogo_confirmacion.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        super().accept()

Este código nos permite crear el diálogo en cuestión, sin embargo todavía no responde como quisiéramos. Si el usuario oprime Yes, quisiéramos que el diálogo terminara con un accept y si oprime no, que saliera sin guardar los datos. Así que debemos añadir un último condicional:

def accept(self):
        dialogo_confirmacion = QMessageBox()
        dialogo_confirmacion.setIcon(QMessageBox.Question)
        dialogo_confirmacion.setText("¿Está seguro que desea guardar los datos como una nueva materia?")
        dialogo_confirmacion.setWindowTitle("Confirmación")
        dialogo_confirmacion.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        if dialogo_confirmacion.exec_() == QMessageBox.Yes:
                super().accept()
        else:
                super().reject()

¡Y listo! Es importante notar como, si el usuario oprime no, estamos llamando directamente a la función reject para indicar que el diálogo terminó como si se hubiera cancelado la acción. Ahora, al darle save en nuestro diálogo deberíamos ver el siguiente mensaje:

Si le damos Yes, agregamos la nueva materia y si le damos No, no se realiza ningún cambio.

Ahora miremos otra situación para la que necesitaremos manejar mensajes de error. En nuestra clase Dialogo_nueva_materia vamos a añadir una función para verificar que si los campos se dejan vacíos, se lanza un mensaje de error:

def verificar_valores(self):
        if self.txt_nombre.text() == "" or self.txt_profesor.text()=="" or self.txt_nota.text()=="":
            dialogo_error = QMessageBox()
            dialogo_error.setIcon(QMessageBox.Critical)
            dialogo_error.setText("Los campos de la nueva materia no pueden estar vacíos")
            dialogo_error.setWindowTitle("Error")
            dialogo_error.setStandardButtons(QMessageBox.Ok)
            dialogo_error.exec_()
            return False
        return True

En esta ocasión usamos QMessageBox.Critical y dado que no hay nada que el usuario pueda hacer, solamente tendrá un botón de Ok. Para completar entonces nuestro método de accept añadimos entonces el llamado a la función que acabamos de crear:

def accept(self):
        dialogo_confirmacion = QMessageBox()
        dialogo_confirmacion.setIcon(QMessageBox.Question)
        dialogo_confirmacion.setText("¿Está seguro que desea guardar los datos como una nueva materia?")
        dialogo_confirmacion.setWindowTitle("Confirmación")
        dialogo_confirmacion.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        if self.verificar_valores() and dialogo_confirmacion.exec_() == QMessageBox.Yes:
                super().accept()
        else:
                super().reject()

Y vamos a probar. Si intentamos presionar el botón guardar sin escribir nada, obtendremos un mensaje de error:

Y nuestro diálogo personalizado se cerrará, además sin preguntarnos si queremos guardar los cambios (¡Lo cual es perfecto!). Con esto estamos listos para crear aplicaciones con mensajes informativos y de error.

Para saber más sobre los elementos de qt, puedes visitar la documentación general en este enlace.