Jump to content
shineworld

A native VCL, and not Windows-based, TComboBox control.

Recommended Posts

A native VCL, and not Windows-based, TComboBox cotrol.

In one application I have hundreds and hundreds of TComboBoxes with values None and then from 1 to 256.
I have used many strategies to enter values in these combos but the time required is enormous.
The fastest system was to leave them empty, at FormCreate create the fields by hand by searching
all TComboBox components ...... and on some PCs it takes almost 3 or more seconds....

 

        // creates components hash and binds component events to code and adds inputs/outputs items
        C := ComponentCount - 1;
        for I := 0 to C do
        begin
          Component := Components[I];
          FComponentsHashManager.Add(Component);
          if Component is TComboBox then
          begin
            with TComboBox(Component) do
            begin
              if Pos('InputNum', Name) <> 0 then
              begin
                SendMessage(Handle, WM_SETREDRAW, Ord(False), 0);
                SendMessage(Handle, CB_INITSTORAGE, I_O.MAX_DIGITAL_INPUTS + 1, COMBOBOX_MAX_TEXT_LENGTH * (I_O.MAX_DIGITAL_INPUTS + 1));
                for J := 0 to I_O.MAX_DIGITAL_INPUTS do
                  Items.Insert(J, InputsList.Strings[J]);
                SendMessage(Handle, WM_SETREDRAW, Ord(True), 0);
                Continue;
              end;
              if Pos('OutNum', Name) <> 0 then
              begin
                SendMessage(Handle, WM_SETREDRAW, Ord(False), 0);
                SendMessage(Handle, CB_INITSTORAGE, I_O.MAX_DIGITAL_OUTPUTS + 1, COMBOBOX_MAX_TEXT_LENGTH * (I_O.MAX_DIGITAL_OUTPUTS + 1));
                for J := 0 to I_O.MAX_DIGITAL_OUTPUTS do
                  Items.Insert(J, OutputsList.Strings[J]);
                SendMessage(Handle, WM_SETREDRAW, Ord(True), 0);
                Continue;
              end;
            end;
          end;
		...

 

Share this post


Link to post
20 minutes ago, Lars Fosdal said:

Not sure why this was attached to a different thread, so I split it off

Perhaps it was a request (in Delphi 13) for a new VCL combo box control that is not a subclass of the Win32API COMBOBOX class.

  • Like 1

Share this post


Link to post

That's possible, @JonRobertson, but adding context to code is probably smart.

Anyways, @shineworld should do a proper write-up and perhaps include a demo app with a comparison that shows the benefits of doing it his way, and post it on github and the QualityPortal when it comes back online.

 

Share this post


Link to post
2 hours ago, shineworld said:

SendMessage(Handle, WM_SETREDRAW, Ord(False), 0);
SendMessage(Handle, CB_INITSTORAGE, I_O.MAX_DIGITAL_INPUTS + 1, COMBOBOX_MAX_TEXT_LENGTH * (I_O.MAX_DIGITAL_INPUTS + 1));
for J := 0 to I_O.MAX_DIGITAL_INPUTS do
  Items.Insert(J, InputsList.Strings[J]);
SendMessage(Handle, WM_SETREDRAW, Ord(True), 0);

 

Some suggestions:

  • don't use WM_SETREDRAW directly. Use the TComboBox.Items.(Begin|End)Update() instead, which will use WM_SETREDRAW internally for you.
  • CB_INITSTORAGE helps, but simply don't waste time putting actual string values in so many TComboBoxes to begin with.  Instead, fill them with blank strings, and set their Style property to csOwnerDrawFixed and use their OnDrawItem event to render the actual text whenever a drop-down list is visible.
  • consider using a different UI control, such as a TListBox, or a TListView in vsReport mode. Both have true virtual modes that can handle a lot of items very quickly.
Edited by Remy Lebeau
  • Like 1

Share this post


Link to post

Common to populate the list in OnDropDown. 

procedure TForm1.ComboBox1DropDown(Sender: TObject);
begin
   ComboBox1.Items.BeginUpdate;
   Try
     ComboBox1.Items.Clear;
     ComboBox1.Items.Add('None');
     for var I:integer := 1 to 256 do ComboBox1.Items.Add(IntToStr(I));
   Finally
     ComboBox1.Items.EndUpdate;
   End;
end;

Can also clear the list in OnExit. 

procedure TForm1.ComboBox1DropDown(Sender: TObject);
begin
   Var AComboBox := TComboBox(Sender);
   If AComboBox.Items.Count = 0 then
   begin
     AComboBox.Items.BeginUpdate;
     Try
       AComboBox.Items.Add('None');
       for var I:integer := 1 to 256 do AComboBox.Items.Add(IntToStr(I));
     Finally
       AComboBox.Items.EndUpdate;
     End;
   end;
end;

procedure TForm1.ComboBox1Exit(Sender: TObject);
begin
  TComboBox(Sender).Items.Clear;
end;

 
 

Edited by Brian Evans
  • Like 2

Share this post


Link to post
5 hours ago, shineworld said:

In one application I have hundreds and hundreds of TComboBoxes

Perhaps a combo box isn't the best option here - a numeric input might be a better option - use 0 for none or create your own custom control. 

  • Like 1

Share this post


Link to post

Seems that you populate your controls for an automation / building application. It' necessary to view the values with a combobox ?
 

You can use a label (or other simple control) for every field and when a user click in one of them you can propose a combobox to change the present value or numeric keyboard or others custom control.

Edited by DelphiUdIT
  • Like 1

Share this post


Link to post

I've used a TComboBox set as List because arguments start from None, 1, 2 .. 256 and use can fastly move to any using key:
image.thumb.png.0560ec6e284bbf42a3da5955cec6b376.png

However, I guess I to move them to the Edit field and check the entered data as suggested.
In the past I've used old SweedControls combobox made by zero with Delphi code but moving to 64bit the old and not supported library does not work fine.

 

Share this post


Link to post
1 minute ago, shineworld said:

I guess I to move them to the Edit field and check the entered data as suggested.

Will work, but will need lot of details to be ugly.

 

I am suggesting as Remy suggested, switch all the CobmoBoxes to custom draw (virtual mode) and solve this for good.

Share this post


Link to post

Here is a sample of runtime created controls that use same string.  Plus assigns event handler. 

const
  InputPLC256 = 'x0,x1,x32,pin64,pin128,pin255,None';

procedure TForm1.Button4Click(Sender: TObject);
begin
  for var I := 1 to 100 do
    begin
      var P := TPanel.Create(Self);
      with P do begin
        Name := 'TagInputEdit_' + I.ToString;
        SetBounds(280 - 5, 17 + I * 34, 204, 30);
        Parent := Self;
        Caption := '';
      end;
      var L := TLabel.Create(Self);
      with L do begin
        SetBounds(80, 6, 64, 22);
        Parent := P;
      end;

      var CB := TComboBox.Create(Self);
      with CB do begin
        SetBounds(5, 3, 65, 22);
        Parent := P;
        Show;
        onchange := TagUpdater;
        Tag := NativeInt(L);
        items.CommaText := InputPLC256;//memo1.Lines;
        Text := Items[Random(items.Count)-1];
        L.Caption := Text;
      end;
    end;
end;

procedure TForm1.TagUpdater(Sender: Tobject);
begin
  var S := Sender as TComboBox;
  var L := TLabel(S.Tag);
  var N := S.Parent.Name;
  L.Caption := Format('%s %s', [N, S.Text]);
end;

 

  • Like 1

Share this post


Link to post

I will note that most programmers completely reject the "with" construct.

  • Like 3

Share this post


Link to post
27 minutes ago, Stano said:

I will note that most programmers completely reject the "with" construct.

As does the Delphi debugger.

  • Like 3

Share this post


Link to post
14 hours ago, shineworld said:

I've used a TComboBox set as List because arguments start from None, 1, 2 .. 256 and use can fastly move to any using key:
image.thumb.png.0560ec6e284bbf42a3da5955cec6b376.png

 

You say you have hundreds of TComboBoxes, but the screenshot above only shows 7 (presumably 11?).  If this screen is similar for the rest of the app, where each item on the side list displays its own page with just a handful of ComboBoxes on it, then a simple solution would be to NOT load every page at one time, but instead to load one page at a time only when it is made visible to the user, and then unload it when it is no longer visible to the user.  That will greatly speed up your load times, and then you don't have to resort to using hacks like owner-drawing, OnDropDown refreshes, etc.  You can move each page to its own TFrame that you create and destroy when needed, that way you still regain design-time support for your page layout.

  • Like 4

Share this post


Link to post

Alternatively, your UI seems to be very similar to a table/grid layout so you could potentially use a grid component with support to input fields such as combo boxes. Those grids are usually highly optimized to handle thousands of rows with various data types and input options. They usually allow the dynamic instantiation of input controls (such as combo boxes) when a row is actually being edited to save resources.

I highly recommend DevExpress' ExpressQuantumGrid: https://docs.devexpress.com/VCL/171093/ExpressQuantumGrid/vcl-data-grid

Or their ExpressVerticalGrid: https://docs.devexpress.com/VCL/403720/ExpressVerticalGrid/concepts/vcl-vertical-grid

Both of which I extensively use for this kind of tasks.

  • Like 2

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×