Заполнение TComboBox элементами из TStringList с помощью Thread

Я использую ComboBox для отображения списка многих элементов (не волнуйтесь, различия между элементами позволяют быстро выбирать их с помощью AutoComplete :-).

Этот список создается во время создания формы (событие OnCreate), но чтобы форма не зависала при заполнении, я добавил TThread, который выполняет заполнение TStringList, а затем назначает его ComboBox.Items.

Это работает, но когда форма отображается, она не отображается должным образом, пока поток не завершится и содержимое в Combo уже не будет отображаться. Разве использование потока не должно сохранить это?

Вот мой код: (в настоящее время он не написан в среде IDE, поэтому могут быть опечатки...)

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;

Функция HebNumber объявлена ​​в модуле HebNumFunc следующим образом:

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;

Я вызываю это в событии OnCreate просто так:

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

В событии OnCreate нет ничего другого, что вызывает задержку окрашивания формы, кроме вызова для создания и запуска потока. В нем также нет ничего лишнего, в нем выполняется дополнительная операция.

Этого кода недостаточно для диагностики вашей проблемы. Единственный способ, которым форма не будет правильно рисовать, как вы описали, - это если ваш основной поток пользовательского интерфейса блокируется во время работы рабочего потока. Но в показанном коде такой блокировки нет, поэтому она должна быть в коде, который вы не показали. Предоставьте минимальный воспроизводимый пример, который можно скопировать/вставить непосредственно в IDE, запустить как есть и воспроизвести проблему.

Remy Lebeau 17.11.2022 17:57

Вы используете VCL или FireMonkey?

Dalija Prasnikar 17.11.2022 20:05

@Remy Lebeau Пожалуйста, смотрите отредактированный вопрос. Также я добавил код функции, которая вызывается в цикле (правда, он вообще не связан ни с какой графической составляющей). Ничто другое не вызывает задержку (я проверил это, удалив строки создания потока в событии OnCreate).

yonni 17.11.2022 21:20

@Dalija Prasnikar Я использую стандартный компонент VCL TComboBox.

yonni 17.11.2022 21:22

Я попробовал ваш код и не вижу никаких проблем с рисованием формы - при отображении формы очень короткое мигание, поэтому либо у вас очень медленный компьютер, либо в форме есть другие элементы управления, которые также способствуют эффект, который вы видите. Мое тестовое приложение — это просто новый проект VCL с одним выпадающим списком и вашим кодом. Вы должны попробовать протестировать такое простое приложение, чтобы увидеть, как оно себя ведет.

Dalija Prasnikar 17.11.2022 21:30

@Dalija Prasnikar Даже для меня эффект длится всего около секунды, но без этого кода форма сразу отображается правильно (мой компьютер 2,3 ГГц...). В любом случае, странно, что код, работающий в отдельном потоке, мешает событиям рисования формы. Только после цикла идет код синхронизации.

yonni 17.11.2022 21:35

Мешает не код, работающий в отдельном потоке, а назначение 6000 элементов в поле со списком, что вызывает проблемы с отрисовкой формы. Обойти это невозможно, так как вы должны сделать это в основном потоке.

Dalija Prasnikar 17.11.2022 21:42

Идея добавлять так много предметов в TComboBox плохая. Имеет смысл отображать отфильтрованные значения. Я не могу себе представить, как пользователи будут работать с таким количеством элементов в поле со списком. И TComboBox не предназначен для очень быстрого добавления элементов.

Ivan Yuzafatau 17.11.2022 22:47

Или вы можете написать собственный ComboBox или найти сторонний ComboBox, который поддерживает виртуальные элементы.

Remy Lebeau 18.11.2022 01:37

Вы пытались окружить вас MainFrm.YListCombo.Items с помощью MainFrm.YListCombo.Items.BeginUpdate и MainFrm.YListCombo.Items.EndUpdate? это предотвращает перекрашивание во время заливки.

Pieter B 18.11.2022 12:46

@PieterB BeginUpdate и EndUpdate вызываются в методе TStrings.Assign. Нет смысла снова звонить BeginUpdate и EndUpdate.

Ivan Yuzafatau 18.11.2022 15:16
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
11
208
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В вашем коде нет ничего плохого. Добавление 6000 элементов в ComboBox происходит очень медленно и может помешать первоначальному рисованию формы, поскольку добавление элементов в ComboBox также выполняется в основном (GUI) потоке.

Вы не можете переместить это назначение в фоновый поток, так как работа с элементами управления GUI должна выполняться в основном потоке.

