ÍNDICE de ARTÍCULOS


Aquí tienes numerosos artículos de Delphi, espero que te gusten y sean de tu agrado.  Selecciona el articulo que quieres leer.

  1. Ficheros INI.
  2. Revisión en profundidad para mostrar y esconder ventanas.
  3. Componentes en tiempo de diseño.
  4. Cursores animados.
  5. Programando el planificador de tareas.
  6. Soluciones simples con el planificador de tareas.
  7. Bitmaps transparentes en Delphi.
  8. Vista preliminar de la impresora.
  9. Acción inmediata.
  10. Triggers y Vistas.
  11. ¿Qué es un Alias en Delphi?
  12. ¿Tabla o Consulta?
  13. Ventanas de Formas irregulares.
  14. Marcas en Windows.
  15. Aplicaciones con ayuda HTML compilada.
  16. La huella de los programadores.
  17. EXE generado con Delphi, ¿Demasiado grande?
  18. Asociación de Ficheros a las aplicaciones que tu creas.
  19. Aplicacxiones de consola.
  20. ActiveX Script en windows 98, vuelven los comandos.
  21. ActiveX Scripting, guiones de ejecucion.

Ficheros INI

 

Introducción

Voy a contar como Delphi gestiona los ficheros ini. Por fichero ini se entiende todo fichero que tiene extensión ini, está en formato Ascii, y tiene una estructura interna dividida en secciones. Un claro ejemplo es el famoso Win.ini, que lo podéis encontrar en el directorio Windows. Se puede abrir con el Bloc de Notas de Windows, aunque Windows, incluye un programa visor para ver los ficheros del sistema, que se llama Sysedit, si ejecutáis este programa podréis ver el contenido de los ficheros de sistema, entre ellos los de extensión ini.

Los ficheros ini, normalmente, tienen una estructura. Están divididos en bloques o secciones, y dentro de cada sección hay valores que se usan en la configuración del programa que gestiona ese fichero o esa sección. Digo esa sección porque el fichero Win.ini almacena datos sobre Windows, y algunos programas añaden secciones a él. Aquí hay que hacer una aclaración ya que esto solo ocurre en Windows 3.x, y Windows 95 tiene este archivo aunque esta en desuso (por compatibilidad).

Los nombres de las secciones están indicados entre corchetes, e inmediatamente van los variables que haya puesto el programa con sus valores. Este es un ejemplo de una entrada de mi fichero Win.ini:


[Ports]
LPT1:=
LPT2:=
LPT3:=
COM1:=9600,n,8,1,x
COM2:=9600,n,8,1,x
COM3:=9600,n,8,1,x
COM4:=9600,n,8,1,x
FILE:=


Como podéis ver es todo Ascii, así que alguien puede pensar que con lo que sabe de ficheros, se puede sentar delante del ordenador y hacerse unas rutinas para leerlos, a lo cual yo respondo que tu mismo, pero que Delphi ya tiene un sistema para leerlos, y muy bueno.

Delphi tiene una unidad donde tiene todos los procedimientos y funciones definidos. Así que lo único que tienes que hacer para poder empezar a trabajar con estos ficheros es añadir la palabra IniFiles a la cláusula uses de tu unidad. Esta es la lista de funciones y procedimientos para leer y escribir en estos ficheros.

Create(Filename)

Para acceder a un fichero

Free

Cierra el fichero

ReadSecctionValues(Seccion,TString)

Para leer todas la variables

ReadSections(TString)

Lee las secciones

ReadSection(Seccion,TString)

Lee una Seccion entera

ReadString(Seccion,Variable, Defecto)

Lee una variable tipo String

ReadInteger(Seccion,Variable,Defecto)

Lee una variable tipo integer

ReadBool(Seccion,Variable,Defecto)

Lee una variable tipo boleano

WriteString(Seccion,Variable,Valor)

Escribe un valor en una Variable.

WriteInteger(Seccion,Variable,Valor)

Escribe un valor tipo Integer

WriteBool(Seccion,Variable,Valor)

Escribe un valor boleano

Quizás ante tanto nombre os asustéis, pero en cuanto explique un par de detalles el resto será coser un cantar. Para acceder a un fichero ini, lo primero es indicar que fichero es, y eso se hace cuando creamos el objeto. He dicho objeto, y lo he soltado así de refilón, resulta que como Delphi es un lenguaje orientado al objeto, y el sistema de lectura/escritura de ficheros ini, es una clase pues hay que inicializarlo. Seguro que hay alguno de esta pensando que se ha metido en un lió, pero Delphi no nos abandona en los momentos difíciles, y el sistema es muy fácil. Un procedimiento genérico (cuando digo genérico es que se puede poner dentro de cualquier procedimiento) seria como sigue:


Var
MiFicheroIni : TiniFile;
Begin
MiFicheroIni := TIniFile.Create ('pepe.ini');
MiFicheroIni.Free;


En este mini programa lo que he hecho crear una variable tipo IniFile, la cual inicializo en la primera línea indicando que el fichero con el cual vamos a trabajar se llama Pepe.ini, después podría hacer lo que me plazca, y al final libero la variable que he creado, para que no ocupe memoria. Atención porque sino se indica el nombre del fichero con la ruta completa se asume que este está en el directorio Windows, y NO donde se esta ejecutando tu programa. Si quieres acceder al fichero dentro de otro directorio, debes ponerlo así:


MificheroIni := TiniFile.Create ('c:\mi directorio\pepe.ini');


Os comento que los ejemplo que voy a poner trabajarán en el directorio donde se ejecuta la aplicación. Antes de que se me pase, cuando el fichero no se encuentra este es creado.

Vamos hacer un ejemplo donde se usen las principales funciones. Para lo cual construye un formulario como este, con dos botones, dos campos edit, un CheckBox, y un par de etiquetas. Como os dije, el fichero se creará en el directorio donde se ejecuta este programa, así que para ello debemos saber donde se ejecuta, la manera que yo he usado es declarar un variable en la sección privada de la unidad, la cual contendrá el directorio y el nombre del fichero, y estos datos le serán asignados en el momento de la creación del formulario, así que este procedimiento será como sigue:


procedure TForm1.FormCreate(Sender: TObject);
begin
Fichero := ExtractFileDir (ParamStr(0))+'\Fichero.ini';
end;


El truco para saber el directorio donde está ejecutandose el programa, está en ParamStr, el cual contiene en su elemento 0 (es un array), el nombre con la ruta completa del programa. Con la función ExtractFileDir obtengo solo la ruta.

El LeerDatos, que se llama Button1, tiene las siguientes líneas de código:

procedure TForm1.Button1Click(Sender: TObject);
Var
MiFichero : TIniFile;
Edad : Integer;
begin
MiFichero := TiniFile.Create (Fichero);
Edit1.Text := MiFichero.ReadString ('Usuario','Nombre','Desconocido');
Edad := MiFichero.ReadInteger ('Usuario','Edad',99);
CheckBox1.Checked := MiFichero.ReadBool ('Usuario','Español',True);
MiFichero.Free;
Edit2.Text := IntToStr (Edad);
end;

Las funciones de lectura de datos devuelven los valores en el formato que los leen. Así cuando usamos ReadString la variable que recoge los datos debe ser un String, cae de cajón, pero por si las moscas. Además tienen tres parametros, el primero es el nombre de la sección, el segundo el nombre de la variable, y el tercero el valor por defecto, en el caso que no exista tal variable, sección, o incluso si el fichero no existiese y fuera creado en el momento de su apertura. Como la edad he decido almacenarla como un valor numérico (integer), para poderla mostrarla en el campo Edit2 he de convertirla a texto, que es lo que hace la última línea.

El procedimiento de escritura es similar:


procedure TForm1.Button2Click(Sender: TObject);
Var
MiFichero : TIniFile;
Edad : Integer;
begin
Edad := StrToInt (Edit2.Text);
MiFichero := TiniFile.Create (Fichero);
MiFichero.WriteString ('Usuario','Nombre',Edit1.Text);
MiFichero.WriteInteger ('Usuario','Edad',Edad);
MiFichero.WriteBool ('Usuario','Español',CheckBox1.Checked);
MiFichero.Free;
end;


Fijaros que el procedimiento es casi indentico al anterior, pero esta vez se usan las ordenes de escritura. Aqui ocurre lo mismo, si el fichero, la seccion, o la variable no existen son creadas. Como regla general se puede decir que si al leer o escribir algo no existe, este es creado.

He dejado atrás funciones que son: ReadSectionValues, ReaSections y ReadSection, las cuales tienen como parametros variables de tipo TStrings, las cuales más que ser una variable es un objeto, el cual no esta de más que vallas tratando porque es muy potente. Básicamente es una lista de cadenas, algo así como un Array o Matriz, pero se pueden hacer muchas cosas con el, añadir elementos, borrarlos, acceder por el indice, ordenarlos, etc. Y lo mejor es que los componentes que usan esten objeto lo hacen practicamente todo ellos. Un componente que usa este objeto son los TListBox, es el séptimo componente empezando por la derecha en la paleta Standar.


Estas funciones leen partes de un fichero ini. Así ReadSectionValues, lee los valores de las variables contenidas en una seccion. ReadSections, lee el nombre de las secciones de un fichero ini, y ReadSection lee el nombre de las variables de la sección. He hecho un pequeño ejemplo que lee de nuestro fichero ini.

He puesto en un formulario tres botones que invocan a estos tres procedimientos y un TListBox, el cual su propiedad items, es del tipo TStrings. Esta es la imagen del formulario que preparé y los tres procedimientos.


procedure TForm1.Button1Click(Sender: TObject);
Var
MiFichero : TiniFile;
begin
MiFichero := TIniFile.Create (Fichero);
MiFichero.ReadSection ('Usuario',ListBox1.Items);
MiFichero.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
Var
MiFichero : TiniFile;
begin

MiFichero := TIniFile.Create (Fichero);
MiFichero.ReadSections (ListBox1.Items);
MiFichero.Free;

end;

procedure TForm1.Button3Click(Sender: TObject);
Var
MiFichero : TiniFile;
begin
MiFichero := TIniFile.Create (Fichero);
MiFichero.ReadSectionValues ('Usuario',ListBox1.Items);
MiFichero.Free;

end;

Bueno pues los ficheros ini no tienen más secretos, solo comentar que yo personalmente prefiero trabajar sobre un fichero ini propio que tocar en el win.ini. El motivo es que así el fichero ini lo creo en el directorio donde esta el programa ejecutable, con lo que obtengo una ventaja, y es que es que si el usuario borra el programa, borrando el directorio donde está el programa ya borra el fichero ini, y no se queda en el directorio Windows y si hubiera añadido entradas al fichero win.ini este no quedaria engordado inultilmente.       

ir al índice


Introducción

Las aplicaciones que implementan capacidades de lenguajes macro o lenguajes interpretados en tiempo de ejecución, suministran una funcionalidad extraordinaria a los usuarios. Un ejemplo muy conocido es el editor de documentos Microsoft Word.

Este artículo muestra una forma de incorporar lenguajes interpretados en tiempo de ejecución a las aplicaciones, mediante la tecnología ActiveX Script de Microsoft, utilizando como plataforma de construcción a Delphi versión 3 o posterior.

ActiveX Script es una especificación de interfaces de objetos COM (Common Object Model), que posibilitan a los desarrolladores, incorporar en sus aplicaciones los motores de ejecución de lenguajes interpretados de una manera fácil.

Por desarrolladores en este trabajo, entenderemos las empresas o personas cuyo producto es un programa o paquete especifico, por ejemplo un editor de texto, un sistema financiero contable, un navegador de Internet, etc, para distinguirlo de aquel que se especializa en desarrollar motores o maquinas interpretación de guiones de ejecución, que llamaremos proveedor.

La definición del lenguaje del guión de ejecución (el script en si mismo), la sintaxis, el formato de almacenamiento, el modelo de ejecución y otras temas relacionadas, son responsabilidad de los proveedores de motores ActiveX Script. El hospedar o soportar un motor de interprete de guiones de ejecución, es responsabilidad del desarrollador de la aplicacion.

El guión de ejecución (el script) en general es un texto, escrito con las reglas del lenguaje que soporta el motor en el cual se ejecuta.

Los pasos básicos

Las componentes de ActiveX Script caen en dos categorías: Los hospederos (HOST) de guiones de ejecución y los motores de interpretación (SCRIPT ENGINE).

Los hospederos son las aplicaciones que crean los guiones y llaman al motor de interpretación adecuado para ejecutar el guión. Un ejemplo de aplicación hospedera es el Microsoft Internet Explorer, tan utilizado para navegar por las páginas WEB.

Los motores de interpretación se pueden desarrollar para cualquier lenguaje y ambiente de ejecución, por ejemplo el Microsoft Visual Basic Scripting Edition (VBScript), utilizado en el Microsoft Internet Explorer.

Los pasos involucrados en la secuencia básica para la ejecución de un guión es la siguiente (algunos elementos utilizados de momento, serán explicados mas adelante):

  • Por algún medio se crea un texto en el lenguaje soportado en el motor de interpretación que es el guión de ejecución.
  • El hospedero crea una instancia del motor de interpretación y la inicializa. Generalmente esto se realiza haciendo una llamada a la función CoCreateInstance a la que se le especifica el identificador del motor de interpretación. La función devuelve un puntero a la interfaz IActiveScript soportada por el motor. A esta interfaz se le pregunta para obtener un puntero a la interfaz IActiveScriptParse, utilizado el método QueryInterface. Posteriormente, se llama a la funcion InitNew de la interfaz IActiveScriptParse obtenida, para inicializar el motor de interpretación.
  • El hospedero suministra al motor de interpretación, que objetos tiene disponibles para la ejecución de los guiones, llamando a la funcion AddNamedItem de la interfaz IActiveScript.
  • El hospedero alimenta el guión de ejecución al motor de interpretación. El hospedero que mantiene el guión como un texto llama a la funcion ParseScriptText de la interfaz IActiveScriptParse.
  • El hospedero instruye al motor de interpretación que ejecute el guión. Esto se realiza llamando a la función SetScriptState de la interfaz IActiveScript.
  • El motor de interpretación obtiene información de los objetos del hospedero a través de la función GetItemInfo de la interfaz IActiveScriptSite implementada por el hospedero.
  • Antes de ejecutar el guión, el motor de interpretación conecta los eventos de todos los objetos relevantes a través de la interfaz IConnectionPoint si es soportada.
  • Finalmente el motor ejecuta los métodos de los objetos referenciados en el guión llamando la función Invoke y notifica al hospedero de los eventos que ocurren.
El hospedero

La aplicación (hospedero) que soporte guiones de ejecución ActiveX Script tiene que contener al menos una instancia de un objeto que exponga la interfaz IActiveScriptSite. Este objeto, es el punto de interacción del hospedero con el motor de interpretación. Usualmente este objeto es el contenedor de todos los otros objetos que son visibles para el guión que ejecuta el hospedero. La figura 1 muestra el código pascal de definición de dicha interfaz.

A continuación veamos una breve descripción de cada uno de los miembros de la interfaz IActiveScriptSite

GetLCID Esta función permite al hospedero indicar las constantes de localización para la interacción del motor de interpretación y el usuario de la aplicación.
GetItemInfo Esta función es llamada por el motor de interpretación para buscar los objetos nominalizados dentro de la aplicación.
GetDocVersionString Esta función es llamada por el motor de interpretación para obtener una el número de versión textualizado del documento vigente. Esta cadena puede ser usada para validar que cualquier estado congelado en el motor de ejecución pueda ser salvado consistentemente en el documento vigente.
OnScriptTerminate Esta función es llamada cuando el motor de interpretación términa. En muchos motores, esta función no es llamada y es posible utilizar el guión interpretado para generar eventos en la aplicación hospedera
OnStateChange Esta función es llamada cuando el motor de intérprete cambia de estado tanto explicitamente mediante la función SetScriptState o implícitamente por los eventos que se generan en el guión de ejecución.
OnScriptError Esta función es llamada cuando durante el análisis sintáctico del guión o durante la ejecución del mismo, se encuentra un error. El motor de interpretación suministra una implementación de la interfaz IActiveScriptError que describe el error en tiempo de ejecución en términos de una estructura EXCEPINFO, en adición a la indicación de la localización del error en el texto original del guión.
OnEnterScript Esta función es llamada por el motor de ejecución para indicar el comienzo de una unidad de trabajo.
OnLeaveScript Esta función es llamada por el motor de ejecución para indicar la terminacion de una unidad de trabajo.
 
Type
IActiveScriptSite = Interface( IUnknown )
['{DB01A1E3-A42B-11CF-8F20-00805F2CD064}']
function GetLCID( out Lcid: TLCID ): HRESULT; stdcall;
function GetItemInfo( const pstrName: POleStr;
dwReturnMask: DWORD;
out ppiunkItem: IUnknown;
out Info: ITypeInfo): HRESULT; stdcall;
function GetDocVersionString( out Version: TBSTR): HRESULT; stdcall;
function OnScriptTerminate( const pvarResult: OleVariant;
const pexcepinfo: TExcepInfo): HRESULT; stdcall;
function OnStateChange( ScriptState: TScriptState ): HRESULT; stdcall;
function OnScriptError( const pscripterror: IActiveScriptError ): HRESULT; stdcall;
function OnEnterScript: HRESULT; stdcall;
function OnLeaveScript: HRESULT; stdcall;

end;

figura 1. Definición de la interfaz IActiveScriptSite

El motor de interpretación

El motor de interpretación ActiveX Script, tiene que implementar la interfaz IActiveScript. Cuando el hospedero crea una instancia del motor de interpretación, llama a la función SetScriptSite de esta interfaz, para establecer el puente de interacción hospedero-motor de interpretación.

A continuación veamos una breve descripción de cada uno de los miembros de la interfaz IActiveScript

SetScriptSite Informa al motor de lenguaje del objeto establecido en el hospedero que expone la interfaz IActiveScriptSite
GetScriptSite Puede ser llamada para obtener el hospedero del motor de interpretación
SetScriptState Pone el motor de interpretación en un estado dado
GetScriptState Obtiene el estado del motor de interpretación
Close Causa que el motor de interpretación abandone cualquier guión cargado, pierda el estado en que se encuentra, librere los objetos internos y punteros a las interfaces de otros objetos y entre en el estado closed
AddNamedItem Adiciona un nombre en el nivel primario del espacio nominal del motor de interpretación
AddTypedLib Adiciona una librería de tipo en el espacio nominal del motor de interpretación
GetScriptDispath Obtiene un puntero a la interfaz IDispatch para los métodos y propiedades asociadas con el guión que se está ejecutando
GetCurrentScriptThread Obtiene el identificador definido del motor de interpretación para el hilo de ejecución (thread) vigente
GetScriptThreadID Obtiene el identificador de hilo de ejecución (thread) asociado en el motor de interpretación asociado con el hilo de ejecución dado en Microsoft WIN32
GetScriptThreadState Obtiene el estado vigente en el hilo de ejecución (thread) del guión
InterrupScriptThread Interrumpe la ejecución del guión que se esta ejecutando
Clone Sintetiza una copia gemela del motor de interpretación (excepto cualquier estado de ejecución), retornando una nueva instancia del motor de interpretación cargado sin asociación al hospedero en el hilo de ejecución vigente (thread)

El motor de interpretación permite controlar la funcionalidad básica de la ejecucion del guion; pero si soporta guiones textuales y evaluación de expresiones textuales compatibles con el lenguaje, entonces tiene que implementar la interfaz IActiveScriptParse.

La interfaz IActiveScriptParse constituye el analizador sintáctico del lenguaje interpretado y el ejecutor de las instrucciones del guión del motor de interpretación. El puntero a esta interfaz se obtiene llamando el metodo QueryInterface de IActiveScript. Los miembros fundamentales de la misma son:

InitNew Inicializa el motor de interpretacion
AddScriptlet Adiciona un fragmento de codigo a un guion
ParseScriptText Efectúa el analisis sintactico de un guion, adicionando las declaraciones dentro del espacion nominal del motor y evaluando apropiadamente el codigo

La función ParseScriptText ejecuta el analisis sintáctico de acuerdo a las banderas de control que se establezcan en su llamada. Esta función tiene la siguiente sinopsis:

HRESULT ParseScriptText (
  LPCOLESTR pstrCode, // Buffer con el texto del guion
  LPCOLESTR pstrItemName, // Nombre de item que suministra el contexto
  IUnknown* punkContext, // Contexto de depuracion
  LPCOLESTR pstrEndDelimiter, // Delimitador de fin del guion
  DWORD dwFlags, // Banderas de control
  VARIANT* pvarResult, // Retorno de los resultados de la ejecucion
  EXCEPINFO* pexcepinfo); // Buffer para retornar los errores

El parámetro dwFlags, determina el procesamiento y puede ser cualquiera o una combinacion de las siguientes constantes:

SCRIPTTEXT_ISEXPRESSION Si la distinción entre una expresion computacional y una sentencia es importante pero sintacticamente ambigua en el lenguaje, esta bandera especifica si el texto debe ser interpretado como una expresion o como una lista de sentencias. Implicitamente se asume que son sentencias
SCRIPTTEXT_ISPERSISTENT Indica que el codigo adicionado durante la llamada a esta funcion debe ser salvado en el motor de interpretacion o si éste debe retornar a su condicion en el estado de inicializado
SCRIPTTEXT_ISVISIBLE Indica si el texto del guion debe ser visible (y por tanto, accesable por nombre) como un metodo global del espacio nominal del guion

El parámetro pexcepinfo apunta a una estructura para recibir la informacion de excepcion, que llenada si la funcion ParseScriptText retorna el valor DISP_E_EXCEPTION.

Aplicacion de ejemplo

Se puede construir una aplicación rudimentaria que actúe de hospedero de los motores de interpretación. El código que acompaña este artículo, tienen las fuentes completas del ejemplo que explicamos a continuación. Para crear el hospedero, creemos una nueva aplicacion en Delphi. Incorporemos un componente Memo1 del tipo TMemo, para la edición de los guiones. Adicionemos un componente TMenu con al menos un elemento TMenuItem para ejecutar el guión editado asociado al siguiente codigo:

procedure TForm1.mnEjecutar1Click(Sender: TObject);
var
    ActiveScriptParse : IActiveScriptParse;
    ActiveScript : IActiveScript;
    ExcepInfo : TExcepInfo;
    AppScriptSite : TScriptSiteWindow;
    EngineID : TCLSID;
