PDA

Ver la Versión Completa : Crear una clase descendiente de TInifile


bucanero
12-07-2018, 12:54:36
Hola a todos

Estoy intentando crear una clase descendiente de TIniFile, con la idea de tener una unidad para gestionar la configuración de laS aplicaciones de tal forma que si se le pasa alguna clase descendiente de un TINIFILE, trabaje con esta clase, en caso de no declara nada que trabaje con la clase general de TINIFile.

Este es el código para la unidad de configs.pas

unit configs;

interface

uses inifiles;

type
TClassIniFile = class of TInifile;

var
ClassIniFile: TClassIniFile = nil;
IniFileName: string = 'c:\tmp\config.ini';


function config: TIniFile;

implementation

uses Dialogs;

var
IniFile:TInifile = Nil;

function config: TIniFile;
begin
if not Assigned(IniFile) then begin
if not Assigned(ClassIniFile) then begin
//Si no hay definida una clase especifica se usa la clase generia de INIFILE
IniFile := TiniFile.Create(IniFileName)
end
else begin
// se usa la clase especifica
IniFile := ClassIniFile.Create(IniFileName)
end;
// esto es solo en pruebas y es para mostrar que clase se esta utilizando
MessageDlg(IniFile.ClassName, mtWarning, [mbOK], 0);
end;
Result := IniFile;
end;

initialization

finalization
if assigned(IniFile) then
IniFile.Free;
end.


Y la forma de utilizarlo en cualquier parte del programa:


uses configs;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
with config do
WriteString('prueba', 'prueba', '123');
end;


y así es como redefino una nueva clase descendiente de TInifile

unit MyIniFiles;

interface

uses IniFiles;

type
TMyIniFile = class(TIniFile)
private
public
constructor Create(const FileName: string); overload;
end;

implementation

uses dialogs, configs;

{ TMyIniFile }
constructor TMyIniFile.Create(const FileName: string);
begin
// esto es solo en pruebas y es para mostrar si pasa por aqui
MessageDlg('TMyIniFile.create', mtInformation, [mbOK], 0);
inherited Create(FileName);
end;

initialization
ClassIniFile := TMyIniFile;
end.



Y aquí es donde viene el problema, tal como esta todo esto declarado, el código del constructor de la clase descendiente (TMyIniFile.Create) no se ejecuta al crear la instancia del objeto desde la variable ClassIniFile y pasa directamente a ejecutar el constructor de la clase padre (TiniFile.create).

Por el contrario si en el procedimiento config lo pusiera de esta forma, entonces si que funciona perfectamente:

function config: TIniFile;
begin
if not Assigned(IniFile) then begin
if not Assigned(ClassIniFile) then
//Si no hay definida una clase especifica se usa la clase generia de INIFILE
IniFile := TiniFile.Create(IniFileName)
else if (ClassIniFile = TMyIniFile) then
// se usa la clase especifica
IniFile := TMyIniFile.Create(IniFileName)
else
// se usa la clase especifica
IniFile := ClassIniFile.Create(IniFileName);
// esto es solo en pruebas y es para mostrar que clase se esta utilizando
MessageDlg(IniFile.ClassName, mtWarning, [mbOK], 0);
end;

Result := IniFile;
end;


Pero de esta forma no quiero ponerlo, por que así estaría forzado a tener que añadir siempre en el uses de la unidad configs la unidad que contiene la clase descendiente, pudiendo estar unas veces incluida en el proyecto y otras veces no.

Me explico mas, mi idea es si en una determinada aplicación que utiliza la unidad CONFIGS le agrego la otra unidad con la clase descendiente, entonces esa aplicación usara esa clase descendiente para gestionar como y donde guardar la configuración, pero si en otra aplicación distinta que también usa la misma unidad CONFIGS, no tiene agregada ninguna unidad descendiente de TINIFILE, entonces se pretende que utilice la clase genérica de TINIFILE.

No se si alguien me puede arrojar luz sobre por que no se ejecuta la clase constructora descendiente al llamarla desde la variable que contiene la clase descendiente.

Gracias por vuestro tiempo
y saludos

Neftali [Germán.Estévez]
13-07-2018, 11:23:35
Creo que el problema está en que el create de la clase base:

constructor Create(const FileName: string);


Está definido de esta forma. Ni virtual, ni abstract,...


