为什么我的装饰器在应用它的元素改变时没有重新渲染?

20
在我正在构建的用户界面中,每当面板中的控件获得焦点时,我希望装饰该面板。因此,我处理了IsKeyboardFocusWithinChanged事件,并在元素获得焦点时添加装饰器,并在失去焦点时移除装饰器。这似乎运行良好。
我的问题是,如果装饰元素的边界发生更改,则装饰器不会重新渲染。例如,在这个简单的情况下:
<WrapPanel Orientation="Horizontal"
           IsKeyboardFocusChanged="Panel_IsKeyboardFocusChanged">
   <Label>Caption</Label>
   <TextBox>Data</TextBox>
</WrapPanel>

TextBox获得焦点时,装饰器可以正确地装饰WrapPanel的边界,但是当我输入文本时,TextBox会在装饰器边缘下方扩展。当然,只要我做任何强制装饰器重新渲染的操作,例如ALT-TAB退出应用程序或给另一个面板焦点,它就会自动纠正。但是当被装饰元素的边界发生变化时,如何让它重新呈现?

2个回答

65
WPF内置了一种机制,可以在相应的AdornedElement更改大小、位置或变换时导致所有Adorners重新测量、重新排列和重新渲染。编写adorner时需要遵循某些规则,其中并非所有规则都有清晰的文档说明。
首先回答您标题中的问题:为什么adorner不会一直重新渲染,然后解释修复它的最佳方法。
每当AdornerLayer收到LayoutChanged通知时,它会扫描其每个adorner以查看AdornedElement是否已更改大小、位置或变换。如果是,则设置标志强制Adorner重新测量、排列和渲染,大致相当于InvalidateMeasure(); InvaliateArrange(); InvalidateVisual()。
通常情况下,控件首先被测量,然后被排列,最后被渲染。事实上,WPF试图使这成为最常见的情况,因为这是最有效的顺序。但是,在许多情况下,控件可能会在重新测量之前被重新排列和/或重新渲染。在WPF中,这是合法的事件顺序(允许灵活的布局技术),但它并不常见,因此经常没有被测试。
正确实现的Adorner或其他UIElement应该在任何可能影响渲染的情况下小心调用InvalidateVisual(),除非只有AffectsRender依赖属性被更改。
在您的情况下,您adorner的大小显然会影响渲染。大小属性不是AffectsRender依赖属性,因此在更改它们时需要手动调用InvalidateVisual()。如果没有这样做,WPF可能永远不知道要重新渲染您的adorner。
在您的情况下,可能发生以下情况:
1. 布局完成并触发LayoutChanged事件。 2. AdornerLayer发现了AdornedElement的大小更改。 3. AdornerLayer将adorner排程为重新测量、重新排列和重新渲染。 4. 一些内容导致Arrange()被调用,在重新测量之前导致重新排列和重新渲染。这使WPF认为adorner不再需要重新排列或重新渲染。 5. 布局引擎检测到adorner需要进行测量并调用Measure。 6. adorner的MeasureOverride重新计算所需大小,但对WPF未告知adorner需要重新渲染。 7. 布局引擎决定没有更多工作要做,因此adorner永远不会重新渲染。
解决方案当然是修复adorner中的错误,通过在控件重新测量时调用InvalidateVisual()来实现,如下所示:
protected override Size MeasureOverride(Size constraint)
{
  var result = base.MeasureOverride(constraint);
  // ... add custom measure code here if desired ...
  InvalidateVisual();
  return result;
}

这样做将使您的Adorner始终遵守WPF的所有规则,因此它将在所有情况下按预期工作。这也是最有效的解决方案,因为InvalidateVisual()只在真正需要时才会起作用。


2
多奇怪啊,尺寸属性不影响渲染——以前从未遇到过这种情况,但似乎是一个非常棘手的问题,我很惊讶它不经常出现!你知道(或者可以猜测)为什么WPF设计师会这样构建它吗?无论如何,感谢您提供如此清晰、详细和有用的答案。 - itowlson
@itowlson:你可能以前从未遇到过这种情况,因为在最常见的情况下——即仅在ArrangeCore中更新RenderSize且渲染不依赖于任何其他大小的情况下——重新渲染会始终被触发,而无需调用InvalidateVisual()。记录一系列的ArrangeOverride和OnRender调用,以更好地了解其工作原理。我猜设计师们不想在每次设置RenderSize时自动安排一个OnRender,因为他们设想了一些场景,其中RenderSize将被更改,然后立即更改回来。 - Ray Burns
不渲染的另一个原因可能是您必须传递给Adorner基构造函数的UIElement不在装饰层内。例如,如果您使用AdornerDecorator创建本地装饰层,然后错误地引用了超出该视觉/逻辑树的UIElement - Dennis
我正在装饰的控件在网格中,Grid.Row="1"。 当我将AdornerDecorator放在它周围时,我认为我应该将Grid.Row="1"移到AdornerDecorator上面。然后我发现我的装饰没有更新,上述建议也不起作用。一旦我将Grid.Row="1"移回到我的控件上,它就正常工作了,至少在我的情况下,不需要上述建议。希望这能帮助某些人。 - Buzz

1
你需要在面板上调用分派程序。将处理程序添加到TextBox的SizeChanged事件中:
    private void myTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        panel.Dispatcher.Invoke((Action)(() => 
        {
            if (panel.IsKeyboardFocusWithin)
            {
                // remove and add adorner to reset
                myAdornerLayer.Remove(myAdorner);
                myAdornerLayer.Add(myAdorner);
            }
        }), DispatcherPriority.Render, null);
    }

这基本上来自于这篇文章:链接

6
这有点像用大锤打苍蝇:虽然可以解决问题,但方法笨拙低效,并没有解决问题的根本原因——你女儿把纱门开着。 :-) 这种方法之所以奏效,是因为从图层中删除装饰器会导致它暂时失去呈现源,从而触发“需要渲染”的标志。请查看我的答案,了解更多细节和更简洁的修复方式。 - Ray Burns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接