弹出窗口在PlacementTarget的中心和底部对齐

时间:2012-08-06 20:14:29

标签: wpf xaml popup controltemplate

我基本上试图在按钮悬停上实现弹出窗口。当用户悬停在按钮上时,我希望弹出窗口显示。如果不是,我只想出现标签。它有点像工具提示,除了我不希望Popup在经过一段时间后消失。我有点使用按钮上的ControlTemplate工作,有两个警告:

  1. 当我将鼠标悬停在按钮下方的区域时,屏幕会在弹出窗口和标签之间闪烁。
  2. 我希望Popup在底部和中心对齐。
  3. Xaml代码:

    <Window>
        <Window.Resources>
            <Style x:Key="LabelStyle" TargetType="Label">
                <Setter Property="Margin" Value="0, 0, 0, 5" />
                <Setter Property="Width" Value="58" />
                <Setter Property="Height" Value="28" />
                <Setter Property="Padding" Value="1, 0, 1, 0" />
            </Style>
    
            <ControlTemplate x:Key="ButtonControlTemplate" TargetType="Button">
                <StackPanel>
                    <Button Width="48" Height="48" Background="White" Name="ItemButton">
                        <ContentPresenter Content="{TemplateBinding Property=ContentControl.Content}" />
                    </Button>
                    <Label Style="{StaticResource LabelStyle}" VerticalContentAlignment="Top" HorizontalContentAlignment="Center" Name="ItemLabel">
                        <TextBlock TextWrapping="Wrap" TextAlignment="Center" FontSize="11" LineHeight="13" LineStackingStrategy="BlockLineHeight">
                            Hello World!
                        </TextBlock>
                    </Label>
                    <Popup Name="ItemPopup" Placement="Bottom" PlacementTarget="{Binding ElementName=ItemButton}">
                        <TextBlock Background="Red">Hello World!</TextBlock>
                    </Popup>
                </StackPanel>
                <ControlTemplate.Triggers>
                    <Trigger SourceName="ItemButton" Property="IsMouseOver" Value="True">
                        <Setter TargetName="ItemLabel" Property="Visibility" Value="Hidden" />
                        <Setter TargetName="ItemPopup" Property="IsOpen" Value="True" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Window.Resources>
    
        <StackPanel>
            <Button Background="Green" Template="{StaticResource ButtonControlTemplate}">
                <Image Source="http://leduc998.files.wordpress.com/2010/10/msft_logo.jpg" />
            </Button>
        </StackPanel>
    </Window>
    

    编辑:修复了闪烁问题。只需要将Popup放置在底部和中心位置。

5 个答案:

答案 0 :(得分:2)

我最终必须编写一个转换器,根据弹出窗口和放置目标的高度将其移动。

使用这样的多重绑定将信息传递给我的转换器以获取VerticalOffset:

<MultiBinding Converter="{StaticResource PopupVerticalAligner}">
    <Binding RelativeSource="{RelativeSource Self}" Path="PlacementTarget.ActualHeight" />
    <Binding RelativeSource="{RelativeSource Self}" Path="ActualHeight" />
</MultiBinding>

答案 1 :(得分:0)

您是否尝试过MouseEnter事件?然后,您可以在DispatcherTimer上打开弹出窗口,然后再次关闭它。

答案 2 :(得分:0)

添加到sohum的answer,这是我如何将我的ListView-Popup底部置于ToggleButton下面。它可以根据listview的宽度正确地进行偏移。我还留下了点滴和碎片,给了togglebutton直观的行为,比如再次单击togglebutton来隐藏弹出窗口。

<ToggleButton x:Name="ParentToggleButton" IsChecked="{Binding ToggleButtonStatus}" IsHitTestVisible="{Binding ElementName=ToggledPopup, Path=IsOpen, Converter={StaticResource BoolToInvertedBoolConverter}}" >
  <ToggleButton.Content>...</ToggleButton.Content>
</ToggleButton>
<Popup PlacementTarget="{Binding ElementName=ParentToggleButton}"  Placement="Bottom" StaysOpen="False" IsOpen="{Binding ToggleButtonStatus}" x:Name="ToggledPopup">
  <Popup.HorizontalOffset>
    <MultiBinding Converter="{StaticResource CenterToolTipConverter}">
      <Binding RelativeSource="{RelativeSource Self}" Path="PlacementTarget.ActualWidth"/>
      <Binding ElementName="INeedYourWidth" Path="ActualWidth"/>
    </MultiBinding>
  </Popup.HorizontalOffset>
  <ListView x:Name="INeedYourWidth" ItemsSource="{Binding ItemsSource}" >
    <ListView.ItemTemplate>
      <DataTemplate>...</DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</Popup>

BoolToInvertedBoolConverter如果为false则返回true,如果为true则返回false(允许弹出窗口在用户尝试取消它时折叠),并且可以在sohum的link中找到CenterToolTipConverter。

答案 3 :(得分:0)

更好的方法是将 PlacementTarget 控件置于Grid内,并使Popup控件成为Grid的同一个孩子,同时保持Placement=Bottom }。这将在PlacementTarget控件下显示您的Popup以底部为中心。没有转换器,没有样式,简单的简单XAML。

答案 4 :(得分:0)

尽管这已经是一个老问题了,但我也有同样的需求-我需要能够将Popup与其放置目标对齐。我对转换器解决方案不满意,我想出了自己的解决方案,使用了附加的依赖项属性,在这里与您以及有同样需求的任何人共享这些属性。