Si añades un método como este a tu clase:

TMyIniFile = class(TIniFile)
private
public
constructor Create(const FileName: string); overload;
procedure WriteString(const Section, Ident, Value: String); override;
end;




Comprobará que realmente el objeto es de la clase correcta, porque al ejecutar este código:



with config do
WriteString('prueba', 'prueba', '123');




Realmente ejecuta el método:



procedure TMyIniFile.WriteString(const Section, Ident, Value: String);

bucanero
13-07-2018, 13:59:04
Hola Neftali, gracias por responder!!

Si después de unas cuantas pruebas llegue a esa misma conclusión, el problema se encuentra en esta definición class of TInifile que es la que determina el orden de búsqueda, y es que al no poder hacer override sobre el constructor por que la clase padre no lo permite, si el compilador se encuentra con varias clases constructoras declaradas de forma idéntica, entonces la búsqueda de procesos la realiza internamente a algo parecido a una lista con esta forma:


1:TINIFile{ constructor Create(const FileName: string); }
2: Resto de clases ascendentes a TINIFile
...
N:TMyIniFile { constructor Create(const FileName: string); }

así que el compilador simplemente coge el primer método que encuentra en la lista que sea del tipo que busca, en este caso el definido para TINIFile.

Y lo solucione creando una clase intermedia (TAvIniFile) donde se define el constructor de forma virtual para así poder sobre-escribirlo en las clases descendientes,



interface
type
TAvIniFile = class(TIniFile)
public
// se marca el constructor como virtual para poder sobre-escribirlo mas adelante
constructor Create(const FileName: string); overload; virtual;
end;

TMyIniFile = class(TAvIniFile )
private
public
constructor Create(const FileName: string); override; // ya si se puede usar el override
end;

TClassIniFile = class of TAvIniFile;

implementation

constructor TAvIniFile.Create(const FileName: string);
begin
inherited;
// aqui no es necesario insertar nada mas, solo esta definido para poder hacer override despues
end;

...



y finalmente sustituyo la declaración de classInifile por TclassInifile = class of TAvInifile, de esta forma la lista de búsqueda quedaría así:

1: clases descendientes a TAvIniFile con el constructor sobre-escrito, aquí se incluye TMyIniFile
2: TAvIniFile{ constructor Create(const FileName: string); }
3: TINIFile{ constructor Create(const FileName: string); }
4: Resto de clases ascendentes a TINIFile


Gracias de nuevo por responder
Un saludo

dec
13-07-2018, 14:45:12
Hola a todos,

Ha estado bien visto esto último, bucanero. Yo, en mi ignorancia, iba a responder de entrada que el método constructor de tu clase INI tenía que ser "override" y no "overload". Pero, al probarlo aquí, me dí cuenta de que no era tan sencillo como hacer esto... no alcancé yo hasta donde llegó el compañero Neftalí, que fue descubrir que el constructor de la clase TIniFile no puede sobrescribirse. Vamos, ni alcabcé yo donde el Neftalí llegó, ni por supuesto se me ocurrió pensar en hacer lo que tú al final has hecho. Está bien saberlo. :)

Neftali [Germán.Estévez]
13-07-2018, 14:54:39
Yo, en mi ignorancia, iba a responder de entrada que el método constructor de tu clase INI tenía que ser "override" y no "overload". Pero, al probarlo aquí, me dí cuenta de que no era tan sencillo como hacer esto...


Si, yo también me di cuenta del cambio de palabra overload/override David, pero el problema está en la definición de las clases base, que como he dicho no están definidas para que se puedan "sobreescribir".
Como bien dices, al realizar el cambio da el error de "Cannot override a non-virtual method".

movorack
13-07-2018, 16:49:25
Puedes usar reintroduce (http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Methods_(Delphi)#Reintroduce)


unit Unit1;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, IniFiles;

type
TMyIniFile = class(TIniFile)
private
public
constructor Create(const FileName: string); reintroduce;
end;

TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
FConfig: TMyIniFile;
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

{ TMyIniFile }

constructor TMyIniFile.Create(const FileName: string);
begin
inherited Create(FileName);

// esto es solo en pruebas y es para mostrar si pasa por aqui
MessageDlg('TMyIniFile.create', mtInformation, [mbOK], 0);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
FConfig := TMyIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
end;

end.


https://i.imgur.com/Zt3FZI8.png

https://i.imgur.com/3HNpRjI.png

bucanero
13-07-2018, 17:21:24
Gracias DEC y movorack por responder

;527591']Si, yo también me di cuenta del cambio de palabra overload/override David, pero el problema está en la definición de las clases base, que como he dicho no están definidas para que se puedan "sobreescribir".
Como bien dices, al realizar el cambio da el error de "Cannot override a non-virtual method".

Ese es el error que me estuvo trastocando todo el tiempo :confused: y la necesidad de buscar alguna alternativa al problema.


En cuanto al reintroduce también lo intente y el resultado fue el mismo. En tu código te ha funcionado porque has llamado directamente al método create de la clase TMyIniFile, y de este modo incluso con el overload funciona
Puedes usar reintroduce (http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Methods_(Delphi)#Reintroduce)


FConfig := TMyIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));



pero en mi código la llamada es a traves de una variable que apunta a una clase derivada de TINIFile y no de TMyIniFile, y para este caso el reintroduce también sigue fallando.


type
TClassIniFile = class of TInifile;

var
ClassIniFile: TClassIniFile = nil;
...
ClassIniFile:=TMyIniFile;
ClassIniFile.create(IniFileName);




Gracias a todos por dedicarle tiempo a esta cuestión
Un saludo

gatosoft
13-07-2018, 19:03:22
hola bucanero.

Sugiero que utilices la RTTI. Para ello debes cambiar tu codigo de la unidad configs asi:

unit configs;

interface

uses inifiles;

type
TClassIniFile = class of TInifile;

var
ClassIniFile: TClassIniFile = nil;
IniFileName: string = 'c:\tmp\config.ini';
ClassIniFileName: String = '';

function config: TIniFile;

implementation

uses Dialogs, SysUtils, ObjectClone, RTTI;

var
IniFile:TInifile = Nil;

function config: TIniFile;
var
RttiContext: TRttiContext;
RttiType: TRttiInstanceType;
Begin
if Assigned(IniFile) then
exit(IniFile);

if ClassIniFileName <> '' then
begin
RttiContext:= TRttiContext.Create;
RttiType := RttiContext.FindType(ClassIniFileName) as TRttiInstanceType;

IniFile:= RttiType.GetMethod('Create')
.Invoke(RttiType.MetaclassType,
[IniFileName]).AsObject as TIniFile;

end
else
IniFile := TIniFile.Create(IniFileName);

Result:= IniFile;
end;


initialization

finalization
if assigned(IniFile) then
IniFile.Free;
end.



y tus unidades descendientes, algo asi:

unit MyIniFiles;

interface

uses IniFiles, uIMyInis;

type
TMyIniFile = class(TIniFile)
private
public
class procedure mydummy; static;
constructor Create(const FileName: string);
end;

implementation

uses dialogs, configs;

{ TMyIniFile }

constructor TMyIniFile.Create(const FileName: string);
begin
inherited Create(FileName);
MessageDlg('TMyIniFile2.Crear', mtInformation, [mbOK], 0);
end;

class procedure TMyIniFile.mydummy;
begin

end;

Initialization
TMyIniFile.mydummy; //Me encontré con este error (https://stackoverflow.com/questions/3460382/delphi-2010-rtti-rtticontext-findtype)
ClassIniFileName:='MyIniFiles.TMyIniFile';
end.

gatosoft
13-07-2018, 19:12:59
Igual, amigo bucanero, creo que vas por un lado que no es. Lo que tu quieres implementar, puede salir mejor utilizando interfaces ==> El problema con el caso específico que propones es que las interfaces no admiten constructores....

movorack
13-07-2018, 19:41:46
Al fin no nos tomaron la foto donde TopX, Tu y yo estábamos revisando este hilo.

bucanero
16-07-2018, 10:36:09
Gracias gatosoft por responder

hola bucanero.

Sugiero que utilices la RTTI. Para ello debes cambiar tu codigo de la unidad configs asi:


En cuanto a tu sugerencia de usar la RTTI es por donde en un principio pensé que podría ir la solución a este problema, aunque en estos temas de la RTTI me pierdo...

Viendo tu código me encuentro que en la linea donde debe de buscar y localizar la classe devuelve NIL y no la encuentra.


RttiType := RttiContext.FindType(ClassIniFileName) as TRttiInstanceType;



