WPF:Multibind到绝对位置

时间:2013-12-17 13:35:09

标签: wpf attached-properties multibinding adorner

我有一个装饰模板,其中包含:

<ControlTemplate x:Key="myAdornerTemplate">
  <Canvas x:Name="canvas">
     <Line X1="0" Y1="0" X2="(?)" Y2="(?)"/>
     <DockPanel x:Name="root" >
        <AdornedPlaceHolder HorizontalAlignment="Left"/>
     </DockPanel>
  </Canvas>
</ControlTemplate>

我希望我的线始终与视觉上的装饰占位符“连接”,它在运行时相对于画布移动。 dockpanel可能在某个时候也独立于adornedplaceholder移动。如何绑定AdornedPlaceHolder相对于Canvas的位置? (我不能依赖于dockpanel,因为它独立移动,也不会占用我的占位符。)

1 个答案:

答案 0 :(得分:0)

1。问题

(如果您对所有聊天内容不感兴趣,只想查看代码,可以跳到第2部分。)

要获得控件相对于另一个不是父控件的控件的位置(a-ka左上角),您可以执行以下操作:

Point posCtrl1 = control1.PointToScreen(new Point(0, 0));
Point posCtrl2 = control2.PointToScreen(new Point(0, 0));

Point positionOfControl1RelativeToControl2 =
    new Point(posCtrl1.X - posCtrl2.X, posCtrl1.Y - posCtrl2.Y);

如果您不需要在两个控件相对于彼此改变位置时动态更新 positionOfControl1RelativeToControl2 ,这是可以的。

但如果你这样做,你就会遇到问题:如何知道control1或control2的位置(a-ka屏幕坐标)何时发生变化, 这样就可以重新计算相对坐标。如何在ControlTemplate友好的XAML中完成?

幸运的是, UIElement 提供了 LayoutUpdated 事件,该事件会在 UIElement 的位置或大小发生变化时触发。

嗯,这不完全正确。不幸的是,这是一个非常特殊的事件,不仅会发生关于 UIElement 的事情,而且每次在树中的任何地方发生一些布局更改时都会触发。甚至更糟糕的是, LayoutUpdated 事件并不提供发件人(发件人参数只是 null )。其背后的原因是 explained here

LayoutUpdated 的特殊态度要求我们的代码跟踪我们想要从这样的 LayoutUpdated 事件触发时获取屏幕坐标的控件。

  

注意:虽然linked blog post指的是Silverlight,但我发现在&#34;普通&#34;中也是如此。 WPF。   但是,我仍然建议验证此处列出的方法是否适用于您的代码。

但是,除此之外还有一个障碍:我们如何在XAML中告诉应该跟踪哪些 UIElement 的屏幕坐标(我们不想跟踪每一个 UIElement ,因为这可能会导致性能严重下降), 我们如何才能获得并绑定这些屏幕坐标?

附属物业来救援。我们需要两个附加属性。一个用于启用/禁用屏幕坐标跟踪,另一个用于提供屏幕坐标的只读附加属性。


2。 ScreenCoordinates.IsEnabled:用于启用/禁用屏幕坐标跟踪的附加属性

注意:所有代码都应放在名为 ScreenCoordinates 的静态类中(因为附加的属性引用此类名称)。

布尔附加属性 ScreenCoordinates.IsEnabled 将启用/禁用设置为的 UIElement 的屏幕坐标跟踪。

它还将负责向一个集合添加和删除相应的 UIElement ,该集合跟踪我们想要从中获取屏幕坐标的UIElements。

附加属性的代码非常简单:

public static readonly DependencyProperty IsEnabledProperty =
    DependencyProperty.RegisterAttached(
        "IsEnabled",
        typeof(bool),
        typeof(ScreenCoordinates),
        new FrameworkPropertyMetadata(false, OnIsEnabledPropertyChanged)
    );

public static void SetIsEnabled(UIElement element, bool value)
{
    element.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(UIElement element)
{
    return (bool) element.GetValue(IsEnabledProperty);
}

private static void OnIsEnabledPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if ((bool) e.NewValue)
        AddTrackedElement((UIElement) d);
    else
        RemoveTrackedElement((UIElement) d);
}

处理跟踪UIElements实际集合的代码必须考虑两件事。

首先, WeakReference 用于存储集合中的UIElements。这允许通过GUI丢弃的UIE元素的GC,尽管它们的 WeakReference 仍然存储在集合中。如果没有弱引用,代码将无法真正确定GUI是否仍在使用 UIElement ,这可能会导致内存/资源泄漏。

其次,该集合将在 LayoutUpdated 事件期间枚举,该事件通过与实际屏幕坐标的数据绑定(我们将在稍后讨论) - 可能会触发用户代码更改一个 ScreenCoordinates.IsEnabled 属性,它将更改集合,从而搞砸我们的 LayoutUpdated 事件处理程序中的枚举。

