当 children 不可见时,我的面板 OnRender 不会被调用

My panel OnRender is not called when children are invisible

我有一个自定义面板,可以为它们绘制兴趣点和模板化标签。自定义面板然后绘制一条从兴趣点到标签的引出线。

我覆盖 MeasureOverrideArrangeOverrideOnRender 来处理不同的事件:

在正常情况下一切正常:

在一种情况下它不起作用:

相关属性标有FrameworkPropertyMetadataOptions.AffectsParentArrangeFrameworkPropertyMetadataOptions.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();            
}