Подождите, пока макет управления не закончен
Я загружаю довольно много богатого текста в RichTextBox
(WPF) и я хочу прокрутить до конца контента:
richTextBox.Document.Blocks.Add(...)
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();
Это не работает, ScrollToEnd
выполняется, когда макет не закончен, поэтому он не прокручивается до конца, он прокручивается примерно до первой трети текста.
Есть ли способ заставить ждать, пока RichTextBox
завершил свои операции покраски и макета, чтобы ScrollToEnd
на самом деле прокручивается до конца текста?
Благодарю.
Вещи, которые не работают:
РЕДАКТИРОВАТЬ: я попробовал LayoutUpdated
событие, но оно запускается немедленно, та же проблема: элемент управления все еще выкладывает больше текста внутри richtextbox, когда он запускается, так что даже ScrollToEnd
там не работает... Я попробовал это:
richTextBox.Document.Blocks.Add(...)
richTextBoxLayoutChanged = true;
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();
и внутри richTextBox.LayoutUpdated
обработчик события:
if (richTextBoxLayoutChanged)
{
richTextBoxLayoutChanged = false;
richTextBox.ScrollToEnd();
}
Событие запускается правильно, но слишком рано, richtextbox все еще добавляет больше текста, когда оно запущено, макет еще не закончен, поэтому ScrollToEnd
снова не удается
РЕДАКТИРОВАТЬ 2: После на dowhilefor ответ: MSDN на InvalidateArrange говорит
После аннулирования у элемента будет обновлен его макет, который будет происходить асинхронно, если только впоследствии не будет принудительно установлен UpdateLayout.
Еще даже
richTextBox.InvalidateArrange();
richTextBox.InvalidateMeasure();
richTextBox.UpdateLayout();
НЕ ждет: после этих вызовов richtextbox все еще добавляет текст и асинхронно размещает его внутри себя. ARG!
7 ответов
Посмотрите на UpdateLayout
особенно:
Вызов этого метода не имеет никакого эффекта, если макет не изменился, или если ни расположение, ни состояние измерения макета недопустимы
Поэтому вызов InvalidateMeasure или InvalidateArrange, в зависимости от ваших потребностей, должен работать.
Но учитывая ваш кусок кода. Я думаю, что это не сработает. Большая часть загрузки и создания WPF отложена, поэтому добавление чего-либо в Document.Blocks не обязательно напрямую изменяет пользовательский интерфейс. Но я должен сказать, что это всего лишь предположение, и, возможно, я ошибаюсь.
У меня была похожая ситуация: у меня есть диалоговое окно предварительного просмотра, которое создает необычный рендеринг. Обычно пользователь нажимает кнопку, чтобы фактически распечатать ее, но я также хотел использовать ее для сохранения изображения без участия пользователя. В этом случае создание изображения должно ждать завершения макета.
Мне удалось это с помощью следующего:
Dispatcher.Invoke(new Action(() => {SaveDocumentAsImage(....);}), DispatcherPriority.ContextIdle);
Ключ является DispatcherPriority.ContextIdle
, который ждет, пока фоновые задачи не будут выполнены.
Изменить: согласно запросу Зака, включая код, применимый для этого конкретного случая:
Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle);
Я должен отметить, что я не очень доволен этим решением, так как оно кажется невероятно хрупким. Тем не менее, это, кажется, работает в моем конкретном случае.
С.net 4.5 или пакетом async blc вы можете использовать следующий метод расширения
/// <summary>
/// Async Wait for a Uielement to be loaded
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static Task WaitForLoaded(this FrameworkElement element)
{
var tcs = new TaskCompletionSource<object>();
RoutedEventHandler handler = null;
handler = (s, e) =>
{
element.Loaded -= handler;
tcs.SetResult(null);
};
element.Loaded += handler;
return tcs.Task;
}
Ответ @Andreas работает хорошо.
Однако что, если элемент управления уже загружен? Событие никогда не сработает, и ожидание может зависнуть навсегда. Чтобы это исправить, немедленно вернитесь, если форма уже загружена:
/// <summary>
/// Intent: Wait until control is loaded.
/// </summary>
public static Task WaitForLoaded(this FrameworkElement element)
{
var tcs = new TaskCompletionSource<object>();
RoutedEventHandler handler = null;
handler = (s, e) =>
{
element.Loaded -= handler;
tcs.SetResult(null);
};
element.Loaded += handler;
if (element.IsLoaded == true)
{
element.Loaded -= handler;
tcs.SetResult(null);
}
return tcs.Task;
}
Дополнительные советы
Эти подсказки могут или не могут быть полезны.
Код выше действительно полезен в прикрепленном свойстве. Присоединенное свойство срабатывает только при изменении значения. При переключении присоединенного свойства для его запуска используйте
task.Yield()
перевести вызов в конец очереди диспетчера:await Task.Yield(); // Put ourselves to the back of the dispatcher queue. PopWindowToForegroundNow = false; await Task.Yield(); // Put ourselves to the back of the dispatcher queue. PopWindowToForegroundNow = false;
Код выше действительно полезен в прикрепленном свойстве. При переключении присоединенного свойства для его запуска вы можете использовать диспетчер и установить приоритет
Loaded
:// Ensure PopWindowToForegroundNow is initialized to true // (attached properties only trigger when the value changes). Application.Current.Dispatcher.Invoke( async () => { if (PopWindowToForegroundNow == false) { // Already visible! } else { await Task.Yield(); // Put ourselves to the back of the dispatcher queue. PopWindowToForegroundNow = false; } }, DispatcherPriority.Loaded);
Попробуйте добавить свой richTextBox.ScrollToEnd(); вызовите обработчик событий LayoutUpdated вашего объекта RichTextBox.
Вы должны иметь возможность использовать событие Loaded
если вы делаете это более одного раза, то вы должны посмотреть на событие LayoutUpdated
myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();
Единственное (kludge) решение, которое работало для моего проекта WPF, - это запустить отдельный поток, который какое-то время спал, а затем просил прокрутить до конца.
Важно не пытаться вызвать этот "сонный" поток в основном графическом интерфейсе, чтобы пользователь не был приостановлен. Поэтому вызывайте отдельный "сонный" поток и периодически отправляйте Dispatcher.Invoke на основной поток GUI и просите прокрутить до конца.
Работает отлично и пользовательский опыт не страшен:
using System;
using System.Threading;
using System.Windows.Controls;
try {
richTextBox.ScrollToEnd();
Thread thread = new Thread(new ThreadStart(ScrollToEndThread));
thread.IsBackground = true;
thread.Start();
} catch (Exception e) {
Logger.Log(e.ToString());
}
а также
private void ScrollToEndThread() {
// Using this thread to invoke scroll to bottoms on rtb
// rtb was loading for longer than 1 second sometimes, so need to
// wait a little, then scroll to end
// There was no discernable function, ContextIdle, etc. that would wait
// for all the text to load then scroll to bottom
// Without this, on target machine, it would scroll to the bottom but the
// text was still loading, resulting in it scrolling only part of the way
// on really long text.
// Src: https://stackru.com/questions/6614718/wait-until-control-layout-is-finished
for (int i=1000; i <= 10000; i += 3000) {
System.Threading.Thread.Sleep(i);
this.richTextBox.Dispatcher.Invoke(
new ScrollToEndDelegate(ScrollToEnd),
System.Windows.Threading.DispatcherPriority.ContextIdle,
new object[] { }
);
}
}
private delegate void ScrollToEndDelegate();
private void ScrollToEnd() {
richTextBox.ScrollToEnd();
}
Попробуй это:
richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary