sábado, 25 de septiembre de 2010

Los sizer de wxPython en wxFormBuilder.

Una manera de disponer los widgets de una aplicación wxPython es especificar explícitamente la posición y el tamaño de cada widget cuando se crea (con wx.Point y wx.Size). Un ejemplo de ello lo podemos ver en el diseñador de Frames de Boa Constructor. Aunque este método es razonablemente sencillo (como el usado en Microsoft Visual Studio) tiene sus defectos, más concretamente cuando el tamaño de los widgets y de las fuentes de letras difieren, puede resultar bastante complicado posicionarlos correctamente en todos los sistemas (Windows, Linux, Mac). Además, se debe cambiar explícitamente la posición de cada widget cada vez que el usuario redimensiona el contenedor padre.

Sin embargo podemos utilizar otra técnica. El mecanismo de disposición de widgets en wxPython se denomina sizer. Cada sizer maneja el tamaño y posición de sus windows basado en un conjunto de reglas.

NOTA: Se recuerda al lector que en wxPython una window no es una ventana tal como la conocemos, sino cualquier objeto en wx.

Los sizer se asignan a un contenedor window (normalmente un wx.Panel, aunque puede ser también a un wx.Frame). Así los subwindows creados dentro del padre deben de añadirse al sizer, de manera que son ellos (los sizer) son los que administran el tamaño y posición de cada widget.

Dejando algunos conceptos claros: Frame y Panel

Toda la interacción que se da en un programa wxPython tiene lugar dentro de un widget contenedor, denominado Frame, el cual es una instancia de la clase wx.Frame. Lo normal en una aplicación wxPython es crear subclases de wx.Frame y crear instancias de dichas subclases. Para aclarar mejor el concepto vamos a ver un ejemplo que utiliza posicionamiento por coordenadas de los widgets (utilizando wx.Point y wx.Size), incluyendo dichos widgets directamente en el Frame. El código es el siguiente:

# -*- coding: cp1252 -*-
# 25/09/2010
# Ejemplo de wxPython
# El Viaje del Navegante

import wx

class subclase_frame(wx.Frame):
def __init__(self):
# Llamamos al init (código del constructor) del wx.Frame del que
# heredamos.
wx.Frame.__init__(self, None, -1, 'Registro (Subclase de Frame)',
pos=wx.Point(200, 200),size=wx.Size(400,340))

# Creamos un widget wx.Button, 3 widget wx.StaticText y
# 3 widget wx.TextCtrl.
# Botón de salida de la aplicación.
self.boton = wx.Button(parent=self,id=-1,label="Salir",
pos=wx.Point(296,250),size=wx.Size(75,23))

# Nombre.
self.etiquetaNombre = wx.StaticText(id=-1,label='Nombre',
name='etiquetaNombre', parent=self,pos=wx.Point(16, 18),
size=wx.Size(54, 13), style=0)

self.textoNombre = wx.TextCtrl(id=-1, name='textoNombre',
parent=self, pos=wx.Point(80, 18), size=wx.Size(288, 21))

# Apellidos.
self.etiquetaApellidos = wx.StaticText(id=-1,label='Apellidos',
name='etiquetaApellidos', parent=self,pos=wx.Point(16, 42),
size=wx.Size(54, 13), style=0)

self.textoApellidos = wx.TextCtrl(id=-1, name='textoApellidos',
parent=self, pos=wx.Point(80, 42), size=wx.Size(288, 21))

# NIF.
self.etiquetaNIF = wx.StaticText(id=-1,label='NIF',
name='etiquetaNIF', parent=self,pos=wx.Point(16, 66),
size=wx.Size(54, 13), style=0)

self.textoNIF = wx.TextCtrl(id=-1, name='textoNIF',parent=self,
pos=wx.Point(80, 66), size=wx.Size(288, 21))

# Dirección.
self.etiquetaDireccion = wx.StaticText(id=-1,label='Dirección',
name='etiquetaDireccion', parent=self,pos=wx.Point(16, 90),
size=wx.Size(54, 13), style=0)

self.textoDireccion = wx.TextCtrl(id=-1, name='textoDireccion',
parent=self,pos=wx.Point(80, 90), size=wx.Size(288, 21))

# Código postal.
self.etiquetaCP = wx.StaticText(id=-1,label='Cód.Postal',
name='etiquetaCP', parent=self,pos=wx.Point(16, 114),
size=wx.Size(54, 13), style=0)

self.textoCP = wx.TextCtrl(id=-1, name='textoCP',
parent=self,pos=wx.Point(80, 114), size=wx.Size(50, 21))

# Población.
self.etiquetaPoblacion = wx.StaticText(id=-1,label='Población',
name='etiquetaPoblacion', parent=self,pos=wx.Point(140, 114),
size=wx.Size(54, 13), style=0)

self.textoPoblacion = wx.TextCtrl(id=-1, name='textoPoblacion',
parent=self,pos=wx.Point(200, 114), size=wx.Size(170, 21))

