Firemonkey: cascade a styled Lookup change for a FMXObject where other objects inherit the stylename

518 views Asked by At

I am not sure if is possible - but it seems like it should be to me. Essentially, I want to trigger all the components to refresh their styles when I change a StyleLookup.

I have a FMXComponent which is a TLabel called BaseStyleLabel. The StyleName property of the component is 'BaseStyle'. It itself looks up its style from a style resource, so its StyleLookup property is set to 'BaseStyle1'. I also have 'BaseStyle2', 'BaseStyle3' ...

I have a dependent Tlabel which is called MyTextLabel and its StyleLookup property is set to BaseStyle, ie the StyleName of BaseStyleLabel.

That all seems to work fine. I see that MyTextLabel inherits the style from MyBaseStyle of 'BaseStyle1'.

When I execute this line of code

 BaseStyleLabel.StyleLookup := 'BaseStyle2';
 Self.repaint; // repaint whole form

I expect that BaseStyleLabel changes to 'BaseStyle2' (which it does). However, MyTextLabel should also change style and then look like 'BaseStyle2', but it does not: it remains as BaseStyle1;

The qualifier is that both BaseLabel and MyTextLabel are also from a style resource. They are not actually a component created on the form, they are created by the style.

So my question is this.

  1. Is this approach valid?
  2. Is there a standard approach eg using a Style Object which I can update?
  3. Have I just not refreshed the right thing or;
  4. used the wrong a method to refresh, maybe ApplyStyle?

...EDIT.... Below is the requested Example...

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Graphics, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.StdCtrls;

type
  TForm1 = class(TForm)
    MyTextLabel: TLabel;
    StyleBook1: TStyleBook;
    BaseStyleLabel: TLabel;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

procedure TForm1.FormCreate(Sender: TObject);
begin
  // Set up BaseStyleLabel as a Style Source
  BaseStyleLabel.StyleName := 'BaseStyle';
  // Set its Style to a Resources Style "STYLE ONE"
  BaseStyleLabel.StyleLookup := 'BaseStyle1';

 // Point MyTextLabel to whatever "BaseStyleLabel" is styled as...
   MyTextLabel.StyleLookup := 'BaseStyle'; // also says "STYLE ONE"

end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  //Change BaseStyle Label to "STYLE TWO" (works OK)
  BaseStyleLabel.StyleLookup := 'BaseStyle2';

  // ... BUT MyTextLabel stays as "STYLE ONE"
  // can I repaint???
  //   Auto Cascade?
  //   What about when Other TLabels are part of a different component style?

end;

end.

Use the following Style File

object TStyleContainer
  object TLabel
    StyleName = 'BaseStyle1'
    DesignVisible = False
    Height = 17.000000000000000000
    Position.X = 521.000000000000000000
    Position.Y = 432.000000000000000000
    Text = 'STYLE ONE'
    Width = 120.000000000000000000
  end
  object TLabel
    StyleName = 'BaseStyle2'
    DesignVisible = False
    Height = 17.000000000000000000
    Position.X = 521.000000000000000000
    Position.Y = 432.000000000000000000
    Text = 'STYLE TWO'
    Width = 120.000000000000000000
  end
  object TLabel
    StyleName = 'BaseStyle3'
    Height = 17.000000000000000000
    Position.X = 521.000000000000000000
    Position.Y = 432.000000000000000000
    Text = 'STYLE 3'
    Width = 120.000000000000000000
  end
end
1

There are 1 answers

0
soddoff Baldrick On

OK - I worked this all through can came up with a solution.

I looked through the ancestor class for TStyledControl. There is no global trigger to cause a style lookup to cascade. The fact that an individual component refreshes its style does not cause any components (which just so happen to depend on that components StyleName) to refresh. So it seems you have to locate all the affected components manually and update them all.

This wasn't good enough for me, so I wrote a helper method for TStyledControl.

unit FMX.CascadingStyleLookup;
// Written by Glen Kleidon - 2018 - twitter: @sobaldrick4
interface

uses System.SysUtils, FMX.Types, FMX.Forms, FMX.Controls;

Type
  TCascadingStyleLookup = Class Helper for TStyledControl
    Procedure CascadeStyleLookup(AStyleLookup: String); overload;
    procedure CascadeStyleLookup(AStyleLookup: string;
      AComponent: TFMXObject); overload;
    Function FindUltimateParentForm(AChild: TFMXObject): TFMXObject;
  End;

implementation

procedure TCascadingStyleLookup.CascadeStyleLookup(AStyleLookup: String);
var
  lUltimateParent: TFMXObject;
begin
  // Apply New style to myself.
  Self.styleLookup := AStyleLookup;

  // Check if I have dependent Styles.
  if (length(Self.StyleName) = 0) then
    exit;

  // Re-Apply my own style to everything.
  lUltimateParent := Self.FindUltimateParentForm(Self);
  if lUltimateParent <> nil then
    CascadeStyleLookup(Self.StyleName, lUltimateParent);
end;

procedure TCascadingStyleLookup.CascadeStyleLookup(AStyleLookup: string;
  AComponent: TFMXObject);
var
  lChild: TFMXObject;
  lStyledControl: TStyledControl;
begin
  if (AComponent = nil) or (AComponent.Children = nil) then
    exit;

  if (AComponent.InheritsFrom(TStyledControl)) then
  begin
    lStyledControl := AComponent as TStyledControl;
    if SameText(AStyleLookup, lStyledControl.styleLookup) then
    begin
      // re-apply
      lStyledControl.styleLookup := AStyleLookup;
      // Re-cascade this style.
      if (length(lStyledControl.StyleName) > 0) and
        (NOT(SameText(lStyledControl.styleLookup, AStyleLookup))) then
        CascadeStyleLookup(lStyledControl.StyleName);
    end;
  end;
  if AComponent.Children = nil then
    exit;

  // Check the children of this component for the style.
  for lChild in AComponent.Children do
    CascadeStyleLookup(AStyleLookup, lChild);

end;

function TCascadingStyleLookup.FindUltimateParentForm(AChild: TFMXObject)
  : TFMXObject;
begin
  Result := nil;
  if (AChild.Parent <> nil) and (NOT(AChild.Parent.InheritsFrom(TForm))) then
    Result := FindUltimateParentForm(AChild)
  else
    Result := AChild.Parent;
end;

end.

This allows any nested "stylelookup" properties to be automatically searched for and updated for the entire form where the component resides.

I updated my original example with an additional component MyTextLabel2 which depends on the style of MyTextLabel.

So, now setting BaseStyleLabel to any of *BaseStyle1, 2 or 3* will cascade to MyTextLabel. When MyTextLabel changes its style,this will cascade to MyTextLabel2. So, all three labels will show "STYLE TWO" when BaseStyleLabel is updated by simply calling BaseStyleLabel.CascadeStyleLookup('BaseStyle2');

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  System.Variants,
  FMX.Types, FMX.Graphics, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.StdCtrls,
  FMX.CascadingStyleLookup
  ;

type
  TForm1 = class(TForm)
    MyTextLabel: TLabel;
    StyleBook1: TStyleBook;
    BaseStyleLabel: TLabel;
    Button1: TButton;
    MyTextLabel2: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

procedure TForm1.FormCreate(Sender: TObject);
begin
  // Set up BaseStyleLabel as a Style Source
  BaseStyleLabel.StyleName := 'BaseStyle';
  // Set its Style to a Resources Style "STYLE ONE"
  BaseStyleLabel.StyleLookup := 'BaseStyle1';

  // Point MyTextLabel to whatever "BaseStyleLabel" is styled as...
  MyTextLabel.StyleName := 'MyTextBaseStyle';
  MyTextLabel.StyleLookup := 'BaseStyle'; // also says "STYLE ONE"

  // New component MyTextLabel2 points to whatever "MyTextLabel" is styled as...
  MyTextLabel2.StyleLookup := 'MyTextBaseStyle'; // also says "STYLE ONE",
                                                 // (from MyTextLabel)

end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  // Change BaseStyle Label to "STYLE TWO" (now cascades to all)
  BaseStyleLabel.CascadeStyleLookup('BaseStyle2');

end;