begin
    // En esta aplicacion, se utilizara el motor de interpretacion de Microsoft
    // para Visual Basic (VBScript).
    EngineID := ClSID_VBScript;
    if Memo1.Lines.Text = '' then Exit;
    FResult := UnAssigned;
    // Crear el objeto que implementa la interfase IActiveScriptSite del hospedero
    AppScriptSite := TScriptSiteWindow.Create;
    AppScriptSite.WindowHandle := Application.Handle;
    // Inicializar las variables que sostendran las interfases que suministrara
    // el motor de interpretacion. El moldeo a Pointer es requerido, para
    // evitar la manipulacion automatica de Delphi del contador de referencias
    // de la interfase IUnknown.
    Pointer(ActiveScript) := nil;
    Pointer(ActiveScriptParse) := nil;
    // Crear una instancia del motor de interpretacion y obtener el puntero a su
    // interfase IActiveScript.
    // Si hay error en la creacion de la instancia del objeto, OleCheck genera una
    // excepcion y la instancia AppScriptSite creada anteriormente será liberada
    // automaticamente al salir del marco.
    OleCheck( CoCreateInstance(
        EngineID, // Indentificador del motor de interpretacion.
        nil,
        ClsCtx_InProc_Server, // El motor tiene que estar implementado
        // en una biblioteca de enlace dinamico
        IID_IActiveScript, // Identificador de la interfase implementada
        // en el objeto que se quiere obtener
        ActiveScript) // Puntero para recibir el retorno la
        // interfase solicitada.
        );
    // Obtener el puntero a la interface IActiveScriptParse del motor de
    // interpretacion.
    ActiveScript.QueryInterface(IID_IActiveScriptParse, ActiveScriptParse);
    // Registrar con el motor de interpretacion, el hospedero del mismo.
    OleCheck( ActiveScript.SetScriptSite( AppScriptSite ));
    try
        // Comenzar la ejecucion de un nuevo guion en el motor de interpretacion
        if SUCCEEDED( ActiveScriptParse.InitNew ) then
        begin
        // Para crear los objetos COM globales del hospedero que pueden ser
        // accesados por el motor de interpretacion se indica en comentario
        // el codigo a ejecutar.
        // Los objetos globales se adicionan en un listado
        // El primer caso es un objeto COM no registrado que es parte de la
        // aplicacion. El segundo adiciona un objeto registrado en la PC donde se
        // corre esta aplicacion. El primer parametro de procedimiento AddGlobal,
        // es el nombre con que el objeto va a ser referenciado en los guiones
        // de ejecucion.
        // AppScriptSite.AddGlobal('AppObj', TObjetoDeAplicacion.Create);
        // AppScriptSite.AddGlobal('PCO', CreateOleObject('PCOBJDLL.PCObjeto') );
        // Adicionados los objetos globales accesables desde el guion, cuando el
        // motor de interpretacion necesita obtener informacion de cualquier objeto
        // llamará a la funcion GetItemInfo de la interfase IActiveScriptSite.
        // Adicionemos pues los seudonimos de los objetos, al espacio global de
        // nombres de hospedero, donde hara sus busquedas el motor de interpretacion.
        { ActiveScript.AddNamedItem('AppObj',
        SCRIPTITEM_ISVISIBLE
        or SCRIPTITEM_GLOBALMEMBERS //Permitir al guion usar los miembros del
        //objeto sin referenciar directamente el objeto
        or SCRIPTITEM_ISPERSISTENT //Permitir que el objeto sea salvado
        //directamente por el motor de interpretacion
        );
        ActiveScript.AddNamedItem('PCO', SCRIPTITEM_ISVISIBLE or
        SCRIPTITEM_GLOBALMEMBERS or
        SCRIPTITEM_ISPERSISTENT
        );
        }
    // Activar el motor de interpretacion
    OleCheck( ActiveScript.SetScriptState(SCRIPTSTATE_CONNECTED) );
    // Analizar y ejecutar el guion
    OleCheck( ActiveScriptParse.ParseScriptText(
        POleStr(WideString(Memo1.Lines.Text)), // Texto del guion de ejecucion
        nil, // Nombre del elemento que da contexto al guion a ejecutar
        nil, // Contexto de depuracion (debugger)
        nil, // Delimitador del fin del guion de ejecucion
        0, // Contexto fuente del Cookie
        0, // Linea de comienzo de guion
        SCRIPTTEXT_ISPERSISTENT or SCRIPTTEXT_ISVISIBLE, // Banderas
        FResult, // Buffer para retornar el resultado de la ejecucion del guion
        ExcepInfo // Buffer para recibir la informacion de error
        ));
    end;
    except // Si algo fue mal, mostrar el mensaje de error
        On e:Exception do
            ShowMessage( e.Message );
    end;
    // Finalmente es interesante notar que
    // El compilador de Delphi (version 3 o posterior), genera automaticamente el
    // codigo para liberar (llamada a procedimiento _Release del objeto) los
    // objetos creados que implementan la interfaz IUnknown cuando estos se van
    // fuera de su marco validez.
end;

El código ha sido abundantemente comentado para permitir identificar claramente los pasos explicados para la implementación de la tecnología ActiveX Script en una aplicación.

ir al índice

Microsoft(r) Windows(r) Scripting Host es un hospedero de guiones de ejecución independiente del lenguaje para motores de interpretación ActiveX™ en plataformas Windows de 32 bits.

Las plataformas de Microsoft Windows(r) 98, Windows NT(r) Workstation versión 5.0 y Windows NT Server versión 5.0 soportan este hospedero de guiones de ejecución. La propia compañía ya ha incluído los motores Microsoft(r) Visual Basic(r) Scripting Edition (VBScript) y Microsoft(r) JScript™. Otras compañías de proporcionarán otros motores ActiveX™ para lenguajes como Perl, TCL, REXX, Python y otros.

Este hospedero presenta baja ocupación de memoria durante la ejecución directa de los guiones, las secuencias de comandos no necesitan estar incrustadas en un documento HTML y es ideal para implementar comandos no interactivos que realicen tareas administrativas y de inicio de sesiones.

Windows Scripting Host utiliza la extensión del archivo de comandos para determinar qué motor de secuencias de comandos debe utilizar. Como resultado, quien escribe secuencias de comandos no necesita obtener el ID de motor de secuencias de comandos. El hospedero mantiene por sí solo, una asignación de extensiones de secuencias de comandos con ID de los motores y utiliza el modelo de asociación de Windows para iniciar el motor apropiado para la secuencia de comandos dada.

Windows Scripting Host expone nueve objetos globales de los cuales sólo tres están disponibles para los guiones de ejecución. Estos son WScript, WshArguments y WshShell. Las propiedades y metodos de los mismos quedan fuera del alcance de este articulo. El lector interesado, puede encontrar la documentacion de referencia en la ayuda del Personal Web Server de Windows 98.

En Windows 98 puede correrse desde la consola un guión de ejecución con el utilitario CSCRIPT.EXE. En la carpeta SAMPLES\WHS dentro del directorio de instalación de Windows 98, encontrará el archivo SHOWVAR.VBS, que es un guión de ejecucion en VBScript. Revise su contenido con el utilitario NOTEPAD.EXE. Para correrlo, haga doble CLICK sobre el mismo o ejecute desde la opcion EJECUTAR del menú de inicio la siguiente linea de comando,

CSCRIPT SHOWVAR.VBS

ir al índice


Aplicaciones de Consola

El ambiente de Delphi, está concebido fundamentalmente para desarrollar aplicaciones que soportan el ambiente gráfico de Windows de forma rápida, sin embargo, existe un conjunto de aplicaciones que no requieren de una interfaz gráfica que Delphi construye muy eficientemente también. En este artículo, vamos a examinar las aplicaciones de tipo consola, que son programas que se caracterizan por ser rápidos y el tamaño del ejecutable es pequeño.

Las generalidades

Las aplicaciones de consola, son programas escritos sin hacer uso de una interfaz gráfica de usuario.

En Windows, las aplicaciones de consola se ejecutan en una ventana especial basada en texto, conocida como la ventana de consola, a través de la cual se realiza la interacción con la aplicación. Los dispositivos estandares de entrada y salida de texto, están automáticamente asociados a esta ventana de consola.

Delphi no crea una nueva forma cuando crea una aplicación de tipo consola. Los controles visuales de la VCL no son generalmente empleados en una aplicacion de tipo consola, sin embargo, pueden ser usados.

El progama más simple de una aplicación de tipo consola que usted puede compilar en Delphi y correr desde la línea de comando es el siguiente:

Program Consola

{$APPTYPE CONSOLE}

Begin
  Write( 'Hola a todos' );
End.

La primera línea, declara el nombre del programa, que tiene que ser igual nombre del fichero en que se guarda la aplicación con extensión PAS. La directiva {$APPTYPE CONSOLE} instruye al compilador que construya una aplicación del tipo consola. Esta directiva, tiene sólo sentido para un programa y no debe se usada en una librería, package o unit. Debemos resaltar, que las aplicaciones de consola, son programas de 32 bits dentro del ambiente de Delphi.

El contenido de lo que hay que ejecutar dentro del programa, se enmarca entre el begin y end. En este ejemplo, el programa simplemente escribe una cadena al dispositivo estandard de salida (ie. el monitor de la computadora).

El procedimiento Write, está definido en la unidad SYSTEM y es implícitamente importada en todos los programas. En Delphi 4, el ejecutable de este programa compilado tendrá un tamaño de fichero de 15.5 Kb. Compárelo contra el tamaño del programa más simple que puede construirse, usando una interfaz gráfica.

Las aplicaciones de consola, pueden dividirse en varios ficheros. En este caso, utilizaremos la claúsula Uses para indicar los ficheros que se incluyen en la aplicación. También, podemos incorporar recursos, en particular, es útil adicionar recursos de versión. Esto se realiza adicionando la directiva {$R *.RES}.

Una forma alternativa y equivalente de indicar, dentro del ambiente de Delphi, que estamos compilando una aplicación de tipo consola, es con la opción Generate Console Application del LINKER. Personalmente preferimos, hacerlo explícitamente con la directiva antes señalada.

Las particularidades

En la unidad SYSTEM, está declarada una variable lógica, IsConsole, que puede ser usada para examinar en tiempo de ejecución, si el programa que está corriendo es una consola o tiene interfaz gráfica y tomar las acciones correspondientes.

El tamaño pequeño de las aplicaciones consola, está vinculado a no usar la unidad Forms. Si en el ejemplo inicial, adicionaramos la línea,

Uses Forms;

el tamaño del ejecutable generado, cambiaría a 271 Kb. O sea, la aplicación incrementa el tamaño del ejecutable en más de 17 veces. El único argumento, por el cual desearíamos adicionar la unit Forms a una aplicación de consola, sería para proporcionar acceso a la variable Application, que es una instancia del objeto TApplication. Así que, no incluya la unit Forms en sus aplicaciones de tipo consola.

Para acceder a la cola del comando, cuando se invoca una aplicación, podemos hacer uso de las funciones ParamCount y ParamStr.

La ParamCount, retorna el número de parámetros pasados a la aplicación en la cola del comando. Por su parte, ParamStr(n) retorna la n-ésima cadena de la cola del comando. ParamStr(0) siempre retorna el nombre de la aplicación con el camino completo desde la carpeta en que reside. Si el valor de n que se suministra es mayor que el valor retornado por ParamCount, entonces tendremos una cadena vacía.

Adicionalmente, la unidad System, suministra un conjunto de variables públicas muy útiles, que son inicializadas cuando se carga la misma y el conjunto de funciones básicas requeridas en cualquier aplicación: manipulación de cadenas, acceso a ficheros, operaciones con variants y manejo de memoria. Recomendamos que eche un vistazo a esta unidad.

Algo avanzado

Vamos a ver ahora, a través de un ejemplo sensillo, cómo una aplicación con interfaz grafica de windows interactúa con una aplicación de consola.

La aplicacion con interfaz gráfica MiGUI

Esta aplicación muestra en su interfaz un botón para lanzar la aplicación de consola, una caja de edición para establecer los parámetros del comando de consola que se va a invocar y un control MEMO para mostrar el resultado.

La aplicacion de consola MiCon1

La aplicación de consola es muy sencilla. Simplemente realiza la suma de dos números suministrados en la cola de comando y escribe el resultado.

El esquema funcional de interacción de ambas aplicaciones es el siguiente:

  • La aplicación de Windows, MiGUI, lanza la aplicación de consola MiCon1, pasándole por la cola de comando los parámetros funcionales (dos números que se van a sumar).
  • La aplicación de consola, MiCon1, ejecuta su trabajo y escribe al dispositivo de salida estandard, el resultado.
  • La aplicación MiGUI, captura la salida de la aplicación de consola y la muestra en su interfaz.

Asociado al botón de lanzar de la aplicación MiGUI se tiene el siguiente código, que ha sido abundantemente comentado para facilitar su comprensión:

procedure TForm2.Button1Click(Sender: TObject);
const
  BufSize = $4000;
var
  si : TStartupInfo;
  pi : TProcessInformation;
  ec, attr, BytesRead : DWORD;
  WriteHandle, ReadHandle : THandle;
  ConsoleOut : string;

      procedure TerminarConsola;
      begin
        Button1.Enabled := True;
        CloseHandle( ReadHandle ); // Cerrar los handles del pipe
        CloseHandle( WriteHandle );
      end;
begin
  // Creamos un pipe anonimo cuyo extremo de escritura vamos a
  // utilizar como dispositivo de salida del proceso de consola
  // que se va a lanzar. El buffer del pipe tiene que ser suficientemente
  // grande como contener la salida del proceso de consola.
  if not CreatePipe(ReadHandle, WriteHandle, nil, BufSize) then Exit;

  // Inicializar la estructura de informacion de arranque del proceso
  FillChar(si, SizeOf( TStartupInfo ), 0);
  si.cb := SizeOf( TStartupInfo );

  // Establecemos los dispositivos de salida y error de proceso de consola
  si.hStdOutput  := WriteHandle;
  si.hStdError   := WriteHandle;

  // Establecer la prioridad del hilo de ejecucion de la aplicacion de consola
  // Variantes: IDLE_PRIORITY_CLASS, HIGH_PRIORITY_CLASS, REALTIME_PRIORITY_CLASS
  attr := DETACHED_PROCESS or NORMAL_PRIORITY_CLASS;

  // Indicar que se atienda los valores pasados en el miembro wShowWindow y
  // los handles suministrados en la estructura
  si.dwFlags     := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
  // Indicar la forma en que se muestra la aplicacion lanzada
  // Variantes: SW_SHOWNORMAL, SW_MAXIMIZE, SW_MINIMIZE
  si.wShowWindow := SW_HIDE;

  // La funcion CreateProcess, crea un nuevo proceso donde ejecuta el fichero
  // ejecutable especificado.
  ConsoleOut := Edit1.Text; // Ejecutable a lanzar
  try
    if not CreateProcess(nil,	// lpApplicationName (nil para consolas)
             PChar(ConsoleOut),  // Ejecutable de consola y cola de comando
             nil,		// LPSECURITY_ATTRIBUTES
             nil,		// LPSECURITY_ATTRIBUTES
             FALSE,	// bInheritHandles
             attr,         // dwCreationFlags
             nil,	   	// lpEnvironment
             nil,		// lpCurrentDirectory
             si,		// lpStartupInfo
             pi )		// lpProcessInformation

    then // Si la aplicacion de consola no es lanzada, retornar
    begin
      TerminarConsola;
      Exit;
    end;
  except
    TerminarConsola;
    Exit;
  end;

  // Evitar recurrencia al lanzar la aplicacion de consola
  Button1.Enabled := False;

  // Ciclo para esperar que la aplicacion de consola termine su trabajo
  Repeat
    if not GetExitCodeProcess(pi.hProcess, ec) then
    begin
      TerminarConsola;
      Exit;
    end;
    Application.ProcessMessages;
  Until (ec <> STILL_ACTIVE);

  try
    // Leer del pipe, la informacion escrita por la aplicacion de consola
    SetLength(ConsoleOut, BufSize);
    ReadFile( ReadHandle, ConsoleOut[1], BufSize, BytesRead, nil );
    SetLength(ConsoleOut, BytesRead);

    // Copiar al memo, el texto retornado por la aplicacion de consola
    Memo1.Lines.Text := ConsoleOut;
  finally
    TerminarConsola;
  end;
end;

El ejemplo es muy sencillo; pero permite imaginar su alcance. En primer lugar, la aplicación que sostiene la interfaz gráfica y la aplicación de consola, se ejecutan automáticamente en dos hilos de ejecución diferentes. En un ambiente multitarea y multihilo como Win32, se pueden establecer prioridades diferentes para cada aplicación. La aplicación de consola se ejecuta sin ser mostrada. Si ésta cae en un ciclo de ejecución prolongado, desde la aplicación con interfaz puede cancelarse la misma o inclusive ejecutar otras cosas.

Construya la aplicación MiGUI (las fuentes están construidas con Delphi 4) suministrada con este artículo y ejecute a través de ella, el siguiente ejemplo de aplicación de consola, que es parte del sistema operativo:

mem /c

Interesante, ¿ Verdad ?

   ir al índice


Asociación de ficheros a las aplicaciones que tu creas


En el Explorador de Windows, cuando pinchamos con el botón derecho del ratón sobre un fichero con extensión BMP, automáticamente emerge un menú, en una de cuyas opciones encontraremos Abrir. Si seleccionamos esta opción, se abre la aplicación Paint mostrando la imagen contenida en el fichero para ser editada. Esta funcionalidad, se logra con la asociación de tipos de ficheros a las aplicaciones que son capaces de cargarlos para ejecutar algún trabajo con él.

La asociación de ficheros a una aplicación es conocida desde Windows 3.1, donde bastaba adicionar en la sección Extensions del fichero del sistema WIN.INI, la siguiente línea:

BMP=C:\WINDOWS\PAINT.EXE   ^.BMP

Pero, en la plataforma Win32, el soporte de WIN.INI solamente es mantenido por compatibilidad con las versiones anteriores. En Windows 95, Windows 98, Windows 2000 y Windows NT, el registro de Windows es el repositorio donde se almacena la información de asociación de ficheros con una aplicación, ahora extendida con un modificador llamado verbo (verb), que califica la acción a ejecutar sobre el mismo. 

El verbo indica la acción que la aplicación asociada debe realizar sobre el documento contenido en un fichero. Existen tres verbos clásicos: Open, utilizado para abrir y visualizar el documento, Edit utilizado para abrir el documento en modo de edición y Print, utilizada para mandar el documento a la impresora predeterminada en el sistema.

El procedimiento de asociar un tipo de fichero a una aplicación se realiza en dos pasos:

  1. Inscribir el tipo de documento utilizando la extensión del fichero que lo contiene en el Registro de Windows, con la clase de una aplicación, que implicitamente lo va a manipular.
  2. Registrar la clase de la aplicación que es capaz de abrir un documento y los verbos para el Shell de Windows, con los cuales es capaz de actuar sobre el documento.

En lo adelante, supondremos que las operaciones con el registro de Windows, se realizan bajo al llave HKEY_CLASSES_ROOT

Para realizar el primer punto, debemos añadir una llave con el nombre de la extensión del fichero (incluyendo el punto precedente de la extensión), bajo la cual estableceremos una llave con el nombre default a la que asociaremos como valor el nombre de la clase de la aplicación. Usualmente, se utiliza como nombre de clase de una aplicación, el nombre de la misma, seguida de un punto y la palabra Document. Así, en el editor del registro de Windows, se vería:

HKEY_CLASSES_ROOT\ 
.ext\ 
Default = "MiApp.Document"

Para registrar la clase de la aplicación, hay que crear una llave con MiApp.Document. Bajo esta llave, se especifican las acciones que se ejecutan desde el Shell de Windows, donde cada acción visible es una clave cuyo valor es la cadena del comando para ejecutar dicha acción. En el registro de Windows, debe quedar algo como esto,

HKEY_CLASSES_ROOT\
MiApp.Document\
Shell\
Open\Command\Default="MiAppPath\MiApp.exe %1"
Edit\Command\Default="MiAppPath\MiApp.exe %1"
Print\Command\Default="MiAppPath\MiApp.exe /p %1"

En el ejemplo a continuación se muestra el código completo de una unit, que contiene el procedimiento RegisterAssociation, que le permite asociar un tipo de fichero a su aplicación, que adicionalmente incorpora el icono principal de la aplicación a las metáforas gráficas que son mostradas en el Explorador. Simplemente, incorpore la llamada a este procedimiento con cuanto tipo de fichero usted quiera asociar a su aplicación dentro del evento de CreateForm de la forma principal de su aplicación.

Unit Associat;

{******************************************************************************}
Interface

Uses Windows, Forms, Registry, SysUtils, Dialogs;

  procedure RegisterAssociation(const ext, Desc: string);

{******************************************************************************}
Implementation

Const
  OPEN_CMD = '\Shell\Open\Command';
  EDIT_CMD = '\Shell\Edit\Command';
  ICON_OBJ = '\DefaultIcon';

{-------------------------------------------------------------------------------
 Registra la asociacion de ficheros de la aplicacion en WINDOWS.
 ej: Si los ficheros asociados a la aplicacion desde donde se llama esta
 funcion son de extension TIF, entonces se registran estos ficheros mediante
 la siguiente llamada

     RegisterAssociation( '.TIF', 'Archivo de imagen TIF' );

 Importante: No olvidar el punto de las extensiones.
-------------------------------------------------------------------------------}

procedure RegisterAssociation(const ext, Desc: string);
var
  Obj, pth : string;
  lng : Integer;
begin
  with TRegistry.Create do
  try
    RootKey := HKEY_CLASSES_ROOT;

    with Application do
    begin
      lng := Length(ExtractFileName( ExeName )) - Length(ExtractFileExt( ExeName ));
      Obj := Copy(ExtractFileName( ExeName ), 0, lng) + '.Document';
    end;

    pth := ext;
    if ext[1] <> '\' then
    begin
      if ext[1] <> '.' then
        pth := '\.' + ext
      else
        pth := '\' + ext;
    end
    else if ext[2] <> '.' then
      pth := '\.' + Copy(ext, 2, Length(ext) - 1)
    else
      pth := '\' + ext;

    if not KeyExists(pth) then
    begin
      if OpenKey(pth, True) then
        WriteString('', Obj);
      CloseKey;
    end;

    if not KeyExists(Obj) then
    begin
      if OpenKey(Obj, True) then
      begin
        WriteString('', Desc);
        CloseKey;
      end;
      if OpenKey(Obj + OPEN_CMD, True) then
      begin
        WriteString('', Application.ExeName + ' %1');
        CloseKey;
      end;
      if OpenKey(Obj + ICON_OBJ, True) then
      begin
        WriteString('', Application.ExeName + ',1');
        CloseKey;
      end;
    end;
  finally
    Free;
  end;
end;

End.

ir al índice


EXE generado con Delphi ¿demasiado GRANDE ?

Muchas veces puede suceder que a varios nos parezca demasiado grande el .EXE generado por Delphi, y realmente lo es. ¿Por qué ? ¿Cómo evitarlo ? Estas son algunas de las cosas que debemos tener en cuenta al desarrollar una aplicacion hecha en Delphi y queremos que funcionen con el máximo de optimización y eficiencia posibles.

Desde los antiguos programas de MS-DOS generados en Turbo Pascal ó C, a muchos de nosotros nos gustaban usar las famosas librerías TVision ó Turbo Power para darle belleza y colorido a nuestras aplicaciones. Esto por supuesto tenía el inconveniente de mientras más unidades (units) usábamos, mas grande se nos hacia el .EXE resultante.

