等待控件布局完成

发布于 2024-11-18 20:33:47 字数 1362 浏览 6 评论 0原文

我正在将大量富文本加载到 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< /code> 再次失败。

编辑2: 按照 dowhilefor 的回答:MSDN on InvalidateArrange 说

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

但甚至

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

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

I am loading quite a lot of rich text into a RichTextBox (WPF) and I want to scroll to the end of content:

richTextBox.Document.Blocks.Add(...)
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();

This doesn't work, ScrollToEnd is executed when the layout is not finished, so it doesn't scroll to the end, it scrolls to around the first third of the text.

Is there a way to force a wait until the RichTextBox has finished its painting and layout operations so that ScrollToEnd actually scrolls to the end of the text?

Thanks.

Stuff that doesn't work:

EDIT:
I have tried the LayoutUpdated event but it's fired immediately, same problem: the control is still laying out more text inside the richtextbox when it's fired so even a ScrollToEnd there doesn't work...
I tried this:

richTextBox.Document.Blocks.Add(...)
richTextBoxLayoutChanged = true;
richTextBox.UpdateLayout();
richTextBox.ScrollToEnd();

and inside the richTextBox.LayoutUpdated event handler:

if (richTextBoxLayoutChanged)
{
    richTextBoxLayoutChanged = false;
    richTextBox.ScrollToEnd();
}

The event is fired correctly but too soon, the richtextbox is still adding more text when it's fired, layout is not finished so ScrollToEnd fails again.

EDIT 2:
Following on dowhilefor's answer: MSDN on InvalidateArrange says

After the invalidation, the element will have its layout updated, which will
occur asynchronously unless subsequently forced by UpdateLayout.

Yet even

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

does NOT wait: after these calls the richtextbox is still adding more text and laying it out inside itself asynchronously. ARG!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

左岸枫 2024-11-25 20:33:47

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

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

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

关键是 DispatcherPriority.ContextIdle,它会等待后台任务完成。

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

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

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

I have had a related situation: I have a print preview dialog that creates a fancy rendering. Normally, the user will click a button to actually print it, but I also wanted to use it to save an image without user involvement. In this case, creating the image has to wait until the layout is complete.

I managed that using the following:

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

The key is the DispatcherPriority.ContextIdle, which waits until background tasks have completed.

Edit: As per Zach's request, including the code applicable for this specific case:

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

I should note that I'm not really happy with this solution, as it feels incredibly fragile. However, it does seem to work in my specific case.

不必你懂 2024-11-25 20:33:47

特别看看 UpdateLayout

如果满足以下条件,调用此方法没有效果:
布局没有改变,或者如果两者都没有改变
的排列或测量状态
布局无效

因此根据您的需要调用 InvalidateMeasure 或 InvalidateArrange 应该可行。

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

Have a look at UpdateLayout

especially:

Calling this method has no effect if
layout is unchanged, or if neither
arrangement nor measurement state of a
layout is invalid

So calling InvalidateMeasure or InvalidateArrange, depending on your needs should work.

But considering your piece of code. I think that won't work. Alot of WPF loading and creating is deffered, so adding something to Document.Blocks does not necesarilly change the UI directly. But i must say, this is just a guess and maybe i'm wrong.

2024-11-25 20:33:47

您应该能够使用 Loaded 事件

如果您多次执行此操作,那么您应该查看 LayoutUpdated 事件

myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();

you should be able to use the Loaded event

if you are doing this more then one time, then you should look at the LayoutUpdated event

myRichTextBox.LayoutUpdated += (source,args)=> ((RichTextBox)source).ScrollToEnd();
就像说晚安 2024-11-25 20:33:47

使用 .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;
    }

With .net 4.5 or the async blc package you can use the following extension method

 /// <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;
    }
卸妝后依然美 2024-11-25 20:33:47

@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(); // 将我们自己置于调度程序队列的末尾。
    PopWindowToForegroundNow = false;
    等待任务.Yield(); // 将我们自己置于调度程序队列的末尾。
    PopWindowToForegroundNow = false;
    
  • 上面的代码在附加属性中非常有用。当切换附加属性来触发它时,您可以使用调度程序,并将优先级设置为Loaded

    // 确保 PopWindowToForegroundNow 初始化为 true
    //(附加属性仅在值更改时触发)。
    Application.Current.Dispatcher.Invoke(
    异步 
       ()=>
    {
        if (PopWindowToForegroundNow == false)
        {
           // 已经可见!
        }
        别的
        {
            等待任务.Yield(); // 将我们自己置于调度程序队列的末尾。
            PopWindowToForegroundNow = false;
        }
    }, DispatcherPriority.Loaded);
    

The answer by @Andreas works well.

However, what if the control is already loaded? The event would never fire, and the wait would potentially hang forever. To fix this, return immediately if the form is already loaded:

/// <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;
}

Additional hints

These hints may or may not be useful.

  • The code above is really useful in an attached property. An attached property only triggers if the value changes. When toggling the attached property to trigger it, use task.Yield() to put the call to the back of the dispatcher queue:

    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;
    
  • The code above is really useful in an attached property. When toggling the attached property to trigger it, you can use the dispatcher, and set the priority to 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);
    
最美不过初阳 2024-11-25 20:33:47

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

Try adding your richTextBox.ScrollToEnd(); call to the LayoutUpdated event handler of your RichTextBox object.

做个ˇ局外人 2024-11-25 20:33:47

试试这个:

richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary

Try this:

richTextBox.CaretPosition = richTextBox.Document.ContentEnd;
richTextBox.ScrollToEnd(); // maybe not necessary
愚人国度 2024-11-25 20:33:47

对我的 WPF 项目有效的唯一(kludge)解决方案是启动一个单独的线程,该线程休眠一段时间,然后要求滚动到末尾。

重要的是不要尝试在主 GUI 上调用这个“休眠”线程,以免用户被暂停。因此,调用一个单独的“休眠”线程并定期在主 GUI 线程上调用 Dispatcher.Invoke 并要求滚动到末尾。

运行完美,用户体验也不错:

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();
}

The only (kludge) solution that worked for my WPF project was to fire off a separate thread that slept for a time and then asked to scroll to the end.

It is important to not try to invoke this "sleepy" thread on the main GUI, lest the user would be paused. Therefore, invoke a separate "sleepy" thread and periodically Dispatcher.Invoke on the main GUI thread and ask to scroll to the end.

Works perfectly and the user experience is not terrible:

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());
}

and

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();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文