domingo, enero 26, 2014

Crear un proyecto FoxPro ¿por dónde comenzar?

Por: Fernando D. Bozzo

Aunque esto es algo fácil una vez adquirida cierta experiencia, muchos se pierden y no tiene claro como comenzar u organizar un proyecto.

¿Pero crear un proyecto no es simplemente pulsar el botón new / Project y meterle archivos dentro? No, es más que eso, y a continuación vamos a ver una de las formas comunes de hacerlo de acuerdo a las Buenas Prácticas de programación.



Cosas a tener en cuenta


Hay dos modalidades de proyectos, los que hacen nuevos desde cero y los que se crean para poner un poco de orden dentro de uno o más programas existentes o que se quieren dejar de ejecutar con DO <programa> y se necesita hacer un EXE o APP o DLL.
Da igual que modalidad se use, la estructura del proyecto es muy importante, ya que en parte determina la escalabilidad y principalmente la transportabilidad del mismo, ya que un programa o sistema que solo funciona en una ubicación particular y deja de hacerlo si se cambia de directorio o unidad (C,E,G,etc) generará un grave problema de mantenimiento.



Estructura de directorios para Desarrollar


Esta es una estructura de directorios típica:

\raiz           => Dir.principal del PJX y PRG principal (ej: c:\desa\miapp)
     \datos     => Archivos DBF,FPT,CDX,DBC,DCX,DCT,SQL (y texto DB2/DC2)
     \bmps      => Archivos gráficos JPG,PNG,etc
     \forms     => Archivos SCX/SCX (y texto SCA/SC2)
     \clases    => Archivos VCX/VCT (y texto VCA/VC2)
     \include   => Archivos "include" (.H)
     \menus     => Archivos MNX/MNT
     \config    => Aquí puede ir el CONFIG.FPW 
     \prgs      => Archivos PRG
     \reports   => Archivos FRX/FRT/LBX/LBT (y texto FRA/FR2/LBA/LB2)

La raíz puede estar ubicada en cualquier directorio, y desde la misma se crean una serie de subdirectorios que contendrán los componentes agrupados por tipo. Esto permite ubicarlos más rápidamente y no tenerlos todos juntos de forma desordenada, donde buscar algo se puede complicar bastante. Además esta separación ayuda al mantenimiento separado de los componentes y a evitar ciertos errores de manipulación.

En la raíz suele haber 2 archivos principales: el proyecto (PJX) y el programa principal de arranque (MAIN.PRG, PRINCIPAL.PRG o como se llame)

El directorio datos es el directorio de trabajo del usuario, conteniendo todas las tablas, bases de datos, vistas e índices

Y el resto de directorios indica con su nombre el tipo de componente que contiene



El programa principal


En general conviene que el programa principal del proyecto sea un PRG (por ejemplo, MAIN.PRG) y que esté en el directorio raíz junto al proyecto, ya que es donde sea realiza la inicialización del sistema, donde se establece el directorio por defecto y donde se carga el primer form o menú del sistema.

En el caso de un sistema, este programa debería tener un contenido mínimo y dejar todas las implementaciones y procedimientos a los componentes y librerías, de forma que quede abierta la posibilidad de que, con una buena encapsulación funcional de componentes, se puedan reutilizar objetos en otros sistemas o para ser usados de otra forma, como procesos batch (por lotes).

El contenido mínimo de MAIN.PRG (o como lo llamen) para un sistema de mantenimiento (altas, bajas, modificaciones) con formularios de usuario podría ser así:

*-- MAIN.PRG
*-- Configuración del entorno
#INCLUDE INCLUDE\MAIN.H  && Define el archivo de inclusión principal de variables nombradas
PUBLIC glSalir
glSalir = .F.            && Se debe asignar .T. cuando se quiera salir
ON SHUTDOWN DO CerrarSistema
SET TALK OFF             && No mostrar el resultado de los comandos al ejecutarse
SET SAFETY OFF           && No mostrar avisos de confirmación al sobreescribir datos
SET DATE DMY
SET EXCLUSIVE OFF        && No abrir las tablas en Exclusiva, sino SHARED
SET DELETED ON           && No mostrar registros marcados como eliminados
SET CENTURY ON
SET HOURS TO 24
SET TABLEPROMPT OFF      && No mostrar ventana de selección de tabla si no encuentra el alias
SET PATH TO "clases;forms;menus;datos;prgs;bmps" && Rutas relativas a la raíz
_SCREEN.Caption = NOMBRE_SISTEMA
CD (JUSTPATH(SYS(16)))   && Cambiarse al directorio raíz (donde está MAIN.PRG)
DO MENUPRINCIPAL.MPR
DO FORM entrada_datos.scx && Ejecutar el form principal (si hay) o un menú, o ambos
DO WHILE NOT glSalir
   READ EVENTS           && Poner al sistema en espera de acciones del usuario