这个解决方案是两个有一个队列,我们​​在处理 LayoutUpdated 事件期间发生的任何 AddTrackedElement RemoveTrackedElement 调用将在&#34;停放&#34 ;. 在 LayoutUpdated 事件结束时,操作&#34;停放&#34;在该队列中最终被处理(我们稍后在解释第二个附加属性时会看到它)。

//
// We define a custom EqualityComparer for the HashSet<WeakReference>, which
// treats two WeakReference instances as equal if they refer to the same target.
//
private class WeakReferenceTargetEqualityComparer : IEqualityComparer<WeakReference>
{
    public bool Equals(WeakReference wr1, WeakReference wr2)
    {
        return (wr1.Target == wr2.Target);
    }

    public int GetHashCode(WeakReference wr)
    {
        return wr.GetHashCode();
    }
}

private static readonly HashSet<WeakReference> _collControlsToTrack =
    new HashSet<WeakReference>(new WeakReferenceTargetEqualityComparer());

private static readonly List<Action> _listActionsToRunWhenOnLayoutUpdatedCompletes = new List<Action>();
private static bool _isCollControlsToTrackEnumerating = false;

private static void AddTrackedElement(UIElement uiElem)
{
    if (_isCollControlsToTrackEnumerating)
    {
        lock (_listActionsToRunWhenOnLayoutUpdatedCompletes)
        {
            _listActionsToRunWhenOnLayoutUpdatedCompletes.Enqueue(() => AddTrackedElement(uiElem));
        }
        return;
    }

    lock (_collControlsToTrack)
    {
        // Remove all GC'ed UIElements from _collControlsToTrack and then add the given UIElement
        _collControlsToTrack.RemoveWhere(wr => !wr.IsAlive);
        _collControlsToTrack.Add(new WeakReference(uiElem));
    }
}

private static void RemoveTrackedElement(UIElement uiElem)
{
    if (_isCollControlsToTrackEnumerating)
    {
        lock (_listActionsToRunWhenOnLayoutUpdatedCompletes)
        {
            _listActionsToRunWhenOnLayoutUpdatedCompletes.Enqueue(() => RemoveTrackedElement(uiElem));
        }
        return;
    }

    lock (_collControlsToTrack)
    {
        // Remove all GC'ed UIElements from _collControlsToTrack and then remove the given UIElement
        _collControlsToTrack.RemoveWhere(wr => !wr.IsAlive);
        _collControlsToTrack.Remove(new WeakReference(uiElem));
    }
}


3。 ScreenCoordinates.TopLeft:提供屏幕坐标的只读附加属性

注意:所有代码都应放在名为 ScreenCoordinates 的静态类中(因为附加的属性引用此类名称)。

