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 事件期间发生的任何 AddTrackedElement 和 RemoveTrackedElement 调用都将被“停车”。
在 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 的用法与其在 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 的左上角,而行的终点位于右下角。
(请注意,与上面的代码相反,我没有测试以下代码 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。一些注意事项
如果可能,尽量避免使用我在此处解释的方法 - 仅当您没有其他选择并且您真的,真的必须使用它时才使用它(几乎总是有另一种,更好的方式来摆弄你的用户界面 - 就像你的情况一样,也许尝试重构你的 GUI 和 GUI 相关的逻辑,这样你就可以拥有 Line 形状和 AdornerPlaceholder em> 都作为 Canvas 的子级)。如果您仍然决定使用它,请少用它。
由于每当您的 WPF GUI 中任何位置的布局发生更改时都会触发 LayoutUpdated 事件,因此可以频繁且快速地连续触发它。粗心地应用我在此处给出的代码可能会导致对 LayoutUpdated 事件进行大量且大部分不必要的处理,从而导致您的 GUI 像冻结的蜗牛一样快速。
上面描述的代码在非常深奥但可能的情况下存在死锁的风险。
想象一个调用 AddTrackedElement 的非 UI 线程,它即将执行 lock (_collControlsToTrack) 语句。
然而,LayoutUpdated 事件刚刚在 UI 线程上处理,并且 OnLayoutUpdated 锁定了 _collControlsToTrack 稍早。自然地,非 UI 线程在 lock 语句处被阻塞,等待 OnLayoutUpdated 释放锁。
现在想象一下,您已经将一个依赖属性绑定到 ScreenCoordinates.TopLeft。并且该依赖属性有一个 PropertyChangedCallback,它将等待来自上述非 UI 线程的信号。但是那个信号永远不会到来,因为非 UI 线程在 AddTrackedElement 中等待,而 UI 线程在 PropertyChangedCallback 中挂起,永远不会完成 OnLayoutUpdated - - 僵局。
避免这种死锁情况的基本思路是用Monitor.Enter(object, bool) 替换AddTrackedElement 和RemoveTrackedElement 中的lock (_collControlsToTrack),以避免这些方法被阻塞。此外,如果 Monitor.Enter 无法获得锁,您希望利用现有的 _listActionsToRunWhenOnLayoutUpdatedCompletes 来确保对 _collControlsToTrack 的无冲突操作 em>.
根据需要,此处给出的方法也可能不完整。虽然代码处理屏幕坐标,但如果您只是在桌面上拖动主窗口,它不会更新 ScreenCoordinates.TopLeft。
额外跟踪窗口位置需要找到拥有 UIElement 的窗口并跟踪其 Left 和 Top 属性以及窗口是否处于最大化模式。
但这是另一个黑夜和另一个问题的故事......