miércoles, junio 27, 2007

Creación de una calculadora con Python y PyQt4

Introducción:
Vamos a emprender la "aventura" de crear una calculadora. Python nos va a ayudar con su introspección y sus tipos de datos diccionario, lista y tupla; Qt nos va a dar una mano con sus mecanismos de conexión y algunas propiedades útiles que define en su clase base principal, QObject.
Por último, y para hacer menos aburrido el tema, usaremos un autómata finito (aka. maquina de estados) para emular el comportamiento de esas calculadoras económicas de bolsillo, desarrolladas en algún país del este...
Diseño de la interfase:Para esto, utilizaremos el diseñador de Qt, en Debian el binario se llama designer (o designer-qt4 si se tiene Qt3 y Qt4 instalados).
Es una buena oportunidad de usar los layouts, en especial, uno que no usamos a menudo, el layout matricial, o de celdas (QGridLayout).
Luego de jugar un rato, podremos hacer algo como lo siguiente:

Donde hemos definido los nombres de los objetos, respetando un patrón, en este caso, los digitos, se llaman btn_digit_, ej: btn_digit_1, btn_digit_2, btn_digit_3, etc.

Desde tenemos algunos atajos muy útiles, CTRT+R para probar la interfase, F1 para obtener la información del Widget que tenemos seleccionado y CTRL+I, para acceder al diálogo de propiedades del Widget, donde editamos la paleta, las fuentes, dimensiones y demás propiedades netamente visuales.
El .ui de la interfase se puede descargar aquí: interfase.ui
En este ejemplo, he usado el mecanismo de herencia múltiple, no el de carga dinámica.
Para esto tenemos que compilar el .ui en un .py, para automatizar la tarea, suelo usar un Makefile, como este.
Lógica de la calculadora:
Si estuviésemos programando en Visual Basic, Delphi o cualquier otro entorno orientado a eventos, seguramente pondríamos el código de la aplicación en cada pulsación de cada botón, pero en este caso, utilizaremos un autómata.
Un automata finito se define como un alfabeto de entrada (en este caso, los botones de la calculadora), un conjunto de estados, un cojunto de transiciones y un conjunto de acciones asociadas a cada transición.
La maquina de estados se inicializa en algún estado inicial, y a medida que va recibiendo entrada, irá saltando de estado en estado, y realizando las funciones asociadas.
En este caso, la calculadora cuenta con un solo lugar de almacenamiento (o acumulador), además usa la pantalla como buffer temporal de los datos. Tendrá la capacidad de recordar una operación.
El automata consiste en un módulo a parte, llamado automata.py, que posee la tabla de transiciones, y la función que ejecuta las transiciones de acuerdo a la entrada.

automata = {
    'en_cero'   : {
        'cero':      ('cero', [] ),
        'digito':    ('primer_dig', ['establecer']),
        'operacion': ('operando', ['almacenar']),
        'igual':     ('cero',[]),
        'clear':     ('en_cero',['limpiar']),
    },
    'primer_dig': {
        'cero':      ('primer_dig',['actualizar']),
        'digito':    ('primer_dig',['actualizar']),
        'operacion': ('operando',  ['guardar_op', 'almacenar']),
        'igual':     ('resultado', ['almacenar']),
        'clear':     ('en_cero',['limpiar']),
    },
    'operando': {
        'cero':      ('cero_dos', ['establecer']),
        'digito':    ('segundo_dig', ['establecer']),
        'operacion': ('resultado', ['operar']),
        'igual':     ('resultado',['operar']),
        'clear':     ('en_cero',['limpiar']),
    },
    'cero_dos': {
        'cero':      ('cero_dos', []),
        'digito':    ('segundo_dig',['establecer']),
        'operacion': ('cero_dos', []),
        'iugal':     ('cero_dos', []),
        'clear':     ('en_cero',['limpiar']),
    },
    'segundo_dig' : {
        'cero':      ('segundo_dig', ['actualizar']),
        'digito':    ('segundo_dig', ['actualizar']),
        'operacion': ('resultado', ['guardar_op','operar']),
        'igual':     ('resultado', ['operar']),
        'clear':     ('en_cero',['limpiar']),
    },
    'resultado': {
        'cero':      ('en_cero', ['establecer']),
        'digito':    ('primer_dig', ['establecer']),
        'operacion': ('operando',['almacenar', 'guardar_op']),
        'igual':     ('resultado',[]),
        'clear':     ('en_cero',['limpiar']),
    },
}

Para generalizar esta lógica, utilizaremos las capacidades de introspección de Python.
¿Que es esto?
Bueno, es implemente dada una instanacia, obtener una función (que es un objeto después de todo, no?)
funcion = getattr(instancia, "nombre")
funcion(arg1, arg2)
Jejeje, muy fácil. De esta manera, para ejecutar el autómata tenemos una función muy simple:

def ejecutar_automata( caracter, vista ):
    '''Ejecuta la logia de la caluladora.'''
    entrada = tipo_entrada[str(caracter)]
    transicion = automata[ getattr(vista, "estado") ][entrada]
    # Realizar la lista de acciones relacionadas con el cambio de estado
    # si es que existen...
    if transicion[FUNCIONES]:
        for i in transicion[FUNCIONES]:
            if DEBUG:
                print "estado %s > %s(%s)" % (getattr(vista, "estado") ,i, caracter)
            accion = getattr(vista, i)
            accion(caracter)

    if DEBUG:
        print "Cambiando a %s" % (transicion[PROXIMO_ESTADO])
    setattr(vista, "estado", transicion[PROXIMO_ESTADO])
