带箭头样式的WPF弹出窗口

8
我想实现一个弹窗样式,外观如下设计:

enter image description here

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

是否有示例或一些说明文件可以指导我如何操作?

2个回答

10

好的,我有一个解决方案。它非常复杂。

如果你只是想要一个带尾巴的简单弹出窗口,你可能可以使用其中的一部分(ActualLayout和UpdateTail逻辑)。如果你想要完整的帮助提示体验™,那么你将会经历一段不愉快的旅程。

我认为最好还是采用装饰器路线(我正在考虑重新设计这个来使用装饰器)。我发现了一些问题,仍在进行中。使用弹出窗口会导致它们出现在设计器的其他窗口上面,这真的很烦人。我还注意到它们在某些电脑上的位置不正确,原因很奇怪(但是我没有在安装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;
        }
    }
}

需要注意的事项:

  • 有一个 "ActualPlacement" 可以管理弹出窗口的实际位置,因为设置放置位置只是 WPF 的建议。

  • UpdateTailPath() 重新绘制多边形以便在位置发生变化后正确地放置尾巴。

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

  • 各种监听器用于弹出窗口事件以触发尾巴更新。

  • 我们附加到用户控件的加载和卸载事件。这使我们能够跟踪控件是否在屏幕上以及是否应该显示帮助提示 (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."/>

然后在实际应用中使用它们,我喜欢将它们与相关的控件叠加在网格中,使用对齐方式确定尾巴指向的位置:

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

7

我已经使用了CustomPopupPlacementCallback委托。我甚至考虑了箭头的垂直移动。所以,在下面的例子中,箭头可以左右、上下移动。

你可以按照现有样例来使用它。

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

请告诉我这是否解决了你的问题。

在为您提供的示例中,方法placePopup从未被调用。 - Daniel Peñalba
@DanielPeñalba 整个示例都运行得很好。请再次确认。 - AnjumSKhan
@DanielPeñalba 如果您希望,可以来聊天 - AnjumSKhan
我通过添加 Popup1.Placement = PlacementMode.Custom; 使其工作。 - Daniel Peñalba
@DanielPeñalba 感谢!我会更新并将其标记为答案。 - AnjumSKhan

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