注意:此解决方案不介绍如何在鼠标上显示Popup 悬停。它仅涵盖最棘手的部分-Popup与其放置目标的对齐。有几种方法可以在鼠标悬停时显示Popup,例如使用 Triggers Bindings ,这两种方法在StackOverflow上都有广泛介绍。

附加的依赖项属性解决方案

此解决方案使用单个静态类,该类公开了一些附加的依赖项属性。使用这些属性,您可以将Popup与其PlacementTarget或其PlacementRectangle水平或垂直对齐。仅当Popup的{​​{1}}属性的值表示边缘(PlacementLeftTopRight时,才发生对齐。

实施

PopupProperties.cs
Bottom

工作原理

上面的代码创建了一个静态类,该类公开了using System; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace MyProjectName.Ui { /// <summary> /// Exposes attached dependency properties that provide /// additional functionality for <see cref="Popup"/> controls. /// </summary> /// <seealso cref="Popup"/> /// <seealso cref="DependencyProperty"/> public static class PopupProperties { #region Properties #region IsMonitoringState attached dependency property /// <summary> /// Attached <see cref="DependencyProperty"/>. This property /// registers (<b>true</b>) or unregisters (<b>false</b>) a /// <see cref="Popup"/> from the popup monitoring mechanism /// used internally by <see cref="PopupProperties"/> to keep /// the <see cref="Popup"/> in synchrony with the /// <see cref="PopupProperties"/>' attached properties. A /// <see cref="Popup"/> will be automatically unregistered from /// this mechanism after it is unloaded. /// </summary> /// <seealso cref="Popup"/> 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) { // Attach popup. popup.Opened += Popup_Opened; popup.Unloaded += Popup_Unloaded; // Update popup. UpdateLocation(popup); } else { // Detach popup. 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) { // Validate parameters. if (popup is null) throw new ArgumentNullException(nameof(popup)); // If the popup is not open, we don't need to update its position yet. if (!popup.IsOpen) return; // Setup initial variables. double offsetX = 0d; double offsetY = 0d; PlacementMode placement = popup.Placement; UIElement placementTarget = popup.PlacementTarget; Rect placementRect = popup.PlacementRectangle; // If the popup placement mode is an edge of the placement target, // determine the alignment offset. if (placement == PlacementMode.Top || placement == PlacementMode.Bottom || placement == PlacementMode.Left || placement == PlacementMode.Right) { // Try to get the popup size. If its size is empty, use the size // of its child, if any child exists. Size popupSize = GetElementSize(popup); UIElement child = popup.Child; if ((popupSize.IsEmpty || popupSize.Width <= 0d || popupSize.Height <= 0d) && child != null) { popupSize = GetElementSize(child); } // Get the placement rectangle size. If it's empty, get the // placement target's size, if a target is set. Size targetSize; if (placementRect.Width > 0d && placementRect.Height > 0d) targetSize = placementRect.Size; else if (placementTarget != null) targetSize = GetElementSize(placementTarget); else targetSize = Size.Empty; // If we have a valid popup size and a valid target size, determine // the offset needed to align the popup to the target rectangle. if (!popupSize.IsEmpty && popupSize.Width > 0d && popupSize.Height > 0d && !targetSize.IsEmpty && targetSize.Width > 0d && targetSize.Height > 0d) { switch (placement) { // Horizontal alignment offset. 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; // Vertical alignment offset. 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; } } } // Add the developer specified offsets to the offsets we've calculated. offsetX += GetHorizontalOffset(popup); offsetY += GetVerticalOffset(popup); // Apply the final computed offsets to the 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) { // Stop monitoring the popup state, because it was unloaded. SetIsMonitoringState(popup, false); } } private static void Popup_Opened(object sender, EventArgs e) { if (sender is Popup popup) { OnMonitorState(popup); } } #endregion Event handlers } } 控件的4个附加的依赖项属性。即,它们是PopupHorizontalPlacementAlignmentVerticalPlacementAlignmentHorizontalOffset

VerticalOffsetHorizontalPlacementAlignment附加的依赖项属性使您可以相对于弹出窗口的VerticalPlacementAlignmentPlacementTarget对齐弹出窗口。为此,该机制使用PlacementRectanglePopup.HorizontalOffset属性来定位Popup.VerticalOffset

由于该机制使用PopupPopup.HorizontalOffset属性来工作,因此此类提供了自己的Popup.VerticalOffsetHorizontalOffset属性(附加的依赖项属性)。除了对齐方式,您还可以使用它们来调整VerticalOffset的位置。

每次打开Popup时,该机制都会自动更新Popup的位置。但是,当弹出窗口大小更改或其放置目标或放置矩形大小更改时,其位置不会自动更新。尽管如此,只要投入更多的工作,该功能就可以轻松实现。

用法示例

您可以像下面的示例一样在Popup上使用附加的属性。在此示例中,我们有一个简单的Popup和一个ButtonPopup显示为与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",它将与放置目标的水平中心对齐,并且还具有2个WPF垂直偏移单位。

请注意在Popup上使用xmlns:ui="clr-namespace:MyProjectName.Ui"。此属性仅从项目上的Popup命名空间导入类型,并通过使用XAML属性上的MyProjectName.Ui前缀使它们可用。在示例中,为简单起见,在ui:上设置了此属性,但是通常在使用这些自定义附加依赖项属性的PopupWindow上设置它。

结论

使用附加的依赖项属性来实现此功能的想法是使它在XAML中的使用尽可能简单。对于简单的一次性需求,使用转换器可能更易于实现。但是,在这种情况下,使用附加的依赖项属性可能会提供一种更加动态且易于使用的方法。