【问题标题】:accessing components of frame1 with frame2, with frame2 being on frame1, in delphi, frames are created dynamically用frame2访问frame1的组件,frame2在frame1上,在delphi中,框架是动态创建的
【发布时间】:2012-01-29 13:16:55
【问题描述】:

最初的问题必须是如何访问组件,但是我设法弄明白了。我只是在学习 Delphi,所以我很容易出现愚蠢和明显的问题。我也处于这样一个阶段,我实际上并没有写任何有用的东西,而只是在乱写一些随机的东西,看看它是如何工作的,也许还能学到一些东西。

传入的文字墙,我想解释一下我目前正在探索的内容...

基本上我有一个带有button1的form1,按下它会创建一个frame2,那个frame2有一个button2,按下button2会在frame2中创建一个frame3(它是frame3的父级和所有者)。每个框架都有另一个 freeandnil 按钮。按下每个按钮 1/2/3 后,它会被禁用以防止创建多个实例。我最初的问题是,在使用 freeandnil-button 之后,我无法访问前一帧上的按钮(它对表单工作正常,form1.button1.enabled:=true 在 frame2 内工作正常)被禁用以便重新启用它(frame2.button1.enabled:=true我认为,从 frame3 内部会造成访问冲突)。

假设我将来会写一些需要这种交流的东西?所以我在每个框架上添加了一个编辑框,另一个上有一个按钮来更改编辑框文本,这是我目前的工作解决方案:

procedure TFrame2.Button3Click(Sender: TObject);
var i,z:integer;
begin
for i := 0 to ComponentCount - 1 do
  if components[i] is tframe3 then
    for z := 0 to (components[i] as tframe3).ComponentCount - 1 do
      if (components[i] as tframe3).Components[z] is TEdit then
         ((components[i] as tframe3).Components[z] as TEdit).Text:='ping';
end;

procedure TFrame3.Button3Click(Sender: TObject);
var i:integer;
begin
for i := 0 to parent.ComponentCount-1 do
  if parent.components[i] is TEdit then
    (parent.Components[i] as TEdit).Text:='pong';
end;

如果我有一堆编辑框或其他任何东西,我想我可以使用 Tag 属性来识别它们,但是,这么多组件计数和传递 something AS something 对我来说看起来并不正确或足够有效。

我现在的问题是:能以更好的方式完成吗?有人可以提供为什么我不能以愚蠢的方式从“子框架”访问“父框架”组件的原因(即:frame2.button1.enabled:=true from inside frame3)?

【问题讨论】:

    标签: delphi components communication frames


    【解决方案1】:

    几点:

    • 控件/组件通常设置为由控制其生命周期的窗体/框架拥有,并作为显示它们的控件的父级。因此,当您创建 TEdit 以坐在 TForm 上的 TPanel 上时,您将 TForm 设置为所有者,并将 TPanel 设置为 TEdit 的父级。对于框架,您也可以这样做:框架是其上所有控件的所有者,控件是框架上任何容器(可以是框架本身)的父级,因此负责显示(绘制)它。

    • 当您迭代控件的子级时,迭代组件使用 Owner 关系;对 Controls 进行迭代使用 Parent 关系。因此,如果要遍历控件,上面的代码已经做得更少了。

    • 当然,您可以以“愚蠢”的方式引用其他控件,但您必须提供访问方法。如果您想要其他人(不仅仅是子框架),您至少必须声明一个方法来检索该控件。很多方法可以做到这一点。一种是在需要时“询问”表单(使用表单上的方法),或者让表单在创建时在框架上设置属性...

    • 避免访问表单和框架的已发布属性。它们主要用于 Delphi 的流系统。当您使用上述方法将应用程序捆绑在一起时,它很快就会变成一个令人难以置信的混乱......

    示例即将发布(今晚某个时间),我还需要一些时间来解释,而且我还有工作要做...


    以下示例仅处理帧之间的乒乓球比赛。从它自己的事件处理程序之一中释放任何表单或框架并不是一个好主意。为此使用Release 方法,因为它可以防止表单/框架在释放后处理消息。 (顺便说一下,关于这个的很多问题)。此外,当您确实从它自己的一个按钮释放框架时,您需要注意创建它的框架有机会消除它可能对该框架持有的引用,否则您正在为一些有趣的事情做准备调试混乱。查看“通知”和“NotifyControls”以及发送给其所有者/父级的表单和框架的自动通知,以便这些可以从其组件/控件集合中删除控件。在您的示例中,如果您要从其自己的“FreeAndNil”按钮的 OnClick 事件处理程序中释放 Frame3,您必须确保 Frame2 响应删除(我认为)通知消息,并且 nil 对 Frame3 的任何引用(除了那些将在组件/控件集合中自动清除的)。

    现在,乒乓球比赛。有几种方法可以解决这个问题。

    方法一

    第一种方法是您已经尝试过在另一个框架的组件上循环。虽然这肯定是一种避免“使用”另一个框架的方法,但它很麻烦而且不是很简洁。另外,当您在表单/框架上获得更多控件时,您将必须在名称上添加一个检查,以知道您拥有正确的 TEdit。然后你也可以直接使用这个名字,尤其是当一个框架在它的uses子句中已经有了另一个框架,因为它正在创建它。

    // PingFrame (your Frame2)
    uses
      ...
      Pong_fr;
    
    type
      TPingFrame = class(TFrame)
        ...
        procedure CreateChildBtnClick(Sender: TObject);
        procedure PingPongBtnClick(Sender: TObject);
      private
        FPong: TPongFrame; // This is the "extra" reference you need to nil when
                           // freeing the TPongFrame from one of its own event handlers.
        ...
      end;
    
    procedure TPingFrame.CreateChildBtnClick(Sender: TObject);
    begin
      CreateChildBtn.Enabled := False;
      FPong := TPongFrame.Create(Self);
      FPong.Parent := ContainerPnl;
      FPong.Align := alClient;
    end;
    
    procedure TPingFrame.PingPongBtnClick(Sender: TObject);
    begin
      if Assigned(FPong) then
        FPong.Edit1.Text := 'Ping';
    end;
    

    在另一端:

    // PongFrame (your Frame3)
    type
      TPongFrame = class(TFrame)
        ...
        procedure PingPongBtnClick(Sender: TObject);
      end;
    
    implementation
    
    uses
      Ping_fr;
    
    procedure TPongFrame.PingPong1BtnClick(Sender: TObject);
    begin
      (Owner as TPingFrame).Edit1.Text := 'Pong called';
    end;
    

    这种方法看起来很不错,但也有缺点:

    • 您需要使用保存 TPingFrame 的单元(本例中为 Ping_fr)。我猜还不错,但是...由于 Ping_fr 已经使用 Pong_fr(持有 TPongFrame 的单元),因此您不能在接口部分使用 Pong_fr 使用 Ping_fr,也不能在接口部分使用 Ping_fr 使用 Pong_fr。这样做会使 Delphi 由于循环引用而引发错误。这可以通过将其中一种用途移至实现部分来解决(如在 Pong_fr 单元中使用 Ping_fr 的示例中所做的那样。
    • 更大的缺点是现在两个框架之间的耦合非常紧密。您不能将任一控件中的 TEdit 更改为另一种类型的控件(除非碰巧也具有 Text 属性),而不必更改另一个单元中的代码。这种紧密耦合是一个令人头疼的问题,最好尽量避免这种情况。

    方法二

    减少框架之间的耦合并允许每个框架更改它认为合适的控件的一种方法是不直接使用另一个窗体/框架的控件。为此,您在每个框架上声明一个其他可以调用的方法。每个方法都会更新自己的控件。从 OnClick 事件处理程序中,您不再直接访问其他框架的控件,而是调用该方法

    type
      TPingFrame = class(TFrame)
        ...
      public
        procedure Ping;
      end;
    
    implementation
    
    procedure TPingFrame.PingPongBtnClick(Sender: TObject);
    begin
      if Assigned(FPong) then
        FPong.Ping;
    end;
    
    procedure TPingFrame.Ping;
    begin
      Edit1.Text := 'Someone pinged me';
    end;
    

    在另一端:

    type
      TPongFrame = class(TFrame)
        ...
      public
        procedure Ping;
      end;
    
    implementation
    
    procedure TPongFrame.PingPongBtnClick(Sender: TObject);
    begin
      (Owner as TPingFrame).Ping;
    end;
    
    procedure TPongFrame.Ping;
    begin
      Edit1.Text := 'Someone pinged me';
    end;
    

    这比方法 1 更好,因为它允许两个框架更改它们的控件,而不必担心“外人”引用它们,但仍然存在只能通过移动一个“使用”来“解决”循环引用的缺点到实施部分。

    最好尝试完全避免循环引用,而不是通过将单元移动到实现部分的 uses 子句来修补它们。我使用的经验法则是:

    任何表单/框架都可以知道并使用它实例化的表单/框架的公共接口(尽管它应该避免该接口的“默认可见性”部分中的控件),但任何表单/框架都不应该对创建它的特定表单/框架有任何了解。

    方法3

    实现这一点的一种方法(有很多)它使用事件,就像 TButton 的 OnClick 事件一样。

    在 TPongFrame 方面,您可以删除 Ping_fr 的使用。你不再需要它了。

    然后你需要声明一个属性和它引用的字段,并声明一个触发事件的方法。

    type
      TPongFrame = class(TFrame)
      private
        FOnPingPongClicked: TNotifyEvent;
      protected
        procedure DoPingPongClicked;
      public
        property OnPingPongClicked: TNotifyEvent 
          read FOnPingPongClicked write FOnPingPongClicked;
      end;
    

    Ping 方法保持不变,其实现不变。 PingPongBtnClick 事件处理程序也保留了,但它的实现现在变为:

    procedure TPongFrame.PingPongBtnClick(Sender: TObject);
    begin
      DoPingPongClicked;
    end;
    
    procedure TPongFrame.DoPingPongClicked;
    begin
      if Assigned(FOnPingPongClicked) then
        FOnPingPongClicked(Self);
    end;
    

    您可以将来自 DoPingPongClicked 的代码直接放在此处,但在单独的方法中触发事件是一种很好的做法。如果您有一个可以从代码的多个部分触发的事件,它可以避免重复完全相同的代码。它还允许后代(当你得到它们时)覆盖“触发”方法(你必须在祖先中将其标记为虚拟)并在触发事件的所有时间做一些特定的事情。

    在 TPingFrame 方面,您需要为新事件编写处理程序:

    type
      TPingFrame = class(TFrame)
        ...
      protected
        procedure HandleOnPingPongClicked(Sender: TObject);
    

    请注意,此处理程序的签名需要是 TNotifyEvent 指定的。当然,您需要确保在 TPongFrame 中触发事件时调用此事件处理程序:

    procedure TPingFrame.CreateChildBtnClick(Sender: TObject);
    begin
      CreateChildBtn.Enabled := False;
      FPong := TPongFrame.Create(Self);
      FPong.Parent := ContainerPnl;
      FPong.Align := alClient;
      // Listen to event fired by PongFrame whenever it's PingPingBtn is clicked
      FPong.OnPingPongClicked := HandleOnPingPongClicked;
    end;
    

    在处理程序代码中,您可以做您需要做的事情。可以是一般的:

    procedure TPingFrame.HandleOnPingPongClicked(Sender: TObject);
    begin
      Edit1.Text := 'OnPingPongClicked event was fired';
    end;
    

    当然,您也可以使用事件传递对触发事件的实例的引用这一事实:

    procedure TPingFrame.HandleOnPingPongClicked(Sender: TObject);
    var
      Pong: TPongFrame;
    begin
      // This checks that Sender actually is a TPongFrame and throws an exception if not
      Pong := Sender as TPongFrame; 
    
      // Use properties from the Pong instance that was passed in
      Edit1.Text := 'OnPingPongClicked event was fired by ' + Pong.Name;
    end;
    

    享受吧!

    【讨论】:

    • 谢谢,非常有用。我了解方法 1 和 2,但是我不熟悉如何准确使用 TNotifyEvent,尤其是加上我自己的属性(或为此覆盖现有属性,尝试过,不知何故确实有效)。将不得不在我眼前使用 delphi 和一些代码阅读和研究方法 3。
    • @Raith 很高兴为您提供帮助。充分利用互联网,有很多关于 Delphi 的博客,搜索关于 SO 的 Delphi 标记问题也会有很大帮助。当然,您可以随时在这里提出更多问题...
    • 我希望看到一些涵盖高级主题的综合性在线免费课程。最好有一本书,但没有一本是免费的。我发现 delphi.about.com 特别有用,但是是的,通常我在谷歌上搜索我的答案......那个,或者 Delphi 帮助文件/文档,它可以更详细地解释某些东西......
    猜你喜欢
    • 2014-08-09
    • 2018-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-05
    • 1970-01-01
    • 2019-11-22
    • 1970-01-01
    相关资源
    最近更新 更多