-2

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?

manlio
  • 18,345
  • 14
  • 76
  • 126
Julian
  • 493
  • 4
  • 22
  • 2
    I'm sorry, but 512 comboboxes on a single form is simply ridiculous. You need to seriously reconsider your user interface. The fact that they all contain the same items makes it even more ludicrous. – Ken White Jun 27 '16 at 03:18
  • @KenWhite Thanks for the Comment! Well, to be honest there are 8 different kinds of content but I simplified it a bit here, since this shouldn't matter for the problem... The Form needs to display a lot of information. I could replace the 512 boxes with labels and just let a box appear when one clicks on a label, but I'm not sure this would be faster... – Julian Jun 27 '16 at 03:20
  • 1
    @Julian: Sure it would. For one thing, `TLabel` is a graphical control, so it doesn't take up as many resources as a windowed control, like `TComboBox`. 512 graphical controls will run faster than 512 windowed controls. And if you are really adventurous, you could write your own custom graphical control to replace those 512 separate labels with a single control (I did that once to replace hundreds of colored `TPanel` controls with a few instances of a custom graphical control that displays multiple colored squares, and performance was vastly increased while reducing overhead by like 20-odd%). – Remy Lebeau Jun 27 '16 at 19:38
  • 1
    @Julian: Second, there are existing controls that can display multiple items and provide embedded editing of them when clicked, such as `TListView`, `TStringGrid`, `TValueListEditor`, etc. – Remy Lebeau Jun 27 '16 at 19:38

1 Answers1

3

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...
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank you for the detailed answer, answering my question and giving me advice for a better solution! I'm not happy using a pure table layout, though, since this information is sectioned and combined with labels and buttons... For now I plan to write a custom TLabel were a TEdit or TCombobox pops up when displayed info is clicked... I would need 32 of those custom TLabels then on the form... do you think this would be a valid approach or will those still be too many Objects for the form? – Julian Jun 28 '16 at 01:14