sohum
sohum

Reputation: 3227

Popup aligned on center and bottom of PlacementTarget

I'm basically trying to implement a popup on a button hover. When a user is hovered over the button I want the popup to appear. When they're not, I want only the label to appear. It is kinda like a tooltip except that I don't want the Popup going away after some amount of time passes. I kinda have it working using a ControlTemplate on the button with two caveats:

  1. When I hover over the area below the button, the screen flickers between the popup and a label.
  2. I want the Popup to be aligned bottom and center.

Xaml Code:

<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>

Edit: Fixed the flicker issue. Just need the placement of the Popup to be bottom and center.

Upvotes: 4

Views: 9671

Answers (5)

Unknown Coder
Unknown Coder

Reputation: 392

Although this is already an old question, I had the same need too — I needed to be able to align a Popup to its placement target. Not happy with the converter solution, I came up with my own solution, using attached dependency properties, which I'm sharing here with you and anyone with the same need.

NOTE: This solution doesn't cover how to show a Popup on mouse hover. It covers only the most tricky part — the alignment of the Popup to its placement target. There are several ways to show a Popup on mouse hover, like using Triggers or Bindings, both widely covered on StackOverflow.

Attached dependency properties solution

This solution uses a single static class that exposes some attached dependency properties. Using these properties, you can align a Popup to its PlacementTarget or its PlacementRectangle, either horizontally or vertically. The alignment only occurs when the value of Popup's Placement property represents an edge (Left, Top, Right or Bottom).

Implementation

PopupProperties.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


    }
}

How it works

The code above creates a static class that exposes 4 attached dependency properties for Popup controls. Namely, they are HorizontalPlacementAlignment, VerticalPlacementAlignment, HorizontalOffset and VerticalOffset.

HorizontalPlacementAlignment and VerticalPlacementAlignment attached dependency properties allow you to align a popup relative to its PlacementTarget or PlacementRectangle. To achieve this, the mechanism uses Popup.HorizontalOffset and Popup.VerticalOffset properties to position the Popup.

Because the mechanism uses Popup.HorizontalOffset and Popup.VerticalOffset properties to work, this class provides its own HorizontalOffset and VerticalOffset properties (attached dependency properties). You can use them to adjust the position of the Popup in addition to its alignment.

The mechanism automatically updates the Popup position every time the Popup is opened. However, its position will not be automatically updated when the popup size changes or when its placement target or placement rectangle sizes change. Nonetheless, with a bit more work put into it, that functionality could be easily implemented.

Usage example

You would use the attached properties on a Popup like on the example below. In this example, we have a simple Button and a Popup. The Popup is displayed aligned to the bottom of the Button and horizontally centered to the Button's center.

<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>

By adding ui:PopupProperties.HorizontalPlacementAlignment="Center" and ui:PopupProperties.VerticalOffset="2" to a Popup, it will be aligned to the horizontal center of its placement target and have 2 WPF units of vertical offset as well.

Note the use of xmlns:ui="clr-namespace:MyProjectName.Ui" on the Popup. This attribute only imports the types from MyProjectName.Ui namespace on your project and makes them available through the usage of the ui: prefix on your XAML attributes. In the example, this attribute is set on the Popup for simplicity, but you would usually set it on your Window or ResourceDictionary where you're using these custom attached dependency properties.

Conclusion

The idea behind using attached dependency properties to achieve this functionality is to make its usage in XAML as simple as possible. For a simple one-time need, using converters may be simpler to implement. However, using attached dependency properties, in this case, may provide a more dynamic and usage-friendly approach.

Upvotes: 2

dotNET
dotNET

Reputation: 35450

Much better is to put your PlacementTarget control inside a Grid and make your Popup control a child of the same Grid while keeping Placement=Bottom. This will show your Popup bottom-centered under the PlacementTarget control. No converters, no styles, plain simple XAML.

Upvotes: 0

DharmaTurtle
DharmaTurtle

Reputation: 8467

Adding to sohum's answer, here's how I got my ListView-Popup bottom centered under a ToggleButton. It correctly horizontaly offsets depending on how wide the listview is. I also left in bits and pieces that give the togglebutton intuitive behavior, like clicking the togglebutton again to hide the popup.

<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 returns true if false and false if true (to allow the popup to collapse when the user tries to untoggle it) and CenterToolTipConverter can be found in sohum's link.

Upvotes: 0

sohum
sohum

Reputation: 3227

I ended up having to write a converter that moved it down based on the height of the popup and the placement target.

Use a multibinding like this to pass the info into my converter for VerticalOffset:

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

Upvotes: 2

LueTm
LueTm

Reputation: 2380

Have you tried the MouseEnter event? Then you can open the popup on a DispatcherTimer and then close it again.

Upvotes: 0

Related Questions