El problema imagino que viene de que para que la pueda localizar hay que registrar previamente la clase.

RegisterClass(TMyIniFile);

Pero aquí aparece otro problema, y es que el procedimiento RegisterClass(AClass: TPersistentClass) requiere que la clase a registrar sea descendiente de TPersistentClass, y yo estoy intentando registrar una clase descendiente de TINIFile que a su vez es una una classe descendiente directamente de TObject y por este motivo el compilador da el siguiente error E2010 Incompatible types: 'TPersistentClass' and 'class of TMyIniFile'


Igual, amigo bucanero, creo que vas por un lado que no es. Lo que tu quieres implementar, puede salir mejor utilizando interfaces ==> El problema con el caso específico que propones es que las interfaces no admiten constructores....
En cuanto a esta propuesta... sinceramente, no veo como aplicar una solución...

Porque mi idea final a parte de lo ya explicado es sobre-escribir los métodos de lectura/escritura de mi clase descendiente de TINIFile, para que en vez de guardar los datos en un fichero los guarde directamente en la BBDD, manteniendo la máxima compatibilidad con el componente original para que en determinados casos que no quiera utilizar mi componente propio y/o guardar en la BBDD puede usar directamente un TINIFile.

bucanero
16-07-2018, 10:39:10
Hola Movorack, realmente no se a que te refieres con esto :confused:....
Al fin no nos tomaron la foto donde TopX, Tu y yo estábamos revisando este hilo.

gatosoft
16-07-2018, 16:46:45
En cuanto a tu sugerencia de usar la RTTI es por donde en un principio pensé que podría ir la solución a este problema, aunque en estos temas de la RTTI me pierdo...
Viendo tu código me encuentro que en la linea donde debe de buscar y localizar la classe devuelve NIL y no la encuentra.
El problema imagino que viene de que para que la pueda localizar hay que registrar previamente la clase.


Yo también me encontré con ese problema del Nil, y por eso linkeaba la solución en ésta linea de código.

Initialization
TMyIniFile.mydummy; //Me encontré con este error (https://stackoverflow.com/questions/3460382/delphi-2010-rtti-rtticontext-findtype)



en el hilo se dice:


Probably the class has not included by the delphi linker in the final executable. A fast try can be the following:

1.Declare a static method on your class. This method should be an empty method, a simple begin end.
2.Call this static method in the initialization section of this unit.
3. Ensure that the unit is referenced in your project somewhere.
4.Now you should see the class with TRttiContext.FindType



En cuanto a esta propuesta... sinceramente, no veo como aplicar una solución...

Porque mi idea final a parte de lo ya explicado es sobre-escribir los métodos de lectura/escritura de mi clase descendiente de TINIFile, para que en vez de guardar los datos en un fichero los guarde directamente en la BBDD, manteniendo la máxima compatibilidad con el componente original para que en determinados casos que no quiera utilizar mi componente propio y/o guardar en la BBDD puede usar directamente un TINIFile.

Bueno, eso es nuevo, creí que querías extender la funcionalidad, y no sobre escribirla.

Existe una técnica que te puede servir y es la de interceptores, pero como todo, no es para abusar de ella.

y funciona mas o menos asi:

En la clausula Uses defines la unidad Inifiles, que contiene la implementación nativa del componente.

Después, defines la clase "interceptora" TIniFile (Tiene el mismo nombre de la nativa) la cual hereda de System.IniFiles.TIniFile (debes especificar la unidad).

Y en esta nueva clase, puedes definir nueva funcionalidad y sobreescribir la existente.



unit Unit1;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, IniFiles, Vcl.StdCtrls;

type
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;


TIniFile = Class(System.IniFiles.TIniFile)
Private
Public
procedure WriteString(const Section, Ident, Value: String); reintroduce;
End;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
Var vIniFile: TIniFile;
begin
Try
vIniFile:= TIniFile.Create('C:\MyFile.ini');
vIniFile.WriteInteger('prueba','numero',5); //este lo escribe
vIniFile.WriteString('prueba','texto','helou'); //ese lo muestra en un showmessage
Finally
vIniFile.Free;
End;
end;

{ TIniFile }

procedure TIniFile.WriteString(const Section, Ident, Value: String);
begin
showMessage('Sobre-escribí el metodo '+Value);
end;

end.





