2010-03-15 28 views
14

在我正在构建的一个用户界面中,只要面板中的某个控件具有焦点,我就想装饰一个面板。所以我处理IsKeyboardFocusWithinChanged事件,并在获得焦点时将元素添加到元素,并在失去焦点时移除装饰元素。这似乎工作确定。为什么我的装饰元素在应用元素发生变化时不会重新渲染?

我遇到的问题是,如果装饰元素的边界发生变化,装饰器不会重新渲染。例如,在这个简单的例子:

<WrapPanel Orientation="Horizontal" 
      IsKeyboardFocusChanged="Panel_IsKeyboardFocusChanged"> 
    <Label>Caption</Label> 
    <TextBox>Data</TextBox> 
</WrapPanel> 

装饰器正确地装点WrapPanel的边界时TextBox接收焦点,但正如我键入文本时,TextBox扩大装饰器的边缘的下方。当然,只要我做了任何迫使装饰者渲染的东西,比如ALT-TAB不在应用程序中,或者让另一个面板成为焦点,它就会自行纠正。但是当装饰元素的边界改变时,我怎么才能让它重新渲染呢?

回答

54

WPF有每当相应的内置机制,使所有Adorners被重新测量,重新排列,并重新呈现AdornedElement更改大小,位置或变换。这个机制要求你在编写你的装饰者时遵循一定的规则,但并不是所有的文件都应该清楚地记录下来。

我会先回答你的标题问题为什么你的装潢不符合重新渲染,然后说明解决它的最好方法。

为什么装饰器不重新渲染

每当AdornerLayer接收它扫描它的每一个装饰器,看看该AdornedElement的大小,位置改变或变换LayoutChanged通知。如果是这样,它将设置标志来强制Adorner进行测量,排列和再次渲染 - 大致相当于InvalidateMeasure(); InvaliateArrange(); InvalidateVisual();

在这种情况下通常会发生的情况是控制首先被测量,然后进行排列,然后进行渲染。事实上,WPF试图使这是最常见的情况,因为它是最有效的序列。然而,在重新测量之前,很多情况下控制可能会重新排列和/或重新排列。这是WPF中事件的合法顺序(允许灵活的布局技术),但它并不常见,所以通常不会进行测试。

一个正确实施Adorner或其他UIElement会小心调用InvalidateVisual()任何时候渲染可能会受到影响,除非AffectsRender依赖属性发生了变化。

在你的情况下,你的装饰大小显然会影响渲染。大小属性不是AffectsRender依赖项属性,因此在更改时需要手动调用InvalidateVisual()。如果你不这样做,WPF可能永远不知道重新渲染你的装饰者。

什么在你的情况正在发生的事情大概是这样的:

  • 布局完成和LayoutChanged事件触发
  • AdornerLayer发现你的AdornedElement
  • AdornerLayer时间表大小改变你对重新测量装饰器,重新布局和重新渲染
  • 某些原因Arrange()被调用,导致在重新测量之前重新布局和重新渲染。这导致WPF认为装饰者不再需要重新布局或重新渲染。
  • 布局引擎检测到装饰器需要测量,并呼吁Measure
  • 装饰器的MeasureOverride重新计算所需的大小,但确实没什么好说的WPF装饰器需要重新渲染
  • 布局引擎决定有什么更多的工作要做,因此装饰器永远不会重新呈现

你能做些什么来解决它

解决的办法是,当然,通过调用InvalidateVisual()以修复Adorner的bug。只要控制重新测量,像这样:

protected override Size MeasureOverride(Size constraint) 
{ 
    var result = base.MeasureOverride(constraint); 
    // ... add custom measure code here if desired ... 
    InvalidateVisual(); 
    return result; 
} 

这样做将使你的装饰器始终如一地遵守WPF的所有规则,所以它会在所有情况下按预期工作。这也是最有效的解决方案,因为InvalidateVisual()什么都不会做,除非在真正需要的情况下。

+2

尺寸属性​​不会影响渲染有多奇怪 - 以前没有碰到过,但是看起来像是一个讨厌的小问题,我很惊讶它不会更经常出现!你知道(或者你可以推测)为什么WPF设计者以这种方式构建它?无论如何,谢谢你提供了这样一个清晰,详细和翔实的答案。 – itowlson 2010-03-27 01:48:49

+0

@itowlson:因为在最常见的情况下(RenderSize仅在ArrangeCore中更新并且渲染不依赖于任何其他大小),您可能从来没有碰到过它,因为在不需要InvalidateVisual()调用的情况下始终触发渲染。记录一系列ArrangeOverride和OnRender调用,以更好地感受这种工作方式。我的猜测是,设计师不想在每次设置RenderSize时自动调度OnRender,因为他们设想RenderSize会被更改并立即改回的场景。 – 2010-03-29 21:13:28

+0

不渲染的另一个原因可能是您必须传递给Adorner基础构造函数的'UIElement'不在adorner层中。例如如果使用'AdornerDecorator'来创建本地装饰器图层,然后错误地引用超出视觉/逻辑树的'UIElement'。 – Dennis 2012-08-15 10:07:16

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

这主要来自这篇文章:http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

+6

这感觉有点像使用大锤拍打苍蝇:它有效,但是很笨拙,效率低下,并没有解决问题的根本原因,那就是你的女儿离开了屏幕门。 :-)这个原理起作用的原因是,从图层中删除装饰器会导致暂时失去其启用“需要渲染”位的PresentationSource。查看我的答案以获取更多详细信息和更简单的解决方法。 – 2010-03-26 05:06:03

相关问题