Search code examples
c++buildervcl

Virtual TListView Item->SubItems->Assign() during OnData triggers refresh and hence never ending updates


Maintaining an older project using c++ Builder 2009

In a TListView instance (ViewStyle = vsReport), setup to operate virtual (OwnerData = true) I wanted to try and improve speed as much as possible. Every tiny bit helps. In the OnData event I noticed that Item->SubItems->Capacity = 0 to start with, and it increases per 4 as sub items are added. I read in the docs that Capacity is read only, yet I want to avoid TStrings' internal reallocating as much as possible. Since I also need to do caching I figured I'd use a TStringList as cache that has already grown to the required capacity. I assume TStrings Assign() will then immediately allocate an array big enough to store the required amount of Strings in ?

Item->SubItems->Assign(Cache.SubItems) ;

While this works I noticed this triggers ListView to call OnData again, and again and ... causing it to never stop.

Easily fixed again by doing this:

for (int x = 0 ; x < Cache.SubItems->Count ; x++)
    Item->SubItems->Add(Cache.SubItems->Strings[x]) ;

But of course the whole point was to be able to tell SubItems the amount of strings from the start.

I realize I may be running against an old VCL issue ? That has long been resolved ? Or is there a point to this behavior that I don't understand right now ?

Is there a way to 'enable' Capacity to accept input ? So that it allocates just enough space for the strings that will be added ?


Solution

  • The TListView::OnData event is triggered whenever the ListView needs data for a given list item, such as (but not limited to) drawing operations.

    Note that when the OnData event is triggered, the TListItem::SubItems has already been Clear()'ed beforehand. TStringList::Clear() sets the Capacity to 0, freeing its current string array. That is why the Count and Capacity are always 0 when entering your OnData handler.

    The SubItems property is implemented as a TSubItems object, which derives from TStringList. The TStrings::Capacity property setter is implemented in TStringList, and does what you expect to pre-allocate the string array. But that is all it does - allocate VCL memory for the array, nothing more. There is still the aspect of updating the ListView subitems themselves at the Win32 API layer, and that has to be done individually as each string is added to the SubItems.

    When your OnData handler calls SubItems->Assign(), you are calling TStrings::Assign() (as TStringList and TSubItems do not override it). However, TStrings::Assign() DOES NOT pre-allocate the string array to the size of the source TStrings object, as one would expect (at least, not in CB2009, I don't know if modern versions do or not). Internally, Assign() merely calls Clear() and then TStrings::AddStrings() (which neither TStringList nor TSubItems override to pre-allocate the array). AddStrings() merely calls TStrings::AddObject() in a loop (which both TStringList and TSubItems do override).

    All of that clearing and adding logic is wrapped in a pair of TStrings::(Begin|End)Update() calls. This is important to note, because TSubItems reacts to the update counter. When the counter falls to 0, TSubItems triggers TListView to make some internal updates, which includes calling Invalidate() on itself, which triggers a whole repaint, and thus triggering a new series of OnData events for list items that need re-drawing.

    On the other hand, when you call SubItems->Add() in your own manual loop, and omit the (Begin|End)Update() calls, you are skipping the repaint of the whole ListView. TSubItems overrides TStrings::Add/Object() to (among other things) update only the specific ListView item that it is linked to. It does not repaint the whole ListView.

    So, you should be able to set the Capacity before entering your manual loop, if you really want to:

    Item->SubItems->Capacity = Cache.SubItems->Count;
    for (int x = 0; x < Cache.SubItems->Count; ++x)
        Item->SubItems->Add(Cache.SubItems->Strings[x]);
    

    In which case, you can use AddStrings() instead of a manual loop:

    Item->SubItems->Capacity = Cache.SubItems->Count;
    Item->SubItems->AddStrings(Cache.SubItems);