# Provincia.
self.etiquetaProvincia = wx.StaticText(id=-1,label='Provincia',
name='etiquetaProvincia', parent=self,pos=wx.Point(16, 138),
size=wx.Size(54, 13), style=0)

self.textoProvincia = wx.TextCtrl(id=-1, name='textoProvincia',
parent=self,pos=wx.Point(80, 138), size=wx.Size(288, 21))

# País.
self.etiquetaPais = wx.StaticText(id=-1,label='País',
name='etiquetaPais', parent=self,pos=wx.Point(16, 162),
size=wx.Size(54, 13), style=0)

self.textoPais = wx.TextCtrl(id=-1, name='textoPais',
parent=self,pos=wx.Point(80, 162), size=wx.Size(288, 21))

# Correo electrónico.
self.etiquetaEmail = wx.StaticText(id=-1,label='e-mail',
name='etiquetaEmail', parent=self,pos=wx.Point(16, 186),
size=wx.Size(54, 13), style=0)

self.textoEmail = wx.TextCtrl(id=-1, name='textoEmail',
parent=self,pos=wx.Point(80, 186), size=wx.Size(288, 21))

# Teléfono.
self.etiquetaTelefono = wx.StaticText(id=-1,label='Teléfono',
name='etiquetaTelefono', parent=self,pos=wx.Point(16, 210),
size=wx.Size(54, 13), style=0)

self.textoTelefono = wx.TextCtrl(id=-1, name='textoTelefono',
parent=self,pos=wx.Point(80, 210), size=wx.Size(288, 21))

# Creamos los manejadores de eventos, ligando los eventos a
# los métodos que tendrán el código asociado.
self.Bind(wx.EVT_BUTTON, self.OnBotonSalir)
self.Bind(wx.EVT_CLOSE, self.OnSalir)

# Definimos los métodos que contienen el código que se ejecutará
# cuando sean llamados a petición de los eventos definidos anteriormente.

def OnBotonSalir(self, event):
# Cerramos la ventana.
self.Close(True)

def OnSalir(self, event):
# Destruimos el widget.
self.Destroy()

# Creamos una aplicación simple wx.
aplicacion = wx.PySimpleApp()

# Creamos el objeto frame, fruto de la instanciación de la clase
# subclase_frame.
frame = subclase_frame()

# Mostramos la instanciación de la clase subclase_frame.
frame.Show()

# Lanzamos el MainLoop, para escuchar peticiones de eventos.
aplicacion.MainLoop()


Dando como resultado:

La aplicación expuesta está lo suficientemente comentada para comprender el funcionamiento de la misma, por lo que no me pararé en este punto. Sin embargo podemos darle algo más de funcionalidad a este mantenimiento, incluyendo características extras mediante la utilización de paneles.

Un panel es una instancia de la clase wx.Panel, siendo este un contenedor simple para otros widgets con poca funcionalidad. Se debería usar casi siempre el wx.Panel como subwidget de alto nivel para un Frame, por diversas razones:

1) Se puede reusar el código, ya que un mismo panel se puede utilizar en varios Frames.
2) Las instancias de wx.Panel tienen un color de background por defecto en Microsoft Windows de blanco, en vez de gris.
3) Cuando se pulsa la tecla Enter el Panel responde con eventos de teclado de tabulación (para pasar de un widget a otro, como si fuera pulsando la tecla TAB).

Así pues, en el ejemplo anterior, podríamos incluir una instancia de wx.Panel y modificar todos los widget de modo que ahora el padre sea el panel que actuará como subwidget de alto nivel. Esto es, habría que crear el panel justo después de llamar al constructor del wx.Frame en el __init__:

# Creamos un panel.
self.panel = wx.Panel(self, -1)

Y en cada widget cambiar el parent, tal que así:

# Botón de salida de la aplicación.
self.boton = wx.Button(parent=self.panel,id=-1,label="Salir",
pos=wx.Point(296,250),size=wx.Size(75,23))

Aquí he incluido solo el widget botón, pero hay que hacerlo en todos.

El resultado sería:

No solo cambia el color del background, sino que podemos "saltar" de una caja de texto a otra simplemente pulsando la tecla Enter (ó Intro), dando una funcionalidad de la tecla de tabulación TAB.

¿Qué es en realidad un sizer?

Un sizer es un algoritmo automatizado para disponer ó enmarcar un grupo de widgets. Un sizer se adjunta a un contenedor, normalmente un frame ó un panel, como se ha comentado anteriormente. Los subwidgets que se crean dentro del contenedor padre se deben de añadir por separado al sizer. Dicho sizer administra la disposición de los widgets que están dentro de él en el momento en el que se adjunta al contenedor.

NOTA: A los widgets que están dentro de un sizer normalmente se les llama los hijos del sizer.

Las ventajas de utilizar un sizer son sustanciales. El sizer recalculará la disposición de sus widgets hijos cuando cambie el tamaño del contenedor en el que se encuentra. De igual manera, si un widget hijo cambia de tamaño, el sizer puede refrescar automáticamente la disposición de sus hijos. Además, los sizer son fáciles de administrar cuando se quiere cambiar la disposición de los widgets hijos. La principal desventaja del uso de sizers es que pueden llegar a ser restrictivos en algunas ocasiones.

Tipos de sizer

Un sizer de wxPython en un objeto cuyo único propósito es administrar el posicionamiento de un conjunto de widgets dentro de un contenedor (un Frame ó un Panel). Hay que tener claro que el sizer no es un contenedor ó un widget propiamente dicho. Un sizer es la representación de un algoritmo de posicionamiento en pantalla. Todos los sizer son instancias de una subclase de la clase abstracta wx.Sizer. Por último hay que tener en cuenta que un sizer puede estar incluido dentro de otro sizer.

Hay 5 tipos (los más utilizados) de sizer en wxPython, a saber:
  • wx.BoxSizer
  • wx.FlexGridSizer
  • wx.GridSizer
  • wx.GridBagSizer
  • wx.StaticBoxSizer
¿Cómo crear un sizer?

Los pasos para crear un sizer son los siguientes:
  • Crear el panel ó frame (un contenedor) donde se quiere que se automatice el dimensionado.
  • Crear el sizer.
  • Crear los subwindows (widgets ó cualquier otra cosa) como se haría normalmente.
  • Añadir cada subwindow al sizer utilizando su método Add(). Utilizando este método se le pasa al sizer información adicional, incluyendo la cantidad de espacio que rodea al window, cómo alinear el window dentro del espacio asignado que es administrado por el sizer y cómo ampliar el window cuando el contenedor se redimensiona.
  • Los sizer pueden anidarse, es decir, se pueden añadir sizers dentro de un sizer padre como objetos window. Además se puede dejar a un lado un cierta cantidad de espacios en blanco que actúe como separador.
  • Llamar al método SetSizer(sizer) del contenedor.
Utilizando sizers con wxFormBuilder

Decir que las aplicaciones gráficas podemos diseñarlas con herramientas alternativas, como wxGlade (del que tanto se ha hablado en este blog), las cuales nos permiten crear frames visualmente, de manera que veamos in situ como quedarán nuestras ventanas. Cuando se tiene una gran experiencia se llega al punto en el cual no se necesitan de estos diseñadores gráficos, y normalmente el programador directamente lo programa por código, más aún si tiene una jerarquía de clases de donde heredar componentes gráficos, que sería lo deseable para proyectos grandes, esto es, tener frames tipo, menús tipo, paneles tipo, ya diseñados, e ir intercambiándolos según nos convengan mediante herencia y creación de nuevas clases.

NOTA: Se recuerda al lector que para la utilización de wxPython no es necesario ningún diseñador gráfico, todo se puede programar por código. Sin embargo estos programas nos ayudan a visualizar como quedará realmente nuestra aplicación de la capa gráfica, mostrando los atributos y métodos de los componentes que se nos ofrecen.


wxFormBuilder es un diseñador gráfico para las wxWidgets. Esta herramienta está pensada
para ser utilizada en aplicaciones C++, pero también puede generar código Python utilizando en consecuencia wxPython. Este tipo de aplicación, al igual que wxGlade, sirve únicamente para el diseño y construcción de interfaces gráficas, no es un RAD propiamente dicho.

Pequeña introducción a wxFormBuilder

Pasos para utilizar wxFormBuilder:

1) Descargar la aplicación, según nuestra plataforma de trabajo, desde su sitio web, e instalarla.

2) Ejecutar la aplicación, y configurar un proyecto para que genere código Python, en vez de C++, que es el predeterminado. La primera vez que abrimos wxFormBuilder:

A la derecha de la pantalla, en el Object Properties, tenemos las características del proyecto que queremos generar. Como se puede observar está preparado para generar código C++. Esto hay que cambiarlo. Además hay que darle un nombre al proyecto, así como la ruta en donde se generarán los ficheros .py con el código wxPython. Por ejemplo, tal que así:

Hemos creado un proyecto llamado sizers, que generará un fichero sizers.py, en wxPython, puesto que hemos elegido como code_generation a Python.

