Craig
Craig

Reputation: 1956

How to replace TListbox Items property with my own published object list based type in a TCustomListBox control?

Overview

This question is a second attempt based on this one I recently asked: How can I make a TList property from my custom control streamable?

Although I accepted the answer in that question and it worked, I soon realized that TCollection is not the solution or requirement I was looking for.

Requirements

To keep my requirements as simple and clear to understand as possible, this is what I am attempting to:

With that in mind, I know how to create the custom control, I know how to work with TList or even TObjectList for example, I know how to ownerdraw the control and I also know how to create the property editor.

Problem

What I don't know is how to replace the standard listbox Items type with my own? well I kind of do (publishing my own property that shares the same name), only I need to make sure it is fully streamable with the dfm.

I have searched extensively on this subject and have tried studying code where TListView and TTreeView etc publishes its Items type but I have found myself more confused than ever.

In fact I came across this very old question asked by someone else on a different website which asks very much what I want to do: Streaming a TList property of a component to a dfm. I have quoted it below in the event the link is lost:

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?

I came across that whilst searching keywords such as DefineProperties() given that this was an alternative option Remy Lebeau briefly touched upon in the previous question linked at the top, it also seemed to be the answer to that question.

Question

I need to know how to replace the Items (TStrings) property of a TCustomListBox derived control with my own Items (TList) or Items (TObjectList) etc type but make it fully streamable with the dfm. I know from previous comments TList is not streamable but I cannot use TStrings like the standard TListBox control does, I need to use my own object based list that is streamable.

I don't want to use TCollection, DefineProperties sounds promising but I don't know how exactly I would implement this?

I would greatly appreciate some help with this please.

Thank you.

Upvotes: 2

Views: 1257

Answers (1)

Yuriy Afanasenkov
Yuriy Afanasenkov

Reputation: 1440

Override DefineProperties procedure in your TCustomListBox (let's name it TMyListBox here). In there it's possible to "register" as many fields as you wish, they will be stored in dfm in the same way as other fields, but you won't see them in object inspector. To be honest, I've never encountered having more then one property defined this way, called 'data' or 'strings'.

You can define 'normal' property or binary one. 'Normal' properties are quite handy for strings, integers, enumerations and so on. Here is how items with caption and ImageIndex can be implemented:

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;

that's DefineProperties implementation:

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

fourth argument, hasData is Boolean. When your component is saved to dfm, DefineProperties is called and it's possible to decide at that moment is there any data worth saving. If not, 'data' property is omitted. In this example, we won't have this property if there is no items present.

If we expect to ever use visual inheritance of this control (for example, create a frame with this listBox with predefined values and then eventually change them when put to form), there is a possibility to check, is value of this property any different than on our ancestor. Filer.Ancestor property is used for it. You can watch how it's done in 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;

This would save a little bit of space (or lots of space if image is stored within) and sure is elegant, but in first implementation it can well be omitted.

Now the code for WriteData and ReadData. Writing is much easier usually and we may begin with it:

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;

In dfm it will look like this:

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

Output from TCollection seems more elegant to me (triangular brackets and then items, one after another), but what we have here would suffice.

Now reading it:

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;

That's it. In such a way rather complex structures can be streamed with ease, for example, two-dimensional arrays, we WriteListBegin when writing new row and then when writing new element.

Beware of WriteStr / ReadStr - these are some archaic procedures which exist for backward compatibility, ALWAYS use WriteString / ReadString instead!

Other way to do is to define binary property. That's used mostly for saving images into dfm. Let's say, for example, that listBox has hundreds of items and we'd like to compress data in it to reduce size of executable. Then:

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;

In dfm it would look like this:

object MyListBox1: TMyListBox1
  data = {
    789C636260606005E24C86128654865C064386FF40802C62C40002009C5607CA}
end

78 is sort of signature of ZLib, 9C means default compression, so it works (there are only 2 items actually, not hundreds). Of course, this is just one example, with BinaryProperties any possible format may be used, for example saving to JSON and putting it into stream, or XML or something custom. But I'd not recommend to use binary unless it's absolutely inevitable, because it's difficult to see from dfm, what happens in component.

It seems like good idea to me to actively use streaming when implementing component: we can have no designer at all and set all values by manually editing dfm and see if component behaves correctly. Reading/loading itself can be tested easily: if component is loaded, then saved and text is just the same, it's all right. It's so 'transparent' when streaming format is 'human-readable', self-explaining that it often overweighs drawbacks (like file size) if there are any.

Upvotes: 3

Related Questions