Jerry Dodge
Jerry Dodge

Reputation: 27276

Implementing `Auto` property in component's persistent

I have a component which has a corresponding persistent. This persistent is of course published as a property of the component, showing in the object inspector. It has a few different properties of its own, mainly 4 integers (Left, Top, Right, Bottom). I also have a property in this persistent called Auto which is intended to automatically calculate the 4 integers based on the size of the component.

More specifically, this graphic component has a border around it, each edge can have a different sized border. This persistent and its properties specify the thickness of the border on each side. When Auto is enabled, the 4 border edges are calculated based on the size of the component.

Now it seems to work fine for the most part, except, somehow this Auto property keeps going back to False randomly. Also, in design-time, when resizing the component with Auto enabled, it does in fact calculate things accordingly, perfectly. But, in run-time, it goes back to False, and no longer calculates them anymore. Upon saving with Auto enabled, closing the form, then re-opening it, this Auto property is back to False again.

The 4 integer properties have setters, which if they are set, it turns this Auto property to false. I'm assuming this is what's causing it to keep going to false again, except, I'm not telling it to set these properties anywhere.

Here's the persistent:

  TJDGlassBorder = class(TPersistent)
  private
    fOwner: TJDGlass; //This is the parent component
    fGlow: Integer;
    fBottom: Integer;
    fLeft: Integer;
    fTop: Integer;
    fRight: Integer;
    fColor: TColor;
    fOnEvent: TNotifyEvent;
    fAuto: Bool;
    procedure SetBottom(const Value: Integer);
    procedure SetColor(const Value: TColor);
    procedure SetGlow(const Value: Integer);
    procedure SetLeft(const Value: Integer);
    procedure SetRight(const Value: Integer);
    procedure SetTop(const Value: Integer);
    function GetBottom: Integer;
    function GetLeft: Integer;
    function GetRight: Integer;
    function GetTop: Integer;
    procedure SetAuto(const Value: Bool);
  public
    constructor Create(AOwner: TJDGlass);
    destructor Destroy; override;
    procedure Event;
    procedure Assign(Source: TPersistent); override;
  published
    property Auto: Bool read fAuto write SetAuto default True;
    property Left: Integer read GetLeft write SetLeft default 3;
    property Top: Integer read GetTop write SetTop default 2;
    property Right: Integer read GetRight write SetRight default 3;
    property Bottom: Integer read GetBottom write SetBottom default 4;
    property Color: TColor read fColor write SetColor;
    property Glow: Integer read fGlow write SetGlow default 1;
    property OnEvent: TNotifyEvent read fOnEvent write fOnEvent;
  end;

/////////////

{ TJDGlassBorder }

procedure TJDGlassBorder.Assign(Source: TPersistent);
begin
  inherited Assign(Source);
  Event;
end;

constructor TJDGlassBorder.Create(AOwner: TJDGlass);
begin
  fOwner:= AOwner;
  fAuto:= True;
  fColor:= clBlack;
  fGlow:= 1;
  Event;
end;

destructor TJDGlassBorder.Destroy;
begin

  inherited;
end;

procedure TJDGlassBorder.Event;
begin
  if assigned(fOwner) then 
    if fOwner <> nil then
      fOwner.Invalidate;
  if assigned(fOnEvent) then
    fOnEvent(Self);
end;

function TJDGlassBorder.GetBottom: Integer;
begin
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= Max(2, fOwner.Height div 10);
        fBottom:= Result;
      end;
    end;
  end else begin
    Result:= fBottom;
  end;
end;

function TJDGlassBorder.GetLeft: Integer;
begin
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= (Top + Bottom) div 2;
        fLeft:= Result;
      end;
    end;
  end else begin
    Result:= fLeft;
  end;
end;

function TJDGlassBorder.GetRight: Integer;
begin
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= (Top + Bottom) div 2;
        fRight:= Result;
      end;
    end;
  end else begin
    Result:= fRight;
  end;
end;

function TJDGlassBorder.GetTop: Integer;
begin
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= Max(1, fOwner.Height div 30);
        fTop:= Result;
      end;
    end;
  end else begin
    Result:= fTop;
  end;
end;

procedure TJDGlassBorder.SetAuto(const Value: Bool);
begin
  fAuto := Value;
  Event;
end;

procedure TJDGlassBorder.SetBottom(const Value: Integer);
begin          
  fAuto:= False;
  fBottom := Value;  
  Event;
end;

procedure TJDGlassBorder.SetColor(const Value: TColor);
begin
  fColor := Value;  
  Event;
end;

procedure TJDGlassBorder.SetGlow(const Value: Integer);
begin
  fGlow := Value;  
  Event;
end;

procedure TJDGlassBorder.SetLeft(const Value: Integer);
begin         
  fAuto:= False;
  fLeft := Value;  
  Event;
end;

procedure TJDGlassBorder.SetRight(const Value: Integer);
begin       
  fAuto:= False;
  fRight := Value; 
  Event;
end;

procedure TJDGlassBorder.SetTop(const Value: Integer);
begin           
  fAuto:= False;
  fTop := Value;
  Event;
end;

EDIT:

I attempted 3 more things in the above code, and still have the problem. This is what I did:

1: Published the Auto property after the other 4 properties, thinking of the order that these properties are retrieved.

published
  property Auto: Bool read fAuto write SetAuto default True;
  property Left: Integer read GetLeft write SetLeft default 3;
  property Top: Integer read GetTop write SetTop default 2;
  property Right: Integer read GetRight write SetRight default 3;
  property Bottom: Integer read GetBottom write SetBottom default 4;

Changed to:

published
  property Left: Integer read GetLeft write SetLeft default 3;
  property Top: Integer read GetTop write SetTop default 2;
  property Right: Integer read GetRight write SetRight default 3;
  property Bottom: Integer read GetBottom write SetBottom default 4;
  property Auto: Bool read fAuto write SetAuto default True;

2: In the property setters for these integers, I'm checking to see if the new value differs from the existing value...

procedure TJDGlassBorder.SetTop(const Value: Integer);
begin         
  if Value <> fTop then begin  
    fAuto:= False;
    fTop := Value;
    Event;
  end;
end;

3: In the property getters for these integers, I changed how it checks the existing value...

function TJDGlassBorder.GetTop: Integer;
begin
  Result:= fTop;
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= Max(1, fOwner.Height div 30);
        fTop:= Result;
      end;
    end;
  end;
end;

Again, none of these attempts worked, I still have this problem.

Upvotes: 3

Views: 763

Answers (2)

Jerry Dodge
Jerry Dodge

Reputation: 27276

FIXED!

The 3 attempts I put in my edit above were partially the problem, but the actual fix was removing the default of the Auto property. The thing is, I had this defaulted to True, which, in this case, that property is not saved in the DFM file. Therefore it did not even attempt to set this Auto property. Removing the Default fixed, because now whether it's true or false, it's always saved in the DFM file, therefore always setting this value. The order of publishing the properties was also half of the problem.


Here's the final code for what I posted above:

  TJDGlassBorder = class(TPersistent)
  private
    fOwner: TJDGlass; //This is the parent component
    fGlow: Integer;
    fBottom: Integer;
    fLeft: Integer;
    fTop: Integer;
    fRight: Integer;
    fColor: TColor;
    fOnEvent: TNotifyEvent;
    fAuto: Bool;
    procedure SetBottom(const Value: Integer);
    procedure SetColor(const Value: TColor);
    procedure SetGlow(const Value: Integer);
    procedure SetLeft(const Value: Integer);
    procedure SetRight(const Value: Integer);
    procedure SetTop(const Value: Integer);
    function GetBottom: Integer;
    function GetLeft: Integer;
    function GetRight: Integer;
    function GetTop: Integer;
    procedure SetAuto(const Value: Bool);
  public
    constructor Create(AOwner: TJDGlass);
    destructor Destroy; override;
    procedure Event;
    procedure Assign(Source: TPersistent); override;
  published
    property Left: Integer read GetLeft write SetLeft default 3;
    property Top: Integer read GetTop write SetTop default 2;
    property Right: Integer read GetRight write SetRight default 3;
    property Bottom: Integer read GetBottom write SetBottom default 4;
    property Color: TColor read fColor write SetColor;
    property Glow: Integer read fGlow write SetGlow default 1;
    property OnEvent: TNotifyEvent read fOnEvent write fOnEvent;
    property Auto: Bool read fAuto write SetAuto;
  end;

/////////////

{ TJDGlassBorder }

procedure TJDGlassBorder.Assign(Source: TPersistent);
begin
  inherited Assign(Source);
  Event;
end;

constructor TJDGlassBorder.Create(AOwner: TJDGlass);
begin
  fOwner:= AOwner;
  fAuto:= True;
  fColor:= clBlack;
  fGlow:= 1;
  Event;
end;

destructor TJDGlassBorder.Destroy;
begin

  inherited;
end;

procedure TJDGlassBorder.Event;
begin
  if assigned(fOwner) then 
    if fOwner <> nil then
      fOwner.Invalidate;
  if assigned(fOnEvent) then
    fOnEvent(Self);
end;

function TJDGlassBorder.GetBottom: Integer;
begin
  Result:= fBottom;
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= Max(2, fOwner.Height div 10);
        fBottom:= Result;
      end;
    end;
  end;
end;

function TJDGlassBorder.GetLeft: Integer;
begin
  Result:= fLeft;
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= (Top + Bottom) div 2;
        fLeft:= Result;
      end;
    end;
  end;
end;

function TJDGlassBorder.GetRight: Integer;
begin
  Result:= fRight;
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= (Top + Bottom) div 2;
        fRight:= Result;
      end;
    end;
  end;
end;

function TJDGlassBorder.GetTop: Integer;
begin
  Result:= fTop;
  if fAuto then begin
    if assigned(fOwner) then begin
      if fOwner <> nil then begin
        Result:= Max(1, fOwner.Height div 30);
        fTop:= Result;
      end;
    end;
  end;
end;

procedure TJDGlassBorder.SetAuto(const Value: Bool);
begin
  fAuto := Value;
  Event;
end;

procedure TJDGlassBorder.SetBottom(const Value: Integer);
begin
  if Value <> fBottom then begin
    fAuto:= False;
    fBottom := Value;  
    Event;
  end;
end;

procedure TJDGlassBorder.SetColor(const Value: TColor);
begin
  fColor := Value;  
  Event;
end;

procedure TJDGlassBorder.SetGlow(const Value: Integer);
begin
  fGlow := Value;  
  Event;
end;

procedure TJDGlassBorder.SetLeft(const Value: Integer);
begin         
  if Value <> fLeft then begin
    fAuto:= False;
    fLeft := Value;  
    Event;
  end;
end;

procedure TJDGlassBorder.SetRight(const Value: Integer);
begin       
  if Value <> fRight then begin
    fAuto:= False;
    fRight := Value; 
    Event;
  end;
end;

procedure TJDGlassBorder.SetTop(const Value: Integer);
begin        
  if Value <> fTop then begin   
    fAuto:= False;
    fTop := Value;
    Event;
  end;
end;

Upvotes: 2

Ken White
Ken White

Reputation: 125669

First, as you've already noted, remove the default. If you want the property to be True unless specifically set to another value, set it in the constructor. When the DFM is streamed in from your executable, any value stored in it will replace the one set in the constructor.

Second, your issue is based in part on faulty logic. :) If you want the Auto property to control the others (meaning if Auto = True then ignore any values set for the other properties), then test for that in the setters:

procedure TJDGlassBorder.SetTop(const Value: Integer);
begin
  // Only change the value if Auto is not True         
  if (not FAuto) and (Value <> fTop) then 
  begin  
    fTop := Value;
    Event;
  end;
end;

procedure TJDGlassBorder.SetAuto(const Value: Boolean);
begin
  if (Value <> FAuto) then
  begin
    FAuto := Value;
    if FAuto then
    begin
      FTop := 0;     // Or whatever. Set field and not property to 
      FLeft := 0;    // avoid the setter's side effects
      FWidth := 0;
      FHeight := 0;
    end;
    // Whatever you need to do now.
  end;
end;

Doing it this way means that you avoid the automatic changes by calling the setter.

I'd suggest if you can, you defer the call to Event at runtime by using Loaded; this is called after the component is fully streamed from the .dfm, and you can perform any calculations or redraws there after all properties are set. You can determine if you're in the IDE or at runtime by examining the ComponentState property; if it contains csDesigning, you're in the IDE at designtime, and if csLoading is set you're in runtime. (ComponentState is a set, so you check using if csDesigning in ComponentState.)

Upvotes: 2

Related Questions