WPFアプリケーションの図形で外枠線と塗りつぶしが重ならないようにする

Strokeを半透明にしたときに、塗りつぶしと重ならないようにするよ。

こんにちは!

冬が近づき空気が乾燥していくなかで、リップクリームで唇の荒れをふさぎつつ、 このへんは進化の過程でどうにかならなかったんだろうか、、と嘆いているあらたまです!

今日は WPFアプリケーションの図形の枠線のお話です。

Strokeで外枠線を引いてみる

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

今回はサンプルとして円形の図形を使用しました。

<Window x:Class="BlogEntry84.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:local="clr-namespace:BlogEntry84"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Horizontal" Margin="15">
        <Ellipse Fill="Green"
                 Stroke="Orange"
                 StrokeThickness="25"
                 Height="243"
                 Width="243"
                 >
        </Ellipse>
    </StackPanel>
</Window>

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

通常はこれで問題ないのですが、外枠を半透明にしたい要望があった場合に問題が生じます。

Strokeを半透明にしてみる

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

<Window x:Class="BlogEntry84.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:local="clr-namespace:BlogEntry84"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Horizontal" Margin="15">
        <Ellipse Fill="Green"
                 StrokeThickness="25"
                 Height="243"
                 Width="243"
                 >
            <Ellipse.Stroke>
                <SolidColorBrush Color="Orange" Opacity="0.5" />
            </Ellipse.Stroke>
        </Ellipse>
    </StackPanel>
</Window>

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

オレンジ色の枠線と、緑色の塗りつぶしが重なってしまっています。。

この緑色の塗りつぶしを、枠線と重ならないようにする方法です。

ソースコード

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

FrameworkElementの派生クラス

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace BlogEntry84
{
    class OutlinedEllipse : FrameworkElement
    {
        public static readonly DependencyProperty FillProperty = DependencyProperty.Register(
            "Fill",
            typeof(Brush),
            typeof(OutlinedEllipse),
            new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender));

        public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(
            "Stroke",
            typeof(Brush),
            typeof(OutlinedEllipse),
            new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender));

        public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(
            "StrokeThickness",
            typeof(double),
            typeof(OutlinedEllipse),
            new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// 塗りつぶしブラシ
        /// </summary>
        public Brush Fill
        {
            get { return (Brush)GetValue(FillProperty); }
            set { SetValue(FillProperty, value); }
        }

        /// <summary>
        /// 外枠線ブラシ
        /// </summary>
        public Brush Stroke
        {
            get { return (Brush)GetValue(StrokeProperty); }
            set { SetValue(StrokeProperty, value); }
        }

        /// <summary>
        /// 外枠線の太さ
        /// </summary>
        public double StrokeThickness
        {
            get { return (double)GetValue(StrokeThicknessProperty); }
            set { SetValue(StrokeThicknessProperty, value); }
        }

        /**
         * OutlinedEllipse描画時の処理
         **/
        protected override void OnRender(DrawingContext drawingContext)
        {
            // 外側円のGeometory取得
            Geometry bigGeometry = getGeometry(0);
            // 内側円のGeometory取得
            Geometry smallGeometry = getGeometry(this.StrokeThickness);

            // 枠線分のGeometry
            var strokeGeometry = Geometry.Combine(bigGeometry, smallGeometry, GeometryCombineMode.Exclude, null);

            // 外枠描画
            drawingContext.DrawGeometry(this.Stroke, null, strokeGeometry);

            // 塗りつぶし描画
            drawingContext.DrawGeometry(this.Fill, null, smallGeometry);
        }

        /// <summary>
        /// Geometry取得
        /// </summary>
        private Geometry getGeometry(double strokeThickness)
        {

            double width = this.ActualWidth - strokeThickness * 2;
            double height = this.ActualHeight - strokeThickness * 2;

            height = height < 0 ? 0 : height;
            width = width < 0 ? 0 : width;

            Shape shape = new Ellipse()
            {
                Height = height,
                Width = width
            };

            shape.Measure(new Size(width, height));
            shape.Arrange(new Rect(0, 0, width, height));

            Geometry geometry = shape.RenderedGeometry.Clone();
            geometry.Transform = new TranslateTransform(strokeThickness, strokeThickness);

            return geometry;
        }
    }
}

XAML

<Window x:Class="BlogEntry84.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:local="clr-namespace:BlogEntry84"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Horizontal" Margin="15">
        <Ellipse Fill="Green"
                 StrokeThickness="25"
                 Height="243"
                 Width="243"
                 Margin="0,0,25,0"
                 >
            <Ellipse.Stroke>
                <SolidColorBrush Color="Orange" Opacity="0.5" />
            </Ellipse.Stroke>
        </Ellipse>
        <local:OutlinedEllipse Fill="Green"
                               StrokeThickness="25"
                               Height="243"
                               Width="243">
            <local:OutlinedEllipse.Stroke>
                <SolidColorBrush Color="Orange" Opacity="0.5" />
            </local:OutlinedEllipse.Stroke>

        </local:OutlinedEllipse>
    </StackPanel>
</Window>

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

枠線と塗りつぶしが重ならないようになりました。

簡単な説明

【OutlinedEllipseクラス】

FrameworkElementの派生クラスとして作成しています。
描画時に必要な、塗りつぶし色、外枠線色、外枠線太さを依存プロパティとしてXAMLより受け取られるようにしています。

自身の描画時(OnRenderメソッド)で、外側円のGeometry、内側円のGeometryを作成して その差分を取ることで外枠線のGeometryとしています。

あとは、内側線のGeometryを塗りつぶし色で描画、外枠線のGeometryを外枠線で描画するだけです。

【XAML】

OutlinedEllipseクラスに塗りつぶし色、外枠線色、外枠線太さ、高さ、幅を与えて呼び出しています。

高さ、幅については、OutlinedEllipseクラスの基底クラスであるFrameworkElementクラスのものを使用しています。

色々な図形への対応

今回は円というシンプルな図形のため、OutlinedEllipseクラスのgetGeometryメソッドがとてもシンプルになっています。

ここでのwidth, height, TranslateTransformの生成のしかたを工夫することで、三角形や矢印などの様々な図形にも対応可能です!