本記事は原作者の許可を得てオリジナル二次配信として再掲載しています。転載、共有は自由です。
原文著者:眾尋
原文リンク:https://www.cnblogs.com/ZXdeveloper/p/8391864.html
このところ仕事がそれほど忙しくなかったので、簡易的な初心者向けチュートリアルガイドの小さなデモを作ってみました。プロジェクトで使うものではないので、かなり粗削りですが、必要な方に少しでもアイデアを提供できればと思います。
初心者ガイド機能とは、ユーザーに画面上の操作順序(最初に何をすべきか、次に何をすべきか、など)を伝え、最終的にガイドページを閉じるというものです。
私の癖ですが、まずは効果をご覧いただきましょう。

とてもシンプルな表示で、ユーザーに操作してほしいコントロールをハイライトして示しています。
この機能を実現するための考え方はおおよそ以下の通りです。
一、マスクウィンドウ
メインウィンドウに半透明のマスクをかけます。よく使われるマスク方法としては、背景色を設定し、透明度を設定する方法があります(例:ブログ「WPF 透明窗体制作」参照)。しかし実際に運用すると問題が発生します。通常の半透明方式を使うと、黄色い枠の部分から白いメインウィンドウの内容が透けて見えなくなります。背景色があるからです。そこで本記事では、Clip による消去(くり抜き)方式を使っています。効果は以下の画像を参照。参考ブログ:「WPF 用 Clip 属性实现蒙板特效」

まず透明なウィンドウを設定します。
<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>
XAML コードからわかるように、Background プロパティに Transparent ではなく #01FFFFFF を使っています。Transparent だと本当に透明になってしまい、メインウィンドウのコントロールを直接クリックできてしまいます。それを防ぐために、ほぼ透明な色である #01FFFFFF を設定しています。
二、操作対象のコントロールを表示する
あるコントロールをガイドするには、まずそのコントロールを枠で囲む必要があります。そのためにまず、コントロールの現在のウィンドウ内での座標位置を取得します。
Point point = fe.TransformToAncestor(Window.GetWindow(fe)).Transform(new Point(0, 0));
座標を取得したら、そのコントロールを枠で囲みます。私の方法は、現在の座標から-5 し、幅と高さに+10 した領域を空白として描画します。実質的には消去(くり抜き)の処理です。
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);
三、ガイド用のユーザーコントロール (UC) を作成する
ガイド用 UC のデザインは比較的簡単です。見た目はシンプルです。

Path を使って領域を描画していますが、破線枠は最初は Line で作ろうと思いましたが手間がかかるので、直接 StrokeDashArray プロパティを使いました。Stroke は Path 自体の境界線ですが、実際に境界線になってしまうため、Margin や Padding を設定しにくいです。そこで最終的には、外側に境界線を含まない別の領域を描画し、塗りつぶし色は同じにしています。
<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>
内容表示部分は TextBlock です。ここで改行の問題に遭遇しました。TextBlock は Width が設定されていないと改行されません。しかし最外層が Viewbox であるため、UC の Width や ActualWidth を取得してもうまくいきませんでした。そこで最終的な解決策として、UC の幅・高さを外部から渡すのではなく、ウィンドウの幅と高さをパラメータとして受け取るようにしました。
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;
}
四、次のステップへのトリガー
次のステップへのトリガーは、子コントロールからメインコントロールのイベントを呼び出す形になります。そのためにデリゲートを使い、メインウィンドウで具体的なメソッドを実装します。
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)); // コントロールの座標を取得
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);
}
外部で現在の List のインデックスを記録するための index 変数を宣言します。まず、現在の内容が空でないかを判定し、空でなければそれをクリアします。クリアしないと複数のヒントボックスが表示されてしまいます。そして、List の最後のコントロールかどうかを判定し、最後であれば「次へ」ボタンを非表示にします。
五、拡張部分
小さなデモなのでいくつか問題がありますが、解決はしていません。例えば、メインウィンドウがボーダーレスでない場合、座標の取得に問題が生じます。
これは、ガイドウィンドウがメインウィンドウのサイズを取得しても、Point でコントロールの座標を取得するときにメインウィンドウのヘッダー部分が含まれないためです。マスクにはヘッダーがないため、位置がずれてしまいます。これに対する良い解決策はまだ見つかっていません。もしご存知の方がいらっしゃいましたら、ご教授いただけると幸いです。

ガイド内容の表示部分を Grid に置き換えれば、UserControl を渡すことも可能です。ご興味があればご自身で修正してみてください。
ソースコード:Demo
サイト管理者の使用感
効果は確かに良いです。管理者は原作者のソースコードを少し修正しました(コード)。マスクが必要なコントロールを Image コントロールに置き換えても同じ効果が得られました。nice:
