La etapa que normalmente se espera más dentro de la creación de una aplicación es la de la implementación, sin embargo es importante haber llegado a un buen avance de las partes que la preceden para evitar 'improvisar' el código. Normalmente no se debe crear una aplicación desde cero, se puede utilizar alguna herramienta que escriba el código a partir de los diagramas de clases (por ej. Rose), o bien un entorno de desarrollo que cree un 'esqueleto' de la aplicación (ej. Visual C++). Por desgracia en nuestro caso en particular, disponiamos solamente de una herramienta para la creación de ventanas llamada fdesign.
Durante la etapa de implementación, se hizo
énfasis en los siguientes puntos principalmente:
5.1 Las clases C_ObjetoInteractivo y C_ObjetoModificable.
En la etapa de diseño definimos la estructura
de clases que se acerca a lo que podemos llamar 'definitivo'. Sin embargo,
existen algunos problemas que surgen en el momento de la implementación
a nivel del paso de referencias a objetos. Si recordamos el funcionamiento
del manejador de ventanas, este es capaz de instanciar ventanas para diversos
tipos de clientes, pero a nivel de código, esto puede presentar
alguna dificultad. Para resolverla, se decidió crear una nueva clase,
de la cual hereda cualquier objeto que pretenda hacer uso del manejador,
su nombre es C_ObjetoInteractivo. De esta forma, el manejador recibe una
referencia a C_ObjetoInteractivo y así, cualquier objeto que haya
heredado de esta clase entra en la categoría. Aprovechando esta
clase, se le incluyó una referencia a C_Ventana, de igual forma
que lo mencionado anteriormente, cualquier ventana puede entrar dentro
de esta categoría, adelante veremos cuando se utiliza esta referencia.
Finalmente, la clase tiene tambien un identificador a la ventana que
se tiene asociada. A continuación se muestra la declaración
de esta clase.
class C_ObjetoInteractivo
{
protected:
id_vent IdVentana;
C_Ventana *VentanaAsociada;
public:
id_vent LeeIdVentana()
{return IdVentana;}
void AsociaVentana(C_Ventana
*ApVent) {VentanaAsociada=ApVent;}
};
Esta misma estrategia se empleo para el paso de referencias
dentro del procesador de imágenes, para ello se emplea otra clase
llamada C_ObjetoModificable. Hay que notar que un objeto interactivo no
es necesariamente modificable por algun proceso y viceversa. El objeto
modificable sólo contiene un identificador de la clase cliente.
5.2 Encapsulando la interfase de ventanas.
Un problema difícil con el cual nos topamos durante la implementación fue el de encapsular la librería de interfase de ventanas dentro del modelo. A continuación, se muestra un diagrama que muestra el procedimiento a seguir para obtener interacción por parte del usuario, se trata del diagrama de secuencia de la interacción con la ventana principal.
En este diagrama podemos ver cuales son los pasos que debe seguir un objeto que desea interacción por parte del usuario. Primero, debe llamar a la función SeleccionaVentana() del manejador, le envia dos parámetros, un identificador de la ventana que desea abrir (definidos en el archivo General.h), y una referencia al objeto que va a ser el cliente, en este caso se trata de si mismo, por eso se envia 'this' (puede ser una referencia a otro objeto que a si mismo).
Del lado del manejador, se procesan las peticiones de la manera siguiente:
bool C_ManejadorDeVentanas::SeleccionaVentana(id_vent
seleccion,C_ObjetoInteractivo* Cliente)
{
C_Ventana *VentanaActual;
bool RetVal;
// El valor que regresa la ventana despues de que se cierra.
switch(seleccion)
{
case IDV_APLICACION:
// LLamada realizada por CAplicacion::Inicia()
VentanaActual=new
C_VentanaPrincipal(Cliente);
Cliente->AsociaVentana(VentanaActual);
// Asocia la ventana actual
break;
case IDV_SEL_ALGORITMO:
// LLamada realizada por CAplicacion::Menu_Procesar()
.
. (<- Aqui van otras líneas de otras ventanas)
.
// _________________________________________________________________________
VentanaActual->AbreVentana();
// Abre la ventana.
VentanaActual->ActivaVentana();
// Entra al ciclo de espera de eventos
RetVal=VentanaActual->CierraVentana();
// Cierra la ventana.
delete VentanaActual;
// Libera la memoria...
return RetVal;
}
Como vemos, el manejador instancia la ventana, enseguida la asocia al objeto cliente (esto esta garantizado pues se deriva de C_ObjetoInteractivo). El manejador invoca la operación AbreVentana() en donde se realizan inicializaciones y después invoca ActivaVentana() que básicamente es la entrada al ciclo de espera de eventos. Enseguida se cierra la ventana, se destruye el objeto y se regresa un valor que da la ventana.
Regresando al diagrama de secuencia, vemos que al recibir interacción, la ventana llama a ciertas operaciones de la aplicación. El control esta entonces del lado de la ventana, sin embargo puede aparecer la necesidad de que durante una de las operaciones del objeto cliente se necesite realizar alguna notificación al usuario, eso se realiza gracias a que el cliente tiene una referencia de su ventana, y a través de ella puede acceder a las operaciones públicas de esta.
Todas las ventanas tienen entonces que tener como mínimo las operaciones que se declaran en Ventana.h por ello, se derivan todos de esta clase, garantizando que el manejador no intentará llamar operaciones inexistentes.
class C_Ventana
{
protected:
bool RetVal;
// Valor de eventual retorno
void Inicializa()
{;} // Posibles Inicializaciones.
public:
C_Ventana() {RetVal=TRUE;}
virtual bool AbreVentana()=0;
// Abre una ventana, recibe referencia de la clase que invoco al manejador!
virtual bool ActivaVentana()=0;
// Recibe entrada de la ventana
virtual bool CierraVentana()=0;
// Cierra la ventana
};
La ventaja de esta solución es que se pueden emplear diversas librerias para implementar la interacción con el usuario, ya que casi siempre se sigue una secuencia similar de pasos a seguir para mostrar una ventana en los diversas librerías de interfases que existen.
También mostramos a continuación cómo se implementa del lado de una ventana el llamado a las operaciones de su cliente. En el caso de la librería que empleamos, hubo que implementar un despachador de mensajes, pero esto puede no ser necesario para otras librerias de GUI (interfase gráfica con el usuario).
void C_VentanaPrincipal::Despacha_Callback(long
param)
{
switch(data)
{
case(CB_MENU_ARCHIVO):
Cliente->OnMenuArchivo();
break;
case(CB_MENU_PROCESAR):
Cliente->OnMenuProcesar();
break;
case(CB_MENU_DESPLIEGUE):
Cliente->OnMenuDespliegue();
break;
case(CB_MENU_AYUDA):
Cliente->OnMenuAyuda();
break;
case(CB_BOTON_ARRIBA):
case(CB_BOTON_ABAJO):
case(CB_BOTON_IZQ):
case(CB_BOTON_DER):
Cliente->PonCursor(COLOR_MARCO);
Cliente->MueveCursor(para);
Cliente->PonCursor(COLOR_CURSOR);
break;
case(CB_BOTON_ZOOM):
Cliente->OnBotonZoom();
break;
}
}
Nota: En xforms, no se puede tener acceso directamente a los atributos de la clase desde una de sus operaciones si esta es llamada como 'callback' pues en ese caso hay que declarar a esta operación de tipo 'static'. Es por ello que el archivo manejador.cpp difiere un poco del código mostrado arriba. Posteriormente se tratará este punto con mayor profundidad.
En todas las ventanas se siguen pasos similares a
los de la ventana principal, con excepción de los diálogos
que no necesitan las funciones de AbreVentana() y CierraVentana(), en los
diálogos estas están implementadas como funciones vacías.
5.3 Mejoras en el área gráfica.
El área gráfica es fundamental para
esta aplicación, de ahí que sea necesario un buen desempeño
de su parte. Un punto importante que discutiremos es que la clase implementa
operaciones que permiten el despliegue de los diversos objetos. Esto puede
parecer erroneo pues normalmente uno esperaría que los objetos instanciados
a partir de los derivados de C_ColeccionDeDatos y algunos otros pudieran
'saber' como desplegarse, dado que tienen una operación llamada
DespliegaDatos().
Sin embargo, debemos recordar que uno de los objetivos
del diseño de la aplicación fue de separar las partes con
riesgo de ser dependientes de la plataforma en clases aparte. De esta manera,
ninguna de las clases centrales al modelo contiene comandos que no sean
estándares al lenguaje de programación estándar. Si
por ejemplo un StackDeImagenes incorpora comandos gráficos dentro
de su operación DespliegaDatos(), se pierde la posibilidad de portar
sin modificaciones los archivos que definen esta clase a otra plataforma.
Otra posibilidad sería que la clase tuviera todas las primitivas gráficas encapsuladas en sus operaciones, y los objetos centrales a la aplicación llamarán a estas primitivas para desplegarse. Desgraciadamente, esto traería otro problema pues los objetos también tendrían que encargarse de que el despliegue se este realizando de manera correcta (inicializar el despliegue, colores, etc...).
Es por ello que se decidió que el área gráfica no encapsulara todas las primitivas más básicas de la graficación, sino que pudiera desplegar también los objetos que se tienen definidos, aunque para acceder a los datos de los objetos que se quieren desplegar tendria que obtenerlos a través de sus operaciones. Esto permite que el área gráfica se encargue de 'administrar' el despliegue para que se realice de manera correcta y no se rompen las reglas de encapsulamiento.
Otro punto importante tiene que ver con el hecho de que el área gráfica debe ser capaz de aceptar diversas librerías de primitivas gráficas, en nuestro caso empleamos OpenGL, pero en teoría se podria emplear alguna otra (DirectX por ejemplo). Existen algunas funciones que estan implementadas en una librería y que en otra podrian no estarlo, por ejemplo, el uso de doble buffer para el dibujo. El área gráfica se encarga de hacer 'transparente' esto para los objetos de la aplicación, y ellos sólo tienen que solicitar el tipo de despliegue que se desee (2D/3D) y lo demás se realiza de manera automática.
Algunas dificultades aparecierón por el uso de OpenGL, serán discutidas más adelante.
5.4 Mejoras en la clase C_Imagen.
Una pregunta puede surgir al estudiar el diseño de la aplicación y es ¿ Por qué la clase C_Imagen no se deriva de C_ColeccionDeDatos, si se trata básicamente de un arreglo ? La respuesta es que C_Imagen se usa para instanciar la clase C_StackDeImagenes y si se derivara de la colección, tendría que existir una clase más básica para poder instanciar a C_Imagen y sería algo inutil.
En vez de eso, C_Imagen encapsula un arreglo de elementos de tamaño variable. Sin embargo esto no se realizó con una clase parametrizada, pues C_Imagen debe ser capaz de manejar tipos de dato no estándares como por ejemplo 3 bytes por dato. La solución propuesta fue que el arreglo de elementos fuera de tipo 'void *'. Este tipo de datos prefiere evitarse normalmente, pero las ventajas que proporciono en nuestro caso justifico claramente su utilización.
Un problema que surge con tipos de dato no estándares es que no se puede declarar una funcion LeeDato() que regrese un void *. Lo que se hizo es que para tipos de dato menores a 3 bytes, se emplea la función LeeDato que regresa un entero (tipo de dato en el cuál caben 1 o 2 bytes sin problema). Para 3 bytes o más, se emplea la funcion LeeDatoComoArreglo() que regresa un arreglo de n bytes. Es así como se puede manejar cualquier tipo de dato por la imágen. Esto permite que en el futuro se puedan procesar otros tipos de información, para ejemplificarlo, la aplicación permite desplegar archivos de 3 bytes de información por pixel (1 byte por componente R, G y B) y que tiene como resultado el despliegue de imágenes a color.
5.5 Mejoras y otros usos para la lista ligada.
El uso de una estructura de datos para manejar el arreglo de datos de cualquier tipo dentro de la colección de datos es una idea útil. La clase C_ListaLigada declara una lista de referencias a objetos. La razón por la cual la lista no contiene las instancias es por que la lista se pretende emplear en algunos momentos como un 'vehiculo' para enviar parámetros. Es por ello que no debe contener las instancias de los objetos, de lo contrario, estos serian destruidos junto con la lista. De ahí que la responsabilidad de creación de los objetos no es de la lista ligada, sin embargo, se le puede pedir que destruya a los objetos que referencia antes de destruirse.
Dadas las caracteristicas de la lista ligada, se le encontraron otros usos dentro de la aplicación, por ejemplo dentro de la aplicación, se tenía un stack original y uno de trabajo, pero que sucede si se aplican varios procesos, donde se guarda la referencia a los stacks resultantes de los procesos? Una solución elegante fue emplear una lista ligada para contener todos los Stacks con los que trabaja la aplicación, esto permite tener una cantidad variable sin limite de tamaño. Otro empleo que se le dio a la lista ligada fue para la clase C_Volumen pues un volumen no es sólo una colección de puntos en el espacio tridimensional, sino que cada uno de esos puntos tiene una normal que define su orientación respecto a las fuentes de luz, y un color. Hubiera sido posible crear una clase derivada de C_ColeccionDeDatos para el manejo de las normales, pero las normales no pueden 'existir' como objeto individual de un volúmen, y no hay posibilidad de desplegarlas. Por todo ello, mejor se emplearon dos listas ligadas dentro de C_Volumen, una de puntos para contener las normales y una de colores. Es interesante notar que un volumen no necesita forzosamente que estas listas contengan datos para poder desplegarse satisfactoriamente.
Otros lugares donde se emplean listas ligadas es para los algoritmos. Por ejemplo, la aplicación necesita pasar dos stacks distintos al proceso de reconstrucción, el stack original (niveles de gris) y el segmentado. Estos se introducen a una lista ligada que se pasa al proceso. A partir de los stacks que recibe, el proceso de reconstrucción crea un nuevo stack, el de contornos, y lo añade a la lista que se le envio, así al terminar el proceso, la aplicación tiene acceso al nuevo stack.
Para poder emplear listas ligadas de las diversas
clases, se han definido varias instancias de la clase C_ListaLigada, en
particular podemos mencionar:
Un problema que surgio al emplear listas ligadas de gran tamaño fue que el acceso a sus datos se volvió lento. Esto es debido a que la lista se recorre desde el inicio cuando se le pide uno de sus elementos. Para mejorar el tiempo de acceso, se decidió que la lista mantuviera un arreglo de apuntadores a sus nodos, así no se tiene que recorrer toda la lista para llegar a un nodo dado. Para ello se incluyó la operación ActualizaTabla(). Como su nombre lo indica, esta función actualiza la tabla de apuntadores a los nodos y cambia una bandera de la lista que indica que la lista esta actualizada. De esta forma, cuando se invoca al método LeeDato(DatoNum), la lista utiliza automáticamente su tabla. La actualización se invalida también de manera automática cuando se inserta o se borra un elemento a la lista. Al copiarse la lista, se actualiza la tabla de la lista fuente para optimizar la velocidad.
El resultado de utilizar una lista fue una mejora en todos los aspectos de la aplicación.
5.6 La configuración del stack y sus usos.
Hemos explicado ampliamente los puntos importantes que conciernen al stack de imágenes. Sin embargo, los requerimientos de la aplicación pedian la posibilidad de interactuar con el despliegue, por ejemplo, de ver detalladamente una imágen o de variar la manera en que se despliega un stack. Esto es claramente algo fuera de las responsabilidades del stack, sin embargo, también está fuera de las responsabilidades del área gráfica, que se limita a proveer a lo máximo un área gráfica con ciertas carácteristicas. Si por ejemplo queremos desplegar todo el stack, o solo una parte, quien debe responsabilizarse de realizar la distribución de las imágenes en la pantalla ?
La solución empleada fue la de crear una nueva abstracción, la llamamos C_StackConfig y está asociada al C_StackDeImagenes. Si observamos sus atributos, encontramos que contiene todo lo necesario para poder desplegar un stack en un área rectangular de la pantalla, desde la escala a la cual se despliegan las imágenes, hasta la franja que existe de espacio entre imágenes. También contiene una referencia al área gráfica en la que se despliega el stack. Existe una redundancia al tener una referencia al área gráfica en el Stack y en su configuración. Esto se debe a que la configuración a pesar de tener una referencia a un stack, no puede conocer el área gráfica en que este se despliega, pues esto no es parte de las operaciones que tiene la colección. Por ello el Stack le dá a conocer su área gráfica a su configuración. La utilidad de tener esto es que ante un cambio de tamaño del área de despliegue, la configuración puede actualizar la distribución de las imágenes en la pantalla, así como el tamaño máximo de despliegue.
Otro uso de la configuración apareció posteriormente con la posibilidad de ver una ampliación de una imágen en la pantalla principal, esto se logra simplemente ajustando la escala actual a la máxima posible, pero esto lo realiza automáticamente la configuración, y es posible despues regresar al tamaño original.
La configuración puede ser utilizada para
desplegar una sóla imagen en la pantalla, se debe crear un stack
de una imágen, y esto automáticamente le creará una
configuración. Esto es útil para mostrar imágenes
centradas o escaladas. El uso de la configuración del stack permitió
tener mucha flexibilidad en el despliegue de las imágenes dentro
de la aplicación.