hbuGUI, la GUI unificada para Harbour

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

Después de muchos meses de contactos y negociaciones es para mi un honor presentar el proyecto hbuGUI, la GUI multiplataforma unificada para Harbour. El proyecto, impulsado desde Harbour Magazine, ha conseguido crear un consenso entre muchos integrantes de la comunidad Harbour que a partir de ahora van a abandonar sus desarrollos propietarios para volcarse en el nuevo proyecto que tendrá licencia open source.

hbuGUI toma como base el proyecto hbui comenzado por Rafal Jopek para desarrollar un auténtico GUI multiplataforma para Harbour, contando con la participación, entre otros, de los siguientes desarrolladores: Przemyslaw Czerpak, Viktor Szakats, Alexander Kresin, Rafa ‘thefull’ Carmona, Antonio Linares, José F. Gímenez, Ignacio Ortíz, Roberto López, Grigory Filatov, Ron Pinkas, Patrick Mast, Teo Fonrouge, Manu Expósito y Manuel Calero. Cada uno de ellos aportará el conocimiento previo adquirido en el desarrollo de sus propios productos propietarios para contribuir al desarrollo de hbuGUI.

Para el primer trimestre del próximo 2019 está previsto que se libere el código de la versión 1.0 del nuevo GUI que contará con los siguientes elementos principales:

  • editor + IDE propio multiplataforma
  • doble jerarquía de clases para facilitar la herencia y los cambios a medida
  • motor de informes basado en fastreport
  • acceso nativo a BBDD relacionales basado en HDO
  • acceso a MongoDB
  • gestor de pruebas unitarias
  • ORM propio basado en Eloquent
  • y todo lo que puedas pedir a sus majestades los Reyes Magos de Oriente

Si has leído hasta aquí debes saber que en España hoy es el día de los inocentes, y evidentemente esto es una broma. Aunque más que una broma es un deseo, el de contar con un único GUI para Harbour que permita que continue el desarrollo de nuestro amado lenguaje y salga del atasco en el que se encuentra ahora.

¡ Féliz Navidad y que el 2019 nos traiga muchas novedades relacionadas con Harbour !

Internacionalización con Harbour

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

Este es un artículo de Quim Ferrer para Harbour Magazine.

Introducción

Aquellos que venimos desarrollando aplicaciones, programas o utilidades desde hace ya algún tiempo, tendemos -algunos- a dejar para el final, casi después de la documentación, el tema de la traducción de nuestros programas a otros idiomas.

Excusas como ya lo haré (futuro inconcreto), no tengo aún demanda o es muy costoso en tiempo y recursos, son por lo general las motivaciones para posponer ‘sine die’ el tema de la traducción.

Desde hace bastante tiempo, existen herramientas generalmente del mundo GNU como es gettext https://www.gnu.org/software/gettext que nos han facilitado la transición por el mundo de las traducciones, organizando el trabajo a realizar, para que pueda ser multidisciplinar y colaborativo.

Las GNU Tools nos permiten automatizar tareas para nuestros desarrollos, principalmente :

  • Extraer cadenas alojadas en variables de nuestro código fuente
  • Agrupar dichas cadenas como definiciones en una plantilla
  • Generar a partir de la plantilla anterior un archivo para cada idioma a traducir
  • Obtener un archivo binario resultado de la compilación de los archivos de traducciones

A nivel técnico, el sistema de traducciones GNU-gettext se basa en la construcción y mantenimiento de los siguientes tipos de archivos :

  • POT. Plantilla de objeto portable. Archivo que se obtiene cuando se extraen textos de una aplicación. Es el formato de texto para enviar a los traductores.
  • PO. Objeto portable. Archivo de texto recibido de los traductores. Incluye los textos originales y su correspondencia con las traducciones.
  • MO. Objeto Máquina. Archivo compilado (binario) obtenido a partir del archivo *.po para cada idioma de la traducción.

El uso de este sistema de traducciones gettext de forma masiva por parte de sitios web multilingües, ha facilitado la adopción ‘de facto’ como un estándar de traducción para varios lenguajes de programación, por ejemplo en PHP.

Antecedentes

Uno de los principales problemas para la difusión (o evangelización) de Harbour es la falta de documentación ‘comprensible’ para su público potencial. Los excelentes desarrolladores de Harbour dotan al compilador de funcionalidades increíbles en relación a una audiencia que considera Harbour una mera ‘traducción’ del CA-Clipper de los años 90. Pero estas ‘extensiones’ fabulosas, no llegan a veces, a sus potenciales usuarios.

Normalmente buscar este tipo de información requiere seguir muy de cerca el grupo de desarrollo de Harbour y profundizar en su código fuente.

Una de las primeras aproximaciones que descubrí para abordar el tema de las traducciones en Harbour, fué el excelente trabajo de investigación de José Luis, editor de Harbour Magazine en una entrada de su blog https://alanit.com/2003/07/i18n-en-xharbour

En aquellos tiempos xHarbour disponía de las primeras herramientas para la internacionalización y en dicho blog, se describe la problemática desde el punto de vista del desarrollador en Harbour que además, quiera utilizar recursos en forma de *.rc, *.dll, etc.

José Luis nos comenta una herramienta llamada hbdict.exe, que es la encargada de extraer los literales tratados con la función i18n. De dicha herramienta, no tengo constancia que esté portada a Harbour ni podamos disponer de ella, si alguien dispone de información, será un placer incluirla en esta documentación.

El siguiente trabajo de investigación me lleva a la guía de uso de la utilidad make de Harbour, llamada hbmk2.exe. Leyendo los innumerables flags de que dispone, presto atención a dos opciones significativas :

  • -hbl[=<output>] nombre-de-archivo .hbl resultante. macro %{hb_lng} es aceptada en nombre-de-archivo.
  • -lng=<languages> lista de idiomas a ser reemplazados en %{hb_lng} macros en archivos .pot/.po y nombres de archivos y salida .hbl/.po. Lista separada por comas: -lng=en,hu-HU,de

En este punto es donde me doy cuenta por primera vez la relación entre Harbour y los formatos pot/po. La conclusión es evidente, Harbour no trata con el formato compilado *.mo ya que dispone de su propio formato, el *.hbl (HarBour Language?) A nivel binario son muy parecidos y desconozco el motivo por el cual los desarrolladores de Harbour optaron por un formato propio y no producir el formato estándar *.mo

Para utilizar otros sistemas make, harbour/bin dispone de la utilidad hbi18n.exe que también es capaz de generar salida *.hbl a partir de *.po

Cómo puede Harbour aprovechar GNU-gettext ?

Con esta pequeña guía, pretendo facilitar la labor a otros desarrolladores que quieran implementar multilenguaje en sus aplicaciones.

  • Para empezar, definimos una macro para poder implementar cambios globales de forma unitaria. En este ejemplo, transformo también la cadena a UTF8
#define _txt( x ) hb_UTF8ToStr( hb_i18n_gettext(x) )
  • Preparar nuestro código Harbour
@ 2,1 SAY _txt(“Editar”)
  • En Fivewin :
REDEFINE SAY VAR _txt(“Editar”) ID 200 OF oDlg
  • Compilar fuente para obtener salida en formato *.pot
harbour -m -n -j app.prg
  • Descargar poedit, herramienta gratuita de edición y traducción : https://poedit.net/download
  • Ejecutar poedit y crear nueva traducción, Archivo-> Nueva desde archivo POT/PO. La primera vez que ejecutemos poedit, nos pedirá el idioma base de traducción.
  • Elegir la plantilla app.pot generada en el proceso de compilación de Harbour. Poedit nos pregunta por el idioma de traducción
  • Empezar con las traducciones:
  • Guardar traducción, por ejemplo para idioma inglés en.po
  • Vemos que también genera el archivo *.mo que no vamos a utilizar
  • Una vez finalizada(s) la(s) traducción(es) es el momento de generar el binario del idioma o idiomas que cargaremos en nuestra aplicación. Para ello disponemos del superpoder de hbmk2.exe, utilidad make de Harbour.
hbmk2 -hbl en.po
  • La utilidad crea el archivo en.hbl a partir de en.po
  • Ya sólo nos queda implementarlo en nuestra aplicación :
cFile := hb_MemoRead( “en.hbl” )if hb_i18n_Check( cFile )   hb_i18n_Set( hb_i18n_RestoreTable(cFile) )endif

Con este procedimiento, todos los literales en código fuente quedan traducidos y si queremos cambiar de idioma en tiempo de ejecución, basta con apuntar a otro set de idioma, llamando a las instrucciones anteriores.

Cada vez que exista un cambio en nuestro código, solamente habrá que generar de nuevo el archivo de plantilla *.pot, abrir cada archivo de idioma *.po y desde la opción del menú de poedit, Catálogo->Actualizar desde archivo POT. Los cambios anteriores permanecen intactos y las nuevas entradas quedan pendientes de traducir, con facilidad para buscarlas.

Os dejo un enlace a github https://github.com/QuimFerrer/i18n con código de ejemplo y uso de un script make hbmk2, para producir la compilación en múltiples idiomas automáticamente.

Para terminar, os animo a investigar, mejorar y comentar, experiencias que tengáis en la internacionalización y traducción de vuestras creaciones

Visual Studio Code para Harbour

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

Dentro de las herramientas de trabajo de los programadores, el editor de código ocupa un lugar destacado. Es uno de los programas con el que pasas más tiempo trabajando, de manera que es muy importante sentirte cómodo trabajando con él y también debes conocerlo lo suficiente para ser productivo utilizándolo.

