一、概要
本篇文章分享一个新手界面提示的案例,我们经常会在各种app中会遇到不断让你点下一步引导你使用客户端的提示,根据不同的参数配置显示不同提示气泡的样式。这里就分享一下在WPF中如何去实现,我们先看下面的效果。
文章中只出现了部分关键代码全部代码在,源码地址在Github上。 https://github.com/JusterZhu/2021PlanJ
二、思路
通过上图显示的内容我做了以下分析:
开始之前我们先明白几个概念,不然思路不清楚后面的事情很难做。
- 目标控件 - 指的是我们需要解释提示的控件
- 气泡 - 具体的提示内容,同时支持下一步
- 线 - 将气泡和目标控件连接起来,达到视觉辅助
- 位置、样式 - 通过简单算法计算出目标控件和提示气泡的位置并用线连接起来,且初始化线和气泡的样式。
1.半透明灰的遮罩层
<Window
x:Name="gw"
Title="GuideWin"
AllowsTransparency="True"
Background="#01FFFFFF"
ShowInTaskbar="False"
WindowStyle="None"
mc:Ignorable="d">
<Grid>
<Border
x:Name="border"
BorderBrush="White"
BorderThickness="2"
CornerRadius="5"
Opacity="0.8">
<Border.Effect>
<DropShadowEffect
BlurRadius="8"
ShadowDepth="0"
Color="#FF414141" />
</Border.Effect>
<Border
Margin="0"
Background="Black"
CornerRadius="5"
Opacity="0.5" />
</Border>
<Canvas x:Name="canvas" />
</Grid>
</Window>
这里会定义一个window将它的样式设置透明等样式便于覆盖到其他窗体上,遮罩层使用的是border控件,为什么选择它因为有两个特性。
- 第一个是因为它支持圆角和方角可以适应不同的窗体样式。
- 第二个它支持阴影效果。
2.提示气泡
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
Background="Transparent"
mc:Ignorable="d">
<Viewbox MouseLeftButtonDown="Viewbox_MouseLeftButtonDown">
<Grid>
<Border
x:Name="RootBrd"
Background="Transparent"
BorderThickness="1">
<TextBlock
x:Name="ContentTxb"
Margin="3"
Background="Transparent" />
</Border>
</Grid>
</Viewbox>
</UserControl>
这里会定义一个UserControl,这个UserControl是用来当做气泡显示具体内容的,也方便"嵌入"到窗体中使用。后面如果需要一整套提示流程时创建一个集合装好这些一步步初始化好的提示气泡的样式和内容即可。
3.线和气泡出现的位置和样式
public void StartGuide(GuideModel guide)
{
var focusElemt = guide.UserControl;
var point = focusElemt.TransformToAncestor(Window.GetWindow(focusElemt)).Transform(new Point(0, 0));
var rectangleGeometry = new RectangleGeometry();
rectangleGeometry.Rect = new Rect(0, 0, this.Width, this.Height);
borderGeometry = Geometry.Combine(borderGeometry, rectangleGeometry, GeometryCombineMode.Union, null);
border.Clip = borderGeometry;
var rectangleGeometry1 = new RectangleGeometry();
rectangleGeometry1.Rect = new Rect(point.X - 5, point.Y - 5, focusElemt.ActualWidth + 10, focusElemt.ActualHeight + 10);
borderGeometry = Geometry.Combine(borderGeometry, rectangleGeometry1, GeometryCombineMode.Exclude, null);
border.Clip = borderGeometry;
InitHintControl(guide, point, focusElemt.ActualWidth, focusElemt.ActualHeight, guide.IsNear);
currentStep++;
}
画线的思路:线有两个端点,我在这里称为线的起始点和线的终点,线的起始点出现的位置通常是“目标控件”的高的二分之一处,所以通过TransformToAncestor方法拿到控件坐标之后从左上角的0,0点的位置在带入控件的宽高计算出线起始点的坐标。
/// <summary>
/// 计算相对位置
/// </summary>
/// <param name="startPoint">已知起始点坐标</param>
/// <param name="angle">相对位置角度</param>
/// <param name="bevel">相对距离</param>
/// <returns></returns>
private Point CalculateRelativePoint(Point startPoint, int angle, double bevel)
{
var radian = angle * Math.PI / 180;
var xMargin = Math.Cos(radian) * bevel;
var yMargin = Math.Sin(radian) * bevel;
return new Point(startPoint.X + xMargin, startPoint.Y + yMargin);
}
气泡出现的位置实现思路: 1.知道起始位置,2.知道相对位置的角度,3.知道相对距离,4.计算出相对位置
公式为:
- 相对半径 =(角度 * π / 180);
- 相对位置X轴坐标 = Cos(相对半径)* 相对距离;
- 相对位置Y轴坐标 = Sin(相对半径)* 相对距离;
最终得出X和Y的值,从而确认到气泡的初始位置和线的结束位置。
/// <summary>
/// 初始化气泡内容及位置
/// </summary>
/// <param name="guide"></param>
/// <param name="point"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="isNear"></param>
private void InitHintControl(GuideModel guide, Point point, double width, double height, bool isNear)
{
hintWin = new Hint(guide);
hintWin.NextHintEvent -= OnNextHintEvent;
hintWin.NextHintEvent += OnNextHintEvent;
canvas.Children.Add(hintWin);
var startPoint = new Point { X = point.X + width + 5, Y = point.Y + height / 2 };
var relativePoint = CalculateRelativePoint(startPoint, 45 + guide.Angle, 100);
if (isNear)
{
var nearRelativePoint = CalculateRelativePoint(point, 45 + guide.Angle, 1);
Canvas.SetLeft(hintWin, nearRelativePoint.X);
Canvas.SetTop(hintWin, nearRelativePoint.Y);
}
else
{
Canvas.SetLeft(hintWin, relativePoint.X - 5);
Canvas.SetTop(hintWin, relativePoint.Y - hintWin.Height / 2);
}
if (guide.Direct == Direct.Visible)
{
InitPath(startPoint, relativePoint, 3, guide.PathColor, guide.BulgeStyle);
}
}
/// <summary>
/// 初始化连接气泡的线样式及位置
/// </summary>
/// <param name="startPoint"></param>
/// <param name="relativePoint"></param>
/// <param name="pathThickness"></param>
/// <param name="pathColor"></param>
/// <param name="bulgeStyle"></param>
private void InitPath(Point startPoint, Point relativePoint,
int pathThickness = 1, string pathColor = "#000000", BulgeStyle bulgeStyle = BulgeStyle.Dotted)
{
var path = new Path();
var pathGeometry = new PathGeometry();
var pathFigure = new PathFigure();
pathFigure.StartPoint = startPoint;
var segmentCollection = new PathSegmentCollection();
segmentCollection.Add(new LineSegment() { Point = relativePoint });
pathFigure.Segments = segmentCollection;
pathGeometry.Figures = new PathFigureCollection() { pathFigure };
path.Data = pathGeometry;
path.Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString(pathColor));
if (bulgeStyle == BulgeStyle.Dotted)
{
path.StrokeDashArray = new DoubleCollection { 2, 4 };
}
path.StrokeThickness = pathThickness;
canvas.Children.Add(path);
}
4.下一步功能
下一步这个功能大致的思路就是,不断的从“步骤集合”中取出下一个元素。根据对应的model初始化好下一步提示气泡里的内容即可,当走到最后一个元素时通常会是最后一步那么直接关闭掉“遮罩层窗体”即可。
/// <summary>
/// 下一步具体实现
/// </summary>
private void OnNextHintEvent()
{
if (_guideModels.Count == 0) return;
var beforGuide = _guideModels[currentStep - 1];
if (_guideModels.Count == currentStep)
{
if (beforGuide.Callback != null)
{
beforGuide.Callback.Invoke();
}
else
{
EndStep();
}
return;
}
var currentGuide = _guideModels[currentStep];
if (beforGuide != null)
{
canvas.Children.Clear();
if (beforGuide.Callback != null)
{
beforGuide.Callback.Invoke();
}
}
if (currentGuide != null)
{
StartGuide(currentGuide);
}
}
发布于刚刚