Подождите, пока макет элемента управления не будет завершен

Я загружаю довольно много форматированного текста в 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 снова терпит неудачу.

EDIT 2: после ответа dowhilefor: MSDN на InvalidateArrange говорит

После аннулирования макет элемента будет обновлен, что будет происходить асинхронно, если впоследствии не будет принудительно выполнено с помощью UpdateLayout.

Но даже

richTextBox.InvalidateArrange();
richTextBox.InvalidateMeasure();
richTextBox.UpdateLayout();

НЕ ждет: после этих вызовов richtextbox все еще добавляет больше текста и размещает его внутри себя асинхронно. АРГ!


person SemMike    schedule 07.07.2011    source источник
comment
Пожалуйста, перестаньте писать теги в заголовках.   -  person Lightness Races in Orbit    schedule 17.07.2011


Ответы (8)


Взгляните на UpdateLayout.

особенно:

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

Поэтому вызов InvalidateMeasure или InvalidateArrange, в зависимости от ваших потребностей, должен работать.

Но учитывая ваш кусок кода. Я думаю, это не сработает. Большая часть загрузки и создания WPF откладывается, поэтому добавление чего-либо в Document.Blocks не обязательно напрямую изменяет пользовательский интерфейс. Но я должен сказать, что это всего лишь предположение, и, возможно, я ошибаюсь.

person dowhilefor    schedule 07.07.2011
comment
Спасибо, это не работает, но попробовать стоило. Даже вызов как InvalidateArrange, так и InvalidateMeasure перед UpdateLayout не решает проблему, RichTextBox по-прежнему добавляет/размещает текст асинхронно после всех этих вызовов. Это не ждет. - person SemMike; 07.07.2011

У меня была похожая ситуация: у меня есть диалоговое окно предварительного просмотра, которое создает причудливый рендеринг. Обычно пользователь нажимает кнопку, чтобы распечатать его, но я также хотел использовать его для сохранения изображения без участия пользователя. В этом случае создание образа приходится ждать завершения компоновки.

Мне удалось это, используя следующее:

Dispatcher.Invoke(new Action(() => {SaveDocumentAsImage(....);}), DispatcherPriority.ContextIdle);

Ключ — это DispatcherPriority.ContextIdle, который ожидает завершения фоновых задач.

Изменить: по запросу Зака, включая код, применимый для этого конкретного случая:

Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle);

Я должен отметить, что я не очень доволен этим решением, так как оно кажется невероятно хрупким. Однако, похоже, это работает в моем конкретном случае.

person Patrick Simpson    schedule 09.02.2015
comment
Патрик, это отличное решение. Я нашел, что эта работа работает лучше всего среди всех решений, приведенных здесь. Dispatcher.Invoke(() => { richTextBox.ScrollToEnd(); }), DispatcherPriority.ContextIdle); Не могли бы вы обновить свой ответ, включив в него этот код, чтобы его было лучше видно? - person Zach; 22.04.2015
comment
Спасибо за этот ответ! Это также помогло мне в случае, когда я получал PropertyChanged событий пользовательского элемента управления, происходящих из пользовательских свойств зависимостей, которые вызывались после свойств зависимостей стандартных элементов управления. Не уверен, что я где-то ошибся при их реализации или это связано с привязками. В любом случае, Dispatcher.Invoke без указания приоритета всегда будет выполняться ДО этих PropertyChanged событий моих пользовательских свойств зависимостей. Используя DispatcherPriority.ContextIdle, делегат диспетчера внезапно выполняется ПОСЛЕ них. - person j00hi; 09.12.2017
comment
Использование Dispatcher.Invoke с DispatcherPriority.ContextIdle вызывает визуальную задержку после загрузки элементов управления перед применением дополнительного действия. Вместо этого использование await Task.Yield() не вызывает такой визуальной задержки. - person Etienne Charland; 12.07.2018

вы должны иметь возможность использовать событие Loaded

если вы делаете это более одного раза, вам следует посмотреть LayoutUpdated

myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();
person Muad'Dib    schedule 07.07.2011
comment
то же замечание: спасибо, но я генерирую этот длинный текст много раз во время выполнения, а не только при загрузке RichTextBox (текст генерируется из запросов к базе данных)... - person SemMike; 07.07.2011
comment
@SemMike аааа, ты пропустил ЭТО немного информации :) - person Muad'Dib; 07.07.2011
comment
спасибо, но я уже пробовал событие UpdateLayout (следуя загруженным предложениям), см. мое первое редактирование... - person SemMike; 07.07.2011

С .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;
    }
person Andreas    schedule 20.04.2016
comment
Это работает хорошо, однако может зависнуть (теоретически). Я опубликовал улучшенную версию этого кода. - person Contango; 16.05.2016

Ответ @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);
    
person Contango    schedule 16.05.2016

Попробуйте добавить свой richTextBox.ScrollToEnd(); вызов обработчика событий LayoutUpdated вашего объекта RichTextBox.

person Philipp Schmid    schedule 07.07.2011
comment
Это вводит в заблуждение. И я думаю, что многие все еще думают, что это событие вызывается только тогда, когда изменяется макет элемента управления, где был добавлен слушатель. Но это не так. LayoutUpdated вызывается, когда ЛЮБОЙ элемент меняет свой макет. Посмотрите здесь - person dowhilefor; 07.07.2011

Попробуй это:

richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary
person Pollitzer    schedule 12.01.2015

Единственным (запутанным) решением, которое сработало для моего проекта 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://stackoverflow.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();
}
person Mike    schedule 15.02.2019