3

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.

yonni
  • 248
  • 2
  • 11
  • 1
    This is not enough code to diagnose your problem. The only way the Form would not draw itself correctly as you have described is if your main UI thread is being blocked while the worker thread is running. But there is no such blockage in the code shown, so it has to be in code you have not shown. Please provide a [mcve] that can be copy/pasted directly into the IDE and run as-is and reproduces the issue. – Remy Lebeau Nov 17 '22 at 16:57
  • Are you using VCL or FireMonkey? – Dalija Prasnikar Nov 17 '22 at 19:05
  • @Remy Lebeau Please see the edited question. I also added the code of the function that is called in the loop (_although it is not related to any graphic component at all_). There is nothing else causing the delay (I tested this by removing the Thread creation lines in the `OnCreate` event). – yonni Nov 17 '22 at 20:20
  • @Dalija Prasnikar I am using a standard VCL `TComboBox` component. – yonni Nov 17 '22 at 20:22
  • I have tried your code and I don't see any issues with painting the form - there is a very short blink when the form is shown, so either you have very slow computer or there are other controls on the form that also contribute to the effect you are seeing. My test application is just new VCL project with one combobox and your code. You should try testing such plain app to see how it behaves. – Dalija Prasnikar Nov 17 '22 at 20:30
  • @Dalija Prasnikar Even for me, the effect lasts only about a second, but without this code the form is immediately displayed correctly (my computer has 2.3 GHz...). Anyway, it's weird that code running on a separate thread is interfering with the form's paint events. Only after the loop is the Synchronize code. – yonni Nov 17 '22 at 20:35
  • 1
    It is not the code running in separate thread that is interfering, it is assigning 6000 items to the combobox that is causing issues with painting the form. There is no way around it as you have to do that in the main thread. – Dalija Prasnikar Nov 17 '22 at 20:42
  • The idea of adding so many items to `TComboBox` is bad. It makes sense to display filtered values. I can't imagine how users will work with so many items in the combobox. And `TComboBox` is not designed to add items very quickly. – Ivan Yuzafatau Nov 17 '22 at 21:47
  • 1
    Or, you could write a custom ComboBox, or find a 3rd-party ComboBox, that supports virtual items. – Remy Lebeau Nov 18 '22 at 00:37
  • Have you tried surrounding you MainFrm.YListCombo.Items with MainFrm.YListCombo.Items.BeginUpdate and MainFrm.YListCombo.Items.EndUpdate? this prevents repainting during filling. – Pieter B Nov 18 '22 at 11:46
  • 2
    @PieterB `BeginUpdate` and `EndUpdate` are called in the `TStrings.Assign` method. There is no point in calling `BeginUpdate` and `EndUpdate` again. – Ivan Yuzafatau Nov 18 '22 at 14:16

2 Answers2

1

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Dalija Prasnikar
  • 27,212
  • 44
  • 82
  • 159
  • 3
    Rather than `Sleep()`'ing the thread, you could just `Update()` the Form instead, to force an immediate and complete redraw. As for the user accessing an empty ComboBox, you could just disable it initially and then enable it once it has been filled. – Remy Lebeau Nov 18 '22 at 00:42
  • It seems that you are right (the delay in coloring was caused during the allocation). Although the filling of the `ComboBox` should not happen more than once, I don't care if it is displayed empty (or with a predefined value) when the form is displayed. I use a `ComboBox` to prevent the user from entering a value that does not exist in the list (with the help of `AutoCloseUp`), and there is no need to search for the value in the list of items... – yonni Nov 18 '22 at 08:02
  • @RemyLebeau You are right about disabling/enabling combo box. However, `Update` does not help as the issue is not that combo box is not updated after being filled up, but that other controls are stuck being half painted while combo box items are being populated, which takes some time. – Dalija Prasnikar Nov 18 '22 at 08:59
  • @DalijaPrasnikar which is why I suggested `Update()`'ing the Form before filing the ComboBox. But in any case, with this many items, I would change the UI to use a virtual ListBox/ListView instead to speed up the fill by reusing the `TStringList` that is already filled. – Remy Lebeau Nov 18 '22 at 15:41
  • @RemyLebeau Question is at which point would you call `Update`? – Dalija Prasnikar Nov 18 '22 at 15:50
  • Yes, using virtual control is another solution, but you need to have such control available. AFAIK, `TComboBox` does not have virtual mode. – Dalija Prasnikar Nov 18 '22 at 15:53
  • 2
    @DalijaPrasnikar the Form's `Update()` can be called at any time. It will invalidate and redraw the Form immediately. I would call it before assigning the TStringList to the ComboBox, so all of the controls are repainted onscreen just before the ComboBox fill freezes the UI from processing subsequent paint messages. – Remy Lebeau Nov 18 '22 at 15:58
  • 1
    @RemyLebeau You are right, calling `Update` before assigning items to combo box works. I missed forest from a tree. This is definitely a better solution than sleeping in a thread. – Dalija Prasnikar Nov 18 '22 at 16:28
  • @Dalija Prasnikar Do you like this way? https://stackoverflow.com/a/74506769/18279795 – yonni Nov 20 '22 at 17:22
  • Doesn't calling `Update` as first line inside `Synchronize` method does not work for you? I cannot comment on `WaitForInputIdle` since I am not very familiar with it. – Dalija Prasnikar Nov 20 '22 at 19:02
0

Thanks to all the replies and comments, you helped me a lot!

The code I finally used (based on @Dalija Prasnikar answer) is this:

procedure TMyForm.FillYListCombo;
var
  SL: TStrings;
begin
  SL := TStringList.Create;
  try
    for var I: Integer := 1 to 6000 do
      SL.Add(HebNumber(I));

    //Waiting for the form to finish loading...
    WaitForInputIdle(GetCurrentProcess, 1000);

    TThread.Synchronize(nil,
                        procedure
                        begin
                          YListCombo.Items.Assign(SL);
                        end);
  finally
    SL.Free;
  end;
end;

procedure TMyForm.FormCreate(Sender: TObject);
begin
  TThread.CreateAnonymousThread(FillYListCombo).Start;
  {...}
end;

I'm not sure if this is the best way, but it worked great for me!

yonni
  • 248
  • 2
  • 11