WPF Expression Blendの吹き出し図形で外枠線と塗りつぶしが重ならないようにする

吹き出しの先端がずれないように、枠線をつくるよ。

こんにちは!

先日リビングの照明をLEDに変えました。 天井がすっきりしてなんだかとってもハイソサエティーな気分で、 夕食にカップラーメンをほおばっているあらたまです!

さて、今日は WPFアプリケーションのExpression Blend吹き出し図形の枠線のお話です。

おさらい

前日のエントリー では、図形の枠線と塗りつぶしを重ならないようにする方法をご案内しました。

しかしその方法ではうまく対応できない図形がありました。 それはExpression Blendの吹き出し画像です。

図形のサイズを縮小してその差分で外枠線を作り出しているのですが、 吹き出しの場合、その方法だと先端がずれてしまします。

今回は吹き出し画像でこのずれを発生させないように、 外枠線と塗りつぶしが重ならないようにする方法をご案内します。

まずはStrokeで外枠線を引いてみる

まずはベーシックな方法で、StrokeとStrokeThicknessを使って外枠線を引いてみましょう。

<Window x:Class="BlogEntry85.MainWindow"
        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:exctrl="clr-namespace:Microsoft.Expression.Controls;assembly=Microsoft.Expression.Drawing"
        xmlns:local="clr-namespace:BlogEntry85"
        mc:Ignorable="d"
        Title="MainWindow" Height="377.168" Width="711.561">
    <StackPanel Orientation="Horizontal" Margin="15" VerticalAlignment="Top">
        <exctrl:Callout StrokeThickness="20"
                        Fill="Green"
                        Stroke="Orange"
                        Height="230" Width="230"
                        AnchorPoint="0.2,1.2"
                        />
    </StackPanel>
</Window>

下の画像のような、オレンジ色の外枠線が引かれましたね。

Strokeを半透明にしてみる

では外枠を半透明にしてみましょう。

<Window x:Class="BlogEntry85.MainWindow"
        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:exctrl="clr-namespace:Microsoft.Expression.Controls;assembly=Microsoft.Expression.Drawing"
        xmlns:local="clr-namespace:BlogEntry85"
        mc:Ignorable="d"
        Title="MainWindow" Height="377.168" Width="711.561">
    <StackPanel Orientation="Horizontal" Margin="15" VerticalAlignment="Top">
        <exctrl:Callout StrokeThickness="20"
                        Fill="Green"
                        Height="230" Width="230"
                        AnchorPoint="0.2,1.2"
                        >
            <exctrl:Callout.Stroke>
                <SolidColorBrush Color="Orange" Opacity="0.5" />
            </exctrl:Callout.Stroke>
        </exctrl:Callout>

    </StackPanel>
</Window>

下の画像のようになりました。

やはり他の図形などと同じように、オレンジ色の枠線と緑色の塗りつぶしが重なってしまっています。。

では、この重なりをなくす方法です。

ソースコード

まずは対処後のソースコードをお見せしますね。

ユーザーコントロールのXAML

<UserControl x:Class="BlogEntry85.OutlinedCallout"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:exctrl="clr-namespace:Microsoft.Expression.Controls;assembly=Microsoft.Expression.Drawing"
             xmlns:local="clr-namespace:BlogEntry85"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <!-- 外枠線用図形 -->
        <exctrl:Callout x:Name="CalloutStroke"
                        StrokeThickness="40"
                        AnchorPoint="0.25,1.1"
                        Fill="Transparent"
                        Clip="{Binding CalloutStrokeClip, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
                        >
            <exctrl:Callout.Stroke>
                <SolidColorBrush Color="Orange" Opacity="0.5" />
            </exctrl:Callout.Stroke>
        </exctrl:Callout>
        <!-- 塗りつぶし用図形 -->
        <exctrl:Callout x:Name="CalloutFill"
                        StrokeThickness="40"
                        Fill="Green"
                        AnchorPoint="0.25,1.1"
                        >
            <exctrl:Callout.Stroke>
                <SolidColorBrush Color="Black" Opacity="0" />
            </exctrl:Callout.Stroke>
        </exctrl:Callout>
    </Grid>
</UserControl>

