我有一个自定义面板,可以为他们绘制兴趣点和模板标签。然后,自定义面板将从感兴趣的点到标签绘制一条引导线。
我覆盖MeasureOverride
,ArrangeOverride
和OnRender
来处理不同的事件:
在正常情况下,一切正常:
在一个案例中,它不起作用:
相关属性标有FrameworkPropertyMetadataOptions.AffectsParentArrange
或FrameworkPropertyMetadataOptions.AffectsRender
。我甚至从我添加到InternalChildren结构的回调中手动调用InvalidateVisual()
。
问题似乎是WPF做了一个优化,如果所有孩子都是零大小或根本不在那里,就不会调用OnRender。这意味着无法调用最后一个引导线。
如果是这种情况,我该如何解决呢?
好的,我把从我们的专用网络导出的代码子集堵塞到GitHub中。如果有错误编译它,我道歉并将尝试解决它后,我回到家里,我有一台可以访问互联网的机器上的编译器。
GitHubRepository:https://github.com/bloritsch/WpfRenderIssue
主分支演示了这个问题,而Kluge-Fix演示了我的答案(所有一行代码......)。
警告:下面包含了相当大的代码。
主窗口XAML:
<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>
主窗口代码 - 背后:
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);
}
}
好的,现在是大班的......
FloorPlanLayout:
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);
}
}
最后一个对象对问题并不重要。这是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));
}
}
}
答案 0 :(得分:3)
我有一个答案,所以如果有人有更优雅的解决方案,请告诉我。在OnRender和ArrangeOverride上设置断点之后,我看了一些控制Panel和UIElement内部深处的度量/排列/渲染回调的标志。
我发现在安排好孩子后,视觉并不总是无效。这只是一个非常明显的案例。在这种情况下的解决方案是始终在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;
}
这当然是非常沉重的。只要我们像现在一样每秒更新一次平面图,我们就可以了。持续更新将是一个问题。
同样,这个解决方案确实有效,但它远非理想。希望它能帮助某人为这个问题找到更好的答案。