jueves, 7 de junio de 2012

Sobre Dibujos y Bibliotecas

El fin de semana pasado me puse a escribir un graficador de de diagramas de Gantt. Mi hermano está trabajando en el modelado de problemas de de planificación y me había comentado que necesitaba algo para visualizar mejor los resultados que le daba el solver. En principio dibujar un diagrama de Gantt no debería requerir de mucha magia, son solo rectangulitos de colores agrupados con algún criterio por fila. Pero otra vez se me apareció la misma pregunta que me hago cada vez que tengo que hacer un programa de esta naturaleza. Por un programa de esta naturaleza entiéndase un programa con gráficos armados a pata (donde dibujo lineas, rectángulos, puntos uno por uno), lo cual implica una API gráfica con funciones de un nivel relativamente bajo, pero donde hay cosas que configurar, que ingresar, que elegir, y entonces donde también quiero tener alguna que otra facilidad de más alto nivel, como la posibilidad de hacer menúes, botones, y demás controles y cuadros de diálogo estándar.

El problema de como dibujar lo enfrenté muchas veces desde que entré en la universidad y lo solucioné de formas muy diferentes. La primera vez necesitaba ver una simulación de una colonia de hormigas para un contest, donde el objetivo era "programar" una hormiga, pero los organizadores no nos facilitaban el simulador. Como la cosa era contra-reloj, no tenía tiempo de aprender algo nuevo, así que fui por el viejo y querido QBasic y sus comandos de dibujo en los modos gráficos de una ventana de DOS. Muy poco elegante, pero sirvió para salir del paso. La segunda vez tenía que simular la planificación de tareas de un sistema operativo para un práctico de la materia Sistemas Operativos, y para verlo usé OpenGL, sin saber mucho y copiando pedazos de código que encontré en Internet. Antes de la tercera vez pasé por la materia Captura y Procesamiento de Imágenes, donde nos enseñaron a utilizar la biblioteca CImg.
Esta biblioteca tiene de todo para dibujar y operar sobre imágenes (desde puntos hasta transformadas de Fourier), y además algunas facilidades para mostrar los resultados en una ventana y manejar básicamente sus eventos. Es excelente a nivel didáctico (muy fácil de usar y permite probar algoritmos muy rápidamente) y también ideal para salir del paso, pero demasiado lenta como para manipular las cosas que se dibujan en tiempo real. Esta es la que usé cuando hice el primer graficador de diagramas de flujo para PSeInt. Para la segunda generación de graficadores (desde que se pueden editar), empecé otra vez desde cero, y como quería que sea animado, fluido, dinámico, etc... (en fin, que llame un poco la atención) utilicé OpenGL. Lo mismo había hecho para mi proyecto final de carrera, donde debía mostrar elementos geométricos.

Analizando pros y contras de las experiencias, por ahora me quedo con OpenGL. Permite dibujar eficientemente, y se puede embeber dentro de otras cosas (por ejemplo en una ventana de wxWidgets, ya llegaremos a eso). Mi gran problema con OpenGL es el texto. OpenGL tiene funciones de dibujo de un nivel bastante bajo, así que no hay nada para mostrar texto. Y aquí aparecen dos alternativas: utilizar GLUT (o freeglut que es la incluida con ZinjaI), que tiene una función para dibujar texto utilizando unas fuentes que trae predefinidas (no considero otras bibliotecas porque no quiero agregar muchas dependencias a programas relativamente simples); o intentar dibujar cada letra a pata a partir de puntos y lineas. La primera opción es un poco más prolija, pero como las fuentes son raster, no se "escalan". A los caracteres de la segunda alternativa se le aplican todas las transformaciones que sean necesarias (traslación, escalado, rotación, etc). Por esto usé la segunda alternativa en PSeInt, pero uso la primera en el que hace los diagramas de Gantt ya que el texto es solo para una leyenda en un eje, o para un rótulo en un evento, y entonces no es necesario ni conveniente transformarlo. Lo de PSeInt fué posible gracias a que tenía disponible una función para graficar un caracter de esta forma hecha por otra persona que ya se había tomado el trabajo de ingresar las coordenadas una por una de cada puntito o cada rayita que forma cada letra. Estas función es parte de uglyfont, que conocí porque su autor, el capitán YS, la escribió para su juego, YSFlight, un simulador de vuelo con gráficos muy básico, pero que no por ello deja de ser muy interesante y robarme unas cuantas horas.

