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

时间:2011-10-18 02:05:57

标签: wpf custom-controls redraw invalidation onrender

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

如上所述,我见过的大多数答案都围绕着使Visual失效,这会使布局无效,这会迫使新的测量并安排非常昂贵的通行证,特别是对于我们的非常复杂的视觉树。但同样,布局更改,VisualTree也不会。唯一能做的就是外部数据的呈现方式不同。因此,这绝对是一个纯粹的渲染问题。

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


更新

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

4 个答案:

答案 0 :(得分:19)

很遗憾,您必须致电InvalidateVisual,在内部拨打InvalidateArrangeOnRender方法作为排列阶段的一部分被调用,因此您需要告诉WPF重新排列控件(InvalidateArrange所做的)并且需要重绘(InvalidateVisual会这样做)。

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

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

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

答案 1 :(得分:10)

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

更新:我已经将接受的答案移到这里有两个原因。一,我希望人们知道解决方案(大多数人只阅读接受的答案并继续前进)和两个,考虑到他有25K的代表,我不认为他'如果我收回它,请介意! :)

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

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'上调用InvalidateVisual时(在按钮的'Click'事件中),我得到了这个......

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即可叔叔! :)

希望这有帮助!

答案 2 :(得分:2)

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

有效地更新控件的视觉效果而不改变其大小。使用DrawingGroup。您创建DrawingGroup并在DrawingContext期间将其放入OnRender(),然后随时可以Open() DrawingGroup更改其可视化绘图命令,WPF将自动< em>并有效地重新呈现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
}

答案 3 :(得分:1)

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

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