尽管这已经是一个老问题了,但我也有同样的需求——我需要能够将
Popup
与其放置目标对齐。不满意转换器解决方案,我想出了自己的解决方案,使用附加依赖属性,在这里与您和任何有相同需求的人分享。
注意:此解决方案不涵盖如何在鼠标悬停时显示Popup
。它仅涵盖最棘手的部分——将Popup
与其放置目标对齐。有几种方法可以在鼠标悬停时显示Popup
,例如使用触发器或绑定,这两种方法都在StackOverflow上广泛覆盖。
附加依赖属性解决方案
这个解决方案使用一个静态类,公开一些附加依赖属性。使用这些属性,您可以水平或垂直地将
Popup
对齐到其
PlacementTarget
或
PlacementRectangle
。仅当
Popup
的
Placement
属性的值表示边缘(
Left
、
Top
、
Right
或
Bottom
)时,才会进行对齐。
实现
using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace MyProjectName.Ui
{
public static class PopupProperties
{
#region Properties
#region IsMonitoringState attached dependency property
private static readonly DependencyProperty IsMonitoringStateProperty
= DependencyProperty.RegisterAttached("IsMonitoringState",
typeof(bool), typeof(PopupProperties),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(IsMonitoringStatePropertyChanged)));
private static void IsMonitoringStatePropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
bool value = (bool)e.NewValue;
if (value)
{
popup.Opened += Popup_Opened;
popup.Unloaded += Popup_Unloaded;
UpdateLocation(popup);
}
else
{
popup.Opened -= Popup_Opened;
popup.Unloaded -= Popup_Unloaded;
}
}
private static bool GetIsMonitoringState(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (bool)popup.GetValue(IsMonitoringStateProperty);
}
private static void SetIsMonitoringState(Popup popup, bool isMonitoringState)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
popup.SetValue(IsMonitoringStateProperty, isMonitoringState);
}
#endregion
#region HorizontalPlacementAlignment attached dependency property
public static readonly DependencyProperty HorizontalPlacementAlignmentProperty
= DependencyProperty.RegisterAttached("HorizontalPlacementAlignment",
typeof(AlignmentX), typeof(PopupProperties),
new FrameworkPropertyMetadata(AlignmentX.Left,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(HorizontalPlacementAlignmentPropertyChanged)),
new ValidateValueCallback(HorizontalPlacementAlignmentPropertyValidate));
private static void HorizontalPlacementAlignmentPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool HorizontalPlacementAlignmentPropertyValidate(object obj)
{
return Enum.IsDefined(typeof(AlignmentX), obj);
}
public static AlignmentX GetHorizontalPlacementAlignment(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (AlignmentX)popup.GetValue(HorizontalPlacementAlignmentProperty);
}
public static void SetHorizontalPlacementAlignment(Popup popup, AlignmentX alignment)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
popup.SetValue(HorizontalPlacementAlignmentProperty, alignment);
}
#endregion
#region VerticalPlacementAlignment attached dependency property
public static readonly DependencyProperty VerticalPlacementAlignmentProperty
= DependencyProperty.RegisterAttached("VerticalPlacementAlignment",
typeof(AlignmentY), typeof(PopupProperties),
new FrameworkPropertyMetadata(AlignmentY.Top,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(VerticalPlacementAlignmentPropertyChanged)),
new ValidateValueCallback(VerticalPlacementAlignmentPropertyValidate));
private static void VerticalPlacementAlignmentPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool VerticalPlacementAlignmentPropertyValidate(object obj)
{
return Enum.IsDefined(typeof(AlignmentY), obj);
}
public static AlignmentY GetVerticalPlacementAlignment(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (AlignmentY)popup.GetValue(VerticalPlacementAlignmentProperty);
}
public static void SetVerticalPlacementAlignment(Popup popup, AlignmentY alignment)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
popup.SetValue(VerticalPlacementAlignmentProperty, alignment);
}
#endregion
#region HorizontalOffset attached dependency property
public static readonly DependencyProperty HorizontalOffsetProperty
= DependencyProperty.RegisterAttached("HorizontalOffset",
typeof(double), typeof(PopupProperties),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(HorizontalOffsetPropertyChanged)),
new ValidateValueCallback(HorizontalOffsetPropertyValidate));
private static void HorizontalOffsetPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool HorizontalOffsetPropertyValidate(object obj)
{
double value = (double)obj;
return !double.IsNaN(value) && !double.IsInfinity(value);
}
public static double GetHorizontalOffset(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (double)popup.GetValue(HorizontalOffsetProperty);
}
public static void SetHorizontalOffset(Popup popup, double offset)
{
if (popup is null)
throw new ArgumentNullException(nameof(offset));
popup.SetValue(HorizontalOffsetProperty, offset);
}
#endregion
#region VerticalOffset attached dependency property
public static readonly DependencyProperty VerticalOffsetProperty
= DependencyProperty.RegisterAttached("VerticalOffset",
typeof(double), typeof(PopupProperties),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(VerticalOffsetPropertyChanged)),
new ValidateValueCallback(VerticalOffsetPropertyValidate));
private static void VerticalOffsetPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool VerticalOffsetPropertyValidate(object obj)
{
double value = (double)obj;
return !double.IsNaN(value) && !double.IsInfinity(value);
}
public static double GetVerticalOffset(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (double)popup.GetValue(VerticalOffsetProperty);
}
public static void SetVerticalOffset(Popup popup, double offset)
{
if (popup is null)
throw new ArgumentNullException(nameof(offset));
popup.SetValue(VerticalOffsetProperty, offset);
}
#endregion
#endregion Properties
#region Methods
private static void OnMonitorState(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
UpdateLocation(popup);
}
private static void UpdateLocation(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
if (!popup.IsOpen)
return;
double offsetX = 0d;
double offsetY = 0d;
PlacementMode placement = popup.Placement;
UIElement placementTarget = popup.PlacementTarget;
Rect placementRect = popup.PlacementRectangle;
if (placement == PlacementMode.Top || placement == PlacementMode.Bottom
|| placement == PlacementMode.Left || placement == PlacementMode.Right)
{
Size popupSize = GetElementSize(popup);
UIElement child = popup.Child;
if ((popupSize.IsEmpty || popupSize.Width <= 0d || popupSize.Height <= 0d)
&& child != null)
{
popupSize = GetElementSize(child);
}
Size targetSize;
if (placementRect.Width > 0d && placementRect.Height > 0d)
targetSize = placementRect.Size;
else if (placementTarget != null)
targetSize = GetElementSize(placementTarget);
else
targetSize = Size.Empty;
if (!popupSize.IsEmpty && popupSize.Width > 0d && popupSize.Height > 0d
&& !targetSize.IsEmpty && targetSize.Width > 0d && targetSize.Height > 0d)
{
switch (placement)
{
case PlacementMode.Top:
case PlacementMode.Bottom:
switch (GetHorizontalPlacementAlignment(popup))
{
case AlignmentX.Left:
offsetX = 0d;
break;
case AlignmentX.Center:
offsetX = -(popupSize.Width - targetSize.Width) / 2d;
break;
case AlignmentX.Right:
offsetX = -(popupSize.Width - targetSize.Width);
break;
default:
break;
}
break;
case PlacementMode.Left:
case PlacementMode.Right:
switch (GetVerticalPlacementAlignment(popup))
{
case AlignmentY.Top:
offsetY = 0d;
break;
case AlignmentY.Center:
offsetY = -(popupSize.Height - targetSize.Height) / 2d;
break;
case AlignmentY.Bottom:
offsetY = -(popupSize.Height - targetSize.Height);
break;
default:
break;
}
break;
default:
break;
}
}
}
offsetX += GetHorizontalOffset(popup);
offsetY += GetVerticalOffset(popup);
popup.SetCurrentValue(Popup.HorizontalOffsetProperty, offsetX);
popup.SetCurrentValue(Popup.VerticalOffsetProperty, offsetY);
}
private static Size GetElementSize(UIElement element)
{
if (element is null)
return new Size(0d, 0d);
else if (element is FrameworkElement frameworkElement)
return new Size(frameworkElement.ActualWidth, frameworkElement.ActualHeight);
else
return element.RenderSize;
}
#endregion Methods
#region Event handlers
private static void Popup_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is Popup popup)
{
SetIsMonitoringState(popup, false);
}
}
private static void Popup_Opened(object sender, EventArgs e)
{
if (sender is Popup popup)
{
OnMonitorState(popup);
}
}
#endregion Event handlers
}
}
工作原理
以上代码创建了一个静态类,为Popup
控件公开了4个附加的依赖属性。它们分别是HorizontalPlacementAlignment
、VerticalPlacementAlignment
、HorizontalOffset
和VerticalOffset
。
HorizontalPlacementAlignment
和VerticalPlacementAlignment
附加依赖属性允许您将弹出窗口与其PlacementTarget
或PlacementRectangle
相对齐。为实现这一点,机制使用Popup.HorizontalOffset
和Popup.VerticalOffset
属性来定位Popup
。
由于该机制使用Popup.HorizontalOffset
和Popup.VerticalOffset
属性来工作,因此该类提供自己的HorizontalOffset
和VerticalOffset
属性(附加依赖属性)。您可以使用它们来调整Popup
的位置,除了其对齐之外。
机制会在每次打开弹出窗口时自动更新
Popup
的位置。然而,当弹出窗口大小改变或其放置目标或放置矩形大小改变时,其位置将不会自动更新。尽管如此,如果再多加一些工作,这个功能就可以很容易地实现。
用法示例
您可以像下面的示例一样,在Popup
上使用附加属性。在这个示例中,我们有一个简单的Button
和一个Popup
。弹出窗口显示在Button
的底部,并水平居中于Button
的中心。
<Button x:Name="MyTargetElement">My Button</Button>
<Popup xmlns:ui="clr-namespace:MyProjectName.Ui"
PlacementTarget="{Binding ElementName=MyTargetElement}"
Placement="Bottom"
ui:PopupProperties.HorizontalPlacementAlignment="Center"
ui:PopupProperties.VerticalOffset="2">
</Popup>
通过添加
ui:PopupProperties.HorizontalPlacementAlignment="Center"
和
ui:PopupProperties.VerticalOffset="2"
到
Popup
,它将与其放置目标的水平��心对齐,并且还具有 2 个 WPF 单位的垂直偏移量。
请注意,在
Popup
上使用
xmlns:ui="clr-namespace:MyProjectName.Ui"
。此属性仅导入您项目中
MyProjectName.Ui
命名空间的类型,并使它们可通过在 XAML 属性上使用
ui:
前缀来使用。在本例中,为简单起见,该属性设置��
Popup
上,但通常会在使用这些自定义附加依赖属性的
Window
或
ResourceDictionary
上进行设置。
结论
使用附加依赖属性实现此功能的思路是尽可能简化其在 XAML 中的使用。对于简单的一次性需求,使用转换器可能更容易实现。然而,在这种情况下使用附加依赖属性可能提供更动态和易用的方法。