如何手动告诉所有者绘制的 WPF 控件刷新/重绘而不执行测量或安排通道?

Posted

技术标签:

【中文标题】如何手动告诉所有者绘制的 WPF 控件刷新/重绘而不执行测量或安排通道?【英文标题】:How can I manually tell an owner-drawn WPF Control to refresh/redraw without executing measure or arrange passes? 【发布时间】:2011-12-09 17:49:36 【问题描述】:

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

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

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


更新:

好吧,看起来没有任何这样的调用,因为即使 AffectsRender 标志仍然会在内部导致 Arrange 传递(根据下面 CodeNaked 的回答),但我发布了第二个答案,显示了内置行为以及一种解决方法来禁止您的布局密码以简单的可为空大小作为标志运行。见下文。

【问题讨论】:

【参考方案1】:

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

FrameworkPropertyMetadata.AffectsRender 选项只是告诉 WPF 在关联属性更改时调用 InvalidateVisual

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

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

【讨论】:

我不确定这是否 100% 正确。看看 MSDN 中关于AffectsRender 标志的第一句话...msdn.microsoft.com/en-us/library/… 具体说“...以某种方式影响总体布局,不会特别影响排列或测量,但需要重绘。所以机制是没有安排的。做上面提到的faux-DP会比改变视觉树更好。(我会看看挖掘Reflector是否会发现任何东西。) @MarqueIV - 实际上,我在调查此问题时使用了 Reflector。我几乎从不依赖文档。您可以看到OnRender 是从Arrange 调用的,FrameworkElement.OnPropertyChanged 最终会检查AffectsRender 标志并调用InvalidateVisual(如果设置)。 Touche' 到文档注释。我不止一次被这种情况烧伤,我应该知道得更好! :) 也就是说,是时候测试应用了!我将创建一个设置了 AffectsRender 位和子类 MeasureOverride 和 ArrangeOverride 的 DP,并查看当我更改它时它们是否被调用。如果你是对的并且确实如此,那么我的下一步将是设置一个状态标志并缓存来自重载的最后调用的返回值,设置标志并调用 InvalidateVisual。在覆盖中,如果设置了标志,我只需返回缓存的值。如果没有,我让他们做他们的事。 @MarqueIV - 它们可能不会被调用,但使用 AffectsRender 与调用 InvalidateVisual 相同。里面有很多代码可以防止不必要的安排/测量。 我开始怀疑我是否应该创建一个 WPF 博客,而不是像下面那样用冗长冗长的答案乱扔垃圾。 ...或者你认为这样的事情在这里有帮助吗? (我问是因为你是 14K 而我是低 1K,所以你比我有更多的经验。想法?)【参考方案2】:

好的,我回答这个问题是为了向人们展示为什么 CodeNaked 的答案是正确的,但如果您愿意,请加上星号,并提供一种解决方法。但在良好的 SO-citizenship 中,我仍然将他标记为已回答,因为他的回答将我带到了这里。

更新:我已将接受的答案移至此处,原因有两个。一,我想让人们知道解决这个问题(大多数人只阅读接受的答案并继续前进)二,考虑到他的代表是 25K,我不认为他' d介意我把它拿回来! :)

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

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


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

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

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

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

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

ArrangeOverride called for MainTestPanel.
OnRender called for MainTestPanel.

注意没有调用任何测量覆盖,只调用了外部控件的 ArrangeOverride。

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

但是,如果您知道没有一个子控件具有设置了 AffectsParentArrange 位的属性(同样,我们这样做了),您可以更好地使用 Nullable Size 作为标志来抑制 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);
    


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

希望这会有所帮助!

【讨论】:

大卫,请不要说“这不是正确的方法......”之类的话,尤其是不要说别人的方法有什么问题。例如,虽然与您提出的答案不同,但这是一种完全有效的方式,并且遵循所有 WPF 规则。此外,添加您的答案足以说明您的观点,而无需将 cmets 添加到所有其他答案中,再次说明它们是错误的而不指出原因。只需说“这是一种我觉得更好的方法”,然后解释原因。 此方法不会阻止InvalidateVisual() 触发 other 控件上的布局,并在 other 控件上重新调用 OnRender() 以重新创建视觉图。如果您针对更新DrawingGroup 对该方法进行性能测试,您会发现它要慢得多。尽管在 WPF 上进行性能测试是一项棘手的工作,因为 OnRender 实际上并不绘制。 另外,来自InvalidateVisual() 上的 MSDN 文档:“通常不会从您的应用程序代码中调用此方法...仅在高级方案中才需要调用此方法。一种这样的高级场景是,如果您要为不在 Freezable 或 FrameworkElement 派生类上的依赖属性创建 PropertyChangedCallback,该类在更改时仍会影响布局。" 正如我最初的问题中所述,我们正在绘制控件的内容,因此您对其他控件的评论不适用,因为它们不存在于此处。这仍然是知识共享的好信息。但回到我最初的评论,即使在这里,您也将这些信息放在 cmets 中。相反,您应该在 your 答案中说出这些信息,以便人们可以找到它。他们通常不会阅读评论链,而是会浏览答案。将您的答案格式化为“这是另一种方法,可以通过其他一些答案解决以下问题。”【参考方案3】:

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