ユーザーコントロールのコードビハインド

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace BlogEntry85
{
    /// <summary>
    /// OutlinedCallout.xaml の相互作用ロジック
    /// </summary>
    public partial class OutlinedCallout : UserControl
    {
        public OutlinedCallout()
        {
            InitializeComponent();

            // 塗りつぶし用図形が変更されたときのイベント設定
            this.CalloutFill.RenderedGeometryChanged += OnRenderedGeometryChanged;
        }

        /// <summary>
        /// 外枠線用図形の内側をくり抜くためのGeometry
        /// </summary>
        public static readonly DependencyProperty CalloutStrokeClipProperty = DependencyProperty.Register(
            "CalloutStrokeClip",
            typeof(Geometry),
            typeof(OutlinedCallout),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

        public Geometry CalloutStrokeClip
        {
            get { return (Geometry)GetValue(CalloutStrokeClipProperty); }
            set { SetValue(CalloutStrokeClipProperty, value); }
        }


        /// <summary>
        /// 塗りつぶし用図形が変更されたときのイベントメソッド
        /// </summary>
        private void OnRenderedGeometryChanged(object sender, EventArgs e)
        {
            // くり抜き用のGeometry作成
            GeometryGroup gGroup = new GeometryGroup();
            gGroup.Children.Add(new RectangleGeometry(new Rect(-9999, -9999, 19999, 19999)));

            Geometry geometry = this.CalloutFill.RenderedGeometry.Clone();
            gGroup.Children.Add(geometry);

            // くり抜き用のGeometryをメンバプロパティに設定
            this.CalloutStrokeClip = gGroup;
        }
    }
}

呼び出し側XAML

<Window x:Class="BlogEntry85.MainWindow"
        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:exctrl="clr-namespace:Microsoft.Expression.Controls;assembly=Microsoft.Expression.Drawing"
        xmlns:local="clr-namespace:BlogEntry85"
        mc:Ignorable="d"
        Title="MainWindow" Height="377.168" Width="711.561">
    <StackPanel Orientation="Horizontal" Margin="15" VerticalAlignment="Top">
        <!-- 通常のStrokeを使ったパターン -->
        <exctrl:Callout StrokeThickness="20"
                        Fill="Green"
                        Height="230" Width="230"
                        AnchorPoint="0.2,1.2"
                        Margin="0,0,25,0"
                        >
            <exctrl:Callout.Stroke>
                <SolidColorBrush Color="Orange" Opacity="0.5" />
            </exctrl:Callout.Stroke>
        </exctrl:Callout>

        <!-- 変更後のパターン -->
        <local:OutlinedCallout Height="230" Width="230" />

    </StackPanel>
</Window>

実行結果は下の画像のようになります。
左側が通常のStrokeを使用した場合、右側が対処後です。

吹き出しの先端がずれることなく、枠線と塗りつぶしも重ならないようになりました!

簡単な説明

【ユーザーコントロールのXAML】

外枠用と塗りつぶし用、2つの吹き出し図形を定義しています。

外枠用の吹き出し図形のClipに、コードビハインドで定義したGeometry型のメンバプロパティーをバインディングしています。

これにより塗りつぶしと重なっている部分の枠線は切り取られてしまうため、StokerThicknessは引きたい枠線の太さの2倍の値を設定しています。 (ここでは20pxの枠線を引きたいため、40を設定している)

塗りつぶし用図形のStrokeにTransparentブラシを設定してしまうと、BorderThicknessも0が指定されたものとして扱われてしまうため、 BlackのブラシのOpacityを0にすることで内側の図形を描画しています。

【ユーザーコントロールのコードビハインド】

コンストラクタで、塗りつぶし用の図形が変更された場合にOnRenderedGeometryChangedメソッドが呼び出されるように設定しています。

CalloutStrokeClipメンバプロパティは外枠線用図形のClip属性に設定するGeometryを保持します。

OnRenderedGeometryChangedでは画面以上に大きな四角形ジオメトリの中に、 塗りつぶし用の図形のジオメトリを指定することで、塗りつぶし用図形の外側をのこして内側をくり抜くGeometryGroupを作成しています。

そのあと、作成したくり抜き用GeometryGroupをCalloutStrokeClipメンバプロパティに格納しています。

【呼び出し側XAML】

ユーザーコントロールに高さと幅を指定して呼び出しています。

以上です! これで無事、吹き出しの先端がずれることなく、外枠線と塗りつぶしが重ならないようにすることができました。