This article is republished as an original work with the author's permission. Feel free to share.
Original author: 眾尋
Original link: https://www.cnblogs.com/ZXdeveloper/p/8391864.html
I had some free time in the past couple of days, so I created a simple new user guidance demo. Since it’s not for a real project, it’s quite rough—just intended to give those in need an idea.
The new user guidance feature tells users the order of operations on the page: first step, second step, and so on, until finally closing the guidance window.
As usual, let me show you the effect first.

The effect is very simple: it just highlights the controls that users need to interact with.
To implement this functionality, the general approach involves the following steps:
1. Mask Window
Overlay the main window with a semi-transparent effect. Common masking methods usually set a background color with transparency, similar to the blog post WPF Transparent Form Creation. However, in practice, issues arise. If a normal semi-transparent method is used, the yellow box area cannot show through the white background of the main window because the background color is already there. Therefore, this article uses a clipping method for semi-transparency, as shown in the following image. The reference blog is WPF Mask Effect Using Clip Property.

First, create a transparent window:
<Window x:Class="SimpleGuide.GuideWin" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SimpleGuide" mc:Ignorable="d" Title="GuideWin" WindowStyle="None" AllowsTransparency="True" x:Name="gw" Background="#01FFFFFF" ShowInTaskbar="False">
<Grid>
<Border x:Name="bor" BorderBrush="White" BorderThickness="2" CornerRadius="5" Opacity="0.8">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" Color="#FF414141" BlurRadius="8" />
</Border.Effect>
<Border Background="Black" Opacity="0.5" Margin="0" CornerRadius="5" />
</Border>
<Canvas x:Name="can"></Canvas>
</Grid>
</Window>
From the XAML code, you can see that the Background property is not set to "Transparent" but to "#01FFFFFF". Because if "Transparent" were used, it would be fully transparent, allowing clicks to pass through to the main window's controls, which we don’t want. So we set "#01FFFFFF", a nearly transparent color.
2. Display the Control to Operate
To guide the user to a specific control, we need to highlight it. The first task is to obtain the coordinates of the control relative to the current window.
Point point = fe.TransformToAncestor(Window.GetWindow(fe)).Transform(new Point(0, 0));
After obtaining the coordinates, we need to outline the control. My approach is to take the current coordinates minus 5, and the width and height plus 10, to create an empty area—essentially a clipping region.
RectangleGeometry rg1 = new RectangleGeometry();
rg1.Rect = new Rect(point.X - 5, point.Y - 5, fe.ActualWidth + 10, fe.ActualHeight + 10);
borGeometry = Geometry.Combine(borGeometry, rg1, GeometryCombineMode.Exclude, null);
3. Draw a Guidance User Control (UC)
Designing the guidance UC is straightforward—the appearance is simple:

It uses a Path to draw a region. Initially, I thought of using a Line for the dashed border, but that seemed too cumbersome. So I directly used the StrokeDashArray property. Stroke is the border line of the Path itself. However, since it's truly the border, setting Margin or Padding is inconvenient. The final approach was to draw an outer region without a border line but with the same fill color.
<Path Fill="#FF2FBEED">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 8,22 A 12,12 0 1 1 22,8 L 102 8 L 102 62 L 8 62 Z" />
</GeometryGroup>
</Path.Data>
</Path>
<Path StrokeThickness="1" Stroke="White" StrokeDashArray="2,1" Fill="#FF2FBEED">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M 10,20 A 10,10 0 1 1 20,10 L 100 10 L 100 60 L 10 60 Z" />
</GeometryGroup>
</Path.Data>
</Path>
The content area uses a TextBlock. One issue I encountered was line wrapping—the TextBlock needs an explicit Width to wrap text. Since the outermost container is a Viewbox, trying to get the UC's Width or ActualWidth didn't work. So the final solution was to pass the window's width and height into the UC instead of setting its width and height externally.
public HintUC(string xh, string content, Visibility vis = Visibility.Visible, int width = 260, int height = 160)
{
InitializeComponent();
this.Width = width;
this.Height = height;
this.tb_nr.Width = width / 4;
this.tb_xh.Text = xh;
this.tb_nr.Text = content;
this.btn_next.Visibility = vis;
}
4. Triggering the Next Step
Triggering the next step essentially involves the child control calling an event on the main control. This is done by writing a delegate and implementing the specific method in the main window.
private void show(int xh, FrameworkElement fe, string con, Visibility vis = Visibility.Visible)
{
Point point = fe.TransformToAncestor(Window.GetWindow(fe)).Transform(new Point(0, 0)); // Get control coordinates
RectangleGeometry rg = new RectangleGeometry();
rg.Rect = new Rect(0, 0, this.Width, this.Height);
borGeometry = Geometry.Combine(borGeometry, rg, GeometryCombineMode.Union, null);
bor.Clip = borGeometry;
RectangleGeometry rg1 = new RectangleGeometry();
rg1.Rect = new Rect(point.X - 5, point.Y - 5, fe.ActualWidth + 10, fe.ActualHeight + 10);
borGeometry = Geometry.Combine(borGeometry, rg1, GeometryCombineMode.Exclude, null);
bor.Clip = borGeometry;
HintUC hit = new HintUC(xh.ToString(), con, vis);
Canvas.SetLeft(hit, point.X + fe.ActualWidth + 3);
Canvas.SetTop(hit, point.Y + fe.ActualHeight + 3);
hit.nextHintEvent -= Hit_nextHintEvent;
hit.nextHintEvent += Hit_nextHintEvent;
can.Children.Add(hit);
index++;
}
private void Hit_nextHintEvent()
{
if (list[index - 1] != null)
{
can.Children.Clear();
}
if (index == list.Count - 1)
show(index + 1, list[index].Uc, list[index].Content, Visibility.Collapsed);
else
show(index + 1, list[index].Uc, list[index].Content);
}
We need to declare an external index variable to track the current position in the List. First, check if the current content is not null; if so, clear it. If not cleared, you'll see a pile of tooltips. Then, determine whether it's the last control in the List. If it is, hide the "Next" button.
5. Extensions
Since this is a small demo, I noticed some issues but didn't solve them, such as coordinate positioning problems when the main window is not borderless.
This happens because the guidance window captures the main window's size, but when Point obtains the control's coordinates, the main window does not include its title bar. Since the mask lacks the title bar area, the positioning is off. I haven't found a good solution yet. If any expert knows how to fix this, please advise. Thank you.

The guidance content area could also be replaced by a Grid, allowing you to pass in a UserControl. Interested readers can modify it accordingly.
Source code: Demo
Webmaster's Experience
The effect is quite good. The webmaster modified the source code slightly (code). Replacing the target controls with Image controls also works fine—nice:
