Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF popup with arrow head style

I would like to implement a popup style that looks like the following design:

enter image description here

The gray square represents the UIElement that shows the popup when it is clicked. The popup style is just a border (the easy part) with an arrow head that points to the center of the target element (the hard part). Also the aligment is important, when a control is placed on the right part of the window, the popup should be aligned to the right, otherwise to the left.

Is there an example or some documentation that instruct me how to proceed?

like image 608
Daniel Peñalba Avatar asked Sep 30 '16 07:09

Daniel Peñalba


2 Answers

Okay, I have a solution for this. It's frustratingly complex.

If you're just after a plain pop-up, just with a tail, you can probably use chunks of this (ActualLayout and UpdateTail logic). If you're after the whole Help-Tip Experience™ you're in for an unpleasant ride.

I do think that it might be better to go down the Adorner route though (and I'm thinking of re-working this to use adorers). I've spotted some issues and it's still in work. Using pop-ups causes them to appear in the designer on the top of other windows and it's really annoying. I've also noticed they are not positioned correctly on some computers for some weird reasons (but none where I've got Visual Studio installed to properly debug).

It produces something like this:

enter image description here

With the following criteria:

  • Only a single help tip can be displayed on the screen at each time

  • If the user changes tab, and the control a help-tip is attached to is no longer visible, the help tip dissapears and the next help-tip is displayed

  • Once closed, a help-tip of that type won't be displayed again

  • Help tips can be turned off through one central option

Okay. So, the actual help tip is a usercontrol that's completely transparent and added to the UI. It has a pop-up that's managed using a static class. Here's the control:

<UserControl x:Class="...HelpPopup"
             d:DesignHeight="0" d:DesignWidth="0">
    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </UserControl.Resources>
    <Canvas>
        <Popup x:Name="Popup"
               d:DataContext="{d:DesignInstance {x:Null}}"
               DataContext="{Binding HelpTip, ElementName=userControl}"
               StaysOpen="True" PopupAnimation="Fade"
               AllowsTransparency="True"
               materialDesign:ShadowAssist.ShadowDepth="Depth3"
               Placement="{Binding Placement, ElementName=userControl}"
               HorizontalOffset="-10"
               VerticalOffset="{Binding VerticalOffset, ElementName=userControl}">
            <Grid Margin="0,0,0,0" SnapsToDevicePixels="True">
                <Canvas Margin="10">
                    <local:RoundedCornersPolygon Fill="{StaticResource PrimaryHueDarkBrush}"
                                                 SnapsToDevicePixels="True"
                                                 ArcRoundness="4"
                                                 Points="{Binding PolygonPath, ElementName=userControl}"
                                                 Effect="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Popup}, Path=(materialDesign:ShadowAssist.ShadowDepth), Converter={x:Static converters:ShadowConverter.Instance}}"/>
                </Canvas>
                <Border BorderBrush="Transparent" BorderThickness="10,25,10,25">
                    <Grid x:Name="PopupChild">
                        <materialDesign:ColorZone Mode="PrimaryDark" Margin="5">
                            <StackPanel>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*"/>
                                        <ColumnDefinition Width="AUTO"/>
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Text="Useful Tip"
                                               FontWeight="Bold"
                                               Margin="2,0,0,0"
                                               Grid.ColumnSpan="2"
                                               VerticalAlignment="Center"/>

                                    <Button Style="{StaticResource MaterialDesignToolButton}" Click="CloseButton_Click" Grid.Column="1" Margin="0" Padding="0" Height="Auto">
                                        <Button.Content>
                                            <materialDesign:PackIcon Kind="CloseCircle" Height="20" Width="20" Foreground="{StaticResource PrimaryHueLightBrush}"/>
                                        </Button.Content>
                                    </Button>

                                </Grid>
                                <TextBlock Text="{Binding Message}"
                                           TextWrapping="Wrap"
                                           MaxWidth="300"
                                           Margin="2,4,2,4"/>
                                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                                    <Button Content="Close" Padding="8,2" Height="Auto" Click="CloseButton_Click"
                                            Margin="2"
                                            Style="{StaticResource MaterialDesignFlatButtonInverted}"/>
                                    <Button Content="Never show again"
                                            Margin="2"
                                            Padding="8,2"
                                            Height="Auto"
                                            Click="NeverShowButton_Click"
                                            Style="{StaticResource MaterialDesignFlatButtonInverted}"/>
                                </StackPanel>
                            </StackPanel>
                        </materialDesign:ColorZone>
                    </Grid>
                </Border>
            </Grid>
        </Popup>
    </Canvas>
