Download 3 en raya en modo gráfico

Document related concepts
no text concepts found
Transcript
MIDDLEWARE
Pensando en Python (II):
3 en raya en modo gráfico
DIEGO LZ. DE IPIÑA GZ. DE ARTAZA (profesor del departamento de Ingeniería del Software de la facultad de
Ingeniería (ESIDE) de la Universidad de Deusto)
En esta segunda entrega de la serie
sobre Python aprenderemos a realizar
aplicaciones gráficas y a guardar de
manera persistente datos que han de
subsistir a la ejecución de un
programa. Extenderemos la
aplicación de tres en raya de la
anterior entrega, con una interfaz
gráfica y la capacidad de guardar en
ficheros los detalles de los jugadores
y sus estadísticas.
Introducción
La clave del éxito de toda aplicación profesional es
proveer una interfaz gráfica amigable que facilite
la interacción con el usuario y la explotación de su
funcionalidad. Además, la mayoría de los programas que usamos a diario deben guardar (o recordar) datos o preferencias utilizados en anteriores
ejecuciones de los mismos. Es decir, a menudo las
aplicaciones requieren de un mecanismo para
persistir datos a disco, siendo los mecanismos
más convencionalmente utilizados los ficheros o
bases de datos. En este artículo aprenderemos a
usar Python en el desarrollo de interfaces gráficas
y la serialización de datos en ficheros.
Pospondremos la programación de bases de datos
desde Python hasta la tercera entrega de esta
serie.
Programación gráfica en Python
Material complementario
El material complementario de
este artículo está disponible en
http://digital.revistasprofesionales.com
SOLO PROGRAMADORES nº 119
26
Python ofrece varias librerías de clases (o toolkits)
alternativas para la implementación de una aplicación gráfica. Entre otras, las más conocidas son:
Tkinter, PyGTK, PyQt, Pythonwin y wxPython. A
continuación las analizamos y elegimos una de
ellas, wxPython.
Tkinter es la toolkit para el desarrollo de interfaces
gráficas (GUIs) que acompaña a la distribución de
Python. La documentación sobre esta API es parte
de la Python Library Referente (http://www.
python.org/doc/current/lib/module-Tkinter.html).
Tkinter es una adaptación en Python de la API
gráfica tk ofrecida por el lenguaje de programación Tcl. Se distingue por ser multiplataforma y
venir integrada con la distribución estándar de
Python, no tendremos que instalar nada adicional.
Sin embargo, no presenta una apariencia nativa.
Es similar a Java Swing que independientemente
de la plataforma (Windows, UNIX o Mac) ofrece,
por defecto, la misma apariencia. Su uso es muy
sencillo, pero es lenta y ofrece un número limitado de controles gráficos. Carece de controles
avanzados como los paneles multipestaña, los
árboles de directorios o incluso controles más
sencillos, como el combobox. Pmw (Python meta
widgets) disponible en http://pmw.sourceforge.net,
define encima de Tkinter una serie de componentes más sofisticados. Aunque resuelve las limitaciones de controles de Tkinter, hereda su lentitud
y su apariencia no nativa.
Existen otras toolkits que definen finas capas de
Python encima de toolkits gráficas más conocidas
que tk tales como GTK y Qt en Linux y la Windows
API. Estos módulos Python son llamados PyGTK
(http://www.riverbankcomputing.co.uk/pyqt/),
PyQt (http://www.pygtk.org/) y Pythonwin
(http://www. python.org/windows/pythonwin/),
respectivamente. Su principal desventaja es que
están orientados a una plataforma específica
(UNIX o Windows), aunque las interfaces que producen son muy rápidas dado que interactúan
directamente con APIs nativas de la plataforma.
Su uso, es más complicado que en el caso de
Tkinter.
La toolkit wxPython (http://www.wxpython.org/)
es multiplataforma, presenta apariencia nativa y
ofrece un alto rendimiento gráfico. Está basada en
la API en C++ wxWidgets (http://www.wxwidgets.org/). wxWidgets ha venido representado
desde hace unos años una alternativa muy capaz
y sencilla tanto a las APIs de Windows: MFC
(Microsoft Foundation Classes) e incluso .NET
Windows Forms, como a las toolkits gráficas de
Linux: GTK y Qt. El combo wxWidgets/wxPython
nos permite realizar interfaces gráficas muy sofisticadas de una manera tan sencilla como Tkinter.
A juicio del autor wxPython es la mejor toolkit
para el desarrollo de interfaces gráficas con
Python. Combina eficiencia, con sencillez de uso y
nos permite escribir código fuente una sola vez
MIDDLEWARE
Pensando en Python (II): 3 en raya en modo gráfico
independientemente de la plataforma de explotación, adoptando tu aplicación la apariencia y
forma más apropiada a tu plataforma. En la
siguiente sección estudiamos esta toolkit en más
profundidad.
wxPython
wxWidgets fue creada para proveer una manera
barata y flexible de maximizar la inversión realizada en el desarrollo de aplicaciones con interfaz
gráfica. A pesar de que otras librerías de clases ya
existían para desarrollo multiplataforma, ninguna
cumplía los siguientes criterios:
Bajo precio
Código abierto (Open Source)
Facilidad de programación
Soporte de un amplio rango de compiladores
Hasta Febrero del 2004, wxWidgets era conocida
como wxWindows. Sin embargo, dadas las repercusiones legales que el uso del término
“Windows”, marca registrada por Microsoft,
podría tener, los creadores de wxWindows y
wxPython, Julian Smart y Robin Dunn, respectivamente, decidieron, previa sugerencia de Microsoft,
renombrar el proyecto como wxWidgets.
wxPython añade una simple capa de Python encima de wxWidgets, aislando la complejidad de la
programación en C++ y todavía ofreciendo la
misma funcionalidad de esta completísima API a
los programadores de Python.
Instalando wxPython
En Windows, wxPython tan sólo requiere que
hayas instalado previamente Python en tu sistema.
De la dirección de Internet http://www.wxpython.org/
download.php se puede bajar un ejecutable con
un instalador sobre el que sólo hay que hacer
doble clic. Para Linux, la instalación es un poco
más complicada. wxPython depende de las librerías glib y gtk+, que normalmente ya están instaladas en todos los sistemas Linux. Además, requiere
el uso de la librería compartida wxGTK. Para detalles sobre cómo conseguir su instalación,
mirar tanto la URL http://www.wxpython.org/
download.php como http://www.wxwidgets.org,
sección de Downloads.
Programando con wxPython
En wxPython todas las clases están definidas
dentro del módulo wx. Por tanto, en toda aplicación debemos importar ese módulo. Para inicializar una aplicación wxPython hay que crear
una clase que derive de la clase wx.App y sobreescriba su método App.OnInit. Toda aplicación
debe tener al menos un marco (Frame) o diálogo (Dialog) como nivel superior. Cada marco o
diálogo contendrá, a su vez, opcionalmente,
una o más instancias de clases como Panel,
SplitterWindow u otras ventanas, que son a su
vez contenedoras de controles. Toda ventana
puede tener una barra de menús y herramientas (MenuBar y ToolBar), una línea de estado
(StatusBar) y un icono para cuando ésta se
minimice.
Los marcos, diálogos y paneles se usan para
colocar controles que sirven para interactuar con
una aplicación. Algunos de los controles más
comunes son: Button, CheckBox, Choice, ListBox,
RadioBox y Slider. wxWidgets/wxPython proporciona cualquiera de los controles que te puedes
encontrar en tus aplicaciones gráficas en Linux o
Windows. Estos controles derivan de la clase
base Control, la cual deriva a su vez de la clase
base Window. Para facilitar la creación de aplicaciones gráficas, wxPython provee un conjunto de
clases de diálogo comunes ya preparadas tales
como MessageDialog o FileDialog.
Aplicación “Hola Solop”
Pongamos en práctica estas nociones sobre
wxPython con el más simple de los ejemplos.
Vamos a desarrollar una simple ventana que
muestre el logo de Sólo Programadores (véase la
figura 1). El listado 1 muestra el código de esta
aplicación. En primer lugar, creamos una clase
MiAplic que deriva de wx.App, requerimiento que
debe cumplir toda aplicación wxWidgets. Para evitar que la aplicación termine una vez creada la
instancia de MiAplic, invocamos su método
MainLoop, el cuál hará que el programa empiece
a escuchar eventos de ratón y teclado sobre la
aplicación gráfica.
La clase MiAplic sobrescribe el método OnInit de
wx.App, donde se crean los elementos gráficos de
la aplicación. En este método llamamos en primer
lugar a la función wx.InitAllImageHandlers() para
inicializar los gestores de imágenes (bmp, jpg,
png, etc.), dado que vamos a usar un fichero .jpg
con el logo de Sólo Programadores. Luego, creamos una instancia de una imagen, pasando como
parámetros el nombre de la imagen (solop.jpg) y el
tipo de la imagen (wx.BITMAP_TYPE_JPEG). A
continuación, creamos una instancia de la clase
MiMarco. Finalmente, visualizamos la ventana
MiMarco y nos aseguramos que sea la ventana
principal de la aplicación al invocar el método
SetTopWindow.
La clase MiMarco convierte la imagen a un bitmap, obtiene el tamaño de la imagen creada e inicializa la clase padre de MiMarco, que es un
wx.Frame. Finalmente, inicializa el atributo
self.bmp con una instancia de un control
wx.StaticBitmap, que contiene el bitmap creado
con anterioridad.
No siempre es necesario (o conveniente) crear de manera programática las interfaces en wxPython.
Hay herramientas que nos ayudarán a generar el código wxPython
Figura 1. Aplicación “Hola
Solop” en wxPython.
27
SOLO PROGRAMADORES nº 119
MIDDLEWARE
LISTADO 1
“Hola Solop” en wxPython
#!/usr/bin/env python
import wx
Gestores de posición: sizers
class MiMarco(wx.Frame):
“””Clase frame que visualiza una imagen.”””
def __init__(self, imagen, padre=None, id=-1,
pos=wx.DefaultPosition, titulo=’Hola solo programadores’):
“””Crea una instancia de Frame y visualiza una imagen.”””
temp = imagen.ConvertToBitmap()
tamano = temp.GetWidth(), temp.GetHeight()
wx.Frame.__init__(self, padre, id, titulo, pos, tamano)
self.bmp = wx.StaticBitmap(self, -1, temp)
class MiAplic(wx.App):
“””La clase aplicacion.”””
def OnInit(self):
wx.InitAllImageHandlers()
imagen = wx.Image(‘solop.jpg’, wx.BITMAP_TYPE_JPEG)
self.miMarco = MiMarco(imagen)
self.miMarco.Show()
self.SetTopWindow(self.miMarco)
return True
correspondiente. wxGlade (http://wxglade.sourceforge.net/) es un buen ejemplo. wxGlade es un
diseñador de interfaces que asiste en la creación
de interfaces gráficas con wxPython y
wxWidgets. Genera código fuente tanto en
Python como en C++.
Manejo de eventos
Toda ventana, y, por extensión, todo control
(dado que los controles derivan de la clase
wx.Window), definen una serie de eventos que
se lanzan cuando el usuario interactúa sobre
ellos. wxPython define por cada control una
tabla de eventos que asocia un tipo de evento
con un gestor del mismo. Por ejemplo, como
se ilustra debajo, para el evento EVT_BUTTON
que ocurre cuando se pulsa un botón
(botonNuevoUsuario), se podría asociar el
manejador OnNuevoUsuario. Obsérvese que el
segundo parámetro de este método evt es de
tipo wx.Event. Este objeto contiene información sobre el evento: su tipo, identificador del
control que lo generó, cuándo ocurrió el
evento, una referencia al objeto que lo generó, etc.:
...
wx.EVT_BUTTON(self, self.botonNuevoUsuario.
GetId(), self.OnNuevoUsuario)
Figura 2. Ventana de login a
la aplicación tres en raya.
...
def OnNuevoUsuario(self, evt):
# código para crear nuevo usuario
Para poder conocer qué eventos puede generar cada control en wxPython remitimos al
lector a la documentación de wxWidgets, que
acompaña a la distribución de wxPython. Por
28
Los gestores de posición, representados por la
clase wx.Sizer y sus descendientes en la jerarquía de clases de wxPython, se han convertido en el método elegido para definir el posicionamiento de controles en diálogos, dada su
habilidad para crear diálogos atractivos independientes de la plataforma, que tengan en
consideración las diferencias en tamaño y
estilo de los controles individuales. El cuadro
“Gestores de posición más comúnmente utilizados” muestra algunos de los sizers disponibles en wxPython.
Gestores de posición más comúnmente
utilizados
Sizer
GridSizer
if __name__ == ‘__main__’:
miAplic = MiAplic()
miAplic.MainLoop()
SOLO PROGRAMADORES nº 119
cada control se detallan los eventos que éste
puede generar.
BoxSizer
Clase base abstracta
Un gestor de posición que dispone
los controles en una matriz donde
las celdas tienen el mismo tamaño.
Gestor para disponer controles en
una fila o columna.
Dibujando gráficos
En wxPython no se puede dibujar directamente en una ventana. Es preciso utilizar un contexto de dispositivo (device context o DC). DC
es la clase base para ClientDC, PaintDC,
MemoryDC,
PostScriptDC,
MemoryDC,
MetafileDC and PrinterDC. La virtud de los DC
es que se puede utilizar exactamente el mismo
código para dibujar encima de cualquiera de
estos dispositivos. Algunos ejemplos de las
funciones disponibles para dibujar son
DC.DrawLine, DC.DrawCircle o DC.DrawText.
Para optimizar el pintado de figuras geométricas, es recomendable incluir las primitivas de
dibujo entre las llamadas DC.BeginDrawing y
DC.EndDrawing. El color del fondo de cada
figura dibujada se puede controlar con el concepto de brocha (Brush) y el grosor y color de
los bordes de cada figura con el concepto de
bolígrafo (Pen).
Añadiendo interfaz gráfica a la aplicación
de tres en raya
Recordemos que en la anterior entrega definimos
una aplicación de tres en raya en modo texto que
requería el login de cada jugador y mantenía en
memoria las estadísticas sobre los resultados de
sus partidas. Usando wxPython, añadiremos
ahora las siguientes ventanas gráficas a la aplicación tres en raya:
1- Ventana de login (véase la figura 2) a través de
la cual el usuario puede entrar en la aplicación,
crear nuevos detalles de login de un usuario o
simplemente salir de la aplicación.
MIDDLEWARE
Pensando en Python (II): 3 en raya en modo gráfico
2- Ventana de nuevo usuario (véase la figura 3),
donde se puede introducir el nombre de usuario y contraseña de un nuevo usuario
3- Ventana principal de la aplicación (véase la figura 4), a través de la cual el usuario puede iniciar
una partida, observar sus estadísticas de partidas jugadas o regresar a la ventana de login.
4- Ventana de partida de tres en raya (véase la
figura 4), en la cual el usuario verá un tablero
de tres en raya sobre el cual podrá jugar.
5- Diálogo de resultado de partida (véase la figura 4), en el que se indicará a la conclusión de
una partida el ganador de la misma o si ha
habido empate.
6- Ventana de estadísticas (véase la figura 5) que
muestre los resultados obtenidos por un jugador.
Ventana de login
El listado 2 muestra un extracto del constructor de la clase LoginGUI, responsable de generar la ventana mostrada en figura 2. La clase
LoginGUI hereda de wx.Frame. Su constructor
recibe como parámetro obligatorio una referencia al registro de jugadores y estadísticas
de partidas. Esta clase podría instanciarse con
la llamada:
ventanaLogin = LoginGUI(registro)
en el constructor de LoginGUI, después de
invocar el constructor de la clase padre, hacemos que el fondo de la pantalla tenga color
blanco. A continuación, guardamos una referencia al registro y creamos una barra de menú
a la que añadimos la cabecera de menú
TresEnRaya, que contiene como una única
opción de selección Salir. Asociamos al evento
de selección de esta opción, el método de la
clase self.OnSalir, mediante la sentencia:
wx.EVT_MENU(self, 101, self.OnSalir)
El contenido de este método es mostrado en el listado 3. Simplemente destruye las ventanas que
hayan sido creadas a partir de ella,
ventanaUsuario (véase la figura 4) si existe, y
luego se destruye a si misma para acabar la aplicación.
Después de crear la barra de menú el constructor
(véase el listado 2) crea los campos de entrada de
datos “nombre de usuario” y “contraseña”. Lo
hace usando los controles wx.StaticText y
wx.TextCtrl. Nótese que el botón “Login” en la
figura 2 sólo se activará si hay datos en los dos
campos de entrada self.nombreUsuarioTxtCtrl y
self.passwordTxtCtrl. Esto se consigue al manejar
el evento de introducción de texto en uno de esos
controles, mediante llamadas en la forma:
wx.EVT_TEXT(self, self.nombreUsuarioTxtCtrl.
GetId(), self.EvtText)
Tras crear los campos de entrada, el constructor
instancia los botones de la ventana de login:
self.botonNuevoUsuario, self.botonLogin y
self.botonSalir. Mediante las llamadas en la
forma:
wx.EVT_BUTTON(self, <identificadorBotónPulsado>,
<métodoAInvocar>)
Asocia métodos que serán invocados cada vez que
se pulse uno de estos botones. Por ejemplo:
wx.EVT_BUTTON(self, self.botonSalir.GetId(),
self.OnSalir)
La implementación de los manejadores de eventos para estos
botones: OnLogin, OnNuevoUsuario y OnSalir pueden verse en
el listado 3. El manejador
OnNuevoUsuario creará una
ventana
NuevoUsuarioGUI
LISTADO 2
Figura 3. Ventana de nuevo
usuario.
Constructor de clase LoginGUI
class LoginGUI(wx.Frame):
def __init__(self, registro, parent=None, id=-1, title=’Juego tres en
raya: login’):
wx.Frame.__init__(self, parent, id, title)
self.SetBackgroundColour(wx.WHITE)
self.registro = registro
# Preparamos la barra de menu
menuBar = wx.MenuBar()
menu1 = wx.Menu()
menu1.Append(101, “&Salir”, “Haz clic para salir”)
menuBar.Append(menu1, “&TresEnRaya”)
self.SetMenuBar(menuBar)
wx.EVT_MENU(self, 101, self.OnSalir)
nombreUsuarioLabel = wx.StaticText(self, -1, “Nombre usuario:”)
self.nombreUsuarioTxtCtrl= wx.TextCtrl(self, -1, “”, size=(125, -1))
passwordLabel = wx.StaticText(self, -1, “Password:”)
self.passwordTxtCtrl = wx.TextCtrl(self, -1, “”, size=(125, -1),
style=wx.TE_PASSWORD)
...
self.botonNuevoUsuario = wx.Button(self, -1, “Nuevo usuario”)
wx.EVT_BUTTON(self, self.botonNuevoUsuario.GetId(),
self.OnNuevoUsuario)
self.botonLogin = wx.Button(self, -1, “Login”)
self.botonLogin.Enable(False)
wx.EVT_BUTTON(self, self.botonLogin.GetId(), self.OnLogin)
self.botonSalir = wx.Button(self, -1, “Salir”)
wx.EVT_BUTTON(self, self.botonSalir.GetId(), self.OnSalir)
bsizer = wx.BoxSizer(wx.VERTICAL)
topSizer = wx.BoxSizer(wx.HORIZONTAL)
topSizer.Add(nombreUsuarioLabel, 0, wx.GROW|wx.ALL, 4)
topSizer.Add(self.nombreUsuarioTxtCtrl, 0,
wx.GROW|wx.ALL|wx.ALIGN_RIGHT , 4)
bsizer.Add(topSizer, 0, wx.GROW|wx.ALL, 4)
...
botonAccionSizer = wx.BoxSizer(wx.HORIZONTAL)
botonAccionSizer.Add(self.botonNuevoUsuario, 0, wx.GROW|wx.ALL, 4)
...
bsizer.Add(botonAccionSizer, 0, wx.GROW|wx.ALL, 4)
bsizer.Fit(self)
self.SetAutoLayout(True)
self.SetSizer(bsizer)
wx.EVT_CLOSE(self, self.OnSalir)
29
SOLO PROGRAMADORES nº 119
MIDDLEWARE
LISTADO 3
Manejadores de eventos de la clase LoginGUI
def OnSalir(self, evt):
self.__cerrarAplicacion()
def OnLogin(self, evt):
try:
self.registro.login(self.nombreUsuarioTxtCtrl.GetValue(),
self.passwordTxtCtrl.GetValue())
except:
mostrarDialogo(self, ‘No hay ningun usuario registrado con el nombre
de usuario y password especificados’, ‘Login incorrecto’)
return
try:
self.ventanaUsuario =
LoggedInGUI(self.nombreUsuarioTxtCtrl.GetValue(),
self.passwordTxtCtrl.GetValue(), parent=self)
self.ventanaUsuario.Show()
self.Hide()
except Exception, e:
mostrarDialogo(self, “Problemas haciendo login para usuario “ +
self.nombreUsuarioTxtCtrl.GetValue() + “: “ + str(e.args),
“Error al hacer login”)
def OnNuevoUsuario(self, evt):
ventanaNuevoUsuario = NuevoUsuarioGUI(self)
ventanaNuevoUsuario.Show()
self.Hide()
def __cerrarAplicacion(self):
if self.__dict__.has_key(‘ventanaUsuario’):
if self.ventanaUsuario:
self.ventanaUsuario.Destroy()
self.Destroy()
def EvtText(self, event):
if len(self.nombreUsuarioTxtCtrl.GetValue().strip()) > 0 and
len(self.passwordTxtCtrl.GetValue().strip()) > 0:
self.botonLogin.Enable(True)
else:
self.botonLogin.Enable(False)
Figura 4. Ventana principal de
la aplicación tres en raya, con
ventana de partida y diálogo
de resultado.
(véase la figura 3) donde se podrán introducir
los detalles de un nuevo usuario y esconderá
la ventana de login. El manejador OnLogin,
comprobará que el usuario ha sido registrado
y si es así, esconderá la ventana de login y
creará una ventana principal de la aplicación
(véase la figura 4).
El constructor (líneas finales del listado 2) usa
gestores de posicionamiento de tipo BoxSizer
para colocar tanto en vertical como en horizontal los controles antes mencionados. Cuando
creamos un BoxSizer indicamos la dirección en
la que colocaremos los controles, y luego simplemente usamos el método Add del BoxSizer
para añadir controles al mismo. Como última
línea del constructor definimos un manejador
para el evento cerrar ventana.
Ventana de partida (figura 4)
El código de la ventana de partida
(JuegoTresEnRayaGUI, véase el cuadro “Material
complementario”), presenta como una única
novedad el uso del manejador GridSizer. El
siguiente método ilustra cómo se inicializa el
tablero de tres en raya con bitmaps que indican
casillas no seleccionadas. Básicamente, se define una matriz de 3x3 en la que se colocan los
botones sobre los que el usuario puede hacer
clic. Cuando el usuario hace clic se invoca el
método self.OnClick que cambiará el icono para
mostrar que esa casilla ha sido seleccionada. La
clase JuegoTresEnRayaGUI hereda de la clase
JuegoTresEnRaya vista en la anterior entrega:
def __initTablero(self):
self.gridsizer = wx.GridSizer(3,3,0,0)
self.celdaTablero = []
self.bitmaps = []
for i in range(9):
self.bitmaps.append(wx.Bitmap
(“images/blank.png”, wx.BITMAP_TYPE_PNG))
self.celdaTablero.append(wx.BitmapButton
(self, i, self.bitmaps[i]))
wx.EVT_BUTTON(self, i , self.OnClick)
self.gridsizer.Add(self.celdaTableroa[i])
self.gridsizer.Fit(self)
self.SetAutoLayout(True)
self.SetSizer(self.gridsizer)
Diálogo de resultado de partida (figura 4)
Todos los resultados, o mensajes que hay que dar
al usuario se visualizan a través de una función de
ayuda, que simplemente instancia un
wx.MessageDialog:
def mostrarDialogo(ventanaPadre, mensaje,
titulo, modo=wx.OK):
dialog = wx.MessageDialog(ventanaPadre,
mensaje, titulo, modo);
dialog.ShowModal()
Ventana de estadísticas (figura 5)
El listado 4 muestra la implementación de la
clase (EstadisticasGUI) que hereda de
SOLO PROGRAMADORES nº 119
30
MIDDLEWARE
Pensando en Python (II): 3 en raya en modo gráfico
LISTADO 4
Implementación de la ventana de estadísticas
class EstadisticasGUI(wx.Frame):
def __init__(self, ventanaUsuario, nombreUsuario,
resultadoEstadisticas):
...
self.anchura = 640
self.altura = 480
self.SetSize((self.anchura, self.altura)) # asociar tamaño a ventana
self.SetBackgroundColour(wx.WHITE) # cambiar color fondo
wx.EVT_PAINT(self, self.OnPaint)
def OnPaint(self, evt):
dc = wx.PaintDC(self) # inicializar un contexto de dispositvo
dc.Clear()
Figura 5. Ventana de estadísticas.
wx.Frame. En el constructor se le asigna un
tamaño fijo de 640x480 pixels y se define un
manejador OnPaint para el redibujado de la
ventana. Este evento será invocado cada vez
que se mueva, maximice o cambie el tamaño
de la ventana de estadísticas. En OnPaint, se
crea una instancia de DC, se limpia el área de
dibujado del marco y se inicia el redibujado
con la sentencia dc.BeginDrawing(). A continuación, se inicializa el formato de los trazos a
dibujar con un objeto Pen de color negro:
dc.SetPen(wx.Pen(“BLACK”,1)). Similarmente,
se inicializa una brocha de color rojo para el
relleno de la primera barra de resultados a
dibujar: dc.SetBrush(wx.Brush(“RED”)). Luego
se dibujan las barras que muestran el porcentaje de victorias, empates y partidas perdidas
por un jugador. Se calculan las alturas de cada
una de las barras y a continuación se procede
a su dibujado, por medio de las primitivas
dc.DrawRectangle. Es importante indicar que
el origen de coordenadas de dibujado se
encuentra en la parte superior izquierda de la
ventana. Esa es la razón por la que se utilizan
valores negativos para indicar tamaños en
vertical. Por ejemplo:
dc.DrawRectangle(100, 350, 120,
-alturaBarraGanadas)
dibuja un rectángulo que empieza en las coordenadas (100, 350), tiene 120 pixels de anchura y se
extiende hasta la coordenada 350-alturaBarraGanadas de altura.
Serialización de datos en Python
La mayoría de los datos en los sistemas de
información se guardan en ficheros. Python
ofrece unas interfaces muy sencillas para
guardar y recuperar datos de ficheros. Para
leer o escribir un fichero en Python usamos la
función predefinida open, que tiene la
siguiente firma:
open(<nombre-fichero>, <modo: r (read),
w(write), a(append), b(binario)>
dc.BeginDrawing() # indicar que se va a empezar a dibujar
dc.SetPen( wx.Pen(“BLACK”,1) )
dc.SetBrush( wx.Brush(“RED”) )
partidasJugadas = self.resultadoEstadisticas[0] +
self.resultadoEstadisticas[1] + self.resultadoEstadisticas[2] + 0.0
if partidasJugadas > 0:
alturaBarraGanadas =
int(250*(self.resultadoEstadisticas[0]/partidasJugadas))
alturaBarraEmpatadas =
int(250*(self.resultadoEstadisticas[1]/partidasJugadas))
alturaBarraPerdidas =
int(250*(self.resultadoEstadisticas[2]/partidasJugadas))
if alturaBarraGanadas < 1:
alturaBarraGanadas = 1
if alturaBarraEmpatadas < 1:
alturaBarraEmpatadas = 1
if alturaBarraPerdidas < 1:
alturaBarraPerdidas = 1
else:
alturaBarraGanadas = 1
alturaBarraEmpatadas = 1
alturaBarraPerdidas = 1
cabecera = “Total partidas jugadas: “ + str(int(partidasJugadas))
(w, h) = dc.GetTextExtent(cabecera)
dc.DrawText(cabecera, 320-(w/2), 70-h) # dibujar cabecera gráficos
(w, h) = dc.GetTextExtent(“Ganadas”) # cabecera barra Ganadas
dc.DrawText(“Ganadas”, 160-(w/2), 390-h)
dc.DrawRectangle(100, 350, 120, -alturaBarraGanadas) # dibujar barra
de ganadas
(w, h) = dc.GetTextExtent(`self.resultadoEstadisticas[0]`)
dc.DrawText(`self.resultadoEstadisticas[0]`, 160-(w/2), 350alturaBarraGanadas-20)
dc.SetBrush( wx.Brush(“GREEN”) )
# código similar a barra Ganadas para barra verde de empates
...
dc.SetBrush( wx.Brush(“BLUE”) )
# código similar a barra Ganadas para barra azul de empates
...
dc.EndDrawing()
Esta función devuelve un objeto de tipo fichero que define entre otros los métodos read,
para leer uno o un buffer de caracteres, write,
para escribir uno o un grupo de caracteres,
readlines para leer todas las líneas de un
fichero y close para cerrar un fichero. A continuación se muestra el código para leer un
fichero de texto línea a línea. Como siempre,
remitimos al lector a la Python Library
Referente (http://docs.python.org/lib/lib.html)
para más detalles:
# leerfichero.py
31
SOLO PROGRAMADORES nº 119
MIDDLEWARE
LISTADO 5
Haciendo persistente el registro de jugadores
class RegistroJugadoresPersistente(RegistroJugadores):
def __init__(self):
RegistroJugadores.__init__(self)
self._RegistroJugadores__jugadores = shelve.open(“jugadores”)
if not self._RegistroJugadores__jugadores.has_key(‘solop’):
self._RegistroJugadores__jugadores[‘solop’] = ‘solop’
self._RegistroJugadores__estadisticas = shelve.open(“estadisticas”)
if not self._RegistroJugadores__estadisticas.has_key(‘solop’):
# jugador -> [ganados, empatados, perdidos]
self._RegistroJugadores__estadisticas[‘solop’] = [0, 0, 0]
def __del__(self):
self._RegistroJugadores__jugadores.close()
self._RegistroJugadores__estadisticas.close()
fh = open(“holamundo.py”) # open crea un
objeto de tipo fichero
for line in fh.readlines() : # lee todas
las líneas en un fichero
print line,
fh.close()
$ python leerfichero.py
#!/usr/bin/python
print “Hola mundo”
El siguiente código muestra cómo escribir datos a
un fichero de texto:
# escribirfichero.py
fh = open(“out.txt”, “w”)
fh.write (“estamos escribiendo ...\n”)
fh.close()
$ python escribirfichero.py
$ cat out.txt
estamos escribiendo ...
Serialización de objetos: Pickle y shelves
Al igual que otros lenguajes modernos como
Java y C#, Python ofrece mecanismos para
serializar y deserializar objetos desde disco o a
través de la red. El módulo pickle implementa
un algoritmo para la serialización y deserialización de objetos en Python. Para serializar
una jerarquía de objetos, deberemos crear un
Pickler, y luego llamar al método dump(),
pasándole como argumento el objeto a serializar. Para deserializar crearemos un Unpickler e
invocaremos su método load(). El módulo
shelve, por su parte, define diccionarios persistentes. Las claves tienen que ser cadenas de
caracteres mientras que los valores pueden ser
cualquier objeto que se puede serializar con
pickle. A continuación mostramos un ejemplo
de cómo implementar un diccionario o mapa
persistente:
import shelve
d = shelve.open(filename) # abre un
fichero
SOLO PROGRAMADORES nº 119
32
d[key] = data # guarda un valor bajo
key
data = d[key] # lo recupera
del d[key] # lo borra
d.close() # cierra el diccionario
persistente
Añadiendo persistencia de datos
a la aplicación tres en raya
Una vez añadida una interfaz gráfica a nuestra
aplicación de tres en raya, quizás el único
aspecto que le resta para darle un carácter más
profesional, es añadirle la capacidad de recordar los usuarios que se registran con la misma,
así como las estadísticas de los resultados
obtenidos por esos usuarios. En la primera
entrega de esta serie definimos una clase llamada RegistroJugadores que mantenía en dos
diccionarios, en memoria, información sobre
los jugadores registrados (__jugadores) y sobre
los resultados obtenidos por los mismos
(__estadisticas). Para hacer que estos datos se
guarden de manera persistente, sólo debemos
definir una nueva clase que herede de
RegistroJugadores a la que llamaremos
RegistroJugadoresPersistente. El listado 5
muestra el código de esta clase. En el constructor de esta nueva clase nos aseguramos
que los miembros de RegistroJugadores
__jugadores y __estadisticas pasen de ser simples mapas a shelves, es decir, mapas de datos
persistentes. Es preciso recordar que en Python
se usa la técnica de name mangling, para hacer
que todos los campos y funciones privadas o
protected, no sean visibles desde fuera. Por esa
razón, tenemos que usar la notación
_nombreClase__nombreCampoPrivado, por
ejemplo, self._RegistroJugadores__estadisticas, para tener acceso a estos campos.
Finalmente, para asegurarnos que el mapa persistente es correctamente cerrado, añadimos
en el destructor de la clase (__del__) llamadas
a los metodos close() de las baldas de datos
(shelves).
Conclusiones
En esta entrega hemos incrementado nuestros
conocimientos del lenguaje de programación
Python, aprendiendo cómo se pueden crear ventanas gráficas con la toolkit wxPython y cómo se
pueden serializar datos a través de ficheros, y los
módulos pickle y shelve. Con ésto, hemos conseguido darle una apariencia y un funcionamiento
mucho más profesional a la aplicación de tres en
raya que comenzamos en la anterior entrega. En
el próximo artículo aprenderemos a crear una
interfaz web al juego de tres en raya y a guardar
los datos de jugadores y estadísticas en una base
de datos.