如何在不执行测量或安排传递的情况下手动告诉所有者绘制的 WPF 控件刷新/重绘?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/7801680/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 23:08:21  来源:igfitidea点击:

How can I manually tell an owner-drawn WPF Control to refresh/redraw without executing measure or arrange passes?

wpfcustom-controlsredrawinvalidationonrender

提问by Mark A. Donohoe

We are doing custom drawing in a control subclass's OnRender. This drawing code is based on an external trigger and data. As such, whenever the trigger fires, we need to re-render the control based on that data. What we're trying to do is find out how to force the control to re-render but without going through an entire layout pass.

我们正在控件子类的OnRender. 此绘图代码基于外部触发器和数据。因此,每当触发器触发时,我们都需要根据该数据重新呈现控件。我们试图做的是找出如何强制控件重新渲染,但不经过整个布局过程。

As stated above, most answers I've seen revolve around invalidating the Visualwhich invalidates the layout which forces new measure and arrange passes which is very expensive, especially for very complex visual trees as ours is. But again, the layout does notchange, nor does the VisualTree. The only thing that does is the external data which gets rendered differently. As such, this is strictly a pure rendering issue.

如上所述,我见过的大多数答案都围绕着无效,Visual这会使布局无效,这会强制执行新的度量并安排非常昂贵的传递,尤其是对于像我们这样非常复杂的视觉树。但同样,布局没有改变,VisualTree也没有改变。唯一能做的就是以不同方式呈现的外部数据。因此,这严格来说是一个纯粹的渲染问题。

Again, we're just looking for a simple way to tell the control that it needs to re-execute OnRender. I have seen one 'hack' in which you create a new DependencyPropertyand register it with 'AffectsRender' which you just set to some value when you want to refresh the control, but I'm more interested in what's going on inside the default implementation for those properties: what they call to affect that behavior.

同样,我们只是在寻找一种简单的方法来告诉控件它需要重新执行OnRender。我见过一个“hack”,你创建一个新的DependencyProperty并用“AffectsRender”注册它,当你想刷新控件时,你只需将它设置为某个值,但我对默认实现中发生的事情更感兴趣这些属性:他们调用什么来影响该行为。



Update:

更新:

Well, it looks like there isn't any such call as even the AffectsRenderflag still causes an Arrange pass internally (as per CodeNaked's answer below) but I've posted a second answer that shows the built-in behaviors as well as a work-around to suppress your layout pass code from running with a simple nullable size as a flag. See below.

好吧,看起来没有任何这样的调用,因为即使AffectsRender标志仍然会在内部引起排列传递(根据下面 CodeNaked 的回答),但我已经发布了第二个答案,显示了内置行为以及工作-周围以抑制您的布局传递代码以简单的可为空大小作为标志运行。见下文。

采纳答案by Mark A. Donohoe

Ok, I'm answering this to show people why CodeNaked's answer is correct, but with an asterisk if you will, and also to provide a work-around. But in good SO-citizenship, I'm still marking his as answered since his answer led me here.

好的,我回答这个是为了向人们展示为什么 CodeNaked 的答案是正确的,但如果您愿意,请使用星号,并提供解决方法。但是在良好的 SO 公民身份中,我仍然将他标记为已回答,因为他的回答将我带到了这里。

Update: I've since moved the accepted answer to here for two reasons. One, I want people to know there isa solution to this (most people only read the accepted answer and move on) and two, considering he has a rep of 25K, I don't think he'd mind if I took it back! :)

更新:出于两个原因,我已将接受的答案移至此处。第一,我想让人们知道有到了一个解决方案(大多数人只读接受的答案,继续前进)和二,考虑到他有25K的代表,我不认为如果我带回去,他会介意!:)

Here's what I did. To test this, I created this subclass...

这就是我所做的。为了测试这一点,我创建了这个子类......

public class TestPanel : DockPanel
{
    protected override Size MeasureOverride(Size constraint)
    {
        System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
        return base.MeasureOverride(constraint);
    }

    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
        return base.ArrangeOverride(arrangeSize);
    }

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        System.Console.WriteLine("OnRender called for " + this.Name + ".");
        base.OnRender(dc);
    }

}

...which I laid out like this (note that they are nested):

...我是这样布置的(注意它们是嵌套的):

<l:TestPanel x:Name="MainTestPanel" Background="Yellow">

    <Button Content="Test" Click="Button_Click" DockPanel.Dock="Top" HorizontalAlignment="Left" />

    <l:TestPanel x:Name="InnerPanel" Background="Red" Margin="16" />

</l:TestPanel>

When I resized the window, I got this...

当我调整窗口大小时,我得到了这个......

MeasureOverride called for MainTestPanel.
MeasureOverride called for InnerPanel.
ArrangeOverride called for MainTestPanel.
ArrangeOverride called for InnerPanel.
OnRender called for InnerPanel.
OnRender called for MainTestPanel.

