Jump to content

component creation


Recommended Posts

  • Moderators

Question is this article still the preferred way to develop visual components ?

I developed some Material Design components some years ago (here and here) but they need to be ported to the latest rtl as they don't compile anymore.

Hence this question.

As an example I ported a simple component (switch) using the recipe in Jon's article and it works ok, see below

Just a bit elaborate.

unit Switch;

/******************************************************************************
LynkFS / 20 - 11 - 2018

The Switch component consists of
- a W3Panel as the main container for the Switch
- a W3Panel for the Switch
- a W3Panel for the sliderline
- a W3Panel as the highlighter

The component is by default styled according to the css theme of the project.
Styling can be modified by overriding the following css classes :

The Switch CSS classes in BEM notation are
.TSwitch
.TSwitch__Panel
.TSwitch__Panel__Line
.TSwitch__Panel__Line--StateOn
.TSwitch__Panel__Line--StateOff
.TSwitch__Panel__Knob
.TSwitch__Panel__Knob--StateOn
.TSwitch__Panel__Knob--StateOff
.TSwitch__Panel__HighLight
.TSwitch__Panel__HighLight--Active
.TSwitch__Panel__HighLight--InActive

*/

interface

uses
  W3C.DOM,
  System.Types,
  System.Time,
  System.Colors,
  System.Events,
  {$IFDEF THEME_AUTOSTYLE}
  SmartCL.Theme,
  {$ENDIF}
  SmartCL.System,
  SmartCL.Touch,
  SmartCL.Effects,
  SmartCL.borders,
  SmartCL.Components,
  SmartCL.Controls.Panel,
  SmartCl.Css.StyleSheet;

type

  TSwitch = class(TW3CustomControl)
  private
    Panel0   : TW3Panel;       //background
    Panel1   : TW3Panel;       //sliderline
    Panel2   : TW3Panel;       //indicator
    Panel3   : TW3Panel;       //Switch knob
    procedure SetStateOn(StateOn: Boolean);
    function  GetStateOn: Boolean;
    IsStateOn: Boolean := false;
  protected        // see https://jonlennartaasenden.wordpress.com/2018/05/07/
    function  MakeElementTagObj: THandle; override;       //1
    procedure StyleTagObject; override;                   //2
    procedure ObjectReady; override;                      //3
    procedure InitializeObject; override;                 //4
    procedure Resize; override;                           //5
    procedure FinalizeObject; override;
    Procedure ShowSwitch;                                 //included in 5
    Procedure InsertCSS;                                  //included in 2
  public
    property StateOn: Boolean read getStateOn write setStateOn;
  end;

implementation

{ TSwitch }

function TSwitch.MakeElementTagObj: THandle;          //1
begin
  result := w3_createHtmlElement('div');              //div = default, so could be omitted
end;

procedure TSwitch.StyleTagObject;                     //2
begin
  inherited;
  {$IFDEF THEME_AUTOSTYLE}
  ThemeBorder := btDecorativeBorder;
  {$ENDIF}

  InsertCSS;                                          //insert all styling in stylesheet
end;

procedure TSwitch.ObjectReady;                        //3
begin
  inherited;
  //set some initial properties

  Panel0.SetBounds(0,0,self.width,self.height);       //background
  Panel1.SetBounds(round(self.height/2),              //slider line
                   round(self.height/2)-round(self.height*0.2),
                   self.Width-self.height,
                   round(self.height*0.4));
  Panel2.SetBounds(0,0,self.height,self.height);      //highlight circle
  var x : integer := round(self.height/5);            //switch knob
  Panel3.SetBounds(x,x,Panel2.width-2*x,Panel2.height-2*x);

  TW3Dispatch.WaitFor([Panel0, Panel1, Panel2, Panel3], 5,
    procedure (Success: boolean)
    begin
      if Success then
      begin
        // set some initial properties
        If IsStateOn then begin
          Panel2.Left := self.Width - Panel2.Width;
          Panel3.Left := self.Width - Panel2.Width + round((Panel2.Width-Panel3.Width)/2);
        end;

        // Do an immediate resize
        Resize();
      end;
    end);
end;

procedure TSwitch.InitializeObject;              //4
begin
  inherited;

