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

时间:2015-11-13 19:39:42

标签: c# wpf custom-controls

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

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

  • MeasureOverride:计算平面图中所有儿童的尺寸,以及相关的标签尺寸
  • ArrangeOverride:将项目放在平面图上,消除标签歧义并放置它们。
  • OnRender:从感兴趣的点到相关标签
  • 绘制引线

在正常情况下,一切正常:

  • 在添加任何子项之前,屏幕上没有任何内容
  • 随着孩子的添加,领导者行显示
  • 如果我移动视觉范围(查看更大的整体平面图),标签将继续移动以避免与边缘或彼此碰撞。领导者行都进行了适当的更新。

在一个案例中,它不起作用:

  • 如果我删除了所有孩子,或者将它们全部标记为不可见,则永远不会调用OnRender,因此最后一个引导线恰好保留在屏幕上。当我移动视觉范围时,它永远不会更新。

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

1 个答案:

答案 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;
}

这当然是非常沉重的。只要我们像现在一样每秒更新一次平面图,我们就可以了。持续更新将是一个问题。

同样,这个解决方案确实有效,但它远非理想。希望它能帮助某人为这个问题找到更好的答案。