Проблема, которую вы видите, возникает из-за того, что код внутри потока выполняется довольно быстро и синхронизируется с основным потоком до того, как форма сможет правильно отрисовать себя.

Есть несколько возможных решений:

  1. Поскольку код в фоновом потоке выполняется быстро, удалите поток и просто заполните поле со списком непосредственно в обработчике событий OnCreate.

  2. Переместите код из OnCreate в обработчик событий OnShow, чтобы запустить код ближе к моменту отображения формы. Однако это событие может запускаться несколько раз, поэтому вам потребуется дополнительное логическое поле, чтобы заполнить ComboBox только один раз.

Но этого все равно будет недостаточно, чтобы предотвратить сбой. Для этого добавьте Sleep() в поток, чтобы немного замедлить его выполнение и дать форме некоторое время для завершения первоначального рисования. Затем вы можете вызвать Synchronize(), чтобы заполнить ComboBox.

Возможно, вам придется поэкспериментировать с периодом ожидания, так как более медленным компьютерам потребуется больше времени для завершения рисования формы, но не спите слишком долго, так как в этом случае пользователь сможет получить доступ к пустому ComboBox.

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;

Примечание. Я использовал анонимный поток для заполнения ComboBox, так как этот подход не требует создания дополнительного класса и имеет более простой код. Но вам не нужно менять эту часть кода, если вы этого не хотите.

Вместо того, чтобы Sleep() создавать ветку, вы можете просто Update() форму, чтобы вызвать немедленную и полную перерисовку. Что касается пользователя, обращающегося к пустому ComboBox, вы можете просто сначала отключить его, а затем включить после его заполнения.

Remy Lebeau 18.11.2022 01:42

Похоже, вы правы (задержка в окраске была вызвана при выделении). Хотя заполнение ComboBox не должно происходить более одного раза, меня не волнует, отображается ли оно пустым (или с предопределенным значением) при отображении формы. Я использую ComboBox, чтобы пользователь не мог ввести значение, которого нет в списке (с помощью AutoCloseUp), и нет необходимости искать значение в списке элементов...

yonni 18.11.2022 09:02

@RemyLebeau Вы правы насчет отключения/включения поля со списком. Однако Update не помогает, поскольку проблема не в том, что поле со списком не обновляется после заполнения, а в том, что другие элементы управления застревают, будучи наполовину окрашенными, пока элементы поля со списком заполняются, что занимает некоторое время.

Dalija Prasnikar 18.11.2022 09:59

@DalijaPrasnikar, поэтому я предложил Update() заполнить форму перед заполнением поля со списком. Но в любом случае с таким количеством элементов я бы изменил пользовательский интерфейс, чтобы вместо этого использовать виртуальный ListBox/ListView, чтобы ускорить заполнение за счет повторного использования уже заполненного TStringList.

Remy Lebeau 18.11.2022 16:41

@RemyLebeau Вопрос в том, в какой момент вы бы позвонили Update?

Dalija Prasnikar 18.11.2022 16:50

Да, использование виртуального управления — еще одно решение, но вам необходимо иметь такое управление. Насколько я знаю, у TComboBox нет виртуального режима.

Dalija Prasnikar 18.11.2022 16:53

@DalijaPrasnikar Форма Update() может быть вызвана в любое время. Это аннулирует и немедленно перерисует форму. Я бы вызвал его перед назначением TStringList для ComboBox, поэтому все элементы управления перерисовываются на экране непосредственно перед тем, как заполнение ComboBox заморозит пользовательский интерфейс от обработки последующих сообщений рисования.

Remy Lebeau 18.11.2022 16:58

@RemyLebeau Вы правы, вызов Update перед назначением элементов в поле со списком работает. Я скучал по лесу с дерева. Это определенно лучшее решение, чем спать в потоке.

Dalija Prasnikar 18.11.2022 17:28

@Dalija Prasnikar Нравится ли вам это? stackoverflow.com/a/74506769/18279795

yonni 20.11.2022 18:22

Разве вызов Update в качестве первой строки внутри метода Synchronize не работает для вас? Я не могу комментировать WaitForInputIdle, так как я не очень знаком с этим.

Dalija Prasnikar 20.11.2022 20:02

Спасибо всем за ответы и комментарии, вы мне очень помогли!

Код, который я наконец использовал (на основе ответа @Dalija Prasnikar), таков:

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;

Я не уверен, что это лучший способ, но он отлично сработал для меня!

Другие вопросы по теме