En el anterior capítulo ya teníamos las maletas preparadas (instalación de nuestras herramientas) y empezamos a leer nuestra guía de viajes para nuestro viaje por el mundo de las aplicaciones de escritorio usando Python y Qt. En este capítulo voy a escribir el 'Hola, Mundo' de rigor para empezar a ver conceptos clave.
Índice:
- Instalación de lo que vamos a necesitar.
- Qt, versiones y diferencias.
- Hola, Mundo (este capítulo).
- Módulos en Qt.
- Añadimos icono a la ventana principal.
- Tipos de ventana en un GUI.
- Ventana de inicio - SplashScreen.
- Menú principal. Introducción.
- Mejorando algunas cosas vistas.
- Gestión de eventos o Acción y reacción.
- Introducción a Designer.
- Los Widgets vistos a través de Designer: Primera parte.
- Los Widgets vistos a través de Designer: Segunda parte.
- Los widgets vistos a través de Designer: Tercera Parte.
- Los widgets vistos a través de Designer: Cuarta Parte.
- Los widgets vistos a través de Designer: Quinta Parte.
- Los widgets vistos a través de Designer: Sexta parte.
- TBD… (lo actualizaré cuando tenga más claro los siguientes pasos).
[Los materiales para este capítulo los podéis descargar de aquí]
[INSTALACIÓN] Si todavía no has pasado por el inicio del curso, donde explico cómo poner a punto todo, ahora es un buen momento para hacerlo y después podrás seguir con esta nueva receta.
[AVISO] Cuando ejecute algo de código siempre voy a asumir que os encontráis en la carpeta correcta del repositorio y que tenéis activado el entorno virtual pyboqt. Todos los códigos estarán en la carpeta apps del repositorio. Por ejemplo, si voy a ejecutar un código del capítulo 02 (este capítulo) y os digo que lo ejecutéis desde una línea de comandos usando python programa_a_ejecutar.py siempre voy a asumir que la línea de comandos del terminal la tenéis localizada en la carpeta donde se encuentra el programa. Por ejemplo, si quiero ejecutar el programa main.py del capítulo 02 deberás estar en:
cd /ruta/al/repo/pyboqt/apps/02-HolaMundo # En linux, MacOS
cd C:\ruta\al\repo\pyboqt\apps\02-HolaMundo # En windows
Deberás tener el entorno virtual instalado y activado. Si aun no lo tienes instalado mira en los párrafos anteriores:
conda activate pyboqt
Y una vez que el terminal esté ahí podréis hacer:
python main.py
Para que se ejecute todo correcto.
Hola, Mundo
Vamos a escribir nuestro primer GUI con Python y Qt y vamos a aprender una serie de conceptos sobre los GUIs, Qt,... No os preocupéis si lo que vemos no tiene mucho sentido ahora mismo. Lo veremos más en detalle en próximos capítulos.
[Descargo de responsabilidad] Este GUI no va a hacer nada útil.
El Código
'''
Curso de creación de GUIs con Qt5 y Python
Author: Kiko Correoso
Website: pybonacci.org
Licencia: MIT
'''
import os
os.environ['QT_API'] = 'pyside2'
import sys
from qtpy.QtWidgets import QApplication, QWidget
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
w.resize(500, 300)
w.move(0, 0)
w.setWindowTitle('Hola, Mundo')
w.show()
sys.exit(app.exec_())
Ejecución del código
Guardad el anterior código en un fichero con extensión .py, por ejemplo, fichero.py. en la carpeta que queráis. Si habéis descargado el material del repositorio tendréis el anterior código en el fichero "/ruta/al/repo/pyboqt/apps/02-HolaMundo/main.py". Abrid un terminal o Anaconda Prompt (Windows) y escribid:
conda activate pyboqt
y pulsad 'Intro' para que se active el entorno virtual donde tenemos todo instalado.
En este momento vamos a instalar qtpy. Igual ya está instalado pero por si acaso. Para ello hacemos:
conda install qtpy -c conda-forge
Ahora, podéis hacer lo siguiente:
python /ruta/al/fichero.py # Linux o Mac
o
python C:\ruta\al\fichero.py # Windows
y pulsad 'Intro'. Si todo funciona correctamente os debería de salir una ventanita como la siguiente:

Enhorabuena. Has creado tu primer GUI con Qt5.
El código explicado
import os
os.environ['QT_API'] = 'pyside2'
import sys
Importamos la biblioteca sys (luego comento para qué la usamos) y establecemos la variable de entorno QT_API para usar PySide2 con ayuda de la biblioteca os (comenta las dos primeras líneas si prefieres usar PyQt5). De momento, esto no hace ninguna magia para mostrarnos GUIs en pantalla.
from qtpy.QtWidgets import QApplication, QWidget
Importamos QApplication y QWidget del módulo QtWidgets de PyQt5 o de PySide2.
QApplication: Cualquier aplicación debe crear una instancia de esta clase. Más información abajo.QWidget: Es la clase base de todos los widgets. Los widgets vienen a ser las interfaces que permiten interactuar al usuario con el GUI (botones, líneas de texto, deslizadores,...).
if __name__ == '__main__':
app = QApplication(sys.argv)
Aquí es donde creamos una instancia de QApplication. Todo GUI hecho con PyQt5/PySide2 debe tener una instancia de QApplication. Esta instancia nos proporciona acceso a información global como la carpeta de la aplicación, la hoja de estilos usada, el tamaño de la pantalla en la que corre el GUI,...
Si hiciéramos un print(dir(app)) obtendríamos lo siguiente:
['ApplicationFlags', 'ColorSpec', 'CustomColor', 'ManyColor', 'NormalColor', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'aboutQt', 'aboutToQuit', 'activeModalWidget', 'activePopupWidget', 'activeWindow', 'addLibraryPath', 'alert', 'allWidgets', 'allWindows', 'applicationDirPath', 'applicationDisplayName', 'applicationFilePath', 'applicationName', 'applicationNameChanged', 'applicationPid', 'applicationState', 'applicationStateChanged', 'applicationVersion', 'applicationVersionChanged', 'arguments', 'autoSipEnabled', 'beep', 'blockSignals', 'changeOverrideCursor', 'childEvent', 'children', 'clipboard', 'closeAllWindows', 'closingDown', 'colorSpec', 'commitDataRequest', 'connect', 'connectNotify', 'cursorFlashTime', 'customEvent', 'deleteLater', 'desktop', 'desktopSettingsAware', 'destroyed', 'devicePixelRatio', 'disconnect', 'disconnectNotify', 'doubleClickInterval', 'dumpObjectInfo', 'dumpObjectTree', 'dynamicPropertyNames', 'emit', 'event', 'eventDispatcher', 'eventFilter', 'exec_', 'exit', 'findChild', 'findChildren', 'flush', 'focusChanged', 'focusObject', 'focusObjectChanged', 'focusWidget', 'focusWindow', 'focusWindowChanged', 'font', 'fontDatabaseChanged', 'fontMetrics', 'globalStrut', 'hasPendingEvents', 'inherits', 'installEventFilter', 'installTranslator', 'instance', 'isEffectEnabled', 'isFallbackSessionManagementEnabled', 'isLeftToRight', 'isQuitLockEnabled', 'isRightToLeft', 'isSavingSession', 'isSessionRestored', 'isSetuidAllowed', 'isSignalConnected', 'isWidgetType', 'isWindowType', 'keyboardInputInterval', 'keyboardModifiers', 'killTimer', 'lastWindowClosed', 'layoutDirection', 'layoutDirectionChanged', 'libraryPaths', 'metaObject', 'modalWindow', 'mouseButtons', 'moveToThread', 'notify', 'objectName', 'objectNameChanged', 'organizationDomain', 'organizationDomainChanged', 'organizationName', 'organizationNameChanged', 'overrideCursor', 'palette', 'paletteChanged', 'parent', 'platformName', 'postEvent', 'primaryScreen', 'primaryScreenChanged', 'processEvents', 'property', 'queryKeyboardModifiers', 'quit', 'quitOnLastWindowClosed', 'receivers', 'registerUserData', 'removeEventFilter', 'removeLibraryPath', 'removePostedEvents', 'removeTranslator', 'restoreOverrideCursor', 'saveStateRequest', 'screenAdded', 'screenRemoved', 'screens', 'sendEvent', 'sendPostedEvents', 'sender', 'senderSignalIndex', 'sessionId', 'sessionKey', 'setActiveWindow', 'setApplicationDisplayName', 'setApplicationName', 'setApplicationVersion', 'setAttribute', 'setAutoSipEnabled', 'setColorSpec', 'setCursorFlashTime', 'setDesktopSettingsAware', 'setDoubleClickInterval', 'setEffectEnabled', 'setEventDispatcher', 'setFallbackSessionManagementEnabled', 'setFont', 'setGlobalStrut', 'setKeyboardInputInterval', 'setLayoutDirection', 'setLibraryPaths', 'setObjectName', 'setOrganizationDomain', 'setOrganizationName', 'setOverrideCursor', 'setPalette', 'setParent', 'setProperty', 'setQuitLockEnabled', 'setQuitOnLastWindowClosed', 'setSetuidAllowed', 'setStartDragDistance', 'setStartDragTime', 'setStyle', 'setStyleSheet', 'setWheelScrollLines', 'setWindowIcon', 'signalsBlocked', 'startDragDistance', 'startDragTime', 'startTimer', 'startingUp', 'staticMetaObject', 'style', 'styleHints', 'styleSheet', 'sync', 'testAttribute', 'thread', 'timerEvent', 'topLevelAt', 'topLevelWidgets', 'topLevelWindows', 'tr', 'translate', 'wheelScrollLines', 'widgetAt', 'windowIcon']
Si os fijáis, al instanciar QApplication le pasamos sys.argv como argumento. Esto nos permite recoger los argumentos que pasamos a través de la línea de comandos. En el caso anterior lo único que le hemos pasado a Python es el nombre del programa (python fichero.py) y ese sería el único argumento de entrada y que podemos obtener mediante sys.argv[0] (sys.argv es una lista).
QApplication puede recibir una serie de argumentos desde la línea de comandos que le permiten definir su estado interno. Algunos ya vienen predefinidos desde C++ (más información aquí). De momento nos vale con saber esto y ya veremos más sobre esto (o no) en otras recetas.
w = QWidget()
w.resize(500, 300)
w.move(0, 0)
w.setWindowTitle('Hola, Mundo')
w.show()
En el ejemplo creamos una instancia de QWidget, que es la ventana que vemos. A QWidget le podríamos pasar un objeto padre (un objeto al que pertenecería nuestra instancia w, luego veremos más en detalle qué significa esto). Como no le pasamos ningún padre esta será la ventana padre, ventana principal de nuestra aplicación o una ventana top level window. La instancia w dispone de varios métodos. En este caso usamos:
resizepara darle un tamaño a la ventana (500 x 300 píxeles),movepara colocar la ventana en la posición que queremos de nuestra pantalla (arriba a la izquierda en el ejemplo),setWindowTitlenos permite poner un texto en la barra de arriba de la ventana y, por último, llamamos ashowpara que se muestre en pantalla ya que hasta ahora solo estaba en memoria.
sys.exit(app.exec_())
Aquí llamamos al método exec_ de la instancia de QApplication. Notad que el método lleva una barra baja al final para diferenciarlo del exec de Python y que no haya conflictos. Cuando llamamos al método exec_ lo que estamos haciendo es iniciar el bucle de eventos o event loop.
¿Qué es el bucle de eventos?
Digamos que, desde el momento en que llamamos a app.exec_(), la aplicación inicia el bucle de eventos y su estado pasa a ser el de esperar/escuchar/estar pendiente de eventos/señales que vengan de la propia aplicación. Estos eventos/señales pueden ser cosas como un click en un botón, un cambio de estado de una celda de una tabla (cuyo widget en Qt es un QTableWidget), un timeout de un temporizador,..., y la reacción del GUI a estos mismos eventos.
El bucle de eventos estará 'escuchando' hasta que llamemos al método exit de la instancia app.
¿Por qué llamamos a app.exec_() dentro de sys.exit?
De esta forma nos aseguramos que el proceso finaliza correctamente o, si ha surgido un error, podemos devolver un código de error al sistema.
El bucle de eventos en más detalle
Voy a dejar un esquema de cómo funciona, grosso modo, el bucle de eventos principal del GUI. Este esquema está adaptado (más bien copiado) del libro de Mark Summerfield [1]:

En la anterior imagen:
- [1] indica que llamamos al método
exec_de la instancia deQApplication. - [2] Lo anterior provoca que se inicie el bucle de eventos y que nuestra aplicación se ponga a 'escuchar'. Si no ocurre ningún evento sigue esperando. Pero si ocurre un evento reacciona.
- [3] Si el evento NO es llamar al método
exitde la instancia deQApplicationlo procesará como corresponda y volverá a [2] a seguir esperando por nuevos eventos. - [4] Si el evento SÍ es llamar al método
exitde la instancia deQApplicationentonces cerrará la aplicación y devolverá el mensaje de estátus (normalmente0si ha ido todo correctamente).
Recapitulando un poco el ciclo de vida de un GUI
- La aplicación será una instancia de
QApplication. - En el momento que se ha instanciado la aplicación podemos llamar y mostrar nuestras ventanas.
- Arrancamos el bucle de eventos (event loop) de la ventana principal y Qt empieza a gestionar los eventos relacionados con la aplicación.
- Finalizamos la aplicación cuando llamamos al método
exitde la aplicación o cuando la cerramos, como en el caso del ejemplo, pulsando la 'X' de arriba a la derecha de la ventana o pulsando 'Alt+F4'. El hecho de pulsar es un evento que recibe el event loop y reacciona como le hayamos dicho. En este caso, la reacción es llamar al metodoexitde forma implícita vía el widget.
Terminando
Esto ya se ha puesto más interesante y hemos visto como crear nuestra primera ventana. Han surgido conceptos nuevos que se han explicado muy brevemente. Muchos de ellos los veremos en los próximos capítulos.
Estad atentos al siguiente capítulo.
Referencias
- Mark Summerfield, 2007: "Rapid GUI programming with Python and Qt: the definitive guide to PyQt programming". Prentice Hall.
P.D.: Si tienes alguna duda puedes usar los comentarios.
Pybonacci