【问题标题】:WPF: Multibind to absolute positionWPF:多重绑定到绝对位置
【发布时间】:2013-12-17 13:35:09
【问题描述】:

我有一个包含以下内容的装饰器模板:

<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>

我希望我的线条在视觉上始终与装饰占位符“连接”,该占位符在运行时相对于画布移动。停靠面板有时也可能独立于装饰占位符移动。如何绑定到 AdornedPlaceHolder 相对于 Canvas 的位置? (我不能依赖停靠面板,因为它独立移动,也不能将占位符从中取出)。

【问题讨论】:

  • X2/Y2 是否应该绑定到装饰器的实际宽度/高度(也就是右下角)?如果是这样,请尝试将它们绑定到装饰器的 ActualWidthActualHeight 属性。
  • 如果您需要画布坐标,请使用对 DockPanel 的 Canvas.Left/Canvas.Top/Canvas.Right/Canvas.Bottom(附加)属性的绑定。
  • 嗨,这实际上是在一个 listviewitem (它有一个滚动查看器)内,所以我的装饰器实际宽度和高度是项目面板的宽度和高度......所以不会这样做。 ..还有其他复杂性...我的问题实际上是如何绑定我提到的这两件事...谢谢。
  • 不确定这与您提供的画布和停靠面板的示例有何关系...
  • 绑定到停靠面板坐标通常可以工作,但由于我的控件位于 Telerik 停靠控件内部,因此当我关闭其他控件时,停靠面板会独立于装饰的占位符移动。所以我不能绑定到它,因为它与画布的装饰占位符不同。

标签: wpf attached-properties multibinding adorner


【解决方案1】:

1。问题

(如果你对那些闲聊不感兴趣,只想看看代码,你可以跳到第 2 节。)

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

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 的位置(即屏幕坐标)何时发生变化, 以便重新计算相对坐标。以及如何在 ControlTemplate 友好的 XAML 中完成?

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

嗯,这并不完全正确。不幸的是,这是一个非常特殊的事件,不仅会在与 UIElement 有关的事情发生时触发,而且每当树中的任何位置发生布局更改时都会触发。更糟糕的是,LayoutUpdated 事件没有提供发送者(发送者参数只是 null)。这背后的原因是explained here

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

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

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

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


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

注意:所有代码都应该放在一个名为ScreenCoordinates的静态类中(因为附加的属性是指这个类名)。

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

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

附加属性的代码相当简单:

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);
}

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

首先,WeakReference 已用于将 UIElements 存储在集合中。这允许对 GUI 丢弃的 UIElements 进行 GC,尽管它们的 WeakReference 仍存储在集合中。如果没有弱引用,代码将没有实际的方法来确定 GUI 是否仍在使用 UIElement,这可能会导致内存/资源泄漏。

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

解决方案是两个有一个队列,我们​​在处理 LayoutUpdated 事件期间发生的任何 AddTrackedElementRemoveTrackedElement 调用都将被“停车”。 在 LayoutUpdated 事件结束时,最终处理“停在”队列中的操作(我们将在稍后解释第二个附加属性时看到这一点)。

//
// 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 的布局系统和使用的面板/容器将控制定位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 事件,我们不需要依赖任何特定的 UIElements 在任何时候被 GUI 使用。

private static UIElement _uiElementForEvent;

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

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

我们差不多完成了。剩下要做的是实现 LayoutUpdated 事件的事件处理程序。 (注意 _isCollControlsToTrackEnumerating 的用法与其在 AddTrackedElementRemoveTrackedElement 方法中的使用有关。)

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 要跟踪时才将事件处理程序附加到事件上才有意义。 所以让我们回到方法 AddTrackedElementRemoveTrackedElement 并应用必要的修改:

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 的左上角,而行的终点位于右下角。

(请注意,与上面的代码相反,我没有测试以下代码 sn-ps。如果它们包含任何错误,我深表歉意。但我希望你能明白......)

<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 仅从绑定中获取数值,将它们相加并应以双精度形式返回(根据 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. 如果可能,尽量避免使用我在此处解释的方法 - 仅当您没有其他选择并且您真的,真的必须使用它时才使用它(几乎总是有另一种,更好的方式来摆弄你的用户界面 - 就像你的情况一样,也许尝试重构你的 GUI 和 GUI 相关的逻辑,这样你就可以拥有 Line 形状和 AdornerPlaceholder em> 都作为 Canvas 的子级)。如果您仍然决定使用它,请少用它。

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

  2. 上面描述的代码在非常深奥但可能的情况下存在死锁的风险。

    想象一个调用 AddTrackedElement 的非 UI 线程,它即将执行 lock (_collControlsToTrack) 语句。 然而,LayoutUpdated 事件刚刚在 UI 线程上处理,并且 OnLayoutUpdated 锁定了 _collControlsToTrack 稍早。自然地,非 UI 线程在 lock 语句处被阻塞,等待 OnLayoutUpdated 释放锁。

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

    避免这种死锁情况的基本思路是用Monitor.Enter(object, bool) 替换AddTrackedElementRemoveTrackedElement 中的lock (_collControlsToTrack),以避免这些方法被阻塞。此外,如果 Monitor.Enter 无法获得锁,您希望利用现有的 _listActionsToRunWhenOnLayoutUpdatedCompletes 来确保对 _collControlsToTrack 的无冲突操作 em>.

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

【讨论】:

  • 已采取“一些笔记”的建议#1……这解决了我的问题。事实证明,将这两个元素重组到同一个 Canvas 中比采用您建议我不应该使用的这种方法更容易也更安全:p 这个问题比我想象的要复杂,值得这样做# 1如你所说。谢谢+100
  • @Baye,睡个好觉,我刚刚注意到我的注释 #3 可以很容易地解决,而无需在事件处理程序中花费太多成本。我会在接下来的几分钟内更新我的答案(即使你不使用那种方法,这很好:))
猜你喜欢
  • 1970-01-01
  • 2021-04-27
  • 2010-12-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-13
相关资源
最近更新 更多