我们正在控制子类的OnRender
中进行自定义绘制。此绘图代码基于外部触发器和数据。因此,每当触发器触发时,我们都需要根据该数据重新呈现控件。我们要做的是找出如何强制控件重新渲染但不经过整个布局传递。
如上所述,我见过的大多数答案都围绕着使Visual
失效,这会使布局无效,这会迫使新的测量并安排非常昂贵的通行证,特别是对于我们的非常复杂的视觉树。但同样,布局不更改,VisualTree也不会。唯一能做的就是外部数据的呈现方式不同。因此,这绝对是一个纯粹的渲染问题。
同样,我们只是在寻找一种简单的方法来告诉控件它需要重新执行OnRender
。我已经看到一个'hack',你在其中创建一个新的DependencyProperty
并将其注册为'AffectsRender',当你想要刷新控件时你只需设置一些值,但我对正在发生的事情更感兴趣在这些属性的默认实现中:他们调用什么来影响该行为。
好吧,看起来没有任何这样的调用,因为即使AffectsRender
标志仍然在内部导致一个排列传递(根据CodeNaked的答案如下),但我发布了第二个答案,显示内置在行为和解决方案中解决您的布局传递代码,以简单的可空大小作为标志运行。见下文。
答案 0 :(得分:19)
很遗憾,您必须致电InvalidateVisual,在内部拨打InvalidateArrange。 OnRender
方法作为排列阶段的一部分被调用,因此您需要告诉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调用一些虚拟委托,这将导致具有该优先级或更高优先级的任何内容也被调用,从而导致重新呈现。