10

我正在将大量富文本加载到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 的回答:InvalidateArrange 上的 MSDN 说

失效后,元素将更新其布局,这将异步发生,除非随后被 UpdateLayout 强制。

然而即使

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

不等待:在这些调用之后,richtextbox 仍在添加更多文本并将其异步放置在自身内部。啊!

4

8 回答 8

9

我有一个相关的情况:我有一个创建精美渲染的打印预览对话框。通常,用户会单击一个按钮来实际打印它,但我也想用它来保存图像而无需用户参与。在这种情况下,创建图像必须等到布局完成。

我使用以下方法进行了管理:

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

关键是DispatcherPriority.ContextIdle,它一直等到后台任务完成。

编辑:根据 Zach 的要求,包括适用于这种特定情况的代码:

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

我应该注意到我对这个解决方案并不满意,因为它感觉非常脆弱。但是,它似乎确实适用于我的具体情况。

于 2015-02-09T12:38:17.000 回答
3

看看更新布局

尤其:

如果布局不变,或者布局的排列和测量状态均无效,则调用此方法无效

因此,根据您的需要调用 InvalidateMeasure 或 InvalidateArrange 应该可以工作。

但考虑到你的一段代码。我认为那行不通。许多 WPF 加载和创建被延迟,因此向 Document.Blocks 添加一些内容并不一定会直接更改 UI。但我必须说,这只是一个猜测,也许我错了。

于 2011-07-07T17:52:35.063 回答
1

您应该能够使用Loaded事件

如果您这样做不止一次,那么您应该查看LayoutUpdated事件

myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();
于 2011-07-07T17:39:08.760 回答
1

使用 .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;
    }
于 2016-04-20T09:32:28.260 回答
1

@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);
    
于 2016-05-16T15:45:32.310 回答
0

尝试添加您的richTextBox.ScrollToEnd(); 调用RichTextBox 对象的LayoutUpdated 事件处理程序。

于 2011-07-07T17:53:59.997 回答
0

试试这个:

richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary
于 2015-01-12T08:59:42.930 回答
0

唯一适用于我的 WPF 项目的(kludge)解决方案是启动一个单独的线程,该线程休眠了一段时间,然后要求滚动到最后。

重要的是不要尝试在主 GUI 上调用这个“休眠”线程,以免用户被暂停。因此,调用一个单独的“休眠”线程并定期 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();
}
于 2019-02-15T21:10:21.270 回答