Board index » delphi » Choosing ancestor for a custom component (LONG)

Choosing ancestor for a custom component (LONG)

I've read FAQs, I've read Borland's docs, I've read a couple of books on
Delphi, and now that I need to sit and write code, I have a component
writer's block :)

I've written simple non-visual components before and have modified custom
visual components, but I haven't written a visual component from the ground
up. I thought a custom listbox would be a good place to start, not too
hard, and lots of freeware w/source components to learn from. I need a
listbox that supports images and checkboxes, as well as some fancy custom
drawing, so that it can do this:
        http://www.lodz.pdi.net/~eristic/yyy/stylelistbox.gif
(Just using OwnerDrawFixed with an OnDrawItem event isn't enough, because I
need to handle CN_DRAWITEM in order to suppress the focus rectangle.)

I expected drawing to be the hardest part (never used canvas before), but
it was actually the easiest to figure out. The hard part is how to maintain
state objects along with the listbox's items and make sure they're always
valid and won't get accidentally clobbered by outside code. I've looked at
several free components from Torry's and DSP, and used some of them in my
apps before, but now that I'm actually *analysing* how they work, I'm
seeing some rather suspicious stuff there :)

The major problem is this: the listbox needs to maintain state of its items
(checkstate, imageindex, etc). At the same time, I want the Items.Objects
property to remain available. I would use a separate TList, but it's
impossible to keep it in sync with Items - if Items get sorted, for
example, my "state list" will be out of touch with reality.

So, the state objects must not be in a separate list; they must be stored
*in* Items.Objects. I thought of descending from TCustomListBox and storing
state in Items.Objects, as
  TStateObject = class
    CheckState : TCheckBoxState;
    ImageIndex : integer;
    Data : TObject; // for user data
  end;

I figured out how to make sure these objects are created and disposed
properly whenever items are added/inserted/deleted/cleared in the list. But
this approach won't work if someone simply does
        MyListBox.Items.AddStrings( SomeStrings )
because at this point my previous state objects will be dropped and
replaced with whatever objects SomeStrings has. After that, a reference
such as
        TStateObject( Items.Objects[i] ).CheckState
will kill the listbox.

The only way to prevent this from happening would be to override the Items
accessors so that Items cannot be manipulated directly. I've done it:

function TmyListBox.GetItems: TStrings;
begin
  raise EMyListBoxError.Create( 'Cannot access Items property directly.' );
end;

It works, but it's also counter-productive and would annoy terribly anyone
who tried to use the listbox (myself included).

Next try: descend from TWinControl and create my listbox closely following
VCL, with modifications where appropriate. But this leads to the same
problem as above, as long as I have an Items property which points to some
TStrings object under the hood. It begins to look like an imperfect OO
design: the property exposes a class (a TStrings descendant), allowing
anyone to fiddle with the class outside of the owner's (listbox's)
knowledge or control. The TStrings class, TListBoxStrings specifically,
doesn't even have any notification mechanism (like the TList has).

Last attempt: descend from TCustomControl and write my own listbox
implementation from scratch. Don't use TStrings for items, use a custom
TCollection or TList instead; give access to items in a more controlled
manner than TListBox does. Well, here the problem is that I don't know how
to :) To what extent am I controlling the listbox, and to what extent is
Windows doing its own job? Specifically, if I descend from TCustomControl,
should I still pretend I'm writing a "normal" listbox and subclass it via
CreateSubClass(Params, 'LISTBOX')? If I do that, and not have items in a
TStrings class, will Windows like it? If I don't, will my listbox get all
the LB_xxx messages it needs? What about the styles set in CreateParams -
should I use LBS_HASSTRINGS if I really don't have "strings"?  And is it
really necessary to go this far just to have checkboxes in a listbox?

(I've seen the VCL implementation of TCheckListBox, and it uses a rather
tricky runaround with a class named TCheckListBoxDataWrapper which I
haven't quite figured out yet.)

At this point, I'm really stumped. The components I've seen usually take
the easy route and wedge state objects onto Items.Objects (often neglecting
to dispose of them!) but this technique seems very dangerous, it can be so
easily subverted by whoever uses the component. I've also thought of a
"virtual" mode, e.g. the DrawItem routine could get state for each item via
an event, but this will only work for image index (which is constant), bnit
for states of checkboxes, which change with user actions (i.e. the listbox
must maintain them). So, what's the right thing to do? (Except for going
back in time to my college days and choosing a different major - I studied
English, so don't laugh ;)

Thanks in advance,
.marek

--
/"\ ASCII Ribbon Campaign - Say NO to HTML in email
\ / Homepage, PGP Public Key: http://www.pdi.net/~eristic/
 X  No ads, no nags freeware: http://www.pdi.net/~eristic/free/
/ \
"Most of what I've learned over the years has come from
signatures." (Larry Wall)

 

Re:Choosing ancestor for a custom component (LONG)


Have you looked at how tCheckListBox handles things (unit CheckLst.pas)? You
might find that it helps.

Re:Choosing ancestor for a custom component (LONG)


Hi Marek,
how about deriving your own TMyListBoxStrings class
from the VCL one? It would use the inherited
object-fields to store your TStateObject's,
and it would override InsertItem (or what it is called)
to take care to store the user-visible objects
into TStateObject.Data (similarly GetItem(?) for the
other way round).
Just a raw idea, but maybe it helps.  :-)
Ciao, Uli.

Re:Choosing ancestor for a custom component (LONG)


Quote
marek jedlinski wrote in message ...

[...]

Quote
>So, the state objects must not be in a separate list; they must be stored
>*in* Items.Objects. I thought of descending from TCustomListBox and
storing
>state in Items.Objects, as
>  TStateObject = class
>    CheckState : TCheckBoxState;
>    ImageIndex : integer;
>    Data : TObject; // for user data
>  end;

>I figured out how to make sure these objects are created and disposed
>properly whenever items are added/inserted/deleted/cleared in the list.
But
>this approach won't work if someone simply does
> MyListBox.Items.AddStrings( SomeStrings )
>because at this point my previous state objects will be dropped and
>replaced with whatever objects SomeStrings has. After that, a reference
>such as
> TStateObject( Items.Objects[i] ).CheckState
>will kill the listbox.

Perhaps you could cleverly interpose yourself between what people
_think_ is in Objects[], and what is actually there. You have a
Data field in the record declaration above; you might re-implement
all the properties and methods (should be quite a few, but doable)
that get at the Objects array and reroute them to that.

I can think of one easy way where that would fail: if a variable
is declared as a TListBox, but the actual value is your component,
the wrong code may be called to read the Objects property. Maybe
that could somehow be fixed, I don't know.

[...]

Quote
>Next try: descend from TWinControl and create my listbox closely following
>VCL, with modifications where appropriate. But this leads to the same
>problem as above, as long as I have an Items property which points to some
>TStrings object under the hood. It begins to look like an imperfect OO
>design: the property exposes a class (a TStrings descendant), allowing
>anyone to fiddle with the class outside of the owner's (listbox's)
>knowledge or control. The TStrings class, TListBoxStrings specifically,
>doesn't even have any notification mechanism (like the TList has).

Your component might expose a TStrings property, but nobody says
that it has to be implemented using a TListBoxStrings value. Your
TVeryOwnerDrawListBoxStrings object could have all the notifications
you want and more, and the component could know about it, but you
don't have to tell anyone else.

Quote
>Last attempt: descend from TCustomControl and write my own listbox
>implementation from scratch. Don't use TStrings for items, use a custom
>TCollection or TList instead; give access to items in a more controlled
>manner than TListBox does. Well, here the problem is that I don't know how
>to :) To what extent am I controlling the listbox, and to what extent is
>Windows doing its own job? Specifically, if I descend from TCustomControl,
>should I still pretend I'm writing a "normal" listbox and subclass it via
>CreateSubClass(Params, 'LISTBOX')? If I do that, and not have items in a
>TStrings class, will Windows like it?

Windows will have no problem. TStrings is an abstract class, and the
VCL TListBox implements it by querying the Windows control for values
as they are read. Your component could keep its own records, and return
the right values to queries through TStrings, and respond to all the
right messages for Windows, and it would work.

Quote
>                                      If I don't, will my listbox get all
>the LB_xxx messages it needs? What about the styles set in CreateParams -
>should I use LBS_HASSTRINGS if I really don't have "strings"?  And is it
>really necessary to go this far just to have checkboxes in a listbox?

Ignoring that last question, I think LBS_HASSTRINGS is a hint to
Windows that the control keeps its own strings. So if you implement
that, no matter _how_ you implement it, you probably should.

Quote
>(I've seen the VCL implementation of TCheckListBox, and it uses a rather
>tricky runaround with a class named TCheckListBoxDataWrapper which I
>haven't quite figured out yet.)

Figuring that out might actually be the best thing to do. It's probably
a solid implementation, even if not intuitive.

Groetjes,
Maarten Wiltink

Re:Choosing ancestor for a custom component (LONG)


On Tue, 20 Aug 2002 15:45:50 +0200, "Maarten Wiltink"

Quote
<maar...@kittensandcats.net>  wrote:
>>(I've seen the VCL implementation of TCheckListBox, and it uses a rather
>>tricky runaround with a class named TCheckListBoxDataWrapper which I
>>haven't quite figured out yet.)

>Figuring that out might actually be the best thing to do. It's probably
>a solid implementation, even if not intuitive.

Maarten, thank a lot for your answer. This last thing is what I did, and it
looks like a solid solution. Thank you!

.marek

--
/"\ ASCII Ribbon Campaign - Say NO to HTML in email
\ / Homepage, PGP Public Key: http://www.pdi.net/~eristic/
 X  No ads, no nags freeware: http://www.pdi.net/~eristic/free/
/ \
"Invalid thought detected. Close all mental processes and
restart body."

Other Threads