20 de junio de 2017

Emplear Hiperenlaces en informes

Artículo original: Hyperlinks Your Reports
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


El mes pasado, Doug Hennig abordó la nueva clase ReportListener en VFP 9 y cómo puede ser utilizada para controlar la salida de informes de formas que anteriormente eran imposibles. Este mes, habla sobre cómo agregar hiperenlaces vivos a la salida generada de informes, permitiendo algunas acciones que se pueden realizar cuando se hace clic sobre ellos.

¿No sería bueno poderle decir a VFP que agregue hiperenlaces a un campo en un informe? Entonces, el cliente podría hacer Clic sobre el hiperenlace para navegar a alguna información relacionada. Por ejemplo, un informe que muestre los clientes y sus sitios Web o sus direcciones de correo electrónico podría tener enlaces vivos, haciendo clic a sus sitios Web podría navegar el examinador (browser) a esta URL.

Incluso, mucho más interesante sería tener la posibilidad de navegar a algún otro lugar dentro de su propia aplicación. Por ejemplo, haciendo clic en un nombre de compañía en un informe podría saltar a un formulario de entrada de datos del cliente para el que ha seleccionado la compañía.

Debido a que la vista preliminar del informe que viene con VFP no admite objetos vivos, sobre los que se pueda hacer clic en un informe, la vía más sencilla de implementarlo es usando HTML, que admite hiperenlaces de forma nativa.

Hiperenlazando informes

VFP viene con un report listener que crea HTML (la clase HTMLListener generada en la aplicación ReportOutput.APP e incluida también en _ReportListner.VCX en la carpeta  FFC), pero yo estaba seguro que necesitaríamos mucho trabajo para lograr el resultado con hiperenlaces. Sin embargo, me sorprendí gratamente al descubrir cuán poco esfuerzo hace falta para ello.

Primeramente, una pequeña introducción. HTMLListener es una subclase de XMLDisplayListener, el que a su vez es una subclase de XMLListener y esta es una subclase de _ReportListener, la clase que he descrito el mes pasado y recomiendo su uso normalmente como clase padre para sus propios listeners.

(Nota de la traductora: Este artículo fue publicado en la Revista FoxTalk 2.0 en  Marzo-2005. El autor se refiere a un artículo publicado en el mes de Febrero en la propia revista bajo el título Lintening to a Report, recientemente ha sido también liberado y se encuentra disponible en las páginas de Microsoft enhttp://msdn.microsoft.com/library/en-us/dnfoxtk05/html/ft05b6.asp.

Cuando utiliza HTMLListener, ya sea directamente intanciándolo y utilizándolo como un listener para un informe, o al especificar OBJECT TYPE 5 en el comando REPORT, en realidad genera un XML para el informe (esto lo hace la clase padre), entonces aplica una transformación XSL del XML para crear un HTML. El XSLT que utiliza está definido en el método GetDefaultUserXSLTAsString.

El XSLT predeterminado utilizado por HTMLListener es muy complejo, y no es mucho más que un experto XSL, yo pensé que podría ser una tarea agobiante determinar qué cambios se debían agregar para que admitiera hiperenlaces. Sin embargo, comencé a escarbar en GetDefaultUserXSLTAsString, y he descubierto lo siguiente:

<xsl:when test="string-length(@href) > 0">
<A href="{@href}">
<xsl:call-template name="replaceText"/>
</A>
</xsl:when>

Este XSL agrega una etiqueta al HTML si existe un atributo HREF en el elemento actual en el XML.Esto es genial - significa que el HTMLListener ya admite hyperlinks! Sin embargo, no hay búsquedas por "HREF" en XMLDisplayListener ni XMLListener, entonces, ¿cómo se puede agregar este atributo a un elemento, especialmente de forma dinámica?

Antes de avanzar mucho más, encontré que los atributos para un elemento en particular se configuran en el métodoGetRawFormattingInfo de XMLListener. Entonces, he subclaseado HTMLListener y añadido el comportamiento deseado a este método.

El siguiente código, tomado de HyperlinkListener.PRG, proporciona un listener que genera un hiperenlace sobre un objeto en el informe si el campo User memo de este objeto contiene la directiva "*:URL =" seguida por la expresión que utiliza como URL

define class HyperlinkListener as HTMLListener ;
  of home() + 'ffc\_ReportListener.vcx'
  QuietMode = .T.
  && QuietMode queda predeterminado para que no haya retorno 
  dimension aRecords[1]
  && matriz de información de cada registro FRX
  * Antes de ejecutar el informe, se recorre el FRX y
  * se guarda información sobre cada registro con sus directivas
  * en el campo memo USER en la matriz aRecords.
  function BeforeReport
    dodefault()
    with This
      .SetFRXDataSession()
      dimension .aRecords[reccount()]
      scan for atc('*:URL', USER) > 0
        .aRecords[recno()] = ;
          alltrim(strextract(USER, '*:URL =', ;
          chr(13), 1, 3))
      endscan for atc('*:URL', USER) > 0
      .ResetDataSession()
    endwith
  endfunc
  * Si el campo actual tiene una directiva, añade el URL
  * a los atributos del nodo.
  function GetRawFormattingInfo(tnLeft, tnTop, ;
    tnWidth, tnHeight, tnObjectContinuationType)
    local lcInfo, ;
    lnURL
    with This
      lcInfo = dodefault(tnLeft, tnTop, tnWidth, ;
        tnHeight, tnObjectContinuationType)
      lcURL = .aRecords[recno('FRX')]
      if not empty(lcURL)
        .SetCurrentDataSession()
        lcInfo = lcInfo + ' href="' + ;
          textmerge(lcURL) + '"'
       .ResetDataSession()
     endif not empty(lcURL)
    endwith
    return lcInfo
  endfunc
enddefine

El evento BeforeReport se dispara justo antes de que se ejecute el informe. Utiliza el método SetFRXDataSession para seleccionar la sesión de datos en la que está el cursor FRX y luego busca a través del FRX y coloca la expresión URL para cada objeto que tiene la directiva en una matriz. Llama a ResetDataSession al final para restaurar la sesión de datos en la que está el listener.

El método GetRawFormattingInfo utiliza DODEFAULT() para configurar el comportamiento habitual, el que genera los atributos para un elemento XML como una cadena. Esto entonces verifica el elemento de matriz apropiado (la sesión de datos del cursor FRX se seleccionó por código en XMLListener antes de que se ejecute este código) para ver si el objeto actual del informe tiene la directiva y si es así, agrega un atributo HREF al elemento XML. Llama a SetCurrentDataSession para seleccionar la sesión de datos utilizada por los datos del informe y utiliza TEXTMERGE() sobre la expresión URL porque la expresión va a contener algo específico para cada registro, algo como <<CustomerID>>. Finalmente, realiza labores esenciales de mantenimiento al llamar a .ResetDataSession() para dejar la sesión de datos tal y como la encontramos.

¡¡ Es todo !! Veamos ahora algunos ejemplos de cómo podemos utilizar este listener.

Ejemplo 1: Enlaces vivos a URLs

Enlaces.FRX es un ejemplo sencillo que muestra cómo trabaja este listener. Es un informe sobre la tabla Links, que contiene una lista de nombres de compañías y sus sitios Web. El campo website en el informe tiene  "*:URL = http://<<trim(website)>>" en el campo User memo. El programa Links.PRG ejecuta este informe, utilizando HyperlinkListener como el report listener, y utiliza la clase _ShellExecute en las FFC para mostrar el archivo HTML en su examinador predeterminado. La Figura 1 muestra los resultados.

loListener = newobject('HyperlinkListener', ;
  'HyperlinkListener.prg')
loListener.TargetFileName = fullpath('Links.html')
report form Links object loListener
loShell = newobject('_ShellExecute', ;
  home() + 'ffc\_Environ.vcx')
loShell.ShellExecute(loListener.TargetFileName)
Figura 1

Ejemplo 2: Informes tipo Drilldown (cambio rápido)

HyperlinkReports.SCX  es un ejemplo más complejo. Como puede ver en la Figura 2, presenta una lista de información de cliente. Sin embargo, este HTML es mostrado en un control ActiveX Web Browser embebido en un formulario VFP en lugar de una ventana examinar. Al hacer clic en un nombre de compañía, VFP ejecuta un informe de las órdenes de ese cliente y los muestra en el formulario como se observa en la Figura 3. El informe de las órdenes tiene también hiperenlaces que vuelven a mostrar la lista de clientes. Entonces, este formulario proporciona informes del tipo drilldown (cambio rápido).

Figura 2 y Figura 3

El método Init de un formulario utiliza la clase HyperlinkListener para generar un archivo HTML hiperenlazado desde el informe HyperlinkCustomers y llama al método ShowReport para mostrarlo en el control Web Browser. Además, mantiene una colección de archivos HTML generados por el formulario, de tal forma que puedan ser eliminados cuando se cierra el formulario.

with This
  * Crea una colección de los archivos HTML que hemos creado
  * para que podamos nuclearlos (nuke) todos cuando hayamos cerrado.
  .oFiles = createobject('Collection')
  * Create the customers report.
  .oListener = newobject('HyperlinkListener', ;
    'HyperlinkListener.prg')
  .oListener.TargetFileName = ;
    fullpath('HyperlinkCustomers.html')
  report form HyperlinkCustomers object .oListener
  .oFiles.Add(.oListener.TargetFileName)
  * Display it.
  .ShowReport()
endwith

El método ShowReport simplemente comunica al control Web Browser que cargue el archivo HTML actual:

local lcFile
lcFile = This.oListener.TargetFileName
This.oBrowser.Navigate2(lcFile)

En lugar de generar informes de órdenes para cada cliente e hiperenlazarlos, he decidido generar los informes en dependencia del pedido que se haga en cada momento, al hacer clic sobre el nombre de cliente. Para hacer esto, necesité interceptar el clic al hiperlink. Afortunadamente, esto es fácil de hacer: Sencillamente coloqué código en el evento BeforeNavigate2 del control Web Browser.

Para decirle a BeforeNavigate2 que no es un hiperenlace normal, he utilizado la convención de "vfps://," el que  entiende por "VFP script," en lugar de "http://." El código para BeforeNavigate2  busca esta cadena en la URL y si la encuentra, ejecuta el código en el resto de la URL en lugar de estar navegando por ella. Por ejemplo, el campo memo User del campo CompanyName  en el HyperlinkCustomers.FRX  tiene lo siguiente:

*:URL = vfps://Thisform.ShowOrdersForCustomer('<<CustomerID>>')

El report listener HyperlinkListener convertirá esto en una etiqueta como <a href="vfps://Thisform.ShowOrdersforCustomer('ALFKI')">  para el cliente cuyo CustomerID es ALFKI. Al hacer clic en este hiperenlace en el control Web Browser, BeforeNavigate2 se dispara y el código de este evento se desentiende de este pedazo "vfps://" y ejecuta el resto. Además, establece el parámetro Cancel pasado por referencia, a .T. para indicar que la navegación normal no puede tener lugar (similar a utilizar NODEFAULT en un método VFP). He aquí el código para BeforeNavigate2:

LPARAMETERS pdisp, url, flags, targetframename, ;
  postdata, headers, cancel
local lcMethod
if url = 'vfps://'
  lcMethod = substr(url, 8)
  lcMethod = left(lcMethod, len(lcMethod) - 1)
  && strip trailing /
  &lcMethod
  cancel = .T.
endif url = 'vfps://'

El método ShowOrdersForCustomer , ejecutado al hacer Clic en un nombre de compañía, ejecuta el informe HyperlinkOrders para un cliente especificado, lo muestra en el control Web Browser, y agrega el nombre de archivo a la colección de archivos a ser eliminados cuando se cierra el formulario.

lparameters tcCustomerID
with This
  .oListener.TargetFileName = ;
    fullpath(tcCustomerID + '.html')
  report form HyperlinkOrders object .oListener ;
    for Orders.CustomerID = tcCustomerID
  .ShowReport()
  .oFiles.Add(.oListener.TargetFileName)
endwith

El campo CustomerName  en el informe HyperlinkOrders tiene  "*:URL = vfps://Thisform.ShowCustomers()"  en el campo memo User, así que al hacer clic en este hiperenlace en el informe vuelve a mostrar la lista de clientes.

Ejemplo 3 Lanzar un formulario VFP.

El formulario CustomerReport.SCX  es similar al HyperlinkReports.SCX; pero es un poco más sencillo. Contiene además un control Web Browser que muestra el HTML desde un informe, EditCustomers.FRX, el que tiene el mismo aspecto que el ejemplo anterior. Sin embargo, al hacer clic en un nombre de cliente en este formulario, muestra un mantenimiento para el cliente seleccionado.

El informe EditCustomers.FRX  es un clon del informe HyperlinkCustomers utilizado en el ejemplo anterior; pero tiene en su lugar  User "*:URL = vfps://Thisform.EditCustomer('<<CustomerID>>')" en el campo memo del campo Companyname. El método EditCustomer llamado desde BeforeNavigate2 cuando se hace clic en un nombre de cliente, lanza un formulario Customers, pasándole el CustomerID para el cliente seleccionado. El formulario Customers es un mantenimiento sencillo de la tabla Customers, con controles enlazados a cada campo y botones Save (Guardar) y Cancel (Cancelar).

NavPaneListener

El MVP Fabio Vázquez ha creado otro tipo de listener que tiene hiperenlaces,  aunque para un propósito completamente diferente. Su NavPaneListener, disponible para descarga desdehttp://ReportListener.com, ofrece una vista previa de informe HTML con una tabla de contenidos para el informe. Como puede ver en la Figura 4, a la izquierda se muestra una vista en miniatura (thumbnail) de cada página y la página actual se muestra a la derecha. Al hacer clic sobre la vista en miniatura navega a la página adecuada.

Figura 4

Al igual que HyperlinkListener, NavPaneListener es bastante sencillo. Su evento OutputPage, llamado cuando va a salir cada página, genera simplemente un archivo GIF para la página al llamarse a si mismo nuevamente con los parámetros adecuados. tnDeviceType  inicialmente es -1, lo que significa que no hay salida, ya que el ListenerType del listener es igual a 2. En ese caso, OutputPage se llama a si mismo, pasando el nombre de la ruta para un GIF para generar (la propiedad cPath predetermina la ruta actual) y un tipo de dispositivo que indica un archivo GIF. En la segunda llamada, además del comportamiento normal (generar el GIF especificado, OutputPage pasa el nombre y la ruta al método AddPage de un objeto colaborador guardado en la propiedad oNavigator. (Nota: He traducido un poco el código de Fabio a inglés para hacerlo más legible.)

procedure OutputPage(tnPageNo, teDevice, ;
  tnDeviceType)
  local lnDeviceType
  with This
    do case 
      case tnDeviceType = -1 && None
        lnDeviceType = 103 && GIF
        .OutputPage(tnPageNo, .cPath + 'Page' + ;
          transform(tnPageNo) + '.gif', lnDeviceType)
      case tnDeviceType = 103
        .oNavigator.AddPage(teDevice)
    endcase
  endwith
endproc

Después de que está listo el informe, el objeto navegador crea un par de documentos HTML - un documento que define un conjunto de paneles  con la tabla de contenidos en el panel de la izquierda y los contenidos a mostrar en el derecho y otro documento que contiene la tabla de contenidos como vistas en miniatura del GIF hiperenlazado para mostrar el archivo GIF de tamaño completo. El objeto navegador automatiza luego el Explorador de Internet para mostrar el conjunto de paneles de documentos.

Conclusiones

Observe que nada en e código en ninguno de estos ejemplos es complicado, ni tampoco hay mucho que hacer. Como resultado, toma solo unos momentos implementar informes con hiperenlaces vivos, informes tipo drilldowns, informes que lanzan otros formularios VFP y otras acciones, o informes con paneles de navegación. ¡ Esto muestra verdaderamente el poder de los report listeners !

El próximo mes, veremos un tópico similar -  vista preliminar de informes que permitan realizar algunas acciones cuando se hace clic al utilizar una técnica completamente diferente que nos brindará posibilidades como son búsqueda de texto y marcadores.

16 de junio de 2017

Poner la ventana principal de VFP en el primer plano

Función API para poner la Ventana Principal de VFP en el primer plano fijo, situa nuestra aplicación por encima de cualquier otra que se encuentre abierta en Windows.

***************************************************************************
***************************************************************************
*
*   Función    : VentanaTopMost
*   Proposito  : Pasar la ventana Principal de VFP al primer plano fijo
*                Situar nuestra aplicación siempre encima de todas las 
*                demas ventanas de windows
*   Parametros : 1 - Pasar la ventana Principal de VFP al primer plano fijo
*                0 - Quitar la ventana Principal de VFP del primer plano fijo
*   Regresa    : Nada
*   Ejemplo    : =VentanaTopMost(1)
*     
***************************************************************************
***************************************************************************
FUNCTION VentanaTopMost(n_Estado)
DECLARE Integer SetWindowPos IN WIN32API ;
      Integer  nWnd, ;
      Integer  nWndInsertAfter, ;
      Integer  nTop, ;
      Integer  nLeft, ;
      Integer  nHeight, ;
      Integer  nWidth, ;
      Integer  nFlags
 
DECLARE INTEGER FindWindow IN WIN32API ;
STRING cNULL, ;
STRING cWinName
      
#define SWP_NOSIZE          1
#define SWP_NOMOVE          2
#define HWND_TOPMOST       -1
#define HWND_NOTOPMOST     -2
 
*--- se obtiene el manejador de la ventana principal
n_FoxHwnd = FindWindow( 0, _SCREEN.Caption )
 
*--- si el parametro es 1
IF n_Estado = 1
 
   *--- pasar a primer plano fijo
   =SetWindowPos(n_FoxHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE + SWP_NOMOVE )
 
ENDIF
 
*--- si el parametro es 0
IF n_Estado = 0
 
   *--- la quita del primer plano fijo
   =SetWindowPos(n_FoxHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE + SWP_NOMOVE )
 
ENDIF

ENDFUNC
***************************************************************************
***************************************************************************

Carlos Tarello (Puebla, Mexico)

13 de junio de 2017

El entorno de datos en las clases.

No se si les pasó a ustedes el tener que repetir una y otra vez el alta de las mismas tablas en el entorno de datos para similares ABM de datos, al crear un form de datos fijos repetitivos como clientes, proveedores, localidades, etc.

Si hacia una clase para cada una de estas tablas para luego crear el form de nuestro cliente derivado de la clase escrita, siempre me faltaba algo. Trabajar en los campos. (agregarlos en el form, o editar su ControlSource) y crear el entorno de datos en el formulario.

Todos los proveedores tienen campos similares, nombre, domicilio, etc. Y todas esas tablas se llaman igual. ¿Por qué no crear entonces un entorno de datos en la clase?

Lo que consideré al hacer esto fue:

  • Tablas similares deben usarse con un mismo nombre (aunque pertenezcan a bases de datos distintas) Ej. articulos.dbf, rubros.dbf, etc.
  • Estas tablas tienen siempre similares estructuras. (en algunos casos se agregan o quitan campos)
  • Ubicaciones en directorios diferentes y nombre de bases de datos diferentes.

1 - Cree una clase no visual visible para todos los formularios:

Define Class Cursores_01 As Cursor Name = "Cursores_01"
 Exclusive = .F.
 ReadOnly = .F.
 *- esta propiedad que sigue, es de lectura y escritura en modo de ejecucion:
 *-  _oSis.cDbc nombre de la base de datos de la aplicación del framework.
 Database = Juststem(_oSis.cDbc)+".DBC" 
 Procedure Init
  Lparameters tcTabla
  Set Exclusive Off
  Set Multilocks On
  tcTabla = Juststem(tcTabla)
  If !File(This.Database)
   Messagebox(This.Database+" no existe.",48,This.Name)
   Return .F.
  Endif
  If !File(tcTabla+'.dbf')
   Messagebox("No se puede encontrar: "+tcTabla+".dbf",16,This.Name)
   Return .F.
  Endif
  This.Alias = tcTabla
  This.CursorSource = tcTabla  && nombre largo de la tabla
  This.Comment = 'Cursor '+tcTabla
 Endproc
Enddefine

2 - En el método LOAD de la clase form escribí;

*- Ej. ABM de datos para proveedores:
If This.DataSession # 2
 Messagebox('No ha especificado sesion privada de datos',16,This.Name)
 Return .F.
Endif
If !"CLASES_01"$Set("Procedure")
 Set Procedure To Clases_01 Additive
Endif
Set Deleted On

With Thisform.DataEnvironment
 .CloseTables() && libera el entorno de datos cargado
 .AddObject("CurProveedores", "Cursores_01", ‘proveedores’)
 .AddObject("CurLocalidades", "Cursores_01", ‘Localidades’)
 *- ... etc. y todas las tablas que se necesitan en el form.
 .OpenTables()
Endwith
Return This.nError = 0
*- Fin Load.

(a partir de aqui esta clase ya puede tener todos los campos y sus ControlSource establecidos en tiempo de diseño y funcionará sin ninguna modificación adicional en el formulario que, de entrada no tendrá ni una linea de código y funcionará.)

3 - Crear el form derivado de esta clase de abm y establecer DataSession = 2

Guardar el formulario en el directorio del cliente (o aplicación) para modificar en el futuro las especificaciones exclusivas del cliente en el. (como agregar campos, cambiar Valid, etc.)

Los problemas del futuros podrían ser:

Agregar campos a la tabla: Siempre se puede sobrescribir el método load del form, copiando y pegando el de la clase y agregando el o los cursores adicionales.

Quitar campos: No implica problema en los pocos casos en que esto sucede se puede usar la propiedad Visible del campo. La tabla no hay porque modificarla.

Espero que les sirva como me sirvió a mi.

Saludos.

Alberto Rodriguez

8 de junio de 2017

Opciones alternativa a la función FILE()

Como se ha visto últimamente en los foros de noticias de microsoft, la función FILE() tiene algunos "errores" que pueden afectar el comportamiento de nuestros programas, aquí encontrarás algunos métodos adicionales.

Normalmente, la función File() la utilizamos para revisar si existe un archivo en cierta ruta, pero el comportamiento de la misma función tiene sus detalles que están documentados en la ayuda:

"Si el archivo no puede encontrarse en el directorio predeterminado, se buscará en la ruta de acceso de Visual FoxPro establecida con SET PATH. "

Que significa esto? Que aunque se le indique la ruta completa de un archivo a buscar, la función File() buscará también ese mismo archivo dentro de la ruta establecida por PATH, dándole un tiempo adicional en hacer su labor. Por ejemplo:

Caso 1.

Intentamos buscar el archivo "HolaMundo.txt" en una ruta especifica:

? FILE("c:\miApp\miDirectorio\HolaMundo.txt")

Si el archivo no existe en ese directorio devolverá .F., pero..., si algún archivo llamado "HolaMundo.txt" se encuentra en algunas de las rutas establecidas por el comando SET PATH, o en el directorio de trabajo de la aplicación, entonces devolverá .T., cuestión que puede afectar en gran medida el comportamiento de nuestro sistema.

Caso 2

Buscamos el archivo "HolaMundo.txt" sin poner ruta completa para hacer validaciones de X tipo:

IF File("HolaMundo.txt")
   Messagebox("... blah blah blah....")
ENDIF

Si por alguna razón, nuestro archivo se encuentra en cualquier otra localidad establecida por SET PATH, la función también devolverá .T., una vez más, caso dificil de controlar, si suponemos que podemos tener cientos de rutas establecidas.

Como es de imaginarse, los inconvenientes que puede ocasionar este comportamient normal de la función FILE() puede ser algo engorroso.

Muy bien, que hay por hacer? Hasta el momento, gracias a la colaboración que ha surgido en los mensajes de los newsgroup de microsoft, tenemos lo siguiente:

Opción 1

Quitar las rutas establecidas por SetPath antes de utilizar la función File():

Function SureFile
  LParameters tcFileName
  Local lcOldPath
  Local llRetValue 
       lcOldPath = SET("PATH")
       SET PATH TO
          llRetValue = File(m.tcFileName)
       SET PATH TO &lcOldPath
      Return llRetValue
EndFunction

Opcion 2

Utilizar la función ADIR(), la cual, no presenta el inconveniente de la tan mencionada función FILE().

Function SureFile
  LParameters tcFileName
  RETURN (ADIR(laDummy, m.tcFileName) > 0)
EndFunction

Opcion 3

Utilizando la función SYS(2000):

Function SureFile
  LParameters tcFileName
  RETURN NOT EMPTY(SYS(2000,m.tcFileName))
ENdFunction

Opcion 4

Utilizar una API para llevar a cabo la labor:

*** En tu programa de inicio, solo una vez *****
declare Integer GetFileAttributes in win32api string @
****************************************

Function SureFile
   LParameters tcFileName
       return (GetFileAttributes(@m.tcFileName)  <> -1)
EndFunction

Esta última opción es la que he visto funciona un poco más rapido que las anteriores, ya que no hay que re-establecer ningún setting, ni crear ningún arreglo, ni buscar nada que no sea exactamente lo que deseamos.

Así pues, pongo a consideración las opciones disponibles, hagamos nuestras y pruebas y si tienen algún comentario al respecto, comentemoslo ya sea aquí, o en los newsgroups de microsoft (son públicos y gratuitos).

Espero que la información les sea de utilidad.

Espartaco Palma Martínez