带有箭头样式的 WPF 弹出窗口

标签 wpf xaml popup styles

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

enter image description here

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

是否有示例或一些文档指导我如何进行?

最佳答案

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

如果你只是在一个简单的弹出窗口之后,只是有一个尾部,你可能可以使用它的块(ActualLayout 和 UpdateTail 逻辑)。如果您想体验整个 Help-Tip Experience™,那么您将经历一段不愉快的旅程。

我确实认为沿着 Adorner 路线走可能会更好(我正在考虑重新设计它以使用 adorers)。我发现了一些问题,它仍在工作中。使用弹出窗口会使它们出现在设计器中其他窗口的顶部,这真的很烦人。我还注意到由于一些奇怪的原因,它们在某些计算机上的位置不正确(但我没有安装 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"/>
    

    关于带有箭头样式的 WPF 弹出窗口,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39786296/

    相关文章:

    jquery 弹出图像

    .net - 创建新 GUI 时,WPF 是不是 Windows 窗体的首选?

    c# - 根据MVVM模式对gridview进行排序

    WPF Windows 打开文件对话框打开时间过长

    c# - 如何在选择 TextBlock 中的文本的位置显示弹出窗口

    .net - 是否有 WPF XAML 用于统一拟合但恒定的 StrokeThickness

    html - Bootstrap 弹出窗口中的组按钮

    javascript - 如何使用javascript在弹出框中显示消息

    wpf - 如果 xaml 调用依赖属性的 SetValue(),则通知

    c# - 在 Window 的代码后面实现 IValueConverter