Personal Website

My Web: MindEchoes.com

Tuesday, May 19, 2009

PyQt: Algunos Trucos (Mini Tutorial)

Bueno, estos dias estuve jugando con PyQt, y queria postear un par de cosas por si le llega a servir a alguien.

Primero que nada PyQt es un binding de la libreria grafica Qt para Python (y es Multiplataforma).
Vamos a mostrar como crear una aplicación basica con PyQt usando un par de cosas interesantes:

Primero que nada las librerias que necesitamos importar en nuestro archivo que llamaremos "pyExample.py":

import sys
from PyQt4 import QtGui, QtCore


Importando el modulo de "sys" podemos usar varias variables y funciones para manipular varias partes del runtime del ambiente de Python.
Importando QtGui estaremos trayendo los modulos necesarios para trabajar con los componentes graficos, y QtCore nos sera necesario para el manejo de la conexión de señales en este caso.

Ahora creamos nuestra Clase y su constructor:

Codigo fuente

class PyQtExample(QtGui.QMainWindow):

def __init__(self):
QtGui.QMainWindow.__init__(self)
self.resize(550, 100)
self.setWindowTitle("Main Program")

style = self.style()
self.setWindowIcon(style.standardIcon(
QtGui.QStyle.SP_ComputerIcon))

self.languages = {}
self.languages['c'] = 'cout << "Hello World";'
self.languages['java'] = 'System.out.println("Hello World");'
self.languages['python'] = 'print "Hello World"'

self.create_widgets()
self.create_menu()
self.create_radio_menu()

Esta clase "PyQtExample" hereda de la clase QtGui.QMainWindow para ser un contenedor del tipo ventana que se mostrara en la pantalla.
Luego con:

self.resize(550, 100)
self.setWindowTitle("Main Program")


Le damos el tamaño a la ventana (ancho, alto) y el titulo de la ventana ("Main Program") respectivamente.

Con el siguiente codigo consigo utilizar los recursos de las imagenes standard que posee Qt (las cuales pueden ser consultadas aqui)
Obtengo una instancia de "style", y luego seteo el icono de la ventana usando los Standard Pixmap con los que cuenta Qt.

style = self.style()
self.setWindowIcon(style.standardIcon(
QtGui.QStyle.SP_ComputerIcon))

Y con el código restante todo lo que se hace es crear un diccionario para utilizarlo luego en otra parte de la aplicacion, y se llama a 3 funciones que se veran a continuación para setear distintos aspectos de la interfaz y el programa.

-----

Ahora agregamos a nuestra clase 3 nuevas funciones, la primera de ellas "create_widgets":

Codigo fuente

def create_widgets(self):
self.label = QtGui.QLabel("Results")
self.text = QtGui.QLineEdit()
self.exit = QtGui.QPushButton('&Close')

QtCore.QObject.connect(self.exit,
QtCore.SIGNAL("clicked()"),
self, QtCore.SLOT("close()"))

h_box = QtGui.QHBoxLayout()
h_box.addWidget(self.label)
h_box.addWidget(self.text)
h_box.addWidget(self.exit)
central_widget = QtGui.QWidget()
central_widget.setLayout(h_box)
self.setCentralWidget(central_widget)

Analizando la función "create_widget" vemos que con el siguiente código creo 3 elementos, un Label, un Campo de Texto y un Boton:

self.label = QtGui.QLabel("Results")
self.text = QtGui.QLineEdit()
self.exit = QtGui.QPushButton('&Close')


Luego le agrego comportamiento al boton uniendo la "señal" de cuando el boton es presionado, al fragmento de código ya predefinido de cerrar la ventana que posee la ventana.

QtCore.QObject.connect(self.exit, QtCore.SIGNAL("clicked()"),
self, QtCore.SLOT("close()"))


De esta forma como se ve dentro de la función "connect" estoy teniendo los siguientes parametros: "Objeto que emite la señal", "señal que emite(y se desea conectar)", "Objeto que va a recibir la señal", "Acción/Código a ejecutar".
Luego con el código restante seteamos un Layout a utilizar para definir como se acomodaran los componentes graficamente en la interfaz, le agregamos a ese layout los componentes que habiamos creado. Luego creamos un Widget, le asignamos ese Layout al Widget y terminamos agregando ese Widget a la ventana principal:

h_box = QtGui.QHBoxLayout()
h_box.addWidget(self.label)
h_box.addWidget(self.text)
h_box.addWidget(self.exit)
central_widget = QtGui.QWidget()
central_widget.setLayout(h_box)
self.setCentralWidget(central_widget)


Luego creamos la siguiente función "create_menu":

Codigo fuente

def create_menu(self):
self.menuBar = QtGui.QMenuBar(self)
self.setMenuBar(self.menuBar)
self.menu = self.menuBar.addMenu('&Menu')

actionC = self.menu.addAction('c')
QtCore.QObject.connect(actionC, QtCore.SIGNAL("triggered()"),
self.print_from_c)

actionJava = self.menu.addAction('java')
QtCore.QObject.connect(actionJava, QtCore.SIGNAL("triggered()"),
lambda: self.print_from_language('java'))

actionPython = self.menu.addAction('python')
QtCore.QObject.connect(actionPython, QtCore.SIGNAL("triggered()"),
lambda: self.repeat_from_language('python', 3))

En esta función primero se crea una Barra de Menu, se agrega esa Barra de Menu a la ventana principal y luego a esta Barra se le agrega un Menu.

self.menuBar = QtGui.QMenuBar(self)
self.setMenuBar(self.menuBar)
self.menu = self.menuBar.addMenu('&Menu')


Como se puede ver al menu que se agrega se le pasa la cadena '&Menu' esto indica que a ese menu podremos acceder presionando "Alt+M".

Luego se ven 3 conceptos interesantes, se crean 3 acciones (se podria trabajar de otra forma, pero se hace asi a modo de ejemplo), esas acciones son agregadas al menu como parte de este y luego se conecta la señal que producen esas acciones al ser ejecutadas con cierto código.
Cabe aclarar que en los menus solo se pueden agregar Acciones o Menus, pero las Acciones en realidad pueden tomar la forma que necesitemos para representar otros componentes como RadioButton, CheckButton, etc. Como se vera mas adelante.

En el primer caso:

actionC = self.menu.addAction('c')
QtCore.QObject.connect(actionC, QtCore.SIGNAL("triggered()"),
self.print_from_c)


Creamos nuestra acción "actionC" en base a la acción que es devuelta al agregar una acción al menu directamente a través de "addAction" y luego conectamos esa acción, con su señal "triggered()" (que para los menus es cuando son presionados) junto con una función dentro de nuestra clase que recibira el nombre de "print_from_c".
Hay que tener en cuenta que la función que estamos seteando que sera conectada a la señal no lleva los parentesis, es decir:

self.print_from_c [CORRECTO]
self.print_from_c() [INCORRECTO]

Ademas otra cosa a tener en cuenta, es que como veremos mas adelante al definir esta función, la misma no recibe ningun argumento, es por eso que no presenta problema al realizar esta conexión, pero si quisieramos que se invocara una función que recibe argumentos, estariamos en el segundo caso:

actionJava = self.menu.addAction('java')
QtCore.QObject.connect(actionJava, QtCore.SIGNAL("triggered()"),
lambda: self.print_from_language('java'))


En este caso se hace lo mismo que antes, se crea la acción y luego se conecta su señal con una función, pero como en este caso necesitamos llamar a una función que recibe argumentos, y estos no pueden especificarse directamente en el metodo "connect" por la forma en que se especifican las funciones a ser conectadas, se recurre al uso de "lambda". Como se ve en el código para la conexión de "actionJava" se utiliza "lambda" para que se tome esta como la función a utilizar, siendo que ahi mismo definimos el comportamiento de lambda y por lo tanto podemos utilizar llamadas a funciones con la cantidad de argumentos que necesitemos.

En el caso anterior se llama a una función con 1 argumento, pero bien podria hacerse con 2 argumentos como el caso de "actionPython" o con cualquier otra cantidad de argumentos segun definamos.

actionPython = self.menu.addAction('python')
QtCore.QObject.connect(actionPython, QtCore.SIGNAL("triggered()"),
lambda: self.repeat_from_language('python', 3))


Es muy importante mencionar, que en Python se permite conectar una señal de un componente a cualquier función de Python, sin necesidad de especificar que dicha función es un SLOT como deberia hacerse en Qt/C++.

Luego definimos la ultima función que nos estaba faltando: "create_radio_menu":

Codigo fuente

def create_radio_menu(self):
self.menuRadio = self.menuBar.addMenu('Menu &Radio')
self.radio1 = self.menuRadio.addAction('Results (en)')
self.radio2 = self.menuRadio.addAction('Resultados (es)')

