delphi - 如何在 TCustomListBox 控件中将 TListbox Items 属性替换为我自己发布的基于对象列表的类型?

标签 delphi

概述

这个问题是基于我最近问的这个问题的第二次尝试:How can I make a TList property from my custom control streamable?

尽管我接受了该问题的答案并且它有效,但我很快意识到 TCollection 不是我正在寻找的解决方案或要求。

要求

为了使我的要求尽可能简单明了,易于理解,我正在尝试:

  • 基于TCustomListBox派生新的自定义控件
  • Items 属性替换为我自己的 Items 类型,例如 TList
  • TList(Items 属性)将保存对象,每个对象包含标题和图像索引属性等。
  • 所有者绘制我的列表框并绘制其图标和文本等。
  • 创建一个属性编辑器以在设计时编辑Items

考虑到这一点,我知道如何创建自定义控件,我知道如何使用 TList 甚至 TObjectList 例如,我知道如何拥有控件,我也知道如何创建属性编辑器。

问题

我不知道如何用我自己的类型替换标准列表框 Items 类型?我有点这样做(发布我自己的同名属性(property)),只是我需要确保它可以通过 dfm 完全流式传输。

我对这个主题进行了广泛的搜索,并尝试研究 TListViewTTreeView 等发布其 Items 类型的代码,但我发现自己比以往任何时候都更加困惑。

事实上,我在另一个网站上遇到了其他人提出的这个非常古老的问题,它询问了我想做什么:Streaming a TList property of a component to a dfm 。如果链接丢失,我在下面引用它:

I recently wrote a component that publishes a TList property. I then created a property editor for the TList to enable design-time editing. The problem is that the TList doesn't stream to the dfm file, so all changes are lost when the project is closed. I assume this is because TList inherits from TObject and not from TPersistant. I was hoping there was an easy work around for this situation (or that I have misunderstood the problem to begin with). Right now all I can come up with is to switch to a TCollection or override the DefineProperties method. Is there any other way to get the information in the TList streamed to and from the dfm?

我在搜索诸如 DefineProperties() 之类的关键字时发现,鉴于这是 Remy Lebeau 在顶部链接的上一个问题中简要提到的替代选项,它似乎也是答案对于这个问题。

问题

我需要知道如何用我自己的 Items (TList) 替换 TCustomListBox 派生控件的 Items (TStrings) 属性,或者Items (TObjectList) 等类型,但使其完全可通过 dfm 进行流式传输。我从之前的评论中知道 TList 不可流式传输,但我无法像标准 TListBox 控件那样使用 TStrings ,我需要使用我自己的基于对象可流式传输的列表。

我不想使用 TCollectionDefineProperties 听起来很有希望,但我不知 Prop 体如何实现?

请您提供一些帮助,我将不胜感激。

谢谢。

最佳答案

重写 TCustomListBox 中的 DefineProperties 过程(我们在这里将其命名为 TMyListBox)。在那里可以“注册”任意数量的字段,它们将以与其他字段相同的方式存储在 dfm 中,但您不会在对象检查器中看到它们。老实说,我从未遇到过以这种方式定义多个属性,称为“数据”或“字符串”。

您可以定义“正常”属性或二进制属性。 “普通”属性对于字符串、整数、枚举等非常方便。以下是如何实现带有 captionImageIndex 的项目:

TMyListBox = class(TCustomListBox)
private
  //other stuff
  procedure ReadData(reader: TReader);
  procedure WriteData(writer: TWriter);
protected
  procedure DefineProperties(filer: TFiler); override;
  //other stuff
public
  //other stuff
  property Items: TList read fItems; //not used for streaming, not shown in object inspector. Strictly for use in code itself. We can make it read-only to avoid memory leak. 
published
  //some properties
end;

这就是 DefineProperties 实现:

procedure TMyListBox.DefineProperties(filer: TFiler);
begin
  filer.DefineProperty('data', ReadData, WriteData, items.Count>0);
end;

第四个参数,hasData 是 bool 值。当您的组件保存到 dfm 时,将调用 DefineProperties,此时可以决定是否有任何数据值得保存。如果不是,则省略“数据”属性。在此示例中,如果不存在任何项目,我们将不会拥有此属性。

如果我们希望使用此控件的视觉继承(例如,使用带有预定义值的列表框创建一个框架,然后最终在放入表单时更改它们),则可以检查此属性的值与我们的祖先有什么不同。 Filer.Ancestor 属性用于此目的。您可以在 TStrings 中观看它是如何完成的:

procedure TStrings.DefineProperties(Filer: TFiler);

  function DoWrite: Boolean;
  begin
    if Filer.Ancestor <> nil then
    begin
      Result := True;
      if Filer.Ancestor is TStrings then
        Result := not Equals(TStrings(Filer.Ancestor))
    end
    else Result := Count > 0;
  end;

begin
  Filer.DefineProperty('Strings', ReadData, WriteData, DoWrite);
end;

这会节省一点空间(或者如果存储图像则节省大量空间)并且确实很优雅,但在第一个实现中它很可能被省略。

现在是 WriteData 和 ReadData 的代码。通常写作要容易得多,我们可以从它开始:

procedure TMyListBox.WriteData(writer: TWriter);
var i: Integer;
begin
  writer.WriteListBegin; //in text dfm it will be '(' and new line
  for i:=0 to items.Count-1 do begin
    writer.WriteString(TListBoxItem(items[I]).caption);
    writer.WriteInteger(TListBoxItem(items[I]).ImageIndex);
  end;
  writer.WriteListEnd;
end;

在 dfm 中,它看起来像这样:

object MyListBox1: TMyListBox
  data = (
    'item1'
    -1
    'item2'
    -1
    'item3'
    0
    'item4'
    1)
end

TCollection 的输出对我来说似乎更优雅(三角括号,然后是项目,一个接一个),但我们这里拥有的就足够了。

现在阅读:

procedure TMyListBox.ReadData(reader: TReader);
var item: TListBoxItem;
begin
  reader.ReadListBegin;
  while not reader.EndOfList do begin
    item:=TListBoxItem.Create;
    item.Caption:=reader.ReadString;
    item.ImageIndex:=reader.ReadInteger;    
    items.Add(item); //maybe some other registering needed
  end;
  reader.ReadListEnd;
end;

就是这样。通过这种方式,可以轻松地流式传输相当复杂的结构,例如二维数组,我们在写入新行时写入WriteListBegin,然后在写入新元素时写入WriteListBegin。

小心 WriteStr/ReadStr - 这些是一些为了向后兼容而存在的过时程序,始终使用 WriteString/ReadString 相反!

另一种方法是定义二进制属性。主要用于将图像保存到 dfm 中。例如,假设 listBox 有数百个项目,我们希望压缩其中的数据以减少可执行文件的大小。然后:

TMyListBox = class(TCustomListBox)
private
  //other stuff
  procedure LoadFromStream(stream: TStream);
  procedure SaveToStream(stream: TStream);
protected
  procedure DefineProperties(filer: TFiler); override;
//etc
end;

procedure TMyListBox.DefineProperties(filer: TFiler);
  filer.DefineBinaryProperty('data',LoadFromStream,SaveToStream,items.Count>0);
end;

procedure TMyListBox.SaveToStream(stream: TStream);
var gz: TCompressionStream;
    i: Integer;
    value: Integer;
    item: TListBoxItem;
begin
  gz:=TCompressionStream.Create(stream);
  try
    value:=items.Count;
    //write number of items at first
    gz.Write(value, SizeOf(value)); 
    //properties can't be passed here, only variables
    for i:=0 to items.Count-1 do begin
      item:=TListBoxItem(items[I]);
      value:=Length(item.Caption);
      //almost as in good ol' Pascal: length of string and then string itself
      gz.Write(value,SizeOf(value));
      gz.Write(item.Caption[1], SizeOf(Char)*value); //will work in old Delphi and new (Unicode) ones
      value:=item.ImageIndex;
      gz.Write(value,SizeOf(value));
    end;
  finally
    gz.free;
  end;
end;

procedure TMyListBox.LoadFromStream(stream: TStream);
var gz: TDecompressionStream;
    i: Integer;
    count: Integer;
    value: Integer;
    item: TListBoxItem;
begin
  gz:=TDecompressionStream.Create(stream);
  try
    gz.Read(count,SizeOf(count)); //number of items
    for i:=0 to count-1 do begin
      item:=TListBoxItem.Create;
      gz.Read(value, SizeOf(value)); //length of string
      SetLength(item.caption,value);
      gz.Read(item.caption[1],SizeOf(char)*value); //we got our string
      gz.Read(value, SizeOf(value));  //imageIndex
      item.ImageIndex:=value;
      items.Add(item); //some other initialization may be needed
    end;
 finally
    gz.free;
 end;
end;

在 dfm 中它看起来像这样:

object MyListBox1: TMyListBox1
  data = {
    789C636260606005E24C86128654865C064386FF40802C62C40002009C5607CA}
end

78 是 ZLib 的签名,9C 表示默认压缩,因此它可以工作(实际上只有 2 项,而不是数百项)。当然,这只是一个示例,BinaryProperties 可以使用任何可能的格式,例如保存为 JSON 并将其放入流、XML 或自定义格式中。但我不建议使用二进制,除非它绝对不可避免,因为很难从 dfm 中看到组件中发生了什么。

对我来说,在实现组件时积极使用流似乎是个好主意:我们可以根本没有设计器,并通过手动编辑 dfm 来设置所有值,并查看组件是否行为正确。读取/加载本身可以很容易地进行测试:如果组件已加载,然后保存并且文本是相同的,那就没问题了。当流格式是“人类可读的”时,它是如此“透明”,不言自明,以至于它常常会压倒缺点(例如文件大小)(如果有的话)。

关于delphi - 如何在 TCustomListBox 控件中将 TListbox Items 属性替换为我自己发布的基于对象列表的类型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37534193/

相关文章:

delphi - 两个 TDateTime 变量之间的秒数差异

delphi - 当客户端断开连接时,为什么Indy 10的回声服务器具有较高的CPU使用率?

delphi - MVP : other constructor's parameters than view and model?

当网络丢失时,Delphi 使用 TFilestream 写入网络共享锁定文件

delphi - 为什么访问动态数组的越界索引不会引发 AV?

delphi - 如何判断我的程序的另一个实例是否已经在运行?

oracle - 如何使用 dbExpress 以 SYSDBA 身份连接到 Oracle 数据库?

amazon-web-services - 为什么我无法从 Delphi VCL 应用程序读取和写入 Amazon S3?

android - Delphi FMX TCalendar - 禁用周末

delphi - 打印 TDBGrid