我想实现一个类似于以下设计的弹出样式:
灰色方块代表 UIElement
单击时显示弹出窗口。弹出样式只是一个边框(简单的部分),带有一个箭头指向目标元素的中心(难的部分)。对齐也很重要,当控件放在窗口的右侧时,弹出窗口应该向右对齐,否则向左对齐。
是否有示例或一些文档指导我如何进行?
最佳答案
好的,我有一个解决方案。这是令人沮丧的复杂。
如果你只是在一个简单的弹出窗口之后,只是有一个尾部,你可能可以使用它的块(ActualLayout 和 UpdateTail 逻辑)。如果您想体验整个 Help-Tip Experience™,那么您将经历一段不愉快的旅程。
我确实认为沿着 Adorner 路线走可能会更好(我正在考虑重新设计它以使用 adorers)。我发现了一些问题,它仍在工作中。使用弹出窗口会使它们出现在设计器中其他窗口的顶部,这真的很烦人。我还注意到由于一些奇怪的原因,它们在某些计算机上的位置不正确(但我没有安装 Visual Studio 以正确调试)。
它产生这样的东西:
符合以下标准:
好的。因此,实际的帮助提示是一个完全透明并添加到 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;
}
}
}
注意事项。
放置更改后的正确位置。
内容等),以及 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/