在组件的持久化中实现 `Auto` 属性
Posted
技术标签:
【中文标题】在组件的持久化中实现 `Auto` 属性【英文标题】:Implementing `Auto` property in component's persistent 【发布时间】:2012-01-09 19:44:34 【问题描述】:我有一个具有相应持久性的组件。这个持久化当然是作为组件的属性发布的,显示在对象检查器中。它有自己的几个不同的属性,主要是 4 个整数(左、上、右、下)。我在这个持久化中还有一个名为Auto
的属性,旨在根据组件的大小自动计算 4 个整数。
更具体地说,这个图形组件周围有一个边框,每条边都可以有不同大小的边框。这个持久性及其属性指定了每边边框的厚度。当Auto
开启时,4个边框边是根据组件的大小来计算的。
现在它似乎在大多数情况下都可以正常工作,但不知何故,这个 Auto
属性会随机返回 False。此外,在设计时,在启用Auto
的情况下调整组件大小时,它实际上会完美地相应地进行计算。但是,在运行时,它会返回 False,不再计算它们。在启用Auto
保存后,关闭表单,然后重新打开它,这个Auto
属性又回到了False。
这 4 个整数属性具有设置器,如果设置了它们,它会将 Auto
属性设置为 false。我假设这是导致它再次变为 false 的原因,但我没有告诉它在任何地方设置这些属性。
这是持久的:
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;
编辑:
我在上面的代码中尝试了另外 3 件事,但仍然有问题。这就是我所做的:
1:在其他 4 个属性之后发布 Auto
属性,考虑这些属性的检索顺序。
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;
改为:
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:在这些整数的属性设置器中,我正在检查新值是否与现有值不同...
procedure TJDGlassBorder.SetTop(const Value: Integer);
begin
if Value <> fTop then begin
fAuto:= False;
fTop := Value;
Event;
end;
end;
3:在这些整数的属性 getter 中,我更改了它检查现有值的方式...
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;
同样,这些尝试都没有奏效,我仍然有这个问题。
【问题讨论】:
阅读我对您发布的答案所做的评论。此外,您已经了解到default
会阻止存储该属性,除非该值与默认值不同,但它不会将该属性设置为默认值。为什么你会认为整数属性的处理方式与布尔值不同?
@KenWhite:我从来没有提到治疗上有什么不同。但我确实知道发布属性的顺序很重要。保存到 DFM 时,我相信它会按照它们在类中发布的顺序保存它们,同时,它们会按照它们在 DFM 中保存的顺序加载。如果先加载Auto
,然后加载4个整数,那么整数设置会触发Auto
再次切换回false,即使它被保存为True
。移动 Auto
在整数之后发布,使其在整数之后从 DFM 加载,这是部分原因
顺序没有区别。它们是根据 RTTI 加载的(这就是为什么存储在 DFM 中的所有属性都必须是 published
,并且会自动添加 $M+
编译器选项。您的逻辑是基于对流式传输工作原理的误解。您可能有setter 过程搞砸了,但流式机制不可能这样做。
我在标准 VCL 库中看到了很多地方(不知道具体示例),在代码中明确注释了某些属性必须在某些其他属性之前/之后发布,因为某些原因.我不完全确定保存/加载东西的顺序,但我确信从 DFM 加载/设置属性的顺序确实有很大的不同。成像 1) 将 Auto
设置为 True,2) 将 Left
设置为 20,然后 3) 将 Left
设置为 20(上面是我的代码)将 Auto
设置回 false。
(也就是说,即使不考虑 DFM,也可以考虑按不同的顺序设置这些属性)
【参考方案1】:
已修复!
我在上面的编辑中进行的 3 次尝试是部分问题,但实际修复是删除 Auto
属性的 default
。问题是,我将此默认设置为 True,在这种情况下,该属性未保存在 DFM 文件中。因此它甚至没有尝试设置这个Auto
属性。去掉 Default 固定,因为现在无论是真还是假,它总是保存在 DFM 文件中,因此总是设置这个值。发布属性的顺序也是问题的一半。
这是我上面发布的最终代码:
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;
【讨论】:
如果您希望它基本上为 True 除非另有特别设置,请在构造函数中将其设置为 True。然后当 .DFM 流入时,其中的任何存储值都将替换您在构造函数中设置的值。【参考方案2】:首先,正如您已经注意到的,删除default
。如果您希望属性为True
,除非专门设置为另一个值,请在构造函数中设置它。当DFM
从您的可执行文件中流入时,其中存储的任何值都将替换构造函数中的集合。
其次,您的问题部分是基于错误的逻辑。 :) 如果您希望 Auto
属性控制其他属性(意味着如果 Auto = True
则忽略为其他属性设置的任何值),然后在设置器中进行测试:
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;
这样做意味着您可以通过调用 setter 来避免自动更改。
如果可以的话,我建议您在运行时使用Loaded
推迟对Event
的调用;这在组件从 .dfm 完全流式传输后调用,您可以在设置所有属性后执行任何计算或重绘。您可以通过检查ComponentState
属性来确定您是在IDE 中还是在运行时;如果它包含csDesigning
,那么您在设计时就在IDE 中,如果设置了csLoading
,那么您就在运行时。 (ComponentState
是一个集合,所以你检查使用if csDesigning in ComponentState
。)
【讨论】:
实际上,它应该在设置值时更改 fAuto,无论是在设计时还是运行时。这就是它的全部目的。与设置(例如,其他一些 VCL 控件的“ParentColor”)的工作方式相同。它一开始并没有按预期工作,但我已经弄清楚了。谢谢。 我的意思是,如果用户(在设计或运行时)决定分配 4 个整数值之一,那么应该将 Auto 属性设置为 false。 另外,如果启用自动,我没有 FTop、FLeft 等 - 它需要根据控件的当前大小在属性 getter 中动态计算它。如果我像这样使用这些字段,那么当控件调整大小时,它将保持不变。这样做的重点是当场计算这些值,而不是存储字段。 如果自动未启用,你从哪里得到值(如果你没有内部存储)?例如,假设我设置了四个值(Top
、Left
、Width
和Height
),然后不小心将Auto
设置为True
,实现错误,并将其设置回False
。然后我必须返回并重置其他四个属性吗?如果没有,您必须在内部某个地方为它们存储空间。正确的?哦,是的 - 你确实有 fLeft
、fTop
等 - 我只是看看。 :)
是的,我有它们,但如果启用了自动,我就不会使用它们。属性获取器检查 Auto 属性,如果启用了 Auto,那么它会查看父控件的尺寸并当场计算值。如果您不小心在例如面板中启用了“ParentColor”,并将其切换回 False,同样的处理。以上是关于在组件的持久化中实现 `Auto` 属性的主要内容,如果未能解决你的问题,请参考以下文章
有啥方法可以单独在 html 和 javascript 中实现持久的、可共享的存储?