提供屏幕坐标的附加属性 ScreenCoordinates.TopLeft 是只读的,因为尝试设置它显然没有意义(WPF&#39; s布局系统和使用过的面板/容器将控制UIElements的定位。

ScreenCoordinates.TopLeft 属性将屏幕坐标返回为 Point 类型,其相关代码相当简单:

public static readonly DependencyPropertyKey TopLeftPropertyKey =
    DependencyProperty.RegisterAttachedReadOnly(
        "TopLeft",
        typeof(Point),
        typeof(ScreenCoordinates),
        new FrameworkPropertyMetadata(new Point(0,0))
    );

public static readonly DependencyProperty TopLeftProperty = TopLeftPropertyKey.DependencyProperty;

private static void SetTopLeft(UIElement element, Point value)
{
    element.SetValue(TopLeftPropertyKey, value);
}
public static Point GetTopLeft(UIElement element)
{
    return (Point) element.GetValue(TopLeftProperty);
}

这很容易。哦等等......仍然缺少处理 LayoutUpdated 事件的代码,并将屏幕坐标输入到这个附加属性中。

要接收 LayoutUpdated 事件,我们将使用自己的私有 UIElement 。它永远不会在UI中显示,也不会干扰程序的其余部分。 好消息是,它仍然为我们提供了 LayoutUpdated 事件,我们不需要依赖GUI随时使用的任何特定UIElements。

private static UIElement _uiElementForEvent;

static ScreenCoordinates()
{
    Application.Current.Dispatcher.Invoke( (Action) (() => { _uiElementForEvent = new UIElement(); }) );
}

ScreenCoordinates 的静态构造函数中的代码确保将在UI线程上创建* _uiElementForEvent *。

我们差不多完成了。剩下要做的是为 LayoutUpdated 事件实现事件处理程序。 (注意 _isCollControlsToTrackEnumerating AddTrackedElement RemoveTrackedElement 方法中的使用情况。)

private static void OnLayoutUpdated(object s, EventArgs e)
{
    if (_collControlsToTrack.Count > 0)
    {
        bool doesCollectionHaveGCedElements = false;

        _isCollControlsToTrackEnumerating = true;
        lock (_collControlsToTrack)
        {
            foreach (WeakReference wr in _collControlsToTrack)
            {
                UIElement uiElem = (UIElement)wr.Target;
                if (uiElem != null)
                    SetTopLeft(uiElem, uiElem.PointToScreen(new Point(0, 0)));
                else
                    doesCollectionHaveGCedElements = true;
            }

            //
            // If any GC'ed elements where encountered during enumeration
            // of _collControlsToTrack, then purge the collection from them.
            // In the vast majority of LayoutUpdated events, the UIElements
            // in the collection should be alive. Thus, the performance
            // impact of this code should be (hopefully) negligible.
            //
            if (doesCollectionHaveGCedElements)
                _collControlsToTrack.RemoveWhere(wr => !wr.IsAlive);

            _isCollControlsToTrackEnumerating = false;

            //
            // If there were any AddTrackedElement or RemoveTrackedElement action queued while
            // OnLayoutUpdated was enumerating _collControlsToTrack, then execute them now.
            // (Note that synchronization via _collControlsToTrack is still in effect, thus invocations of
            // AddTrackedElement or RemoveTrackedElement by other threads cannot interleave with the
            // order of actions.
            //
            lock (_listActionsToRunWhenOnLayoutUpdatedCompletes)
            {
                foreach (Action a in _listActionsToRunWhenOnLayoutUpdatedCompletes)
                    a();

                _listActionsToRunWhenOnLayoutUpdatedCompletes.Clear();
            }

            if (_collControlsToTrack.Count == 0)
            {
                _uiElementForEvent.LayoutUpdated -= OnLayoutUpdated;
                _isOnLayoutUpdatedAttachedToEvent = false;
            }
        }
    }
}

最后要做的是将事件处理程序添加到事件...


4。将事件处理程序附加到事件 - 重新访问AddTrackedElement / RemoveTrackedElement

由于 LayoutUpdated 事件可能会经常触发,因此仅当有要跟踪的UIElements时才将事件处理程序附加到事件是有意义的。 因此,让我们回到方法 AddTrackedElement RemoveTrackedElement 并应用必要的修改:

private static bool _isOnLayoutUpdatedAttachedToEvent = false;

private static void AddTrackedElement(UIElement uiElem)
{
    if (_isCollControlsToTrackEnumerating)
    {
        lock (_listActionsToRunWhenOnLayoutUpdatedCompletes)
        {
            _listActionsToRunWhenOnLayoutUpdatedCompletes.Enqueue(() => AddTrackedElement(uiElem));
        }
        return;
    }

    lock (_collControlsToTrack)
    {
        // Remove all GC'ed UIElements from _collControlsToTrack and then add the given UIElement
        _collControlsToTrack.RemoveWhere(wr => !wr.IsAlive);
        _collControlsToTrack.Add(new WeakReference(uiElem));

        if (!_isOnLayoutUpdatedAttachedToEvent)
        {
            _uiElementForEvent.LayoutUpdated += OnLayoutUpdated;
            _isOnLayoutUpdatedAttachedToEvent = true;
        }
    }
}

private static void RemoveTrackedElement(UIElement uiElem)
{
    if (_isCollControlsToTrackEnumerating)
    {
        lock (_listActionsToRunWhenOnLayoutUpdatedCompletes)
        {
            _listActionsToRunWhenOnLayoutUpdatedCompletes.Enqueue(() => RemoveTrackedElement(uiElem));
        }
        return;
    }

    lock (_collControlsToTrack)
    {
        // Remove all GC'ed UIElements from _collControlsToTrack and then remove the given UIElement
        _collControlsToTrack.RemoveWhere(wr => !wr.IsAlive);
        _collControlsToTrack.Remove(new WeakReference(uiElem));

        if (_isOnLayoutUpdatedAttachedToEvent && _collControlsToTrack.Count == 0)
        {
            _uiElementForEvent.LayoutUpdated -= OnLayoutUpdated;
            _isOnLayoutUpdatedAttachedToEvent = false;
        }
    }
}

请注意布尔变量 _isOnLayoutUpdatedAttachedToEvent ,它指示当前是否附加了事件处理程序。


5。所有与你的问题有什么关系?

现在,我必须承认,我仍然无法准确了解您希望获得与AdornerPlaceholder相关的行的起点和终点。

因此,对于以下示例,我假设线的起点位于AdornerPlaceholder的左上角,线端点位于右下角。

(请注意,与上面的代码相反,我没有测试过以下代码片段。如果它们包含任何错误我会道歉。但我希望你能得到这个想法......)

<ControlTemplate x:Key="myAdornerTemplate">
  <Canvas x:Name="canvas">
     <Line>
        <Line.X1>
            <MultiBinding Converter="{StaticResource My:ScreenCoordsToVisualCoordsConverter}" ConverterParameter="X" >
                <Binding ElementName="canvas"/>
                <Binding ElementName="ado" Path="(My:ScreenCoordinates.TopLeft)"/>
            </MultiBinding>
        </Line.X1>
        <Line.Y1>
            <MultiBinding Converter="{StaticResource My:ScreenCoordsToVisualCoordsConverter}" ConverterParameter="Y" >
                <Binding ElementName="canvas"/>
                <Binding ElementName="ado" Path="(My:ScreenCoordinates.TopLeft)"/>
            </MultiBinding>
        </Line.Y1>

        <Line.X2>
            <MultiBinding Converter="{StaticResource My:AdditionConverter}">
                <Binding ElementName="canvas" Path="X1" />
                <Binding ElementName="ado" Path="ActualWidth"/>
            </MultiBinding>
        </Line.X2>
        <Line.Y2>
            <MultiBinding Converter="{StaticResource My:AdditionConverter}">
                <Binding ElementName="canvas" Path="Y1" />
                <Binding ElementName="ado" Path="ActualHeight"/>
            </MultiBinding>
        </Line.Y2>
     </Line>
     <DockPanel x:Name="root" >
        <AdornedPlaceHolder x:Name="Ado" HorizontalAlignment="Left"/>
     </DockPanel>
  </Canvas>
</ControlTemplate>

关于此示例中使用的转换器的一些说法XAML

AdditionConverter 只接受绑定中的数值,添加它们并将它们作为double返回(根据 MultiBinding 的目标类型)。

ScreenCoordsToVisualCoordsConverter 将屏幕坐标中的一个点转换为 Visual UIElement )的本地坐标系中的一个点。 因此,它希望提供两个值:第一个值是 Visual ,第二个值是屏幕坐标中的点。 这个转换器的逻辑如下:

Visual v = (Visual) values[0];
Point screenPoint = (Point) values[1];

Point pointRelativeToVisual = v.PointFromScreen(screenPoint);

ConverterParameter 参数只是定义是否返回 pointRelativeToVisual 的X或Y坐标。


6。一些笔记

  1. 如果可能,请尽量避免我在此解释的方法 - 只有在没有其他选项的情况下才使用它,并且真的,真的必须使用它(几乎总是有另一种选择) ,更好的方法是如何摆弄你的UI - 比如在你的情况下,或许尝试重构你的GUI和GUI相关的逻辑,这样你就可以拥有 Line 形状和 AdornerPlaceholder 两者都是 Canvas 的孩子。如果你仍然决定使用它,请稀疏地使用它。

    由于 LayoutUpdated 事件会在WPF GUI中的任何位置发生关于布局的更改时触发,因此可以相当频繁地连续触发。我在这里给出的代码的粗心应用可能导致 LayoutUpdated 事件的大量且大部分不必要的处理,导致您的GUI与冻结的蜗牛一样快。

  2. 上面描述的代码具有在相当深奥但仍然可能的情况下陷入僵局的风险。

    想象一个非UI线程调用 AddTrackedElement ,它即将执行lock (_collControlsToTrack)语句。 但是,现在刚刚在UI线程上处理 LayoutUpdated 事件,并且 OnLayoutUpdated 锁定 _collControlsToTrack 。当然,非UI线程在 lock 语句中被阻止,等待 OnLayoutUpdated 重新锁定。

    现在想象一下,您已将一个依赖属性绑定到 ScreenCoordinates.TopLeft 。并且该依赖项属性具有 PropertyChangedCallback ,它将等待来自前述非UI线程的信号。但是这个信号永远不会到来,因为非UI线程在 AddTrackedElement 中等待,并且UI线程在 PropertyChangedCallback 中挂起,永远不会完成 OnLayoutUpdated - - 僵局

    避免该死锁方案的基本思想是用Monitor.Enter(object, bool)替换 AddTrackedElement 中的lock (_collControlsToTrack) RemoveTrackedElement ,以避免这些方法被阻止。此外,如果 Monitor.Enter 无法获取锁定,您希望利用现有的 _listActionsToRunWhenOnLayoutUpdatedCompletes 来确保 _collControlsToTrack的无冲突操作

  3. 根据需要,此处给出的方法可能也不完整。虽然代码处理屏幕坐标,但如果只是将主窗口拖到桌面上,它就不会更新 ScreenCoordinates.TopLeft 。 额外跟踪窗口位置需要找到拥有UIElement的窗口并跟踪其 Left Top 属性以及窗口是否处于最大化模式。 但这是另一个黑夜和另一个问题的故事......