我有一个装饰器模板,它包含以下内容:
<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相对于画布的位置?(我不能依赖dockpanel,因为它独立移动,也不能从中取出占位符)。
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事件,当UI元素的位置或大小发生变化时会触发该事件。
事实并非如此。不幸的是,这是一个非常特殊的事件,不仅在发生了与UIElement有关的事情时,而且每次在树中的任何位置发生布局更改时,都会触发。更糟糕的是,LayoutUpdated事件没有提供发送方(发送方参数只是null)。这背后的原因在此处解释。
LayoutUpdated的特殊态度要求我们的代码跟踪我们想要从此类LayoutUpdated事件触发时获得屏幕坐标的控件。
注意:虽然链接的博客文章提到了Silverlight,但我发现在"普通"WPF中也是如此。然而,我仍然建议验证这里列出的方法是否适用于您的代码。
但是,除此之外还有一个障碍:我们将如何在XAML中判断屏幕坐标应跟踪哪个UIElement(我们不想跟踪GUI中的每个UI元素,因为这可能会导致严重的性能下降),我们将如何获得并绑定这些屏幕坐标?
附属财产前来救援。我们将需要两处附属房产。一个用于启用/禁用屏幕坐标的跟踪,另一个用于提供屏幕坐标的只读附加属性。
2.屏幕坐标。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丢弃的UI元素进行GC’ing,尽管它们的WeakReference仍存储在集合中。如果没有弱引用,代码将无法真正确定UIElement是否仍被GUI使用,这可能会导致内存/资源泄漏。
其次,集合将在LayoutUpdated事件期间枚举,通过与实际屏幕坐标的数据绑定(稍后我们将讨论),这可能会触发用户代码更改屏幕坐标。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的静态类中(因为附加的属性引用了这个类名)
提供屏幕坐标的附加属性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事件,并且我们不需要在任何时候依赖GUI正在使用的任何特定UIElement。
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事件可能会经常触发,因此只有当有UIElement要跟踪时,才将事件处理程序附加到事件上是有意义的。因此,让我们回到方法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的左上角,线的终点在右下角。
(注意,与上面的代码相反,我没有测试以下代码片段。如果它们包含任何错误,我很抱歉。但我希望你明白…)
<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.一些注意事项
如果可能的话,尽量避免我在这里解释的方法-只有当你没有其他选择时才使用它,而且你真的,真的必须使用它(几乎总是有另一种更好的方法来处理你的UI——比如在你的情况下,也许可以尝试重组你的GUI和GUI相关逻辑,这样你就可以将Line形状和AdornerPlaceholder都作为Canvas的子对象)。如果你仍然决定使用它,那就少用它。
由于LayoutUpdated事件会在WPF GUI中任何位置的布局发生变化时触发,因此可以频繁且快速地连续触发。我在这里给出的代码的粗心应用可能会导致对LayoutUpdated事件的大量且大部分不必要的处理,导致您的GUI像冻结的蜗牛一样快速上面描述的代码在非常深奥但仍然可能出现死锁的情况下承担了死锁的风险。
想象一个非UI线程调用AddTrackedElement,它正要执行lock (_collControlsToTrack)
语句。但是,LayoutUpdated事件刚刚在UI线程上处理,并且OnLayoutUpdated锁定_collControlsToTrack。自然地,非UI线程在lock语句处被阻塞,等待OnLayoutUpdated重新释放锁。
现在想象一下,您已经将一个依赖属性绑定到ScreenCoordinates.TopLeft。该依赖属性有一个PropertyChangedCallback,它将等待来自上述非UI线程的信号。但这个信号永远不会到来,因为非UI线程在AddTrackedElement中等待,而UI线程挂在PropertyChangedCallback,永远不会完成OnLayoutUpdated--死锁。
避免死锁情况的基本思想是用Monitor.Enter(object,bool)替换AddTrackedElement和RemoveTrackedElement[/em>中的lock (_collControlsToTrack)
,以避免这些方法被阻塞。此外,如果Monitor.Enter无法获取锁,则需要使用现有的_listActionsToRunWhenLayoutUpdatedCompletes来确保对_collControlsToTrack的无冲突操作。根据需要,这里给出的方法也可能是不完整的。尽管代码处理屏幕坐标,但如果您只是在桌面上拖动主窗口,它不会更新ScreenCoordinates.TopLeft。对窗口位置的额外跟踪需要找到拥有UIElement的窗口,并跟踪其Left和Top属性,以及窗口是否处于最大化模式。但这是另一个黑夜和另一个问题的故事。。。