在不改变控件大小的情况下有效地更新控件的视觉效果。使用DrawingGroup。您创建 DrawingGroup 并在 OnRender() 期间将其放入 DrawingContext,然后您可以随时通过 Open() DrawingGroup 更改其可视化绘图命令,WPF 将自动高效重新渲染 UI 的那部分。 (如果您希望使用可以进行增量更改而不是每次都重绘的位图,也可以将此技术与RenderTargetBitmap 一起使用)

这就是它的样子:

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

【讨论】:

一个有趣的方法,我赞成,虽然我不同意你的笼统陈述“你不应该调用 InvalidateVisual() 除非你的控件的大小发生变化”。这取决于控件排列逻辑的复杂性、控件是否有任何嵌套的子级、InvalidateVisual 被调用的频率等等。在许多情况下,这种技术可能过于矫枉过正。我假设 MS 在 WPF 中没有包含标准方法来“重绘”控件而不强制排列传递,这意味着性能影响不是那么大的问题。 @StevenRands 来自 InvalidateVisual() 上的 MSDN 文档:“此方法通常不会从您的应用程序代码中调用...仅在高级场景中才需要调用此方法。这样一种高级场景是,如果您正在为不在 Freezable 或 FrameworkElement 派生类上的依赖属性创建 PropertyChangedCallback,当它发生变化时仍会影响布局。" @StevenRands 当您的控件大小保持不变时,InvalidateVisual() 的理由为零。布局在 WPF 中是一个非常昂贵的过程,所以要不惜一切代价避免它。如果只有内容发生变化,则应使用 AffectsRender 的依赖属性或通过 DrawingGroup 触发重新渲染,如上所述。 DP 上的AffectsRender 选项最终调用InvalidateVisual,according to this。这只是一种方便的机制,可以避免在 DP 的值发生变化时自己调用该方法。此外,布局传递的成本在很大程度上取决于控件是否有子项,这些子项是否有子项,等等:对于没有子项的自定义 FrameworkElement 派生类,布局调用的成本将是最小的. 问题...如果您只是从无参数的 Render() 方法中调用它,那么单独调用 Render(DrawingContext) 的目的是什么?为什么不把你的绘图电话放在那里?如果它是在多个上下文之间共享渲染代码,这将是有意义的,但此处显示的情况并非如此。【参考方案4】:

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

简而言之,您调用优先级为 DispatcherPriority.Render 的某个虚拟委托,这将导致任何具有该优先级或更高优先级的东西也被调用,从而导致重新渲染。

【讨论】:

刚刚看过,但我不确定这是否符合他的想法。我刚刚调用了“MainTestPanel.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate)”,但我的 OnRender 没有被调用。想法? (这看起来很有希望,但缺乏回应有点否决了我的热情。另外,为了确保调用了代表,我实际上在其中添加了一个 Console.WriteLine 输出所以我知道这不是问题。)跨度> 是的,重读后我认为您是对的。它只是强制已经排队的委托。对不起! 刚刚又看了一遍。这行不通。这实际上并没有强制渲染的原因。它强制以渲染优先级排队的事物执行,但如果有意义的话,它不会强制控制到渲染队列。换句话说,这表示“嘿……您现在可以渲染,无需等待!”但控制说“谢谢,但我不必!”然而,在他的示例中,它确实需要渲染,因为他设置了标签的内容,该标签确实标记了要渲染的控件。很酷的技巧,但解决了一个不同的问题。 你打败了我! :) 我仍在投票给你,因为来这里寻求答案的几个人可能正在寻找那个解决方案,这很酷。 这两者都不起作用,并且不是在不重新布局的情况下更新控件的正确方法。看我的回答。

以上是关于如何手动告诉所有者绘制的 WPF 控件刷新/重绘而不执行测量或安排通道?的主要内容,如果未能解决你的问题,请参考以下文章

鼠标移动上的CSS js重绘而不消失

如何强制秘银重绘而不进行差异化? (在秘银中整合德拉古拉)

wpf如何判断在其它操作时,重绘使用禁止

WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本

五种情况下会刷新控件状态(刷新所有子FWinControls的显示)——从DFM读取数据时新增加子控件时重新创建当前控件的句柄时设置父控件时显示状态被改变时

C#winform怎么可以让鼠标移动到控件时显示