【问题标题】:Popup aligned on center and bottom of PlacementTarget在 PlacementTarget 的中心和底部对齐的弹出窗口
【发布时间】:2012-08-06 20:14:28
【问题描述】:

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

  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>

编辑:修复了闪烁问题。只需要将 Popup 的位置置于底部和中心即可。

【问题讨论】:

  • 我设置 ClipToBounds = True 来解决闪烁问题。

标签: wpf xaml popup controltemplate


【解决方案1】:

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

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

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

【讨论】:

【解决方案2】:

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

【讨论】:

  • 好:)。您可以使用 MVVM-Light 和 RelayCommands 在视图模型中调用弹出窗口。从那里使用子窗口服务或类似的东西来处理窗口逻辑。
  • 我真的不希望将依赖项添加到另一个工具包,只是为了在正确的位置和方式显示标签。上面的代码几乎有效。我觉得我的触发器只是在错误的对象上,并且我错过了如何使用 Popup 的放置属性的技巧。
  • 嗯...您可以做一个标记扩展并将其绑定到事件。如果我没记错的话,你可以对混合行为做同样的事情。
  • 我真的不得不说,虽然 MVVM 灯如果不是在视图模型上调用东西的最佳选择的话,它确实很棒。多次挽救了这一天。
  • 我能够解决我在触发器中不知道的 SourceName 属性的闪烁问题。现在弹出问题迫在眉睫。考虑到 MSFT 为我们提供了 Center 和 Bottom,默认情况下不提供这似乎很愚蠢。
【解决方案3】:

添加到 sohum 的 answer,这就是我如何让我的 ListView-Popup 底部在 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。

【讨论】:

    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

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

      附加依赖属性解决方案

      此解决方案使用单个静态类公开一些附加的依赖属性。使用这些属性,您可以将Popup 与其PlacementTarget 或其PlacementRectangle 水平或垂直对齐。仅当PopupPlacement 属性的值表示一条边(LeftTopRightBottom)时,才会发生对齐。

      实施

      弹出属性.cs
      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 的位置。但是,当弹出窗口大小发生变化或放置目标或放置矩形大小发生变化时,它的位置不会自动更新。尽管如此,只要投入更多的工作,该功能就可以轻松实现。

      使用示例

      您将使用Popup 上的附加属性,如下例所示。在这个例子中,我们有一个简单的Button 和一个PopupPopupButton 的底部对齐显示,并以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 中的使用尽可能简单。对于简单的一次性需求,使用转换器可能更易于实现。但是,在这种情况下,使用附加的依赖属性可能会提供一种更加动态和使用友好的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-05-01
        • 2014-08-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-01-30
        相关资源
        最近更新 更多