group = QtGui.QActionGroup(self.menuRadio)
self.radio1.setCheckable(True)
self.radio2.setCheckable(True)
self.radio1.setActionGroup(group)
self.radio2.setActionGroup(group)
self.radio1.setChecked(True)

QtCore.QObject.connect(self.radio1,
QtCore.SIGNAL("triggered()"),
lambda: self.label.setText('Results'))
QtCore.QObject.connect(self.radio2,
QtCore.SIGNAL("triggered()"),
lambda: self.label.setText('Resultados'))

Esta función como podemos ver, crea un Menu utilizando la misma tecnica empleada en la función anterior, y luego crea 2 acciones que agregara a este menu:

self.menuRadio = self.menuBar.addMenu('Menu &Radio')
self.radio1 = self.menuRadio.addAction('Results (en)')
self.radio2 = self.menuRadio.addAction('Resultados (es)')


Hasta este momento estas son acciones simples como las definidas anteriormente, pero si deseamos que nuestras acciones cumplieran la función de un CheckButton en el menu utilizariamos la función:

self.radio1.setCheckable(True)

De esta forma nuestra acción "radio1" pasaria a convertirse en un CheckButton situado en el menu, pero si quisieramos en realidad que nuestras 2 acciones ingresadas fueran 2 RadioButton donde solo uno a la vez pueda estar seleccionado, solo necesitariamos crear un ActionGroup y setearla a nuestras acciones la pertenencia a este grupo, tal como se ve en el código siguiente:

group = QtGui.QActionGroup(self.menuRadio)
self.radio1.setCheckable(True)
self.radio2.setCheckable(True)
self.radio1.setActionGroup(group)
self.radio2.setActionGroup(group)
self.radio1.setChecked(True)


Luego, lo unico que queda es conectar dichos componentes con su respectivo comportamiento, y eso es lo que hace el código siguiente utilizando el recurso "lambda" para realizar la operación que se desea directamente en ese lugar sin tener que andar llamando a otras funciones:

QtCore.QObject.connect(self.radio1,
QtCore.SIGNAL("triggered()"),
lambda: self.label.setText('Results'))
QtCore.QObject.connect(self.radio2,
QtCore.SIGNAL("triggered()"),
lambda: self.label.setText('Resultados'))


-----
Ahora vamos a definir las 3 funciones utilizadas en la función "create_menu":

Codigo fuente

def print_from_c(self):
self.text.clear()
self.text.setText(self.languages['c'])

def print_from_language(self, lang):
self.text.clear()
self.text.setText(self.languages[lang])

def repeat_from_language(self, lang, num):
self.text.clear()
self.text.setText(self.languages[lang] * num)

El código en estas funciones es simple y no requiera de mucha explicación, simplemente se presentan los 3 casos mencionados anteriormente de funciones sin argumentos, con 1 argumento y con 2 argumentos, las cuales eliminan primero el contenido del componente de Texto y luego le setean el contenido dependiendo del valor que devuelve el diccionario definido dentro de la clase. En el caso de la ultima función se imprime ese valor devuelto por el diccionario un numero "n" de veces especificado por el argumento "num" recibido por la función.

-----
Para ir finalizando, vamos a mostrar como hariamos si quisieramos redefinir una función de la clase de la que estamos heredando para que, por ejemplo, al intentar cerrar la aplicación se ejecute algún comportamiento definido por nosotros:

Codigo fuente

def closeEvent(self, event):
reply = QtGui.QMessageBox.question(self, "Exit",
"Quit Application?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)

if reply == QtGui.QMessageBox.Yes:
event.accept()
else:
event.ignore()

Mediante este código, al intentar cerrarse la aplicación con la X, o con el Boton "Close" definido por nosotros, aparecera un mensaje que nos preguntara si queremos salir de la aplicación o no, y dependiendo si nuestra respuesta es positiva o negatica, se aceptara o se rechazara dicho evento.

-----
Ahora si para terminar, una vez escrita nuestra clase, solo debemos agregar al final de nuestro archivo, sin identación, el siguiente código para hacer ejecutable nuestra aplicación:

Codigo fuente

app = QtGui.QApplication(sys.argv)
main = PyQtExample()
main.show()

sys.exit(app.exec_())

Ahora solo queda ejecutar nuestra aplicación escribiendo en consola: "python [nombre_archivo]" Y LISTO!!

El archivo fuente generado para este ejemplo puede ser descargado en este link.
Cualquier sugerencia o mejora es Bienvenida!

Ejemplo realizado con:
Python Version: 2.6.2
Qt Version: 4.5

No comments: