c++c++buildervclc++builder-xe2tcombobox

Faster way to create TCombobox at Runtime


I want to fill a form at runtime with a lot of comboboxes with identical lists. They also get the same event handler, which is acting depending on the Sender objects's Name. However, this takes a pretty long time and I was guessing I do something wrong.

I'm using XE2 Rad Studio C++ Builder and the VCL GUI.

Edit: Those boxes contain a different kinds of content and are distributed over a few tabPages within the form. however, it's necessary to display what it selected at at least 80 of them at a glance. Would it maybe be better to replace them with TLabels and create a TCombobox when clicking on the TLabel to select a different element?

The Code looks similar to this:

void __fastcall TForm::TForm(){
    int i=0;
    TStringList* targetlist = new TStringList();
    targetlist->Add("Normal");
    targetlist->Add("Inverted");
    Vcl::Stdctrls::TComboBox **com = new Vcl::Stdctrls::TComboBox[512];
    for(i=0;i<512;++i){
        com[i]=new Vcl::Stdctrls::TComboBox(this);
        com[i]->Parent=this;
        com[i]->Name.printf(L"Combo_%d", i);
        com[i]->SetBounds(10, 198 + 20 * i, 130, 200);
        com[i]->Items = targetlist;
        com[i]->ItemIndex = 0;
        com[i]->Style = csDropDownList;
        com[i]->OnChange = MyComboTriggerChange;
    }
}

One iteration seems to take around 20ms on my machine (testedt with std::clock), which make this part ~10s long. The pointers are deleted at the form's destruction. I just put their declarations here for simplifications.

Is there a better way to create multiple comboboxes? Maybe clone them?


Solution

  • You seriously need to redesign your UI. Using 512 TComboBox controls on one screen with the same list of values makes no logical sense, and is a waste of time and resources. There are better ways to display 512 strings on a screen, such as a TListView in report mode, or a TListBox (both of them support a virtual mode so they can share common data without wasting memory). Or use a TValueListEditor or TStringGrid with an esPickList inline editor. Or, if you are really adventurous, write a custom control from scratch so you use 1 efficient control instead of 512 separate controls. Anything is better than 512 TComboBox controls.

    That being said, TComboBox does not support a virtual mode, like TListBox and TListView do, but there are a couple of optimizations you can still make to speed up your TComboBoxes a little:

    1. don't make 512 copies of the same TStringList content. Anything you add to the TComboBox::Items is stored inside the TComboBox's memory. You should strive to reuse your single TStringList and let everything delegate to it as needed. In this case, you can set the TComboBox::Style property to csOwnerDrawFixed and use the TComboBox::OnDrawItem event to draw the TStringList strings on-demand. You still need to add strings to each TComboBox, but they can be empty strings, at least.

    2. subclass TComboBox to override its virtual CreateParams() method and it remove the CBS_HASSTRINGS window style, then the TComboBox does not actually need to store empty strings in its memory.

    Try something like this:

    class TMyComboBox : public Vcl::Stdctrls::TComboBox
    {
        typedef Vcl::Stdctrls::TComboBox inherited;
    
    private:
        TStrings *fSharedItems;
    
        void __fastcall SetSharedItems(TStrings *Values)
        {
            if (fSharedItems != Values)
            {
                fSharedItems = Values;
    
                Items->BeginUpdate();
                try
                {
                    Items->Clear();
                    if (fSharedItems)
                    {
                        for (int i = 0; i < fSharedItems->Count; ++i)
                            Items->Add(L"");
                    }
                }
                __finally
                {
                    Items->EndUpdate();
                }
            }
        }
    
    protected:
        virtual void __fastcall CreateParams(TCreateParams &Params)
        {
            inherited::CreateParams(Params);
            Params.Style &= ~CBS_HASSTRINGS;
        }
    
        virtual __fastcall DrawItem(int Index, TRect Rect, TOwnerDrawState State)
        {
            // draw the items however you want...
    
            if (fSharedItems)
                Canvas->TextRect(Rect.Left, Rect.Top, fSharedItems->Strings[Index]);
        }
    
    public:
        __fastcall TMyComboBox(TComponent *Owner)
            : Vcl::Stdctrls::TComboBox(Owner)
        {
            Style = csOwnerDrawFixed;
        }
    
        __property TStrings* SharedItems = {read=fSharedItems, write=SetSharedItems};
    };
    
    class TMyForm : public TForm
    {
        ...
    private:
        TStringList* targetlist;
        TMyComboBox **com;
        void __fastcall MyComboTriggerChange(TObject *Sender);
        ...
    public:
        __fastcall TMyForm(TComponent *Owner);
        __fastcall ~TMyForm();
        ...
    };
    
    __fastcall TMyForm::TMyForm(TComponent *Owner)
        : TForm(Owner)
    {
        targetlist = new TStringList;
        targetlist->Add("Normal");
        targetlist->Add("Inverted");
    
        com = new TMyComboBox*[512];
        for(int i=0;i<512;++i)
        {
            com[i] = new TMyComboBox(this);
            com[i]->Parent = this;
            com[i]->Name = String().sprintf(L"Combo_%d", i);
            com[i]->SetBounds(10, 198 + 20 * i, 130, 200);
            com[i]->SharedItems = targetlist;
            com[i]->ItemIndex = 0;
            com[i]->OnChange = &MyComboTriggerChange;
        }
    }
    
    __fastcall TMyForm::~TMyForm()
    {
        delete targetlist;
        delete[] com;
    }
    
    void __fastcall TMyForm::MyComboTriggerChange(TObject *Sender)
    {
        TMyComboBox *cb = static_cast<TMyComboBox*>(Sender);
        // use targetlist->Strings[cb->ItemIndex] as needed...
    }