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:
1 |
cd /ruta/al/repo/pyboqt/apps/02-HolaMundo # En linux, MacOS |
1 |
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:
1 |
conda activate pyboqt |
Y una vez que el terminal esté ahí podréis hacer:
1 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
''' 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:
1 |
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:
1 |
conda install qtpy -c conda-forge |
Ahora, podéis hacer lo siguiente:
1 |
python /ruta/al/fichero.py # Linux o Mac |
o
1 |
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
1 2 3 |
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.
1 |
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,…).
1 |
if __name__ == '__main__': |
1 |
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:
1 |
['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.
1 2 3 4 5 |
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:
resize
para darle un tamaño a la ventana (500 x 300 píxeles),move
para colocar la ventana en la posición que queremos de nuestra pantalla (arriba a la izquierda en el ejemplo),setWindowTitle
nos permite poner un texto en la barra de arriba de la ventana y, por último, llamamos ashow
para que se muestre en pantalla ya que hasta ahora solo estaba en memoria.
1 |
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
exit
de la instancia deQApplication
lo procesará como corresponda y volverá a [2] a seguir esperando por nuevos eventos. - [4] Si el evento SÍ es llamar al método
exit
de la instancia deQApplication
entonces cerrará la aplicación y devolverá el mensaje de estátus (normalmente0
si 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
exit
de 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 metodoexit
de 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.
Muchas gracias por el curso en general y por bajar a tanto nivel de detalle. Ya tengo medio desarrollada una aplicación y me está sirviendo para pulir la GUI y para entender ciertos conceptos que había incorporado con un copypaste pero sin saber por qué.
Me alegro que te esté resultando útil. En cuanto encuentre algo de tiempo libre seguiré añadiendo más capítulos.
Saludos.