等到控制布局完成

时间:2022-08-23 18:04:32

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

我正在将很多富文本加载到RichTextBox(WPF)中,我想滚动到内容的结尾:

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.

这不起作用,ScrollToEnd在布局未完成时执行,因此它不会滚动到结尾,它会滚动到文本的前三分之一左右。

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?

是否有办法强制等待RichTextBox完成其绘制和布局操作,以便ScrollToEnd实际滚动到文本的末尾?

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:

编辑:我已经尝试了LayoutUpdated事件,但它立即被触发,同样的问题:当它被触发时,控件仍然在richtextbox中布置更多文本,所以即使ScrollToEnd也没有用...我试过这个:

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

and inside the richTextBox.LayoutUpdated event handler:

并在richTextBox.LayoutUpdated事件处理程序内:

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.

事件被正确触发但很快,richtextbox在触发时仍然添加更多文本,布局未完成,因此ScrollToEnd再次失败。

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

编辑2:关注dowhilefor的回答:InvalidateArrange上的MSDN说

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

失效后,元素将更新其布局,这将异步发生,除非随后由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!

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

7 个解决方案

#1


2  

Have a look at UpdateLayout

看看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.

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

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.

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

#2


8  

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.

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

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

编辑:根据Zach的请求,包括适用于此特定案例的代码:

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.

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

#3


1  

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

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

#4


1  

you should be able to use the Loaded event

你应该能够使用Loaded事件

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

如果您这样做的时间超过一次,那么您应该查看LayoutUpdated事件

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

#5


1  

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

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

#6


0  

Try this:

尝试这个:

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

#7


0  

The answer by @Andreas works well.

@Andreas的答案很有效。

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:

    上面的代码在附加属性中非常有用。附加属性仅在值更改时触发。切换附加属性以触发它时,使用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;
    
  • 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);
    

#1


2  

Have a look at UpdateLayout

看看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.

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

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.

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

#2


8  

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.

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

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

编辑:根据Zach的请求,包括适用于此特定案例的代码:

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.

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

#3


1  

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

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

#4


1  

you should be able to use the Loaded event

你应该能够使用Loaded事件

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

如果您这样做的时间超过一次,那么您应该查看LayoutUpdated事件

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

#5


1  

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

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

#6


0  

Try this:

尝试这个:

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

#7


0  

The answer by @Andreas works well.

@Andreas的答案很有效。

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:

    上面的代码在附加属性中非常有用。附加属性仅在值更改时触发。切换附加属性以触发它时,使用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;
    
  • 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);