//creating sub-components

  Self.TagStyle.Add('TSwitch');       //superfluous, will be auto generated

  Panel0 := TW3Panel.Create(self);
  Panel0.TagStyle.Clear;              //dont inherit css from TW3Panel
  Panel0.TagStyle.Add('TSwitch__Panel');

  Panel1 := TW3Panel.Create(Panel0);
  Panel1.TagStyle.Clear;              //dont inherit css from TW3Panel
  Panel1.TagStyle.Add('TSwitch__Panel__Line TSwitch__Panel__Line--StateOff');

  Panel2 := TW3Panel.Create(Panel0);
  Panel2.TagStyle.Clear;              //dont inherit css from TW3Panel
  Panel2.TagStyle.Add('TSwitch__Panel__HighLight TSwitch__Panel__HighLight--InActive');

  Panel3 := TW3Panel.Create(Panel0);
  Panel3.TagStyle.Clear;              //dont inherit css from TW3Panel

  self.SimulateMouseEvents := True;   //touch enable

  self.handle.addEventListener("click", procedure()
  begin

    IsStateOn := not IsStateOn;

    If IsStateOn then begin
      Panel3.fxMoveBy(self.width - self.height, 0, 0.5);
      Panel2.fxMoveBy(self.width - self.height, 0, 0.5);
    end else begin
      Panel3.fxMoveBy(-self.width + self.height, 0, 0.5);
      Panel2.fxMoveBy(-self.width + self.height, 0, 0.5);
    end;

    Panel2.TagStyle.Clear;            //dont inherit css from TW3Panel
    Panel2.TagStyle.Add('TSwitch__Panel__HighLight TSwitch__Panel__HighLight--Active');
    TW3Dispatch.Execute (Procedure()
    begin
      Panel2.TagStyle.Clear;          //dont inherit css from TW3Panel
      Panel2.TagStyle.Add('TSwitch__Panel__HighLight TSwitch__Panel__HighLight--InActive');
    end, 80);

    ReSize;
  end);
end;

procedure TSwitch.Resize;
begin
  inherited;

  if not (csDestroying in ComponentState) then
  begin
    // Make sure we have ready state
    if (csReady in ComponentState) then
    begin
      // Check that child elements are all assigned
      // and that they have their csReady flag set in
      // ComponentState. This can be taxing. A more lightweight
      // version is TW3Dispatch.Assigned() that doesnt check
      // the ready state (see class declaration for more info)
      if TW3Dispatch.AssignedAndReady([Panel0, Panel1, Panel2, Panel3]) then
      begin
        // Finally: layout the controls.
        ShowSwitch;
      end;
    end;
  end;
end;

Procedure TSwitch.ShowSwitch;
begin
//
  Panel1.TagStyle.Clear;
  Panel3.TagStyle.Clear;
  If IsStateOn then begin
    Panel3.TagStyle.Add('TSwitch__Panel__Knob TSwitch__Panel__Knob--StateOn');
    Panel1.TagStyle.Add('TSwitch__Panel__Line TSwitch__Panel__Line--StateOn');
  end else begin
    Panel3.TagStyle.Add('TSwitch__Panel__Knob TSwitch__Panel__Knob--StateOff');
    Panel1.TagStyle.Add('TSwitch__Panel__Line TSwitch__Panel__Line--StateOff');
  end;
//
end;

procedure TSwitch.InsertCSS;
var
  cssProperties : string;
begin
  //make stylesheet
  var styleSheet : TW3StyleSheet := TW3StyleSheet.Create;

  styleSheet.Add(".TSwitch", '');

  styleSheet.Add(".TSwitch__Panel", '');

  cssProperties := 'cursor: pointer; border: 0px; border-radius: 6px;';
  styleSheet.Add(".TSwitch__Panel__Line", cssProperties);

  cssProperties := 'background-color: rgba(164,114,234,.50);';
  styleSheet.Add(".TSwitch__Panel__Line--StateOn", cssProperties);

  cssProperties := 'background-color: rgba(0,0,0,.26);';
  styleSheet.Add(".TSwitch__Panel__Line--StateOff", cssProperties);

  cssProperties := 'cursor: pointer; border: 0px; border-radius: 50%;';
  styleSheet.Add(".TSwitch__Panel__HighLight", cssProperties);

  cssProperties := 'background-color: rgba(164,114,234,.50);';
  styleSheet.Add(".TSwitch__Panel__HighLight--Active", cssProperties);

  cssProperties := 'background-color: transparent;';
  styleSheet.Add(".TSwitch__Panel__HighLight--InActive", cssProperties);

  cssProperties := 'cursor: pointer; border-radius: 50%; box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);';
  styleSheet.Add(".TSwitch__Panel__Knob", cssProperties);

  cssProperties := 'background-color: rgba(164,114,234,.90);';
  styleSheet.Add(".TSwitch__Panel__Knob--StateOn", cssProperties);

  cssProperties := 'background-color: white;';
  styleSheet.Add(".TSwitch__Panel__Knob--StateOff", cssProperties);

/*
Alternatively add this to a custom stylesheet :

.TSwitch {
}

.TSwitch__Panel {
}

.TSwitch__Panel__Line {
    cursor: pointer;
    border: 0px;
  border-radius: 6px;
}

.TSwitch__Panel__Line--StateOn {
  background-color: rgba(164,114,234,.50);
}

.TSwitch__Panel__Line--StateOff {
  background-color: rgba(0,0,0,.26);
}

.TSwitch__Panel__HighLight {
    cursor: pointer;
    border: 0px;
  border-radius: 12px;
}

.TSwitch__Panel__HighLight--Active {
  background-color: rgba(164,114,234,.50);
}

.TSwitch__Panel__HighLight--InActive {
  background-color: transparent;
}

.TSwitch__Panel__Knob {
    cursor: pointer;
     border-radius: 10px;
  box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
}

.TSwitch__Panel__Knob--StateOn {
  background-color: rgba(164,114,234,.90);
}

.TSwitch__Panel__Knob--StateOff {
  background-color: white;
}
*/

end;

procedure TSwitch.FinalizeObject;
begin

  Panel0.Free;
  Panel0 := nil;

  inherited;
end;

procedure TSwitch.SetStateOn(StateOn: Boolean);
begin
  IsStateOn := StateOn;
end;

function  TSwitch.GetStateOn: Boolean;
begin
  Result := IsStateOn;
end;

end.

usage

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components

  Switch1 := TSwitch.Create(self);
//  Switch1.StateOn := true;    //default false

  Switch1.SetBounds(50,50,60,35);

end;

 

 

Link to post
Share on other sites
  • Administrators

Let me start by commenting on Jon's blog post. For the most part it's accurate, but he makes a small mistake with the "common mistake":

Quote

A common mistake with ObjectReady() is that the ready state somehow covers any child elements you might have created for your control. This is not the case. ObjectReady() is called when the current control is finished with its initialization, and is ready for manipulation.

ObjectReady() calls ReadySync() which actually does check if the child elements are ready. So, when you override ObjectReady(), the child controls you created in InitializeObject should also be ready. And when ObjectReady() happens, it also triggers the first Resize for the component.

Let me comment on these three lines:

    Quote
    • Write your own checking routines inspired by ReadySync
    • Ignore the whole thing and just check ready-state and that child elements are not NIL in Resize(). This is what most people do.
    • Use TW3Dispatch and throw in an extra Resize() call somewhere in ObjectReady()

    Users don't need to write as complex checking routines as ReadySync() is. Let the RTL take care of that. It does it very well. However, do always check your controls in Resize(). Minimum is to check that they are not nil. And if you want to be extra sure, check the ready state too.

    And by far the best way to do this all is pretty simple:

    1. In your Resize() procedure, always check if your controls are ready
    2. If they are not, call Self.Invalidate(). Do not call Resize() directly or via TW3Dispatch as Jon suggests.

    Think about Invalidate() as a request for a new Resize(). It calls an internal procedure in the RTL called ResizeWhenReady() and it does exactly what it says: It will call Resize() with a short delay and it will check that any new child components you added after the previous resize are also ready. The delay helps to minimize the number of Resize()-calls and it simply makes sense to not let a Resize() happen before your child components are ready.

    I think Jon is explaining how the RTL did work before I got involved. It makes sense to rely on ObjectReady() when you have a simple, static control but when you have a control like TW3ListBox, it does not work at all as the TW3ListBox dynamically creates new child components while you use the control.

    Link to post
    Share on other sites
    • Moderators

    Thanks @jarto

    So a good boilerplate for visual components would be something like this

    function TSwitch.MakeElementTagObj: THandle;          //1
    begin
      result := w3_createHtmlElement('div');              //or any other html element
    end;
    
    procedure TSwitch.StyleTagObject;                     //2
    begin
      inherited;
      {$IFDEF THEME_AUTOSTYLE}
      ThemeBorder := btDecorativeBorder;                  //or whatever is appropriate
      {$ENDIF}
    
      ....                                                //optionally add styles
    end;
    
    procedure TSwitch.InitializeObject;                   //3
    begin
      inherited;
      ...                                                 //create sub-components
    end;
    
    procedure TSwitch.ObjectReady;                        //4
    begin
      inherited;
      ....                                                //optionally set some initial properties
    end;
    
    procedure TSwitch.Resize;
    begin
      inherited;
    
      if not (csDestroying in ComponentState) then
      begin
        // Make sure we have ready state
        if (csReady in ComponentState) then
        begin
          ...                                              // layout the controls
        end else begin
          self.Invalidate();
        end;
      end;
    end;


     

     

    Link to post
    Share on other sites

    Join the conversation

    You can post now and register later. If you have an account, sign in now to post with your account.

    Guest
    Reply to this topic...

    ×   Pasted as rich text.   Paste as plain text instead

      Only 75 emoji are allowed.

    ×   Your link has been automatically embedded.   Display as a link instead

    ×   Your previous content has been restored.   Clear editor

    ×   You cannot paste images directly. Upload or insert images from URL.

    ×
    ×
    • Create New...