Я использую 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
нет ничего другого, что вызывает задержку окрашивания формы, кроме вызова для создания и запуска потока. В нем также нет ничего лишнего, в нем выполняется дополнительная операция.
Вы используете VCL или FireMonkey?
@Remy Lebeau Пожалуйста, смотрите отредактированный вопрос. Также я добавил код функции, которая вызывается в цикле (правда, он вообще не связан ни с какой графической составляющей). Ничто другое не вызывает задержку (я проверил это, удалив строки создания потока в событии OnCreate
).
@Dalija Prasnikar Я использую стандартный компонент VCL TComboBox
.
Я попробовал ваш код и не вижу никаких проблем с рисованием формы - при отображении формы очень короткое мигание, поэтому либо у вас очень медленный компьютер, либо в форме есть другие элементы управления, которые также способствуют эффект, который вы видите. Мое тестовое приложение — это просто новый проект VCL с одним выпадающим списком и вашим кодом. Вы должны попробовать протестировать такое простое приложение, чтобы увидеть, как оно себя ведет.
@Dalija Prasnikar Даже для меня эффект длится всего около секунды, но без этого кода форма сразу отображается правильно (мой компьютер 2,3 ГГц...). В любом случае, странно, что код, работающий в отдельном потоке, мешает событиям рисования формы. Только после цикла идет код синхронизации.
Мешает не код, работающий в отдельном потоке, а назначение 6000 элементов в поле со списком, что вызывает проблемы с отрисовкой формы. Обойти это невозможно, так как вы должны сделать это в основном потоке.
Идея добавлять так много предметов в TComboBox
плохая. Имеет смысл отображать отфильтрованные значения. Я не могу себе представить, как пользователи будут работать с таким количеством элементов в поле со списком. И TComboBox
не предназначен для очень быстрого добавления элементов.
Или вы можете написать собственный ComboBox или найти сторонний ComboBox, который поддерживает виртуальные элементы.
Вы пытались окружить вас MainFrm.YListCombo.Items с помощью MainFrm.YListCombo.Items.BeginUpdate и MainFrm.YListCombo.Items.EndUpdate? это предотвращает перекрашивание во время заливки.
@PieterB BeginUpdate
и EndUpdate
вызываются в методе TStrings.Assign
. Нет смысла снова звонить BeginUpdate
и EndUpdate
.
В вашем коде нет ничего плохого. Добавление 6000 элементов в ComboBox происходит очень медленно и может помешать первоначальному рисованию формы, поскольку добавление элементов в ComboBox также выполняется в основном (GUI) потоке.
Вы не можете переместить это назначение в фоновый поток, так как работа с элементами управления GUI должна выполняться в основном потоке.
Проблема, которую вы видите, возникает из-за того, что код внутри потока выполняется довольно быстро и синхронизируется с основным потоком до того, как форма сможет правильно отрисовать себя.
Есть несколько возможных решений:
Поскольку код в фоновом потоке выполняется быстро, удалите поток и просто заполните поле со списком непосредственно в обработчике событий OnCreate
.
Переместите код из 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, вы можете просто сначала отключить его, а затем включить после его заполнения.
Похоже, вы правы (задержка в окраске была вызвана при выделении). Хотя заполнение ComboBox
не должно происходить более одного раза, меня не волнует, отображается ли оно пустым (или с предопределенным значением) при отображении формы. Я использую ComboBox
, чтобы пользователь не мог ввести значение, которого нет в списке (с помощью AutoCloseUp
), и нет необходимости искать значение в списке элементов...
@RemyLebeau Вы правы насчет отключения/включения поля со списком. Однако Update
не помогает, поскольку проблема не в том, что поле со списком не обновляется после заполнения, а в том, что другие элементы управления застревают, будучи наполовину окрашенными, пока элементы поля со списком заполняются, что занимает некоторое время.
@DalijaPrasnikar, поэтому я предложил Update()
заполнить форму перед заполнением поля со списком. Но в любом случае с таким количеством элементов я бы изменил пользовательский интерфейс, чтобы вместо этого использовать виртуальный ListBox/ListView, чтобы ускорить заполнение за счет повторного использования уже заполненного TStringList
.
@RemyLebeau Вопрос в том, в какой момент вы бы позвонили Update
?
Да, использование виртуального управления — еще одно решение, но вам необходимо иметь такое управление. Насколько я знаю, у TComboBox
нет виртуального режима.
@DalijaPrasnikar Форма Update()
может быть вызвана в любое время. Это аннулирует и немедленно перерисует форму. Я бы вызвал его перед назначением TStringList для ComboBox, поэтому все элементы управления перерисовываются на экране непосредственно перед тем, как заполнение ComboBox заморозит пользовательский интерфейс от обработки последующих сообщений рисования.
@RemyLebeau Вы правы, вызов Update
перед назначением элементов в поле со списком работает. Я скучал по лесу с дерева. Это определенно лучшее решение, чем спать в потоке.
@Dalija Prasnikar Нравится ли вам это? stackoverflow.com/a/74506769/18279795
Разве вызов Update
в качестве первой строки внутри метода Synchronize
не работает для вас? Я не могу комментировать WaitForInputIdle
, так как я не очень знаком с этим.
Спасибо всем за ответы и комментарии, вы мне очень помогли!
Код, который я наконец использовал (на основе ответа @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;
Я не уверен, что это лучший способ, но он отлично сработал для меня!
Этого кода недостаточно для диагностики вашей проблемы. Единственный способ, которым форма не будет правильно рисовать, как вы описали, - это если ваш основной поток пользовательского интерфейса блокируется во время работы рабочего потока. Но в показанном коде такой блокировки нет, поэтому она должна быть в коде, который вы не показали. Предоставьте минимальный воспроизводимый пример, который можно скопировать/вставить непосредственно в IDE, запустить как есть и воспроизвести проблему.