当 children 不可见时,我的面板 OnRender 不会被调用
My panel OnRender is not called when children are invisible
我有一个自定义面板,可以为它们绘制兴趣点和模板化标签。自定义面板然后绘制一条从兴趣点到标签的引出线。
我覆盖 MeasureOverride
、ArrangeOverride
和 OnRender
来处理不同的事件:
- MeasureOverride:计算我的平面图中所有 children 的大小,以及它们相关的标签大小
- ArrangeOverride:将项目放置在平面图上,消除标签歧义并将它们也放置。
- OnRender:绘制从兴趣点到关联标签的引导线
在正常情况下一切正常:
- 在添加任何 children 之前,屏幕上不会绘制任何内容
- 随着children的添加,引出线出现
- 如果我移动我的视觉范围(查看更大的总体平面图),标签将继续移动以避免与边缘或彼此发生碰撞。引导线都已适当更新。
在一种情况下它不起作用:
- 如果我删除所有 children,或将它们标记为不可见,则永远不会调用 OnRender,因此最后一条引导线恰好保留在屏幕上。当我移动我的视觉范围时,它永远不会更新。
相关属性标有FrameworkPropertyMetadataOptions.AffectsParentArrange
或FrameworkPropertyMetadataOptions.AffectsRender
。我什至从添加到 InternalChildren 结构的回调中手动调用 InvalidateVisual()
。
问题似乎是 WPF 进行了优化,如果所有 children 大小为零或根本不存在,则不会调用 OnRender。这意味着不能调用最后一条引导线。
如果是这种情况,我该如何解决?
好的,我将从我们的专用网络导出的代码子集手动塞入 GitHub。如果在编译它时出现错误,我深表歉意,并会在我回家后尝试解决它,我在一台可以访问互联网的机器上安装了一个编译器。
GitHub存储库:https://github.com/bloritsch/WpfRenderIssue
master分支演示了问题,Kluge-Fix演示了我的答案(都是一行代码...)。
警告:下面包含大量代码。
主要WindowXAML:
<Window x:Class="WpfRenderIssue.MainWIndow"
<!-- namespace declarations here -->
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToggleButton x:Name="Toggle" IsChecked="True">Show/Hide</ToggleButton>
<project:FloorPlanLayout x:Name="Layout" Grid.Row="1" LabelOffset="20" LeaderThickness="2">
<project:FloorPlanLayout.LabelTemplate>
<DataTemplate>
<Border Background="#80008000" SnapsToDevicePixels="True">
<TextBlock Margin="3" FontSize="16" FontWeight="SemiBold" Text="{Binding Path=(project:FloorPlanLayout.Label), Mode=OneWay}"
Foreground="{Binding Path=(project:FloorPlanLayout.LabelBrush), Mode=OneWay}"/>
</Border>
</DataTemplate>
</project:FloorPlanLayout.LabelTemplate>
</project:FloorPlanLayout>
</Grid>
</Window>
主要WindowCode-Behind:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
// The layout control is typically used with items
// generated from data, and added after loading.
// We'll just hard code the one element to show the problem
Rectangle rectangle = new Rectangle
{
Width = 30,
Height = 30,
Fill = Brushes.DodgerBlue
};
Canvas.SetLeft(rectangle, 100);
Canvas.SetTop(rectangle, 50);
FloorPlanLayout.SetLabel(rectangle, "Test Label");
FloorPlanLayout.SetLabelBrush(rectangle, Brushes.Black);
BindingOperations.SetBinding(rectangle, VisibilityProperty, new Binding
{
Source = Toggle,
Path = new PropertyPath(ToggleButton.IsCheckedProperty),
Converter = new BooleanToVisibilityConverter()
});
Layout.Children.Add(rectangle);
}
}
好的,那么现在是大class.....
楼层平面布局:
public class FloorPlanLayout : Canvas
{
// Attached properties:
public static readonly DependencyProperty LabelProperty =
DependencyProperty.RegisterAttached("Label", typeof(string), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty LabelBrushProperty =
DependencyProperty.RegisterAttached("LabelBrush", typeof(Brush), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(Brushes.Transparent, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsParentArrange));
// private attached dependency properties
private static readonly DependencyProperty IsLabelProperty =
DependencyProperty.RegisterAttached("IsLabel", typeof(bool), typeof(FloorPlanLayout),
new PropertyMetadata(false));
private static readonly DependencyProperty LabelPresenterProperty =
DependencyProperty.RegisterAttached("IsLabel", typeof(ContentProperty), typeof(FloorPlanLayout));
// public properties
public static readonly DependencyProperty LabelOffsetProperty =
DependencyProperty.Register("LabelOffset", typeof(double), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(20.0, FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependentyProperty LabelTemplateProperty =
DependencyProperty.Register("LabelTemplate", typeof(DataTemplate), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty LeaderThicknessProperty =
DependencyProperty.Register("LeaderThickness", typeof(double), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(1.0, FrameowrkPropertyMetadataOptions.AffectsRender));
// Skipping the boilerplate setters/getters and class properties for
// brevity and keeping this to the important stuff
public FloorPlanLayout()
{
ClipToBounds = true;
// NOTE: for completeness I would have to respond to the Loaded
// event to handle the equivalent callback to create the label
// presenters for items added directly in XAML due to the XAML
// initializers circumventing runtime code
}
public override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
{
NotifyingUIElementCollection collection = new NotifyingUIElementCollection(this, logicalParent);
collection.CollectionChanged += ChildrenCollectionChanged;
return collection;
}
protected override Size MeasureOverride(Size availableSize)
{
Size newDesiredSize = new Size(
(double.IsInfinity(availbleSize.Width) ? double.MaxValue : availableSize.Width),
(double.IsInfinity(availableSize.Height) ? double.MaxValue : availableSize.Height));
foreach(UIElement child in InternalChildren)
{
child.Measure(availableSize);
newDesiredSize.Width = Math.Max(newDesiredSize.Width, child.DesiredSize.Width);
newDesiredSize.Height = Math.Max(newDesiredSize.Height, child.DesiredSize.Height);
}
return newDesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach(UIElement child in InternalChildren.OfType<UIElement>()
.Where(e => !GetIsLabel(e))
.OrderByDescending(GetZIndex))
{
Rect plotArea = PositionByCanvasLocationOrIgnore(child, finalSize);
ContentPresenter labelPresenter = GetLabelPresenter(child);
Rect labelRect = new Rect(labelPresenter.DesiredSize)
{
X = plotArea.Right + LabelOffset,
Y = plotArea.Y + ((plotArea.Height - labelPresenter.DesiredSize.Height) / 2)
};
labelPresenter.Arrange(labelRect);
}
return finalSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
double dpiFactor = 1;
if(LabelTemplate == null || LeaderThickness < 0.25)
{
// nothing to do if no label template, or leader thickness too small
return;
}
PresentationSource source = PresentationSource.FromVisual(this);
if(source != null && source.CompositionTarget != null)
{
// Adjust for DPI
Matrix matrix = source.CompositionTarget.TransformToDevice;
dpiFactor = 1 / matrix.M11;
}
foreach(FrameworkElement element in
InternalChildren.OfType<FrameworkElement>().Where(child => !GetIsLable(child)))
{
FrameworkElement label = GetLabelPresenter(element);
if(label == null || !label.IsVisible || !element.IsVisible)
{
// don't draw lines if there are no visible labels
continue;
}
Brush leaderBrush = GetLabelBrush(element);
if(leaderBrush == null || Equals(leaderBrush, Brushes.Transparent)
{
// Don't draw leader if brush is null or transparent
continue;
}
leaderBrush.Freeze();
Pen linePen = new Pen(leaderBrush, LeaderThickness * dpiFactor);
linePen.Freeze();
Rect objectRect = new Rect(element.TranslatePiont(new Point(), this), element.RenderSize);
Rect labelRect = new Rect(label.TranslatePoint(new Point(), this), label.RenderSize);
double halfPenWidth = linePen.Thicnkess / 2;
// Set up snap to pixels
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(objectRect.Right + halfPenWidth);
guidelines.GuidelinesX.Add(labelRect.Left + halfPenWidth);
guidelines.GuidelinesY.Add(objectRect.Top + halfPenWidth);
guidelines.GuidelinesY.Add(labelRect.Top + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
if(objectRect.Width > 0 && labelRect.Width > 0)
{
Point startPoint = new Point(objectRect.Right + linePen.Thickness,
objectRect.Top + (objectRect.Height / 2));
Point endPoint = new Point(labelRect.Left,
labelRect.Top + (labelRect.Height / 2));
drawingContext.DrawLine(linePen, startPoint, endPoint);
drawingContext.DrawLine(linePen, labelRect.TopLeft, labelRect.BottomLeft);
}
drawingContext.Pop();
}
}
private static Rect PositionByCanvasLocationOrIgnore(UIElement child, SIze finalSize)
{
double left = GetLeft(child);
double top = GetTop(child);
if (double.IsNaN(left))
{
// if no left anchor calculate from the right
double right = GetRight(child);
left = double.IsNaN(right) ? right : finalSize.Width - right - child.DesiredSize.Width;
}
if(double.IsNaN(top))
{
double bottom = GetBottom(child);
top = double.IsNaN(top) ? bottom : finalSize.Height - bottom - child.DesiredSize.Height;
}
if(double.IsNaN(left) || double.IsNaN(top))
{
// if it's still unset, don't position the element
returnRect.Empty;
}
Rect plotArea = new Rect(new Point(left, top), child.DesiredSize);
child.Arrange(plotArea);
return plotArea;
}
private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if(args.OldItems != null)
{
foreach(UIElement child in args.OldItems)
{
RemoveLabelForElement(child);
}
}
if(args.NewItems != null)
{
foreach(UIElement child in args.NewItems)
{
CreateLabelForElement(child);
}
}
// Try to clean up leader lines if we remove the last item
InvalidateVisual();
}
private void CreateLabelForElement(UIElement element)
{
if(LabelTemplate == null || element == null || GetIsLabel(element))
{
// prevent unnecessary work and recursive calls because labels
// have to be children too.
return;
}
ContentPresenter label = new ContentPresenter
{
Content = element
};
SetIsLabel(label, true);
BindingOperations.SetBinding(label, ContentPresenter.ContentTemplateProperty, new Binding
{
Source = this,
Path = new PropertyPath(LabelTemplateProperty),
Mode = BindingMode.OneWay
});
BindingOperations.SetBinding(label, VisibilityProperty, new Binding
{
Source = element,
Path = new PropertyPath(VisibilityProperty),
Mode = BindingMode.OneWay
});
BindingOperations.SetBinding(label, ZIndexProperty, new Binding
{
Source = element,
Path = new PropertyPath(ZIndexProperty),
Mode = BindingMode.OneWay
});
SetLabelPresenter(element, label);
Children.Add(label);
}
private void RemoveLabelForElement(UIElement element)
{
if (element == null)
{
return;
}
ContentPresenter label = GetLabelPresenter(element);
if(label == null)
{
// true if we never added a label, and if the element was a label to begin with
return true;
}
BindingOperations.ClearBinding(label, ContentPresenter.ContentTemplateProperty);
BindingOperations.ClearBinding(label, VisibilityProperty);
BindingOperations.ClearBinding(label, ZIndexProperty);
Children.Remove(label);
SetLabelPresenter(element, null);
}
}
最后一个 object 对问题来说真的不是那么重要。这是 NotifyingUIElementCollection:
public class NotifyingUIElementCollection : UIElementCollection
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public NotifyingUIElementCollection(UIElement visualParent, FrameworkElement logicalParent)
: base(visualParent, logicalParent)
{}
public override int Add(UIElement element)
{
int index = base.Add(element);
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Add, element);
return index;
}
public override void Clear()
{
base.Clear();
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Reset, null);
}
public override void Remove(UIElement element)
{
base.Remove(element);
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Remove, element);
}
public override void RemoveAt(int index)
{
base.RemoveAt(index);
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Remove, this[index]);
}
public override void RemoveRange(int index, int count)
{
UIElement[] itemsRemoved = this.OfType<UIElement>().Skip(index).Take(count).ToArray();
base.RemoveRange(index, count);
OnNotifyCollectionCnaged(NotifyCollectionChangedAction.Remove, itemsRemoved);
}
private void OnNotifyCollectionChanged(NotifyCollectionChangedAction action, params UIElement[] items)
{
if(CollectionChanged != null)
{
CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, items));
}
}
}
我有很多答案,所以如果有人有更优雅的解决方案,请告诉我。在 OnRender 和 ArrangeOverride 上设置断点后,我查看了一些控制 Panel 和 UIElement 深处的 measure/arrange/render 回调的标志。
我发现在安排我的children之后,视觉效果并不总是失效。这只是一个非常明显的案例。这种情况下的解决方案是始终在 ArrangeOverride() 结束时调用 InvalidateVisual()。
protected override Size ArrangeOverride(Size finalSize)
{
foreach(UIElement child in InternalChildren.OfType<UIElement>()
.Where(e => !GetIsLabel(e))
.OrderByDescending(GetZIndex))
{
Rect plotArea = PositionByCanvasLocationOrIgnore(child, finalSize);
ContentPresenter labelPresenter = GetLabelPresenter(child);
Rect labelRect = new Rect(labelPresenter.DesiredSize)
{
X = plotArea.Right + LabelOffset,
Y = plotArea.Y + ((plotArea.Height - labelPresenter.DesiredSize.Height) / 2)
};
labelPresenter.Arrange(labelRect);
}
// NEW CODE: force the visual to be redrawn every time.
InvalidateVisual();
return finalSize;
}
当然,这是非常严厉的。只要我们像现在一样每秒只更新一次平面图,就可以了。持续更新会出问题
同样,此解决方案确实有效,但远非理想。希望它可以帮助人们更好地回答这个问题。
您不需要 InvalidateVisual()
除非控件的大小发生变化。
如果您只想重新绘制控件的 UI...在 OnRender()
期间将 DrawingGroup
放入可视化树中,然后您可以随时 DrawingGroup.Open()
并更改 DrawingGroup
中的绘图命令。只要子集合发生变化,您就会这样做。
看起来像这样:
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();
}
我有一个自定义面板,可以为它们绘制兴趣点和模板化标签。自定义面板然后绘制一条从兴趣点到标签的引出线。
我覆盖 MeasureOverride
、ArrangeOverride
和 OnRender
来处理不同的事件:
- MeasureOverride:计算我的平面图中所有 children 的大小,以及它们相关的标签大小
- ArrangeOverride:将项目放置在平面图上,消除标签歧义并将它们也放置。
- OnRender:绘制从兴趣点到关联标签的引导线
在正常情况下一切正常:
- 在添加任何 children 之前,屏幕上不会绘制任何内容
- 随着children的添加,引出线出现
- 如果我移动我的视觉范围(查看更大的总体平面图),标签将继续移动以避免与边缘或彼此发生碰撞。引导线都已适当更新。
在一种情况下它不起作用:
- 如果我删除所有 children,或将它们标记为不可见,则永远不会调用 OnRender,因此最后一条引导线恰好保留在屏幕上。当我移动我的视觉范围时,它永远不会更新。
相关属性标有FrameworkPropertyMetadataOptions.AffectsParentArrange
或FrameworkPropertyMetadataOptions.AffectsRender
。我什至从添加到 InternalChildren 结构的回调中手动调用 InvalidateVisual()
。
问题似乎是 WPF 进行了优化,如果所有 children 大小为零或根本不存在,则不会调用 OnRender。这意味着不能调用最后一条引导线。
如果是这种情况,我该如何解决?
好的,我将从我们的专用网络导出的代码子集手动塞入 GitHub。如果在编译它时出现错误,我深表歉意,并会在我回家后尝试解决它,我在一台可以访问互联网的机器上安装了一个编译器。
GitHub存储库:https://github.com/bloritsch/WpfRenderIssue
master分支演示了问题,Kluge-Fix演示了我的答案(都是一行代码...)。
警告:下面包含大量代码。
主要WindowXAML:
<Window x:Class="WpfRenderIssue.MainWIndow"
<!-- namespace declarations here -->
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToggleButton x:Name="Toggle" IsChecked="True">Show/Hide</ToggleButton>
<project:FloorPlanLayout x:Name="Layout" Grid.Row="1" LabelOffset="20" LeaderThickness="2">
<project:FloorPlanLayout.LabelTemplate>
<DataTemplate>
<Border Background="#80008000" SnapsToDevicePixels="True">
<TextBlock Margin="3" FontSize="16" FontWeight="SemiBold" Text="{Binding Path=(project:FloorPlanLayout.Label), Mode=OneWay}"
Foreground="{Binding Path=(project:FloorPlanLayout.LabelBrush), Mode=OneWay}"/>
</Border>
</DataTemplate>
</project:FloorPlanLayout.LabelTemplate>
</project:FloorPlanLayout>
</Grid>
</Window>
主要WindowCode-Behind:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
// The layout control is typically used with items
// generated from data, and added after loading.
// We'll just hard code the one element to show the problem
Rectangle rectangle = new Rectangle
{
Width = 30,
Height = 30,
Fill = Brushes.DodgerBlue
};
Canvas.SetLeft(rectangle, 100);
Canvas.SetTop(rectangle, 50);
FloorPlanLayout.SetLabel(rectangle, "Test Label");
FloorPlanLayout.SetLabelBrush(rectangle, Brushes.Black);
BindingOperations.SetBinding(rectangle, VisibilityProperty, new Binding
{
Source = Toggle,
Path = new PropertyPath(ToggleButton.IsCheckedProperty),
Converter = new BooleanToVisibilityConverter()
});
Layout.Children.Add(rectangle);
}
}
好的,那么现在是大class.....
楼层平面布局:
public class FloorPlanLayout : Canvas
{
// Attached properties:
public static readonly DependencyProperty LabelProperty =
DependencyProperty.RegisterAttached("Label", typeof(string), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty LabelBrushProperty =
DependencyProperty.RegisterAttached("LabelBrush", typeof(Brush), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(Brushes.Transparent, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsParentArrange));
// private attached dependency properties
private static readonly DependencyProperty IsLabelProperty =
DependencyProperty.RegisterAttached("IsLabel", typeof(bool), typeof(FloorPlanLayout),
new PropertyMetadata(false));
private static readonly DependencyProperty LabelPresenterProperty =
DependencyProperty.RegisterAttached("IsLabel", typeof(ContentProperty), typeof(FloorPlanLayout));
// public properties
public static readonly DependencyProperty LabelOffsetProperty =
DependencyProperty.Register("LabelOffset", typeof(double), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(20.0, FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependentyProperty LabelTemplateProperty =
DependencyProperty.Register("LabelTemplate", typeof(DataTemplate), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty LeaderThicknessProperty =
DependencyProperty.Register("LeaderThickness", typeof(double), typeof(FloorPlanLayout),
new FrameworkPropertyMetadata(1.0, FrameowrkPropertyMetadataOptions.AffectsRender));
// Skipping the boilerplate setters/getters and class properties for
// brevity and keeping this to the important stuff
public FloorPlanLayout()
{
ClipToBounds = true;
// NOTE: for completeness I would have to respond to the Loaded
// event to handle the equivalent callback to create the label
// presenters for items added directly in XAML due to the XAML
// initializers circumventing runtime code
}
public override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
{
NotifyingUIElementCollection collection = new NotifyingUIElementCollection(this, logicalParent);
collection.CollectionChanged += ChildrenCollectionChanged;
return collection;
}
protected override Size MeasureOverride(Size availableSize)
{
Size newDesiredSize = new Size(
(double.IsInfinity(availbleSize.Width) ? double.MaxValue : availableSize.Width),
(double.IsInfinity(availableSize.Height) ? double.MaxValue : availableSize.Height));
foreach(UIElement child in InternalChildren)
{
child.Measure(availableSize);
newDesiredSize.Width = Math.Max(newDesiredSize.Width, child.DesiredSize.Width);
newDesiredSize.Height = Math.Max(newDesiredSize.Height, child.DesiredSize.Height);
}
return newDesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach(UIElement child in InternalChildren.OfType<UIElement>()
.Where(e => !GetIsLabel(e))
.OrderByDescending(GetZIndex))
{
Rect plotArea = PositionByCanvasLocationOrIgnore(child, finalSize);
ContentPresenter labelPresenter = GetLabelPresenter(child);
Rect labelRect = new Rect(labelPresenter.DesiredSize)
{
X = plotArea.Right + LabelOffset,
Y = plotArea.Y + ((plotArea.Height - labelPresenter.DesiredSize.Height) / 2)
};
labelPresenter.Arrange(labelRect);
}
return finalSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
double dpiFactor = 1;
if(LabelTemplate == null || LeaderThickness < 0.25)
{
// nothing to do if no label template, or leader thickness too small
return;
}
PresentationSource source = PresentationSource.FromVisual(this);
if(source != null && source.CompositionTarget != null)
{
// Adjust for DPI
Matrix matrix = source.CompositionTarget.TransformToDevice;
dpiFactor = 1 / matrix.M11;
}
foreach(FrameworkElement element in
InternalChildren.OfType<FrameworkElement>().Where(child => !GetIsLable(child)))
{
FrameworkElement label = GetLabelPresenter(element);
if(label == null || !label.IsVisible || !element.IsVisible)
{
// don't draw lines if there are no visible labels
continue;
}
Brush leaderBrush = GetLabelBrush(element);
if(leaderBrush == null || Equals(leaderBrush, Brushes.Transparent)
{
// Don't draw leader if brush is null or transparent
continue;
}
leaderBrush.Freeze();
Pen linePen = new Pen(leaderBrush, LeaderThickness * dpiFactor);
linePen.Freeze();
Rect objectRect = new Rect(element.TranslatePiont(new Point(), this), element.RenderSize);
Rect labelRect = new Rect(label.TranslatePoint(new Point(), this), label.RenderSize);
double halfPenWidth = linePen.Thicnkess / 2;
// Set up snap to pixels
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(objectRect.Right + halfPenWidth);
guidelines.GuidelinesX.Add(labelRect.Left + halfPenWidth);
guidelines.GuidelinesY.Add(objectRect.Top + halfPenWidth);
guidelines.GuidelinesY.Add(labelRect.Top + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
if(objectRect.Width > 0 && labelRect.Width > 0)
{
Point startPoint = new Point(objectRect.Right + linePen.Thickness,
objectRect.Top + (objectRect.Height / 2));
Point endPoint = new Point(labelRect.Left,
labelRect.Top + (labelRect.Height / 2));
drawingContext.DrawLine(linePen, startPoint, endPoint);
drawingContext.DrawLine(linePen, labelRect.TopLeft, labelRect.BottomLeft);
}
drawingContext.Pop();
}
}
private static Rect PositionByCanvasLocationOrIgnore(UIElement child, SIze finalSize)
{
double left = GetLeft(child);
double top = GetTop(child);
if (double.IsNaN(left))
{
// if no left anchor calculate from the right
double right = GetRight(child);
left = double.IsNaN(right) ? right : finalSize.Width - right - child.DesiredSize.Width;
}
if(double.IsNaN(top))
{
double bottom = GetBottom(child);
top = double.IsNaN(top) ? bottom : finalSize.Height - bottom - child.DesiredSize.Height;
}
if(double.IsNaN(left) || double.IsNaN(top))
{
// if it's still unset, don't position the element
returnRect.Empty;
}
Rect plotArea = new Rect(new Point(left, top), child.DesiredSize);
child.Arrange(plotArea);
return plotArea;
}
private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if(args.OldItems != null)
{
foreach(UIElement child in args.OldItems)
{
RemoveLabelForElement(child);
}
}
if(args.NewItems != null)
{
foreach(UIElement child in args.NewItems)
{
CreateLabelForElement(child);
}
}
// Try to clean up leader lines if we remove the last item
InvalidateVisual();
}
private void CreateLabelForElement(UIElement element)
{
if(LabelTemplate == null || element == null || GetIsLabel(element))
{
// prevent unnecessary work and recursive calls because labels
// have to be children too.
return;
}
ContentPresenter label = new ContentPresenter
{
Content = element
};
SetIsLabel(label, true);
BindingOperations.SetBinding(label, ContentPresenter.ContentTemplateProperty, new Binding
{
Source = this,
Path = new PropertyPath(LabelTemplateProperty),
Mode = BindingMode.OneWay
});
BindingOperations.SetBinding(label, VisibilityProperty, new Binding
{
Source = element,
Path = new PropertyPath(VisibilityProperty),
Mode = BindingMode.OneWay
});
BindingOperations.SetBinding(label, ZIndexProperty, new Binding
{
Source = element,
Path = new PropertyPath(ZIndexProperty),
Mode = BindingMode.OneWay
});
SetLabelPresenter(element, label);
Children.Add(label);
}
private void RemoveLabelForElement(UIElement element)
{
if (element == null)
{
return;
}
ContentPresenter label = GetLabelPresenter(element);
if(label == null)
{
// true if we never added a label, and if the element was a label to begin with
return true;
}
BindingOperations.ClearBinding(label, ContentPresenter.ContentTemplateProperty);
BindingOperations.ClearBinding(label, VisibilityProperty);
BindingOperations.ClearBinding(label, ZIndexProperty);
Children.Remove(label);
SetLabelPresenter(element, null);
}
}
最后一个 object 对问题来说真的不是那么重要。这是 NotifyingUIElementCollection:
public class NotifyingUIElementCollection : UIElementCollection
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public NotifyingUIElementCollection(UIElement visualParent, FrameworkElement logicalParent)
: base(visualParent, logicalParent)
{}
public override int Add(UIElement element)
{
int index = base.Add(element);
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Add, element);
return index;
}
public override void Clear()
{
base.Clear();
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Reset, null);
}
public override void Remove(UIElement element)
{
base.Remove(element);
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Remove, element);
}
public override void RemoveAt(int index)
{
base.RemoveAt(index);
OnNotifyCollectionChanged(NotifyCollectionChangedAction.Remove, this[index]);
}
public override void RemoveRange(int index, int count)
{
UIElement[] itemsRemoved = this.OfType<UIElement>().Skip(index).Take(count).ToArray();
base.RemoveRange(index, count);
OnNotifyCollectionCnaged(NotifyCollectionChangedAction.Remove, itemsRemoved);
}
private void OnNotifyCollectionChanged(NotifyCollectionChangedAction action, params UIElement[] items)
{
if(CollectionChanged != null)
{
CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, items));
}
}
}
我有很多答案,所以如果有人有更优雅的解决方案,请告诉我。在 OnRender 和 ArrangeOverride 上设置断点后,我查看了一些控制 Panel 和 UIElement 深处的 measure/arrange/render 回调的标志。
我发现在安排我的children之后,视觉效果并不总是失效。这只是一个非常明显的案例。这种情况下的解决方案是始终在 ArrangeOverride() 结束时调用 InvalidateVisual()。
protected override Size ArrangeOverride(Size finalSize)
{
foreach(UIElement child in InternalChildren.OfType<UIElement>()
.Where(e => !GetIsLabel(e))
.OrderByDescending(GetZIndex))
{
Rect plotArea = PositionByCanvasLocationOrIgnore(child, finalSize);
ContentPresenter labelPresenter = GetLabelPresenter(child);
Rect labelRect = new Rect(labelPresenter.DesiredSize)
{
X = plotArea.Right + LabelOffset,
Y = plotArea.Y + ((plotArea.Height - labelPresenter.DesiredSize.Height) / 2)
};
labelPresenter.Arrange(labelRect);
}
// NEW CODE: force the visual to be redrawn every time.
InvalidateVisual();
return finalSize;
}
当然,这是非常严厉的。只要我们像现在一样每秒只更新一次平面图,就可以了。持续更新会出问题
同样,此解决方案确实有效,但远非理想。希望它可以帮助人们更好地回答这个问题。
您不需要 InvalidateVisual()
除非控件的大小发生变化。
如果您只想重新绘制控件的 UI...在 OnRender()
期间将 DrawingGroup
放入可视化树中,然后您可以随时 DrawingGroup.Open()
并更改 DrawingGroup
中的绘图命令。只要子集合发生变化,您就会这样做。
看起来像这样:
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();
}