miércoles, 16 de marzo de 2016

Describiendo los cuadros de diálogo de ZinjaI

Tengo en ZinjaI muchísimos pequeños y no tan pequeños cuadros de configuración. Estos cuadros suelen representar un conjunto bastante lineal de "valores" configurables. En general estos "valores" se pueden reducir a tres o cuatro tipos básicos, y entonces todos los cuadros y paneles se pueden reducir a una lista ordenada de valores con sus tipos, más unas pocas indicaciones adicionales y opcionales sobre el estilo visual o pequeños agregados. Estuve rediseñando todo el código alrededor de la confección de estos cuadros de diálogo para que reorganizarlos, cambiar los estilos, o agregar y quitar elementos sea mucho más simple, rápido, y a prueba de errores.

Supongamos por ejemplo que en algún lugar hay que configurar una ruta, un path de un archivo o de una carpeta. Por ejemplo, en las opciones de proyecto al elegir el lugar del ejecutable, la carpeta de temporales, o el directorio de trabajo; en las preferencias al definir dónde buscar cada herramienta externa, o el archivo de autocódigos; en las opciones de Valgrind o CppCheck para los archivos de supresiones; o en las de Doxygen para la carpeta de salida; etc.

Cuadros de texto en la configuración de la compilación y ejecución de un programa simple.

En todos los casos es lo mismo: hay que poner en la ventana primero un texto fijo con la descripción de lo que se configura, y luego un cuadro de texto editable para que el usuario ingrese lo que quiera, y por comodidad un botoncito a la deracha, el de los tres puntos, para que lo seleccione con un selector de archivos en lugar de ingresar todo a mano. Y todo esto con un borde de 5 pixeles general, como para separarlo de lo demás y dar la idea de que va asociado. Armarlo así a mano con wx lleva unas 10 líneas de código como mínimo. Hay que crear un wxStaticText con la descripción, un wxTextCtrl para el valor, un wxTextButton para el botoncito, un wxBoxSizer para alinear texto y botón, y aglutinar todo con las propiedades adecuadas para que quede bien.

Las primera vez que tuve que hacer esto lo hice a mano, paso por paso. La segunda y varias de las siguientes lo copié y pegué de la primera. Cuando ya iban como 10 decidí armar una función que lo haga todo y mandarla a una clase de utilería (mxUT). Pero luego aparecieron variaciones. Si el texto no es una ruta, se parece mucho pero sin botón. Pero, a veces aunque no sea un path va botón igual, y lo uso para otra cosa. Y a veces la etiqueta y el cuadro de texto van en la misma linea porque el valor es cortito. Y el valor podría ser un número en lugar de un texto. Y así hay unas cuantas alternativas. Esto llevó a que esa función auxiliar se convierta en un conjunto de sobrecargas, cada una con no menos de 6 o 7 parámetros. Rápidamente dejó de ser prolijo. Era compacto y rápido, sí, pero no prolijo. Y "no prolijo" implica propenso a errores y difícil de mantener. Entonces me puse a pensar, ¿cómo me gustaría describir (con código) estas ventanas, de forma que sea simple, compacto, cómodo, fácil de usar bien y difícil de usar mal?

La estructura final de la descripción terminó recordando (sin intención) de alguna forma a código html, donde cada elemento empieza y termina con cierta etiqueta (como <H1> y </H1>), y donde hay propiedades básicas implícitas (dadas por una hoja de estilo) que se pueden alterar agregando a la etiqueta solo los atributos que se desee cambiar y nada más. Agregar el primer cuadro de texto de la primer captura se convirtió en algo como:

     CreateSizer(this)
         .BeginText( "Parametros extra para el compilador") )
            .Value( m_source->GetCompilerOptions() )
            .Button( ID_COMPILE_OPTIONS_EXTRA )
          .EndText()
        /* más cosas con .BeginCosa ... .EndCosa */ 
      .SetAndFit();

BeginText y EndText son los tags que marcan el comienzo y final de la descripción de un valor de tipo "Text", y los atributos Value y Button indican qué valor colocar en el interior y que debe tener un botón "...", respectivamente. Todo lo demás, queda por default.

¿Cómo funciona esto? CreateSizer crea un objeto que gestiona el sizer principal del panel o cuadro de diálogo. Tiene métodos como BeginText que crea una clase proxy que representa la información necesaria para agregar un cuadro de texto con todos sus adornos en ese sizer. El constructor de ese proxy define las propiedades más comunes por defecto, y luego con métodos como Value y Button permite modificar algunas. Cada método retorna una referencia a *this, de modo que se los puede encadenar. Al final, el método EndText es el que efectivamente crea los controles, con toda la información acumulada. Y retorna nuevamente al objeto inicial que era dueño del sizer, para poder encadenar más controles.

 Resultado del segundo ejemplo de código

Veamos un ejemplo más complejo (el de la captura), donde BeginLine crea un subcontenedor con interfase similar a la del que crea el CreateSizer, pero donde todo lo que agrego va en una misma linea:

    .BeginLine()
        .BeginCombo( "Nivel de advertencias") )
            .Add("Ninguna")
            .Add("Predeterminadas"))
            .Add("Todas")
            .Add("Extra")
            .Select(configuration->warnings_level)

          .EndCombo()
        .BeginCheck( "como errores" )
            .Value( configuration->warnings_as_errors )

          .EndCheck()
      .EndLine();


Como resultado, en el código expreso exactamente lo que quiero, y nada más que lo que quiero, y todo lo demás está implícito. Tocando, por ejemplo, el código del método EndText puedo alterar la apariencia de todos los cuadros de texto de todas las ventanas desde un solo lugar, como si ese único cpp contuviera mi "hoja de estilos". Más aún, puedo implementar ahí dentro cosas adicionales, como two-way data binding para ahorrarme bastante código luego en los eventos del cuadro de diálogo. Los objetos proxy solo tienen métodos relacionados al tipo de control que estoy agregando, y cada método hace solo una cosa y tiene un nombre descriptivo, por lo que es imposible equivocarse, el código cliente es bastante autoexplicativo y el autocompletado sirve de ayuda memoria para construirlo con facilidad. En definitiva, cumple con todos los objetivos :-).

No hay comentarios:

Publicar un comentario