El código del autómata se puede ver completo en: automata.py
Vemos que la función recibe una vista como parámetro. Viene a ser algo así como un patrón visitante venido a menos.
Por último, tenemos que generar la aplicación y hereder de QMainWindow, relaizar los connects y ponerle un splash...

#! /usr/bin/python
# *-* encoding: utf-8 *-*

#
# Autor: Nahuel Deofossé (c) 2007
# Licencia: GPL
#

# Todo PyQt4 :)
from PyQt4.Qt import *

# Generado a través del editor de designar
# cuando se actualiza el GUI, se debe correr Make para que
# se transformen en código
from interfase import Ui_MainWindow
# Para los argumentos de linea de entrada
import sys
# Todas las definiciones del automata que hemos separado en otro modulo (py)
from automata import ejecutar_automata

# Algunas funciones utiles


class Calculadora(QApplication):
    '''Aplicacion calculadora simple'''
    def __init__(self, *argumentos):
        QApplication.__init__(self, *argumentos)
        splash_pxmap = QPixmap("./resources/splash.png")
        splash = QSplashScreen(splash_pxmap)
        splash.show()
        ventana = VentanaCalculadora()
        ventana.show()
        splash.finish(ventana)
        self.exec_()

class VentanaCalculadora(QMainWindow, Ui_MainWindow):
    '''GUI de la calculadora.'''
    estado = "en_cero"  # De acuerdo a los estados del automata
    valor = 0
    operacion = ""
    def __init__(self, padre = None):
        '''Constructor de la ventana'''
        QMainWindow.__init__(self, padre)
        self.setupUi(self)
        self.connect(self.actionSalir, SIGNAL("triggered()"), qApp.exit )
        # Conectamos los digitos
        for i in [  self.btn_digit_1, self.btn_digit_2,
                    self.btn_digit_3, self.btn_digit_4,
                    self.btn_digit_5, self.btn_digit_6,
                    self.btn_digit_7, self.btn_digit_8,
                    self.btn_digit_9, self.btn_digit_0,
                    ]:
            self.connect(i, SIGNAL("clicked()"), self.in_digito)
    # Ahora conectamos las operaciones

    def in_digito(self):
        '''Pulsado de un digito'''
        num = self.sender().objectName()[-1:]
        ejecutar_automata(num, self)

    def on_btn_op_add_pressed(self):
        ejecutar_automata("+", self)

    def on_btn_op_sub_pressed(self):
        ejecutar_automata("-", self)

    def on_btn_op_mul_pressed(self):
        ejecutar_automata("*", self)

    def on_btn_op_div_pressed(self):
        ejecutar_automata("/", self)

    def on_btn_op_res_pressed(self):
        ejecutar_automata("=", self)

    def on_btn_clear_pressed(self):
        ejecutar_automata("c", self)



    # Funciones que llama el automata
    def actualizar(self, c):
        '''Agrega un caracter al display '''
        self.lineResultado.setText( self.lineResultado.text() + c )

    def establecer(self, c):
        '''Establece el valor del display'''
        self.lineResultado.setText(c)
    def operar(self, c):
        pass

    def almacenar(self, c):
        self.valor = float(self.lineResultado.text())

    def guardar_op(self,c):
        self.operacion = c

    def operar(self, c):
        result = 0
        self.valor2 = float(self.lineResultado.text())
        if self.operacion == "+":
            result = self.valor + self.valor2
        elif self.operacion == "-":
            result = self.valor - self.valor2
        elif self.operacion == "/":
            result = self.valor / self.valor2
        elif self.operacion == "*":
            result = self.valor * self.valor2
        else:
            print "Nada"
        self.lineResultado.setText(str(result))

    def limpiar(self, c):
        self.lineResultado.setText("0")
        self.valor = 0


if __name__ == "__main__":
    app = Calculadora(sys.argv)
Como verán, el código es muy simple y corto. Se puede extender con facilidad.
Hemos visto varias cosas interesantes de Python y de PyQt.
Un diagrama, de lo que sucede sería como el siguietnte:

En el rectángulo verde vemos el conjunto de los eventos, el rectángulo rojo es el autómta y sobre la esquina superior derecha, el rectángulo naranja es el estado que se mantiene en la vista... y las funciones que cuelgan, son las que el autómata utiliza como interface para modificar el estado de la vista...
Finalmente, habrán notado que he mezclado el estado (modelo) con la vista, en este caso, generar una separación complicaba las cosas -llevando las cosas fuera del scope de lo que quería mostrar-.

El código completo se encuentra en:
https://github.com/D3f0/calculadora/edit/master/README

jueves, junio 07, 2007

Virtualización Libre

Luego de ser usuario de VMWare por algún tiempo, decidí probar VirtualBox, la cual, venía en cómodos deb's y hoy ya con repo propio.
La instalación fue muy sencilla, constando solo de un dpkg -i. Solito compiló sus modulos, los instaló sin chistar, aún con mi kernel altamente patchado.
La interfase (en Qt, haaaaaaaaaa :) ) es muy clara, y la instalación del SO huésped es muy sencilla.
Les voy dejando algunas imágenes:
Más tarde
Más tarde...

Más tarde, no cometería el atroz error de usar IE, así que...
Uno de los Shot que utilicé para la charla de SVN/trac que dimos ayer en la facu...
Por úlitmo, hoy actualizada, vía apt la última version, 1.4

Mejor que VMWare? En velocidad es un poco más lenta, casi imperceptible, pero tiene muchas comodidades que la hacen más accesible para el usuario poco experimentado. Por ejemplo, el administrador de discos, que nos permite intercambiar los discos asignados a una máquina virtual con otra.
Creo que vale la pena probarla. Quizás para muchos el factor de desición es que es descarga gratuita!

Ir al sitio >>