Jump to content
aehimself

Read and reapply design time properties to specific controls - runtime

Recommended Posts

Hello,

 

I have a method that hides, rearranges and resizes components based on some conditions runtime. All is working fine, but the question eventually arrived: is is possible to "reset" everything to it's original (design time) state?

Instead of creating a list of all changed components (and it's properties) I'm wondering if it's possible to re-read and re-apply the .DFM settings to a number of selected components?

 

A list would be easier, but the DFM streaming seems to be the "professional" way to do it.

 

I guess I'll need a TReader for that but I have absolutelly no idea how that works 🙂

Share this post


Link to post

I guess it is theoretically possible to shoehorn Delphi's streaming mechanism to do what you want. Essentially you could just reread your form with an adjusted TReader. Specifically you would not want for the components to be recreated. Have a look at

the TReader events, specifically TReader.OnCreateComponent

One problem though is how to "skip" rereading of components as you want to only select a number of components.

 

 

  • Like 1
  • Thanks 1

Share this post


Link to post

What about creating a new form instance and adjust the components and properties in question from that?

  • Like 2

Share this post


Link to post

A Tag can be used for this purpose. Unless it is used for something else.

Share this post


Link to post
5 hours ago, aehimself said:

I have a method that hides, rearranges and resizes components based on some conditions runtime. All is working fine, but the question eventually arrived: is is possible to "reset" everything to it's original (design time) state?

What about only creating your form without applying the saved changes? = That should be your design time state, or I'm wrong?

Edited by microtronx

Share this post


Link to post
6 hours ago, M.Joos said:

Specifically you would not want for the components to be recreated. Have a look at the TReader events, specifically TReader.OnCreateComponent

I already started to experiment with the TReader component, unfortunately OnCreateComponent will only let me to use a different constructor other than the default. If the component is not created in the handler it will be created anyway.

This thing is still a mess to me though... how do I specify WHERE the new components will be created...? As it needs a stream I need to get it from the executable - I guess it will be stored as a resource...?

I'm only at the beginning. I'll find the answers soon enough.

 

6 hours ago, Uwe Raabe said:

What about creating a new form instance and adjust the components and properties in question from that?

This is actually a really neat idea. As the TReader method will create the components anyway I suppose the memory usage will be close to equal anyway. As a huge bonus, I know how to create a new form instance 🙂

 

3 hours ago, Stano said:

A Tag can be used for this purpose. Unless it is used for something else.

An other good idea. Tags are - unfortunately though - already in use in said project 😞

 

 

I'll dig into the TReader a little bit so I'll have a basic understanding of how it works / what it does. It never hurts to know something, you never know when you'll need such a thing.

 

Thank you all for your answers!

Share this post


Link to post
7 minutes ago, aehimself said:

I already started to experiment with the TReader component, unfortunately OnCreateComponent will only let me to use a different constructor other than the default. If the component is not created in the handler it will be created anyway.

This thing is still a mess to me though... how do I specify WHERE the new components will be created...? As it needs a stream I need to get it from the executable - I guess it will be stored as a resource...?

I'm only at the beginning. I'll find the answers soon enough.

 

This is actually a really neat idea. As the TReader method will create the components anyway I suppose the memory usage will be close to equal anyway. As a huge bonus, I know how to create a new form instance 🙂

 

An other good idea. Tags are - unfortunately though - already in use in said project 😞

 

 

I'll dig into the TReader a little bit so I'll have a basic understanding of how it works / what it does. It never hurts to know something, you never know when you'll need such a thing.

 

Thank you all for your answers!

For inspiration you may also look into this excellent answer from Uwe Raabe on Stackoverflow on a related question: https://stackoverflow.com/questions/47347578/manually-skip-a-component-from-being-created-from-the-dfm

which also shows a hacky way to prevent components from getting created. I guess Uwe wasn't aware of his answer more than 3 years ago 😉

As for loading the dfm from the resource that's in the executable have a look at http://docwiki.embarcadero.com/Libraries/Sydney/de/System.Classes.TStream.ReadComponentRes

  • Thanks 1

Share this post


Link to post

I think this is what you need, you just have to adjust it a bit.

 

// search in dfm resources by Attila Kovacs
unit DFMSearch;

interface

uses
  System.Classes,
  System.Generics.Collections;

type

  TDFMSearch = class
  private
    FFormClasses: TList<string>;
    FResourceNames: TList<string>;
    FSearchDictionary: TDictionary<string, string>;
    function GetFormClasses(AClass: TClass): TList<string>;
    procedure BuildStrings(AResources: TList<string>);
    procedure EnumClasses(AClass: TClass);
  public
    function Find(const ASearchString: string): TList<string>;
    constructor Create(AClass: TClass); overload;
    constructor Create(AClassNames: TList<string>); overload;
    destructor Destroy; override;
  end;

implementation

uses
  Winapi.Windows,
  System.Rtti,
  System.SysUtils,
  System.StrUtils;


function EnumResNames(Module: HMODULE; ResType, ResName: PChar; Obj: Pointer): integer; stdcall;
var
  ds: TDFMSearch;
begin
  ds := TDFMSearch(Obj);
  if ds.FFormClasses.IndexOf(string(ResName)) > -1 then
    ds.FResourceNames.Add(string(ResName));
  Result := 1;
end;


function TDFMSearch.GetFormClasses(AClass: TClass): TList<string>;
var
  cl: TClass;
  ctx: TRttiContext;
  types: TArray<TRttiType>;
  typ: TRttiType;
begin
  Result := TList<string>.Create;
  ctx := TRttiContext.Create;
  types := ctx.GetTypes;
  for typ in types do
  begin
    if typ.TypeKind = tkClass then
    begin
      cl := typ.AsInstance.MetaclassType;
      if (cl <> AClass) and cl.InheritsFrom(AClass) then
        Result.Add(UpperCase(cl.ClassName));
    end;
  end;
end;


procedure TDFMSearch.BuildStrings(AResources: TList<string>);
var
  i: integer;
  Flags: TFilerFlags;
  R: TReader;
  RS: Tresourcestream;
  vt: TValueType;
  Position: integer;
  text, PropName, CompClass, CompName, StrValue, ResStr: string;

  procedure ReadProperty;
  begin
    PropName := R.ReadStr;
    if ContainsText(PropName, 'caption') then
    begin
      vt := R.NextValue;
      case vt of
        vaWString, vaUTF8String:
          StrValue := R.ReadString;
        vaString, vaLString:
          StrValue := R.ReadString;
      else
        R.SkipValue;
      end;
      text := text + ' ' + StrValue;
    end
    else
      R.SkipValue;
  end;

procedure ReadData; forward;

  procedure ReadComponent;
  begin
    R.ReadPrefix(Flags, Position);
    CompClass := R.ReadStr;
    CompName := R.ReadStr;
    ReadData;
  end;

  procedure ReadData;
  begin
    while not R.EndOfList do
      ReadProperty;
    R.ReadListEnd;
    while not R.EndOfList do
      ReadComponent;
    R.ReadListEnd;
  end;


begin
  for i := 0 to AResources.Count - 1 do
  begin
    ResStr := UpperCase(AResources[i]);
    RS := Tresourcestream.Create(HInstance, ResStr, RT_RCDATA);
    try
      R := TReader.Create(RS, 4096);
      try
        text := '';
        R.ReadSignature;
        ReadComponent;
        FSearchDictionary.AddOrSetValue(ResStr, text);
      finally
        R.Free;
      end;
    finally
      RS.Free;
    end;
  end;
end;


constructor TDFMSearch.Create(AClass: TClass);
begin
  FSearchDictionary := TDictionary<string, string>.Create;
  EnumClasses(AClass);
end;


constructor TDFMSearch.Create(AClassNames: TList<string>);
begin
  FSearchDictionary := TDictionary<string, string>.Create;
  BuildStrings(AClassNames);
end;


destructor TDFMSearch.Destroy;
begin
  FSearchDictionary.Free;
  inherited;
end;


procedure TDFMSearch.EnumClasses(AClass: TClass);
begin
  if not Assigned(FSearchDictionary) then
  begin
    FFormClasses := GetFormClasses(AClass);
    try
      FResourceNames := TList<string>.Create;
      try
        EnumResourceNames(HInstance, RT_RCDATA, @EnumResNames, NativeInt(Self));
        BuildStrings(FResourceNames);
      finally
        FResourceNames.Free;
      end;
    finally
      FFormClasses.Free;
    end;
  end;
end;


function TDFMSearch.Find(const ASearchString: string): TList<string>;
var
  i, j, slen: integer;
  sl: TArray<string>;
  pa: TPair<string, string>;
begin
  Result := TList<string>.Create;
  sl := ASearchString.Split([' ']);
  slen := Length(sl);
  for pa in FSearchDictionary do
  begin
    j := 0;
    for i := 0 to High(sl) do
    begin
      if ContainsText(pa.Value, sl[i]) then
        inc(j);
    end;
    if j = slen then
      Result.Add(pa.Key);
  end;
end;

end.

 

  • Thanks 1
  • Haha 1

Share this post


Link to post
21 hours ago, aehimself said:

I'm wondering if it's possible to re-read and re-apply the .DFM settings to a number of selected components

This is not technically possible with TReader.

 

However, I want to mention one different aspect: Such a solution will not be a kind of "professional" solution anyway, because controls are not developed having multiple re-loads in mind. Control's code may use different load process related flags, like csReading, csLoading and may override Loaded method. And the developer may (which is often the case) rely on the fact that the loading happens right after creation.

 

 

 

  • Like 1

Share this post


Link to post
56 minutes ago, balabuev said:

This is not technically possible with TReader.

 

Re-read with TReader -> yes, see my previous comment

Re-apply with TReader -> no, but you can reapply it by updating the corresponding properties by yourself

  • Haha 1

Share this post


Link to post
1 hour ago, Attila Kovacs said:

Re-read with TReader -> yes, see my previous comment

Well, if you use only utility methods, like ReadStr, ReadSignature, etc - then yes, you right. But this way, you need to rewrite most other high level reading code manually.

Edited by balabuev

Share this post


Link to post
2 minutes ago, balabuev said:

But this ways, you need to rewrite most other high level reading code manually.

If there are some non-standard written controls which has to be restored, yes, you are right, it would need it's reader.

On standard controls you can skip the data you don't need or you don't know.

We can't see this from here. Nor the actual software to be able to decide which would be less effort, storing local the initial values or reading the dfm.

I wanted to show him how to read the resources by its classtypes and read some properties, which he asked.

  • Confused 1

Share this post


Link to post
Quote
  • There are the Explicit* properties in the dfm which are storing the design time positions and sizes. 

How to use Explicit properties to restore positions?  Iteration or Is there simple revert message undocumented somewhere.   

 

     

Share this post


Link to post
4 minutes ago, Pat Foley said:

How to use Explicit properties to restore positions?

No reasonable way. ExplicitXXX [pseudo]properties are a part of align subsystem data. And they are not actually always stored in dfm.

Share this post


Link to post

The problem with those ExplicitXXX values is that they are synched with their current XXX counterparts on several occasions. These are WM_SIZE, WM_MOVE and SetBounds. You cannot assume that these values contain the original DFM content, even if that was the case after loading the form.

  • Thanks 1

Share this post


Link to post

The problem with storing/restoring positions is even more general. If some form have child controls, which use Align and/or Anchors features, then any attempt to reset control positions via iterating the controls and assignings original desing-time positions - is an incorrect way, which may end up with the invalid layout. Especially (but it's not required) when the form is resizeable.

Edited by balabuev

Share this post


Link to post
9 minutes ago, Attila Kovacs said:

TWinControl.DisableAlign may help there

Will not be sufficient, especially if we want to treat only selected controls, as topic starter initially mentioned. And in the case when parent form (or control) was already resized.

Edited by balabuev

Share this post


Link to post

He also mentions that he is successfully hides/resizes/rearranges those components. That said, I can't see any problem to reset them. Also, I trust him fully that he knows what he is doing.

  • Haha 1

Share this post


Link to post
1 minute ago, Attila Kovacs said:

He also mentions that he is successfully hides/resizes/rearranges those components

Manual explicit rearrangement is somewhat different from the formal restoring algorithm (like re-reading from dfm). Anyway, I just highlight possible issues, which I beleive are not intuitive from the first look.

May be I complicate things compared to what is practically needed in this particular case.

 

 

Share this post


Link to post
On 2/22/2021 at 12:11 PM, aehimself said:

I'm wondering if it's possible to re-read and re-apply the .DFM settings to a number of selected components?

Nope. I had this discussion with DevEx support. I wanted to implement a "grid settings reset button".

After a lot of pondering; the "contract" that the various components has with the DFM is linked in to the code reading it too. Essentially you cannot change the DFM resource w/o recompiling (i may have to specify that).

If you "reload" at a different state, you will open a can of worms.

 

I have "reverted" to having a "Debug Persistance Writer" function as a part of my application. It essentially writes down the properties of [pertinent] component(s) before anything really happens.

Reading THAT (either using a persistance lib, plain functions or using the components own "persistance" handling admittedly feel unoptimized (first read DFM, then read .ini) but it works.

My end-users can now click "Reset" for any complex DevEx component. With some synchronization (not threads) i can restore a complete frame runtime with DB-awares connected.

 

Quick comment, HTH,

 

/Dany

Share this post


Link to post
4 hours ago, Dany Marmur said:

If you "reload" at a different state, you will open a can of worms.

This is what I've initially talked about.

  • Like 1

Share this post


Link to post
On 2/22/2021 at 2:11 PM, aehimself said:

I have a method that hides, rearranges and resizes components based on some conditions runtime. All is working fine, but the question eventually arrived: is is possible to "reset" everything to it's original (design time) state?

Instead of creating a list of all changed components (and it's properties) I'm wondering if it's possible to re-read and re-apply the .DFM settings to a number of selected components?

If you only want to restore Visible and Bounds, then saving original props in a list is the simplest option.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×