En esta entrada voy a presentar el editor que poco a poco se ha convertido en mi editor preferido, explicaré los motivos por los que lo uso y las extensiones que utilizo, por supuesto orientado a su uso con Harbour. Reconozco que mi interés por Visual Studio Code — VSCode en adelante — fue utilizar los temas oscuros que tiene, motivos meramente estéticos, pero las funcionalidades que incorpora lo han convertido en mi editor favorito. Hay cosas que todavia no conozco bien, como la creación de tareas, pero sigo intentando aprender a utilizarlas.

Antes de continuar quiero hacer un inciso. Si haces software de escritorio debes intentar que tu aplicación tenga un aspecto actual, y para ello debes conocer las aplicaciones actuales. Si usas el Norton Commander y un editor de hace 20 años, no conoces cual es el aspecto de las aplicaciones actuales, con lo que te parece normal usar botones con efecto 3D y esas cosas que tenía Windows XP. Mi recomendación es que uses un editor moderno y VSCode es uno de los más bonitos.

Al hablar de utilizar VSCode y su uso con Harbour lo primero que hay que decir es que debes instalar la extensión de Antonino Perricone para Harbour — aperricone.harbour en adelante — que tienes disponible en https://marketplace.visualstudio.com/items?itemName=aperricone.harbour. Esta extensión facilita enormemente el uso del editor con Harbour como verás a continuación.

Un aspecto típico de VSCode trabajando con un proyecto Harbour es el siguiente, en el que puedes ver 3 áreas diferenciadas: (1) el explorador de código, (2) la ventana de código, (3) el minimapa y (4) la barra de estado que nos dice que el archivo que estoy editando está usando la extensión de Harbour. El minimapa es algo que llama la atención a primera vista, pero yo no le encuentro apenas uso.

VSCode con la extensión Harbour de Antonino Perricone

Dentro del explorador (1) tenemos la lista de editores abiertos, la carpeta de proyecto que estamos utilizando y la vista de esquema. Esta última la muestra la extensión aperricone.harbour y es una lista de árbol con las funciones o métodos de nuestro archivo .prg y las variables que tengamos definidas en ellas. Este esquema facilita enormemente la navegación en archivos grandes de código.

En la ventana de código es donde editaremos nuestro código y tenemos todas las funcionalidades habituales, pero quiero llamar la atención sobre dos cosas que me parecen muy interesantes: (1) la primera es la posibilidad de dividir la zona de edición verticalmente para mostrar al mismo tiempo diferentes o el mismo archivo fuente, y (2) la ayuda que ofrece aperricone.harbour sobre las funciones de Harbour conforme las vamos escribiendo.

VSCode con la ventana de edición dividida y la ayuda de funciones de Harbour

Una de las cosas que me gusta de VSCode es la posibilidad que tiene para añadir funcionalidades extra, y que hay cantidad de información al respecto. Por ejemplo, yo uso mucho la característica de cambiar una palabra a mayúsculas, y VSCode no trae como nativa la manera de hacerlo. Buscando un poco por internet encontré pronto la manera de hacerlo. Entras en Archivo > Preferencias > Métodos abreviados de teclado y tienes que editar el fichero keybindings.json introduciendo lo siguiente:

Definición de atajos de teclado adicionales en VSCode

Para compilar y ejecutar tu aplicación tienes que definir tareas. Actualmente utilizo sólo dos tareas, una para compilar la aplicación y otra para ejecutarla. La definición de las tareas es la que muestro a continuación:

Definición de tareas de compilación y ejecución en VSCode

Con esto tengo suficiente para compilar y ejecutar mi aplicación, pero creo que es un uso realmente mínimo de lo que se puede hacer. Se que con la extensión aperricone.harbour se puede depurar la aplicación desde dentro del editor, pero no sé la manera de hacerlo. Espero que Antonino encuentre tiempo para hacer un tutorial al respecto.

Además de la extensión aperricone.harbour utilizo las siguientes extensiones para VSCode:

Espero que tras este artículo te animes a usar VSCode con Harbour, y si ya lo usas y quieres compartir algún truco al respecto espero que lo hagas en los comentarios.

Aspectos a tener en cuenta en el despliegue de una aplicación xbase

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

El despliegue es la actividad consistente en instalar una aplicación en el ordenador de nuestro usuario. Una vez que hemos probado nuestro programa y consideramos que está listo para que nuestro usuario lo utilice, debemos proceder a instalarlo en su PC. En este momento se plantean distintas maneras de realizar el despliegue, que varían en función del tipo de software que realicemos. Si quieres leer más sobre el tema te recomiendo la entrada Cincomundos de JoelonSoftware — https://www.joelonsoftware.com/2002/05/06/five-worlds/.

En mi caso, desarrollo software ‘empaquetado’, es decir aplicaciones que el usuario instala o actualiza por si mismo, y no tengo manera de acceder al PC del usuario, por lo que el despliegue toma una gran importancia. No me puedo permitir que un usuario actualice un programa y se produzca un error, por lo que tengo que realizar una serie de acciones dentro del programa que garanticen que la actualización se realiza de manera correcta. Esto no forma parte del despliegue propiamente dicho, pero son aspectos muy importantes cuando necesitas que tu programa no de un error tras una instalación.

1.- Comprobar que existen los ficheros de la aplicación.

El primer aspecto a considerar es que existan todos los ficheros de datos de la aplicación en la ruta predeterminada del programa. Por ello al arrancar nuestro programa debemos hacer esta comprobación, y en caso de que falte algún fichero proceder a crearlo. Una manera sencilla es la que muestro a continuación:

METHOD CheckFiles() CLASS tApplication
LOCAL i := 0
LOCAL nLen := 0
LOCAL aFiles := { "familias.dbf", "familias.cdx", ;
? "productos.dbf", "productos.cdx", ;
? "tickets.dbf", "tickets.cdx", ;
? "lineas.dbf", "lineas.cdx" }
// compruebo que están los ficheros
nLen := Len( aFiles )
FOR i := 1 TO nLen
? IF !File( ::cDbfPath + aFiles[ i ] )
? Ut_Actualizar()
? Ut_Indexar()
? EXIT
? ENDIF
NEXT

Más adelante explicaré que hacen las funciones Ut_Actualizar() y Ut_Indexar(), pero básicamente comprueban que los ficheros tienen todos los campos y vuelven a generar índices.

2.- Comprobar si ha cambiado la versión del programa.

Conjuntamente con la comprobación del punto anterior hago la comprobación de que haya cambiado la versión del programa. Las versiones de mis programas siempre tienen la siguiente forma: “x.y.z” donde:

  • x indica la versión mayor. Cuando introduzco funcionalidades que supone añadir tablas a una aplicación incremento la versión mayor.
  • y indica la versión menor. Cuando libero una actualización siempre incremento la versión menor, y cuando esta actualización supone añadir o modificar un campo de una tabla incremento la decena de este número, pasando por ejemplo de la 1.1.a a la 1.10.a.
  • z indica una corrección de errores de la versión. No añado funcionalidades, únicamente corrijo errores de la versión menor actual.

Cuando cambia la versión mayor o la decena de la versión menor llamo a Ut_Actualizar() y Ut_Indexar().

3.- Creación de las tablas del programa desde código.

Llegamos a la marte más importante de la entrada: debes tener escrita la definición de tablas de tu aplicación, de manera que esta pueda crearlas o modificar su estructura cada vez que lo necesites. Ten en cuenta que estamos hablando de software empaquetado, en que no es posible que vayas al PC de tu usuario a modificar la estructura de una tabla usando un programa de tipo DBU.

Mi función Ut_Actualizar lo hace de esta manera:

// productos
oSay:SetText( 'Fichero de Productos' )
dbCreate( oApp():cDbfPath + 'pr', { ;
{ 'PrNombre', 'C', 40, 0 }, ; // Nombre del producto
{ 'PrFaNombre', 'C', 40, 0 }, ; // Nombre de la familia
{ 'PrPrecio', 'N', 6, 2 }, ; // Precio de compra
{ 'PrIVA', 'N', 5, 2 } } ) // Precio de venta
CLOSE ALL
use &( oApp():cDbfPath + 'pr' ) new
SELECT pr
IF File( oApp():cDbfPath + 'productos.dbf' )
DELETE file &( oApp():cDbfPath + 'productos.cdx' )
APPEND from &( oApp():cDbfPath + 'productos' )
dbCommitAll()
dbCloseAll()
DELETE file &( oApp():cDbfPath + 'productos.dbf' )
ENDIF
dbCloseAll()
rename &( oApp():cDbfPath + 'pr.dbf' ) to &( oApp():cDbfPath + 'productos.dbf' )

Lo que hago es crear la tabla con el nombre del alias que luego usaré con ella, incorporo los datos de la tabla real que luego borro, y por último renombro el fichero que he creado con el alias en el nombre real de la tabla. Esto para cada una de las tablas de mi aplicación. Cuando tengo que modificar un campo de una tabla lo hago en esta función, de manera que al arrancar de nuevo el programa las tablas se modifican de manera automática.

Una vez creadas las tablas lo que hago es crear los índices sobre las mismas. Como has podido ver, el fichero índice lo he borrado antes de incorporar los datos de la tabla real, por lo que ahora tengo que crearlo.

// productos
dbCloseAll()
IF File( oApp():cDbfPath + 'productos.cdx' )
 DELETE File ( oApp():cDbfPath + 'productos.cdx' )