en tu caso, modificarias tu codigo aqsí..

unit MyIniFiles;

interface

uses IniFiles;

type
TIniFile = class(System.IniFiles.TIniFile)
private
public
class procedure mydummy; static;
constructor Create(const FileName: string);
end;

implementation



revisa

gatosoft
16-07-2018, 16:54:31
igual, no está de mas pensar en la solución tradicional: Definir una clase derivada de TIniFile, con la funcionalidad que requieres y a partir de ella definir los descendientes de los que hablas.

Cuando tienes una clase padre y muchos posibles descendientes que desconoces, es cuando comienzas a pensar en clases abstractas o en interfaces.

gatosoft
16-07-2018, 16:55:33
Hola Movorack, realmente no se a que te refieres con esto :confused:....


Es que Movorack, Topx y yo, trabajamos en el mismo piso....

Casimiro Notevi
16-07-2018, 18:09:16
Es que Movorack, Topx y yo, trabajamos en el mismo piso....
¿De verdad? :)

bucanero
17-07-2018, 12:14:41
Muchas gracias gatosoft por toda la información aportada


Existe una técnica que te puede servir y es la de interceptores, pero como todo, no es para abusar de ella.

y funciona mas o menos así:

En la clausula Uses defines la unidad Inifiles, que contiene la implementación nativa del componente.

Después, defines la clase "interceptora" TIniFile (Tiene el mismo nombre de la nativa) la cual hereda de System.IniFiles.TIniFile (debes especificar la unidad).

Y en esta nueva clase, puedes definir nueva funcionalidad y sobreescribir la existente.


Si esta técnica es muy interesante y algunas veces la he utilizado para otros temas un poco mas simples, pero en este caso y como bien dices en el siguiente mensaje, a veces lo mas simple es lo mas eficaz, y he optado por realizar directamente mi clase con herencia tradicional.

igual, no está de mas pensar en la solución tradicional: Definir una clase derivada de TIniFile, con la funcionalidad que requieres y a partir de ella definir los descendientes de los que hablas.

Cuando tienes una clase padre y muchos posibles descendientes que desconoces, es cuando comienzas a pensar en clases abstractas o en interfaces.

Todo mi problema se lío, por no poder hacer en un principio override del constructor y a la hora de crear mi clase como no se hacia directamente desde la propia clase, si no desde una instancia genérica derivada de TINIFile, pues no llegaba a ejecutarse el método constructor propio de mi clase.



Yo también me encontré con ese problema del Nil, y por eso linkeaba la solución en ésta linea de código.

Initialization
TMyIniFile.mydummy; //Me encontré con este error (https://stackoverflow.com/questions/3460382/delphi-2010-rtti-rtticontext-findtype)



En cuanto a crear a través de RTTI, vi el procedimiento y la explicación que habías comentado y si lo utilice, pero al principio no llego a funcionarme, creo que porque en tu código, tu guardabas la unidad y el nombre de la clase en una nueva variable para después hacer la búsqueda, y yo en su lugar use la propiedad ClassName de mi variable con la clase, omitiendo el de la unidad.

Después he realizado una pequeña modificación a tu código, para que no sea necesario hacer la búsqueda de la clase por su nombre, ya que a priori se tiene ya la clase y así se pueda utilizar directamente sin necesidad de hacer su búsqueda.
Y así, si ha funcionado también:

function config: TIniFile;
var
RttiContext: TRttiContext;
RttiType: TRttiInstanceType;
Begin
if not Assigned(IniFile) then begin
if Assigned(ClassIniFile) then begin
RttiContext := TRttiContext.Create;
try
RttiType := RttiContext.GetType(ClassIniFile) as TRttiInstanceType;
// si se ha localizado la la clase se llama al metodo create
if Assigned(RttiType) then
IniFile := RttiType.GetMethod('Create').Invoke(RttiType.MetaclassType, [IniFileName]).AsObject as TIniFile;
finally
RttiContext.free;
end;
end;
//si no hay assignada ninguna clase especifica de TINIFile o no se ha conseguido
//localizar la clase especifica, entoces se utiliza la clase TINIFile
if not Assigned(IniFile) then
IniFile := TIniFile.Create(IniFileName);
end;
Result := IniFile;
end;



Gracias nuevamente a todos los que han dedicado parte de su tiempo a este hilo
Un saludo