OBSERVACIÓN: wxFormBuilder tiene multitud de widgets, más que wxGlade y más actualizados, aunque no todos, claro. Así podemos encontrar los formularios (Form) wxFrame, wxPanel y wxDialog; los tipos de sizers wxBoxSizer, wxStaticBoxSizer, wxGridSizer, wxFlexGridSizer, wxGridBagSizer, wxStdDialogButtonSizer y spacer; los contenedores avanzados wxSplitterWindow, wxScrolledWindow, wxNotebook, wxAuiNotebook, wxListBook y wxChoiceBook; los widgets comunes wxButton, wxBitmapButton,wxStaticText, wxTextCtrl, wxStaticBitmap, wxComboBox, wxChoice, wxListBox, wxListCtrl, wxCheckBox, wxRadioBox, wxRadioButton, wxStaticLine, wxSlider y wxGauge; los widgets avanzados wxTreeCtrl, wxHtmlWindow, wxRichTextCtrl, wxCheckListBox, wxGrid, wxToggleButton, wxColourPickerCtrl, wxFontPickerCtrl, wxFilePickerCtrl, wxDirPickerCtrl, wxDatePickerCtrl, wxCalendarCtrl, wxScrollBar, wxSpinCtrl, wxSpinButton, wxHyperlinkCtrl, wxGenericDirCtr y CustomControl (se puede incluir un widget en la interfaz de usuario aunque todavía no esté soportado por wxFormBuilder); así como los widgets de menú y barra de herramientas que se incluyen en los Frames, a saber wxStatusBar, wxMenuBar, wxMenu, wxMenuItem, Sub Menus, Menu Separators, wxToolBar, ToolBar Tools y ToolBar Separators. Todos ellos los podemos encontrar en la Component Pallete de wxFormBuilder:

Decir que según se vaya construyendo la interfaz gráfica, wxFormBuilder genera automáticamente el código apropiado. Esto lo podemos encontrar en la parte inferior de la aplicación, donde podemos ver las pestañas Designer (muestra como quedarán los widgets gráficamente y Python (el código generado a partir de los widgets que se encuentran en la pestaña Designer):

Así, si incluimos un widget Frame (haciendo click en la pestaña Forms del Component Pallete) se nos presenta en el Designer el widget gráfico y en la pestaña Python el código generado automáticamente.

Se puede observar que el código generado es wxPython. Si hacemos click en el botón de generar código (F8) se nos crea el fichero .py que hemos configurado anteriormente.

Como se ha comentado anteriormente a la hora de crear un mantenimiento se recomienda el uso de wx.Panel para incluir en ellos los sizers que a su vez incluirán los widgets, puesto que le dan una funcionalidad extra. Ahora bien, con wxFormBuilder podemos crear componentes Frame y Panel por separado, creando clases independientes, de manera que podemos crear componentes y a partir de ellos crear nuevos mantenimientos. Anteriormente hemos insertado en nuestro proyecto un Frame. Vamos a insertar ahora un Panel, que NO esté incluido en el Frame, sino que será un componente independiente. El widget está en la pestaña Form/Panel:


En el Object Tree (árbol de objetos):

Lo que tenemos ahora es un árbol de componentes independientes, esto es, el proyecto está formado por dos componentes que se pueden utilizar mediante herencia. Vamos a crear un segundo Frame, con características diferentes al anterior e incluir un sizer StaticBoxSizer en el Panel (con esto podremos llegar a entender a lo que me refiero con proyecto de componentes). Para hacer esto último hay que seleccionar el Panel en el Object Tree e ir a la pestaña Layout/wx.StaticBoxSizer del Component Pallete:

Darse cuenta que el sizer se encuentra dentro del Panel, tal como aparece en el árbol de objetos. Incluimos ahora dentro del sizer un botón y una caja de texto, que se encuentran en la pestaña Common (widgets comunes).

Si nos situamos encima del wxBoxSizer en el Object Tree podemos ver las propiedades del sizer. Hay una especialmente importante, que es la orientación (orient) de los widgets dentro del sizer. Viendo su valor vemos que es wx.Vertical. Esto quiere decir que conforme incluyamos widgets dentro de este sizer se irán posicionando uno debajo de otro, en vertical (fijarse en el botón y en la caja de texto, uno debajo de otro). Si cambiamos el valor de orient a wx.Horizontal, lo que hace es posicionar los widgets que se vayan incluyendo en los sizer de izquierda a derecha, en horizontal, tal que así:

De esta manera le dejamos al sizer que se encargue del posicionamiento automático de los widgets que están contenidos en él. Generalizando tendremos que:

Imaginemos que ahora queremos incluir en este panel dos cajas de texto más, una debajo de otra, en vertical. Evidentemente si seguimos incluyendo este tipo de widget dentro del sizer actual nos pasará esto:

Evidentemente esto no es correcto. Si queremos cambiar la orientación lo que podemos hacer es incluir otro sizer, dentro del anterior (sizers anidados), y cambiarle su orientación. En este caso he utilizado un wx.BoxSizer que podemos encontrar en la pestaña Layout/wxBoxSizer). Esto es:

¡Vaya! Hemos incluido dentro de wxStaticBoxSizer un wxBoxSizer. Tal como aparece en el Object Tree se añade al sizer contenedor en una posición horizontal, pero claro, tampoco es lo que queremos. ¿Cómo solucionarlo? La respuesta está en crear un sizer con orientación vertical que contenga a los sizer que hemos creado. ¿Cómo? Lo primero moviendo el wxStaticBoxSizer dentro de un nuevo wxBoxSizer (click botón derecho sobre el contenedor al cual queremos crearle un sizer padre), tal que así:


Darse cuenta de cómo queda el Object Tree. Todo lo que hemos hecho está incluido en un nuevo wxBoxSizer, y su componente hijo es un sizer wxStaticBoxSizer. Ahora solo falta decirle a wxFormBuilder que el wxBoxSizer padre (con orientación vertical) está compuesto por un wxStaticBoxSizer y justo debajo por un wxBoxSizer. Esto es tan sencillo como hacer Drag & Drop, seleccionando y arrastrando el wxBoxSizer (de abajo) y soltándolo sobre el wxBoxSizer padre, quedando:

De esta manera, tenemos un wxBoxSizer padre con orientación vertical, que tiene como hijos un wxStaticBoxSizer, el cual tiene orientación horizontal y en su interior dos widgets (un botón y una caja de texto), y un wxBoxSizer al cual queremos incluirle dos cajas de texto, una debajo de la otra. Lo que falta es justamente eso, asñadirlas y decirle la orientación deseada.

Analizando el árbol de objetos vemos que en realidad lo que hacemos es anidar sizers que contienen widgets que pueden estar a su vez orientados de maneras diferentes dentro de dichos sizers.

La forma en cómo se comportan los sizers a partir de la redimensión de los contenedores padres (en este caso un Panel) se controla mediante los flags de sizeritembase en la pestaña Properties:

En este caso se ha incluido el flag wx.Expand a las cajas de texto, de manera que cuando se redimensione el Panel las cajas también lo hagan.

Una propiedad muy importante es la proportion en sizeritem, que debería de tener valor 0, para controlar la estrechez de los sizer. Si la modificamos en wxStaticBoxSizer mira como queda ahora:

Es más, si también lo modificamos en el wxBoxSizer hijo tenemos que (fijarse en el margen de color rojo que identifica al sizer):

Para que quede mejor el panel podemos decirle a wxFormBuilder que la caja de texto del wxStaticBoxSizer se expanda y se redimensione a todo el tamaño del contenedor. El problema radica en que no podemos hacer eso ahí, puesto que estamos dentro de un sizer con orientación horizontal. ¿Cómo se resuelve? Igual que antes. Este widget lo incluyo dentro de un sizer y hago lo que necesite. Esto es:

E incluimos el flag wx.Expand:

Redimensionamos un poco el panel e incluimos una nuevo texto en el botón así como en texto del wxStaticBoxSizer, quedando:

Bien, la operativa para trabajar con sizers (y sus tipos) es siempre la misma. Lo único que difiere es que hay tipos de sizers con unas características propias que los distinguen, evidentemente (un wxFlexGridSizer se comporta diferente a un wxBoxSizer, pero son sizers).

Si vemos el código generado en la pestaña de Python podemos observar que aparecen los pasos de creación y utilización de sizers que hemos comentado más arriba. Lo único que nos hemos ahorrado ha sido la codificación, creando la interfaz en un entorno gráfico WYSIWYG (lo que ves es lo que obtienes).

Creando una interfaz real con wxFormBuilder

Llegados a este punto vamos a crear el mantenimiento de Registros visto anteriormente, esta vez con sizers. Decir que la operativa se ha seguido es la misma que hemos visto en el apartado anterior, por lo que solamente muestro el árbol de objetos y la interfaz terminada.

Como se puede observar el proyecto se compone de dos widgets, un Frame y un Panel, independientes entre ellos. Una vez tenemos creada la interfaz tenemos que crear la clase que herede del Frame e incluir en él el Panel que hemos diseñado. Darse cuenta que en el Frame no hay nada, todos los componentes widgets están dentro del Panel. Esto es especialmente aconsejable ya que podemos tener varias clases de Frames y Paneles, con lo que podemos tener varias clases de interfaces, únicamente mezclando componentes. Esto es lo que se llama reutilización de componentes, se construyen objetos nuevos a partir de trozos de código ya creado, para formar nuevos elementos con diversas funcionalidades.

NOTA: wxFormBuilder da la posibilidad de crear el código para los manejadores de eventos y las funciones que se ejecutarán cuando dichos eventos sean una realidad. Para ello tan fácil como seleccionar el widget al cual se le quiere asignar un manejador de eventos e irse a los Events del Object Properties. Por ejemplo, para incluir la función que se disparará cuando se haga click en el botón de "Salir":

Cuando ocurra el evento OnButtonClick (hacer click en el botón) del widget wxButton botonSalida se disparará la función OnSalir. Si nos vamos a la pestaña de Python vemos el código generado automáticamente:



En este ejemplo el código generado por el diseñador wxFormBuilder se guarda en sizers.py. En él se han creado las clases frame_generico (el Frame) y panel_registro (el Panel). Un código para utilizar estos dos componentes sería el siguiente:

(El entorno en el que he desarrollado la programación de este artículo es PyScripter, una alternativa muy interesante para desarrollos profesionales, muy potente y más ligera que NetBeans. De lo mejor que me he encontrado en IDE's no comerciales para desarrollar en Python. Recomendado!!!)

Dando el siguiente resultado:


CONCLUSIONES

El sizer es la herramienta que nos brinda wxPython para poder disponer los widgets dentro de contenedores padre. De esta manera se ahorra tiempo, dejando la administración del posicionamiento y respuesta de redimensionado en sus manos. Es aconsejable utilizar el componente wx.Panel para el diseño de interfaces, ya que nos brinda un funcionamiento extra, así como la realización de una jerarquía de componentes.

wxFormBuilder es un entorno WYSIWYG para el diseño gráfico de interfaces para wxWidgets, que nos da la posibilidad de generar código Python con la plataforma wxPython. Una de las grandes ventajas de wxFormBuilder es que contiene bastantes widgets, así como la posibilidad de ampliarlos. Además, trabajar con sizers en wxFormBuilder es muy fácil e intuitivo, ya que nos da la posibilidad de ir cambiando las características de nuestra interfaz, viendo los resultado in situ. Hay varias aplicaciones de este tipo, como wxGlade (del que tanto se ha hablado en este blog) ó wxDesigner (excelente, pero de pago). Se aconseja al lector profundizar en estos entornos de diseño de interfaces. En siguientes post veremos los diferentes tipos de sizers de los que dispone wxPython, estudiando su comportamiento en profundidad, mediante wxFormBuilder.

Un saludo.

sábado, 4 de septiembre de 2010

Lanzar informes Crystal Reports desde Python



Hola. En este post vamos a ver cómo lanzar informes de Crystal Reports desde Python. Veremos la forma tanto de enviar directamente el informe a la impresora, como de hacer una visión preliminar utilizando formatos de salida PDF, así como de paso de parámetros a dichos informes, entre otras cosas. Todo ello desde Python, por supuesto.

Si el lector desconoce Crystal Reports, solo decir que es un software de creación de informes de todo tipo a partir de un origen ú orígenes de datos (bases de datos, cubos OLAP, Excel, ficheros de texto, etc), de los más utilizados a nivel mundial, debido fundamentalmente a su gran adaptabilidad con el software de desarrollo de Microsoft (Visual Studio).

No es propósito de este artículo enseñar cómo funciona Crystal Reports, así que se supone que el lector tiene conocimientos mínimos sobre este software de creación de informes. Decir que Crystal está pensado para plataformas Microsoft Windows. En mi caso, utilizaré Crystal Reports XI sobre Windows XP Service Pack 3.

Para poder utilizar Python y Crystal Reports primeramente hay que descargarse, de SourceForge, Python for Windows Extensions (pywin32) de Mark Hammond. En mi caso, que tengo Python 2.5, me descargo pywin32 para Python 2.5 y se instala normalmente.

Darse cuenta que se instala PythonWin.

¿Y por qué instalarse pywin32? La respuesta a continuación.

Microsoft's Common Object Model (COM)

Component Object Model (COM) es una plataforma de Microsoft para componentes de software. Esta plataforma es utilizada para permitir la comunicación entre procesos y la creación dinámica de objetos, en cualquier lenguaje de programación que soporte dicha tecnología. Es decir, utilizando COM se puede escribir código en un lenguaje de programación, como C++ y usar el código en otro lenguaje de programación, como Visual Basic ó Python.

El término COM es a menudo usado en el mundo del desarrollo de software como un término que abarca las tecnologías OLE, OLE Automation, ActiveX, COM+ y DCOM.

Python tiene un soporte excelente para COM. De hecho, el soporte de Python es tan bueno que se puede usar para extender Python.

Python puede usar y crear objetos COM. Si se necesita escribir una extensión Python exclusivamente para Windows, habría que considerar utilizar COM.

Esencialmente COM es una manera de implementar objetos neutrales con respecto al lenguaje, de manera que pueden ser usados en entornos distintos de aquel en que fueron creados. COM permite la reutilización de objetos sin conocimiento de su implementación interna, ya que fuerza a los implementadores de componentes a proveer interfaces bien definidos que están separados de la implementación.

Para utilizar COM en Python es necesario instalar Python Win32 extensions, tal como se ha indicado al comienzo del post.

Utilizando un componente COM

Los pasos para usar un objeto COM en Python son los mismos que en cualquier otro lenguaje de programación. Primero, hay que obtener una instancia de la clase, esto es, un objeto. Una vez instanciado, se utilizan los métodos y atributos del objeto COM como si fueran objetos normales en Python.

Para utilizar un objeto COM en un código Python hay que comenzar ejecutando la utilidad makepy, en PythonWin, para buscar el componente COM que queremos usar.


Esto se hace básicamente por dos motivos (aunque no es obligatorio). El primero, cuando se importan las constantes de clase, se dispone de los nombres de las constantes definidos en la librería de tipos (typelib), y en segundo lugar obtenemos completitud de código de los métodos y atributos del componente (si no se utiliza makepy habrá que usar valores numéricos en ver de nombres de constantes).

Abrimos makepy y se nos muestra una lista de los componentes COM registrados del sistema. Seleccionamos el que queremos utilizar.

¿Qué componente de Crystal Reports utilizar?

NOTA: Hace unos años que empecé a trabajar con Crystal Reports, en su versión 8.5. Atacaba una base de datos, desde Microsoft Visual Basic 6, con el objeto Crystal Reports ActiveX (CRYSTL32.OCX), el cual es sencillo de utilizar. Sin embargo Crystal Decisions (ahora SAP) desde la versión 7 de Crystal Reports recomienda el uso del Report Designer Component (RDC) para todo desarrollo o integración con aplicaciones propias. Es más, trabajar con RDC aporta herramientas nuevas. Por ejemplo con RDC se puede exportar a PDF, no pudiendo con Crystal Reports ActiveX.

El RDC agrupa a cuatro controles (por lo menos en la versión 8.5 de Crystal):

  • Designer Runtime Library (CRAXDRT.DLL): Es un servidor de automatización COM que provee la manipulación del informe así como de sus funciones de impresión, y de exportación.
  • Distributable Report Designer: Es un control ActiveX que permite diseñar y crear informes dentro de una aplicación. Se necesitan licencias de usuario para su uso. No se utilizará aquí.
  • Designer Designe Runtime Library (CRAXDDRT.DLL) que provee toda la funcionalidad del primero pero que además soporta el Distributable Report Designer.
  • Crystal Report Viewer: Es un control ActiveX que se usa para las vistas previas de informes.
En este artículo veremos la utilización de Designer Runtime Library, para aprender como intregrar el RDC con Python.

¿Qué es el RDC?

El Report Designer Component es un modelo de objeto de interfaz dual basado en tecnología COM, un estándar, que como anteriormente se ha comentado, permite a aplicaciones y objetos componentes comunicarse unos con otros. El estandar no especifica como están estructurados los objetos, solo definen como se comunican entre ellos. El RDC se puede utilizar en cualquier entorno de desarrollo que soporte COM, como es el caso de Python.

Buscando por el makepy nos encontramos con el Crystal Reports ActiveX Designer Runtime Library 11.


Haciendo un makepy al Designer Runtime Library

Hacemos click en OK y nos genera el fichero .py en donde se encuentran las clases, constantes y demás cosas que nos interesan para poder acceder a Crystal Reports.

Vamos al directorio gen_py (en C:\Python25\Lib\site-packages\win32com\gen_py) , que es en donde se ha generado el fichero .py.

Utilizando un IDE (aquí he utilizado Pyragua) ó el propio PythonWin podemos ver el fichero que se genera. En realidad lo que tenemos son todas las constantes y clases que necesitaremos para poder acceder a Crystal Reports.

Por otra parte, si nos vamos al manual que trae Crystal Reports para desarrolladores, el Crystal Reports Developer's Help (CrystalDevHelp.chm), en la sección Understanding the RDC Object Model/The Primary Objects and Collections, nos encontramos con un gráfico de la jerarquía de clases para utilizar en Crystal.

NOTA: Los ficheros de ayuda de Crystal Reports que vienen con el producto los podemos encontrar en:

Si nos vamos al fichero .py generado podemos ver que se trata del módulo que contiene la implementación de la jerarquía de clases expuesta.

¿Cómo utilizar un componente COM en Python?

Utilizar un componente COM en Python es muy sencillo. Después de ejecutar makepy se importa el módulo COM soportado:

import win32com.client as componenteCOM

Si se utilizó la utilidad makepy sobre el componente tendremos definidas laos nombres de las constantes de clase.

from win32com.client import constants

Una vez que el módulo se haya importado se puede instanciar el componente COM de la siguiente forma:

objeto = componenteCOM.Dispatch('CrystalRuntime.Application')

Para instanciar un componente COM se necesita saber el nombre de la clase del componente, que se puede encontrar en la documentación de dicho componente ó mirando las entradas del registro para el objeto. Si se ejecuta el makepy sobre el componente el nombre del componente y GUID se generan en el fichero .py creado.


Lanzar informes de Crystal Reports desde Python: El Ejemplo definitivo.

Bien, después de una introducción algo extensa vamos a ir directamente al ejemplo de uso de informes Crystal por parte de Python.

Como se ha comentado anteriormente, Crystal lee de un origen de datos, en nuestro caso tablas de bases de datos, y mediante un diseñador de informes, configuramos y creamos el informe que será lanzado cada vez que se requiera, como por ejemplo un diseño de factura, albarán, cuenta de resultados, etc.

En Crystal hay dos formas de obtener datos de tablas:

1) Crystal Reports dispone de un asistente de bases de datos para crear los vínculos (relaciones) entre tablas. Se pueden crear ú obtener las relaciones entre las tablas para diseñar el report.

2) Podemos crear en nuestra base de datos una tabla, con unos campos definidos, de modo que se recarguen siempre que se quiera realizar un informe. Esto es, calcular previamente y cargar en esa tabla los campos que conformarán nuestro informe. Este es un truco que se realiza muchas veces para minimizar la complejidad de informes Crystal, de modo que las operaciones que se pudieran realizar sobre el informe se hacen por programa y el resultado se guarda en la tabla de la que lee Crystal. Recordar que esto es bueno cuando la base de datos es muy compleja, esto es, tiene unos vínculos muy grandes, muchas relaciones, y es difícil crear informes. De esta manera, solamente es necesario cargar una tabla. Importante tener en cuenta que dicha tabla es de la que lee Crystal Reports.

NOTA: El lector debe de ser consciente que nunca se ha comentado que Crystal sea fácil de manejar. Únicamente que es una herramienta extremadamente potente para la creación y diseño de informes de gestión.

Imaginemos que tenemos una base de datos con la siguiente estructura:


Por un lado imaginemos que se ha creado una capa gráfica por encima (con wxPython, por ejemplo), para insertar datos. Únicamente nos queda realizar ciertos informes para sacar datos por impresora. Vamos a crear un informe tipo factura básico para nuestro sistema de información.

Abrimos Crystal Reports, conectamos con la base de datos, creamos los vínculos entre tablas que nos harán falta, y diseñamos el informe, tal que así:

Suponemos que se han incluido datos en las tablas de la base de datos...

En nuestro fichero .py que se encargará de la gestión del lanzamiento de informes Crystal podría haber un código tal que así:

from win32com.client import Dispatch
import os

Nombres de fichero.
nom_fich_rpt = os.path.realpath('FACTURA_CLIENTE2.rpt')
nom_fich_pdf = os.path.realpath('factura_cliente_aux2.pdf')

Abrimos fichero Crystal Reports RPT.
aplicacion = Dispatch('CrystalRunTime.Application')
informe = aplicacion.OpenReport(nom_fich_rpt)

NOTA: Este informe Crystal Reports ataca a una diagrama de E-R definido en el diseñador de informes de Crystal. Se le tiene que pasar como parámetro el número de factura.

Impresión directa, pidiendo el parámetro.

Descartamos los datos que se hayan grabado con el informe. De esta manera obligamos Crystal a que nos pregunte los parámetros que necesita el informe.
if informe.HasSavedData: informe.DiscardSavedData()

Imprimimos directamente.
informe.PrintOut(promptUser=False)

Exportar informe RPT a PDF y visualizarlo. Para ello se tiene que tener instalado en la máquina programa que abra ficheros PDF, tal como Acrobat Reader ó similar, que esté predeterminado a usarse en la apertura de este tipo de archivos.

Configuramos exportación. Los parámetros de configuración (sus valores) se pueden obtener de los ficheros de ayuda que vienen con Crystal anteriormente comentados.
informe.ExportOptions.DiskFileName = nom_fich_pdf
informe.ExportOptions.DestinationType = '1'
informe.ExportOptions.FormatType = '31'
Obtenemos campos de parámetros.
parametros = informe.ParameterFields
p1 = parametros(1)
Limpiamos valor del parámetro.
p1.ClearCurrentValueAndRange()
Pedimos nuevo parámetro.
j = raw_input('Nº de factura: ')
Configuramos parámetro del informe.
p1.AddCurrentValue(j)
Exportamos informe.
informe.Export(False)
Lanzar el programa por defecto que abre los ficheros PDF.
os.startfile(nom_fich_pdf)

Podemos obtener las tablas que se utilizan en el informe.
for i in informe.Database.Tables: print i.Name

Y mostrar la sentencia SQL del informe.
print informe.SQLQueryString

Si queremos que Crystal nos muestre a qué fichero exportar de una lista predeterminada, puede hacerse, de la siguiente manera:
informe.Export(True)



CONCLUSIONES

En este post se ha visto unas nociones básicas de objetos COM, y como desde Python, por medio de pywin32, se puede utilizar dicha tecnología.

Crystal Reports en un generador de informes, comercial, nada barato (rondando los 600 euros), pero extremadamente potente, que se puede utilizar para la creación de informes para nuestro sistema de información. Evidentemente hay otros productos en el mercado, como ReportLab (del que tanto se ha hablado en este blog), que tiene una versión gratuita, pero sin el diseñador y la potencialidad de su entorno de desarrollo.

Con este artículo se pretende que el lector se haya dado cuenta de las posibilidades de Python de poder acceder a otros paquetes de software, su integración con los mismos, y su extremada sencillez de utilización (uso de acceso a COM, no de utilización de Crystal).

Saludos.