Home > Back-end >  Filling TComboBox with items from TStringList using Thread
Filling TComboBox with items from TStringList using Thread

Time:11-18

I'm using a ComboBox to display a list of many items (don't worry, the differences between the items allow a quick selection by AutoComplete :-).

This list is created during the creation of the form (OnCreate event), but so that the form does not freeze while filling, I added a TThread that performs the filling to TStringList and then assigns it to ComboBox.Items.

It works, but when the form is displayed it is not drawn properly until the thread has finished and the content in the Combo is already displayed. Shouldn't using thread save this?

Here is my code: (it is not currently written in an IDE, so there may be typos...)

type
  TLoadList = class(TThread)
  public
    constructor Create;
    destructor Destroy; override;
  private
    SL: TStrings;
    procedure UpdateCombo;
    procedure Execute; override;
  end;

implementation

uses uMain, HebNumFunc; //The main form unit

constructor TLoadList.Create;
begin
  FreeOnTerminate := True;
  SL := TStringList.Create;
  inherited Create(True);
end;

procedure TLoadList.UpdateCombo;
begin
  MainFrm.YListCombo.Items := SL;
end;

procedure TLoadList.Execute;
begin
  for var I: Integer := 1 to 6000 do SL.Add(HebNumber(I));  //HebNumber This is a function that converts the value of the number to a Hebrew alphabetic value
  Synchronize(UpdateCombo);
end;  

destructor TLoadList.Destroy;
begin
  SL.Free;
  inherited;
end;

The HebNumber function is declared in the HebNumFunc unit in this way:

function HebNumber(const Number: Integer): string;

const
  Letters1: Array of String = ['','א','ב','ג','ד','ה','ו','ז','ח','ט'];
  Letters10: Array of String = ['','י','כ','ל','מ','נ','ס','ע','פ','צ'];
  Letters100: Array of String = ['','ק','ר','ש','ת','תק','תר','תש','תת','תתק'];

  function _ThousandsConv(const Value: Integer): string;
  var
    Input, Hundreds, Tens, Some: Integer;
  begin
    if Value <= 0 then Exit('');
    
    if Value = 1 then Exit(Format('%s'' ', [Letters1[Value]]));

    if Value in [2..9] then Exit(Format('%s"א ', [Letters1[Value]]));

    if Value >= 10 then
    begin
      Input := Value;
      Hundreds := Input div 100;
      Input := Input mod 100;
      Tens := Input div 10;
      Some := Input mod 10;
      Result := Format('%s%s%s"א ', [Letters100[Hundreds], Letters10[Tens], Letters1[Some]]);
    end;
  end;

var
  Input, Thousands, Hundreds, Tens, Some: Integer;
begin
  Input := Number;
  Thousands := Input div 1000;
  if Thousands > 999 then Exit('חריגה');

  Input := Input mod 1000;
  Hundreds := Input div 100;
  Input := Input mod 100;
  Tens := Input div 10;
  Some := Input mod 10;

  if (Thousands > 0) and (Hundreds   Tens   Some = 0) then
    Exit(Format('%sתתר', [_ThousandsConv(Thousands - 1)]));

  Result := Format('%s%s%s%s', [_ThousandsConv(Thousands),
    Letters100[Hundreds], Letters10[Tens], Letters1[Some]]);

  if Result.Contains('יה') then Exit(Result.Replace('יה', 'טו'));
  if Result.Contains('יו') then Result := Result.Replace('יו', 'טז');
end;

I call it in the OnCreate event simply like this:

var
  LoadList: TLoadList;
begin
  LoadList := TLoadList.Create;
  LoadList.Start;
  {...}
end;

There is nothing else in the OnCreate event that causes a delay in coloring the form, other than the call to create and run the Thread. There is also nothing extra an additional operation is performed in it.

CodePudding user response:

There is nothing wrong with your code. Adding 6000 items to the ComboBox is plenty slow and it may interfere with initial painting of the Form, since adding items to the ComboBox also runs in the main (GUI) thread.

You cannot move that assignment in the background thread, as working with GUI controls must be done in the main thread.

The problem you are seeing occurs because the code inside the thread runs pretty fast and will synchronize with the main thread before the Form has the chance to properly paint itself.

There are few possible solutions:

  1. Because code in the background thread runs fast, remove the thread and just fill the ComboBox directly within the OnCreate event handler.

  2. Move the code from OnCreate to the OnShow event handler, to run the code closer to the time when the Form will be shown. However, this event can be triggered multiple times, so you need additional boolean field to fill the ComboBox only once.

But, this will still not be enough to prevent the glitch. To do that, add a Sleep() within the thread to slow down its execution a bit and give some time to the Form to complete its initial painting. Then you can call Synchronize() to fill the ComboBox.

You may need to experiment with the sleep period, as slower computers will need more time to complete painting the Form, but don't sleep for too long as the user will be able to access the empty ComboBox in that case.

procedure MainFrm.FormShow(Sender: TObject);
begin
  if not FFilled then
    begin
      FFilled := True;
      TThread.CreateAnonymousThread(
        procedure
        var
          sl: TStringList;
        begin
          sl := TStringList.Create;
          try
            for var I: Integer := 1 to 6000 do
              sl.Add(HebNumber(I));
          
            // Sleep for some small amount of time before synchronizing
            // with the main thread to allow form to fully paint itself
            Sleep(100);

            TThread.Synchronize(nil,
              procedure
              begin
                YListCombo.Items := sl;
              end);
          finally
            sl.Free;
          end;
        end).Start;
    end;
end;

Note: I used an anonymous thread to fill up the ComboBox, as this approach does not require creating an additional class, and has simpler code. But, you don't have to change that part of your code if you don't want to.

  • Related