WPF弹出箭头样式

时间:2016-09-30 07:47:44

标签: wpf xaml popup styles

我想实现一个类似于以下设计的弹出式样式:

enter image description here

灰色方块表示UIElement,显示点击时的弹出窗口。弹出样式只是一个边框(简单部分),箭头指向目标元素的中心(硬部分)。此外,对齐很重要,当控件放在窗口的右侧时,弹出窗口应该向右对齐,否则向左对齐。

是否有示例或某些文档指示我如何继续?

2 个答案:

答案 0 :(得分:4)

好的,我有一个解决方案。这令人沮丧地复杂。

如果您只是在一个简单的弹出窗口之后,只需要一个尾部,您可以使用这个块(ActualLayout和UpdateTail逻辑)。如果您在整个Help-Tip Experience™之后,您将会遇到不愉快的旅程。

我确实认为沿着Adorner路线走下去可能会更好(而且我正在考虑重新使用它来使用崇拜者)。我发现了一些问题,而且还在工作中。使用弹出窗口会使它们出现在其他窗口顶部的设计器中,这真的很烦人。我还注意到,由于某些奇怪的原因,它们在某些计算机上的位置不正确(但是我没有安装Visual Studio以进行正确调试)。

它产生这样的东西:

enter image description here

符合以下条件:

  • 每次只能在屏幕上显示一个帮助提示

  • 如果用户更改了标签,并且附加了帮助提示的控件不再可见,则帮助提示会消失,并显示下一个帮助提示

  • 关闭后,该类型的帮助提示不会再次显示

  • 可以通过一个中心选项

  • 关闭帮助提示

好。因此,实际的帮助提示是一个完全透明的用户控件,并添加到UI中。它有一个弹出窗口,使用静态类进行管理。这是控件:

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

您可以根据需要更改此样式。我使用了自定义的圆角多边形类和MaterialDesign颜色区域。根据需要更换这些。

现在,背后的代码是......嗯,有很多,而且不愉快:

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

注意事项。

  • 有&#34; ActualPlacement&#34;管理弹出窗口的实际位置,因为设置位置只是WPF的建议。

  • UpdateTailPath()正在重新绘制多边形以获取尾部 放置后的正确位置已经改变。

  • 我们有一个存储信息的HelpTip类(标题, 内容等)和HelpTipOnScreenInstance控制它是否是 在屏幕上。这样做的原因是我们可以有多个帮助提示 屏幕上的相同类型,只想显示一个。

  • 弹出事件的各种侦听器,用于触发尾部更新。

  • 我们附加到usercontrol的加载和卸载事件。这个 允许我们跟踪控件是否在屏幕上 是否应该显示帮助提示 (HelpTipOnScreenInstance.IsOnscreen = true)。

  • 我们还会收听窗口更改事件,以便我们更新位置 如果调整窗口大小或移动窗口,则弹出窗口。

现在,HelpTipOnScreenInstance和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; }
}

然后是一个静态管理员课程,用于跟踪屏幕上的内容以及未显示的内容,并选择接下来显示的内容:

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

那么如何使用呢?您可以在generic.xaml或资源库中添加帮助提示数据,如下所示:

<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."/>

并在这样的实际应用程序中使用它们,我喜欢将它们与它们关联的控件叠加在一个网格中,使用Alignment确定尾部指向的位置:

<controls:HelpPopup HelpTip="{StaticResource KPIGraphMenu}" HorizontalAlignment="Center" VerticalAlignment="Center"/>

答案 1 :(得分:3)

我使用过CustomPopupPlacementCallback Delegate。我甚至考虑过你的箭头垂直移动。因此,现在在下面的示例中,箭头向左/向右,向上/向下移动。

可以按原样使用此样本。

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);
        }
    }
}

请告知这是否解决了您的问题。