在放置目标中心底部对齐的弹出窗口

4
我想在按钮悬停时实现一个弹出窗口。当用户悬停在按钮上时,我希望弹出窗口出现。当他们没有悬停时,我只希望标签出现。这有点像工具提示,但是我不希望弹出窗口在一定时间后消失。我使用了一个带有两个注意事项的按钮控件模板来实现它:
  1. 当我悬停在按钮下面的区域时,屏幕会在弹出窗口和标签之间闪烁。
  2. 我希望弹出窗口底部和中心对齐。

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>

编辑:已解决闪烁问题。只需将弹出窗口放置在底部和中心即可。


我将ClipToBounds = True设置为解决闪烁问题的方法。 - user1958503
5个回答

2

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

使用类似以下的多绑定方式将信息传递到我的转换器以获得VerticalOffset:

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

看看这个问题... 底部对齐并传入宽度计算居中可能更有意义,但是由你决定: https://dev59.com/ZGgu5IYBdhLWcg3w8LWv - sohum

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

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

附加依赖属性解决方案

这个解决方案使用一个静态类,公开一些附加依赖属性。使用这些属性,您可以水平或垂直地将 Popup 对齐到其 PlacementTargetPlacementRectangle。仅当 PopupPlacement 属性的值表示边缘(LeftTopRightBottom)时,才会进行对齐。

实现

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


    }
}

工作原理

以上代码创建了一个静态类,为Popup控件公开了4个附加的依赖属性。它们分别是HorizontalPlacementAlignmentVerticalPlacementAlignmentHorizontalOffsetVerticalOffset

HorizontalPlacementAlignmentVerticalPlacementAlignment附加依赖属性允许您将弹出窗口与其PlacementTargetPlacementRectangle相对齐。为实现这一点,机制使用Popup.HorizontalOffsetPopup.VerticalOffset属性来定位Popup

由于该机制使用Popup.HorizontalOffsetPopup.VerticalOffset属性来工作,因此该类提供自己的HorizontalOffsetVerticalOffset属性(附加依赖属性)。您可以使用它们来调整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 上,但通常会在使用这些自定义附加依赖属性的 WindowResourceDictionary 上进行设置。
结论
使用附加依赖属性实现此功能的思路是尽可能简化其在 XAML 中的使用。对于简单的一次性需求,使用转换器可能更容易实现。然而,在这种情况下使用附加依赖属性可能提供更动态和易用的方法。

0

在sohum的答案的基础上,这是我如何让我的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(以允许弹出窗口在用户尝试取消选择时折叠),而CenterToolTipConverter可以在sohum的link中找到。


0
更好的方法是将您的PlacementTarget控件放置在一个Grid中,并使您的Popup控件成为同一Grid的子级,同时保持Placement=Bottom。这将在PlacementTarget控件下方居中显示您的Popup。没有转换器,没有样式,只有简单的XAML。

0
你尝试过使用MouseEnter事件吗?然后你可以在DispatcherTimer上打开弹出窗口,然后再关闭它。

很好 :). 你可以使用MVVM-Light和RelayCommands在viewmodel中调用弹出窗口。从那里使用子窗口服务或类似的东西来处理窗口逻辑。 - LueTm
我真的不想添加其他工具包的依赖,只是为了使标签以正确的位置和方式显示。上面的代码几乎可以工作。我觉得我的触发器只是放在了错误的对象上,并且我错过了如何使用弹出窗口的放置属性的诀窍。 - sohum
嗯...你可以使用标记扩展并将其绑定到事件。如果我没记错的话,你也可以使用Blend行为来实现相同的效果。 - LueTm
我必须得说,如果不是最好的话,MVVM light在调用viewmodel上的东西方面真的很棒。已经多次救了我的一天。 - LueTm
我已经成功解决了触发器中我之前没有意识到的SourceName属性引起的闪烁问题。现在弹出窗口问题仍然存在。考虑到微软提供了居中和底部,这似乎很荒谬,为什么默认情况下不提供此功能。 - sohum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接