Con la llegada de Windows al mercado, la programacion tradicional de MS-DOs quedó obsoleta y entró un nuevo modo de programar el cual abstraía mucho más a los usuarios de sus aplicaciones en general, se trataba del Sistema de manipulación de Mesjaes de Windows, en el que Windows es el que manda un mensaje a la aplicacion, la aplicación espera por ese mensaje y si lo conoce y tiene un manipulador de este mensaje, sencillamente le da respuesta. Con esta gran ventaja surgió adjunto el desarrollo de la Programación visual Orientado a Objetos que ya venía desde el Ms-DOS y surgieron las librerias de Windows llamadas API. Pero la API no era POO y tenía muchas dificultades al expresar cosas evidentes y muy sencillas, además del tiempo que el usuario gastaba en realizar una sencilla aplicación. Surgieron las MFC (Microsoft Fundation Class) que no son más que las mismas funciones de la API creadas por Microsoft pero ahora con POO y de Borland la VCL (Visual Component Library).

Por supuesto que a partir de ese momento todos los productos de Microsoft soportaban la API que era la librería que ya venía implicita en Windows. Por eso si se desarrolla una aplicacion con un producto de Microsoft, el EXE generado es relativamente más pequeño que si se hace con un producto Borland con la VCL, porque ya las DLLs de MFC están en Windows, mientras que la VCL no y hay que distribuirlas.

Como todos conocemos, Dephi es un compilador de código nativo, o sea, no nesesita ninguna DLL acompañada del .EXE para ejecutarse en una PC sacada de la caja.

Cuando instalamos Delphi, la primera vez que lo corremos el introduce dentro del .EXE las DLLs según las vaya usando. Usted toma ese .EXE y se va para cualquier lugar, lo corre y listo. Pero esto tiene el inconveniente que según se usen mas BPLs (Borland Package Library - Las DLLs de Borland) asi será mas grande el EXE resultante. La solución está en ir al menú Projects/Options/Packages, y poner el Check Box Build whot runtime Packages en TRUE. Esto hará que dentro del .EXE no se metan las BPLs, solo el código nesesario de nuestra aplicación. Los BPLs se distribuyen mediante una utilidad llamada Install shield que viene con Delphi y donde se le dice que BPL usará. Para saber las PBLs que usa nuestro proyecto basta con ir al menú Project/Information y allí veremos todas las BPLs que hay que poner en el Install Shield. Esto es una muy buena opción ya que si Ud. desarrolla varias aplicaciones en una misma PC, todas las aplicaciones usan una misma copia de las BPLs, ahorrando memoria, tiempo de instalación de Install Shield, porque el no sobreescribe la copia del BPL a menos que Ud diga lo contrario. De esta forma es más efectivo y beneficioso porque Build Whit runtime pachkages, como lo trae Delphi por defecto mete los BPLs dentro de nuestro EXE, cosa que no nos satisface.

Pero hay algo que quizás puede pasar inadvertido para algunos usuarios, sobre todo principiantes (a mi me pasó). Como todos sabemos, los componentes de la paleta de compoenetes de Delphi se agrupan en Páginas, c/u de estas páginas está asociada a un BPL en específico, por Ej: Standard - VCL40.BPL, adicional - VCLX40.BPL, Data Access y DataControls VCLDB40 y VCLDBX40.BPL respectivamente, QReport a QRP40.BPL y asi sucesivamente. Que quiere decir esto ?? Sencillo. Si usted usa un componente en Delphi, el le adciona a la clausula uses de la forma la unit a que pertenece el componente, por tanto si agrega un TTable agregará al uses DBTables si agrega un Navigator agregará un DB y usted estrá usando el VCLDB40 y VCLDBX40.BPL en su programa, porque el compilador se guia en as BPLs que uses por la unit en la cláusula uses. Pero cuando eliminamos un componete de nuestra forma, Delphi no le quita la unit de la cláusula uses, y entonces, aunque no tengamos una sola Tabla ó navigator en nuestra forma, si sus respectivas units están reflejadas en la forma, entonces el seguirá pidiendole las BPLs asociadas a estos compoenetes. Por tanto, es recomendable, cuando borremos algún componente si estamos seguros de que no usamos más esa unit para nada (Por eso Dephi no la borra del uses cuando borras un componente) que la ELIMINEMOS de la cláusula uses de nuestra forma. De cualquier manera, existe un componente en la Forma y usted borra su unit, el vuelve a agregarla al compilar. (Cada vez que compila chequea los componentes y las units de c/u de ellos).

Concluyendo:

Este es una de las posibles soluciones, hay para tratar de hacer nuestras aplicaciones eficientes y optimas, minimizando el tiempo de carga ejecucion y usando los recursos del sistema de forma más racional, ahorrando memoria (recuerden que al ejecutar un .EXE todo está en memoria), etc. También debemos optimizar internamente nuestro código y usar las facilidades que nos da el lenguaje Object - Pascal internamente y la POO.

Suerte...

ir al índice


La huella de los programadores

(Nota: Los textos e ideas originales de esta exposición no son míos. En cada caso, se cita la fuente de donde extraje la información, que por curiosa, me pareció interesante hacerselas llegar)

Cuando se ejecuta un gran proyecto de software en un equipo, los programadores, gustan de dejar en el paquete alguna huella que revele su participación. Después de mostrar algunos ejemplos, vamos a relatar una técnica para lograr esto en nuestros proyectos en Delphi.

En el sitio www.ethek.com, podemos encontrar un reseña debida a Jesús Manuel Rodríguez, que nos explica la forma de ver los créditos del equipo de Windows 98.  

  1. Menú de Inicio -> Panel de control -> Fecha y hora, o también hacer doble click sobre la hora en la barra de tareas.
  2. Seleccionar temporalmente la zona horaria GMT +01:00 Bruselas, Conpenhague, Madrid, París, Vilnius
  3. En la imagen del mapa mundi que está mostrada, poner el cursor en el punto 1, pinchar el ratón y  manteniendo oprimida la tecla Ctrl, arrastrarlo hasta el punto 2.
  4. Soltar la tecla Ctrl .
  5. Con el cursor sobre el punto 2, pinchar el ratón y  manteniendo oprimida la tecla Ctrl, arrastrarlo hasta el punto 3.
  6. Soltar la tecla Ctrl .

 

Entonces, se abre la ventana que mientras carga otros recursos, muestra unos números en azul y luego los nombres y fotos del equipo de programación de Windows 98.

No es fácil lograr que se muestren los créditos del equipo de programación de Windows 98. Tendrá que intertarlo varias veces. Yo generalmente lo consigo a la tercera oportunidad. Bueno, lo que se muestra es una página WEB, que se abre temporalmente en el siguiente camino:

    Directorio de Windows\Application Data\Microsoft\Welcome

En el directorio SYSTEM, se encuentra una librería, MEMBG.DLL, que contiene los recursos mostrados en la página WEB, dinámicamente construida.

Otra forma alternativa de ver los nombres de los participantes en el proyecto Windows 98 es la siguiente.

  1. Menú de Inicio -> Panel de control ->Pantalla, o también hacer click con el botón derecho sobre la pantall y seleccionar Propiedades
  2. Seleccionar la pestaña "Protector de Pantalla".
  3. Seleccionar el protector de pantalla "Texto 3D" y en activar la configuración. Escribir el texto "Volcano", aceptar y después hacer una vista previa.
  4. Los nombres de las personas participantes en el equipo, irán alternandose uno a uno.

Veamos ahora una propuesta de hacer algo como esto. Bajo el título "Implementing an Easter Egg", por colaboracion de Michael Burton, en el número 38 (Abril de 1999) de la páginas de UNDU, se suministra un código que al ocurrir una combinación de teclas poco usuales, se activa un diálogo que puede servir para mostrar los créditos de los participantes en la construcción de un proyecto en Delphi.

Aquí reproducimos el código suministrado en el artículo (traducción mediante).

1. En la forma en que se desee activar los créditos del equipo, establezca la propiedad KeyPreview en True. Esto permite a la forma tener la entrada de teclado antes que el control activo en la misma. 

2. Crear dos variables privadas en la forma:

  eeCount: integer;
  sEgg: string;

La variable eeCount, cuenta los golpes de tecla para procesar la activación de los creditos de equipo. La variable sEgg contiene la secuencia de teclas entradas.

3. Crear dos constantes en la forma:

const
  EE_CONTROL: TShiftState = [ssCtrl, ssAlt];
  EASTER_EGG = 'RIMROCK';

EE_CONTROL contiene las teclas de control que deben estar oprimidas cuando el usuario quiere activar los créditos del equipo de programación. Pueden ser cualquier combinacion de ssCtrl, ssShift y ssALt, pero se debe usar cualquier combinacion que no interfiera con los controles en la forma.

4. En el evento OnCreate de la forma, hacer la siguiente inicialización:

eeCount := 1;
sEgg    := EASTER_EGG;

5. En el evento OnKeyDown de la forma implemente el siguiente código:

procedure TForm.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  {are the proper control keys down?}
  if Shift = EE_CONTROL then begin
    {was the proper key pressed?}
    if Key = Ord(sEGG[eeCount]) then begin
      {was this the last keystroke in the sequence?}
      if eeCount = Length(sEGG) then begin
        {Easter egg activation code goes here, e.g.,}
         ShowMessage('This is an easter egg');
          eeCount := 1; {failure - reset the count}
      end else begin
        Inc(eeCount); {success - increment the count}
      end;
    end else begin

      eeCount := 1; {failure - reset the count}
    end;
  end;
end;

Puede reemplazar la llamada a ShowMessage con lo que usted desee. Compile y pruebe.

Los programadores de Delphi, también han dejado sus huellas en los programas. Por ejemplo, en Delphi 1, si usted muestra la ventana de créditos (About box) y sosteniendo oprimidas las teclas Shift y Alt, teclea la palabra TEAM, veremos entonces los créditos de los programadores.

¿ Estará implementada esta curiosidad en las versiones posteriores, digamos en Delphi 3 o 4 ? Si usted lo conoce, hágamelo saber y muchas gracias por anticipado. 

ir al índice


Aplicaciones con ayuda HTML compilada.

Parte 1

Microsoft ha incorporado en su plataforma Windows, un nuevo tipo de ayuda, más en correspondencia con los desarrollos de Internet y nuevas formas de presentación y búsqueda de la información, que es la ayuda HTML compilada (HTML Help compiled). 

Hay 2 razones por la que los desarrolladores tenemos que prestar atención a la ayuda HTML compilada.

  1. Es parte integrante de las nuevas versiones de Windows.
  2. Suministra mayor funcionalidad y es más fácil de construir y mantener que la ayuda basada en ficheros HLP.

Para ver los ficheros de ayuda HTML compilada, se requiere tener instalado los siguientes componentes:

hh.exe Visor de ficheros de ayuda compilada, que se instala en el directorio de WINDOWS.
hhCtrl.ocx Motor de ejecución de la ayuda compilada, que se instala en el directorio SYSTEM.
itircl.dll Librería utilizada en la visualización de la ayuda compilada instalada en el directorio SYSTEM.
itss.dll Librería utilizada en la visualización de la ayuda compilada instalada en el directorio SYSTEM.

Si ha instalado Microsoft Internet Explorer 4 o superior, automáticamente estos componentes se habrán instalados. En Windows 98 y NT 5, ya se suministran como parte del sistema operativo.

En esta parte, queremos mostrar como se accede a los ficheros de ayuda compilada, desde una aplicación de Delphi. Primeramente abordaremos, de una forma muy general, el API de la ayuda compilada, explicaremos la forma en que se especifica una URL en un fichero de ayuda compilada y mediante ejemplos, mostraremos como desde un programa de Delphi se accede a la tabla de contenido, el índice, las búsquedas, y obtener ayuda sensible al contexto. Usted puede apoyarse, durante la lectura de este artículo, descargando la fuentes del programa de ejemplo suministrado.

Para construir la ayuda compilada, debemos usar el paquete MS HTML Help Workshop, que puede ser obtenido gratuitamente, descargandolo en Internet desde el sitio de Microsoft.

El API del HTML Help

El API de HTML Help, permite a los programas de Windows crear ventanas de ayuda y mostrar los tópicos de la ayuda, con un control completo sobre el tipo, estilo y posición de la ventana de ayuda.

También, la ventana de ayuda, puede emerger en un programa Windows sin uso de la tecnología OLE. Además, está implementado la ayuda sensible al contexto, búsqueda por palabras claves, interacción con los programas de Windows y control del panel de navegación en el visor de ayuda.

El API de HTMP Help tiene una sola función que muestra la ventana de la ayuda. A través de esta función usted puede especificar que tópico mostrar en la ventana de la ayuda, si es mostrado en una ventana emergente (popup), si el tópico es accesado a través de su ID, una palabra clave, un salto URL, etc.

HWND HtmlHelp(HWND hwndCaller, LPCSTR pszFile, UINT uCommand, DWORD dwData) ;

   
hwndCaller
Especifica el handle de la ventana que llama la función. Esta ventana es la propietaria de la ventana de ayuda que se crea. Cuando la ventana de ayuda de cierra, el foco es retornado a la ventana propietaria. Adicionalmente, la funcion HtmlHelp le envía cualquier mensaje de notificación desde la ventana de ayuda, si este elemento está habilitado.
pszFile
Depende del valor de uCommand y especifica el camino del fichero de ayuda compilada o un tópico dentro del fichero. Si el comando especificado, no requiere de un fichero, este valor es ignorado y puede ser puesto en 0.
uCommand
Especifica el comando a cumplimentar.
dwData
Especifica cualquier dato que sea requerido basado en el valor de uCommand.

Dependiendo del valor de uCommand especificado, esta funcion retorna:

  • El handle a la ventana de ayuda.
  • 0 (NULL). En algunos casos, esto indica fallo y en otros indica que la ventana de ayuda no ha sido creada.

Un listado de los comandos disponibles (parámetro uCommand de la función HtmlHelp) por categorías, es el siguiente:

Categoría Comando
Tipo de Ventana HH_CLOSE_ALL 
HH_GET_WIN_HANDLE 
HH_GET_WIN_TYPE 
HH_SET_WIN_TYPE 
Ayuda sensible al contexto HH_DISPLAY_TEXT_POPUP 
HH_DISPLAY_TOPIC 
HH_HELP_CONTEXT 
HH_TP_HELP_CONTEXTMENU 
HH_TP_HELP_WM_HELP 
Búsqueda por palabras claves HH_ALINK_LOOKUP 
HH_KEYWORD_LOOKUP 
Panel de Navegación HH_DISPLAY_INDEX 
HH_DISPLAY_SEARCH 
HH_DISPLAY_TOC 
Mensajes de error HH_GET_LAST_ERROR 
Sincronizar con el contenido HH_SYNC
Hilo de ejecución simple HH_INITIALIZE 
HH_PRETRANSLATEMESSAGE 
HH_UNINITIALIZE

El comando más importante es HH_DISPLAY_TOPIC, que permite abrir un fichero de ayuda compilada (CHM) en una ventana de ayuda y mostrar el tópico especificado dentro de este fichero. 

Para acceder al API de la ayuda compilada, hemos traducido al PASCAL el fichero HtmlHelp.h suministrado con el paquete de Microsoft. Usted encontrará este fichero con el nombre HHelp.pas en el archivo compactado que acompaña este artículo. En cada unidad en que haga acceso a la ayuda compilada, deberá incluir el mismo en la claúsula uses.

Las URL de ayuda compilada

En un fichero de ayuda HTML compilada, una URL especifica un fichero o un tópico y opcionalmente el tipo de ventana para mostrar la parte referenciada por la URL.

Para especificar una URL a un fichero de ayuda compilada, se hace como sigue:

          FicheroAyuda.chm[>nombre de Window] 

donde FicheroAyuda.chm es el nombre del fichero de la ayuda HTML compilada y nombre de Window, es el nombre de la ventana de ayuda donde se desea que aparezcan los tópicos.

Para especificar un tópico dentro de un fichero de ayuda compilada, se escribe:

          FicheroAyuda.chm::Topico.htm[>nombre de Window] 

donde Topico.htm, es el nombre del fichero HTML que se quiere mostrar en la ventana de la ayuda.

Si la ayuda compilada ha sido construida, preservando el camino de acceso a los ficheros HTML, entonces una URL a un fichero con estas características, se escribe:

          FicheroAyuda.chm::/Camino/Topico.htm[>nombre de Window] 

donde Camino, es la cadena de carpetas anidadas por la que se llega al fichero.

También es usual, anteceder estas URL con la partícula "ms-its:". Esta partícula, identifica un protocolo, registrado en Windows, similar a cuando en un navegador de internet escribimos "http://".

Así que en general, desde una página HTML, al referirnos a otra página HTML almacenada en un fichero de ayuda compilada, debemos escribir:

          ms-it:FicheroAyuda.chm::/Camino/Topico.htm[>nombre de Window] 

El visor de ayuda HTML compilada

La forma más inmediata y sencilla de ver un fichero de ayuda HTML compilada, es hacer doble CLICK sobre el nombre del mismo (extensión CHM) en el Explorador de Windows.

Desde nuestras aplicaciones, podemos ejecutar la siguiente línea de código, para obtener el mismo resultado:

{ Cuando se instala el visualizador de ficheros de ayuda HTML compilada, los ficheros con  extension CHM, se asocian a la utilidad HH.EXE, que los muestra. La funcion ShellExecute, retorna el handle a la aplicación asociada que abre el fichero de ayuda compilada, Esto puede ser utilizado para realizar algún procesamiento adicional al hecho de visualizar la ayuda }

  ShellExecute(Handle,
               'open',
               PChar(Application.HelpFile), 
               nil,
               nil,
               SW_SHOWNORMAL);

El visor de la ayuda HTML compilada, es una ventana de tres paneles, como usted puede ver mientras lee los artículos del Taller de Delphi. 

Cada panel del visor, puede ser manipulado a través de la estructura HH_WINTYPE y el API de la ayuda HTML compilada; pero esto no presenta interés en esta exposición. 

Tabla de contenido e índice de la ayuda HTML compilada

En lo adelante, supondremos que en la propiedad HelpFile de nuestra aplicación, hemos establecido el nombre del fichero de ayuda HTML compilada.

Para obtener la tabla de contenido de la ayuda, ejecutamos el siguiente comando:

  {Muestra el visor de ayuda con la pestaña de Contenido activada, mostrando la página principal}
  HtmlHelp(Handle,
           PChar(Application.HelpFile),
           HH_DISPLAY_TOC,
           0);

De la misma manera, para obtener el índice de la ayuda, ejecutamos el siguiente comando:

  var
    ss : string;

  {Muestra el visor de ayuda con la pestaña de Indice activada}
  ss := 'Pagina 1';
  HtmlHelp(Handle,
           PChar(Application.HelpFile),
           HH_DISPLAY_INDEX,
           DWORD(PChar(ss)));

Búsquedas por palabras

Para comenzar una búsqueda podemos usar el siguiente procedimiento:

{Muestra el visor de ayuda con la pestaña de Buscar activada y realiza una busqueda por proximidad de la cadena suministrada}
procedure HHelpBuscar(ss: string);
var
  q : HH_FTS_QUERY;
begin
  q.cbStruct        := SizeOf(HH_FTS_QUERY);
  q.fUniCodeStrings := FALSE;
  q.pszSearchQuery  := PChar(ss);
  q.iProximity      := HH_FTS_DEFAULT_PROXIMITY;
  q.fStemmedSearch  := FALSE;
  q.fTitleOnly      := FALSE;
  q.fExecute        := FALSE;
  q.pszWindow       := nil;
  HtmlHelp(Handle,
           PChar(Application.HelpFile),
           HH_DISPLAY_SEARCH,
           DWORD(@q));
end;

Si en la estructura HH_FTS_QUERY, establecemos el miembro fExecute en True, entonces al invocar la función HtmlHelp, se realiza la búsqueda solicitada. Si el miembro fTitleOnly se establece en True, la búsqueda se realiza solamente sobre los títulos de los tópicos de ayuda.

Obteniendo ayuda sobre un tópico

En la ayuda HTML compilada, un tópico se identifica con una página HTML. Si esto es conocido, se puede mostrar un tópico, suministrando al siguiente procedimiento, el nombre de la página donde está contenido. 
 
{Procedimiento para activar la ayuda contenida en una página determinada.}
procedure MostrarTopico(Topico : string);
begin
  HtmlHelp(Handle,
           PChar(Application.HelpFile + '::/' + Topico),
           HH_DISPLAY_TOPIC,
           0);
end;
 
En Delphi, cada componente descendiente de TWinControl, tiene la propiedad HelpContext, que puede contener cualquier valor entero. Nuestro objetivo es mostrar la página que es referenciada por el valor de la propiedad HelpContext. Básicamente, un tópico de la ayuda se invoca, a modo de ejemplo, de la siguiente manera:
 
  { Al invocar la ayuda contextual, es requisito que el fichero de ayuda HTML compilada, haya sido construido llenando en la seccion MAP la asociacion de las cadenas contextuales con las páginas de la ayuda. En el fichero de cabecera tienen estar referidas igualmente las cadenas contextuales con los valores numericos de la ayuda contextual que usamos en el campo HelpContext de los controles.} 
  HtmlHelp(Handle,
           PChar(Application.HelpFile),
           HH_HELP_CONTEXT,
           HelpContext);
 
Para que esto funcione correctamente, tenemos que seguir algunos pasos adicionales durante la construcción de la ayuda HTML compilada:
  1. Construir el fichero asociación de las constantes simbólicas de la ayuda contextual y los valores numéricos de las mismas (fichero de inclusión). 
  2. Adicionar en la sección MAP del proyecto de ayuda compilada, el nombre del fichero de inclusión.
  3. Adicionar en la sección ALIAS del proyecto de ayuda compilada, la correspondencia entre las constantes simbólicas de la ayuda contextual y las páginas HTML contenedoras del texto del tópico de contexto. 
El fichero de inclusión, se construye incorporando para cada valor de contexto una línea, como se muestra en el siguiente ejemplo:
 
// Constantes para los tópicos de ayuda contextual
#define IDH_MAIN        10000
#define IDH_PAGINA1  11000
#define IDH_PAGINA2  12000
#define IDH_POPUP      20000
 
Se acostumbra a llamar a este fichero de inclusión con el nombre CTXHelp.h y a que las constantes simbólicas comiencen con la partícula IDH_.

Para adicionar en la sección MAP, el nombre de fichero de inclusión, abrir con un editor de texto, ie. NOTEPAD.EXE, buscar la sección MAP y si no existe crear y poner el nombre del fichero. Esto quedará como se muestra a continuación:

[MAP]
#include CTXHelp.h

El nombre del fichero de inclusión debe estar contenido, además, en el listado de ficheros de la ayuda HTML compilada, que se encuentra en la sección FILES del proyecto de ayuda.

Finalmente, la sección ALIAS debe ser constuída o de existir ya, adicionar una línea para cada constante simbólica asociada a una página de ayuda contextual, como se muestra en el siguiente ejemplo:

[ALIAS]
IDH_PAGINA1=Pagina1.htm
IDH_PAGINA2=Pagina2.htm

Dedicaremos otro artículo a la construcción de la ayuda HTML compilada y abundaremos más sobre en contenido del proyecto de ayuda. Por el momento, aceptemos esto como necesario para lograr el funcionamiento de la ayuda HTML en nuestros programas.

Obteniendo ayuda sensible al contexto

En los diálogos modales y en las recomendaciones para cumplimentar las especificaciones de Window 95, la ayuda contextual se muestra en ventanas emergentes (POPUPs). El SDK de la ayuda HTML compilada, tiene implementada una forma sencilla de incorporar las cadenas contextuales. Para ello, se crea un fichero de texto: Un contenido se especifica comenzando una línea con un punto (.), seguido de la palabra topic  y la constante simbolica de la ayuda contextual. A continuación se escribe en una o varias líneas, el contenido de ese tópico. A modo de ejemplo, vea la siguiente construcción, utilizada en el programa ilustrativo suministrado con este artículo.

.topic IDH_PROV1
El fruto maduro cae solo; pero nunca en la boca

.topic IDH_PROV2
Vivimos igual que soñamos: solos

.topic IDH_PROV3
Antes de avanzar con su carga, el carnero retrocede

.topic IDH_PROV4
Sin la amistad, el mundo es un desierto

.topic IDH_PROV5
La preocupación debe llevar a la acción, no a la depresión

Este fichero textual se recomienda que tenga el mismo nombre que el fichero de inclusión de las constantes y extensión TXT, ie. CTXHelp.txt y debe ser igualmente adicionado a la sección de FILES del proyecto de ayuda compilada.

Para invocar la ayuda contextual en una ventana emergente, podemos usar una función como la que se muestra a continuación.

{Invocar un tópico de ayuda contextual en fichero de texto 
 mostrado en una ventana emergente. El valor del 
 identificador del contexto es obtenido de la propiedad 
 HelpContext del control. Observe que en el Inspector de 
 Objetos, la propiedad HelpContext tiene el valor de la 
 constante de ayuda contextual.}
procedure MostarAyudaPopUp(ctrl: TWinControl);
var
  sFile : string;
  ids : array[0..2] of DWORD;
begin
  // Obtener especificación completa del  
  // fichero de ayuda. Adicionar la referencia
  // al fichero de cadenas de ayuda contextual. 
  sFile := ExtractFilePath( ParamStr(0) ) + 
           Application.HelpFile + 
           '::/CTXHelp.txt';
  // Nota importante: En Delphi el ID del control 
  // es su handle de Windows. El ID del control es
  // el valor del parámetro WParam, cuando se recibe
  // el mensaje WM_COMMAND de Windows.
  ids[0] := ctrl.Handle;
  ids[1] := ctrl.HelpContext;
  ids[2] := 0;
  // Cualquiera de las formas siguientes, muestran 
  // una ventana emergente, conteniendo el texto de
  // la ayuda contextual. Son equivalentes.
//  HtmlHelp(ctrl.Handle,
//           PChar(sFile),
//           HH_TP_HELP_WM_HELP,
//           DWORD(@ids));
  HtmlHelp(ctrl.Handle,
           PChar(sFile),
           HH_TP_HELP_WM_HELP,
           DWORD(@ids));
end;

Lo más relevante de este código, es que la función HtmlHelp, requiere que se espefiquen los identicadores de contexto como un arreglo de pares de double word, donde el primer elemento es el identificador del control, esto es, el ID del control que es el valor de WParam, cuando se recibe el mensaje WM_COMMAND, y el segundo, es el valor numérico de la constante de ayuda de contexto, la misma que colocamos en la propiedad HelpContext del control. En Delphi, el valor de este identificador del control, es el valor de su propio handle de Windows. Finalmente, este arreglo de pares hay que terminarlo con un cero (0).

Integrando la ayuda HTML con Delphi

Delphi tiene implementado el acceso a la ayuda de ficheros HLP mediante un grupo de procedimientos de la clase TApplication y TCustomForm. Para modificar el comportamiento de la ayuda, podemos escribir el código del evento OnHelp de la forma de nuestra aplicación.

El evento OnHelp, ocurre cuando la aplicación recibe una solicitud de ayuda (se oprime la tecla F1). Este procedimiento, tiene la siguiente sinopsis:

function FormHelp (Command: Word; Data: Longint; var CallHelp: Boolean): Boolean;

En la variable CallHelp, se debe establecer True si la VCL debe responsabilizarse con la llamada a WinHelp, en caso contrario, usaremos False. La función debe retornar True si se realiza un procesamiento exitoso, en caso contrario debe retornar False

El parámetro Command, puede contener cualquiera de las constantes de comando definidas en el fichero Win32 Developer's Reference Help (Win32.HLP). Los posibles valores del parámetro Data, dependen de Command.

Con esta estrategia de implementación, podremos escribir este evento como sigue:

function TForm1.FormHelp(Command: Word; Data: Integer;
  var CallHelp: Boolean): Boolean;
var
  ss : string;
begin
  ss := UpperCase( ExtractFileExt( Application.HelpFile ));
  // Procesar solo el nuevo formato
  CallHelp := ss <> '.CHM';
  Result := True;
  if CallHelp then Exit;
  case Command of
    HELP_CONTENTS:
      HtmlHelp(Handle,
               PChar(Application.HelpFile),
               HH_DISPLAY_TOC, 0);

{  HELP_CONTEXTMENU: Hay que tratarlo de manera diferente. No se puede con la implementacion de FormHelp.}

    HELP_CONTEXT,
    HELP_CONTEXTPOPUP:
      if ActiveControl.HelpContext <> 0 then
        MostrarAyudaPopUp( ActiveControl )
      else
        MostrarTextoPopUp(ActiveControl, 'No hay ayuda disponible.');

    // Para mostrar como se usa la ayuda
    HELP_WM_HELP:
      ShowMessage('Mensaje HELP_WM_HELP');
  end;
end;

Conclusiones

Hasta aquí, hemos visto como integrar la ayuda HTML compilada en los programas de Delphi. En un próximo artículo, abordaremos cómo contruir la ayuda compilada y que posibilidades adicionales tenemos en el momento de construir y operar la ayuda suministrada con nuestos programas.

ir al índice


Marcas en Windows

Son incontables las ocaciones en que requerimos tener una marca para señalizar un estado, durante la ejecución de un programa. Por ejemplo, si deseamos saber si está corriendo un programa, podemos establecer una marca para averiguarlo. Otro tipo de escenario, por ejemplo, sería conocer si durante una sesión de Windows ha sido ejecutado al menos una vez, un proceso determinado. Este artículo, explica cómo establecer marcas, de acuerdo a las características que se puedan requerir de las mismas.

De acuerdo al tiempo de vida de las marcas, podemos dividirlas en dos grupos: las volátiles y las perennes. Las marcas volátiles, tienen vida solamente si el computador se encuentra encendido y funcionando, en cambio las perennes, persisten cuando recién se inicia el computador.

La forma más evidente de establecer una marca es creando una variable en memoria cuando ejecutamos un programa. Por ejemplo, supongamos que deseamos contar el número de instancias creadas de un objeto, en este caso una forma. El siguiente fragmento de código ilustra esto:

Unit uConteo;

Interface

Uses
  Windows,  Messages, SysUtils, Classes,  Graphics,
  Controls, Forms,    Dialogs,  StdCtrls;

Type
  TActualizaConteo = procedure(Conteo: Integer) of object;

  TForm2 = class(TForm)
    StaticText1: TStaticText;
    Button1    : TButton;
    procedure  Button1Click(Sender: TObject);
    procedure  FormCreate(Sender: TObject);
    procedure  FormDestroy(Sender: TObject);
    procedure  FormClose(Sender: TObject;
                         var Action: TCloseAction);
  private
    { Private declarations }
    FActualizaConteo : TActualizaConteo;
  public
    { Public declarations }
    property   ActualizaConteo : TActualizaConteo
                              write FActualizaConteo;
  end;

{ Función para contar el número de instancias creadas del objeto TForm2.}
  function CuantosObjetosForm2: Integer;

Implementation

{$R *.DFM}

Var
{ Variable estática utilizada para contar el número de instancias de un objeto. }
  ConteoForm2 : Integer = 0;

{ Utilizamos un procedimiento para acceder a la variable estática que contiene el número de instancias creadas del objeto TForm2.}
function CuantosObjetosForm2: Integer;
begin
  Result := ConteoForm2;
end;

{ Cada vez que se crea una instancia del objeto, incrementamos el contador de instancias.}
procedure TForm2.FormCreate(Sender: TObject);
const
  Msg = 'Sólo se permiten 5 instancias de este objeto';
begin
  Inc( ConteoForm2 );
  { Limitamos el número de instancias creadas. }
  if ConteoForm2 > 5 then
  begin
    Free;
    raise Exception.Create(Msg);
  end;
end;

{ Cada vez que se destruye una instancia del objeto, decrementamos el contador de instancias. Si el evento de informar el número de instancias está asignado, entonces es llamado.}
procedure TForm2.FormDestroy(Sender: TObject);
begin
  Dec( ConteoForm2 );
  if Assigned(FActualizaConteo) then
    FActualizaConteo(ConteoForm2);
end;

{ Es importante señalizar que se libere el objeto al cerrar la forma, o de lo contrario será ocultado hasta que se libere la aplicación. }
procedure TForm2.FormClose(Sender: TObject;
                      var Action: TCloseAction);
begin
  Action := caFree;
end;

{ Invocar la destucción de la instancia del objeto. }
procedure TForm2.Button1Click(Sender: TObject);
begin
  Close;
end;

End.

El mejor modo de evaluar estos conceptos, es compilar las fuentes suministradas de este ejemplo. Para generar objetos utilizamos un evento asociado a un botón en la forma principal de la aplicación, como se muestra a continuación:

procedure TForm1.ImprimirConteo(conteo: Integer);
begin
  lbConteo.Caption := IntToStr( conteo );
end;

{Utilizamos este evento para crear una instancia del objeto, en este caso una forma.}
procedure TForm1.Button1Click(Sender: TObject);
begin
  {Aquí creamos el objeto cuyo número de instancias es controlado.}
  with TForm2.Create(nil) do
  try
    ActualizaConteo := ImprimirConteo;
    Show;
  except
    On e:Exception do ShowMessage( e.Message );
  end;
  ImprimirConteo( CuantosObjetosForm2 );
end;

Otro tipo de marca, la podemos implementar con los ficheros mapeados en memoria (memory mapped file). A modo de ejemplo, podemos utilizar esta marca para contar el número de instancias que se ejecutan de un programa. Para ello procedemos de la siguiente manera. Al iniciar el programa intentamos escribir a un fichero mapeado en memoria. Si el fichero existe, incrementamos el contador de instancias de la aplicación, en caso contrario, creamos el fichero mapeado en memoria e iniciamos el contador de instancias en 1. Si usted descarga  las fuentes de ejemplo de este artículo, verá una implementación de contar el número de instancias de un programa. De forma simplificada resultará como se muestra en el siguiente fragmento de código: 

Var
  hMap : THandle;
  pDW  : ^DWORD;
  conteo  : DWORD;

begin
  hMap := CreateFileMapping($FFFFFFFF, 0, 
                       PAGE_READWRITE, 0,
                        SizeOf(DWORD), 'CONTADOR');
  if hMap <> 0 then
  begin
    pDW := MapViewOfFile(hMap, FILE_MAP_WRITE, 
                         0, 0, SizeOf(DWORD));
    try
      Inc(pDW^);
      conteo := pDW^;
    finally
      UnmapViewOfFile( pDW );
    end;
  end;
    
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  { Actualizar la forma principal para mostrar el numero de la instancia que se está creando.}
  Form1.lbInst.Caption := IntToStr(conteo);
  Application.Run;

  if hMap <> 0 then
  begin
    pDW := MapViewOfFile(hMap, FILE_MAP_WRITE, 
                         0, 0, SizeOf(DWORD));
    try
      Dec(pDW^);
      conteo := pDW^;
    finally
      UnmapViewOfFile( pDW );
    end;
    if conteo = 0 then
      CloseHandle( hMap );
  end;
end.  

Si deseamos que antes de ejecutar un programa cada vez que inicia la máquina, se ejecute una vez y solamente una vez un determinado proceso,  entonces debemos crear una marca que esté vigente en toda la sesión de Windows. Para esto utilizaremos los átomos (atoms). Un ejemplo de esta situación, se muestra en el siguiente código de ejemplo, donde se muestra una ventana de saludo, solamente la primera vez que se ejecuta el programa en cada sesión de Windows. El código es adicionado en el propio fichero del proyecto de la aplicación.

{Si existe la marca del átomo, la aplicación ya corrió al menos una vez y no hay que mostrar el saludo, en caso contrario, se crea la marca que esta vigente durante toda la sesion de Windows y se muestra el mensaje de saludo.}
  if GlobalFindAtom(AtomName) = 0 then
  begin
    GlobalAddAtom(AtomName);
    // Mostrar la ventana de saludo
    with TForm3.Create(nil) do
    try
      ShowModal;
    finally
      Free;
    end;
  end;    

Las marcas permanentes hay que lograrlas sobre medios que mantengan su estado aún cuando la máquina se encuentra apagada. De hecho, la mayor parte de las veces estamos haciendo uso de ellas. Entre los dispositivos estandares de la computadora sólo dos son convenientes para este fin, el disco duro y la CMOS. En el disco duro la marca se puede establecer creando un directorio en un camino conocido o un fichero o el tamaño del mismo o algunos de sus atributos (fecha de creación, modificación, tamaño, etc). Un caso particular de ficheo que tiene incorporada dentro de Windows una funcionalidad adicional, es el registro de Windows. La CMOS puede, en principio, utilizarse para establecer marcas perennes; pero acceso a la misma no está documentada en Windows y tendría que realizarse de manera no disciplinada, por lo que no recomendamos su utilización.

Resumen

En el ejemplo usado para la confección de este artículo, hemos usado variables en memoria, ficheros mapeados en memoria y átomos, para establecer marcas. Existen diferentes mecanismos implementados en Windows, mediante los cuales podemos crear marcas en nuestras aplicaciones. La selección del mecanismo de marca, depende de las características que deba poseer la misma. De forma resumida son:
 
Marcas temporales
Durante la ejecución del programa: Variables estáticas en memoria, ficheros mapeados en memoria (memory mapped file), etc
Durante la sesión de Windows: Atomos (atoms)
Marcas perennes
Fichero en disco
        Registro de Windows
CMOS

ir al índice


Ventanas de formas irregulares

.

El procedimiento que sigue permite hacer ventanas con formas irregulares. Esta hecho en Delphi 4, es pequeño y fácil de entender. Este procemiento se basa en definir regiones utilizando la API de Window y mezclarlas. También hay un pequeño truco para hacer un panel con forma irregular utilizando la misma filosofía..... 

 

Unit Unit1;

Interface

Uses Windows, Messages, SysUtils, Classes, 
     Graphics, Controls, Forms, Dialogs, 
     StdCtrls, Spin, Buttons, ExtCtrls;

Type
 TForm1 = class(TForm)
   BitBtn1: TBitBtn; 
   BitBtn2: TBitBtn; 
   BitBtn3: TBitBtn; 
   BitBtn4: TBitBtn; 
   SpinEdit1: TSpinEdit;
   Image1: TImage; 
   Panel1: TPanel; 
   Panel2: TPanel; 
   Image2: TImage;
   procedure FormCreate(Sender: TObject);
   procedure BitBtn1Click(Sender: TObject);
   procedure Panel2MouseDown(Sender: TObject; 
                             Button: TMouseButton; 
                             Shift: TShiftState; 
                             X, Y: Integer);
  public
   procedure WMNCHitTest(var M: TWMNCHitTest); message wm_NCHitTest;
end;

var Form1: TForm1;

Implementation

{$R *.DFM}

procedure TForm1.WMNCHitTest(var M: TWMNCHitTest);
  var P: TPoint;
begin
{ Este procedimiento permite definir lo que verá como el encabezamiento de la ventana no es nada del otro mundo solo un panel. }
  inherited;
 GetCursorPos(P);
 P.X := P.X - Left;
 P.Y := P.Y - Top;
  with Panel1 do
  if (P.X > Left) and (P.X < (Left + Width)) and  
     (P.Y > Top) and (P.Y < (Top + Height)) then
   M.Result := htCaption;
end;

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
 Close;
end;

procedure TForm1.FormCreate(Sender: TObject);
  var A: array[0..10] of TPoint;
     I: Integer;
     Rgn, Rgn1: THandle;
begin
  { Todas la funciones aquí utilizada se encuentran en la Api de Window. El procedimiento es muy fácil, solo hay que ir combinando las regiones que se crean. Crea un región eliptica. Esta funcion esta en la API de WindoW }
 Rgn := CreateEllipticRgn(0, 40, 550, 200);

  { Crea una región rectangular para combinarla con la eleptica ya definida. Esta región será el header de la ventana }
 Rgn1 := CreateRoundRectRgn(0, 0, 
                  Panel1.Width, Panel1.Height, 50, 50);
 SetWindowRgn(Panel1.Handle, Rgn1, False);

  with Panel1 do
 Rgn1 := CreateRoundRectRgn(Left, Top, 
                  Left + Width, Top + Height, 50, 50);
 CombineRgn(Rgn, Rgn, Rgn1, RGN_XOR);

  { Define las regiones para los botones de la esquinas. Son regiones rectangulares
 que toman las cordenadas de cada boton. Observe que cada vez que que se crea una
 región hay que combinarla con la primera creada.}

  for I := 0 to (ComponentCount - 1) do begin
  if (Components[I] is TBitBtn) then
   with (Components[I] as TBitBtn) do begin
    Rgn1 := CreateRectRgn(Left, Top, Left + Width, Top + Height);
    CombineRgn(Rgn, Rgn, Rgn1, RGN_XOR);
   end;
  end;

  { Crea una región vacia alrededor del Spin, prueve hacer click en esta parte
 a un icono de fondo y vera que se ejecuta lo que esta debajo.}

  with SpinEdit1 do begin
  Rgn1 := CreateRoundRectRgn(Left - 15, Top - 15, 
           (Left + Width) + 15, (Top + Height) + 15, 25, 25);
  CombineRgn(Rgn, Rgn, Rgn1, RGN_XOR);
  Rgn1 := CreateRectRgn(Left - 5, Top - 5, 
           (Left + Width) + 5, (Top + Height) + 5);
  end;
 CombineRgn(Rgn, Rgn, Rgn1, RGN_XOR);
 SetWindowRgn(Handle, Rgn, True);

  { El código que se muestra a continuación permite hacer un panel con forma irregular, el arreglo de puntos constituye las coordenadas que tendrá la forma irregular de este panel, en este caso se dibujará una flecha roja que podrß ser movida.}
 A[0] := Point(0, 25);
 A[1] := Point(25, 0);
 A[2] := Point(15, 18);
 A[3] := Point(110, 18);
 A[4] := Point(150, 0);
 A[5] := Point(125, 25);
 A[6] := Point(150, 50);
 A[7] := Point(110, 32);
 A[8] := Point(15, 32);
 A[9] := Point(25, 50);
 A[10] := Point(0, 25);

  { Se crea una región poligonal con el arreglo de puntos, se envøa la zona a donde se asignará esta región junto con la región definida. Todas estas funciones están en la API de Window}
 Rgn1 := CreatePolygonRgn(A, 11, ALTERNATE);
 SetWindowRgn(Panel2.Handle, Rgn1, True);
end;

procedure TForm1.Panel2MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  { Para el movimiento de la flecha roja, estos procedimiento se encuentran en la API de Window}
 ReleaseCapture;
 SendMessage(Panel2.Handle, WM_SYSCOMMAND, $F012, 0);
end;

End.

ir al índice


¿ Tabla o consulta ?

Y bien, vamos a comenzar a desarrollar esa gran aplicación de bases de datos. ¿Qué componente debe utilizar para acceder a sus datos: tablas o consultas?

 La respuesta depende de qué operaciones va a permitir sobre los datos, del formato y tamaño de los mismos, y de la cantidad de tiempo que quiera invertir en el proyecto. Cuando lleguemos al final de esta exposición, tendremos elementos suficientes para tomar una decisión. Si los datos están representados en una base de datos de escritorio, lo más indicado es utilizar las tablas del BDE, mediante el componente TTable de la VCL. 

Este componente accede de forma más o menos directa al fichero donde se almacenan los datos. Los registros se leen por demanda, solamente cuando son necesarios. Si usted tiene una tabla de 100.000 clientes y quiere buscar el último, el BDE realizará un par de operaciones aritméticas y le traerá precisamente el registro deseado.

 Para las bases de datos de escritorio, el uso de consultas (TQuery) es una forma indirecta de acceder a los datos. La ventaja de las consultas es que hay que programar menos, pero lo pagamos en velocidad. 

Sin embargo, las consideraciones de eficiencia cambian diametralmente cuando se trata de sistemas cliente/servidor. El enemigo fundamental de estos sistemas es la limitada capacidad de transmisión de datos de las redes actuales. Si usted monta una red local con 100 megabits de tráfico por segundo (tecnología avanzada) esa será la máxima velocidad de transmisión, independientemente de si tiene conectados 10 o 100 ordenadores a la red. Se pueden utilizar trucos: servidores con varios puertos de red, por ejemplo, pero el cuello de botella seguirá localizado en la red.


En tales condiciones, hay operaciones peligrosas para la eficiencia de la red, y la principal de ellas es la navegación indiscriminada. Gumersindo Fernández, un desarrollador de Clipper “de toda la vida” que empieza a trabajar con Delphi, ha visto un par de ejemplos en los que se utilizan rejillas de datos. Como Gumersindo sigue al pie de la letra la metodología MMFM  , pone un par de rejillas en su aplicación para resolver el mantenimiento de sus tablas de 1.000.000 de registros. ¡Y después se queja de que la aplicación va demasiado lenta!


Vaya un momento a un cajero automático, pero no a sacar dinero, sino a observar la interfaz de la aplicación que ejecuta. ¿Ve usted una rejilla por algún lugar? Cierto que se trata de una interfaz horrible, pero sirve para ilustrar una decisión extrema necesaria: Mientras más fácil, mejor. En inglés sería KISS: keep it simple, stupid!

Resumiendo, en la medida en que pueda evitar operaciones de navegación libre sobre grandes conjuntos de datos, hágalo. ¿Qué operaciones le quedan entonces? 

1. Recuperación de pequeños conjuntos de datos (últimos movimientos en la cuenta, por ejemplo).
2. Operaciones de actualización: altas, bajas y modificaciones. Bien, en los sistemas cliente/servidor estas operaciones se realizan de forma más eficiente utilizando consultas (TQuery) y procedimientos almacenados.

 Un componente TQuery puede contener lo mismo una instrucción select para recuperar datos que una instrucción update, insert, delete o cualquier otra del lenguaje de definición o control de datos.


¿Qué pasa si no puede evitar navegar de forma libre sobre determinados conjuntos de datos de medianos a grandes?   Es aquí donde realmente tendrá que decidir entre tablas y consultas. Aunque deberá esperar hasta el capítulo 25 para conocer los detalles de la implementación de tablas y consultas, he aquí un par de consejos:


1. Las consultas del BDE que contienen un select implementan la navegación en dos direcciones a nivel del cliente. Esto quiere decir que para llegar al registro 100.000 deben transferir al cliente los 99.999 registros anteriores. Por lo tanto, descarte las consultas para navegar sobre conjuntos de datos grandes. Por el contrario, al tratarse de un mecanismo relativamente directo de acceder a la inter-faz de programación del sistema cliente/servidor, el tiempo de apertura de una consulta es despreciable.
2. El BDE implementa las tablas utilizando internamente instrucciones select generadas automáticamente. El objetivo es permitir navegar sobre conjuntos de datos grandes de forma transparente, como si se tratase de una tabla en formato de escritorio. 

A pesar de la mala fama que tienen entre la comunidad de programadores cliente/servidor, en la mayoría de los casos este objetivo se logra de modo muy eficiente. La mayor desventaja es que el mecanismo de generación automático necesita información sobre el esquema de la tabla, y esta información hay que extraerla desde el servidor durante la apertura de la tabla. 

En consecuencia, abrir una tabla por primera vez es una operación costosa. No obstante, existe una técnica muy sencilla para disminuir este coste: utilice el parámetro ENABLE SCHEMA CACHE para activar la caché de esquemas en el cliente. 

Hay que aclarar también que las consultas actualizables necesitan el mismo prólogo de conexión que las tablas. Existe otro factor a tener en cuenta, y es la posibilidad de utilizar actualizaciones en caché para obligar al BDE a realizar las actualizaciones sobre consultas navegables en la forma en que deseemos.

ir al índice


¿ Qué es un ALIAS ?

 

Para "aplanar" las diferencias entre tantos formatos diferentes de bases de datos y métodos de acceso, BDE introduce los alias. 

Un alias es, sencillamente, un nombre simbólico para referirnos a una base de datos. Cuando un programa que utiliza el BDE quiere, por ejemplo, abrir una tabla, sólo tiene que especificar un alias y la tabla que quiere abrir. Entonces el Motor de Datos examina su lista de alias y puede saber en qué formato se encuentra la base de datos, y cuál es su ubicación.   

Existen dos tipos diferentes de alias: los alias persistentes y los alias locales, o de sesión. Los alias persistentes se crean por lo general con el Administrador del BDE, y pueden utilizarse por cualquier aplicación que se ejecute en la misma máquina. 

Los alias locales son creados mediante llamadas al BDE realizadas desde una aplicación, y son visibles solamente dentro de la misma. La VCL facilita esta tarea mediante componentes de alto nivel, en concreto mediante la clase TDatabase.   

Los alias ofrecen a la aplicación que los utiliza independencia con respecto al formato de los datos y su ubicación. Esto vale sobre todo para los alias persistentes, creados con el BDE. Se puede estar desarrollando una aplicación  que trabaje con tablas del alias de datos. En la máquina, este alias está basado en el controlador de Paradox, y las tablas pueden encontrarse en un determinado directorio del disco local. El destino final de la aplicación, en cambio, puede ser una máquina en que el alias de datos haya sido definido como una base de datos de Oracle, situada en tal servidor y con tal protocolo de conexión. 

A nivel de aplicación no necesitaremos cambio alguno para que se ejecute en las nuevas condiciones. A partir de la versión 4.0 del BDE, se introducen los alias virtuales, que corresponden a los nombres de fuentes de datos de ODBC, conocidos como DSN (data source names). Una aplicación puede utilizar entonces directamente el nombre de un DSN como si fuera un alias nativo del BDE.

ir al índice


Triggers y Vistas

... triggers y vistas. ¿Y qué tiene que ver la gimnasia con la magnesia? Tiene que ver, y mucho, con la única condición de que estemos trabajando con Oracle o InterBase. La técnica que le voy a explicar es muy sencilla, y pudiera resumirla en un párrafo y un diagrama de sintaxis. Pero quiero también que comprenda todo el partido que se le puede sacar a estos recursos. Tendremos que rebobinar la película y comenzar desde la primera escena.

TODO COMENZO POR...

...las actualizaciones en caché. Muchos programadores piensan que la principal utilidad de las actualizaciones en caché es agrupar determinado número de actualizaciones y enviarlas de golpe al servidor. Los manuales de Borland nos explican que de este modo ahorramos espacio en la transmisión de datos al servidor, y que así podemos disminuir el tráfico en red. Todo esto es verdad, pero no toda la verdad; ni siquiera es la parte más importante de la verdad.

Probablemente, el mayor atractivo de las actualizaciones en caché para el programador experto sea el uso del componente TUpdateSQL y del evento OnUpdateRecord. Estoy seguro de que alguna vez se habrá tropezado con uno de esos profetas que predican: "No hay más Conjunto de Datos que TQuery". Casi siempre, sin embargo, a estos personajes se les olvida aclarar qué condiciones son necesarias para que un componente TQuery se comporte mejor que un TTable.

Supongamos que estamos programando el mantenimiento de una tabla. Traemos una consulta Query1 al formulario, le asignamos un "select * from Tabla" en su propiedad SQL, asignamos True a la propiedad RequestLive y le enchufamos los correspondientes controles visuales: rejillas, cuadros de edición o lo que se le antoje. ¿Estamos haciéndolo bien? No: estamos haciéndolo fatal. Si la consulta no tuviese RequestLive activa, su apertura sería realmente más rápida que la de un TTable. Pero al querer que su resultado sea actualizable, hemos instruido al BDE para que pida información a la base de datos acerca de qué columnas contiene el resultado, de qué tipos, cuál es la clave primaria de la tabla subyacente, etc, etc. La información se solicita a las tablas del catálogo de la base de datos, y es lo que habitualmente hacen los componentes TTable.. Entonces, las consultas actualizables tardarán tanto en abrirse como las tablas, y tendrán encima inconvenientes añadidos
1.    La navegación irá mal. Para llegar al registro 1.000 habrá que pasar por los 999 anteriores. Las operaciones Last, Locate y todas las que utilizan filtros serán lentas.
2.     Se presentarán problemas con la cantidad de registros en el lado cliente. Usted tiene una consulta con cinco registros, digamos por simplificar que en una rejilla. Añade entonces un nuevo registro. ¿Se verán los seis registros en la pantalla? No: se seguirán mostrando cinco registros. Puede que desaparezca el nuevo, o alguno de los anteriores.
3.     El método Refresh no funciona con las consultas. En caso contrario, el problema anterior no sería tan grave. Si queremos "refrescar" una consulta, debemos cerrarla y volverla a abrir. Como perdemos la posición original dentro del cursor, tenemos que regresar a la fila activa mediante un bookmark o una llamada a Locate. Y ya sabemos que ambas operaciones son costosas cuando se aplican a una consulta.
4.    Una transacción confirmada mientras una consulta está abierta obligará a que todos los registros de la consulta sean leídos por el cliente FetchAll). Da lo mismo si se trata de una transacción implícita o explícita. Hay un par de trucos para evitar este problema, pero solamente funcionan para algunos controladores y en algunos casos.

ACTUALIZACIONES EN CACHE, ¡AL RESCATE!

Si las consultas dan tantos dolores de cabeza, ¿cómo es posible que algún programador siga utilizándolas? Es aquí donde comenzamos a aclarar las condiciones necesarias para trabajar con estos componentes. En primer lugar, la mayoría de los problemas enumerados dejan de serlo si la consulta devuelve pocos registros. Ahí va entonces la primera regla:

REGLA NUMERO 1 PARA CONSULTAS
Las consultas que utilicemos para navegación y mantenimiento deben limitarse a pocos registros.

Pero seguimos sin resolver el problema de la lectura del catálogo para que el BDE genere las actualizaciones, y el mal comportamiento del cursor cuando se inserta un registro. Para evitar el largo protocolo inicial de consultas al catálogo podemos activar la opción ENABLE SCHEMA CACHE del Motor de Datos. Dicho sea de paso: así también se resuelve el problema similar de las tablas. Sin embargo, se trata de una solución con sus propios límites, pues por motivos para mí desconocidos, el BDE solamente puede manejar hasta 32 tablas en la caché de esquemas. Un diseño típico de bases de datos tiene muchas más tablas que esta cantidad. ¿Y ahora qué, listillo?

Hagamos una reflexión: ¿por qué el BDE hace preguntas a la base de datos sobre las tablas, en vez de preguntarnos a nosotros? No es una idea descabellada. Puedo imaginar un componente derivado de TDataSet, que tuviera propiedades en tiempo de diseño como PrimaryKey, para especificar la clave primaria, InsertSQL, para indicar la instrucción SQL que queremos lanzar cuando se realiza una inserción, ModifySQL, DeleteSQL, etc. De hecho, ¡existen componentes en este estilo! ¿Ha oído hablar sobre los componentes de acceso directo a InterBase FreeIBComponents? El componente que sustituye a los conjuntos de datos confía en que el programador le indique cómo quiere actualizar cada registro leído por una consulta. Así se ahorra la interrogación inicial de la base de datos.

ACERCA DE FreeIBComponents
Utilice FreeIBComponents, y otros componentes similares, solamente si su aplicación está completamente basada en consultas, en vez de tablas. Los conjuntos de datos de FIB implementan la navegación con el mismo sistema de TQuery. Así que no se le ocurra abrir de golpe una tabla de 10.000 registros con este componente.

Para poder tener este grado de control con los conjuntos de datos del BDE es necesario seguir estos dos pasos:

1.    Activar las actualizaciones en caché (CachedUpdates:= True).
2.     Para quitarle el control al BDE sobre las instrucciones de actualización generadas, hay que asociar un componente TUpdateSQL a la propiedad UpdateObject del conjunto de datos; esto, si basta con una sola instrucción SQL para las actualizaciones. Si el algoritmo de actualización es más complejo, debemos interceptar el evento OnUpdateRecord

En "La Cara Oculta de Delphi 4" (capítulo 32) y en la de "... C++ Builder" (cap. 31), hay unos cuantos ejemplos de esta técnica. Lo que nos interesa en este momento es que, si el conjunto de datos manipulado de este modo es un TQuery, no se produce la larga consulta inicial sobre el catálogo de la bases de datos. Desgraciadamente, esta optimización no se produce con TTable, pues el BDE sigue necesitando la información de catálogo para implementar eficientemente la navegación.

REGLA NUMERO 2 PARA CONSULTAS
Los tipos duros no dejan a una consulta que genere sus propias instrucciones de actualización. En vez de eso, activan las actualizaciones en caché y suministran componentes TUpdateSQL, o interceptan el evento OnUpdateRecord

CONTROL EN EL LADO SQL DE LA VIDA

Realmente, estamos usurpando en Delphi algunas atribuciones más apropiadas para que ser desempeñadas por el servidor SQL.

    create view PedidoCliente as
        select P.Numero, P.Fecha, P.Enviado, C.Nombre
        from   Pedidos P inner join Clientes C
               on (P.Cliente = C.Codigo)

Esta consulta no es actualizable, al menos en InterBase. Sin embargo, la columna Nombre solamente cumple una función decorativa y podemos exigir que no se pueda modificar su valor. En tal caso, existe una correspondencia biunívoca entre las filas de PedidoCliente y de la tabla base Pedidos, considerando que todo pedido tiene un cliente asociado. Entonces, una modificación sobre un registro de PedidoCliente puede traducirse automáticamente en una modificación sobre Pedidos. ¿Cómo le indicamos a InterBase nuestras "ideas" acerca de las actualizaciones sobre PedidoCliente ? Sencillamente creando triggers para la vista en cuestión:

 set term !;
 create trigger UpdPedidoCliente for PedidoCliente
    active before update as
begin  Pedidos
   set    Numero = new.Numero, Fecha = new.Fecha, Enviado = new.Enviado
   where  Numero = old.Numero and Fecha = old.Fecha and Enviado = old.Enviado 
end!
 
create trigger UpdPedidoCliente for PedidoCliente
    active before delete as
begin
    delete from Pedidos
    where   Numero = old.Numero and Fecha = old.Fecha and Enviado = old.Enviado; 
end!
 
create exception InsercionPedidoCliente "No se puede insertar en esta vista"!
 
create trigger UpdPedidoCliente for PedidoCliente
    active before insert as
begin
    exception InsercionPedidoCliente;
end!

Observe que un intento de inserción debe generar una excepción. También observe que, por simplicidad, los borrados y modificaciones utilizan todos los valores anteriores de los campos, como si se tratase de un TDataSet con el valor upWhereAll en su propiedad UpdateMode. Esta ha sido una decisión arbitraria, pues el programador tiene la libertad de programar el trigger según considere pertinente. Ya puestos en el asunto, podíamos haber programado el trigger de modificación así:

set term !;
create exception NoPermitida "Operación no permitida"!
create trigger UpdPedidoCliente for PedidoCliente
    active before update as
begin
   if (old.Numero <> new.Numero) then
    if (old.Numero <> new.Numero) then
       exception NoPermitida;
    update Pedidos
    set    Fecha = new.Fecha, Enviado = new.Enviado
    where  Numero = old.Numero;
    if (old.Nombre <> new.Nombre) then
        update Clientes
        set    Nombre = new.Nombre
        where Nombre = old.Nombre;
end!

Con esto hemos permitido que el usuario pueda corregir el nombre del cliente si detecta, por ejemplo, una falta de ortografía. Sin embargo, del mismo modo se puede establecer que una modificación en el nombre del cliente constituya una asignación del pedido a otro cliente diferente:

create trigger UpdPedidoCliente for PedidoCliente
   active before update as
declare variable NuevoCliente integer;
begin
   if (old.Numero <> new.Numero) then
       exception NoPermitida;
    update Pedidos
    set     Fecha = new.Fecha, Enviado = new.Enviado
    where Numero = old.Numero;
   if (old.Nombre <> new.Nombre) then
   begin
       select Codigo
       from  Clientes
       where Nombre = new.Nombre
       into  :NuevoCliente;
        if (NuevoCliente is null) then
           exception ClienteNoExiste;
       update Pedidos
        set    Cliente = :NuevoCliente
        where  Numero = old.Numero;
    end
end!
 
ADVERTENCIA IMPORTANTE
Esta técnica no nos evita, de todos modos, el uso de actualizaciones en caché y objetos de actualización. El BDE trata a las vistas del modo que a las consultas y tendremos, por lo tanto, los mismos problemas de navegación y apertura simpre. Sin embargo, nos ahorraremos el tener que especificar un comportamiento tan complejo como el anterior utlizando Delphi. Las reglas de empresa siguen su trayectoria habitual: hacia el servidor, siempre que se pueda...

  EN VEZ DE...

... es la traducción de instead of. Y éste es el nombre de la cláusula que necesita Oracle para poder definir un trigger sobre una vista. Por ejemplo:

 

create trigger UpdPedidoCliente
   instead of update on PedidoCliente
   for each row
declare
   NuevoCliente integer;
begin
   if :old.Numero <> :new.Numero then
       raise_application_error(-20001, 'Operación no permitida');
   end if;
   update Pedidos
   set   Fecha = :new.Fecha, Enviado = :new.Enviado
   where Numero = :old.Numero;
   if :old.Nombre <> :new.Nombre then
       select Codigo
       into    NuevoCliente
       from  Clientes
       where Nombre = :new.Nombre;
       if NuevoCliente is null then
            raise_application_error(-20001, 'El cliente no existe');
       end if;
       update  Pedidos
       set        Cliente = NuevoCliente
       where    Numero = :old.Numero;
   end if;
end;

ir al índice


Acción inmediata

Había liquidado los últimos fallos de aquel monstruoso programa y lo estaba mostrando, lleno de orgullo, a uno de sus futuros usuarios. Este dedicaba su atención a una ventana mediante la cual podía contratar determinados servicios de la compañía. El registro del producto contenía un campo lógico, que al ser activado debía añadir un seguro mensual al contrato; el costo del seguro debía reflejarse en el importe total de la contratación. Naturalmente, la edición de dicho campo se realizaba mediante un componente TDBCheckBox.

Pepe, llamémosle así piadosamente, decidido a hacer estallar mi aplicación, seleccionó precisamente la casilla mencionada, después de bregar fatigosamente con el insumiso ratón. Pulsó sobre el control y me miró con insolencia: "Oye, esto no calcula el seguro". Contuve mis deseos de arrancarle la lengua y arrojarla a la perra del portero, y me concentré en el monitor ... ¡oops! ... ahí pasaba algo muy raro. Efectivamente, la opción estaba marcada, ¡pero el importe seguía siendo el mismo! Le arrebaté el ratón y cambié el estado del control unas cuantas veces, pero aquello seguía más tieso que la momia de Lenin. Tras unos insoportables segundos de sudores fríos, comprendí lo que pasaba. "Pepe, viejo" - le dije, dándole a su nombre la misma entonación que la usual en el adjetivo 'comemierda' - "tienes que pulsar la tabulación para pasar al siguiente control". Me miró con sorna, respondiendo: "tú te confundiste también". Y en lo más íntimo de mi encéfalo tuve que reconocer que, por una vez en la vida, aquel espécimen de usuario tenía razón... 

¿CUANDO SE ACTUALIZA UN CAMPO?

La anécdota ficticia que acabo de relatar (¡ninguno de mis usuarios se llama Pepe!) demuestra un comportamiento anómalo de TDBCheckBox, pero que también es padecido por los restantes controles de datos de Delphi. Digámoslo en pocas palabras:

DEL CONTROL DATA-AWARE AL CAMPO
Los cambios realizados en un control de datos de la VCL tienen lugar, por lo general, sólo cuando abandonamos el control.

Es decir: nos ponemos a teclear sobre un TDBEdit, pero mientras tecleamos el contenido original del campo asociado sigue siendo el mismo. Tenemos que pasar al siguiente control dentro del formulario para que las modificaciones surtan efecto. En ese momento es que se disparan, además, los eventos OnValidate y OnChange del campo, si es que tienen código asociado.

¿Por quéeeee? Aceptemos lo contrario: que cada vez que cambie el contenido del editor, se modifique el campo simultáneamente. Esto es posible y sensato si el campo editado es numérico, o si es un nombre, por mencionar un par de casos. Pero no es factible cuando el campo representa una fecha; la cadena "12/", a pesar de nuestras mejores intenciones de completarla en breve, no representa una fecha correcta.

Así que la culpa de todo la tiene el control TDBEdit. Es lógico que suceda lo mismo con un TDBComboBox, pues en el fondo se trata simplemente de un TDBEdit con cuernos ... quiero decir, con lista desplegable. Pero no es tan evidente cuando se trata de un TDBCheckBox, o de un TDBLookupComboBox, pues estos controles siempre (o casi siempre) contienen un valor correcto para el campo que representan. Irónicamente, el culpable TDBEdit ofrece un mecanismo adicional para que el usuario diga "lo que he tecleado hasta aquí vale", sin necesidad de pasar al control siguiente. Si tecleamos Intro sobre el control, su texto pasa inmediatamente al campo.

¿COMO SE ACTUALIZA UN CAMPO?

Ya puestos, mostremos el código que realiza la asignación al campo en los componentes mencionados. El siguiente método, por ejemplo, corresponde a un TDBEdit

procedure TDBEdit.CMExit(var Message: TCMExit);
begin
  try
     FDataLink.UpdateRecord;
  except
      SelectAll;
      SetFocus;
      raise;
  end;
   SetFocused(False);
   CheckCursor;
   DoExit;
end;

La llamada al método UpdateRecord del FDataLink desencadena el proceso de asignación al campo. Esta llamada provoca que el componente de clase TFieldDataLink enganchado en FDataLink envíe una notificación a todos los controles asociados al mismo conjunto de datos, para que todos ellos vuelquen sus contenidos en la fila activa. Los controles escuchan la notificación interceptando el evento interno OnUpdateData del FDataLink. El código correspondiente en el editor es:

constructor TDBEdit.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  //... más instrucciones ...
  FDataLink := TFieldDataLink.Create;
  // ... muchas más instrucciones ...
  FDataLink.OnUpdateData := UpdateData;
end;
 
procedure TDBEdit.UpdateData(Sender: TObject);
begin
  ValidateEdit;
  FDataLink.Field.Text := Text; // ¡Esto es lo que buscábamos!
end;

UNA SOLUCION PARA LOS CHECK BOXES

Mi solución preferida es crear un componente; cualquier otra implicaría añadir chapuzas para simular el cambio del foco del teclado, al menos hasta donde tengo noticia. En el siguiente componente (un derivado de TCustomCheckBox) que simula parte del comportamiento de TDBCheckBox y añade algunas características adicionales, hay que redefinir el método Toggle, que se ejecuta cada vez que cambia el estado del control:

procedure TimDBCheckBox.Toggle;
begin
  if FDataLink.Edit then
  begin
     inherited Toggle;
     FDataLink.Modified;
      if FImmediate then   // Nueva línea
         FDataLink.UpdateRecord;  // Nueva línea
   end;
end;

Immediate es una propiedad de tipo Boolean, y sirve para activar el modo de asignación inmediata a campos. 

ir al índice


Vista preliminar de la impresión

Como programador, siempre he prestado una gran atención a las pantallas que se muestran en los programas que he desarrollado. Y mucho tiempo, he invertido en lograr el balance adecuado entre la estética de la impresión visual y el contenido de la pantalla. Mi talón de Aquiles, han sido las impresiones.

En la pantalla, disponemos de una serie de posibilidades que nos permiten presentar ordenada y lógicamente la funcionalidad y la salida de los programas. Me refiero a los controles de deslizamiento (scroll bars), ventanas de pestañas (Tabs controls), animaciones, etc. y abstraernos un poco de las dimensiones físicas de la superficie donde dibujamos.

Ah, las impresiones! sólo disponemos del área de una hoja de papel, no permite interación de usuario y el posicionamiento de los objetos es absoluto. Cada cosa tiene que ir en un lugar y para colmo al dibujar sobre el papel hay que usar funciones nuevas, StartDoc, StartPage, EndPage, etc, para dar entrada al arsenal de funciones del GDI.

Pero mi mayor tortura, era cuando tenía que esperar que mi lenta impresora de matriz de puntos, terminara su trabajo, para finalmente burlarse, mostrándome una página impresa, que no era satisfactoria.

Así fue, como surgió la idea de emular el impresor y posteriormente perfeccionarla, con la vista preliminar de la impresión. Desde entonces, me he ahorrado mucho tiempo en la espera de que termine una impresión y depurar los programas y ahorrando papel.

El requisito básico para usar este artículo en nuestros programas, es que, en el mismo, tengamos implementada la salida de por la impresora, utilizando el objeto Printer.

La vista preliminar de la impresión la implementaremos de la siguiente forma:

  • Implementaremos un objeto especializado de la clase TPrinter, TWMFPrinter, donde la superficie de dibujo, en vez de papel, sea el Canvas de un Metafile de Windows. TWMFPrinter reproduce a la resolución de la pantalla, las principales propiedades y métodos de la verdadera impresora.
  • Después, crearemos una forma, que permitirá visualizar y navegar por la diferentes páginas impresas por objeto TWMFPrinter.

Dos palabras sobre el Metafile

El metafile, es un objeto gráfico de Windows sobre el cual podemos dibujar. En un metafile, no se graba el dibujo directamente; sino una cadena de comandos con la se genera el dibujo.

Existen dos versiones de los metafiles en Windows. La estandar, que es conocida desde el Windows 3.0 y el metafile mejorado (enhanced metafile) nacida para la plataforma Win32.

El formato de fichero del metafile mejorado, consiste en una cabecera, una tabla de manipuladores de objetos del GDI, una paleta privada y un arreglo de los registros escritos al metafile. La principal característica de los metafiles mejorados, es su independencia del dispositivo de impresión.

El lector interado, puede buscar en la documentación del GDI, el API suministrado para la manipulación de metafiles. Dentro del contexto de este artículo, esto es intrascendente y nosotros haremos uso de la clase TMetafile de la VCL, que encapsula de una forma muy cómoda, las operaciones con metafiles.

El objeto TWMFPrinter

El objeto TWMFPrinter es una clase especializada de TPrinter cuya superficie de dibujo es el Canvas de un metafile, establecido con las dimensiones del papel de la impresora; pero con la resolución de la pantalla.

La figura muestra la definición de la clase TWMFPrinter.

Type
  TWMFPrinter = class(TPrinter)
  private
    // Listado de paginas impresas en formato Metafile
    FwmfList   : TList;
    FPrnCanvas : TMetafileCanvas;
    function     GetPrinterInfo(info: Integer): Integer;
    procedure    ClearPages;
    function     GetPage(AIndex: Integer): TMetafile;
    function     GetPageCount: Integer;
  protected
    function     GetHandle     : HDC;     override;
    function     GetPageHeight : Integer; override;
    function     GetPageWidth  : Integer; override;
    function     GetCanvas     : TCanvas; override;
  public
    constructor Create;
    destructor   Destroy;  override;
    procedure    Abort;    override;
    procedure    BeginDoc; override;
    procedure    EndDoc;   override;
    procedure    NewPage;  override;

    property     PagesCount : Integer read GetPageCount;
    property     Pages[AIndex: Integer]: TMetafile read GetPage;
    property     PagesList  : TList read FwmfList;

    property     PageWidth  : Integer read GetPageWidth;
    property     PageHeight : Integer read GetPageHeight;
    property     Canvas     : TCanvas read GetCanvas;

    property     Handle;
    property     Aborted;
    property     Capabilities;
    property     Copies;
    property     Fonts;
    property     Orientation;
    property     PageNumber;
    property     PrinterIndex;
    property     Printing;
    property     Printers;
    property     Title;
  end;

Desfortunadamente, algunas funciones y procedimientos de la clase TPrinter no fueron declarados virtuales y esto no permite una adecuada especializacion de la clase. Tomemos por ejemplo, el método BeginDoc. Cuando cualquier componente de la VLC que implemente una impresión utilizando el objeto Printer, ie TRichEdit, llama el método BeginDoc, se ejecuta el método de la clase TPrinter, que es la clase que conoce la componente y no el de la clase especializada TWMFPrinter.

Por eso, hemos modificado la clase TPrinter, para hacer virtuales aquellos métodos que son requeridos para especializar la misma. Esto, tiene la desventaja de que nos obliga a recompilar los paquetes de la VCL; pero es el precio, en esta implementación, que debemos pagar, para proveer a nuestros programas de la facilidad de vista preliminar de la impresión. 

Advertencia: Antes de sustituir el fichero Printers.pas en las fuentes de la VCL, por el suministrado en este trabajo, haga una salva de respaldo del mismo.

De modo, que debemos virtualizar en la clase TPrinter, todo lo relacionado con la manipulacion de la superficie de dibujo de la impresora. Es decir, el calculo del ancho y alto del papel, la obtención del Canvas y dispositivo de contexto (HDC), el comienzo y terminacion de documento de impresión, la creación de nuevas páginas y la cancelación durante el trabajo de impresión.

La figura muestras las líneas de código modificadas de la clase TPrinter, marcadas con en comentario con tres asteríscos seguidos, o sea //***

  TPrinter = class(TObject)
  private
    FCanvas: TCanvas;
    FFonts: TStrings;
    FPageNumber: Integer;
    FPrinters: TStrings;
    FPrinterIndex: Integer;
    FTitle: string;
    FPrinting: Boolean;
    FAborted: Boolean;
    FCapabilities: TPrinterCapabilities;
    State: TPrinterState;
    DC: HDC;
    DevMode: PDeviceMode;
    DeviceMode: THandle;
    FPrinterHandle: THandle;
    procedure SetState(Value: TPrinterState);
//*** function GetCanvas: TCanvas;
    function GetNumCopies: Integer;
    function GetFonts: TStrings;
//*** function GetHandle: HDC;
    function GetOrientation: TPrinterOrientation;
    function GetPrinterIndex: Integer;
    procedure SetPrinterCapabilities(Value: Integer);
    procedure SetPrinterIndex(Value: Integer);
    function GetPrinters: TStrings;
    procedure SetNumCopies(Value: Integer);
    procedure SetOrientation(Value: TPrinterOrientation);
    procedure SetToDefaultPrinter;
    procedure CheckPrinting(Value: Boolean);
    procedure FreePrinters;
    procedure FreeFonts;
  protected
    function GetHandle: HDC; virtual;         //***
    function GetPageHeight: Integer; virtual; //***
    function GetPageWidth: Integer; virtual//***
    function GetCanvas: TCanvas; virtual;     //***
  public
    constructor Create;
    destructor Destroy; override;
    procedure Abort; virtual;     //***
    procedure BeginDoc;  virtual; //***
    procedure EndDoc;  virtual;   //***
    procedure NewPage;  virtual//***
    procedure GetPrinter(ADevice, ADriver, APort: PChar; 
                var ADeviceMode: THandle);
    procedure SetPrinter(ADevice, ADriver, APort: PChar;
                ADeviceMode: THandle);
    property Aborted: Boolean read FAborted;
    property Canvas: TCanvas read GetCanvas;
    property Capabilities: TPrinterCapabilities
                           read FCapabilities;
    property Copies: Integer read GetNumCopies
                           write SetNumCopies;
    property Fonts: TStrings read GetFonts;
    property Handle: HDC read GetHandle;
    property Orientation: TPrinterOrientation
                          read GetOrientation
                          write SetOrientation;
    property PageHeight: Integer read GetPageHeight;
    property PageWidth: Integer read GetPageWidth;
    property PageNumber: Integer read FPageNumber;
    property PrinterIndex: Integer read GetPrinterIndex
                                   write SetPrinterIndex;
    property Printing: Boolean read FPrinting;
    property Printers: TStrings read GetPrinters;
    property Title: string read FTitle write FTitle;
  end;

Uso del objeto TWMFPrinter

Ahora, con una instancia de la clase TWMFPrinter, se trabaja igual que si estuvieramos usando el objeto Printer. En general el procedimiento es el siguiente:

  • Se crea una instancia de TWMFPrinter.
  • Mediante el procedimiento SetPrinter, definido en la unidad Printes.pas, se establece este objeto como la impresora.
  • Comenzamos la impresión del documento, llamando Printer.BeginDoc
  • Efectuamos propiamente el código de impresión del documento,
    • Llamar el procedimiento Printer.NewPage, cada vez que efectuemos un cambio de página.
    • Usar los métodos del objeto Printer.Canvas para establecer las características de la impresion (Pen, Brush, etc). Igualmente se puede utilizar Printer.Canvas.Handle para efectuar directamente llamadas a las funciones del GDI que utilicen el dispositivo de contexto.
    • Usar los métodos Printer.PageWidth y Printer.PageHeight para localizar correctamente nuestro dibujo en el Canvas.
    • Terminamos la impresión del documento llamado el método Printer.EndDoc.
  • Se puede, ahora, realizar cualquier manipulación del listado de páginas impresas en el objeto TWMFPrinter (salvarlas en un fichero, mostrarlas en la pantalla como un una vista preliminar de la impresión, etc), cada una de las cuales es un objeto TMetafile.
  • Finalmente, no olvide liberar la instancia del objeto TWMFPrinter.

La figura muestra una posible implementacion del uso de TWMFPrinter para imprimir el texto contenido en un control TRichEdit.

procedure TForm1.btPrintToRichEditClick(Sender: TObject);
var
  ThePrinter : TWMFPrinter;
  j : Integer;
const
  sCaption = 'Ejemplo de impresión: Una página rayada.';
begin
  ThePrinter := TWMFPrinter.Create;
  try
    SetPrinter( ThePrinter );
    // El objeto RichEdit usa Printer para imprimir el documento
    RichEdit1.Print(sCaption);
    // Salvar la primera página impresa.
    TMetafile( ThePrinter.PagesList[0] ).SaveToFile('imgprn.wmf');
  finally
    SetPrinter( nil );
    ThePrinter.Free;
  end;
end;

 

Después que ha terminado la impresión, la instancia del objeto TWMFPrinter contiene una lista de las páginas impresas, cada una de las cuales es una instancia de la clase TMetafile. Estas páginas pueden ser manipuladas con las propiedades PagesCount, Pages y PagesList.

La vista preliminar de la impresión

Ahora, llegado a este punto, mostar una vista preliminar de la impresión es muy fácil. Cada página es un objeto gráfico del tipo TMetafile. Sólo tenemos que asignarlo a una instancia del componente TImage, para verlo. Con estos criterios hemos elaborado una forma para navegar las páginas de la impresión, visualizadas en la pantalla. El ejemplo es tan sencillo, que el lector encontrará más fácil leer el código en el ejemplo suministrado con este artículo, que mediante una descripción textual del mismo.

Básicamente, se ha construido una forma que contiene un componente TImage, donde se muestra la vista preliminar de una página de impresa. Se han adicionado cuatro botones que permiten cambiar la página que es mostrada en el control. Y, se han adicionado 4 botones, que permiten cambiar la ampliación de la vista de la impresion preliminar, para visualizar el conjunto de la página o adentrarnos en los detalles del papel.

La figura muestra la forma de la vista preliminar de la impresión de texto contenido en un control TRichEdit.

Para terminar

La técnica que he explicado, comencé a utilizarla en un programa que implementaba un motor gráfico con un uso intensivo a casi todas las funciones del GDI. Me funcionó tan bien, que no he podido prescindir de ella, en todo lo que he hecho posteriormente; pero esto requiere un paso de osadía por parte suya: Sustituir la fuente de la unidad PRINTERS.PAS, suministrada con Delphi.

ir al índice


Bitmaps transparentes en Delphi

Primero que todo vamos a definir a que le vamos a llamar bitmap transparente. Así vamos a denominar a un bitmap a través del cual una parte del fondo puede ser vista. Para comprender mejor veamos la Figura 1. Si nos fijamos la imagen resultante es el equivalente a si se hubiera ignorado el color negro al dibujar la segunda imagen sobre la primera, aunque no es precisamente esto lo que se hace para lograr la transparencia.

 
   

 Figura 1

 Ahora vamos a definir a que llamaremos bitmap semitransparente, nos referiremos así a una imagen que al ser puesta sobre otra tiene todos sus puntos semitransparentes, es decir, que se puede ver parcialmente esta y el fondo sobre el que se dibujó. En la Figura 2 esta hay un ejemplo de este tipo de transparencia.

 

+ =

 Figura 2

  A continuación vamos a ver como implementar cada una de esas dos transparencias. Comencemos por la primera, los bitmaps con cierto color completamente transparente.

 Bitmaps transparentes

  El Delphi trae implementado este tipo de transparencias, así que aparentemente pudiéramos ignorar que es lo que se hace internamente y usar la transparencia implementada por el este, pero no siempre es lo ideal. El Delphi ha encapsulado muchas cosas (por suerte lo hizo con COM) lo cual facilita el trabajo al programador y le permite a este elaborar un proyecto mas rápidamente evitando tener que usar directamente las APIs(Application Programming Interface) del sistema, sin embargo esto sacrifica la posibilidad de tener un código óptimo, tanto en tamaño como en velocidad, por lo que cuando queremos hacer una aplicación que requiera de mayor velocidad de ejecución, no nos queda mas remedio que usar directamente las APIs del sistema y programar a un nivel un poco mas bajo.

  Entonces el problema esta en aprovechar al máximo las posibilidades que nos ofrece el sistema, usando lo que este nos proporciona para manipular bitmaps.

  Los que programan solamente para Windows98, NT 5.0, o versiones superiores de estos, no necesitan de implementar las transparencias de la forma que lo haremos aquí, ya que a partir de Win98 y el NT 5.0 la GDI (Graphics Device Interface) incorpora funciones para obtener Bitmaps transparentes y semitransparentes. Claro esta que no es muy comercial hacer aplicaciones que no funcionen en Windows95 y NT 4.0, así que seguimos con nuestros bitmaps transparentes.

Necesitaremos usar algunas de las funciones de la GDI que nos proporciona el sistema. En el código de ejemplo están solo ligeramente comentadas, ya que no es mi objetivo hacer de este articulo una ayuda de la API del Windows, los interesados en ver estas con más detalles pueden consultar la ayuda "Win32 Programer Reference" (Win32.hlp) o el MSDN (Microsoft Developer Network).

  Para lograr la transparencia, necesitamos un bitmap máscara en el cual los puntos transparentes sean de color blanco puro ( RGB(255,255,255) ) y los que serán dibujados, de color negro puro ( RGB(0,0,0) ). Primero creamos un bitmap monocromático y luego al bitmap que queremos dibujar transparente (Bitmap Original) le ponemos de color de fondo el color que queremos hacer transparente y finalmente lo copiamos (dibujamos) sobre el monocromático. Ver el fuente del programa para analizar los detalles.

  Ahora que ya tenemos la máscara lo que necesitamos para dibujar el bitmap transparente es hacer tres operaciones de dibujado sobre la superficie de destino haciendo ciertas operaciones. Primeramente dibujamos el bitmap original sobre el destino haciendo un XOR entre los puntos de este y los puntos del destino. El segundo paso es dibujar la máscara sobre el destino haciendo un AND entre los puntos de esta y los del destino, lo que nos deja los puntos del destino correspondientes a los transparentes del bitmap original sin cambio y los que serán ocupados por la parte no transparente quedan en negro sobre la superficie de destino. Finalmente el tercer paso es hacer otro XOR entre los puntos del bitmap original y el destino, lo que nos restaura los puntos transparentes a su estado inicial y los puntos con el color negro son ocupados con los que son dibujados del bitmap original.

  Es decir, las operaciones realizadas sobre cada uno de los bits son:

  •  Para los correspondientes a puntos transparentes :
    ((BDest  XOR  BOrig)  AND  1)  XOR  BOrig  =  BDest  XOR  BOrig  XOR  BOrig  BDest
  • Para los correspondientes a puntos donde se va a dibujar :
    ((BDest  XOR  BOrig)  AND  0)  XOR  BOrig  =  0  XOR  BOrig  =  BOrig

  Como podemos ver se obtiene el resultado deseado. En total necesitamos hacer cuatro llamadas a la función BitBlt, es decir dibujar cuatro veces la imagen, así que podríamos pensar que es cuatro veces mas lento que dibujarlo una sola vez, pero en realidad no es así, ya que dibujar un bitmap monocromático es mucho más rápido que dibujar un bitmap donde se usen 8 bits para representar cada color.

  De todas formas podríamos reducir a tres el número de llamadas a BitBlt, si nos aseguramos que el color que queremos hacer transparente es el negro puro. Entonces las operaciones serian solamente crear la máscara, dibujar la máscara sobre el fondo haciendo un AND con este y luego dibujar la imagen haciendo un OR con el fondo. Ahora las operaciones sobre cada bit serían:

  • Para los correspondientes a puntos transparentes :
    (BDest  AND  1)  OR  BOrig  =  BDest  OR  BOrig  =  BDest  OR  0  =  BDest
  • Para los correspondientes a puntos donde se va a dibujar  :
    (BDest  AND  0)  OR  BOrig  =  0  OR  BOrig  = BOrig

  El dibujado que nos ahorramos con el fondo negro es uno de los dos más lentos, el rendimiento en cuanto a velocidad aumenta en un 35-40% comparándolo con cuando hacemos cuatro, así que vale la pena usar el color negro puro como transparente. Lo difícil ahora seria convencer a los diseñadores de que siempre tomaran el negro puro como color transparente, pero hasta donde yo conozco eso es imposible.

Ejemplo 1

procedure TransBmp (bmp1, bmp2 : TBitmap);
   var
        Alto, Ancho                    : integer;
        ScreenDC, MaskDC,
                  SrcDC, DstDC     : HDC; 
        BmpMono, DstBmp       : HBitmap;
begin
   // Este codigo dibuja el bitmap "bmp2" sobre el
   // bitmap "bmp1" tomando como color trasnparente
   // el color del punto más arriba más a la izquierda.   
   // Se asume que ambos bitmaps son de igual tamaño
   Alto      := bmp1.Width;
   Ancho  := bmp1.Height;
   DstDC := bmp1.Canvas.Handle;
   SrcDC := bmp2.Canvas.Handle;
   // crear bitmap monocromático máscara :
   ScreenDC  := GetDC (0);
   BmpMono := CreateBitmap (Ancho, Alto, 1, 1, nil);
   MaskDC   := CreateCompatibleDC (ScreenDC);
   SelectObject (MaskDC, BmpMono);
   SetBkColor (SrcDC , bmp2.Canvas.Pixels[1,1]);
   BitBlt (MaskDC, 0, 0, Ancho, Alto, SrcDC, 0, 0, SRCCOPY); // COPY
   // dibujar imagen transparente :
   SetBkColor(DstDC, RGB(255, 255, 255));
   SetTextColor(DstDC, RGB(0, 0, 0));
   BitBlt (DstDC, 0, 0, Ancho, Alto, SrcDC, 0, 0, SRCINVERT); // XOR
   BitBlt (DstDC, 0, 0, Ancho, Alto,  MaskDC, 0, 0, SRCAND); // AND
   BitBlt (DstDC, 0, 0, Ancho, Alto, SrcDC, 0, 0, SRCINVERT); // XOR
   // liberar recursos :
   DeleteDC (MaskDC);
   DeleteObject (BmpMono);
   ReleaseDC (0, ScreenDC);
end;

 

 Bitmaps semitransparentes

  Ahora vamos a analizar el segundo tipo de transparencias. Estas no vienen implementadas en Delphi, y en cuanto al sistema operativo, a partir del Windows98 y el NT 5.0 ya hay funciones para hacer estas transparencias, pero no sobre Win95 y NT 4.0, y como hay aun muchos usuarios que usan estos sistemas operativos, de usar las funciones de la API estaríamos limitando el mercado de nuestras aplicaciones.

  En este caso el ejemplo no estará optimizado al máximo para no complicarlo y poder concentrarnos así en lo que más interesa ahora: el algoritmo.

  Los colores tienen una representación en el formato RGB, que son tres valores en un rango de 0 a 255 que indican la intensidad de los colores rojo (Red), verde (Green) y azul (Blue). Esto es asumiendo que se van a tomar 8 bits para cada uno de los valores RGB.

  Tomaremos un valor entre 0 y 1 al que llamaremos valor Alfa, que será el encargado de indicarnos con que intensidad se ve la imagen que vamos a dibujar. Si Alfa es 0.5 entonces la intensidad de la imagen dibujada será igual a la intensidad del fondo, si es mayor que 0.5 será mayor la intensidad de esta que la del fondo, y si es menor, pues la intensidad del fondo será mayor.

  Si multiplicamos a Alpha por 100 entonces esta nos indicará el porciento que se ve de una imagen respecto a la otra, si la imagen que se dibuja encima se ve con un X% entonces la otra se ve con un (100 - X)%. Para obtener el color que se debe ver en cada punto lo que haremos será tomar un X porciento del valor del color de una imagen mas un 100 - X porciento del valor de la otra, esto nos dará el color correcto.

  En el ejemplo 2 esta el código que hace esto. El primer ciclo que se ve en este es una optimización del código. Veamos en que consiste.La operación que hay que realizar sobre cada punto es la siguiente:

 ColorDestino := (Color1 * Alpha) + (Color2 * (1 - Alpha));

  Como podemos ver esta incluye dos multiplicaciones con reales, y dos sumas (resta) también con reales, lo que como sabemos es mucho mas lento que una operación con enteros. La cantidad de veces que hay que hacer esta operación es:

 Veces := Alto * Ancho * 3;

  Es decir, tres veces por cada punto del bitmap, ya que cada punto esta dado por tres valores en RGB. Sin embargo nos podemos dar cuenta de que como los colores están en RGB los valores están entre 0 y 255, así que solo hay 256 posibles operaciones para (Color1 * Alpha) y 256 más para (Color2 * (1 - Alpha)), por lo que podemos hacer una inicialización donde calculemos previamente esos valores y los coloquemos en una tabla y así convertimos la operación del cálculo de color en una suma de enteros:

  ColorDestino := Table[1,Color1] + Table[2,Color2];

 ...donde Table[1,x] es (x * Alpha)  y  Table[2,x] es (x*(1-Alpha)), y esto es lo que se hace en el código de ejemplo.

  El código del ejemplo es muy lento a causa de las llamadas a las funciones GetPixel y SetPixel, aunque ligeramente más rápido que hacerlo puramente con la encapsulación que da la clase TBitmap del Delphi para esto. Hay formas de evitar esa cantidad de llamadas, tomando los datos del bitmap para memoria y trabajar sobre esta sin hacer las llamadas a las funciones que mencionamos anteriormente. La GDI nos provee de funciones para esto. En el ejemplo los bitmaps que se toman se asume que son del mismo tamaño para dar mas claridad al código, pero es simple  generalizarlo.

 
procedure MakeTrans (bmp1, bmp2: TBitmap; Alpha: real);
  var
     r, g, b,
      r1, g1, b1,
       r2, g2, b2    : byte;
     x, y, i             : integer;
     Pixel1, Pixel2 : COLORREF;
     Table             : array[1..2,0..255] of integer;
     Alto, Ancho   : integer;
begin
   Alto := bmp1.Height;
   Ancho := bmp1.Width;
   for i := 0 to 255 do // inicialización
     begin
        Table[1,i] := Trunc ((i * Alpha));
        Table[2,i] := Trunc ((i * (1 - Alpha)));
      end;
   for y:=0 to Alto-1 do
      begin
         for x:=0 to Ancho-1 do
            begin
                // tomo el color de los puntos de ambos bitmaps :
               Pixel1 := GetPixel (bmp1.Canvas.Handle,x,y);
               Pixel2 := GetPixel (bmp2.Canvas.Handle,x,y);
               // separo el color rojo, verde y azul de cada punto :
               r1 := Pixel1 and $000000FF;
               g1 := (Pixel1 and $0000FF00) shr 8;
               b1 := (Pixel1 and $00FF0000) shr 16;
               r2 := Pixel2 and $000000FF;
               g2 := (Pixel2 and $0000FF00) shr 8;
               b2 := (Pixel2 and $00FF0000) shr 16;
               // calculo los rojo, verde y azul resultantes :
               r := Table[1,r2] + Table[2,r1];
               g := Table[1,g2] + Table[2,g1];
               b := Table[1,b2] + Table[2,b1];
               // actualizo el color en el primer bitmap :
              SetPixel (bmp1.Canvas.Handle, x, y, RGB (r,g,b));
          end;
      end;
end;

ir al índice


Soluciones simples con el Planificador de Tareas

Nota: En la documentación original de Windows, el término Task Scheduler, ha sido traducido al español como Programador de Tareas. En este escrito hemos utilizado el término Planificador por Programador, por considerar que se ajusta más al concepto operacional del componente.

En Windows 95, teníamos un componente llamado Agente del Sistema (System Agent, implementado en el ejecutable SAGE.EXE), que suministraba la funcionalidad de planificar la ejecución de tareas. Microsoft ha sustituido esta componente, ahora como parte integrante del sistema operativo, por el Planificador de Tareas (Task Scheduler), que dicho de paso, tiene un comportamiento diferente.

El Planificador de Tareas (Task Scheduler) es un servicio de planificación para la ejecución de tareas, disponible como un recurso común con el sistema operativo Microsoft® Windows® 98 y Microsoft® Windows® NT 5.

Mediante el Planificador de Tareas se puede hacer lo siguiente:

  • Programar una tarea para que se ejecute diaria, semanal o mensualmente, o en determinados momentos, por ejemplo cuando el equipo se inicia o está inactivo.
  • Personalizar la forma en que se ejecutará una tarea en el momento programado.
  • Desactivar o cambiar la programación de una tarea existente.

De esta manera, el Planificador de Tareas, puede proveernos de una gran capacidad de procesamiento automatizado para el mantenimiento de nuestro computador, supervisando un criterio establecido y ejecutando la tarea cuando el criterio es cumplimentado. He aquí algunas alternativas de posibilidades:

  • Discado automático a una red (Automating Dial-up Networking)
  • Descargar el correo 
  • Uso de guiones de ejecución en VBScript y JScript
  • Usar las tareas para recordar eventos importantes (Lanzar una música o una grabación)
  • Apagar el computador con previsión (Computer shutdown)

Para un programador de aplicaciones, principalmente para los programadores RAD (rapid application development), conocer las posibilidades del uso del planificador de tareas y de otros recursos del sistema operativo, significa ahorro de tiempo y de codificación, rápida adaptabilidad a los requerimientos de los clientes y un arsenal de soluciones preconstruidas para incorporar a nuestros propios sistemas.

En este trabajo, expondremos algunas soluciones simples utilizando el Planificador de Tareas.

Arranque manual del Planificador de Tareas

El servicio del Planificador de Tareas no empieza solo; el programador es responsable de determinar el estado actual del Planificador de Tareas

Para activar el Planificador de Tareas, se procede de la siguiente forma:

Menú Inicio 
    -> Programas 
        -> Accesorios 
            -> Herramientas del Sistema 
                -> Tareas programadas

Esto abre una ventana de exploración, como la mostrada en la figura anterior, donde se destacan dos elementos: 

  • Un elemento en el explorador, Agregar tarea programada, para adicionar nuevas tareas.
  • Aparece el menú Avanzado, que suministra las opciones mostrada en la siguente figura
Menú del Planificador de Tareas

La primera opción de este menú, activa o retira de la barra de procesos, el ícono del Planificador de Tareas. Si el Planificador de Tareas no se sigue usando, el mismo no se activará cuando se inicie Windows. La segunda opción, pausa o resume la ejecución del Planificador de Tareas, sin descargarlo de la memoria, en tanto que la tercera, si está chequeada, provocará que se informe de las tareas que no han sido ejecutadas. Finalmente, la cuarta opción, muestra el archivo del registro de actividades del Planificador de Tareas. Queda al lector, profundizar un poco más su conocimiento de estos tópicos, en la ayuda de Windows. Si su directorio de instalación de Windows se encuentra en el camino C:\WINDOWS, entonces pinche aqui para activar esta ayuda.

Cuando se "abre" el elemento Agregar tarea programada, se muestra un asistente que nos guía en los pasos requeridos para crear una nueva tarea. Esta operación, está tan bien documentada en la ayuda de Windows, que no abundaremos más en ella.

Arranque por programa del Planificador de Tareas

Si requerimos desde un programa, garantizar que el Planificador de Tarea se encuentre ejecutando, podemos utilizar el siguiente código:

Const
  SCHED_CLASS            = 'SAGEWINDOWCLASS';
  SCHED_TITLE            = 'SYSTEM AGENT COM WINDOW';
  SCHED_SERVICE_APP_NAME = 'mstask.exe';

function TaskSchedulerActivate: DWORD;
var
  sui : TStartupInfo;
  pi : TProcessInformation;
  szApp: string;
  pszPath : PChar;

      function GetOSVer: DWORD;
      var
        osver: OSVERSIONINFO;
      begin
        osver.dwOSVersionInfoSize := sizeof(OSVERSIONINFO);
        GetVersionEx(osver);
        Result := osver.dwPlatformId;
      end;

begin
  // Determinar que version de OS está corriendo
  // El planificador de tareas sólo esta presente
  // Windows 95/98 y NT que no interesa ahora.
  Result := ERROR_SUCCESS;
  if GetOSVer = VER_PLATFORM_WIN32_WINDOWS then
  begin
    // Version Windows 95/98 del Planificador de tareas
    if FindWindow(SCHED_CLASS, SCHED_TITLE) <> 0 then Exit;

    // Ejecutar el planificador de tareas
    ZeroMemory(@sui, SizeOf(sui));
    sui.cb := SizeOf(STARTUPINFO);
    SetLength(szApp, MAX_PATH);
    if SearchPath(nil, SCHED_SERVICE_APP_NAME, nil,
             MAX_PATH, PChar(@szApp[1]), pszPath) = 0 then
    begin
      Result := GetLastError;
      Exit;
    end;

    if CreateProcess(PChar(szApp), nil, nil, nil, False,
                     CREATE_NEW_CONSOLE or CREATE_NEW_PROCESS_GROUP,
                     nil, nil, sui, pi) = BOOL(0) then
    begin
      Result := GetLastError;
      Exit;
    end;

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
  end;
end;

Esta función retorna si el Planificador de Tareas ya está activo, en caso contrario, busca la aplicación del mismo y la ejecuta.

Una utilidad increible: RUNDLL32.EXE

A partir Windows 95, existen dos utilidades nombradas RUNDLL.EXE y RUNDLL32.EXE, que permiten invocar una función exportada desde una DLL, en 16 y 32 bits respectivamente.

La forma de llamada de este comando es la siguiente:

   RUNDLL32.EXE <dllname>,<entrypoint> <optional arguments>

Mas adelante, veremos ejemplos del uso de este utilitario. Existen tres cuestiones a considerar a la hora de establecer el comando de una tarea usando esta utilidad:

  1. Este utilitario busca la DLL en los lugares preestablecidos para la funcion LoadLibrary. Por tanto, se recomienda que se suministre el camino completo de la biblioteca para asegurar que la misma es hallada. Los mejores resultados se obtienen utilizando los nombres cortos, evitando que aparezcan caracteres ilegales.
  2. El nombre de la DLL no puede contener espacios, comas o comillas.
  3. La coma en la línea de comando entre el nombre de la DLL y el punto de entrada, es muy importante y no deben haber espacios entre el nombre de la DLL, la coma y el punto de entrada de la función que se llama.

RUNDLL32.EXE nos será muy útil para efectuar de manera programada los ejemplos de tareas programadas que exponemos a continuación.

Apagando automáticamente al computadora

Es interesante, cuando terminamos nuestro trabajo normal en la computadora, dejar corriendo las tareas de mantenimiento, salvas, etc y una vez finalizadas éstas, apagar el equipo.

Para apagar el equipo, cree una nueva tarea en el planificador y suministre la siguiente línea de comando

C:\Windows\RUNDLL32.EXE shell32,SHExitWindowsEx X

donde X toma cualquiera de los siguientes valores:

0 Reinicializar el shell de Windows
1 Salir de Windows
2 Reinicializar el computador
4 Cerrar todas  las aplicaciones
8 Salir de Windows y apagar un computador compatible ATX
-1 Reinicializar el Explorador

Alternativamente, puede apagar el computador implementando una tarea con la siguiente línea de comando:

C:\Windows\RUNDLL32.EXE user,ExitWindows

Encendiendo automáticamente la computadora

Este método requiere de un interruptor controlado por un temporizador y funciona en las máquinas que arrancan cuando tienen el suministro de electricidad (no sé si las placas ATX tienen algún jumper que habilite esto). No obstante, algunas placas madres, tienen incluido en el BIOS, arrancar el equipo a determinada hora (verifique en su computadora, si existe esta posibilidad). En cualquier caso, supongamos que el interruptor controlado es programado para conectar a una hora determinada. Entonces, se establecen las tareas que se ejecutan cuando se inicia el sistema, o unos minutos después del tiempo programado en el interruptor.

Discado automático a una red (Automating Dial-up Networking)

Para discar automáticamente y conectarse a una red o un suministrador de servicios de Internet, debemos crear una tarea que ejecute el siguiente comando:

C:\Windows\RUNDLL32.EXE rnaui.dll,RnaDial X

donde X es el nombre completo de la conexión a discar.

Para evitar que se presente la ventana de entrada de usuario que se conecta y su clave de acceso, proceda como sigue (esto funciona bien, si ya ha habido previamente una conexión exitosa con la casilla de Guardar contraseña habilitada):

  1. Abra el explorador del acceso telefónico a redes
Menú Inicio 
    -> Programas 
        -> Accesorios 
            -> Comunicaciones 
                -> Acceso telefónico a redes
  1. En el menú Conexiones, opción Configuracion, desmarcar las casillas Solicitar información antes de marcar y Mostrar un diálogo de confirmación despues de haber realizado la conexión

Descargar el correo 

Un amigo mío, usa como cliente de correos, Outlook Express del Internet Explorer 5. Es libre (free) y funciona muy bien. Tiene programada una tarea que se ejecuta cuando se enciende la máquina, que lanza Outlook Express. Este a su vez, está configurado para buscar nuevo correo cada vez que es ejecutado. A los 20 minutos, la tarea que lanzó el Outlook, lo apaga y posteriormente apaga el computador.  Así descarga en su computador todo el correo que entra en la madrugada.

Outlook 2000, tiene implementado su propio planificador de tarea para efecturar la conexión y buscar y enviar automáticamente los mensajes.

Mostrar los eventos importantes del día

Utilizando los guiones de ejecución, podemos construir muy facilment aplicaciones que ejecutamos como una tarea con el Planificador de Tareas. He aquí un ejemplo, para ilustrar esto.

Cuando uno se consagra al trabajo, a veces pasa por alto celebraciones, que de no ser recordadas, se prefiere estar desterrado a enfrentar sus concecuencias. Tal es el caso, del cumpleaños de los hijos, el aniversario de bodas, etc.

Usando JScript, he construido un pequeño guión de ejecución (que es un fichero textual muy fácil de actualizar) que se activa mediante una tarea programada, cada vez enciendo mi computador. De esta forma, sinceramente me evitado muchos inconvenientes. He aquí código por si a alguien le resulta útil.

/* --------------------------------------------------------------------------------------
Este guión, muestra una ventana con celebraciones
que tienen lugar el día en que se ejecuta el mismo.
por José Manuel Abad Hernández.
---------------------------------------------------------------------------------------- */

// Parámetros de la entrada del libro de celebraciones: Fecha,Nombre,Motivo
// La fecha tiene el formato como dia/mes/año

function entradaLibro(Fecha,Nombre,Motivo)
{
  this.length = 3;
  this.Fecha = Fecha;
  this.Nombre = Nombre;
  this.Motivo = Motivo;
}

// La variable Celebraciones, es un arreglo de entradas que contienen
// la fecha de la celebracion, el nombre de la persona asociada y el
// motivo de la celebración.

var vbOK = 0;
var vbInformation = 64;
var Celebraciones = new Array();

// Cada vez que se quiera adicionar una entrada el libro de celebraciones,
// adicionar una linea al arreglo aquí abajo, con los parámetros apropiados
// e incrementar el valor de la variable TotalEntradas.

var TotalEntradas = 4;
Celebraciones[0] = new entradaLibro("26/12/1953", "Yo", "cumpleaños");
Celebraciones[1] = new entradaLibro("14/7/1955", "Mi esposa", "cumpleaños");
Celebraciones[2] = new entradaLibro("2/6/1979", "Mi hijo", "cumpleaños");
Celebraciones[3] = new entradaLibro("16/1/2000", "El jefe", "asume el cargo de Director");

// Obtener la fecha de hoy
var fechaHoy = new Date();
var diaActual = fechaHoy.getDate();
var mesActual = fechaHoy.getMonth() + 1;
var listaCelebraciones = new String("");

  for(m=0; m<TotalEntradas ;m++)
  {
    i = Celebraciones[m].Fecha.indexOf("/", 0);
    if (i <= 0) continue;
    Dia = parseInt( Celebraciones[m].Fecha.substring(0, i));
    j = Celebraciones[m].Fecha.indexOf("/", i+1);
    if (j <= 0) continue;
    Mes = parseInt( Celebraciones[m].Fecha.substring(i+1,j));
    if ((diaActual == Dia) && (mesActual == Mes))
    listaCelebraciones += "\r\n" + Celebraciones[m].Nombre + ": motivo " + Celebraciones[m].Motivo;
  }

  // Mostrar en un ventana emergente las conmemoraciones de día
  if( listaCelebraciones != "" )
  {
    var Message_Text = "Celebraciones del día de hoy: " + listaCelebraciones;
    var WSHShell = WScript.CreateObject("WScript.Shell");
    WSHShell.Popup(Message_Text, 0, "Celebraciones", vbOK + vbInformation);
  }

/* Fin */

Para ejecutar este guión como una tarea programada, coloque el fichero del guión en el directorio de Windows y cree una tarea con la siguiente linea de comando:

C:\Windows\WScript.exe C:\Windows\Celebraciones.js

Planificar las tareas que deben ejecutarse automáticamente, depende de sus requerimientos. Aquí, hemos pretendido llamar la atención sobre un servicio que es parte del sistema operativo y que en determinados casos, haciendo uso del mismo, nos podemos ahorrar un gran esfuerzo de programación. Si usted tiene alguna tarea que pueda presentar un interés general para los programadores y desea compartirla, envíemela. Actualizaré esta página incorporando su sugerencia.

Algunas de las soluciones expuestas en este trabajo, las he consolidado de la lectura de una gran cantidad de mensajes de la lista de Microsoft, microsoft.public.win98.taskscheduler. Vaya el mérito de las ideas que he ordenado en este artículo, a todos aquellos, que con su creatividad, experiencia y desprendimiento, llenan de respuestas, la infinidad de preguntas que circulan día a día por Internet.

ir al índice


Programando el Planificador de Tareas

El "Planificador de Tarea" (Task Scheduler) es un servicio de planificación para la ejecución de tareas, disponible como un recurso común con el sistema operativo Microsoft® Windows® 98 y Microsoft® Windows® NT 5. 

En un trabajo anterior, expusimos algunas soluciones simples con el planificador de tareas. Nos ocupa ahora el problema de controlar programáticamente el Planificador de Tareas

Con el "Planificador de Tarea", el usuario trabaja dentro de la carpeta Tareas Programadas (Scheduled Tasks) contenida en "Mi Computadora", que contiene el listado de las tareas actualmente establecidas y la herramienta para la creación de nuevas tareas. Se puede recuperar la ubicación física en disco, de la carpeta de las tareas programadas, buscando en el registro de Windows, el valor de la siguiente clave: 

HKEY_LOCAL_MACHINE
      SOFTWARE
            Microsoft
                  SchedulingAgent
                        TasksFolder

El nombre de una tarea, es el nombre del archivo donde se almacena la información de la tarea (estos archivos tienen extensión job). Al programar, el Planificador de Tareas, los nombres de la tarea utilizados por las funciones del API, siempre se dan en carácteres anchos (wide character), así que hay que realizar la correspondiente conversión de toda cadena declarada en Pascal. 

En Windows 98 y NT, el "Planificador de Tarea" se incluye como un conjunto de interfaces de objeto COM, mediante las cuales se manejan todos los aspectos de planificación: comienzo de los trabajos, enumeración de trabajos vigentes, información del estado del trabajo y así sucesivamente. 

ISchedulingAgent
IEnumTasks
ITask
IEnumWorkItems
ITaskTrigger

Interfaces de programación del Planificador de Tareas

No sólo podemos incorporar esta funcionalidad dentro de nuestras de aplicaciones, sino aún proveer mayor funcionalidad a la suministrada implícitamente por el sistema operativo. Comencemos, estableciendo el orden de ideas básico, bajo el cual basaremos los trabajos de programación.

¿ Qué es ...  
Tarea (Task) Una tarea es cualquier objeto que es ejecutado por el "Planificador de Tarea". Para acceder a las tareas, se utiliza la interfaz ITask
Gatillo (Trigger) Un gatillo es un conjunto de criterios que, cuando se cumplimentan, se activa y ejecuta una tarea a la que está asociado. Para manipular los gatillos se utiliza la interfaz ITaskTrigger.
Gatillo ocioso (Idle trigger) Cuando ningún evento de teclado o del ratón ocurre, la computadora es considerada a está en un "estado ocioso" (idle). Un gatillo ocioso es un evento, que se activa un tiempo después de la computadora se vuelve ociosa. Cuando se ponen las banderas asociadas al estado ocioso en una tarea, se crea un gatillo ocioso. Se usa el método de IScheduledWorkItem.SetIdleWait para establecer la cantidad de tiempo que el sistema está ocioso antes que la tarea sea ejecutada.
Artículo de trabajo programado (Scheduled Work Item) Un artículo de trabajo programado es un elemento que el servicio del "Planificador de Tarea" corre en el momento especificado por el gatillo del artículo. Para manipular los artículos se utiliza la interfaz ISheduledWorkItem. Actualmente el único tipo de artículo de trabajo programado es una tarea. Así, que la interfaz es una especialización de un artículo.

 

Accediendo al "Planificador de Tarea"

El punto de entrada para manipular programáticamente el Planificador de Tarea, es la obtención de una referencia a una instancia del objeto ITaskScheduler. La figura a continuación, muestra la declaración de la interfaz del Planificador de Tareas.

  ITaskScheduler = interface
    ['{148BD527-A2AB-11CE-B11F-00AA00530503}']
    function SetTargetComputer(pwszComputer: PWideChar): HRESULT; stdcall;
    function GetTargetComputer(out ppwszComputer: PWideChar): HRESULT; stdcall;
    function Enum(out ppEnumWorkItems: IEnumWorkItems): HRESULT; stdcall;
    function Activate(pwszName: PWideChar; 
                const riid: TGUID; out ppUnk: IUnknown): HRESULT; stdcall;
    function Delete(pwszName: PWideChar): HRESULT; stdcall;
    function NewWorkItem(pwszTaskName: PWideChar; const rclsid: TCLSID; 
                const riid: TGUID; out ppUnk: IUnknown): HRESULT; stdcall;
    function AddWorkItem(pwszTaskName: PWideChar;
                pWorkItem: IScheduledWorkItem): HRESULT; stdcall;
    function IsOfType(pwszName: PWideChar; const riid: TGUID): HRESULT; stdcall;
  end;

Todas las definiciones de las interfaces utilizadas por el Planificador de Tareas pueden ser estudiadas en el fichero MSTask.pas, suministrado con el archivo de fuentes de ejemplo, que es mi versión pascalizada del correspondiente fichero MSTask.h del SDK proporcionado por Microsoft.

ITaskScheduler, suministra el conjunto de métodos básicos para manipular las tareas (crear, adicionar, borrar, contar y obtener). Luego en cualquier punto de entrada de nuestro programa, antes de cualquier referencia al Planificador de Tarea y los objetos asociados, debemos ejecutar un código como el mostrado a continuación:

  CoInitialize(nil);
  // Crear el objeto COM asociado al Planificador
  // de Tareas en la variable SchedulingAgent.
  SchedulingAgent := CreateCOMObject(CLSID_CTaskScheduler) as ITaskScheduler;

De la misma forma, antes de cerrar nuestra aplicación, debemos liberar los recursos que se han reservado al utilizar el planificador de tareas. Básicamente, debemos ejecutar el siguiente código:

  // Liberar el objeto COM que fue creado
  SchedulingAgent := nil;
  CoUninitialize;

 

Obteniendo el listado de tareas programadas

Para listar las tareas programadas existentes, el objeto ITaskScheduler suministra la forma de obtener la referencia a un objeto iterador mediante el método: 

    function Enum(out ppEnumWorkItems: IEnumWorkItems): HRESULT; stdcall;

Al ejecutar el método Enum, se le proporciona como argumento una variable en la que retorna una referencia al objeto iterador. El objeto iterador es una instancia que soporta la interfaz IEnumWorkItems, cuya definición es la siguiente:

  IEnumWorkItems = interface
    ['{148BD528-A2AB-11CE-B11F-00AA00530503}']
    function Next(celt: ULONG; out rgpwszNames: PLPWSTR;
                  out pceltFetched: ULONG): HRESULT; stdcall;
    function Skip(celt: ULONG): HRESULT; stdcall;
    function Reset: HRESULT; stdcall;
    function Clone(out pEnumWorkItems: IEnumWorkItems): HRESULT; stdcall;
  end;

Ahora podemos obtener el listado ejecutando el siguiente código:

procedure FillTaskList(slTask: TStrings);
var
  ppNames : PLPWSTR;
  sName : string;
  i, dwFetched : ULONG;
  pEnum : IEnumWorkItems;
begin
  if SchedulingAgent = nil then Exit;
  slTask.Clear;
  // Obtener el enumerador para iterar sobre las tareas
  if not Succeeded( SchedulingAgent.Enum( pEnum ) ) then Exit;

  pEnum.Reset;
  // Leer las tareas 5 de cada vez
  while Succeeded( pEnum.Next(5, ppNames, dwFetched)) and
        (dwFetched > 0) do
  begin
    for i := 0 to Pred(dwFetched) do
    begin
      sName := WideCharToString( ppNames[i] );
      if sName <> '' then
        slTask.Add(sName);
      // Librerar la cadena allocada en el
      // enumerador para el nombre de la tarea.
      CoTaskMemFree( ppNames[i] );
    end;
    // Librerar el arreglo de cadenas WideChar
    // allocadas en el enumerador.
    CoTaskMemFree(ppNames);
  end;
  // Librerar el objeto de iteración
  pEnum := nil;
end;

Aquí, es interesante notar la necesidad de llamar a la funcion CoTaskMemFree para liberar la memoria reservada para las cadenas. Además, observe que para liberar un objeto referenciado a través de una interfaz, sólo se requiere poner el valor de la variable que contiene la referencia a nil, el resto lo hace la magia del compilador de Delphi.

Cómo crear una tarea nueva

Para crear una nueva tarea, se puede realizar de dos formas diferentes. La primera es llamando ITaskScheduler.AddWorkItem. En este caso, es responsabilidad del programador instanciar el objeto ITask que se suminstra a la función. La figura muesta la definición de la interfaces de los artículos y tareas.

  IScheduledWorkItem = interface
    ['{a6b952f0-a4b1-11d0-997d-00aa006887ec}']
    function CreateTrigger(out piNewTrigger: WORD;
                  out ppTrigger: ITaskTrigger): HRESULT; stdcall;
    function DeleteTrigger(iTrigger: WORD): HRESULT; stdcall;
    function GetTriggerCount(out pwCount: WORD): HRESULT; stdcall;
    function GetTrigger(iTrigger: WORD;
                   out ppTrigger: ITaskTrigger): HRESULT; stdcall;
    function GetTriggerString(iTrigger: WORD;
                 out ppwszTrigger: PWideString): HRESULT; stdcall;
    function GetRunTimes(const pstBegin: TSystemTime; 
                 const pstEnd: TSystemTime; out pCount: WORD;
                 out rgstTaskTimes: TSystemTime): HRESULT; stdcall;
    function GetNextRunTime(out pstNextRun: TSystemTime): HRESULT; stdcall;
    function SetIdleWait(wIdleMinutes: WORD;
                wDeadlineMinutes: WORD): HRESULT; stdcall;
    function GetIdleWait(out pwIdleMinutes: WORD; 
                out pwDeadlineMinutes: WORD): HRESULT; stdcall;
    function Run: HRESULT; stdcall;
    function Terminate: HRESULT; stdcall;
    function EditWorkItem(hParent: HWND; dwReserved: DWORD): HRESULT; stdcall;
    function GetMostRecentRunTime(out pstLastRun: TSystemTime): HRESULT; stdcall;
    function GetStatus(out phrStatus: HRESULT): HRESULT; stdcall;
    function GetExitCode(out pdwExitCode: DWORD): HRESULT; stdcall;
    function SetComment(pwszComment: PWideChar): HRESULT; stdcall;
    function GetComment(out ppwszComment: PWideChar): HRESULT; stdcall;
    function SetCreator(pwszCreator: PWideChar): HRESULT; stdcall;
    function GetCreator(out ppwszCreator: PWideChar): HRESULT; stdcall;
    function SetWorkItemData(cbData: WORD;
                var rgbData: BYTE): HRESULT; stdcall;
    function GetWorkItemData(out pcbData: WORD; 
                out prgbData: BYTE): HRESULT; stdcall;
    function SetErrorRetryCount(wRetryCount: WORD): HRESULT; stdcall;
    function GetErrorRetryCount(out pwRetryCount: WORD): HRESULT; stdcall;
    function SetErrorRetryInterval(wRetryInterval: WORD): HRESULT; stdcall;
    function GetErrorRetryInterval(out pwRetryInterval: WORD): HRESULT; stdcall;
    function SetFlags(dwFlags: DWORD): HRESULT; stdcall;
    function GetFlags(out pdwFlags: DWORD): HRESULT; stdcall;
    function SetAccountInformation(pwszAccountName: PWideChar;
                   pwszPassword: PWideChar): HRESULT; stdcall;
    function GetAccountInformation(out ppwszAccountName: PWideChar): HRESULT; stdcall;
  end;

  ITask = interface(IScheduledWorkItem)
    ['{148BD524-A2AB-11CE-B11F-00AA00530503}']
    function SetApplicationName(pwszApplicationName: PWideChar): HRESULT; stdcall;
    function GetApplicationName(out ppwszApplicationName: PWideChar): HRESULT; stdcall;
    function SetParameters(pwszParameters: PWideChar): HRESULT; stdcall;
    function GetParameters(out ppwszParameters: PWideChar): HRESULT; stdcall;
    function SetWorkingDirectory(pwszWorkingDirectory: PWideChar): HRESULT; stdcall;
    function GetWorkingDirectory(out ppwszWorkingDirectory: PWideChar): HRESULT; stdcall;
    function SetPriority(dwPriority: DWORD): HRESULT; stdcall;
    function GetPriority(out pdwPriority: DWORD): HRESULT; stdcall;
    function SetTaskFlags(dwFlags: DWORD): HRESULT; stdcall;
    function GetTaskFlags(out pdwFlags: DWORD): HRESULT; stdcall;
    function SetMaxRunTime(dwMaxRunTimeMS: DWORD): HRESULT; stdcall;
    function GetMaxRunTime(out pdwMaxRunTimeMS: DWORD): HRESULT; stdcall;
  end;

 El otro método es llamar a ITaskScheduler.NewWorkItem. Al llamar este método se el suministra el nombre de la tarea y la referencia a una variable donde se devuelve la interfaz a la instancia del objeto recien creado. A continuación la forma en que se implementa esto, donde después de crear una nueva tarea, se activa el diálogo para editar sus propiedades.

procedure CrearTarea(NombreTarea: string);
var
  sTaskName : PWideChar;
  pWorkItem : ITask;
begin
  if SchedulingAgent = nil then Exit;
  sTaskName := StringToOleStr( NombreTarea );
  try 
    if Succeeded( 
       SchedulingAgent.NewWorkItem(sTaskName, 
                             CLSID_CTask,
                               IID_ITask,
                     IUnknown(pWorkItem))) then
    begin
      pWorkItem.EditWorkItem(Handle, 0);
    end;
  finally
    // Liberar la instancia de la tarea creada
    pWorkItem := nil;
    SysFreeString(sTaskName);
  end;
end;

 

Cómo borrar una tarea

Para borrar una tarea, se utiliza el método ITaskScheduler.Delete al que se le suministra la cadena con el nombre de la tarea. De acuerdo a la implementación que hemos seguido, esto se escribiría así:

procedure BorrarTarea(ss: string);
var
  sTaskName : PWideChar;
begin
  if SchedulingAgent = nil then Exit;
  sTaskName := StringToOleStr( ss );
  try
    SchedulingAgent.Delete(sTaskName);
  finally
    SysFreeString(sTaskName);
  end;
end;

 

Procedimiento básico de obtención de una referencia a las tareas

Para manipular las tareas es necesario tener una referencia a una instancia de un objeto que soporte la interfaz ITask  (ie. IScheduledWorkItem) asociada a una tarea específica. El procedimiento básico que hemos pensado para lograr esto, se muestra en el código a continuación:

procedure {Cualquier nombre aqui}(nombrePascal: string);
var
  sTaskName : PWideChar;
  pWorkItem : ITask;
begin
  // Retornar si no exite una referencia 
  // al Planificador de Tareas
  if SchedulingAgent = nil then Exit;
  // Convertir una cadena de caracteres Pascal
  // a otra de caracteres anchos (wide char)
  sTaskName := StringToOleStr( nombrePascal );
  try // Obtener la referencia a la tarea seleccionada
    // utilizando el método Activate del Planificador
    if Succeeded( 
       SchedulingAgent.Activate(sTaskName, IID_ITask,
                                IUnknown(pWorkItem)))then
    begin
      // Aquí dentro de este bloque, escribimos el código
      // para acceder a la instancia del objeto que
      // soporta la interfaz ITask y ejecutar sus métodos.
      // Como recurso en los párrafos que siguen, nos
      // referiremos a este espacio, como el bloque TASK
      .
      .
      .
    end;
  finally
    // Liberar la instancia de la tarea obtenida
    pWorkItem := nil;
    // Liberar la memoria reservada para conformar
    // la cadena de caracteres anchos.
    SysFreeString(sTaskName);
  end;
end;

El código ha sido abundantemente comentado para lograr su entendimiento.

Como modificar una tarea existente

Para modificar una tarea existente, inserte en el bloque TASK del procedimiento básico de obtención de una referencia a las tareas, la siguiente línea:

  pWorkItem.EditWorkItem(Handle, 0);

donde "Handle", es el Handle de la ventana desde donde se llama el procedimiento. Esta acción, provoca que se muestre el diálogo de propiedades de la tarea.

Cómo hacer ejecutar una tarea 

Si se quiere provocar que se ejecute programáticamente una tarea, simplemente llamamos el método  IScheduledWorkItem.Run del objeto dentro bloque TASK del procedimiento básico de obtención de una referencia a las tareas. Así quedaría:

  pWorkItem.Run;

Observe que el servicio del "Planificador de Tarea" debe estar corriendo para que este método tenga éxito. 

Cómo detener una tarea que se está ejecutando

Similarmente al caso anterior, utilizando ahora el método IScheduledWorkItem.Terminate. Inserte en el bloque TASK del procedimiento básico de obtención de una referencia a las tareas, las siguientes líneas:

  pWorkItem.GetStatus( hr );
  if hr = SCHED_S_TASK_RUNNING then
      pWorkItem.Terminate;

Antes de terminar la tarea, simplemente verificamos su estado para averiguar si se está ejecutando.

    ir al índice


Cursores animados

¿Cargar un cursor animado en una aplicación? ¡ Acaso, no es esto un tema ya muy conocido de las listas de discuciones de Delphi y de la páginas WEB sobre programación que podemos encontrar llenas de trucos y sugerencias ! 

Ciertamente.

Pero entonces, ¿Por qué artículos sobre temas muy usados?

Mucho he pensado sobre esto, antes de decidirme a escribir este trabajo, que me ha tomado en su totalidad, no más de tres horas. Llegar a la comprensión de un tema esencialmente técnico, depende en gran medida de la experiencia y conocimientos del lector.

He aquí, algunos argumentos que validan la utilización de viejos problemas resueltos en los artículos.

  • Se pueden plantear nuevos enfoques o soluciones mejores de viejos problemas.
  • Se puede integrar una mayor unidad de información de temas ya conocidos.
  • Cubrimos pequeños aportes, que nos gustaría hacer llegar a todo al que le pueda ser útil.
  • Podemos hacer llegar a los constructores de herramientas, nuestras opiniones sobre algunas limitaciones de las mismas, que acostumbrados a usarlas, pasan inadvertidas.
  • Actualizamos el fundamento del problema con las caracteriscas de las nuevas versiones de las herramientas de construccion de aplicaciones.
  • Los programadores noveles que llegan a este mundo fascinante de la programación, ávidos de información, incorporan a su arsenal de soluciones prefabricadas, temas viejos pero vigentes.

Así, en este texto, pretendemos mostrar lo siguiente:

  • Como incorporar en los recursos de una aplicación, casi cualquier cosa. En este caso serán cursores animados.
  • Como cargar y mostrar cursores animados en Delphi.

Para trabajar con cursores animados, necesitamos al menos tener uno de ellos. La instalación de Windows tiene un conjunto de ficheros de cursores animados que puede encontrar en la carpeta CURSORS dentro del directorio de instalación de Windows; pero voy a usar un par de ellos que diseñé hace unos años durante la construcción de una aplicación. En las utilidades encontrará la aplicación con que fueron construidos: ANIEDIT.EXE, que he recompilado de las fuentes suministradas en MSDN de Microsoft, para que puedan realizar sus propios cursores animados.

Infelizmente, no es posible usar la utilidad "Image Editor" que viene con Delphi para crear y adicionar un cursor animado a un fichero de recursos. Tal vez, a la gente de Borland (o Inprise como se llaman ahora) le llegue por alguna vía que se pudiera actualizar ya el "Image Editor". Aquí tenemos que hacerlo por los pasos convencionales.

Creación de los recursos de punteros animados

Después de tener seleccionado los cursores que vamos a utilizar, debemos construir con los mismos, el fichero de recursos que los contiene. Para crear el fichero de recursos compilados, construimos primeramente el fichero fuente de recursos. Los cursores estáticos se declaran con la palabra clave CURSOR; pero el compilador de recursos de Delphi sobreentiende que con esta directiva siempre se va a referir a un cursor estático (*.CUR) y no la podemos usar para los cursores animados (*.ANI). La documentación del SDK de Microsoft, admite esta directiva para los cursores animados y parece que es el compilador al leer el formato del fichero el encargado de incorporar convenientemente el recurso. Por eso, tenemos que incorporar el cursor animado como un recurso binario. La forma que explicamos de aquí en adelante, es válida también, para incorporar en los recursos de la aplicación, prácticamente cualquier fichero.

La figura a continuación, muestra el fichero fuente de recursos para incorporar los cursores animados en un ficheros de recursos:

// Archivo CURES.RC
// Punteros animados

BRGTHARR  RCData  BRIGHTARR.ANI
MOVIECUR  RCData  MOVIE.ANI

Ahora, debemos compilar los recursos en un fichero binario. Para esto, ejecutamos la siguiente línea de comandos:

"($DELPHI)\Bin\BRCC32.EXE" -32 CURES.RC

donde ($DELPHI) es la carpeta de instalación de Delphi, usualmente "C:\Archivos de Programa\Borland\Delphi4".

La aplicacion BRCC32.EXE es el compilador de recursos suministrado con Delphi que se localiza en la carpeta BIN del directorio de instalación de Delphi. De esta forma generamos el fichero binario *.RES que contiene los recursos indicados.

Para incorporar los recursos en la aplicación, basta con adicionar la siguiente línea de código en cualquier unit del proyecto de nuestra aplicación:

{Directiva para incorporar en los recursos de la
 aplicacion los cursores animados.}

{$R CURES.RES}

Cargar los cursores animados en la aplicación

A diferencia de los cursores estáticos, no vamos a poder utilizar una forma tan sencilla para cargar un cursor animado desde los recursos de la aplicación como podría ser la siguiente: 

  hCur := LoadCursor( HInstance, PChar('ACURMOVIE'));
  Screen.Cursors[crMovieProj] := hCur;

Esto es debido a la limitación del compilador de recursos de Delphi de no resolver adecuadamente los ficheros de cursores animados dentro de los recursos con la directiva CURSOR. Por tanto, tenemos que cargar el cursor como si fuera un recurso binario, salvarlo a un fichero temporal como *.ANI y llamar a la función del API, LoadCursorFromFile, para tener finalmente el cursor.

Para ilustrar todo esto, hemos construido una aplicación sencilla. La forma principal contiene dos paneles. A cada panel se le asigna un cursor animado cuando el ratón se encuentra sobre los mismos. Se utiliza el evento FormCreate para cargar los cursores animados desde los recursos de la aplicación, como se muestra en la figura a continuación:

Const
  crBrigthArrow = 5; // puede ser cualquier valor
  crMovieProj   = 6;

procedure TForm1.FormCreate(Sender: TObject);
var
  lpTempDir, lpTempFileName : string;
  nBufferLength : DWORD;
  hCur : HCURSOR;
begin
  // Obtener el directorio temporal de Windows
  SetLength(lpTempDir, 255);
  nBufferLength := GetTempPath(255, @lpTempDir[1]);
  SetLength(lpTempDir, nBufferLength);

  // Obtener el nombre de un fichero temporal
  SetLength(lpTempFileName, 255);
  GetTempFileName(PChar(lpTempDir), PChar('RES'), 0, @lpTempFileName[1]);

  // Establecer correctamente la extensión del un fichero
  // de cursor animado.
  lpTempFileName := ChangeFileExt(lpTempFileName, '.ani');

{ Cargar los cursores animados de los recursos de la aplicacion.
  Observe que aquø, usamos el mismo nombre del recurso que
  establecimos en el fichero *.RC }


  // Primero se lee el fichero del cursor de los recursos de la aplicacion
  // luego se salva a un fichero temporal
  // Ahora se carga el fichero del cursor en el arreglo de cursores del objeto Screen
  // y se asigna al control sobre el que debe aparecer cuando pase sobre el mismo.
  with TResourceStream.Create(HInstance, 'BRGTHARR', RT_RCDATA) do
    try
      SaveToFile( lpTempFileName );
    finally
      Free;
    end;
  Screen.Cursors[crBrigthArrow] := LoadCursorFromFile( PChar(lpTempFileName));
  Panel1.Cursor := crBrigthArrow;

  with TResourceStream.Create(HInstance, 'MOVIECUR', RT_RCDATA) do
    try
      SaveToFile( lpTempFileName );
    finally
      Free;
    end;

  Screen.Cursors[crMovieProj] := LoadCursorFromFile( PChar(lpTempFileName));
  Panel2.Cursor := crMovieProj;

  // Borrar el fichero temporal
  DeleteFile(lpTempFileName);
end;

Con esto, tanto si lo había olvidado o si no lo conocía, creo que debo haberlo motivado a mejorar la interfaz de sus aplicaciones, usando cursores animados. Después de Windows 95, sería demasiada comodidad no incorporarlos. 

    ir al índice


Componentes en tiempo de diseño

En tiempo de diseño, al escoger un componente y colocarlo sobre una forma, Delphi crea una instancia del componente y ejecuta su código, según sea necesario. A causa de esto, es que observamos que los componentes en tiempo de diseño tienen un comportamiento similar que en tiempo de corrida.

Cuando estamos desarrollando un componente, sucede en ocasiones, que queremos que cierto código sólo se ejecute en tiempo de corrida y no en tiempo de diseño. 

Esto puede suceder cuando dicho código consuma muchos recursos, lo que podría molestar en tiempo de diseño. 

La solución a esto está en detectar si el código se está ejecutando en tiempo de diseño o en tiempo de corrida, lo cual se logra verificando la propiedad "ComponentState" de TComponent, el ancestro común de todos lo componentes. El código es el siguiente :

    if csDesigning in ComponentState then
    begin
      // estoy en tiempo de diseño
    end
    else
    begin

      // estoy en tiempo de corrida
    end;

Tenemos que tener mucho cuidado con el tratamiento de las excepciones en los componentes, ya que en tiempo de corrida solo afectan a la aplicacion; pero en tiempo de diseño, como el que ejecuta el código es Delphi, entonces éste el que sufre las consecuencias. 

Una excepción no tratada, un ciclo infinito u otra indisciplina en un componente nos puede llevar a tener que cerrar el Delphi, resultando en ocasiones en unos inexplicables "Borland Database Engine Error".

Generalmente, es preferible crear los componentes, pero no registrarlos como componentes hasta que no esten bien probados, para evitar los errores en tiempo de diseño. 

Eso se lograría, creando los componentes en el evento OnCreate de la Forma, asignandole las propiedades con código en lugar de visualmente y destruyendolo en el OnDestroy. Parace incómodo, pero MUCHO más incómodo es tener que cerrar y cargar el Delphi varias veces.

Algo muy interesante, es cuando se usa en objeto Application en un componente. En tiempo de corrida este se refiere a nuestra aplicación, pero en tiempo de diseño, es Delphi.

Resulta algo curioso,  pero al experimentar con el objeto Application en tiempo de diseño,  podemos desde cerrar, maximizar o minimizar el Delphi, hasta cambiarle el icono de la barra de tareas, por ejemplo con un simple :

    Application.Icon.Assign (NewIcon); 

... o cerrar el Delphi con :

    Application.Terminate;

Al recompilar un paquete de componentes es necesario destruir todos los que están instanciados en tiempo de diseño, recompilarlos, crearlos de nuevo y asignarles los valores correctos de las propiedades.  Esto nos explica porque es que el Delphi cierra todas las ventanas al recompilar un paquete.

ir al índice


Revisión en profundidad para mostrar y esconder ventanas

Revisando varios foros de discusión, he encontrado repetidamente mensajes que contienen algo como lo siguiente:

- ¿ Es posible esconder la barra de Tareas ?

- ¿ Cómo ocultar los íconos del Escritorio ?

Las respuestas, a estas preguntas son inmediatas y breves y todas utilizan la función ShowWindow del API de Windows pasándole como parámetro SW_HIDE (o SW_SHOW).

Yo creo, que en general esto es evidente para cualquier programador. El problema está en cómo encontrar la ventana que debemos pasar a la función ShowWindow. Esta es precisamente la esencia del contenido de este texto.

Listar las ventanas existentes en el ambiente

El API de Windows suministra la función EnumWindows, mediante la cual podemos iterar por todas las ventanas registradas con el sistema operativo. Esta función tiene la siguiente sinopsis:

function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;

donde lpEnumFunc es un puntero a una función que es llamada iterativamente para cada ventana registrada en el sistema operativo, con la siguiente sinopsis:

function lpEnumProc(ww: HWND; lp: LPARAM): BOOL; stdcall;

Esta función tiene que ser construida por el programador y podemos utilizar este hecho para almacenar un listado de las clases diferentes de ventanas registradas en Windows.

La figura a continuación muestra el código de una implementación, donde las clases de las ventanas registradas son almacenadas en un listbox contenido en una forma.

{-----------------------------------------------------------
  Procedimiento llamado por EnumWindows para recorrer todas
  las ventanas que han sido creadas.
------------------------------------------------------------}

function lpEnumWndProc(ww: HWND; lp: LPARAM): BOOL; stdcall;
var
  sName : string;
  n : Integer;
begin
  Result := true;
  if lp = $ABAD then
  begin
    // Obtener el nombre de la clase de la ventana
    SetLength(sName, 255);
    n := GetClassName(ww, @sName[1], 255);
    if n = 0 then Exit;
    SetLength(sName, n);
    // Si el nombre de la clase de la ventana ya está
    // contenida en el listado, continuar la enumeración
    if Form1.cbWnd.Items.IndexOf(sName) >= 0 then Exit;
    // Adicionar el nombre de la clase de la ventana al listado
    Form1.cbWnd.Items.AddObject( sName, Pointer(ww) );
  end;
end;

Algunas ventanas conocidas de Windows

Antes de escribir este texto, ya teníamos hecho el código de ejemplo con que apoyamos su contenido. Así que exploramos las clases de ventanas registradas en Windows. Mediante prueba y error, encontramos algunas de las ventanas que siempre son conocidas por Windows. Utilizando la función

  SetWindowText(hWnd, 'Título de la ventana');

probamos cuales de ellas eran capaces de permitir la modificación de su título. La figura a continuación muestra el listado.

Nombre de la clase Observaciones
tooltips_class32 Utilizada por la barra de tareas de Windows. Admite cambiar el título
ExploreWClass Clase de la Ventana del Explorador de Windows. Admite cambiar el título y se muestra en la barra de captura (caption bar).
Progman Esta es la clase de la ventana utilizada para mostrar u ocultar los iconos de aplicaciones que aparecen en el Escritorio. No permite cambiar el título.
Shell_TrayWnd  Esta clase de ventana, constituye la barra de tareas de Windows. No permite cambiar el título.
SystemTray_Main Clase de la ventana correspondiente a administrador de Energía. Admite cambiar el título y se muestra en la barra de captura (caption bar).

El Escritorio (Desktop) es una ventana especial que cubre toda el área de la pantalla, sobre la cual se dibujan todas las otras ventanas. Para obtener el handle a la ventana del escritorio se usa la función GetDesktopWindow. Al Escritorio, se le puede poner título, lo que puede ser usado como una marca en Windows, ya que esta ventana es única. El Escritorio no puede ser escondido.

Llegado aquí para esconder la barra de tareas, escribiríamos

  ss := 'Shell_TrayWnd';
  ww := FindWindow( PChar(ss), nil);
  if ww <> 0 then
    ShowWindow(ww, SW_HIDE);

Igualmente procederíamos para esconder los íconos del Escritorio, estableciendo ss := 'Progman'

Un ejemplo demostrativo

Como ejemplo demostrativo hemos construido una forma en la que hemos colocado un control listbox. Un botón provoca que se llene el listado llamando a la función EnumWindows

Luego, podemos navegar por los elementos del listado y ver algunas de sus propiedades (ícono, título, etc) en un panel situado a la derecha del mismo.

Un control checkbox, nos permite mostrar y esconder la ventana seleccionada en el listado. De esta forma (aunque los nombres de clase de las mismas, son suficientemente explícitos) podemos reconocer a la ventana y buscar aquella que nos interesa.

ir al índice