Para cerrar el círculo, hay que ver con qué completar OpenGL, ya que OpenGL de por sí solo dibuja cosas en una región de la pantalla, pero no se encarga de crear una ventana para obtener esa región, ni de manejar eventos (entrada por teclado o ratón por ejemplo), ni de muchas otras cosas, solo dibuja. La primer opción suele ser GLUT, ya que es lo más fácil y rápido para tener un prototipo andando. GLUT permite crear ventanas y declarar callbacks para los eventos más básicos, pero carece de muchas otras cosas útiles y de más alto nivel, como por ejemplo widgets (botones, cuadros de texto, barras de scroll, etc). Ahí es donde entra wxWidgets. En mi proyecto final utilicé wxWidgets para la mayor parte de la interfaz (cuadros de configuración, de selección de archivos, menúes, barras de herramientas, etc), y embebí en medio de todo eso un control wxGLCanvas que genera un contexto para que OpenGL se haga cargo del dibujado de una parte de esa ventana de wxWidgets. Entonces, wxWidgets gestiona todos los controles y eventos de alto nivel, y OpenGL dibuja solo la parte donde realmente necesito dibujar libre y eficientemente. La primera vez que uno intenta esto tiene que renegar con unos cuantos detalles de implementación, y hasta de compilación de la propia biblioteca ya que por defecto no incluye el componente de OpenGL (de hecho, por error mio, la última versión de ZinjaI (20120413) incluye una compilación de wx que no lo tiene). Pero teniendo un buen ejemplo completo ya desarrollado, es bastante fácil y rápido, porque combina lo mejor de los dos mundos (el alto nivel de wxWidgets, y el bajo nivel de OpenGL). En la próxima versión de ZinjaI voy a incluir un template de proyecto listo para aprovechar esta mezcla, para que puedan utilizar como base.

OpenGL dibujando un par de cucarachas en un wxFrame

El problema que me quedaba por resolver era cómo hacer que mi programa imprima o exporte lo que dibuja, sin tener que repetir todo el código que efectivamente dibuja con OpenGL otra vez, pero usando otras funciones de una biblioteca para generar imágenes o imprimir. Dado que en realidad, de las mil cosas que ofrece OpenGL yo uso siempre las mismas 6 o 7 funciones, lo que hice fue escribir funciones con exactamente los mismos prototipos que las que uso en OpenGL, y hacer que la función que dibuja en pantalla, en lugar de llamar a las funciones de OpenGL directamente, use punteros a funciones. De esta forma, haciendo que los punteros apunten a las funciones de OpenGL, o a las alternativas, puedo usar exactamente el mismo código (la misma función o método) para dibujar ya sea en la pantalla, en un archivo, o en la impresora. En este caso, las funciones mías actúan como wrappers para los métodos de la clase wxDC, que es la que permite dibujar en un wxBitmap para guardarlo luego en archivo. El único detalle de implementación que me hizo perder bastante tiempo fue el de las convenciones de llamadas. Cuando un compilador compila una función, tiene que elegir entre varias formas de implementar a bajo nivel (en ensamblador) la llamada (cómo pasar argumentos, si usar registros, cómo y cuales, o usar la pila, etc). Si la convención de llamada de las funciones de OpenGL no es la misma que la declarada en las funciones alternativas o en los punteros a funciones, el compilador dará errores extraños (que se pueden omitir con el argumento -fpermissive, pero es un error utilizarlo en este caso). En GNU/Linux no tuve problemas, pero en Windows tuve que agregar __stdcall con un #ifdef a todas las funciones involucradas en este truco para evitar el inconveniente. Habiendo tenido éxito con este truco, voy a tratar de hacer lo mismo para que el nuevo graficador de PSeInt se encargue de exportar a archivos png o jpg, para dejar de depender definitivamente del original.

Finalmente, tengo una combinación explosiva entre una bibliteca de alto nivel con todo tipo de widgets y más (wx es un framework completo, incluye manejo de strings, de archivos, de red, hasta algunas estructuras de datos adicionales, etc), y otra de bajo nivel para dibujar en detalle exactamente lo que quiero, y además un truco fácil para trasladar el dibujo a un archivo o a una impresora sin duplicar el código del mismo, y todo integrado en mi IDE de confianza. Alguien se preguntó cómo es que hice ese programa en un par de días; yo digo que con las herramientas y los ejemplos adecuados (con énfasis en este segundo punto) hay poco de qué preocuparse. Si algo faltara, se le agrega wxFormBuilder para literalmente dibujar las ventanas con el mouse sin casi escribir código, pero en este caso no lo usé porque dejé que el wxAuiManager se encargue de los paneles (paneles que se acoplan y desacoplan, muestran y esconden, redimensionan, etc, todo automáticamente con esta clase), y si bien leí por ahí que con la última versión del wxFormBuilder se pueden hacer aplicaciones con las clases de aui (Advance User Interfase), no he tenido tiempo de ver cómo funciona. Y por si algo más faltara, todo lo involucrado es 100% libre y portable.

No hay comentarios:

Publicar un comentario