</UserControl>

You can change this to style how you want. I used a custom rounded polygon class and MaterialDesign colour zones. Replace these however you want.

Now, the code behind is... well, there's a lot of it, and it's not pleasant:

public enum ActualPlacement { TopLeft, TopRight, BottomLeft, BottomRight }

/// <summary>
/// Interaction logic for HelpPopup.xaml
/// </summary>
public partial class HelpPopup : UserControl, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private ActualPlacement actualPlacement = ActualPlacement.TopRight;
    public ActualPlacement ActualPlacement
    {
        get { return actualPlacement; }
        internal set
        {
            if (actualPlacement != value)
            {
                if (actualPlacement == ActualPlacement.BottomLeft || ActualPlacement == ActualPlacement.BottomRight)
                {
                    Console.WriteLine("-10");
                    VerticalOffset = 10;
                }
                else if (actualPlacement == ActualPlacement.TopLeft || ActualPlacement == ActualPlacement.TopRight)
                {
                    VerticalOffset = -10;
                    Console.WriteLine("10");
                }

                actualPlacement = value;
                UpdateTailPath();
                NotifyOfPropertyChange("ActualPlacement");

            }
        }
    }

    public void UpdateTailPath()
    {
        double height = PopupChild.ActualHeight + 30;
        double width = PopupChild.ActualWidth;

        switch (actualPlacement)
        {
            case ActualPlacement.TopRight:
                polygonPath = "0.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + (height - 15.5) +
                              " 15.5," + (height - 15.5) + " 0.5," + height + " 0.5,15.5"; ;
                break;
            case ActualPlacement.TopLeft:
                polygonPath = "0.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + height + " " + (width - 15.5) + "," + (height - 15.5) +
                              " 0.5," + (height - 15.5) + " 0.5,15.5";
                break;
            case ActualPlacement.BottomRight:
                polygonPath = "0.5,0.5 15.5,15.5 " + (width - 0.5) + ",15.5 " + (width - 0.5) + "," + (height - 15.5) +
                              " 0.5," + (height - 15.5) + " 0.5,0.5";
                break;
            case ActualPlacement.BottomLeft:
                polygonPath = "0.5,15.5 " + (width - 15.5) + ",15.5 " + (width - 0.5) + ",0.5 " + (width - 0.5) + "," + (height - 15.5) +
                              " 0.5," + (height - 15.5) + " 0.5,15.5";
                break;
        }
        NotifyOfPropertyChange("PolygonPath");
    }

    private String polygonPath;
    public String PolygonPath
    {
        get { return polygonPath; }
    }

    public PlacementMode Placement
    {
        get { return (PlacementMode)GetValue(PlacementProperty); }
        set { SetValue(PlacementProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Placement.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty PlacementProperty =
        DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(HelpPopup), new PropertyMetadata(PlacementMode.Top));

    public int VerticalOffset
    {
        get { return (int)GetValue(VerticalOffsetProperty); }
        set { SetValue(VerticalOffsetProperty, value); }
    }

    // Using a DependencyProperty as the backing store for VerticalOffset.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty VerticalOffsetProperty =
        DependencyProperty.Register("VerticalOffset", typeof(int), typeof(HelpPopup), new PropertyMetadata(-10));

    public HelpTip HelpTip
    {
        get { return (HelpTip)GetValue(HelpTipProperty); }
        set { SetValue(HelpTipProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Message.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HelpTipProperty =
        DependencyProperty.Register("HelpTip", typeof(HelpTip), typeof(HelpPopup), new PropertyMetadata(new HelpTip() { Message = "No help message found..." }, HelpTipChanged));

    private static void HelpTipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if ((d as HelpPopup).HelpTipOnScreenInstance == null)
        {
            (d as HelpPopup).HelpTipOnScreenInstance = new HelpTipOnScreenInstance((d as HelpPopup));
        }
        (d as HelpPopup).HelpTipOnScreenInstance.HelpTip = (e.NewValue as HelpTip);
    }

    private static void HelpTipOnScreenInstance_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        HelpTipOnScreenInstance htosi = sender as HelpTipOnScreenInstance;
        if (e.PropertyName.Equals(nameof(htosi.IsOpen)))
        {
            //open manually to avoid stupid COM errors
            if (htosi != null)
            {
                try
                {
                    htosi.HelpPopup.Popup.IsOpen = htosi.IsOpen;
                }
                catch (System.ComponentModel.Win32Exception ex)
                {
                    Canvas parent = htosi.HelpPopup.Popup.Parent as Canvas;
                    htosi.HelpPopup.Popup.IsOpen = false;
                    parent.Children.Remove(htosi.HelpPopup.Popup);
                    Application.Current.Dispatcher.BeginInvoke(new Action(() => {
                        htosi.HelpPopup.Popup.IsOpen = true;
                        parent.Children.Add(htosi.HelpPopup.Popup);
                        htosi.HelpPopup.UpdatePositions();
                    }), DispatcherPriority.SystemIdle);

                }
            }
        }
    }

    private HelpTipOnScreenInstance helpTipOnScreenInstance;
    public HelpTipOnScreenInstance HelpTipOnScreenInstance
    {
        get { return helpTipOnScreenInstance; }
        set
        {
            if (helpTipOnScreenInstance != value)
            {
                if (helpTipOnScreenInstance != null)
                {
                    HelpTipOnScreenInstance.PropertyChanged -= HelpTipOnScreenInstance_PropertyChanged;
                }
                helpTipOnScreenInstance = value;
                HelpTipOnScreenInstance.PropertyChanged += HelpTipOnScreenInstance_PropertyChanged;
                NotifyOfPropertyChange("HelpTipOnScreenInstance");
            }
        }
    }

    private double popupX;
    public double PopupX
    {
        get { return popupX; }
        set
        {
            if (popupX != value)
            {
                popupX = value;
                NotifyOfPropertyChange("PopupX");
            }
        }
    }

    private double popupY;
    public double PopupY
    {
        get { return popupY; }
        set
        {
            if (popupY != value)
            {
                popupY = value;
                NotifyOfPropertyChange("PopupY");
            }
        }
    }

    private void NotifyOfPropertyChange(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public HelpPopup()
    {
        InitializeComponent();

        // Wire up the Loaded handler instead
        this.Loaded += new RoutedEventHandler(View1_Loaded);
        this.Unloaded += HelpPopup_Unloaded;

        Popup.Opened += Popup_Opened;

        //PopupChild.LayoutUpdated += HelpPopup_LayoutUpdated;
        PopupChild.SizeChanged += HelpPopup_SizeChanged;
        UpdatePositions();
    }

    private void Popup_Opened(object sender, EventArgs e)
    {
        UpdateTail();
        UpdateTailPath();
    }

    private void HelpPopup_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        Console.WriteLine(HelpTip.Message + ": " + e.PreviousSize.ToString() + " to " + e.NewSize.ToString());
        UpdateTail();
        UpdateTailPath();
    }

    private void HelpPopup_Unloaded(object sender, RoutedEventArgs e)
    {
        //don't waste resources on never show popups
        if (HelpTip.NeverShow)
        {
            return;
        }
        HelpTipOnScreenInstance.IsOnscreen = false;
    }

    /// Provides a way to "dock" the Popup control to the Window
    ///  so that the popup "sticks" to the window while the window is dragged around.
    void View1_Loaded(object sender, RoutedEventArgs e)
    {
        //don't waste resources on never show popups
        if (HelpTip.NeverShow)
        {
            return;
        }

        //wait for a few seconds, then set this to on-screen
        HelpTipOnScreenInstance.IsOnscreen = true;

        //update so tail is facing right direction
        UpdateTail();

        Window w = Window.GetWindow(this);
        // w should not be Null now!
        if (null != w)
        {
            w.LocationChanged += delegate (object sender2, EventArgs args)
            {
                // "bump" the offset to cause the popup to reposition itself
                //   on its own
                UpdatePositions();
            };
            // Also handle the window being resized (so the popup's position stays
            //  relative to its target element if the target element moves upon 
            //  window resize)
            w.SizeChanged += delegate (object sender3, SizeChangedEventArgs e2)
            {
                UpdatePositions();
            };
        }
    }

    private void UpdatePositions()
    {
        var offset = Popup.HorizontalOffset;
        Popup.HorizontalOffset = offset + 1;
        Popup.HorizontalOffset = offset;

        UpdateTail();
    }

    private void UpdateTail()
    {
        UIElement container = VisualTreeHelper.GetParent(this) as UIElement;
        Point relativeLocation = PopupChild.TranslatePoint(new Point(5, 5), container); //It HAS(!!!) to be this.Child

        if (relativeLocation.Y < 0)
        {
            if (relativeLocation.X < -(PopupChild.ActualWidth-5 / 2))
            {
                ActualPlacement = ActualPlacement.TopLeft;
            }
            else
            {
                ActualPlacement = ActualPlacement.TopRight;
            }
        }
        else
        {
            if (relativeLocation.X < -(PopupChild.ActualWidth-5 / 2))
            {
                ActualPlacement = ActualPlacement.BottomLeft;
            }
            else
            {
                ActualPlacement = ActualPlacement.BottomRight;
            }
        }
    }

    private void CloseButton_Click(object sender, RoutedEventArgs e)
    {
        lock (HelpTip.Lock)
        {
            HelpTip.Closed = true;
            HelpTipOnScreenInstance.IsOpen = false;
        }
    }

    private void NeverShowButton_Click(object sender, RoutedEventArgs e)
    {
        lock (HelpTip.Lock)
        {
            HelpTip.Closed = true;
            HelpTip.NeverShow = true;
            HelpTipOnScreenInstance.IsOpen = false;
        }
    }
}

Things to note.

  • There's "ActualPlacement" to manage the actual placement of the popup, as setting placement is just an advisory to WPF.

  • UpdateTailPath() is re-drawing the polygon to get the tail in the correct position after placement has changed.

  • We have both a HelpTip class that stores the information (title, content etc), and HelpTipOnScreenInstance which controls whether it's on screen. The reason for this is we can have multiple help-tip of the same type on screen and only want to display one.

  • Various listeners for popup events to trigger tail updates.

  • We attach to the load and unload events of the usercontrol. This allows us to keep track of whether the control is on the screen and whether a help tip should or should not be displayed (HelpTipOnScreenInstance.IsOnscreen = true).

  • We also listen for window change events so we can update the position of the pop-up if the window is resized or moved.

Now, HelpTipOnScreenInstance, and HelpTip:

public class HelpTipOnScreenInstance : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public object Lock = new Object();

    private void NotifyOfPropertyChange(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            //handler(this, new PropertyChangedEventArgs(propertyName));
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private HelpTip helpTip;
    public HelpTip HelpTip
    {
        get { return helpTip; }
        set
        {
            if (helpTip != value)
            {
                helpTip = value;
                NotifyOfPropertyChange("HelpTip");
            }
        }
    }

    private bool isOpen = false;
    public bool IsOpen
    {
        get { return isOpen; }
        set
        {
            if (isOpen != value)
            {
                isOpen = value;
                Console.WriteLine("Opening " + HelpTip.Message);
                NotifyOfPropertyChange("IsOpen");
            }
        }
    }

    private bool isOnscreen = false;
    public bool IsOnscreen
    {
        get { return isOnscreen; }
        set
        {
            if (isOnscreen != value)
            {
                isOnscreen = value;
                NotifyOfPropertyChange("IsOnscreen");
            }
        }
    }

    private HelpPopup helpPopup;
    public HelpPopup HelpPopup
    {
        get { return helpPopup; }
        set
        {
            if (helpPopup != value)
            {
                helpPopup = value;
                NotifyOfPropertyChange("HelpPopup");
            }
        }
    }

    public HelpTipOnScreenInstance(HelpPopup helpPopup)
    {
        HelpPopup = helpPopup;
        HelpTipManager.AddHelpTip(this);
    }
}

public class HelpTip : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public object Lock = new Object();

    private void NotifyOfPropertyChange(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            //handler(this, new PropertyChangedEventArgs(propertyName));
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private String id;
    public String ID
    {
        get { return id; }
        set { id = value; }
    }

    private String message;
    public String Message
    {
        get { return message; }
        set
        {
            if (message != value)
            {
                message = value;
                NotifyOfPropertyChange("Message");
            }
        }
    }

    private bool closed;
    public bool Closed
    {
        get { return closed; }
        set
        {
            if (closed != value)
            {
                closed = value;
                NotifyOfPropertyChange("Closed");
            }
        }
    }

    public bool NeverShow { get; set; }
}

And then a static manager class that keeps track of what's on screen and what's not, and chooses who get's displayed next:

public static class HelpTipManager
{
    public static object Lock = new Object();

    private static bool displayHelpTips = false;
    public static bool DisplayHelpTips
    {
        get { return displayHelpTips; }
        set {
            if (displayHelpTips != value)
            {
                displayHelpTips = value;

                if (displayHelpTips)
                {
                    //open next!
                    OpenNext();
                }
                else
                {
                    //stop displaying all
                    foreach(HelpTipOnScreenInstance helpTip in helpTipsOnScreen)
                    {
                        lock (helpTip.HelpTip.Lock)
                        {
                            helpTip.IsOpen = false;
                        }
                    }
                }
            }
        }
    }

    private static List<HelpTipOnScreenInstance> helpTips = new List<HelpTipOnScreenInstance>();
    private static List<HelpTipOnScreenInstance> helpTipsOnScreen = new List<HelpTipOnScreenInstance>();
    private static bool supressOpenNext = false;

    public static void AddHelpTip(HelpTipOnScreenInstance helpTip)
    {
        helpTip.PropertyChanged += HelpTip_PropertyChanged;
        helpTips.Add(helpTip);
    }

    private static void HelpTip_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        HelpTipOnScreenInstance helpTip = sender as HelpTipOnScreenInstance;
        if (helpTip != null)
        {
            //is this on screen or not?
            switch (e.PropertyName)
            {
                case "IsOnscreen":
                    //Update our onscreen lists and perform related behaviour
                    if (helpTip.IsOnscreen)
                    {
                        AddedToScreen(helpTip);
                    }
                    else
                    {
                        RemovedFromScreen(helpTip);
                    }
                    break;
                case "IsOpen":
                    lock (helpTip.Lock)
                    {
                        if (!supressOpenNext)
                        {
                            if (!helpTip.IsOpen)
                            {
                                OpenNext();
                            }
                        }
                    }
                    break;
            }
        }
    }

    private static void OpenNext()
    {
        if (DisplayHelpTips)
        {
            if (helpTipsOnScreen.Count > 0)
            {
                //check if none of them are open
                if (helpTipsOnScreen.Count(ht => ht.IsOpen) == 0)
                {
                    //open the first that's not been closed!
                    HelpTipOnScreenInstance firstNotClosed = helpTipsOnScreen.FirstOrDefault(ht => !ht.HelpTip.Closed);
                    if (firstNotClosed != null)
                    {
                        lock (firstNotClosed.Lock)
                        {
                            firstNotClosed.IsOpen = true;
                        }
                    }
                }
            }
        }
    }

    private static void AddedToScreen(HelpTipOnScreenInstance helpTip)
    {
        lock (Lock)
        {
            helpTipsOnScreen.Add(helpTip);
            OpenNext();
        }
    }

    private static void RemovedFromScreen(HelpTipOnScreenInstance helpTip)
    {
        lock (Lock)
        {
            helpTipsOnScreen.Remove(helpTip);
            supressOpenNext = true;
            helpTip.IsOpen = false;
            //OpenNext();
            supressOpenNext = false;
        }
    }
}

So how to use it? You can add help tip data in your generic.xaml or a resource library like this:

<controls:HelpTip x:Key="KPIGraphMenu" ID="KPIGraphMenu" Message="Right click to change the colour, remove, or move KPI to view as a stacked trace. KPI can also be dragged onto other charts of any type."/>

and use them in the actual application like this, I like to overlay them in a grid with the control they're associated with, using the Alignment to determine where the tail points to:

<controls:HelpPopup HelpTip="{StaticResource KPIGraphMenu}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
like image 184
Joe Avatar answered Sep 28 '22 02:09

Joe


I have used CustomPopupPlacementCallback Delegate . I have even considered vertical shifting of your arrow. So, now in this example below, arrow shifts left/right, up/down.

One can use this sample as it is.

Window1.xaml

<Window ...>

    <Grid>

        <Button Click="Btn_Click" Width="110" Height="25" Content="Button" HorizontalAlignment="Left" Margin="437,26,0,0" VerticalAlignment="Top"/>
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="10,90,0,0" VerticalAlignment="Top" Width="75"/>        
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="139,146,0,0" VerticalAlignment="Top" Width="75"/>
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="180,0,0,0" VerticalAlignment="Top" Width="74"/>
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" Margin="224,333,0,0" VerticalAlignment="Top" Width="76"/>
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Right" VerticalAlignment="Top" Width="75"/>
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="75" />
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="75" />
        <Button Click="Btn_Click" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" />

        <Popup x:Name="Popup1"  Placement="Custom" StaysOpen="False" Opened="Popup1_Opened">
            <Grid x:Name="Grd" Width="300" Height="100" Background="AliceBlue">
                <Canvas x:Name="Cnv">
                    <Path x:Name="TopArrow" Canvas.Left="50" Canvas.Top="25" Margin="5" Data="M0,0 L-5,-5 L-10,0 z" Fill="Black" Stroke="Black" StrokeThickness="2"/>
                    <TextBlock Canvas.Top="35" FontSize="18" x:Name="Tb1"/>
                </Canvas>
            </Grid>
        </Popup>

    </Grid>

</Window>

Window1.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Shapes;

namespace ...
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            Popup1.CustomPopupPlacementCallback =
                new CustomPopupPlacementCallback(placePopup);
        }

        public CustomPopupPlacement[] placePopup(Size popupSize,
                                           Size targetSize,
                                           Point offset)
        {
            CustomPopupPlacement placement2 =
               new CustomPopupPlacement(new Point(-(popupSize.Width - targetSize.Width / 2), targetSize.Height), PopupPrimaryAxis.Vertical);

            CustomPopupPlacement placement1 =
               new CustomPopupPlacement(new Point(targetSize.Width / 2, targetSize.Height), PopupPrimaryAxis.Vertical);

            CustomPopupPlacement placement3 =
               new CustomPopupPlacement(new Point(targetSize.Width/2, -popupSize.Height), PopupPrimaryAxis.Horizontal);

            CustomPopupPlacement placement4 =
               new CustomPopupPlacement(new Point(-(popupSize.Width - targetSize.Width/2), -popupSize.Height), PopupPrimaryAxis.Horizontal);

            CustomPopupPlacement[] ttplaces =
                    new CustomPopupPlacement[] { placement1, placement2, placement3, placement4 };

            return ttplaces;
        }

        private void Btn_Click(object sender, RoutedEventArgs e)
        {
            Popup1.PlacementTarget = sender as Button;
            Popup1.IsOpen = true;
        }

        private void Popup1_Opened(object sender, EventArgs e)
        {
            Path arrow = ((Path)Popup1.FindName("TopArrow"));

            Grid grd = ((Grid)Popup1.FindName("Grd"));
            UIElement elem = (UIElement)Popup1.PlacementTarget;

            Point elem_pos_lefttop = elem.PointToScreen(new Point(0, 0));
            Point popup_pos_lefttop = grd.PointToScreen(new Point(0, 0));

            if (    (elem_pos_lefttop.Y < popup_pos_lefttop.Y )
                    &&
                    ((elem_pos_lefttop.X > popup_pos_lefttop.X))
                )
            {
                    Canvas.SetLeft(arrow, 280);
                    Canvas.SetTop(arrow, 25);
            }
            if ((elem_pos_lefttop.Y < popup_pos_lefttop.Y)
                    &&
                    ((elem_pos_lefttop.X < popup_pos_lefttop.X))
                )
            {
                Canvas.SetLeft(arrow, 30);
                Canvas.SetTop(arrow, 25);
            }
            if ((elem_pos_lefttop.Y > popup_pos_lefttop.Y)
                    &&
                    ((elem_pos_lefttop.X > popup_pos_lefttop.X))
                )
            {
                Canvas.SetLeft(arrow, 280);
                Canvas.SetTop(arrow, 90);
            }
            if ((elem_pos_lefttop.Y > popup_pos_lefttop.Y)
                    &&
                    ((elem_pos_lefttop.X < popup_pos_lefttop.X))
                )
            {
                Canvas.SetLeft(arrow, 30);
                Canvas.SetTop(arrow, 90);
            }

            Tb1.Text = String.Format("Element = {0} \r\n Popup = {1}", elem_pos_lefttop, popup_pos_lefttop);
        }
    }
}

Please tell if this solves your issue.

like image 42
AnjumSKhan Avatar answered Sep 28 '22 00:09

AnjumSKhan