ENDDO

*-- Cerrar forms abiertos
FOR I = _SCREEN.FormCount TO 1 STEP -1
    _SCREEN.Forms(I).HIDE()
    _SCREEN.Forms(I).Release()
ENDFOR


RELEASE ALL
CLOSE ALL

IF _VFP.StartMode <= 1
  RETURN                 && En modo Desarrollo u objeto sale con RETURN
ENDIF

QUIT                     && En modo EXE o APP sale con QUIT

PROCEDURE CerrarSistema
   glSalir = .T.
   ON SHUTDOWN
   CLEAR EVENTS
ENDPROC
*-- Fin MAIN.PRG

Ahora vamos a ver las distintas secciones de este programa



El archivo de inclusión MAIN.H


Los archivos de inclusión permiten definir constantes nombradas, esto es, variables de sólo lectura que durante la compilación del programa se reemplazarán en el  sitio donde sean utilizadas.

Suele tener varios usos, por ejemplo para ejecutar un sistema en modo Depuración o en modo Producción se puede usar una constante DEBUGMODE y luego parametrizar código de programa para que dependiendo de su valor habilite o deshabilite ciertas partes. También se puede usar para la localización de la aplicación, que es poner en constantes todos los textos que muestre la aplicación para poder internacionalizar su interfaz y soportar diferentes idiomas.

Es conveniente usar un archivo de inclusión único que permita referenciar a otros archivos de inclusión si fuera necesario, cosa que en los forms, clases, programas y demás componentes siempre se referencie al principal y éste se encargue de referenciar a los demás.
Este sistema de referenciamiento de archivos include dentro del principal da mucha flexibilidad, ya que por un lado evita duplicar las constantes nombradas que ya están definidas en otros archivos, por ejemplo el foxpro.h que viene con FoxPro.

Este es un ejemplo de archivo de inclusión:

*-- MAIN.H
*-- VARIABLES NOMBRADAS Y LINKEO DE OTROS ARCHIVOS INCLUDE

#INCLUDE \include\foxpro.h
#DEFINE NOMBRE_SISTEMA   "GESTIÓN"

#DEFINE DEBUGMODE .T.

Como se observa, este archivo de inclusión referencia a otro archivo de inclusión (aquí foxpro.h, pero podría referenciar a otros más agregando sentencias #include) y además define una constante para el nombre del sistema que usé para definir el CAPTION de _SCREEN en MAIN.PRG



Configuración del PATH de la aplicación


Para que se puedan encontrar los componentes sin tener que referenciarlos con rutas absolutas, es necesario definir el Path, y lo más conveniente es hacerlo con rutas relativas al directorio raíz, tal como puse en MAIN.PRG:

SET PATH TO "clases;forms;menus;datos;prgs;bmps"

Esto indica que desde el directorio actual o raíz, busque los componentes en los subdirectorios "clases", "forms", etc.

La ventaja de definirlo así es que al mover la aplicación a otro directorio, no hará falta hacer ningún cambio en el código, ya que todo permanecerá perfectamente referenciado, sin importar si se mueve incluso a otra unidad de disco o de red, siempre que se mueva la estructura completa de directorios.



El archivo de configuración CONFIG.FPW


El CONFIG.FPW es opcional, y se usa para algunos seteos iniciales que se desea ejecutar antes de cualquier instrucción de programa y de que se muestre la pantalla principal de FoxPro. Habitualmente aquí se configuran cosas como indicar que no se desea usar el archivo de recursos (foxuser.dbf), o si se quiere que el sistema no tenga visible la ventana principal de FoxPro y solo muestre los forms del usuario o el principal, o incluso se puede configurar los directorios de temporales y de trabajo o la cantidad máxima de variables de memoria que se usarán.

Un archivo CONFIG.FPW puede contener esto:

ALLOWEXTERNAL=OFF
RESOURCE=OFF
SCREEN=OFF

Si se quiere forzar a que todos los temporales vayan a un directorio, solo hay que agregar esto (c:\temp se puede reemplazar por cualquier directorio):

EDITWORK=C:\TEMP
PROGWORK=C:\TEMP
SORTWORK=C:\TEMP
TMPFILES=C:\TEMP



Alternativamente también se puede cambiar la ubicación de todos los temporales, pero por usuario, por ejemplo:

EDITWORK=C:\APLICACION\DATOS\TEMP\USUARIO
PROGWORK=C:\APLICACION\DATOS\TEMP\USUARIO
SORTWORK=C:\APLICACION\DATOS\TEMP\USUARIO
TMPFILES=C:\APLICACION\DATOS\TEMP\USUARIO



Pero requiere algo más de trabajo, y se puede hacer con 2 archivos config. Para comenzar, se requiere un CONFIG.FPW mínimo dentro del proyecto, por ejemplo:

ALLOWEXTERNAL=ON
RESOURCE=OFF
SCREEN=OFF


Y el segundo config.fpw se puede hacer usando un programa lanzador (llamémoslo Lanzador.exe) que invoque al EXE principal del sistema (ej: sistema.exe), previa preparación del archivo de configuración.

Por ejemplo, el lanzador.exe debería hacer lo siguiente:

  1. Crear la carpeta para el usuario, según se obtenga de SYS(0)
  2. Crear un CONFIG.FPW personalizado para ese usuario (como el de arriba con EDITWORK/SORTWORK/etc, pero reemplazando USUARIO por el nombre de usuario obtenido del SYS(0)) y escribirlo en su path particular que se creó en (1)
  3. Finalmente el lanzador invoca al EXE principal con la nueva ruta:
    sistema.exe -cc:\aplicacion\datos\temp\usuario

De esta forma se pueden separar los temporales de cada usuario, lo que, cuando hay muchos usuarios, previene los errores de temporales con el mismo nombre.


En la ayuda de FoxPro, buscando por "config.fpw" en el índice, muestra todas las configuraciones.
Aunque casi todos los seteos se pueden hacer por programa (comandos SET), hay algunos que solamente se pueden hacer aquí.

Nota: El motivo de que haya puesto el CONFIG.FPW en el directorio CONFIG y no haya incluido este directorio en el Path, es por dos cosas:

  1. Porque cuando ejecuto el programa con DO MAIN.PRG en modo Desarrollo, no quiero que me oculte la pantalla o desactive el archivo de recursos
  2. Porque lo anterior quiero que lo haga solo cuando lo ejecuto como EXE.


Truco:
Para esto es fundamental incluir el archivo CONFIG.FPW en el proyecto pero no referenciar su directorio en el Path, de esta forma cuando se ejecuta el programa con DO MAIN.PRG no encontrará el CONFIG.FPW porque su directorio no está en el Path, pero cuando se ejecute como EXE, al estar incluido dentro del proyecto, el EXE encontrará todos los componentes que haya dentro del PJX, aunque no se haya configurado un SET PATH (es como un SET PATH implícito).



Capturar el cierre desde la cruz (o aspa) de la ventana


Si se desea capturar este evento para poder cerrar la ventana desde la cruz, como todas las aplicaciones Windows, entonces se puede usar la sentencia ON SHUTDOWN como hice en MAIN.PRG, que normalmente llama a un programa de cierre que configura una variable global para salir y hace un CLEAR EVENTS.

Es importante tener en cuenta que para que esto funcione no deben haber formularios modales abiertos. Solo puede haber abiertos forms normales (no modales).



Configuración del directorio principal o raíz


En el ejemplo está en MAIN.PRG y se encarga de hacer que el directorio por defecto de la aplicación sea el mismo donde está MAIN.PRG, por eso la importancia de que este programa principal esté en el directorio raíz y no dentro de PRGS con el resto de programas.

CD (JUSTPATH(SYS(16)))

La función SYS(16) devuelve el nombre del programa en ejecución con su ruta absoluta. Por ejemplo, si el programa de nuestro ejemplo está en el directorio c:\desa, SYS(16) devolverá C:\DESA\MAIN.PRG, y si fuera un ejecutable y el proyecto se llamara SISTEMA.EXE devolvería C:\DESA\SISTEMA.EXE
Con la función JUSTPATH() obtenemos la ruta del programa y finalmente con CD (Change Directory) nos cambiamos al directorio obtenido.



Ejecución de un form o menú (o ambos)


Antes de entrar al bucle READ EVENTS, se ejecuta el form o menú que se quiera mostrar al usuario. ¿Y por qué un bucle y no solo Read Events? Bueno, esto es opcional y lo ví así en algunos ejemplos de FoxPro. El posible motivo que encuentro para esto es que si se quiere controlar la salida exclusivamente con la variable pública glSalir y se tiene la costumbre de ejecutar CLEAR EVENTS en el cierre de todos los formularios, esta es un buena forma de controlar de que se vuelva a ejecutar el READ EVENTS y no se salga del sistema si no se elije expresamente salir, lo que pondrá glSalir = .T. y hará el CLEAR EVENTS definitivo.
Por qué se pueda querer hacer un CLEAR EVENTS en cada form no lo sé, pero podría haber algún caso especial que lo requiera y este método lo controla.

DO WHILE NOT glSalir     && No salir con Clear Events hasta que glSalir sea .T.
   READ EVENTS           && Poner al sistema en espera de acciones del usuario
ENDDO






Cerrar los forms abiertos


Esto es útil cuando se tiene un sistema MDI (Múltiple Document Interface) con varias ventanas que pueden estar abiertas a la vez y se quiere poder salir y cerrarlas todas sin tener que estar cerrando una a una.

*-- Cerrar forms abiertos
FOR I = _SCREEN.FormCount TO 1 STEP -1
    _SCREEN.Forms(I).HIDE()
    _SCREEN.Forms(I).Release()
ENDFOR


El motivo de que el conteo sea en orden inverso es el mismo motivo de que la eliminación de elementos de un array debe hacerse de la misma forma: Si hay 5 elementos en el array (o 5 forms en el array de forms) y se quisieran borrar en orden normal, al llegar al  4 ya no existe, porque se borraron 3, y 5 - 3 = 2, con lo cuál daría un error, y para evitar esto se hace en orden inverso.



Retorno final o cierre de sesión


Finalmente se debe devolver el control al sistema... o a la ventana de comandos. Veamos: Si estamos depurando y ejecutando con DO MAIN.PRG, no queremos que se nos cierre la sesión de FoxPro, sino que queremos permanecer en la misma para seguir trabajando, hacer pruebas en la ventana de comandos, etc., pero si estamos en el EXE y este se ejecutó con doble click en el explorador de archivos o se lanzó desde otro programa, no hay ventana de comandos a la que volver, y se debe cerrar la sesión para que no quede una "sesión colgada" donde la ventana de FoxPro no se vé pero se sigue ejecutando (se puede ver con el Administrador de Tareas)

¿Cómo distinguir si estamos ejecutando con DO MAIN.PRG o con el ejecutable? esto lo hacemos consultando la propiedad StartMode, así:

_VFP.StartMode

En la ayuda figuran todos sus valores, pero nos sirve saber que 0=Ejecución con DO, 1=Ejecución con CREATEOBJECT y el resto de valores son ejecución como EXE, DLL, etc.




Distribución de ejecutables



En el caso de que se quiera hacer un ejecutable (EXE) o APP para distribuir, y se use algún instalador, típicamente el directorio de instalación del programa será "Program Files" o "Archivos de programa", pero los datos nunca deberán estar allí, ya que desde Windows 7 ese es un directorio especialmente protegido contra escritura.

Los datos deberán ubicarse en otro directorio que no esté dentro de "Program Files" ni otro directorio del Sistema, como Windows o System32. Cualquier directorio es válido. En este caso, puede que se complique un poco indicar que los programas van en un directorio y los datos en otro que no está bajo el, y además se deberá tener en cuenta para la definición de los PATH de datos, que deberán parametrizarse.

Otra opción, que para mí es mejor, es instalar directamente el programa en otro directorio, por ejemplo, c:\app\mi_sistema, para que pueda mantener la misma estructura de directorios que en desarrollo, y así evitar complicaciones.



Finalizando


Hemos visto una de las formas (hay otras) de crear un proyecto, de definir un programa principal, archivos de inclusión y una estructura transportable usando rutas relativas. Lo único que queda es crear el resto de los componentes (forms, clases, reportes, etc) desde la ventana del proyecto, pero eso ya lo dejo a vuestro criterio.


Hasta la próxima!



20 comentarios:

  1. Muy buen artículo. Seguramente evitaremos muchos dolores de cabeza trabajando ordenadamente.

    ResponderEliminar
  2. Excelente explicación, esto es el fruto de muchos años de experiencia. Felicitaciones y a seguir adelante....

    ResponderEliminar
  3. Tengo años de programar en visual foxpro y nunca supe como definir el "set path to" de manera correcta, enviándome errores de archivos que no se localizaban. excelente reseña. nunca es tarde para seguir aprendiendo gracias a "gurus" como tu. que compartes el conocimiento para todos. Un saludo afectuoso.

    ResponderEliminar
  4. A esto sí se le puede llamar explicación amena y desinteresada. Con este código y la explicación clara y concisa, me he ahorrado varios dolores de cabeza. Gracias.

    ResponderEliminar
  5. Me ayudo a emprender en el lenguaje.. buen trabajo !!

    ResponderEliminar
  6. Cuando el comando set step on en FoxPro para windows regresa un fallo que es necesario hacer compilar todos los proyectos en la misma maquina donde se requiere el set step ?? el error que marca es algo de Out of date

    ResponderEliminar
    Respuestas
    1. Hola, no entiendo la pregunta y la relación con el artículo...

      Eliminar
    2. Hola Axel, no, no tengo, pero en la web hay varios. Mira en los foros de VFP que seguro vas a encontrar. Saludos!

      Eliminar
  7. Hola Fernando Que artículo mas interesante. Gracias por él.

    Te molesto. Estoy compilando un proyecto de inventarios que funciona perfecto en desarrollo arrancando desde el programa main. Cuando lo compilo no genera ningún error pero al ejecutar el .exe me dice que no existe una tabla: "no existe alias i_categoria". Reviso los enviroment de los forms y en todos aparece esta tabla. Alguna idea de que puede ser?

    ResponderEliminar
  8. Ya encontré el problema. Si ejecuto el .exe desde la ventana de comandos de VFP, con la instrucción DO seguida del nombre del ejecutable, la aplicación se detiene donde encuentra el error y lo muestra, cosa que no ocurre si lo ejecuto directamente. Gracias. He aprendido algo nuevo.

    ResponderEliminar
  9. Muchas gracias por el artículos, está muy interesante y es muy útil para tenerlo en cuenta. Una consulta: Si deseo abrir el mismo ejecutable mas de una vez (desde el ejecutable abierto), sin afectar las variables de este ejecutable, dicho de otra manera, sin salir de un ejecutable, quiero abrir el mismo ejecutable, pero sin afectar las variables publicas de este, como podria hacerlo. Espero pueda contestar esta consulta. Muchas gracias.

    ResponderEliminar
    Respuestas
    1. Hola,
      Para lanzar un ejecutable desde otro podés hacerlo con WSHell.Run(), como el usado en este artículo:

      https://fdbozzo.blogspot.com/2015/05/tecnicas-de-programacion-en-vfp.html

      Saludos!

      Eliminar
  10. Excelente aporte!!!.
    Me encantaria que alguien pueda pasarme el correo electronico de esta persona que escribio la rutina. O alguien que sepa "muchisimo y de mucha experiencia", para poder consultarle muchas cosas sobre VISUAL FOXPRO. Dejo mi whatsApp +5493487616158. Desde ya, muchisimas gracias.

    ResponderEliminar
  11. *-- MAIN.PRG
    *-- Configuración del entorno
    #INCLUDE INCLUDE\MAIN.H && Define el archivo de inclusión principal de variables nombradas
    PUBLIC glSalir
    glSalir = .F. && Se debe asignar .T. cuando se quiera salir
    ON SHUTDOWN DO CerrarSistema
    SET TALK OFF && No mostrar el resultado de los comandos al ejecutarse
    SET SAFETY OFF && No mostrar avisos de confirmación al sobreescribir datos
    SET DATE DMY
    SET EXCLUSIVE OFF && No abrir las tablas en Exclusiva, sino SHARED
    SET DELETED ON && No mostrar registros marcados como eliminados
    SET CENTURY ON
    SET HOURS TO 24
    SET TABLEPROMPT OFF && No mostrar ventana de selección de tabla si no encuentra el alias
    SET PATH TO "clases;forms;menus;datos;prgs;bmps" && Rutas relativas a la raíz
    _SCREEN.Caption = NOMBRE_SISTEMA
    CD (JUSTPATH(SYS(16))) && Cambiarse al directorio raíz (donde está MAIN.PRG)
    DO MENUPRINCIPAL.MPR
    DO FORM entrada_datos.scx && Ejecutar el form principal (si hay) o un menú, o ambos
    &&* En el formulario poner en propiedades:
    && WindowState=2 y ShowWindow=2 (fuerza a mostrar ventana al ejecutar EXE)
    && en metodo DESTROY poner: DO cerrarsistema

    DO WHILE NOT glSalir
    READ EVENTS && Poner al sistema en espera de acciones del usuario
    ENDDO

    *-- Cerrar forms abiertos
    FOR I = _SCREEN.FormCount TO 1 STEP -1
    _SCREEN.Forms(I).HIDE()
    _SCREEN.Forms(I).Release()
    ENDFOR

    RELEASE ALL

    IF _VFP.StartMode <= 1
    CLOSE DATABASES ALL &&* Poner todo lo que quieras cerrar/liberar excepto CLOSE ALL que te cierra el proyecto
    CLOSE TABLES ALL
    CLOSE DEBUGGER
    CLOSE FORMAT
    CLOSE INDEXES
    CLOSE PROCEDURE
    RETURN && En modo Desarrollo u objeto sale con RETURN
    ENDIF

    CLOSE ALL &&* Aqui solo cierra todo en modo exe
    QUIT && En modo EXE o APP sale con QUIT

    PROCEDURE CerrarSistema
    glSalir = .T.
    ON SHUTDOWN
    CLEAR EVENTS
    ENDPROC
    *-- Fin MAIN.PRG

    ResponderEliminar
  12. He añadido al proyecto original un par de cambios y un poco de informacion:

    Cambiando el CLOSE ALL de lugar y añadiendo otros CLOSE en modo desarrollo, no me cierra el proyecto cada vez que ejecuto o compilo.

    Informacion extra sobre que poner en un formulario estandar como el que mencionas, entrada_datos.scx para que no se oculte el formulario al ejecutar en modo EXE

    Propiedades: WindowState=2 y ShowWindow=2
    Metodo DESTROY poner: DO cerrarsistema

    Pueden haber ventajas de inconvenientes en estos cambios, pero corrige un par de cosas que pueden despistar a los mas noveles.
    un saludo

    ResponderEliminar
  13. Buenas tardes, amigos. Me parece importante incluir este doc como un link de buenas prácticas que están en los sgtes URLs:
    Reglas mínimas y buenas prácticas de programación
    https://fdbozzo.blogspot.com/2014/08/reglas-minimas-y-buenas-practicas-de.html
    VFP: Guía de Buenas Prácticas de Programación y recomendaciones
    https://fdbozzo.blogspot.com/2014/09/vfp-guia-de-buenas-practicas-de.html
    Crear un proyecto FoxPro ¿por dónde comenzar?
    https://fdbozzo.blogspot.com/2014/01/crear-un-proyecto-foxpro-por-donde.html

    ResponderEliminar
  14. Hola, Fernando. Felicitaciones por este documento. Te cuento que yo le haría un pequeño ajuste a tu escenario. Primero ejecutaría CD antes de SET PATH TO, más o menos así:

    CD (JUSTPATH(SYS(16))) && Cambiarse al directorio raíz (donde está MAIN.PRG)
    SET PATH TO "clases;forms;menus;datos;prgs;bmps" && Rutas relativas a la raíz

    por que los dirs CLASES, FORMS, etc están "dentro" del mismo donde está MAIN.PRG .
    Gracias por compartir.

    ResponderEliminar