18

在我正在构建的 UI 中,只要面板中的一个控件具有焦点,我就想装饰面板。所以我处理这个IsKeyboardFocusWithinChanged事件,并在元素获得焦点时添加一个装饰器,并在它失去焦点时移除装饰器。这似乎工作正常。

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

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

WrapPanel装饰器在接收焦点时正确地装饰了边界TextBox,但是当我输入文本时,TextBox装饰器的边缘下方会展开。当然,一旦我做了任何迫使装饰器渲染的事情,比如 ALT-TAB 退出应用程序或将焦点放在另一个面板上,它就会自行纠正。但是当装饰元素的边界发生变化时,我怎样才能让它重新渲染呢?

4

2 回答 2

62

WPF 有一个内置机制Adorners,每当相应的AdornedElement大小、位置或变换发生变化时,都会重新测量、重新排列和重新呈现所有内容。这种机制要求您在编码您的装饰器时遵循某些规则,并非所有规则都像应有的那样清楚地记录在案。

我将首先回答您的标题问题,即为什么您的装饰器不会一致地重新渲染,然后解释修复它的最佳方法。

为什么装饰器不重新渲染

每当 AdornerLayer 收到 LayoutChanged 通知时,它会扫描其每个 Adorner 以查看其AdornedElement大小、位置或变换是否发生了变化。如果是这样,它会设置标志以强制Adorner重新测量、排列和渲染 - 大致相当于InvalidateMeasure(); InvaliateArrange(); InvalidateVisual();.

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

除非仅更改了依赖项属性,否则任何时候渲染可能会受到影响,正确实现Adorner或其他UIElement人都会小心调用。InvalidateVisual() AffectsRender

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

您的情况可能是这样的:

  • 布局完成并LayoutChanged触发事件
  • AdornerLayer发现您的尺寸变化AdornedElement
  • AdornerLayer安排您的装饰器重新测量、重新布局和重新渲染
  • 在重新测量之前发生了一些导致Arrange()重新布局和重新渲染的调用。这会导致 WPF 认为装饰器不再需要重新布局或重新渲染。
  • 布局引擎检测到装饰器需要测量并调用Measure
  • 装饰器MeasureOverride重新计算所需的大小,但没有告诉 WPF 装饰器需要重新渲染
  • 布局引擎决定没有更多工作要做,因此装饰器永远不会重新渲染

你可以做些什么来修复它

当然,解决方案是通过在重新测量控件时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()除了在真正需要的情况下什么都不做。

于 2010-03-26T04:58:37.350 回答
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

于 2010-03-24T16:21:20.893 回答