ENDIF
Db_OpenNoIndex( "productos", "pr" )
oSay:SetText( i18n( "Fichero de productos" ) )
oMeter:SetRange( 0, LastRec() / nPaso / nPaso )
PACK
INDEX ON Upper( prnombre ) TAG pr01 ;
FOR ! Deleted() ;
Eval ( oMeter:SetPos( nMeter++ ), Sysrefresh() ) EVERY nPaso
INDEX ON Upper( prfanombre ) + Upper( prnombre ) TAG pr02 ;
FOR ! Deleted() ;
Eval ( oMeter:SetPos( nMeter++ ), Sysrefresh() ) EVERY nPaso
UtResetMeter( oMeter, @nMeter )

4.- Controlar todos los aspectos relacionados con la modificación de campos de las tablas

Es evidente que si has incluido un nuevo campo en una tabla, este aparecerá en alguno de tus formularios. Pero también debes mostrarlo en tus rejillas de datos previa a la edición de un registro, o incluir la nueva ordenación en la rejilla en que muestras la tabla. Y tener en cuenta tus clases que muestran datos.

En mis programas uso el interfaz que he llamado Interfaz completa de documento único o Full Single Documento Interface https://alanit.com/?s=fsdi, y una de sus funcionalidades es que guardo la configuración de la rejilla de datos de cada mantenimiento. Si añado un nuevo campo, este campo no se muestra en la rejilla, porque cuando guardé la configuración ese campo no existía. Por eso cada vez que añado un campo — modifico +10 la versión menor de mi aplicación — tengo que borrar la configuración almacenada de la rejilla para mostrar todos los campos de la misma.

Si en tu aplicación realizas más cosas relacionadas con el despliegue de tu aplicación, te agradecería que no explicaras en los comentarios.

Entrevista con Teo Fonrouge

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

En esta nueva entrevista Teo Fonrouge nos cuenta sus inicios en la programación, su participación en el proyecto Harbour y nos da su visión acerca del proyecto.

Hola, mi nombre es Teodoro Fonrouge Orozco, nací en la Ciudad de México, aunque por azares del destino en mi registro de nacimiento dice que vi la primera luz el 26 de octubre de 1966 en la ciudad de Acayucan en el estado de Veracruz, y en realidad siempre he preferido pensar que mi tierra es Veracruz … 🙂

Actualmente vivo en un poblado en la periferia de la ciudad de México, donde creo que se vive en forma más tranquila que en la vertiginosa y apretada ciudad, de mis hijos; tengo 4 chicas y 3 chicos…

Mi relación con las computadoras se remonta a 1985 cuando apenas iba a cumplir 19. En una ocasión, acompañé a mis padres a una tienda departamental grande, de esas que te venden desde un kilo de verduras, hasta lo último en tecnología… y en efecto, lo que ví en ese momento me causó una gran impresión: una computadora Commodore 16, fué amor a primera vista … 🙂 , aunque un amor un poco extraño, ya que en ese momento yo no tenía idea para que me podía servir una computadora, si yo no tenía intenciones de enviar un vehículo tripulado a Marte ! … ya que en ese entonces, eso era lo que yo pensaba que era lo único para lo que servían las computadoras …. jajaja

En la escuela obtuve mi mejores calificaciones en matemáticas, y algo me decía que quizá, con un poco de suerte, podría hacer que una computadora sirviera como apoyo a estudiantes para el aprendizaje de cálculo, álgebra, o al menos para enseñar sumas y restas, o quizá para mantener una conversación con ella … 😀

Así que le rogué a mi padre que me comprara esa cosa para estudiarla detenidamente en casa, lo cual logré eventualmente después de estar todo un mes haciendo méritos para que él así lo hiciera … Ahora me siento inmensamente tonto, pero lo primero que hice al tener la computadora en casa, fue escribir una pregunta en ella: ‘cuando descubrio Cristobal Colon America ?” y darle <return>, y para mi asombro luego leer su respuesta:

? SYNTAX ERROR

READY.

bueno, a partir de ahi, de algún modo esa es la respuesta que mas frecuentemente he obtenido de todas las computadoras con las que me he topado … jajajaja

Al cabo de varias semanas, logré escribir un pequeño programa que hacía figuras geométricas en la pantalla y las enviaba a una impresora ENTEIA (compatible con Epson) con una mezcla de código en Basic y lenguaje ensamblador 6502, al cabo sólo tenía 16kb de memoria RAM disponible que tenía que compartir para el programa y los datos de los dibujos, que básicamente se guardaban en un segmento de memoria que representaba la imagen gráfica en el monitor, bueno en la televisión de mi madre en ese entonces … Con ese programa me presente en las oficinas de la empresa que en ese entonces comercializaba las computadoras Commodore, tuve suerte de que me atendiera el director y le mostré mi creación … me dijo “cuanto quieres ?” y le dije “quiero trabajar aqui…” ese fué mi primer trabajo formal, un par de años después dejé la compañía y puse mi oficina como consultor independiente.

Mi primer contacto con el lenguaje Clipper fue con una de las compañías a las que yo daba consultoría, tenían un pequeño programa escrito en Clipper para controlar inventarios, el cual requería algunos cambios y extensiones, ya que el programador a cargo abandonó el proyecto, mi primer contacto con Harbour fué por 2004, y en realidad fue con xHarbour su pícaro hijo :-), al principio pagué por utilizar el servicio de xharbour.com y poco después empecé a utilizar Harbour, y ya no ví razón para pagar el servicio.

Desde el momento en que empecé a utilizar xHarbour, y después Harbour, me involucré a poco con la comunidad de usuarios, haciendo preguntas y en ocasiones, proporcionando respuestas.

Decidí que mi mejor forma de aportar a la comunidad Harbour era por medio de algún proyecto que la comunidad pudiese aprovechar, así que en 2006 empecé a escribir wxHarbour (https://sourceforge.net/projects/wxharbour/), un port simple de http://wxwidgets.org un GUI multi — plataforma para Harbour.

También hice una exploración para utilizar Qt como plataforma GUI para Harbour (qtHarbour), pero pronto me dí cuenta de que más bien debía orientar esfuerzos para simplificar la lógica de detrás de las estructuras de datos de mis aplicaciones, además de que Qt no es para nada liviano.

En 2012 inicié el proyecto OORDB en sourceforge.net, después lo moví a GitHub ( https://github.com/tfonrouge/oordb ), con la finalidad de manejar tablas de bases de datos relacionales en una forma orientada a objetos, ya que pienso que esto puede simplificar enormemente al diseño, desarrollo y mantenimiento de estructuras complejas de datos, actualmente utilizo este esquema en un proyecto extenso de planeación y control de Producción para compañías manufactureras, esta aplicación esta corriendo actualmente y contiene estructuras complejas de datos como asociación de tablas de datos con estructuras de listas de componentes, operaciones de manufactura, centros y áreas de trabajo, emisión de ordenes de producción/ordenes de trabajo a partir de una programación de entregas de pedidos a clientes, adicionalmente mantiene información de monitoreo industrial de la maquinaria en tiempo real, utilizando Arduino’s conectados a estaciones corriendo con hardware Raspberry Pi, la información se puede consultar en tiempo real por internet utilizando un web service:

sin lugar a dudas, OORDB me ha permitido simplificar la programación de este proyecto de una manera estructurada, manejando clases que representan documentos (una orden de trabajo, un elemento de inventario, etc) que permiten crear subclases para especializar un tipo especifico de documento, adicionalmente, permite que se mantenga la descripción de campos, descripción de índices, y descriptor de relaciones master <-> child entre las tablas.

Así que ahora no necesito un GUI multi — plataforma para la interface de mis aplicaciones, para eso utilizo simple java script (AngularJS) haciendo peticiones a un servidor de datos utilizando un modelo RESTful donde se intercambian documentos JSON entre cliente y servidor.

Desde hace poco mas de dos años, he estado haciendo preparativos para migrar mis bases de datos xBase a un contenedor mas robusto, y con más prestaciones, por lo que elegí MongoDb, así que empecé a escribir un port del MongoDb C Driver para Harbour (https://github.com/tfonrouge/hbmongoc) y he estado integrando el driver a mi OORDB.

Actualmente estoy en proceso de migrar mis estructuras de datos de xBase a MongoDb utilizando OORDB con muy buenos resultados, y migrar el entorno de desarrollo de esta aplicación de manufactura de Harbour a Java. Para ello ya he creado en Java una aplicación integrada al excelente IDE IntelliJ (https://www.jetbrains.com/idea/) que me permite generar automáticamente el código de los descriptores de tablas, indices, relaciones para replicar mis estructuras y lógica de datos de Harbour OORDB a Java, es una version de OORDB que estoy escribiendo para Java.

Mi propósito es mover el desarrollo de esta aplicación de manufactura a Java / MongoDb.

Harbour es un excelente lenguaje de programación, sin embargo tiene algunos inconvenientes que son críticos en mi estructura de negocios, creo que el principal inconveniente es la falta de core-developers (creo que se cuentan con los dedos de las manos) para resolver problemas, bugs en el compilador e implementar mejoras. Y a veces uno tiene que invertir tiempo para resolver algunos de los problemas, cuando lo que se requiere es enfocar en el desarrollo de la aplicación.

Un IDE como IntelliJ no estaría de más 🙂

Una más extensa y unida comunidad no estaría de más, tenemos Harbour y xHarbour…

Conozco, y he programado en muchos otros lenguajes de programación para mis clientes a lo largo de mi actividad laboral, pero últimamente he estado haciendo centralizando mas de mis proyectos en Java / JavaScript.

Aunque creo que me falta tiempo para desarrollar algunas otras áreas, como por ejemplo Sistemas Expertos con IA …

En fin… en algún momento estoy seguro que sucederá.

Un abrazo a toda la comunidad Harbour/xHarbour

Modelo Vista Controlador en Harbour

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

Este es un artículo de Manu Expósito para Harbour Magazine. Manu es un conocido experto en bases de datos y ha desarrollado Harbour Data Objects, una jerarquía de clases para acceder a BB.DD. relacionales con Harbour. Puedes contactar con él en hdoexpoARROBAgmailPUNTOcom

Con este artículo pretendo explicar de una manera muy práctica como programar usando el patrón de diseño MVC en xBase.

Además me gustaría que fuera el punto de inicio para que mediante las reseñas que hagáis, mejorar el propio articulo y el ejemplo que ilustra la explicación.

¡¡¡Sin más dilación empecemos!!!

¿Sabes qué es un patrón de diseño en programación?

Durante muchos años todos los programadores hemos afrontado unos problema que eran comunes y que cada uno resolvía de una manera diferente. Si se comparaban, realmente había muchas similitudes. Alguien se dedicó a estudiar esos retos y sin codificar en un lenguaje concreto hizo unas directrices por escrito donde se indicaba como solucionar el problema. Por lo tanto un patrón de diseño es una técnica definida para afrontar un problema informático. A partir de aquí dejo a cada uno que indague más.

¿En qué consiste el patrón de diseño Modelo Vista Controlador (MVC)?

Es una manera de resolver un problema en la programación que consiste en dividir el problema en tres capas: el modelo, la vista y el controlador. Cada una de ellas está especializada en la solución de una parte del problema. Además se puede usar con otros patrones de diseño.

Sobre todo se seuele utilizar en el desarrollo para internet pero que podemos aprovechar para otros tipos de programación.

¿Qué es el modelo?

Realmente en Wikipedia están muy bien explicados los componentes de este patrón de diseño, pero voy a intentar mostrar qué es de una manera muy práctica.

El modelo representa la información que va a tratar nuestro programa.

Tiene que ser autosuficiente, quiero decir que no pude depender de nada externo. Para obtener la información que lo conforma tiene que acceder a mecanismos internos que podrían ser el uso de otros patrones, por ejemplo usar un servicio DAO para acceder a la información guardada en una base de datos. Pero también podría ser obtenida desde un XML o un JSon o una cadena etc…

El modelo no tiene porqué conocer la existencia de los otros componentes, ni del controlador ni de la vista.

De hecho debería estar completamente desacoplado del resto.

Como regla general se suelen crear clases que representan el modelo.

Podríamos crear una clase abstracta y de ella heredaremos las características generales para crear una clase especializada de la cual instaciaremos los objetos modelo que se necesiten.

El ejemplo va a consistir en un conversor de monedas.

Esta sería la clase abstracta que propongo para la conversión de cualquier moneda:

//----------------------------------------------------------------//
?
#include "HbClass.ch"
?
//------------------------------------------------------------------
// Clase para convertir cualquier moneda a otra
?
CREATE CLASS TConversorModel
?
PROTECTED:
DATA cambio
DATA resultado
?
EXPORTED:
METHOD new( valorCambio ) CONSTRUCTOR
METHOD getResultado()
// SET GET
METHOD setCambio( cambio )
METHOD getCambio()

PROTECTED:
METHOD convMul( cantidad )
METHOD convDiv( cantidad )
?
END CLASS
?
//------------------------------------------------------------------
// Constructor
?
METHOD new( valorCambio ) CLASS TConversorModel
?
if ValType( valorCambio ) == "N"
::cambio := valorCambio
end if
?
return Self
?
//------------------------------------------------------------------
// Pasa euros a la moneda
?
PROCEDURE convMul( cantidad ) CLASS TConversorModel
?
::resultado := if( ValType( cantidad ) == "N", cantidad * ::cambio, 0 )

return
?
//------------------------------------------------------------------
// Pasa las monedas a euros
?
PROCEDURE convDiv( cantidad ) CLASS TConversorModel
?
::resultado := if( ValType( cantidad ) == "N", cantidad / ::cambio, 0 )

return
?
//------------------------------------------------------------------
// Asigna el cambio
?
METHOD setCambio( cambio ) CLASS TConversorModel
?
local ret := ::cambio
?
if ValType( cambio ) == "N"
::cambio := cambio
end if
?
return ret
?
//------------------------------------------------------------------
// Obtiene el cambio
?
METHOD getCambio() CLASS TConversorModel
return ::cambio
?
//------------------------------------------------------------------
?
METHOD getResultado() CLASS TConversorModel
return ::resultado
?
//------------------------------------------------------------------
?

Y esta otra sería la clase especializada para la conversión de euros y pesetas y que hereda de la anterior:

//----------------------------------------------------------------//
?
#include "HbClass.ch"
?
//------------------------------------------------------------------
// Clase para convertir pesetas a Euros
?
CREATE CLASS TConversorModelEurosPesetas FROM TConversorModel
?
METHOD new() CONSTRUCTOR
METHOD deEurosAPesetas()
METHOD dePesetasAEuros()
?
END CLASS
?
//------------------------------------------------------------------
// Constructor
?
METHOD new() CLASS TConversorModelEurosPesetas
?
::setCambio( 166.386 )
?
return Self
?
//------------------------------------------------------------------
// Convierte un importe a ptas
?
METHOD deEurosAPesetas( cantidad ) CLASS TConversorModelEurosPesetas
?
::convMul( cantidad )

return ::getResultado()
?
//------------------------------------------------------------------
// Convierte un importe a euros
?
METHOD dePesetasAEuros( cantidad ) CLASS TConversorModelEurosPesetas
?
::convDiv( cantidad )

return ::getResultado()
?
//------------------------------------------------------------------
?

¿Qué es la vista?

Hemos dicho que el modelo representa la información. Eso en sí no vale de nada si no se puede tratar o ver. Para eso está la vista.

La vista puede ser la manera de visualizar uno más modelos por pantalla. Aunque también podría ser un informe que se imprime o un PDF o cualquier mecanismo en el que se representaciónde datos.

Aquí podríamos usar clases o no.

Por ejemplo, en la programación para internet, suelen ser las páginas web y no suelen ser objetos de una clase sino HTML.

Pero también podrían ser clases que contengan por ejemplo una cabecera y un pie.

Aquí van a ser también clases. Una abstracta y otra especializada:

La clase abstracta:

//----------------------------------------------------------------//
?
#include "HbClass.ch"
?
//------------------------------------------------------------------
// Clase para Abstracta para la creacion de vistas.
// En las clases derivadas hay que implementar al menos estos metodos VIRTUALES
//------------------------------------------------------------------
?
CREATE CLASS TConversorVista
?
PROTECTED:
DATA tipoConversion // "Pesetas a Euros" -> 1 "Euros a Pesetas" -> 2
?
EXPORTED:
METHOD new() CONSTRUCTOR
// Se implementan en cada vista
METHOD msg( cTxt, cTitulo ) VIRTUAL
METHOD muestraMenu() VIRTUAL
METHOD getCantidad() VIRTUAL
METHOD escribeCambio( s ) VIRTUAL
METHOD acercaDe() VIRTUAL
METHOD muestraFin() VIRTUAL
?
// SET GET
METHOD setTipoConversion( cTipo )
METHOD getTipoConversion()
?
END CLASS
?
//------------------------------------------------------------------
// Constructor
?
METHOD new() CLASS TConversorVista
return self
?
//------------------------------------------------------------------
// Asigna el tipo Conversion
?
METHOD setTipoConversion( cTipo ) CLASS TConversorVista
?
local ret := ::tipoConversion
?
if ValType( cTipo ) == 'C' .and. ( cTipo == '1' .or. cTipo == '2' )
::tipoConversion := cTipo
end if
?
return ret
?
//----------------------------_-------------------------------------
// Obtiene el tipo Conversion
?
METHOD getTipoConversion() CLASS TConversorVista
return ::tipoConversion
?
//------------------------------------------------------------------
?

Y esta es la clase desde la que vamos a instanciar nuestro objeto vista en formato texto. Como reto, propongo que hagais una clase para un GUI, por ejemplo FWH.

//----------------------------------------------------------------//
?
#include "HbClass.ch"
?
//------------------------------------------------------------------
// Definicion de la clase VISTA tipo texto
?
CREATE CLASS TVistaTXT FROM TConversorVista
?
METHOD msg( cTxt, cTitulo )
METHOD leeOpcion()
METHOD muestraMenu()
METHOD operacionIncorrecta()
// Implementacion de los metodos de la interfaz vista
METHOD escribeCambio()
METHOD getCantidad()
METHOD acercaDe()
METHOD muestraFin()
?
END CLASS
?
//------------------------------------------------------------------
// Saca un mensaje en pantalla
?
METHOD msg( cMsg, cTitulo ) CLASS TVistaTXT
?
if ValType( cTitulo ) != 'C'
cTitulo := "Atencion"
endif
?
cTitulo := ";" + cTitulo + ";;;"
?
if ValType( cMsg ) != 'C'
cMsg := " "
endif
?
return Alert( cTitulo + cMsg )
?
//------------------------------------------------------------------
// Se encarga de sacar por pantalla la informacion
?
PROCEDURE escribeCambio( s ) CLASS TVistaTXT
?
::msg( s, "Resultado" )
?
return
?
//------------------------------------------------------------------
// Acepta el tipo de opcion por pantalla
?
METHOD leeOpcion() CLASS TVistaTXT
?
local cOpcion := " "
local getList := {}
?
@ 10, 10 SAY "Elige opcion:" GET cOpcion
?
READ
?
return AllTrim( cOpcion )
?
//------------------------------------------------------------------
// Acepta el importe que se va a tratar
?
METHOD getCantidad() CLASS TVistaTXT
?
local nCantidad := 0
local getList := {}
?
@ 15, 10 SAY "Importe:" GET nCantidad
?
READ
?
// Limpia pantalla
@ 15, 10 SAY Space( 30 )
?
return nCantidad
?
//------------------------------------------------------------------
// Muestra el menu por pantalla
?
PROCEDURE muestraMenu() CLASS TVistaTXT
?
cls

? " +----------------------------------------+"
? " | Indica operacion que quieres realizar: |"
? " +----------------------------------------+"
?
? " [1] De pesetas a euros"
? " [2] De euros a pesetas"
? " [3] Acerca de..."
? " [0] Salir"
?
return
?
//------------------------------------------------------------------
// Mensaje de error
?
PROCEDURE operacionIncorrecta() CLASS TVistaTXT
?
::msg( "Opcion incorrecta..." )
?
return
?
//------------------------------------------------------------------
// Informacion del sistema
?
PROCEDURE acercaDe() CLASS TVistaTXT
?
::msg( "Ejemplo en modo Texto;del;Patron de Diseño;MVC", "Acerca del ejemplo" )
?
return
?
//------------------------------------------------------------------
// Aqui ira todo lo que se debe hacer al finalizar la ejecucion a nivel de vista
?
PROCEDURE muestraFin() CLASS TVistaTXT
?
cls
?
::msg( "Fin de la ejecucion" )
?
return
?
//------------------------------------------------------------------
?

Como podeis ver en el código de las cuatro clases vistas hasta ahora ninguna conoce la existencia de las demás. El modelo no sabe nada de la vista ni del controlador y la vista no sabe nada del modelo ni del controlador.

¿Qué es el controlador?

Como su nombre indica es el que controla 🙂

El controlador se encarga atender a los eventos y de despachar las peticiones del usuario (en algunos sistemas se llama acciones que son un tipo de modelo especial). Para ello tiene que conocer la existencia de las vistas y de los modelos que se tienen que propagar por nuestro programa.

Realmente es un intermediario entre el usuario, el modelo y la vista.

Puede haber diferentes tipos de controladores. Uno de los más difundidos son:

El controlador frontal que es único en el sentido que admite todas las peticiones y las despacha todas.

Y puede haber controladores especializados que sólo admiten peticiones y eventos para los que está concevido o de un controlador frontal.

Para seguir con el mismo criterio vamos poner primero la clase abstracta:

//------------------------------------------------------------------
?
#include "HbClass.ch"
?
//------------------------------------------------------------------
// Control 1/1
?
CREATE CLASS TConversorController
?
PROTECTED:
DATA vista
DATA modelo
?
EXPORTED:
METHOD new( vista, modelo ) CONSTRUCTOR
METHOD gestionDeTipoConversion( cTipo ) VIRTUAL
METHOD despachaAcciones()
METHOD fin()
// SET GET
METHOD getVista()
METHOD setVista( vista )
METHOD getModelo()
METHOD setModelo( modelo )
?
END CLASS
?
//------------------------------------------------------------------
// Constructor
?
METHOD new( vista, modelo ) CLASS TConversorController
?
::vista := vista
::modelo := modelo
?
return self
?
//------------------------------------------------------------------
// Gestiona las peticiones
?
PROCEDURE despachaAcciones() CLASS TConversorController
?
local cTipo
?
while .t.
?
switch cTipo := ::vista:leeOpcion()

case '0'
::vista:muestraFin()
::fin()
exit

case '1'
case '2'
::vista:setTipoConversion( cTipo )
::gestionDeTipoConversion()
exit

case '3'
::vista:acercaDe()
exit

otherwise
::vista:operacionIncorrecta()

end switch
end
?
return
?
//------------------------------------------------------------------
// Se ejecuta al final
?
PROCEDURE fin() CLASS TConversorController
?
// Se haria todo lo del final
break
?
return
?
//------------------------------------------------------------------
// Obtiene la vista
?
METHOD getVista() CLASS TConversorController
return ::vista
?
//------------------------------------------------------------------
// Asigna la vista
?
PROCEDURE setVista( vista ) CLASS TConversorController
?
::vista := vista
?
return
?
//------------------------------------------------------------------
// Obtiene el modelo
?
METHOD getModelo() CLASS TConversorController
return ::modelo
?
//------------------------------------------------------------------
// Asigna el modelo
?
PROCEDURE setModelo( modelo ) CLASS TConversorController
?
::modelo := modelo
?
return
?
//------------------------------------------------------------------
?
?

Y ahora nuestro controlador especializado para la conversión entre pesetas y euros:

//----------------------------------------------------------------//
/*
El CONTROLADOR
Desde aqui se reciben las peticiones del usuario y se trasladan al MODELO
*/
?
#include "HbClass.ch"
?
//------------------------------------------------------------------
// Controlador
?
CREATE CLASS TConversorEurosPesetasController FROM TConversorController
?
METHOD gestionDeTipoConversion( cTipo )
?
END CLASS
?
//------------------------------------------------------------------
// Control de conversiones
?
PROCEDURE gestionDeTipoConversion() CLASS TConversorEurosPesetasController
?
local cantidad := ::vista:getCantidad()
?
switch ::vista:getTipoConversion()

case '1'
::vista:escribeCambio( hb_ntos( cantidad ) + " pesetas son: " + ;
hb_ntos( ::modelo:dePesetasAEuros( cantidad ) ) + " euros" )
?
exit
?
case '2'
::vista:escribeCambio( hb_ntos( cantidad ) + " euros son: " + ;
hb_ntos( ::modelo:deEurosAPesetas( cantidad ) ) + " pesetas" )
?
exit
?
otherwise
::vista:msg( "---< Se ha producido un ERROR >---" )

end switch
?
return
?
//------------------------------------------------------------------
?

Si observais el código vereis que ni en el modelo ni en el cotrolador hay salidas a pantalla de eso se encarga la clase especializada TVistaTXT, me encantaría que halguien hiciera una clase TVistaFWH o cualquier otra clase para otro IDE. Ahí queda el reto 😉

Ahora necesitamos un punto de entrada que será nuestro programa.

Si lo diseñais bien tampoco tiene que estar acoplado al GUI que se use:

//------------------------------------------------------------------
?
#include "hbclass.ch"
?
//------------------------------------------------------------------
// Programa principal de la prueba
?
PROCEDURE main()
?
local oAp := TAplicacion():new()

oAp:ejecuta()
?
return
?
//------------------------------------------------------------------
// Clase principal para el ejemplo de pruebas etc
?
CLASS TAplicacion
?
DATA controlador
?
METHOD new() CONSTRUCTOR
METHOD ejecuta()
?
END CLASS
?
//------------------------------------------------------------------
// Constructor
?
METHOD new() CLASS TAplicacion
?
local oVista := TVistaTXT():new()
local oModelo := TConversorModelEurosPesetas():new()

::controlador := TConversorEurosPesetasController():new( oVista, oModelo )
?
return self
?
//------------------------------------------------------------------
// Metodo que pone en marcha el sistema
?
PROCEDURE ejecuta() CLASS TAplicacion
?
::controlador:getVista():muestraMenu()
::controlador:despachaAcciones()
?
return
?
//------------------------------------------------------------------
?

Espero que con este pequeño articulo se cree un debate que promocione a esta gran herramienta que es Harbour Magazine de nuestro gran amigo José Luis, a Harbour y al uso de patrones de diseño en nuestros programas. Si es así podríamos hacer otros artículos con otros patrones de diseño como por ejemplo DAO, Facade, Adapter, Singlenton o Decorator… por poner algunos interesantes.

Espero vuestras reseñas…

Saludos.

Manu Expósito

PD: Todo el código del artículo está disponible en: https://github.com/JoseluisSanchez/MVC_Harbour

Como ser mejor programador Harbour

En el presente artículo voy a exponer mi opinión personal sobre la manera de ser mejor programador Harbour. Repito que es mi opinión personal, basada en mi propia experiencia, y en la observación de la trayectoria de excelentes programadores Harbour que conozco. Para realizar cualquier aportación por favor hazlo en los comentarios de esta artículo. Los enlaces que nombro son los que uso habitualmente, si conoces otros que sean similares, por favor ponlo en los comentarios del artículo.

0.- Aprende Harbour profundamente y colabora con la comunidad.

Aprende las funcionalidades de Harbour que son distintas de Clipper. Descargate la versión completa de Harbour desde el repositorio oficial y revisa la documentación y los ejemplos. Algunas de las distribuciones de Harbour usadas por los GUI no están completas, pues pueden faltar carpetas como contrib, examples, tests,… por eso debes tener siempre una versión completa de Harbour en tu PC. Aprende a construir el compilador y las librerías, y revisa los ejemplos. Siempre recuerdo las palabras de Rafa Carmona: ‘Pensamos en Harbour como un clon de Clipper, pero va mucho más allá’. Cosas como los tipos extendidos de datos o los hash array no existían en Clipper y debes conocrlos y aprender a utilizarlos.

Ten a mano los enlaces de la documentación oficial que está en https://harbour.github.io/doc/harbour.html, y también la excelente lista de funciones de harbour que Petewg tiene alojada en Github. Sigue los grupos Harbour Users y Harbour Developers para enterarte de las novedades y problemas existentes. Revisa la página de proyectos Harbour en GitHub — https://github.com/rjopek/harbour-list — mantenida por Rafal Jopek y, por supuesto, suscríbete a Harbour Magazine.

Participa en los foros del GUI/entorno que utilices, y se amable con el resto de usuarios. Si planteas una cuestión y las respuestas del resto de usuarios te encaminan a resolverla no contestes con un escueto Resuelto, sino explica la manera en que lo has resuelto y publica tu código.

1.- Aprende otro lenguaje de programación.

Te aconsejo que aprendas otro lenguaje de programación, y te sugiero que aprendas un lenguaje que te permita hacer cosas diferentes a Harbour. Para justificar esto te voy a contar mi experiencia.

Hace unos años comencé a aprender Python, en parte por la influencia de un amigo que es pythonista convencido. Me hablaba de las bondades del lenguaje, así que decidí aprenderlo. Hice un curso online superinteresante, compré varios libros, le dediqué bastante tiempo. Python es un lenguaje increible, sin embargo nunca lo utilicé. ¿ Por qué ? Porque con Python puedo hacer lo mismo que con Harbour, y Harbour es mi lenguaje preferido.

Mi buen amigo Manuel Calero tomó un camino diferente. Aprendió Java y PHP con Laravel, y los usó para realizar proyectos para los que no podía usar Harbour. El resultado es que Manuel ha incorporado los conceptos aprendidos en esos lenguajes a sus programas, tal como nos enseñó en su conferencia en Novelda.

Debes aprender otro lenguaje para aprender conceptos que no son ‘nativos’ a los programadores Harbour, muchos de los cuales venimos de Clipper. Conceptos como programar una aplicación totalmente orientada a objetos, MVC, ORM, patrones,… no son fáciles de asimilar desde Harbour si antes no los has utilizado en otros lenguajes. Una vez domines estos conceptos en otros lenguajes, te será más fácil implementarlos en Harbour

2.- Aprende a usar sistemas de control de versiones.

Los sistemas de control de versiones son imprescindibles en la programación actual. Te permiten mantener varias versiones de tu código, marcar versiones estables, y volver atrás en caso de que sea necesario. Puedes alojar tu código de manera gratuita en Github https://github.com si es opensource, en Bitbucket cualquiera que sea tu licencia, o usar sistemas de control de versiones en tu PC.

3.- Aprende a usar SQL.

SQL te va a permitir realizar aplicaciones cliente/servidor y te familiariza con el uso de bases de datos relacionales, totalmente diferente del sistema de archivos invertidos con índices que usamos en los DBF de Harbour. Además, conocer SQL te facilitará el aprendizaje de otros lenguajes de programación.

Harbour cuenta con soporte para SqLite, MySql, Postgres, ADO,… y existen varios proyectos adicionales de librerías de acceso a motores SQL como TDolphin, HDO, ADORDD,… También puedes acceder a MongoDB con el driver de harbour para MongoDb que Teo Fonrouge tiene disponible en https://github.com/tfonrouge/hbmongoc

4.- Aprende conceptos de diseño gráfico / diseño de interfaces de usuario.

Tanto si haces software de escritorio como aplicaciones web debes conocer los principios de diseño de interfaces. Las guias de diseño de los principales sistemas operativos de escritorio los tienes en estos enlaces:

Puedes hacer prototipos de tus programas rapidamente con programas como Pencil lo que te permitirá mostrar a tus clientes los formularios que vas a realizar y la interacción que habrá entre ellos.

Hay algunos libros clásicos como Presos de la tecnología de Alan Cooper, User interface design for programmers de Joel Spolsky, No me hagas pensar de Steve Krug, Seductive interaction design de Stephen Anderson creo que son lecturas muy beneficiosas para cualquier programador. Lo bueno de estos libros es que los conceptos que explican son conocimiento a largo plazo, los podrás aplicar a cualquier lenguaje que utilice.

Ten en cuenta que si tu programa hace cosas maravillosas pero su interfaz es no está bien trabajado, los usuarios puede que prefieran un programa menos potente pero más sencillo de usar.

Ten tu propia guía de diseño de formularios, y si colaboras en un trabajo en grupo debes plantear la cuestión. Yo he visto cosas espantosas, formularios que se recorrían sin orden, botones en diferentes sitios,… Yo publiqué las mias hace mucho tiempo en https://cincomundos.wordpress.com/2006/07/17/guia-de-diseno-de-formularios

5.- Utiliza software moderno, y por supuesto la última versión de tu sistema operativo.

Debes utilizar la última versión de tu sistema operativo, tanto para probar tus aplicaciones como para ver los nuevos conceptos de diseño de interfaces. Los botones 3D de Windows XP y los gradientes de Windows 7 son historia, así que no los utilices si haces aplicaciones Windows.

Fíjate en el aspecto de las aplicaciones modernas e intenta seguir su estilo. Dale una mirada a editores de código como SublimeText o Visual Studio Code, ambos cuentan con paquetes de soporte de sintaxis de Harbour, y en el caso de Visual Studio Code también cuenta con ayuda de las funciones de Harbour tal como puedes ver en la siguiente imagen.

Visual Studio Code con la extensión Harbour de Antonino Perricone.

El paquete de Antonino Perricone https://marketplace.visualstudio.com/items?itemName=aperricone.harbour es una auténtica maravilla y ha hecho que Visual Studio Code sea mi editor preferido.

Soy un enamorado de los interfaces limpios y minimalistas. Si quieres ver el programa que más me ha impresionado en los últimos meses echa un vistazo a Typora, un editor de ficheros Markdown multiplataforma que es sencillamente maravilloso.

6.- Lee libros que no sean de programación.

Cuando comencé a leer a Joel Spolsky, uno de los temas habituales era la discusión sobre libros que debían leer los programadores. Basicamente eran libros sobre desarrollo de software, pero de vez en cuando aparecían libros de sociología, psicología, emprendimiento, creatividad… cosas que no tienen directamente que ver con la programación, sino que están orbitándo alrededor de ella. Así conocí a autores como Jeson Fried — Remoto, Reinicia, Malcolm Gladwell — Fueras de serie, Inteligencia intuitiva -, Dan Pink — La sorprendente verdad sobre qué nos motiva -, Chris Gillebeau — 100€ Startup — , Angela Duckworth — Grit -, Greg McKeown — Esencialismo -, Austin Kleon — Aprende a promocionar tu trabajo, Roba como un artista — y alguno más. Estos libros se leen sin tener delante el ordenador, lo cual es una ventaja. Sirven para desconectar y al mismo tiempo conocer temas interesantes.

Alguno de los libros mencionados.

Al igual que con los libros de diseño de interfaces, se trata de conocimiento a largo plazo que podrás aplicar tanto en tu trabajo como en tu vida diaria.

7.- Lee noticiarios/agregadores de programación.

Para estar al día de novedades de programación lee noticiarios de programación. Dos de mis preferidos son freeCodeCamp y codeburst. Ahí no podrás encontrar noticias de Harbour, pero si de otros lenguajes y tecnologías. También es un buen termómetro para conocer que lenguajes y entornos son los más utilizados, y posiblemente te ayude a hacer elecciones en el futuro. Si quieres tener sincronizadas tus lecturas de esos noticiarios puedes utilizar feedly.

8.- Realiza proyectos personales y hazlos públicos.

Todos mis programas de alanit son proyectos personales. Siempre he tenido un trabajo distinto a tiempo completo y los programas los hacía en casa por la noche. Alanit significa durante la noche en catalán, mi lengua materna. Comencé haciendo los programas para mi, pero me animé a publicarlos y crear una web alrededor de ellos. Ha sido una de las mayores experiencias de mi vida.

el Puchero, uno de mis proyectos personales.

Además de publicar tus programas crea un blog explicando qué hacen y la manera en que los haces. En mi blog personal, y ahora en Harbour Magazine, he escrito muchos artículos explicando funcionalidades de mis programas, cosas nuevas que iba descubriendo y adaptando, clases nuevas, primeras versiones de esquemas de sintaxis para editores,… incluso gané el concurso de XAAC.

Tener un blog personal de programación es una gran manera de contribuir a la comunidad Harbour.

9.- Colabora con Harbour Magazine.

Cuando abrí Harbour Magazine lo hice con la intención de que fuera un blog colectivo, pero la verdad es que he tenido pocas colaboraciones. Sólo Rafa Carmona y Manuel Calero han escrito articulos para HM, pero sé que hay muchos lectores que tienen los conocimientos suficientes para escribir aquí.

Si eres uno de ellos, ponte en contacto conmigo a través del correo mediumharboutmagARROBAgmailPUNTOcom.

Clean Code en Harbour — I

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

Este es un artículo de Manuel Calero Solís para Harbour Magazine.

Introducción

Principios de ingeniería de software, del libro de Robert C. Martin Código limpio , adaptado para Harbour. Esta no es una guía de estilo, es una guía para producir software legible, reutilizable y refactorizable. No todos los principios de este documento deben seguirse estrictamente, y aún menos serán universalmente acordados. Estas son pautas y nada más, pero están codificadas durante muchos años de experiencia colectiva por los autores de Clean Code.

Inspirado en clean-code-php

Basado en la traducción de Francisco Bórquez

Variables

Use nombres de variables significativos y fáciles de pronunciar.

Mal:

$cdt = DateTime();

Bien:

$currentDateTime = DateTime();

Usa el mismo vocabulario para el mismo tipo de variable

Mal:

getUserInfo()
getUserData()
getUserRecord()
getUserProfile()

Bien:

getUser()

Use los nombres para las búsqueda (parte 1)

Es importante que el código que escribimos sea legible y pueda buscarse. Al no nombrar variables que terminan siendo significativas para comprender nuestro programa, molestamos a los futuros lectores de nuestro código. Hagamos que los nombres sean faciles de buscar y entender.

Mal:

// Que significa realmente el dos
fileHandle = fOpen( "fopen.prg", 2)

Bien:

fileHandle = fOpen( "fopen.prg", FO_READWRITE)

Use los nombres para las búsqueda (parte 2)

Mal:

// What the heck is 4 for?
if (user:access == 4)
// do ...
end if

Bien:

#define ACCESS_READ = 1
#define ACCESS_CREATE = 2
#define ACCESS_UPDATE = 4
#define ACCESS_DELETE = 8
?
if (user:access == ACCESS_UPDATE)
// do ...
end if

Usar variables explicativas

Mal:

programsDirectory := Directory( "*.prg" )
?
aeval( programsDirectory, {|x| sendFile( x[1], x[2] ) } )

Bien:

programsDirectory := Directory( "*.prg" )
?
aeval( programsDirectory, {|programFile| sendFile( programFile[F_NAME], programFile[F_SIZE] ) } )

Evite anidar demasiado y regrese pronto (parte 1)

Demasiadas sentencias ‘if’ pueden hacer que su código sea difícil de seguir. Hay autores que llegan a decir que si tu funcion tiene muchos else (incluso solo uno), debe ser reescrita.

Explícito es mejor que implícito.

Mal:

function isShopOpen(day)
?
if !empty(day)
if hb_isstring(day)
day = lower(day)
if (day == 'friday')
return .t.
elseif (day == 'saturday')
return .t.
elseif (day == 'sunday')
return .t.
else
return .f.
end if
else
return .f.
end if
else
return .f.
end if
?
return .f.

Bien:

function isShopOpen(day)

local openingDays := {'friday', 'saturday', 'sunday'}
?
if empty(day)
return .f.
end if
?
return ( ascan( openingDays, lower(day) ) == 0 )

Evite anidar demasiado y regrese pronto (parte 2)

Mal:

function fibonacci( n )
?
if (n < 50)
if (n != 0)
if (n != 1)
return fibonacci(n - 1) + fibonacci(n - 2)
else
return 1
end if
else
return 0
end if
else
return 'Not supported'
end if
?
return n

Bien:

function fibonacci( n )
?
if (n == 0 .or. n == 1)
return n
end if
?
if (n > 50)
return 'Not supported'
end if
?
return fibonacci(n - 1) + fibonacci(n - 2)
?
return n
?

Evitar el mapeo mental

No obligue al lector de su código a traducir lo que significa la variable. Explícito es mejor que implícito.

Mal:

local li
local l := {'Austin', 'New York', 'San Francisco'}
?
for (i := 1 to len( l ))
li = l[i]
doStuff()
doSomeOtherStuff()
// ...
// ...
// ...
// Espera, ¿qué es `li`?
dispatch(li)
next

Bien:

?
local locations := {'Austin', 'New York', 'San Francisco'}
?
foreach location in location
doStuff()
doSomeOtherStuff()
// ...
// ...
// ...
dispatch(location)
next

No agregue contexto innecesario

Si su nombre de clase / objeto le dice algo, no lo repita en su nombre de variable.

Mal:

CLASS Car
?
DATA carMake
DATA carModel
DATA carColor
?
//...
ENDCLASS

Bien:

CLASS Car
?
DATA make
DATA model
DATA color
?
//...
ENDCLASS

Funciones

Parametros de función (lo ideal es 2 o menos)

Limitar la cantidad de parámetros de función es increíblemente importante porque hace que probarla sea más fácil. Tener más de tres conduce a una explosión combinatoriadonde tienes que probar toneladas de casos diferentes con cada argumento por separado.

Cero argumentos es el caso ideal. Uno o dos argumentos están bien, y se deben evitar tres.Algo más que eso debe consolidarse. Por lo general, si tiene más de dosargumentos entonces su función está tratando de hacer demasiado. En los casos en que no lo es, la mayoríadel tiempo, un objeto de nivel superior bastará como argumento.

Mal:

function createMenu(title, body, buttonText, cancellable)
?
// ...
return

Bien:

class MenuConfig
?
DATA title
DATA body
DATA buttonText
DATA cancellable INIT .f.
?
end class
?
config := MenuConfig():New()
config:title := 'Foo'
config:body := 'Bar'
config:buttonText := 'Baz'
config:cancellable := .t.
?
function createMenu( config )
// ...
return

Las funciones deberían hacer una cosa

Esta es, con mucho, la regla más importante en ingeniería de software. Cuando las funciones hacen másnde una cosa, son más difíciles de componer, probar y razonar. Cuando puedes aislaruna función para una sola acción, se pueden refactorizar fácilmente y su código leerá mucholimpio. Si no quita nada más de esta guía que no sea esto, estará adelantede muchos desarrolladores.

Mal:

function emailClients(clients)
?
foreach client in clients
clientRecord = clientModel():find(client)
if clientRecord:isActive()
email(client)
end if
next
return

Bien:

function emailClients(clients)
?
local activeClients := activeClients( clients )

aeval( activeClients, {|client| email( client ) } )
?
return nil
?
function activeClients( clients )
?
local activeClients := {}

aeval( clients, {|client| if( isClientActive( client ), aadd( activeClients, client ), ) } )
?
return ( activeClients )
?
function isClientActive( client )
?
if clientModel():find( client )
return ( clientRecord:isActive() )
end if
?
return ( .f. )

Los nombres de las funciones deberían decir lo que hacen

Mal:

CLASS Email
?
//...
?
METHOD handle()

mail( ::to, ::subject, ::body )

RETURN nil
?
ENDCLASS
?
message := Email(...):New()
// ¿Que es esto?
message:handle()

Bien:

CLASS Email 
?
//...
?
METHOD send()

mail( ::to, ::subject, ::body)
?
RETURN nil
?
ENDCLASS
?
message := Email(...):New()
// Limpio y obvio
message:send()

Las funciones deben tener sólo un nivel de abstracción

Cuando tienes más de un nivel de abstracción usualmente es porque tu función está haciendo demasiado. Separarlas en funciones lleva a la reutilización.

Mal:

function ExecutorStaments( tokenizedStatement )
?
local statement
local statements := hb_atokens( tokenizedStatement, "," )
?
for each statement in statements
if SQLConexion():Parse( statement )
SQLConexion():Execute( statement )
// ...
end if
next
?
return

Bien:

Lo mejor es crear una clase que tenga dentro dependencias a otras clases.

CLASS Tokenizer
?
METHOD new()
?
METHOD tokenizer() inline ( hb_atokens( tokens, "," ) )
?
end CLASS
?
CLASS Conexion
?
data oConexion
?
METHOD new()
?
METHOD Parse( statement ) inline ( ::oConexion:Parse( statement ) )
?
METHOD Execute( statement ) inline ( ::oConexion:Execute( statement ) )
?
ENDCLASS
?
CLASS ExecutorStaments
?
DATA tokenizer
DATA conexion
?
METHOD new()

::tokenizer := Tokenizer():New()
::conexion := Conexion():New()

RETURN ( self )
?
METHOD Execute( statement )

local statement
local statements := ::tokenizer:tokenizer( statement )
?
for each statement in statements
if ::conexion:Parse( statement )
::conexion:Execute( statement )
end if
next
?
RETURN ( self )
?
ENDCLASS

No usar logicos como parámetros de funciones

Los valores logicos le dicen al usuario que la función hace más de una cosa. Las funciones deben hacer sólo una. Divide tus funciones si ellas siguen diferentes caminos basados en un valor booleano.

Mal:

function createFile(name, temp)
?
if (temp) {
fcreate( './temp/' + name)
else
fcreate( name )
end if
?
return

Bien:

function createFile(name, temp)
?
fcreate( './temp/' + name)

return
?
function createTempFile(name)
?
fcreate( './temp/' + name)

return

Evitar efectos secundarios

Una función produce un efecto secundario si hace algo más que tomar un valor y devolver otro. Un efecto secundario puede ser escribir en un archivo, modificar alguna variable global, o accidentalmente darle todo tu dinero a un extraño.

Ahora, ocasionalmente necesitaras los efectos secundarios en un programa. Como los ejemplos anteriores, necesitarás escribir en un archivo. Lo que quieres hacer en esos casos es centralizar donde realizarlos. No tengas muchas funciones y clases que escriban un archivo en particular. Ten un servicio que lo haga. Uno y sólo uno.

El punto principal es evitar trampas comunes como compartir estados entre objetos sin alguna estructura, usar tipos de datos mutables que puedan ser escritos por cualquiera, y no centralizar donde el efecto paralelo ocurre. Si puedes hacerlo, serás más feliz que la mayoría de los demás programadores.

Mal:

// Variable global referenciada por la siguiente función.
// Si tenemos otra función que use el mismo nombre, ahora será un arreglo y podría romperla.
?
memvar name
?
function splitIntoFirstAndLastName()
?
name = hb_atokens( name, ' ' )
?
return nil
?
name = 'Manuel Calero'
?
splitIntoFirstAndLastName()
?
? ( hb_valtoexp( name ) ) // {'Manuel', 'Calero'}

Bien:

function splitIntoFirstAndLastName( name )
?
return hb_atokens( name, ' ' )
?
name := 'Manuel Calero'
newName := splitIntoFirstAndLastName(name)
?
? ( hb_valtoexp( name ) ) // 'Manuel Calero'
? ( hb_valtoexp( newName ) ) // {'Manuel', 'Calero'}

No escribas funciones globales

Llenar de funciones globales es una mala práctica en muchos lenguajes porque puedes chocar con otra librería.

Mal:

function config()
?
return { 'foo' => 'bar' }

Bien:

CLASS Configuration
?
DATA configuration INIT {}
?
METHOD New( configuration )

::configuration := configuration
?
RETURN ( self )
?
METHOD get( key )

if hhaskey( ::configuration, key )
RETURN ( hget( ::configuration, key ) )
end if
?
RETURN ( nil )
?
ENDCLASS

Crea la variable configuration con una instancia de la clase Configuration

configuration := Configuration():new( {'foo' => 'bar'} )

Y ahora puedes usar una instancia de la clase Configuration en tu aplicación.

No usar el patrón Singleton

Singleton es un anti-patrón. Citando a Brian Button:

  1. Son usados generalmente como una instancia global, ¿Por qué eso es malo? Porque escondes las dependencias de tu aplicación en tu código, en lugar de exponerlas. Hacer algo global para evitar pasarlo es una hediondez de código.
  2. Violan el principio de la responsabilidad única]: en virtud del hecho de que ellos controlan su propia creación y ciclo de vida.
  3. Inherentemente causan que el código esté estrechamente acoplado. Esto hace que muchas veces sean difíciles de probar, aunque en Harbour no tenemos bancos de pruebas.
  4. Llevan su estado al ciclo de vida de la aplicación. Otro golpe a las pruebas porque puedes terminar con una situación donde las pruebas necesitan ser ordenadas lo cual es un gran no para las pruebas unitarias. ¿Por qué? Porque cada prueba unitaria debe hacerse independiente de la otra. Misko Hevery ha realizado unas reflexiones interesantes sobre el origen del problema.

Mal:

CLASS DBConnection
?
CLASSDATA instance
?
METHOD New( dsn )
// ...
RETURN ( self )
?
METHOD getInstance() INLINE ( if( empty( ::oInstance ), ::oInstance := ::New(), ), ::oInstance )
?
// ...
ENDCLASS
?
singleton := DBConnection():getInstance()

Bien:

CLASS DBConnection
?
METHOD New( dsn )
// ...
RETURN ( self )
?
ENDCLASS

Crea una instancia de la clase DBConnection y configúrala con DSN.

connection :=  DBConnection():New( dsn )

Y ahora debes usar la instancia de DBConnection en tu aplicación.

Encapsular condicionales

Mal:

if ( article:state == 'published' ) 
// ...
end if

Bien:

if ( article:isPublished() )
// ...
end if

Evitar condicionales negativos

Mal:

function isNodeNotPresent( node )
?
// ...
?
?
if ( !isDOMNodeNotPresent( node ) )
?
// ...
?

Bien:

function isNodePresent( node )
?
// ...
?
if (isNodePresent( node ) )
?
// ...
?

Evitar condicionales

Esta parece ser una tarea imposible. Al escuchar esto por primera vez, la mayoría de la gente dice, “¿cómo se supone que haré algo sin una declaración if?» La respuesta es que la mayoría de las veces puedes usar polimorfismo para lograr el mismo resultado.

La segunda pregunta usualmente es, “bien, eso es genial, ¿pero cómo puedo hacerlo?” La respuesta es un concepto de código limpio que ya hemos aprendido: una función debe hacer sólo una cosa. Cuando tienes clases y funciones que usan declaraciones if, estás diciéndole al usuario que tu función hace más de una cosa. Recuerda, hacer sólo una cosa.

Mal:

CLASS Airplane
?
// ...
?
METHOD getCruisingAltitude()

SWITCH ::type
CASE '777':
RETURN ::getMaxAltitude() - ::getPassengerCount()
CASE 'Air Force One':
RETURN ::getMaxAltitude()
CASE 'Cessna':
RETURN ::getMaxAltitude() - ::getFuelExpenditure()
END
?
RETURN ( 0 )
?
ENDCLASS

Bien:

CLASS Airplane
?
// ...
?
METHOD getCruisingAltitude() VIRTUAL
?
ENDCLASS
?
CLASS Boeing777 FROM Airplane
?
// ...
?
METHOD getCruisingAltitude()

RETURN ::getMaxAltitude() - ::getPassengerCount()

ENDCLASS
?
CLASS AirForceOne implements Airplane

// ...
?
METHOD getCruisingAltitude()

RETURN ::getMaxAltitude()

ENDCLASS
?
CLASS Cessna implements Airplane
?
// ...
?
METHOD getCruisingAltitude()

RETURN ::getMaxAltitude() - ::getFuelExpenditure()

ENDCLASS

Evitar revisión de tipo

Harbour es un lenguaje no tipado, lo que quiere decir que tus funciones pueden tener cualquier tipo de argumento. Algunas veces habrás sentido esta libertad y te habrás tentado a hacer revisión de tipo en tus funciones. Hay muchas maneras de evitar tener que hacerlo.

Mal:

METHOD travelToTexas( vehicle )
?
if vehicle:IsDerivedFrom( 'Bicycle' )
vehicle:pedalTo( Location():New( 'texas' ) )
elseif vehicle:IsDerivedFrom( 'Car' )
vehicle:driveTo( Location():New( 'texas' ) )
end if

RETURN ( nil )

Bien:

METHOD travelToTexas( vehicle )
?
vehicle:travelTo( Location():New( 'texas' ) )

RETURN ( nil )

Quitar código muerto

El código muerto es tan malo como el código duplicado. No hay motivos para mantenerlo en tu código fuente. Si no está siendo llamado, ¡deshazte de él! Siempre estará a salvo en tu versión histórica si aún lo necesitas.

Mal:

CLASS oldRequestModule( url )
?
// ...
?
ENDCLASS
?
CLASS newRequestModule( url )
?
// ...
?
ENDCLASS
?
request := newRequestModule():New( requestUrl )
?
inventoryTracker('apples', request, 'www.inventory-awesome.io')
?

Bien:

CLASS RequestModule( url )
?
    // ...
?
ENDCLASS
?
request := RequestModule():New( requestUrl )
?
inventoryTracker('apples', request, 'www.inventory-awesome.io')

Pon filtros en tus programas

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

En mis programas Windows utilizo un interfaz propio que he llamado FSDI — full simple display interface — del que publiqué varios artículos en mi blog cincomundos y que puede ser descargado junto con el código de Colossus que tengo publicado en GitHub.

Este interfaz FSDI consta basicamente de:

  1. Una barra lateral de acciones, que permite elegir las acciones a realizar con los datos mostrados.
  2. Una rejilla de datos configurable que muestra tipicamente los datos de un fichero DBF.
  3. Una fila de pestañas, que permite elegir la ordenación de los datos de la rejilla.
Interfaz FSDI en Fester, mi programa de gestión de comparsas de moros y cristianos.

El problema de este interfaz es que es demasiado rígido. Normalmente muestran todos los datos del DBF salvo que en un índice se haya incluido una condición. Para añadir flexibilidad a los datos a mostrar podemos incluir opciones de filtrado.

La opción de filtrado despliega un menú con todas las opciones de filtrado y la opción de eliminar el filtro. Normalmente el filtrado lo realizo por todos los campos que son clave ajena de la tabla, así como por algunos valores o rango de valores propios de la tabla como fechas, campos de marcas, etc. Antes de realizar el filtro se permite elegir el valor a filtrar mediante un diálogo auxiliar.

Acción y menú de opciones de filtrado.

En Harbour se puede definir filtros sobre un DBF usando la sentencia dbSetFilter([<bCondition>], [<cCondition>]) y definiendo las condiciones mediante codeblocks. Lo bueno de los filtros es puedes realizar otras acciones sobre las tablas como puede ser cambios de índice o búsquedas, y simplemente estas acciones se realizarán sobre los datos filtrados.

Un problema que podemos tener al definir filtros es que realicemos una acción en bloque sobre la tabla, por lo que deberemos llevar cuidado de quitar el filtro y luego reponerlo. Para guardar el contenido del filtro podemos usar las funciones dbFilter() y para eliminarlo dbClearFilter().

Una acción importante, desde mi punto de vista, cuando creamos un filtro es que el usuario debe tener conocimiento visual de ello, es decir, debemos decirle a nuestro usuario que hay un filtro activado. En mis programas hago esto de dos formas:

  1. Añadiendo el nombre del filtro al nombre del mantenimiento en la barra de opciones.
  2. Cambiando el color de la etiqueta de la opción de definición de filtros a rojo.

Una vez se elimina el filtro, mediante la misma opción de filtrado todos estos elementos visuales desaparecen.

La posibilidad de definición de filtros sobre tablas es una opción existente en Harbour que podemos incluir de manera sencilla en nuestros programas, lo que nos permitirá añadir una gran flexibilidad a los mismos. Es importante que cuando apliquemos filtros lo hagamos de manera que el usuario vea claramente que hay un filtro activo, y no le lleve a confusión.

Noticias de Harbour — diciembre 2017

Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación Harbour.

En los últimos meses tenemos varias novedades sobre Harbour que te presentamos a continuación:

¿ Conoces alguna novedad sobre Harbour, su entorno de programación, artículos sobre el lenguaje, código fuente, etc. que merezcan ser publicados ? Escribeme a [email protected] y lo publicaré en el próximo boletín sobre Harbour.