but when I called InvalidateVisualon 'MainTestPanel' (in the button's 'Click' event), I got this instead...

但是当我调用InvalidateVisual'MainTestPanel'(在按钮的'Click'事件中)时,我得到了这个......

ArrangeOverride called for MainTestPanel.
OnRender called for MainTestPanel.

Note how none of the measuring overrides were called, and only the ArrangeOverride for the outer control was called.

请注意没有任何测量覆盖被调用,并且只调用了外部控件的排列覆盖。

It's not perfect as if you have a very heavy calculation inside ArrangeOverridein your subclass (which unfortunately we do) that still gets (re)executed, but at least the children don't fall to the same fate.

这并不完美,好像您ArrangeOverride的子类中有一个非常繁重的计算(不幸的是我们这样做)仍然会被(重新)执行,但至少孩子们不会落入同样的命运。

However, if you know none of the child controls have a property with the AffectsParentArrange bit set (again, which we do), you can go one better and use a Nullable Sizeas a flag to suppress the ArrangeOverride logic from re-entry except when needed, like so...

但是,如果您知道没有一个子控件具有设置了 AffectsParentArrange 位的属性(同样,我们也是这样做的),您可以做得更好,并使用 NullableSize作为标志来抑制 ArrangeOverride 逻辑重新进入,除非需要时,像这样...

public class TestPanel : DockPanel
{
    Size? arrangeResult;

    protected override Size MeasureOverride(Size constraint)
    {
        arrangeResult = null;
        System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
        return base.MeasureOverride(constraint);
    }

    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        if(!arrangeResult.HasValue)
        {
            System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
            // Do your arrange work here
            arrangeResult = base.ArrangeOverride(arrangeSize);
        }

        return arrangeResult.Value;
    }

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        System.Console.WriteLine("OnRender called for " + this.Name + ".");
        base.OnRender(dc);
    }

}

Now unless something specifically needs to re-execute the arrange logic (as a call to MeasureOverride does) you only get OnRender, and if you want to explicitly force the Arrange logic, simply null out the size, call InvalidateVisual and Bob's your uncle! :)

现在除非有什么特别需要重新执行排列逻辑(就像调用 MeasureOverride 那样),否则你只会得到 OnRender,如果你想显式地强制排列逻辑,只需将大小清空,调用 InvalidateVisual 和 Bob 是你的叔叔!:)

Hope this helps!

希望这可以帮助!

回答by CodeNaked

Unfortunately, you must call InvalidateVisual, which calls InvalidateArrangeinternally. The OnRendermethod is called as part of the arrange phase, so you need to tell WPF to rearrange the control (which InvalidateArrange does) and that it needs to redraw (which InvalidateVisual does).

不幸的是,您必须调用InvalidateVisual,它在内部调用InvalidateArrange。该OnRender方法作为排列阶段的一部分被调用,因此您需要告诉 WPF 重新排列控件(InvalidateArrange 执行的操作)并且它需要重绘(InvalidateVisual 执行的操作)。

The FrameworkPropertyMetadata.AffectsRenderoption simply tells WPF to call InvalidateVisualwhen the associated property changes.

FrameworkPropertyMetadata.AffectsRender选项只是告诉 WPFInvalidateVisual在关联的属性更改时调用。

If you have a control (let's call this MainControl) that overrides OnRender and contains several descendant controls, then calling InvalidateVisual may require the descendant controls to be rearranged, or even remeasured. But I believe WPF has optimizations inplace to prevent descendant controls from being rearranged if their available space is unchanged.

如果您有一个覆盖 OnRender 并包含多个后代控件的控件(我们称之为 MainControl),则调用 InvalidateVisual 可能需要重新排列甚至重新测量后代控件。但我相信 WPF 已经进行了优化,以防止后代控件在可用空间不变的情况下被重新排列。

You may be able to get around this by moving your rendering logic to a separate control (say NestedControl), which would be a visual child of MainControl. The MainControl could add this as a visual child automatically or as part of it's ControlTemplate, but it would need to be the lowest child in the z-order. You could then expose a InvalidateNestedControltype method on MainControl that would call InvalidateVisual on the NestedControl.

您可以通过将渲染逻辑移动到一个单独的控件(比如 NestedControl)来解决这个问题,这将是 MainControl 的可视化子项。MainControl 可以自动将其添加为可视子项或作为其 ControlTemplate 的一部分,但它需要是 z 顺序中最低的子项。然后,您可以InvalidateNestedControl在 MainControl 上公开一个类型方法,该方法将在 NestedControl 上调用 InvalidateVisual。

回答by David Jeske

You shouldn't be calling InvalidateVisual()unless the size of your control changes, and even then there are other ways to cause re-layout.

InvalidateVisual()除非控件的大小发生变化,否则您不应调用,即使如此,仍有其他方法可以导致重新布局。

To efficiently update the visual of a control without changing it's size. Use a DrawingGroup. You create the DrawingGroup and put it into the DrawingContextduring OnRender()and then anytime after that you can Open()the DrawingGroupto change it's visual drawing commands, and WPF will automatically and efficientlyre-render that portion of the UI. (you can also use this technique with RenderTargetBitmapif you'd prefer to have bitmap which you can make incremental changes to, rather than redrawing every time)

有效地更新控件的视觉效果而不改变其大小。使用一个DrawingGroup. 您创建DrawingGroup,并把它放入DrawingContextOnRender(),然后之后的任何时间,你可以Open()DrawingGroup改变它的视觉绘图命令,和WPF将自动高效地重新呈现UI的部分。(RenderTargetBitmap如果您更喜欢可以对位图进行增量更改,而不是每次都重新绘制,则也可以使用此技术)

This is what it looks like:

这是它的样子:

DrawingGroup backingStore = new DrawingGroup();

protected override void OnRender(DrawingContext drawingContext) {      
    base.OnRender(drawingContext);            

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);
}

// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render() {            
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            
}

private void Render(DrawingContext drawingContext) {
    // put your render code here
}

回答by GazTheDestroyer

Here is another hack: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

这是另一个黑客:http: //geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

In short, you call invoke some dummy delegate at priority DispatcherPriority.Render, which will cause anything with that priority or above to be invoked too, causing a rerender.

简而言之,您调用优先级为 DispatcherPriority.Render 的一些虚拟委托,这将导致调用具有该优先级或更高优先级的任何内容,从而导致重新渲染。