TutorialDaboParte 3
TutorialDaboParte 3
PARTE 3
La Aplicación
Ambos, Paul y yo hemos hecho nuestras vidas como consultores, y como tales nosotros
hemos creado aplicaciones para otros y facturádosles por nuestro tiempo. Así que para este
ejercicio crearemos una aplicación parra registrar el tiempo empleado para varios clientes y
luego, con permiso del tiempo, una manera de generar las facturas para enviarlas a nuestros
clientes.
Los Datos
Hay dos tablas para esta aplicación: una para contener la información sobre los clientes, y
otra para registrar el tiempo empleado. En la vida real puede haber muchos tipos de tazas de
facturación, pero nosotros mantendremos esto sencillo y sólo trabajaremos con una
facturación por horas, con incrementos de cuarto de hora. Estas dos tablas se hallan en la
base de datos MySQL “pycon” como las tablas recipe que usamos antes.
Primeros Pasos
Empezaremos por determinar un directorio donde queremos crear nuestra aplicación. En
verdad no importa donde, así siga cualquier organización de archivos que acostumbre a usar.
Yo voy a usar el directorio ~/apps/pycon_hours en mi disco.
Abra una ventana terminal (Usuarios Windows, use Start|Run, y escriba 'cmd' para el nombre
del programa), cámbiese al que será el directorio padre de su aplicación; en este ejemplo,
sería ~/apps. Si yo fuera a ejecutar un listado de directorio obtendría algo como:
Nota. Como ahora tenemos un directorio llamado 'pycon_hours'. Cámbiese a ese directorio y
luego haga un listado de él:
Este es la estructura de directorios principal de una aplicación Dabo, junto con un archivo
llamado main.py que se usa para lanzar su aplicación. De hecho, si usted corre main.py ahora mismo,
usted obtendrá una aplicación real en ejecución. Cierto, esta es sólo una ventana vacía y un
menú , pero corre! Esto es lo que contiene main.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import dabo
dabo.ui.loadUI("wx")
app = dabo.dApp()
app.start()
El script es muy simple, pero contiene algunas cosas importantes por tomar en cuenta.
Primero, importamos el módulo dabo y llamamos al comando dabo.ui.loadUI("wx") para
mover todo el código específico de wxPython al espacio de nombres dabo.ui.
A continuación creamos una instancia de aplicación. Luego asignamos su propiedad
MainFormClass a la clase formulario principal predeterminada proporcionada por Dabo. Esto
le dice a la aplicación cual formulario mostrar desde el inicio.; cuando creemos nuestro propio
formulario para esta aplicación, vamos a cambiar esta por nuestra propia clase.
Finalmente, obtenemos que todo corra mediante la llamada a app.start(). Esto cargará todas
las definiciones de conexión, clases bizobj , clase ui, y cualquier otra cosa más que usted
haya creado, en el espacio de nombres de la aplicación, desplegará el formulario principal, y
luego inicia el ciclo principal de eventos de la IU. Cualquier código agregado después de esta
línea no será ejecutado hasta después de que la aplicación finalice.
El objeto aplicación es importante en Dabo: si una aplicación es una sinfonía de todas sus
partes, el objeto aplicación es el director. Para hacerle fácil codificar para referirse a este
objeto, hemos añadido una propiedad a todas las clases Dabo de manera que usted pueda
referirse a la aplicación usando self.Application.
Creación de la Conexión a la Base de Datos
Dabo tiene una herramienta visual para la creación y prueba de las conexiones a la base de
datos. Esta es denominada CxnEditor (abreviado de 'Connection Editor), y está localizada,
unto con las otras herramientas visuales , en el directorio 'ide'. Iníciela y verá un formulario
cuya apariencia es como la de la imagen de abajo. He introducido los valores que
necesitaremos para este tutorial; allí no puede leer el password, pero éste es 'atlanta'. Le di a
la conexión el nombre 'hours'; el nombre exacto que le usted le de no es importante, pero es
mejor darle algún nombre descriptivo, porque el código de su aplicación obtendrá esa
conexión refiriéndose a ese nombre. Usted puede almacenar múltiples conexiones en un solo
archivo, pero para ser honesto, yo aun no he tenido la necesidad de más de una.
Para estar seguro de que usted escribió todo correctamente, presione el botón 'Test...'
Obtendrá un mensaje informándole que tuvo éxito, o la razón de por qué falló la conexión.
Cuando la tenga trabajando, pulse el botón 'Save'. Éste le presentará el diálogo estándar
'Save as...' de su SO; navegue hasta la carpeta 'db' fuera del directorio principal que creamos
con quickStart, dele a su archivo un nombre como 'hours' y guárdelo. Esto creará un archivo
denominado 'hourcnxml' en el directorio 'db', este es simplemente un archivo XLM que
contiene la información de la conexión.
Usted debe haber notado que la barra de estado de la captura de pantalla de arriba tiene lo
que parece un hipervínculo con el texto 'HomeDirectory' en él. Este documento está siendo
escrito unas pocas semanas antes de PyCon, y nosotros estamos añadiendo nuevas cosas a
nuestras herramientas visuales. En este caso estamos añadiendo el concepto de Home
Directory a nuestras aplicaciones para ayudar con alguna confusión de ruta que pueda surgir.
Dependiendo de cuanto tiempo se disponga para desarrollo de aquí a PyCon, esto pudiera o
no ser liberado. Y si, ese es un control dHyperlink de Dabo que está configurado para abrir un
diálogo estándar para seleccionar un directorio.
Una Nota Sobre Encriptación
Si usted guarda su conexión, Dabo crea un archivo con una extensión '.cnxml'. Este es un
Este es un archivo XML plano y se mostrará como lo que sigue:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<connectiondefs xmlns="http://www.dabodev.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.dabodev.com conn.xsd"
xsi:noNamespaceSchemaLocation = "http://dabodev.com/schema/conn.xsd">
<connection dbtype="MySQL">
<name>hours</name>
<host>dabodev.com</host>
<database>pycon</database>
<user>pycon</user>
<password>Z6DEC5O86ZEEH53Y85O07</password>
<port>3306</port>
</connection>
</connectiondefs>
Esto parece muy directo excepto por el elemento 'password': hemos escrito 'atlanta' como
nuestro password, pero Dabo almacenó en el archivo una versión encriptada. Dabo viene con
un método muy sencillo de encriptación/desencriptación que en verdad no hace mucho
excepto obscurecer el password real ante un vistazo casual, pero si alguien quiere ver cual es
su password, todo lo que tendría que hacer es obtener una copia de Dabo y ejecutar lo
siguiente:
>>> import dabo
>>> app = dabo.dApp()
>>> print "The password is:", app.decrypt("Z6DEC5O86ZEEH53Y85O07")
/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-
packages/dabo/lib/SimpleCrypt.py:52: UserWarning: WARNING: SimpleCrypt is not
secure. Please see http://wiki.dabodev.com/SimpleCrypt for more information
warnings.warn("WARNING: SimpleCrypt is not secure. Please see
http://wiki.dabodev.com/SimpleCrypt for more information")
The password is: atlanta
PyCrypto
Esta librería está disponible en http://www.dlitz.net/software/pycrypto/. Tiene muchas, muchas
, pero la que nosotros vamos a usar son las rutinas de encriptación DES3. Asegúrese de que
la librería está instalada en su maquina, usando pip, easy_install, o una configuración manual.
Una vez que PyCrypto está instalada, lo único que usted necesitará es asignar la propiedad
CryptoKey del objeto Aplicación. Esta puede ser una cadena o, para una mejor seguridad, un
invocable que retorne la cadena. Recuerde, la mejor encriptación es de valor ninguno si usted
la deja por allí en un archivo de texto plano donde cualquiera pueda leerla.
Su Propio Objeto de Encriptación
Para requerimientos mayores de encriptación, usted debería proporcionar su propia clase
personalizada para manejar toda encriptación/desencriptación. La clase debe exponer dos
métodos: encriptar() y desencriptar(). Luego, usted necesita asignar a propiedad
CryptoKey del objeto Aplicación a una instancia de su clase criptografía, y Dabo usará esas
rutinas del objeto antes que las suyas.
class ClientBizobj(dabo.biz.dBizobj):
def afterInit(self):
self.DataSource = "clients"
self.KeyField = "pkid"
self.addFrom("clients")
self.addField("pkid")
self.addField("clientname")
self.addField("attn")
self.addField("rate")
self.addField("street1")
self.addField("street2")
self.addField("city")
self.addField("stateprov")
self.addField("postalcode")
Aquí estamos utilizando la aproximación SQL Builder para definir la estructura de datos del
'bizobj. Comenzamos importando dabo, y heredando nuestra clase de dabo.biz.dBizobj, la
cual es la clase base bizobj. Añadimos nuestro código al método afterInit(), y empezamos
por definir las propiedades más importantes: DataSource y KeyField. Después de eso,
añadimos desde la tabla todos los campos de que queremos usar en nuestra aplicación, en
este caso, todos los campos. Podemos agregar más lógica a la clase posteriormente, pero
esto es suficiente para iniciarnos.
También necesitamos agregar esta clase al espacio de nombres del módulo 'biz'; hacemos
esto editando el archivo __init__.py (el cual fue creado por quickStart ) en el directorio biz.
Añada la línea al final de modo que se lea:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
######
# A fin de que Dabo 'vea' las clases en su directorio biz, añada una
# declaración import aquí por cada clase. E.g., si usted tiene un archivo llamado
# 'MisClases.py' en este directorio, y el define dos clase llamadas 'PrimeraClase'
# y 'SegundaClase', añada estas líneas:
#
# from MisClases import PrimeraClase
# from MisClases import SegundaClase
#
# Ahora, usted puede referirse a esas clase como:
# self.Application.biz.PrimeraCalse y
# self.Application.biz.Segunda Clase
######
Cuando creemos el HoursBizobj más adelante, añadiremos eso a este archivo, también.
Cambie el número de ranuras a dos y presione 'OK'. Ahora verá que el formulario esta
dividido verticalmente en dos secciones. Hablaremos un poco más sobre los dimensionadores
dentro de poco. Sencillamente tenga en cuenta que ellos son básicamente reglas para
disponer los elementos en un formulario. Vamos a añadir los controles para la entrada de
datos a la sección superior y el listado de las horas no facturadas a la mitad inferior.
Una cosa que es muy obvia es que cada control tiene una caja de texto para contener el
valor, pero todos ellos no son campos de texto! Puede cambiar el tipo del control para cada
campo aquí, también, pulsando con el botón derecho sobre la caja de texto. Se le presentarán
unos pocos tipos de controles básicos: caja de texto fecha, caja de edición, contador y caja de
comprobación. En nuestro caso, queremos una caja de texto fecha para el campo servicedate,
un contador para el campo hours, una caja de edición para el campo notes y una caja de
comprobación para el campo billed. Haga todos estos cambios y entonces la vista previa
debería mostrarse ahora como la imagen de la derecha arriba.
Los controles no están todavía exactamente como queremos, pero más adelante podemos
ajustarlos. Cuando tenga la vista previa como la anterior, pulse 'Next'. Entonces verá la
página final del ayudante (abajo, izquierda). Si hemos creado ya nuestro bizobj, podríamos
decirle de nuevo al ayudante que no lo agregue, pero en este caso si queremos que él cree el
bizobj. Así que deje la opción predeterminada y pulse 'Finish'. Usted verá los controles
agregados a la mitad superior de su formulario, y su apariencia será como la de la imagen de
abajo a la derecha.
Este podría ser un excelente momento para guardar su diseño antes de proseguir. En el futuro
no le recordaré que guarde; sólo recuerde hacerlo cada pocos minutos de modo de no perder
demasiado de su trabajo.
Vamos a comprobar para asegurarnos que el bizobj fue creado correctamente. Si regresamos
a un listado de directorio del directorio biz de la aplicación, veremos ahora
[ed@Homer ~/apps/pycon_hours]$ ll biz/
total 48
-rw-r--r--@ 1 ed staff 438B Feb 6 08:03 ClientBizobj.py
-rw-r—r-- 1 ed staff 871B Feb 6 08:32 ClientBizobj.pyc
-rw-r--r-- 1 ed staff 559B Feb 6 08:32 HoursBizobj.py
-rw-r--r-- 1 ed staff 1.0K Feb 6 08:32 HoursBizobj.pyc
-rw-r--r--@ 1 ed staff 577B Feb 6 08:32 __init__.py
-rw-r--r-- 1 ed staff 221B Feb 6 08:32 __init__.pyc
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import dabo
class HoursBizobj(dabo.biz.dBizobj):
def afterInit(self):
self.DataSource = "hours"
self.KeyField = "pkid"
self.addFrom("hours")
self.addField("clientfk")
self.addField("hours")
self.addField("notes")
self.addField("billed")
self.addField("servicedate")
self.addField("pkid")
def validateRecord(self):
"""Returning anything other than an empty string from
this method will prevent the data from being saved.
"""
ret = ""
# Add your business rules here.
return ret
Seguro que se ve muy parecido al bizobj codificado a mano ¿no es así? Adicionalmente él
agrego un talón de método validateRecord() para que nosotros más adelante añadamos nuestra lógica de
negocio. Observe también que añadió la columna 'pkid', aunque nosotros no la seleccionamos
en el ayudante. La razón es que el valor PK tiene que estar presente en los datos, aun si no
see muestra en la IU. El ayudante es suficientemente inteligente como para manejar eso.
Antes de que abandonemos el editor, abra biz/__init__.py. ¡Hey, mire! ¡El ayudante fue
también suficientemente inteligente para agregar la instrucción import para la clase
HoursBizobj por nosotros!
Mejorando la Apariencia
El ayudante nos llevó hasta muy cerca de lo que queremos, pero podemos hacerlo mejor.
Primero, comencemos por cambiar la leyenda del formulario a algo mejor que el
predeterminado 'Dabo Class Designer'. Para ello, debemos seleccionar el formulario mismo,
pero eso es bastante difícil porque tenemos muchas cosas encima de él. La forma de hacerlo
es ir a la ventana Info de Objeto y presione el triángulo negro en la esquina superior derecha.
Eso ocultara la hoja de propiedades y mostrará el Árbol de Objeto. El Árbol de Objeto es una
manera práctica de visualizar la relación anidada entre la jerarquía de contenimiento de
objetos y la jerarquía de dimensionadores. De hecho, dado que los dimensionadores no son
objetos, la única forma de accederlos es en el Diseñador de Clases por medio del Árbol de
Objetos (abajo, izquierda).
Deberá ver una entrada 'DesForm' en el tope; pulse doble para seleccionar el formulario y
regrese a la hoja de propiedades. Presione sobre en la columna de la derecha de la fila
etiquetada Caption y cambie el texto a 'Horas Facturables'. Presione Intro para aceptar el
cambio, y note que la barra de título del formulario ha cambiado ahora para refleja la nueva
leyenda. Los cambios que se hagan en la hoja de propiedades son reflejados
instantáneamente reflejados en la superficie de diseño, haciendo fácil verificar que usted hizo
las cosas correctamente.
Mientras estemos aquí, cambiemos la propiedad Nombre del formulario a algo diferente al
predeterminado 'DesForm'. Esto no es algo crítico por hacer, pero hace las cosas aparezcan
un poco mejor. También, podemos estar editando múltiples clases al mismo tiempo, pero hay
una sola hoja de propiedades y esto nos ayuda a ver cual formulario está reflejando la hoja
de propiedades actual. Entonces , cambie la propiedad Name a FormaHoras', la hoja de
propiedades debe aparecer como la imagen de abajo, a la derecha.
La etiqueta que está exactamente en el tope no se refrescará inmediatamente, pero si usted
mira al árbol de objeto, ella ha sido cambiada allí (abajo, izquierda). Seleccionando cualquier
otro objeto y luego re-seleccionando el formulario refrescará la etiqueta superior.
Me gustaría tomarme un momento aquí para explicar un par de cosas acerca de la Hoja de
Propiedades, o 'prop sheet'. Ella lista todas las propiedades del control(es) seleccionado(s) y
le permite cambiar aquellas que no son sólo lectura. También, mientras usted navega a través
de las propiedades en la rejilla, la sección inferior muestra la cadena de documentación de
esa propiedad. Usted debería notar también que hay otras dos hojas disponibles: La Hoja de
Métodos y la Hoja Métodos Personalizados. Esas están más allá del propósito de este tutorial,
pero ellas proporcionan formas adicionales de personalizar sus clases IU.
Muy bien, así que hemos conseguido arreglar la barra de título del formulario. ¿Qué más
podría ser mejorado? Primero, los controles se ven aplastados contra los bordes del
formulario. Nosotros especificamos un borde de 8 píxeles en el ayudante Entorno de Datos,
pero aparentemente no fue suficiente.
Si mira en el árbol de objeto arriba, podrá observar que todos los controles que agregamos
están dentro de un Dimensionador Rejilla: este es un dimensionador que dispone sus objetos
en un diseño bidimensional parecido a una rejilla. Así que abra el árbol de objetos y pulse
doble sobre la entrada 'Grid Sizer'. Deberá ver una hoja de propiedades parecida a la imagen
de abajo a la derecha.
Observe también, que hay un grupo de propiedades que comienzan todas con 'Sizer_'; esas
no son en verdad propiedades reales, pero son un atajo práctico para cambiar las
propiedades del dimensionador que administra el diseño de este control. En este caso
queremos un borde mayor alrededor de este grupo de controles, por lo tanto, cambie la
propiedad Sizer_Border a algo más grande, como 30. Note que tan pronto usted presiona
Intro, el borde alrededor de los controles se expande apropiadamente.
Bueno, pienso que nos hemos alejado mucho sin explicar completamente esta cosa de los
dimensionadores. Esta es realmente sencilla una vez usted entiende la idea básica, pero si
usted nunca los ha usado antes, puede tomarle algún tiempo aprender a apartarse del
posicionamiento absoluto y trabajar con una aproximación basada en dimensionadores.
Volveremos a la personalización de la IU dentro de muy poco; por ahora, vamos a hablar
sobre los dimensionadores.
Dimensionadores (Sizers)
Si usted ha creado IU anteriormente, probablemente esté acostumbrado a posicionar sus
controles de forma absoluta: especifica una posición izquierda/arriba y un tamaño, y el control
siempre aparece en esa exacta posición y ese exacto tamaño. Eso funciona suficientemente
bien para algunas cosas, pero tiene una cantidad de limitaciones. Por ejemplo, si el usuario
cambia el tamaño del formulario, nosotros deberíamos esperar que los controles reflejen el
nuevo tamaño. O si un usuario tiene dificultades leyendo, aumente el tamaño de la fuente
para compensarlo. O si usted corre el formulario bajo múltiples SOs, o aun múltiples temas en
el mismo SO. Bajo todas esas circunstancias, su cuidadosamente diseñado formulario ahora
luce como una basura.
Los dimensionadores representan una forma alternativa de posicionar sus controles: ellos son
reglas que toman en cuenta el tamaño nativo del control, de otros controles dentro del
dimensionador, y del entorno dentro del cual existe el dimensionador. Hay dos tipos
principales de dimensionadores: De una dimensión que disponen los controles a lo largo de
una dimensión vertical u horizontal, y Dimensionadores de 2 dimensiones que usan un diseño
basado en una rejilla. En Dabo, estos son dSizer y dGridSizer respectivamente. Los
dimensionadores pueden ser anidados dentro de otros dimensionadores como ya hemos
visto: tuvimos un dimensionador rejilla que posicionaba a nuestros controles.
Los dimensionadores no son objetos reales, pero algunas veces es útil pensar en ellos de esa
manera con el fin tener una imagen más clara de como su formulario va a trabajar. En el
Diseñador de Clases usted puede ver esta representación en el Árbol de Objeto.
Vamos a comenzar con el más sencillo dimensionador 1-D. Los objetos agregados a un
dimensionador 1-D pueden tener varias asignaciones que afectan como el dimensionador los
trata:
• Border: el número de píxeles añadidos como un relleno alrededor de el control cuando
se calcula el tamaño y al posición del control. Predeterminado=0.
• BorderSides: ¿a cuales lados del control se aplica el borde? Predeterminado=”todos”,
pero puede ser una lista de alguno de "arriba", "abajo", "izquierda" o "derecha".
• Proportion: después que los anchos básicos de los controles hayan sido asignados,
¿cuál proporción del espacio restante este control obtiene?
• Expand: ¿se expande el control para llenar la “otra” dimensión?
• HAlign, VAlign: estas controlan donde el control es posicionado a lo largo de la “otra”
dimensión. Por esta quiero significar que si este es un dimensionador vertical, la
asignación halign sería relevante y podría ser una de "izquierda", "centro", o "derecha".
Para los dimensionadores horizontales, las asignaciones valign serían una de "arriba",
"medio", o "abajo".
Con el fin de entender como estas afectan al control, revisemos como trabajan los
dimensionadores. Primero, cuando un evento resize es recibido por el formulario, él llama el
método layout() principal del dimensionador. Usted también puede llamar al método layout()
manualmente si su código modifica los controles en el formulario para actualizar la
apariencia del formulario . El dimensionador es instruido sobre cuanto espacio tiene que
llenar y él comienza calculando el tamaño base de cada elemento que administra. Este es el
tamaño normal mostrado por un control si existiera fuera del dimensionador. Piense en una
etiqueta que contiene un texto en una tipografía determinada: esa etiqueta necesita un cierto
tamaño base para desplegar apropiadamente ese texto en esa fuente. Ese es el tamaño base
del control. El dimensionador luego añade algún valor de borde a cada control para obtener
un tamaño base total para todos sus elementos. Si el tamaño disponible es menor que eso,
los controles son recortados y sólo algunos pueden ser visibles. Pero si hay suficiente tamaño,
los controles son asignados con su tamaño base más un borde. Ahora, si hay disponible
algún otro espacio más en la orientación del dimensionador (vertical u horizontal), ese
espacio restante es dividido de acuerdo al peso relativo de la asignación de proporción del
control. Si la proporción de un control es cero, no obtiene espacio adicional. De lo contrario, el
dimensionador totaliza todas las proporciones de todos sus elementos, y dota a cada control
un porcentaje del espacio extra de acuerdo a su proporción
El uso de palabras como las anteriores puede ser muy confuso, así que veamos el siguiente
ejemplo: suponga que tenemos un formulario de 200 píxeles de ancho por 300 píxeles de
altura, que contiene tres controles, cada uno de 50 píxeles de ancho por 20 píxeles de alto. El
formulario tiene un dimensionador vertical, y estos controles son agregados a ese
dimensionador. El primero tiene una proporción de cero; el segundo tiene una proporción de 1
y el tercero tiene una proporción de 2. Todos tienen un borde de cero, de modo que el tamaño
base total en la dimensión vertical es 60px (3 controles x 20px cada uno). El espacio vertical
disponible es 200px, lo que deja 240 px libres. El total de las proporciones de los elementos
es 3 (0 + 1 + 2), luego el dimensionador haría el primer control así que el dimensionador
haría el primer control 20px alto (20px base + (0/3 * 240)).El segundo control sería ahora
(20px base + (1/3 * 240)) o 100px alto, y el tercero sería (20px base + (2/3 * 240)) o 180px
alto. Sumando todo esto obtenemos 20 + 100 + 180, o los 300px totales en la altura del
formulario.
Pero ¿qué en relación con el ancho? Bueno, eso depende de la asignación de Expand: si esta
es Verdadero, el control se ensanchará hasta ocupar todo el ancho del formulario, o 200px. Si
esta es Falso, el ancho permanecerá en sus 20px normales, y su posición será determinada
por su asignación halign.
Esto es mucho más fácil de observar visualmente. Hay una excelente demostración de
dimensionador como parte de la aplicación DaboDemo; usted puede explorarlo para obtener
una mejor opinión de como cambiando las diversas asignaciones afecta al tamaño y la
posición de los controles dentro de un dimesionador
Los dimensionadores rejilla trabajan casi igual, con unas pocas diferencias importantes. En
primer lugar, todos los controles en una fila dada tienen la misma altura, y todos los controles
de la misma columna tienen la misma anchura. Como resultado de esto, las configuraciones
para RowExpand y ColExpand serán las mismas para todos los controles en una fila o
columna determinadas, al cambiar uno se afectan todos los demás. Diferentes filas y
diferentes columnas pueden tener alturas y anchos diferentes respectivamente. La
configuración de 'proporción' no tiene ningún efecto, pero hay dos opciones disponibles que
pueden servir para diseños interesantes RowSpan y ColSpan. Lo que estas hacen es decirle
al dimensionador rejilla que el control deberá extenderse a través de múltiples filas o
columnas cuando se calcule su tamaño y posición. Esto está más allá del alcance de este
tutorial, pero pensé que debía mencionar esto aquí.
De Regreso al Formulario
Bien, aun cuando estoy seguro de que a estas altura usted no se siente enteramente
confortable con todos los conceptos del trabajo con los dimensionadores, eso debe darle
suficiente base para entender las ideas mientras avanzamos. Para refrescar su memoria, la de
abajo es la imagen de cual es la apariencia del formulario ahora mismo.
Hay varias cosas que están mal visualmente. El contador del valor de las Horas es muy
ancho; no tiene sentido que se extienda a todo lo ancho del formulario. La etiqueta Notas está
centrada verticalmente; yo preferiría tenerla incluso en el tope de su control. Y finalmente,
tener la etiqueta de la caja de chequeo dispuesta de esa forma no es normal; típicamente la
etiqueta es parte de la caja de comprobación misma. Abordaremos todo eso ahora.
Para arreglar el contador, pulse botón derecho sobre él y seleccione el ítem 'Edit Sizer
Settings' en el menú contextual. Va ver una caja de diálogo que luce como la imagen de abajo
a la izquierda. Desmarque la caja etiquetada 'Expand' y deberá ver al numerador encogerse
instantáneamente hasta un tamaño más normal. El diálogo de edición de dimensionador es
una manera práctica de probar con conceptos acerca de dimensionadores: usted puede
cambiar cualquiera de las propiedades que guste y ver los efectos de ese cambio de
inmediato, y si le gusta lo que ha hecho, sencillamente pulse Cancel' y el control regresará a
su apariencia original.
Para corregir la etiqueta Notas', pulse con botón derecho en ella y haga subir el diálogo de
edición de dimensionador. Cambie la alineación 'Valign' de 'Medio' a 'Arriba' Puede que
quiera hacer esto con las otras etiquetas, también, dado que ellas igualmente está alineadas
verticalmente al medio – esto sencillamente no es tan notorio porque los dTextBoxes son
cerca de la misma altura que las etiquetas. Para cambiar la caja de chequeo, necesitará hacer
dos cosas. Primero, presione en la caja y ponga su Leyenda a 'Billed?'. Entonces pulse con
botón derecho sobre la etiqueta y seleccione 'Delete'. Verá un hueco rectangular donde
estaba la etiqueta; este es una ranura visual que indica una posición vacía en el
dimensionador. Durante la ejecución este será reemplazado por un panel en blanco, así que
puede dejar esto así.
¿No se ve tan diferente ¿no es cierto? Pero este es un formulario vivo creado desde su diseño
de clase . Arrástrelo por allí y todavía podrá ver su superficie de diseño deba jo. Cambie de
tamaño el formulario y verá como los controles cambian su tamaño también. Aún no hay
datos dado que no le hemos dicho que cargue ninguno. Vamos a arreglar esto ahora.
Entonces, ¿por qué no vemos ningún dato? Bien, ¡por que en ningún momento nosotros le
dijimos al bizobj que obtuviera alguno! Corrijamos esto ahora: en la ventana de edición de
código asegúrese de que todavía el Objeto es el Formulario y busque el método afterInit() en
el desplegable Método. Por favor, agregue la línea self.requery() después de la instrucción
inicial def. Después de haber hecho esto, ejecute de nuevo el formulario; esta vez deberá ver
algo como lo siguiente:
Observe que hay datos reales en los controles y la barra de estado al final dice algo como 48
registros seleccionados en 0.193 segundos. ¡Congratulaciones! Usted ha creado un formulario
que ha consultado una base de datos remota, extraído algunos datos y los ha mostrado en los
campos correctos – ¡con casi ninguna codificación! Hey, vamos a ver si en realidad hay un
puñado de registros allí.
Cierre el formulario, con botón derecho presione en la gran ranura vacía en la mitad inferior
del formulario y seleccione Interactive Controls/Add Button. Usted verá un botón agregado a
esa área , pero probablemente abajo, en el mismo fondo, dado que un botón ocupa muy poco
espacio. No nos preocupemos por la leyenda por que vamos a borrarlo en un minuto, pero
queremos agregarle un poco de código. Con botón derecho pulse en el botón y seleccione
Edit Code; la ventana de edición de código se vendrá al frente y el método onHit() del botón
estará seleccionado. Agregue la línea self.Form.Next() después de la declaración def.
Corra el formulario otra vez y observe lo que pasa cuando presione en el botón: los distintos
registros en el conjunto de datos se muestran en el control, exactamente como usted lo
esperaba. Pero hay algo también erróneo: el campo Cliente muestra un número, no el nombre
del cliente. Ocurre que el numero es el valor de la clave foránea a la tabla cliente para el
cliente asociado con ese registro, pero esto no es muy útil. Un usuario introduciendo sus
horas facturables querrá escoger un nombre, no un número de ID. Arreglemos eso ahora.
Podríamos haber usado los métodos de SQL Builder con un cursor temporal, pero vamos, ¿qué
tan difícil es escribir una sencilla instrucción sql como esta? En cualquier caso, corremos la
consulta básica para obtener las Pks y los nombres y crear dos listas: una que contiene los
nombres y la otra contentiva de las claves.
A continuación, nos aseguramos que el ClientBizobj también esté cargado dentro del
formulario. Así que regrese al Diseñador de Clases y abra el método createBizobjs en la
ventana de edición de código. Es mucho más fácil copia/pegar las líneas que crean el bizobj
hours y cambiar los nombres a client. Él debería aparecer como este:
def createBizobjs(self):
hoursBizobj = self.Application.biz.HoursBizobj(self.Connection)
self.addBizobj(hoursBizobj)
clientBizobj = self.Application.biz.ClientBizobj(self.Connection)
self.addBizobj(clientBizobj)
A continuación, vaya a la superficie de diseño y borre la caja de texto para el cliente. En este
lugar cree un desplegable por medio del usual menú de contexto botón derecho/Data
Controls/Dropdown List. Con el desplegable seleccionado, vaya a la hoja de propiedades y
ponga la propiedad DataSource en "hours", y la propiedad DataField a "clientfk". También
necesitamos asignar la propiedad ValueMode a "Key" para decirle a Dabo que no queremos
almacenar la cadena de texto mostrada, si no el valor Key asociado. ¿Cómo obtenemos las
cadenas y las claves se muestren en el control? Seleccione el control y establezca su
propiedad RegID a 'ClientList'. Como veremos más adelante, el RegID es un identificador
único dentro de un formulario que posibilita referenciar objetos de forma simple y sin
ambigüedades. Ahora abra la ventana de edición de código, seleccione el formulario en la
lista desplegable objeto y seleccione 'afterInitAll' en el desplegable de métodos. Añada el
siguiente código a ese método:
def afterInitAll(self):
clientBiz = self.getBizobj("clients")
names, keys = clientBiz.getNamesAndKeys()
self.ClientList.Choices = names
self.ClientList.Keys = keys
self.ClientList.ValueMode = "Key"
self.requery()
Vamos a ver lo que éste está haciendo: la primera línea le solicita al formulario una referencia
al bizobj client; lo hace pasándole la DataSource del bizobj que le interesa al método
getBizobj() del formulario. Esta es la forma primaria de obtener referencias a bizobj desde
un formulario. Luego, él llama al método que acabamos de crear para para obtener las listas
names y keys. Después, asigna a su propiedad Choices los nombres, y a su propiedad Keys
la lista keys. En los controles lista, la propiedad Choices contiene los elementos a mostrar;
cambiando esta propiedad se cambian las opciones disponibles para el usuario. La propiedad
Keys se usa 'bajo la manga' para asociar el valor mostrado con un valor de clave; esto se
hace mediante una asociación 1:1 entre los dos: la primera entrada de Opciones es asociada
con la primera Clave; la segunda entrada de Opciones es asociada con la segunda Clave; etc.
Ahora, como establecimos la propiedad ValueMode a Key, el control mostrará la Opción que
corresponde al valor clave ligado. Similarmente, si el usuario selecciona una Opción diferente,
el valor subyacente de Clave será cambiado.
Finalmente, llamamos a self.requery() para extraer los datos para el formulario y actualizar
todos los controles con los datos.
Necesitamos añadir ese nuevo método a biz/ClientBizobj.py, así que abra ese archivo en un
editor de texto y agregue el siguiente método:
def getNamesAndKeys(self):
"""Retorna una 2-tupla de listas de los nombres de clientes
y sus claves.
"""
crs = self.getTempCursor()
crs.execute("""select pkid, clientname
from clients
order by clientname""")
ds = crs.getDataSet()
# Crear las listas
names = [rec["clientname"] for rec in ds]
keys = [rec["pkid"] for rec in ds]
return (names, keys)
Idealmente, podríamos correr el formulario modificado, pero tenemos un problema: el bizobj
clientes fue cargado antes de que añadiéramos el método getNamesAndKeys(), y si tratamos
de ejecutar el formulario ahora, éste no encontraría ese método en la versión del bizobj
memoria, y lanzará un error. Así que guarde todo, cierre el Diseñador de Clase, y luego
inícielo de nuevo con el mismo mandato de la última vez. En este momento su clase
guardada abrirá directamente. Ahora puede tratar de ejecutar el formulario y mientras
presiona el botón para navegar por el conjunto de registros, deberá ver cambiar el nombre
del cliente. Cierre el formulario en ejecución y después borre el botón; verá regresar la gran
ranura vacía. Llenaremos esto más a continuación. Pero antes, tratemos de correr este
formulario directamente, en vez de desde el Diseñador de Clase. ¿Recuerda que cuando
creamos la aplicación con dabo.quickStart(), éste puso como la MainFormClass a la clase
predeterminada dabo.ui.dFormMain? Bien, tenemos ahora un formulario que trabaja, de modo
que cambiemos main.py para usar este formulario en su lugar. Cambie su main.py para que
se lea:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import dabo
dabo.ui.loadUI("wx")
app = dabo.dApp()
app.MainFormClass = "hours.cdxml"
app.BasePrefKey = "billing_tutorial"
app.start()
Hay unos pocos cambios que hice aquí. En primer lugar, removí el comentario de advertencia
ya que no se necesita. Segundo, establecí la propiedad MainFormClass del formulario como
la cadena hours.cdxml. Observe que los archivos .cdxml no son clases Python; son sólo
archivos XML que Dabo sabe como procesar para crear clases Python dinámicamente durante
la ejecución. Así que en lugar de asignar a la MainFormClass una clase Python real como
hicimos antes, le asignamos un archivo el nombre de un archivo cdxml que Dabo puede
convertir en una clase Python,. Podríamos haberle asignado la ruta completa al archivo
cdxml, pero como hemos seguido la estructura de directorios estándar de Dabo creada por
quickstart(), Dabo hallará ese archivo en el directorio ui.
La otra cosa que hice fue establecer la propiedad BasePrefKey. Usted puede haber notado
ya que Dabo parece recordar algunas cosas cuando usted corre una aplicación, tal como el
tamaño y posición de las ventanas. Esto es manejado automáticamente por el sistema de
preferencias de Dabo; las preferencias se mantienen bajo la propiedad BasePrefKey de cada
aplicación para conservarla distinta. Si BasePrefKey no está configurada, Dabo usa la ruta al
directorio raíz de la aplicación. Estableciendo este valor aquí estamos seguros de que las
preferencias de nuestra aplicación se mantienen, no importa si movemos la aplicación a otra
parte.
Hagamos una prueba: guarde main.py, y luego córralo. Debería ver el formulario que creamos
en el Diseñador de Clase, completo, con datos. Antes de cerrarlo, cámbielo de tamaño y
reposiciónelo. Cierre el formulario y luego ejecute main.py de nuevo. Note que el formulario
se abre con el mismo tamaño y en la misma posición en que lo dejó.
Hay una herramienta visual para inspeccionar la base de datos preferencias; esta está en la
carpeta ide, y es denominada PrefEditor.py. No es la herramienta más pulida, pero si le
permite a usted editar o borrar cualquier preferencia para sus aplicaciones.
Ahora active el formulario, y deberá ver que la rejilla es poblada con todos los registros
existentes de la tabla hours. Hay varias cosas que necesitamos arreglar. Primero, tenemos el
mismo problema de antes con el cliente: se muestra el ID y no el nombre. Dado que esta área
no es de entrada de datos, no queremos usar un control lista, así que tenemos que añadir la
tabla relacionada al SQL del bizobj hours. La otra cosa notable es que estamos mostrando
todos los registros, cuando todo lo que queríamos eran los registros no facturados. Podemos
agregar esa condición al SQL también. Finalmente, los datos están desordenados aun cuando
tiene mayor sentido tenerlos ordenados por fecha. Así que agreguemos eso al SQL, también.
El código del bizobj hours deberá parecerse al siguiente; las líneas que he agregado están
marcadas con flechas. Tome en cuenta también que tuve que agregar el alias de la tabla a las
llamadas a addField(), dado que la unión con la tabla clientes introduce ahora una
ambigüedad, por que ambas tablas tienen una columna llamada pkid. Técnicamente, sólo ese
campo necesita ser calificado totalmente, pero por el interés de la consistencia los he
cambiado todos.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import dabo
class HoursBizobj(dabo.biz.dBizobj):
def afterInit(self):
self.DataSource = "hours"
self.KeyField = "pkid"
self.addFrom("hours")
self.addJoin("clients",
"hours.clientfk = clients.pkid") # <---
self.addField("hours.clientfk")
self.addField("hours.hours")
self.addField("hours.notes")
self.addField("hours.billed")
self.addField("hours.servicedate")
self.addField("hours.pkid")
self.addField("clients.clientname") # <---
self.addWhere("billed = false") # <---
self.addOrderBy("servicedate asc") # <---
def validateRecord(self):
"""Regresar alguna otra cosa que una cadena vacía desde
este método prevendrá que los datos sean guardados.
"""
ret = ""
Def afterInitAll(self):
clientBiz = self.Form.getBizobj("clients")
names, keys = clientBiz.getNamesAndKeys()
names.insert(0, "-Please Select a Client-")
keys.insert(0, 0)
self.Choices = names
self.Keys = keys
self.requery()
self.new()
Guarde estos cambios , y salga del Diseñador de Clase. Para la fecha, la mayoría de las veces
querremos que sea la fecha del día. Afortunadamente, Dabo provee una forma sencilla de
proporcionar valores predeterminados para registros nuevos; los bizobjs tienen una propiedad
DefaultValues, la cual es un diccionario con los nombres de columna como las claves y los
predeterminados como valores. Así que abra HoursBizobj.py en su editor de texto, y agregue
la siguiente línea:
Por supuesto, asegúrese de añadir la declaración import datetime arriba en el archivo. Cuando
haya guardado estos cambios, corra python main.py para ver como está trabajando la
aplicación. Si usted hizo todo correctamente, debería ver un formulario como el siguiente,
pero con la fecha del día.
Si jugamos un poco con esto, un nuevo problema se hace obvio: si presionamos las flechas
arriba o abajo del contador, los valores se incrementan en una hora cada vez mientras que
estaremos facturando en incrementos de cuartos de hora. Para corregir esto, abra el
Diseñador de Clase y seleccione el contador. En la hoja de propiedades, cambie su propiedad
Increment a 0.25. Ahora cuando presione las flechas, el valor cambiará en incrementos de
cuarto de hora. Y mientras estamos aquí, cambiemos la propiedad Max a 24;
presumiblemente no facturaremos más de 24 horas al día.
Presione con botón derecho en el botón y seleccione 'Edit Code'. La ventana de edición de
código se viene al frente, con el botón como objeto seleccionado. Incluso seleccionó el
método onHit(), ya que ese es el método más probable que necesitará para trabajar cuando
trate con botones. Añada la línea self.Form.save() después de la línea 'def'. Eso es – todo lo
que necesita para guardar sus cambios a través de los botones del formulario es hacer esa
simple llamada.
Necesitaremos un botón 'Cancel', también, pero en lugar de agregarlo como lo hicimos con el
botón Save, ¡sencillamente copiemos y peguemos el botón! Presione con botón derecho sobre
el botón y seleccione 'Copy'. Ahora presione el botón derecho sobre la ranura extrema
derecha – verá una opción 'Paste' en el tope del menú contextual. Selecciónela y una copia
del botón Save aparecerá en esa ranura. Todo lo que tenemos que hacer es cambiar su
leyenda a 'Cancel' y editar su método onHit() para que en vez diga self.Form.cancel().
Los botones lucen bien, pero hay mucho espacio entre ellos (abajo, izquierda). Podríamos
borrar la ranura entre ellos, pero entonces ellos podrían pegarse entre sí. Por contrario,
agreguemos un Espaciador entre ellos. Del menú botón-derecho/Sizers seleccione 'Add a New
Spacer'. Cambie el ancho a 20 y el formulario} ahora tiene la apariencia como la imagen
abajo, derecha.
Guarde esto y salga. Podríamos ejecutar la aplicación, pero antes de hacerlo, hablemos de
validación de datos. Después de todo, esta es una de las funciones más importantes de un
bizobj, pero en verdad no la hemos discutido del todo. Los cambios que hemos hecho
presentan una oportunidad perfecta para validación: si ellos no extraen un cliente del
desplegable, obviamente no podemos facturar a ninguno! Entonces, agreguemos una
comprobación de validación para ello.
Abra HoursBizObj.py en su editor de texto y vaya al método validateRecord(). Modifíquelo
para que se lea:
def validateRecord(self):
"""Retgresar alguna otra cosa que una cadena vacía desde
este método prevendrá que los datos sean guardados.
"""
ret = ""
# Add your business rules here.
If self.Record.clientfk == 0:
ret = "Usted debe seleccionar un cliente"
return ret
Lo que está haciendo es chequear un valor cero para la Clave foránea para el cliente. Si
encuentra ese valor, retorna el mensaje de error. La estructura de trabajo interpretará
cualquier valor de retorno no vacío desde validateRecord() como indicador de una falla de
validación, y levantará una excepción de BusinessRuleViolation.
Guarde sus cambios de bizobj y corra main.py. Agregue algunos valores al formulario, pero de
je el desplegable cliente en el predeterminado selección no especificada. Ahora presione el
botón Save; obtendría un diálogo que despliega el mensaje de error que usted retornó del
método validateRecord().
Corrija este problema de validación seleccionando un cliente verdadero del desplegable y
luego presione 'Save' de nuevo. Esta vez no obtendrá el mensaje de error; por el contrario,
vería un mensaje de confirmación en la barra de estado del formulario.
Pasos Siguientes
Hay muchas cosas que podemos hacer para que esta aplicación sea más funcional, pero
usted puede ver que hemos logrado algo muy cercano a lo que necesitamos bastante rápido,
sin ningún esfuerzo críptico de codificación. Una cosa que sería sencilla de agregar es que la
aplicación llame automáticamente a new() después de un save() exitoso. Sería bueno poder
enterar múltiples sesiones de facturación de una sentada. Así que agreguemos un botón para
ello.
Para corregir el actual problema, en la sección superior desmarque la caja 'Expand' y cambie
HAlign a 'Derecha'. Presione 'OK' y el formulario debe aparecer en la forma que queremos.
Ahora ¿y el código? Podríamos ponerlo en el método onHit() de nuestro nuevo botón, pero
hay una pauta de diseño que va con el Patrón de Cadena de Responsabilidades que
mencionamos anteriormente: los objetos deberían conocer tan solo lo menos posible para
hacer su trabaJo. Entonces, si, pudiéramos poner un montón de código en el botón, pero se
supone que los controles son muy tontos. Ellos deberían sencillamente llamar a un método de
su Formulario para hacerle conocer que el usuario quería hacer algo, y esto es todo. En este
diseño, el Formulario es el cerebro central de la operación. De modo que agregue una sola
línea al método onHit(): self.Form.saveAndNew()
Hay un solo problema: el formulario no tiene un método con ese nombre! Realmente, esto no
es un problema del todo, ya que podemos agregarlo muy fácilmente. Vaya a la ventana de
Edición de Código y seleccione al formulario en el desplegable objeto. El desplegable de
Método contiene los nombres de los métodos más comunes y los manejadores de eventos
disponibles para el objeto seleccionado, pero si usted necesita crear un método nuevo, o hay
problema, para eso está el botón 'New'.
Presione ese botón, e introduzca el nombre de su nuevo método; en este caso este es
'saveAndNew'. Verá ese método aparecer en la ventana Edición de Código y podemos añadir
nuestro código rápidamente:
def saveAndNew(self):
if self.save():
# Un guardado fallido retornará False. Sólo queremos proceder si
# el guardado tiene éxito.
self.new()
self.serviceDate.setFocus()
Espere un segundo – ¿qué es este 'self.serviceDate' en la última línea? Parece como si fuera
una referencia al control fecha, ya que tendría sentido tener en el formulario el foco puesto en
ese cuando se introduce un nuevo registro, pero no hemos creado alguna referencia tal!.
Esto es cierto, pero es muy fácil hacerlo usando la propiedad RegID de todos los controles
Dabo. Esta es una propiedad que generalmente está vacía, pero si usted la establece, ella
debe ser única a través de todos los controles en un formulario dado, y una vez asignada, no
puede ser cambiada. Esto asegura que siempre será posible referirse a ese objeto sin
ambigüedad. Usted podría también usar una lista de nombres, separados por puntos, en la
jerarquía de contenedores del control (p.e.: self.panel1.pageframe1.page3.panel2.button1),
pero esto es muy frágil, dado que los controles son movidos con frecuencia y/o renombrados
durante el desarrollo.
Cuando un control con una RegID no vacía es instanciado, él se “registra” a sí mismo con su
formulario (esa es la parte 'reg' del nombre RegID), y ese RegID se agrega al espacio de
nombres del formulario. Desde ese punto en adelante, cualquier control puede referenciar a
ese control registrado simplemente usando self.Form.RegID, donde RegID es el RegID del
control.
Cada formulario tiene su propia barra de menú, con una referencia a ella en su propiedad
MenuBar. Usted puede obtener referencias a cada menú pasando la leyenda de ese menú al
método getMenu de la barra de menú. Queremos que esos elementos de menú aparezcan
encima de los elementos predeterminados, así que utilizaremos el método prepend() y los
añadiremos desde el final hacia arriba. Primero añadimos un separador , luego los elementos
para las funciones del botón. Tenga en cuenta que podemos pasar la combinación de tecla
HotKey así como también el manejador para ese menú (usando el parámetro estándar
OnHit). La clase formulario viene con manejadores para onSave() y onCancel(), pero el
método saveAndNew() que agregamos anteriormente no es un manejador de evento (i.e., no
acepta un objeto evento como parámetro). Esto es bastante fácil de corregir: podemos añadir
un simple parámetro evt=None a la firma de ese método. Guarde, salga y corra la aplicación.
Verá nuestro menú personalizado (abajo, izquierda).
Observe como la cosa multiplataforma es manejada por usted: especifiqué Ctrl-S como el
atajo, y Dabo sabía que estaba corriendo en una Mac, e introdujo el símbolo Command Key;
también mostró el símbolo para Escape. Si usted corriera la aplicación sobre otra plataforma
diferente, no necesitaría hacer nada distinto del todo. Esta es una captura de la misma
aplicación corriendo sin cambios en Windows XP (abajo, derecha):
Añadir un Informe
¿Qué es una aplicación de facturación si usted no puede generar informes? Agreguemos el
informe más importante en la vida de un consultor: la factura de cliente. Añadiremos otro
elemento de menú que instanciará otro formulario y ese formulario tomará alguna entrada
del usuario para generar el cursor informe. Usaremos el Diseñador de Informes.