Boletín Pascal #6
Los ejemplos completos de código fuente de este número están disponibles para descargar.
![]() |
![]() |
Boletín Pascal #6 INDICE 1. UNAS PALABRAS DEL EDITOR 2. HERRAMIENTAS GRATUITAS DE BORLAND 3. ESCANEANDO ARCHIVOS COMPRIMIDOS 4. EL NOVATO Y EL EXPERTO Constantes, variables y expresiones lógicas (booleanas) ________________________________________________________________________ 1. UNAS PALABRAS DEL EDITOR Muy pronto les estaremos enviando a todos nuestros suscriptores un cuestionario por email. Este cuestionario nos ayudará a evaluar este newsletter y ver que podemos hacer para satisfacer mejor sus necesi- dades. Es muy importante que contesten las preguntas y lo devuelvan para hacer que su opinión cuente en la definición de como serán las futuras ediciones. Contestar va a ser bastante rápido ya que la mayoría de las preguntas tendrán un conjunto de respuestas predefinidas para que ustedes elijan. Apreciaríamos mucho si se pudieran tomar el tiempo en participar de esta encuesta. Desde ya muchas gracias. Saludos, Ernesto De Spirito eds2004 @ latiumsoftware.com ________________________________________________________________________ JfControls Lib. Multilenguaje. Multiapariencia. Skins. Privilegios. Más de 40 componentes integrados y personalizables. Múltiples problemas de programación resueltos. Administración centralizada de recursos. Para Delphi 3-7 y C++ Builder 3-6. http://www.jfactivesoft.com/spindex.htm ________________________________________________________________________ 2. HERRAMIENTAS GRATUITAS DE BORLAND Unos pocos meses atrás, Borland lanzó el compilador Borland C/C++ 5.5 como freeware, una versión en línea de órdenes de 32 bits del compilador usado por C++ Builder, y viene con otras utilidades en línea de órdenes (preprocesador, enlazador, compilador y enlazador de recursos y otras cosas), ayuda (restringida a la operación de las herramientas única- mente), ficheros "include" y bibliotecas, y algunos ejemplos, pero por supuesto no incluye ni la VCL ni una referencia de C++. Si está inte- resado en descargarlo, puede encontrarlo aquí: http://www.borland.com/bcppbuilder/freecompiler/ (7.79 Mb) Más recientemente Borland también lanzó el Turbo Debugger como freeware, y no necesito decirles cuánto necesitarán esta herramienta para descubrir qué es lo que está mal en sus programas! http://www.borland.com/bcppbuilder/turbodebugger/ (590 Kb) No llegarán a la luna con estas herramientas, pero pueden pedir prestado algún viejo libro de C++ y aprender un poquito de C++ si quieren. ;) ________________________________________________________________________ 3. ESCANEANDO ARCHIVOS COMPRIMIDOS En el último número presentamos el componente TZipMaster y las DLLs Zip y Unzip (ZIPDLL.DLL y UNZDLL.DLL). Tal como prometimos, en este número usaremos este componente para habilitar nuestra aplicación de Búsqueda de Archivos para buscar ficheros dentro de archivos comprimidos. Para seguir los pasos necesitan tener la última versión de esta aplica- ción. Si son nuevos a este newsletter o si no lo han estado siguiendo últimamente, pueden descargarla de aquí: http://www.latiumsoftware.com/descarga/p0004.zip También necesitan tener el componente TZipMaster y las DLLs correcta- mente instalados. Pueden encontrar instrucciones en el último newsletter: http://www.latiumsoftware.com/es/pascal/0005.txt Bien, suficiente introducción. Vamos a trabajar. A propósito, en caso que se pregunten ;) algunas líneas nuevas o modificadas han sido marcadas con un asterisco ("*") para resaltarlas, y los puntos suspensivos ("...") significan que el resto es igual. 1) Incluimos al archivo de recursos de mensajes del componente ZipMaster en el archivo de programa: program FindFile; uses ... {$R *.RES} * {$R ZipMsgUS.RES} begin ... También agregamos un mensaje extra en el formulario principal: {$IFDEF Spanish} ... * cstrFileExists = '"%s" ya existe. ¿Sobreescribir?'; {$ELSE} ... * cstrFileExists = '"%s" already exists. Overwrite?'; {$ENDIF} Este es el mensaje que se mostrará al usuario cuando tengamos que descomprimir un fichero en el directorio temporal y ya exista un fichero con ese nombre. 2) Definimos algunas funciones nuevas en la unidad Common: unit Common; interface * uses classes, windows; ... * function FileNameMatchesFilespec(const FileName, Filespec: string): * boolean; * function GetAssociatedSmallIcon(const FileName: string): HICON; * function GetTempDir: string; * function GetWindowsDir: string; * function GetSystemDir: string; implementation * uses sysutils, registry, shellapi, filectrl; ... // =================================================================== function FileNameMatchesFilespec(const FileName, Filespec: string): boolean; // Devuelve True si FileName (por ejemplo 'RESUME.DOC') coincide con // la especificación de ficheros Filespec (por ejemplo 'R*.DO?'). var WName, WExt, FName, FExt: string; // ------------- function ExpressionMatch(const s1, s2: string): boolean; var i, n, n1, n2: integer; p1, p2: pchar; begin n1 := Length(s1); n2 := Length(s2); if n1 < n2 then n := n1 else n := n2; p1 := pchar(s1); p2 := pchar(s2); for i := 1 to n do begin if p2^ = '*' then begin Result := True; exit; end; if (p2^ <> '?') and (p2^ <> p1^) then begin Result := False; exit; end; inc(p1); inc(p2); end; if n1 = n2 then Result := True else if n1 > n2 then Result := False else begin // n1 < n2 for i := n1 + 1 to n2 do begin if (p2^ <> '*') and (p2 <> '?') then begin Result := False; exit; end; inc(p2); end; Result := True; end; end; // ------------- begin WName := AnsiUpperCase(ExtractFileName(Filespec)); WExt := ExtractFileExt(WName); WName := Copy(WName, 1, Length(WName) - Length(WExt)); FName := AnsiUpperCase(ExtractFileName(FileName)); FExt := ExtractFileExt(FName); FName := Copy(FName, 1, Length(FName) - Length(FExt)); if WName = '' then WName := '*'; if WExt = '' then WExt := '.*'; if FExt = '' then FExt := '.'; Result := ExpressionMatch(FName, WName) and ExpressionMatch(FExt, WExt); end; // =================================================================== function RCPos(c: char; const s: string): integer; // Devuelve la posición de la última ocurrencia (o la primera de // derecha a izquierda) de un caracter en una cadena var i: integer; p: pchar; begin i := Length(s); p := pchar(s) + i - 1; for i := i downto 1 do begin if p^ = c then begin Result := i; exit; end; dec(p); end; Result := 0; end; // =================================================================== function GetTempDir: string; // Devuelve el directorio temporal de Windows var TmpDir: array [0..MAX_PATH-1] of char; begin SetString(Result, TmpDir, GetTempPath(MAX_PATH, TmpDir)); Result := ExcludeTrailingBackslash(Result); if not DirectoryExists(Result) then begin Result := GetWindowsDir + '\TEMP'; if not DirectoryExists(Result) then try MkDir(Result); except Result := ExtractFileDrive(Result) + '\TEMP'; if not DirectoryExists(Result) then try MkDir(Result); except Result := ExtractFileDrive(Result) + '\TMP'; if not DirectoryExists(Result) then try MkDir(Result); except Result := ''; end; end; end; end; end; // =================================================================== function GetWindowsDir: string; // Devuelve el directorio de Windows var WinDir: array [0..MAX_PATH-1] of char; begin SetString(Result, WinDir, GetWindowsDirectory(WinDir, MAX_PATH)); end; // =================================================================== function GetSystemDir: string; // Devuelve el directorio System de Windows var SysDir: array [0..MAX_PATH-1] of char; begin SetString(Result, SysDir, GetSystemDirectory(SysDir, MAX_PATH)); end; // =================================================================== function GetAssociatedSmallIcon(const FileName: string): HICON; // Devuelve el icono pequeño de un fiechero o documento dado, ya sea // que realmente exista en el disco o no. var IconIndex: word; SmallIconHandle: HIcon; FileExt, FileType, IconSource: String; p: integer; Reg: TRegistry; PLargeIconHandle: ^HIcon; begin FileExt := UpperCase(ExtractFileExt(FileName)); IconIndex := 0; if ((FileExt = '.EXE') or (FileExt = '.ICO')) and FileExists(FileName) then begin IconSource := FileName; end else begin if FileExt = '.EXE' then FileExt := '.COM'; FileType := ''; IconSource := ''; Reg := TRegistry.Create(KEY_QUERY_VALUE); Reg.RootKey := HKEY_CLASSES_ROOT; if Reg.OpenKeyReadOnly(FileExt) then begin FileType := Reg.ReadString(''); Reg.CloseKey; end; // if if FileType <> '' then begin if Reg.OpenKeyReadOnly(FileType + '\DefaultIcon') then begin IconSource := Reg.ReadString(''); Reg.CloseKey; end; end; Reg.Free; if IconSource = '' then begin IconSource := GetSystemDir + '\SHELL32.DLL'; if FileExt = '.DLL' then IconIndex := 66; end else begin p := RCPos(',', IconSource); if p <> 0 then begin IconIndex := StrToInt(Copy(IconSource, p + 1, Length(IconSource) - p)); IconSource := Copy(IconSource, 1, p - 1); end; end; end; PLargeIconHandle := nil; if ExtractIconEx(pchar(IconSource), IconIndex, PLargeIconHandle^, SmallIconHandle, 1) <> 1 then begin IconSource := GetSystemDir + '\SHELL32.DLL'; if FileExt = '.EXE' then IconIndex := 2 else if FileExt = '.COM' then IconIndex := 2 else if FileExt = '.INI' then IconIndex := 63 else if FileExt = '.INF' then IconIndex := 63 else if FileExt = '.BAT' then IconIndex := 65 else if FileExt = '.DLL' then IconIndex := 66 else IconIndex := 0; if ExtractIconEx(pchar(IconSource), IconIndex, PLargeIconHandle^, SmallIconHandle, 1) <> 1 then Result := 0 else Result := SmallIconHandle; end else Result := SmallIconHandle; end; // =================================================================== end. 3) Agregamos dos componentes ZipMaster al formulario y los llamamos Zip1 y Zip2. Usaremos uno para el "hilo" (thread) de búsqueda y el otro para descomprimir un fichero en el directorio temporal cuando el usuario le haga doble-clic, así podemos abrirlo con su aplicación asociada. 4) Agregamos una casilla de verificación (CheckBox) así los usuarios pueden especificar si quieren escanear dentro de archivos Zip o no. La llamamos "chkScanZIPs" y la etiquetamos como "Revisar archivos &ZIP". Agregamos código para habilitar esta nueva casilla de verificación cuando sea apropiado: procedure TForm1.Button1Click(Sender: TObject); var c: char; begin ... Checkbox1.Enabled := False; * chkScanZIPs.Enabled := False; Button2.Enabled := True; ... end; procedure TForm1.Thread1Done(var AMessage: TMessage); begin ... Checkbox1.Enabled := True; * chkScanZIPs.Enabled := True; ... end; 5) Agregamos código en TThread1.Execute para cargar la DLL Unzip (si se necesita) antes de la búsqueda. begin // procedure TThread1.Execute; Count := 0; Synchronize(Initialize); Keywords := TStringList.Create; GetKeywords(Keywords, OwnerForm.Edit2.Text); n := Keywords.Count - 1; for i := 0 to n do Keywords[i] := UpperCase(Keywords[i]); * if OwnerForm.chkScanZIPs.Checked then * OwnerForm.Zip1.Load_Unz_Dll; ScanFolder(OwnerForm.Edit3.Text); Keywords.Free; Synchronize(Finalize); end; Load_Unz_Dll no hará nada si la DLL ya está cargada. La DLL será automá- ticamente descargada cuando el componente sea destruido (ocurrirá cuando el formulario sea destruido). 6) Agregamos un bloque condicional en el procedimiento ScanFolder para evitar cargar y escanear archivos ZIP dado que no son como los archivos ordinarios y los trataremos separadamente. También agregamos una línea para liberar la memoria ocupada por la cadena "Content" una vez que no la necesitamos más. procedure ScanFolder(const folder: string); var SearchRec: TSearchRec; i: integer; begin if FindFirst(folder + OwnerForm.Edit1.Text, faReadOnly Or faHidden Or faSysFile Or faArchive, SearchRec) = 0 then begin Location := folder; repeat try FileName := SearchRec.Name; * if UpperCase(ExtractFileExt(FileName)) <> '.ZIP' then begin Content := UpperCase(LoadFile(folder + FileName)); Score := 0; for i := 0 to n do if Pos(Keywords[i], Content) <> 0 then inc(Score); * Content := ''; // Libera la memoria ocupada if Score > 0 then begin inc(Count); Time := FileDateToDateTime(SearchRec.Time); Synchronize(AddFileName); end; // if * end; // if except end; // try until Terminated Or (FindNext(SearchRec) <> 0); end; // if FindClose(SearchRec); 7) Agregamos el siguiente código inmediatamente después del que acabamos de reproducir arriba: if OwnerForm.chkScanZIPs.Checked then begin if FindFirst(folder + '*.ZIP', faReadOnly Or faHidden Or faSysFile Or faArchive, SearchRec) = 0 then begin repeat try ScanZip(folder + SearchRec.Name); except end; // try until Terminated Or (FindNext(SearchRec) <> 0); end; // if FindClose(SearchRec); end; Este código busca todos los archivos .ZIP en el directorio (referenciado por la variable "folder") y llama a ScanZip para procesarlos. 8) Definimos el procedimiento ScanZip dentro del procedimiento TThread1.Execute, justo antes de ScanFolder. procedure TThread1.Execute; var ... // ------------------------------------- procedure ScanZip(const ZipName: string); var i, j: integer; PZipDirEntry: ^ZipDirEntry; ZipStream: TZipStream; begin OwnerForm.Zip1.ZipFileName := ZipName; for j := 0 to OwnerForm.Zip1.Count - 1 do begin try PZipDirEntry := OwnerForm.Zip1.ZipContents[j]; if UpperCase(ExtractFileExt(PZipDirEntry.FileName)) = '.ZIP' then begin // A Zip inside a Zip. Skip it. end else if PZipDirEntry.UncompressedSize > 0 then begin FileName := ExtractFileName(PZipDirEntry.FileName); if FileNameMatchesFilespec(FileName, OwnerForm.Edit1.Text) then begin ZipStream := OwnerForm.Zip1.ExtractFileToStream( PZipDirEntry.FileName); SetString(Content, PChar(ZipStream.Memory), ZipStream.Size); ZipStream.Clear; Content := UpperCase(Content); Score := 0; for i := 0 to n do if Pos(Keywords[i], Content) <> 0 then inc(Score); Content := ''; // Libera la memoria ocupada if Score > 0 then begin inc(Count); Location := ZipName + '?' + ExtractFilePath(PZipDirEntry.FileName); Time := FileDateToDateTime(PZipDirEntry.DateTime); Synchronize(AddFileName); end; // if end; // if end; finally end; if Terminated then break; end; // for j end; // ------------------------------------- procedure ScanFolder(const folder: string); ... Lo que este procedimiento hace es primero abrir el archivo ZIP y luego procesar todos sus ficheros en un bucle for..do. Si el fichero dentro del archivo es un archivo ZIP o si su longitud es 0, o si no concuerda con la especificación de ficheros, simplemente lo ignoramos. Decidimos descomprimir los ficheros en memoria, usando el método ExtractFileToStream que devuelve una corriente (Stream, una descendiente de TMemoryStream para ser precisos). Luego ponemos el contenido de esta corriente en una cadena y buscamos las palabras clave como hicimos con los ficheros normales una vez que estaban cargados en una cadena. Si encontramos alguna coincidencia, en vez de solamente el directorio, establecemos "Location" como el camino y nombre completo del archivo ZIP más un signo de interrogación (para señalizar que se trata de un archivo ZIP) y el camino relativo del fichero dentro del archivo. Por ejemplo C:\ZIPARCH.ZIP?VCL\ significa que la ubicación del fichero es el directorio VCL dentro del archivo C:\ZIPARCH.ZIP. 9) La API ExtractIcon sólo funciona con ficheros que están en el disco, pero ahora éste no será siempre el caso porque algunos ficheros estarán dentro de un archivo, así que modificamos TThread1.AddFileName para usar la función GetAssociatedSmallIcon que habíamos escrito en la unidad Common. procedure TThread1.AddFileName; // Synchronized var * ListItem: TListItem; * Icon: TIcon; * IconHandle: HIcon; begin ... ListItem.Caption := FileName; * IconHandle := GetAssociatedSmallIcon(Location + FileName); * Icon := TIcon.Create; * if IconHandle <> 0 then Icon.Handle := IconHandle; * ListItem.ImageIndex := OwnerForm.ImageList1.AddIcon(Icon); * Icon.Free; ListItem.SubItems.Add(Location); ... end; 10) Editamos la propiedad Items de PopupMenu1 y le agregamos un nuevo elemento llamado OpenArchive1, lo etiquetamos como "Abrir Archivo" (propiedad Caption), y luego generamos su evento Click: * procedure TForm1.OpenArchive1Click(Sender: TObject); * begin * OpenArchive(SelectedItem); * end; OpenArchive es un nuevo método que declaramos en las declaraciones privadas del formulario, junto con otros nuevos métodos: TForm1 = class(TForm) ... private { Private declarations } ... * procedure ExecuteAssociation(const FileName: string); * procedure OpenFolder(ListItem: TListItem); * procedure OpenFile(ListItem: TListItem); * procedure OpenArchive(ListItem: TListItem); public { Public declarations } end; Definimos estos métodos en la sección "implementation" como sigue: procedure TForm1.ExecuteAssociation(const FileName: string); begin if ShellExecute(Self.Handle, nil, PChar(FileName), nil, nil, SW_SHOWMAXIMIZED) <= 32 then Application.MessageBox(cstrCouldNotExecApp, 'Error', MB_ICONEXCLAMATION); end; // ------------------------------------------------------------------- procedure TForm1.OpenFolder(ListItem: TListItem); var p: integer; Folder: string; begin Folder := ListItem.SubItems.Strings[0]; p := Pos('?', Folder); if p <> 0 then begin SetLength(Folder, p-1); Folder := ExtractFilePath(Folder); end; ExecuteAssociation(Folder); end; // ------------------------------------------------------------------- procedure TForm1.OpenFile(ListItem: TListItem); var p: integer; Folder, FileName, CurrentDir: string; begin Folder := ListItem.SubItems.Strings[0]; p := Pos('?', Folder); if p = 0 then FileName := Folder + ListItem.Caption else begin // Compressed file FileName := GetTempDir + '\' + ListItem.Caption; if FileExists(FileName) then if Application.MessageBox(PChar(Format(cstrFileExists, [FileName])), 'Warning', MB_ICONQUESTION or MB_YESNO) = IDNO then exit; Zip2.Load_Unz_Dll; Zip2.ZipFileName := Copy(Folder, 1, p - 1); Zip2.FSpecArgs.Add(Copy(Folder, p + 1, Length(Folder) - p) + ListItem.Caption); Zip2.ExtrOptions := [ExtrOverWrite]; CurrentDir := GetCurrentDir; ChDir(ExtractFilePath(FileName)); try Zip2.Extract; finally ChDir(CurrentDir); end; if not FileExists(FileName) then exit; end; ExecuteAssociation(FileName); end; // ------------------------------------------------------------------- procedure TForm1.OpenArchive(ListItem: TListItem); var Archive: string; begin Archive := ListItem.SubItems.Strings[0]; SetLength(Archive, Pos('?', Archive) - 1); ExecuteAssociation(Archive); end; // ------------------------------------------------------------------- 11) Modificamos TForm1.ListView1DblClick, TForm1.Open1Click y TForm1.OpenFolder1Click para llamar a estos nuevos métodos en vez de manejar las cosas por ellos mismos: procedure TForm1.ListView1DblClick(Sender: TObject); var Col: Integer; ListItem: TListItem; begin ListItem := TListViewX(ListView1).GetItemAtX(Last.X, Last.Y, Col); * if ListItem <> nil then * if Col = 0 then * OpenFile(ListItem) * else if Col = 1 then * if Pos('?', ListItem.SubItems.Strings[0]) <> 0 then * OpenArchive(ListItem) * else * OpenFolder(ListItem); end; ... procedure TForm1.Open1Click(Sender: TObject); begin * OpenFile(SelectedItem); end; // ------------------------------------------------------------------- procedure TForm1.OpenFolder1Click(Sender: TObject); begin * OpenFolder(SelectedItem); end; 11) Modificamos TForm1.ListView1MouseDown y TForm1.ListView1KeyDown para habilitar/inhabilitar el nuevo elemento del menú antes de invocar el menú contextual: procedure TForm1.ListView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); ... if (SelectedItem <> nil) and (Col <= 1) then begin * PopupMenu1.Items[2].Enabled := * Pos('?', SelectedItem.SubItems[0]) <> 0; * if PopupMenu1.Items[2].Enabled then * PopupMenu1.Items[2].Default := True * else * PopupMenu1.Items[Col].Default := True; PopupMenu1.Popup( ... procedure TForm1.ListView1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); ... if SelectedItem <> nil then begin * PopupMenu1.Items[2].Enabled := * Pos('?', SelectedItem.SubItems[0]) <> 0; PopupMenu1.Items[0].Default := True; PopupMenu1.Popup( ... Ok, eso debería ser todo. Ya puede intentar ejecutar el ejemplo. Si es muy vago para hacer todos los cambios por usted mismo ;) o si tiene algún problema :( puede descargar el código fuente completo: http://www.latiumsoftware.com/descarga/p0006.zip ________________________________________________________________________ 4. EL NOVATO Y EL EXPERTO Constantes, variables y expresiones lógicas (booleanas) En esta edición hemos decidido comenzar esta nueva sección que apunta a retratar las diferencias entre los estilos de programación de novatos y programadores expertos. Si tiene más ejemplos como los de abajo, por favor compártalos con nosotros para que podamos publicarlos. Hemos decidido comenzar con expresiones booleanas dado que quizás marquen las más notorias diferencias. En los siguientes ejemplos, se supone que "b" es una variables booleana (de tipo boolean)... NOVATO: if a[i] = x then b := True else b := False; EXPERTO: b := a[i] = x; Explicación: "a[i] = x" es una expresión de comparación, y como tal es una expresión booleana, es decir, se evalúa como True o False (verdadero o falso), así que podemos asignar el resultado de la expresión directa- mente a nuestra variable booleana. Por ejemplo, si "a[i] = x" es True (verdadero), éste es el valor que será asignado a "b", y si es False (falso), a "b" se le asignará False. ¿Correcto? -------------- NOVATO: if a[i] = x then b := False else b := True; EXPERTO: b := a[i] <> x; Explicación: Este es similar al ejemplo anterior, pero en este caso invertimos el operador de comparación para invertir el resultado. Por ejemplo, si "a[i] = x" es True, "a[i] <> x" será False, y éste es el valor asignado a "b", y si "a[i] = x" es False, "a[i] <> x" será True y éste es el valor que se asigna a "b". En lugar de "a[i] <> x" podríamos haber escrito "not (a[i] = x)", pero hace la expresión más compleja puesto que usa un operador más, y además es un poco más difícil de leer. -------------- NOVATO: if b = True then c = '*'; EXPERTO: if b then c = '*'; Explicación: El "if" ya de por sí evalúa si la condición es True, así que nosotros no tenemos que hacerlo. -------------- NOVATO: if b = False then c = '*'; EXPERTO: if not b then c = '*'; Explicación: Ahora necesitamos actuar en la falsedad de la condición, pero en lugar de preguntar si es False, podemos invertir la condición con un "not", así que estaríamos preguntando si no es True, en su lugar. -------------- Tenemos que decir que en cada uno de los ejemplos que acabamos de presentar arriba, ambas formas de programación son correctas y en teoría el compilador de Delphi es lo suficientemente inteligente como para producir idéntico código de máquina. La única diferencia es que los profesionales escriben menos y muestran que entienden las expresiones booleanas... ________________________________________________________________________ Si no has recibido el archivo con el código fuente completo de los ejemplos que se presentan en este boletín, puedes descargarlo de la siguiente dirección: http://www.latiumsoftware.com/descarga/p0006.zip ________________________________________________________________________ Página principal: http://www.latiumsoftware.com/es/pascal/index.php Página del grupo: http://espanol.groups.yahoo.com/group/boletin-pascal/ Para suscribirse / apuntarse: boletin-pascal-subscribe@gruposyahoo.com Para cancelar / removerse: boletin-pascal-unsubscribe@gruposyahoo.com Para reportar problemas con la suscripción: eds2004 @ latiumsoftware.com ________________________________________________________________________ Este boletín se provee "TAL Y COMO ESTA", sin garantía de ninguna clase. Su uso implica la aceptación de nuestros términos de licencia y de la ausencia de garantía que puedes leer en nuestro sitio web. Allí también encontrarás una nota sobre marcas registradas. Te animamos a que redis- tribuyas este boletín, siempre y cuando lo hagas en forma completa (incluyendo la información de copyright), sin modificaciones y de manera gratuita. Los artículos son copyright de sus respectivos autores y se reproducen aquí con el permiso de los mismos. ________________________________________________________________________ Latium Software http://www.latiumsoftware.com/es/index.php Copyright (c) 2000 por Ernesto De Spirito. Todos los derechos reservados ________________________________________________________________________ |
Los ejemplos completos de código fuente de este número están disponibles para descargar.
![]() |
¿Errores? ¿Omisiones? ¿Comentarios? Por favor contáctanos!






