【问题标题】:TComboBox 'Control has no parent window' in destructor析构函数中的TComboBox'控件没有父窗口'
【发布时间】:2013-08-27 02:54:09
【问题描述】:

我正在使用 Delphi XE2。我构建了一个自定义 TComboBox,以便我可以轻松添加键/字符串对并在组件的析构函数中处理清理。

全部 if not (csDesigning in ComponentState) 为简洁起见省略代码。

interface

type
  TKeyRec = class(TObject)
    Key: string;
    Value: string;
  end;

  TMyComboBox = class(TComboBox)
  public
    destructor Destroy; override;
    procedure AddItemPair(const Key, Value: string);
  end;

implementation

destructor TMyComboBox.Destroy;
var i: Integer;
begin
  for i := 0 to Self.Items.Count - 1 do
    Self.Items.Objects[i].Free;
  Self.Clear;
  inherited;
end;

procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
  rec := TKeyRec.Create;
  rec.Key := Key;
  rec.Value := Value;
  Self.Items.AddObject(Value, rec);
end;

当应用程序关闭时,析构函数被调用,但Items.Count 属性不可访问,因为TComboBox 必须有父控件才能访问此属性。在调用析构函数时,它不再有父控件。

我以前见过这个问题,不得不将对象存储在单独的TList 中并单独释放它们。但这仅起作用,因为我将它们添加到TList 的顺序始终与添加到组合框中的字符串相同。当用户选择一个字符串时,我可以使用组合框索引在TList 中查找相关对象。如果组合框已排序,则索引将不匹配,因此我不能总是使用该解决方案。

还有其他人看过吗?你是如何解决这个问题的?能够在组件的析构函数中释放对象真是太好了!

【问题讨论】:

  • 是的。 this one 也有同样的问题。 ComboBox 将它的项目与本机控件一起保存,当它的父/所有者销毁自身时,它早已不复存在。
  • 您是否尝试过使用 BeforeDestruction (docwiki.embarcadero.com/Libraries/XE2/en/…) 代替?
  • @GerryColl - 很好地考虑了使用 BeforeDestruction。但是,同样的问题仍然存在......
  • 你不应该在清理完你的东西后至少调用继承的析构函数吗?我认为这不会解决您的问题,但是必须这样做。
  • @alzaimar - 是的,我正在这样做。我只是忘了在这里添加它。谢谢!

标签: delphi delphi-xe2 destructor


【解决方案1】:

有一种方法可以避免重写组件以使用另一个列表来保存对象。解决方案是使用WM_DESTROY 消息和ComponentState 属性。 当组件即将被销毁时,它的状态会变为csDestroying,因此下次它收到WM_DESTROY 消息时,它不会参与窗口重建过程。 我们成功地在我们的组件库中使用了这个方法。

TMyCombo = class(TCombobox)
...
  procedure   WMDestroy(var message: TMessage); message WM_DESTROY;
...

procedure TMyCombo.WMDestroy(var message: TMessage);
var
  i: integer;
begin
  if (csDestroying in ComponentState) then
    for i:=0 to Items.Count - 1 do
      Items.Objects[i].Free;
  inherited;
end;

【讨论】:

【解决方案2】:

你可以覆盖函数GetItemsClass:

function GetItemsClass: TCustomComboBoxStringsClass; override;

Combo 调用它来创建项目(默认情况下它可能是TComboBoxStrings)。 然后可以创建自己的TComboBoxStrings后代,例如TComboBoxStringObjects,其中 您可以释放与项目链接的对象(当项目被删除时)。

【讨论】:

  • 谢谢安德烈。我认为这可能会起作用,因为我可以向 TComboBoxStringObjects 类添加一个析构函数。但是,在尝试解决这个问题时,我已经阅读了很多内容,现在倾向于同意 David Heffernan 和 NGLN 在 Sertac 的链接中的评论/答案,这表明不鼓励将对象存储在 GUI 控件中。我刚刚写完将对象存储在TList 中的组件。现在,当组件被销毁时,所有内容都已正确释放。我正准备将其作为答案与您的答案一起发布。再次感谢!
  • @James:James,我仍然相信覆盖 GetItemsClass 是更好的解决方案。在您的代码中,无论如何您都在 UI 控件中存储对象。您的解决方案的一些缺点:除了标准之外,您还必须提供一些额外的方法,如果有一天您使用标准方法(例如 Items.AddObject)而不是额外的方法,您将遇到问题。我认为更好(同时更容易)的解决方案是按照我的建议改变项目的行为。在这种情况下,您将拥有标准界面 + 一些附加功能(释放链接对象的能力)。
  • 安德烈,我同意你的 cmets。我已经完成了一个解决方案,当你发布你的答案时它已经很晚了(例如,凌晨 2 点)。我今天会试一试,让你知道它是如何工作的。
  • 覆盖GetItemsClass 仍然有同样的问题。自定义TComboBoxStringObjects 类的析构函数仍然调用太晚,并且在访问Count 属性时会引发“无父窗口”错误。我认为使它工作的唯一其他方法是使用原始 Windows API 调用来检测和释放对象,这实际上是在本机代码中,但由于 {$IF DEFINED(CLR)} 语句而没有使用。
  • @詹姆斯。是的,似乎在销毁时 Win 对象不再可以通过消息访问,或者至少不能完全正常工作。您仍然可以这样做 - 如果您也覆盖 AddObject 方法并将对象的副本保存在您自己的列表中。但我只是检查代码,还有更严重的问题。 Combo 默认使用类 TComboBoxStrings,但是这个类定义在 StdCtrls 的实现部分而不是接口部分,因此不能用于扩展。因此,最好保持现有的方式或单独保存对象。
【解决方案3】:

在阅读the link from Sertac (David Heffernan's comment and NGLN's answer) 之后,我相信将对象存储在托管列表中而不是 GUI 控件中的解决方案是最好的。为此,我创建了一个从TCustomComboBox 下降的组合框。这让我可以将除Sorted 之外的所有属性提升到published。这使内部FList 与组合框Items 属性中的字符串保持同步。我只是在添加它们之前确保它们按照我想要的方式排序......

以下显示了我所做的。为简洁起见,我只包含了基本代码(较少的范围检查),但包含了一些条件逻辑,允许在没有对象的情况下使用组合框。

FListdestructor 中被正确销毁,释放所有对象而没有任何运行时异常,并且对象列表在组件本身内进行管理,而不必在其他地方进行管理——使其非常便携。当控件在设计时添加到窗体或在运行时创建时,它可以工作。我希望这对其他人有用!

interface

type
  TMyComboBox = class(TCustomComboBox)
  private
    FList: TList;
    FUsesObjects: Boolean;
    function GetKey: string;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure AddItemPair(const Key, Value: string);
    procedure ClearAllItems;
    procedure DeleteItem(const Index: Integer);
    property Key: string read GetKey;
  published
    // all published properties (except Sorted) from TComboBox
  end;

implementation

type
  TKeyRec = class(TObject)
    Key: string;
    Value: string;
  end;

function TMyComboBox.GetKey: string;
begin
  if not FUsesObjects then
    raise Exception.Create('Objects are not used.');

  Result := TKeyRec(FList.Items[ItemIndex]).Key;
end;

constructor TMyComboBox.Create(AOwner: TComponent);
begin
  inherited;

  if not (csDesigning in ComponentState) then
  begin
    FUsesObjects := False;
    FList := TList.Create;
  end;
end;

destructor TMyComboBox.Destroy;
begin
  if not (csDesigning in ComponentState) then
  begin
    ClearAllItems;
    FreeAndNil(FList);
  end;

  inherited;
end;

procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
  FUsesObjects := True;
  rec := TKeyRec.Create;
  rec.Key := Key;
  rec.Value := Value;
  FList.Add(rec);
  Items.Add(Value);
end;

procedure TMyComboBox.ClearAllItems;
var i: Integer;
begin
  if not (csDesigning in ComponentState) then
  begin
    if FUsesObjects then
    begin
      for i := 0 to FList.Count - 1 do
        TKeyRec(FList.Items[i]).Free;
      FList.Clear;
    end;
    if not (csDestroying in ComponentState) then
      Clear; // can't clear if the component is being destroyed or there is an exception, 'no parent window'
  end;
end;

procedure TMyComboBox.DeleteItem(const Index: Integer);
begin
  if FUsesObjects then
  begin
    TKeyRec(FList.Items[Index]).Free;
    FList.Delete(Index);
  end;
  Items.Delete(Index);
end;

end.

【讨论】:

  • 这和把它们放在Objects里没什么区别。您仍然拥有拥有这些对象的 GUI 小部件。
  • @DavidHeffernan - 当我写下答案时,我想到了用于绘制下拉列表的TStrings 列表。对象不由它管理。我同意它们仍然归 GUI 小部件所有。我也同意我的设计并不比让TStrings 管理它们更好,除了现在可以在小部件被销毁时释放它们。无论是我这样做还是按照 Andrei 的建议,您认为管理小部件中的对象是一个糟糕的设计吗?
  • 我不想说。我没有掌握所有的设计标准和约束。
猜你喜欢
  • 2016-12-20
  • 2011-04-16
  • 2014-06-12
  • 2019-06-12
  • 2017-08-13
  • 1970-01-01
  • 2011-12-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多