C#での動的プログラミング

C#4.0から動的プログラミングがサポートされるようになりました。ちゃんと理解できてますか?この動的プログラミング、今回は80:20の法則に従って20を説明して80をわかった気になってもらおうと思います。

動的プログラミング、色々ありますが”dynamic” と “ExpandoObject” この2つをカバーすれば実際使うケースの80%をカバーできるのではないかと思います。

 

1.dynamicキーワード

C#3.0からvarという匿名型が出ました、これはコンパイル時に型を推定するものです。LINQでselectメソッドを使うと匿名クラスがすぐできるのでこれを受けるために(かどうかは知らないけど)varが生まれました。対して、dynamicは実行時に型を推定します、サンプルです。

TestDynamicKeyword.cs

namespace DynamicTestConsoleApp
{
    using System;
    using System.Text;
    public class TestDynamicKeyword
    {
        public static void Sample()
        {
            // dynamicキーワードのテスト
            // objは「ダイナミック型」として実行時に型情報を評価される
            dynamic str = "This type is string";

            // 1.存在しないプロパティにアクセスしてもコンパイルエラーにはならない
            try
            {
                str.CallNonExistingMethod();
            }
            catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e)
            {
                // ただし、存在しない型なので実行時に例外がスローされる
                Console.WriteLine("1: " + e.Message);
            }

            // 2.同一オブジェクトに、別の型のオブジェクトも代入できる
            str = new StringBuilder("This type is stringbuilder.");

            // 3.もちろん型名はちゃんとその型になっている
            Console.WriteLine("3: " + str.GetType());

            // 4.varはコンパイル時に型を推定するが、dynamicなオブジェクトを入れると
            //   それはdynamicキーワードで受けたのと同じことになる。
            var copiedVarObject = str;
            Console.WriteLine("4: " + copiedVarObject.GetType());

            // 5.キーワードが"dynamic"で宣言されたかどうかを取得する方法はない

            // 6.object型に対するメリットは例えば下記
            object num = 10;
            //   num *= 10; <- これがコンパイルエラーになる。
            //   明示的にキャストをすればエラーにならない。
            num = (int)num * 10;
            //   dynamicならキャストも不要。
            dynamic dynamicNum = 10;
            dynamicNum *= 10;

            // 7.ただintellisenseが無効なことに注意。
        }
    }
}

dynamic1

静的/静的はそれぞれの善し悪しがあります。C#は基本的に静的なな型付けをするため、実際に上のような例でdynamicを使うメリットはありません。よく言われることですが、Pythonなど他の言語と結合する部分で使うメリットがあります。「そんな言語使わねー氏」って方にちょっとべんりかもしれない例を挙げます。

WPFアプリのとあるコード

private static object SearchTargetHierarchy(dynamic parentElement)
{
    if( !(parentElement is FramworkElement) ||
        !(parentElement is DependencyObject) ||
        !(parentElement is FrameworkContentElement))
    {
        return null;
    }
    foreach (var item in LogicalTreeHelper.GetChildren(parentElement))
    {
        // 適当にやりたいこと
        WindowsFormsHost host = item as WindowsFormsHost;
        if (host != null && host.Child != null)
        {
            return host.Child;
        }
        var result = SearchTargetHierarchy(item);
        if (result != null)
        {
            return result;
        }
    }
    return null;
}

やっていることはツリーの要素を見回ってWindowsFormsHostがあればそのChildを取得することなんですが、そんなことはどうでもいいんです。上の例でdynamicが生きてくるポイントは、LogicalTreeHelper.GetChildren()です。このメソッドは引数として、ガード節でチェックしている3つの型を取ります。そのため3つのうちどれかとして呼びたいだけであっても具体的な型にキャストして呼ばないといけません、objectとして受けることもできませんし、一意にキャストするのもダメです。その代案として型チェックをした上で(=型の安全性を確認した上で)、dynamicで受けると不要なキャストと繰り返しコードがすっきりできます。

 

2.ExpandoObjectクラス

“Expando”は造語だそう、「拡張可能なオブジェクト」と思えば十分でしょう。サンプルです。

namespace DynamicTestConsoleApp
{
    using System;
    public class TestExpandoObject
    {
        public static void Sample()
        {
            // ExpandoObject型のテスト
            // 動的クラスを拡張するためのクラスであり、プロパティ追加などが可能
            var item = new System.Dynamic.ExpandoObject();

            // 1.上記varで受けるとitemはExpandoObject型なので、Aなどというプロパティはない。
            //   そのため、下記はコンパイルエラーになる。
            //item.A = "Property A";

            // 2. ExpandoObjectを使う場合はdynamicで受けないとメリットがでない
            dynamic obj = new System.Dynamic.ExpandoObject();
            obj.A = "Property A";
            Console.WriteLine("2: " + obj.A);

            // 3-1. メソッドも追加が可能
            obj.Method = new Action(() => Console.WriteLine("3-1: This is method call without argment"));
            obj.Method();

            // 3-2. 引数ももちろん取れます
            obj.MethodWithArg = new Action<int, int>((a, b) => Console.WriteLine("3-2 " + a + b));
            obj.MethodWithArg(1, 2);

            // 4.イベントも可能
            //   但し、初回アクセスから+=はNG。
            //obj.Event += new EventHandler(test); 実行時エラーになる
            obj.Event = new EventHandler(test);
            obj.Event += new EventHandler(test2);
            obj.Event(null, EventArgs.Empty);
        }

        static void test(object sender, EventArgs e)
        {
            Console.WriteLine("4-1: This is test1.");
        }
        static void test2(object sender, EventArgs e)
        {
            Console.WriteLine("4-2: This is test2.");
        }
    }
}

dynamic2

dynamicは動的な型を受け付ける仕組みですが、動的に型が作れないならそれはすべてvarで受けたらいい話。なのでdynamicとExpandoObjectはセットで出てきますが、ちゃんと違いを意識しましょう。ExpandoObject は内部的にはディショナリとして動的処理を作り出しますが、そんなことは使う上ではあまり知らなくて良いかと思います。

子供が夜泣きしたのでこれで;;

広告

C#での契約プログラミング – おまけ

前回紹介したCode Contract、動的なコントラクトの一部をご紹介しましたが他にも「インターフェースに対してコントラクトを設定する」とか、付加情報をもっと付けるとか、色々あります。また、動的な解析以外にも [静的な解析] および、[doc出力機能] が備わっています。これらをさらっと見ておきます。

 

1.静的な解析

これはCodeContractをインストールした上で、プロジェクトのプロパティから[Code Contracts]を選択し、Static Checkingにチェックを付けます。

static_analysis

コントラクトが静的に守られているかをチェックしてくれるものです。例えば、public APIのXMLドキュメントが用意されていなかったら、

codecontractの静的解析

こんな感じでWarningが出てきます(アウトプットウィンドウにも)。無効化したい場合は

namespace CodeContractSample
{
    using System.Diagnostics.Contracts;

    /// <summary>
    /// クラスInvariantSampleのサマリーです
    /// </summary>
    [ContractVerification(false)]
    public class InvariantSample
    {
        /// <summary>
        /// TestPropertyのサマリーです
        /// </summary>
        public int TestProperty { get; set; }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(TestProperty > 0);
        }
    }
}

上のようにContractVerificationAttributeにfalseを設定しておきます。HELPドキュメントにもありますが、正確に静的解析させるにはCodeContractの深い理解が必要であるようです。パフォーマンスの都合もありますし、”おまけ”で扱うような軽い気持ちでは正しくは理解できなそうです;

 

2.doc出力

こちらは簡単です。同じくプロジェクトのプロパティから [Code Contracts] の [Emit contracts into XML doc file] にチェックを付けてビルドするだけです。

output_docこれをいつぞやご紹介したSand castleへの入力とすると、

contract_doc

こんな感じにContractsの項目ができます。ユーザにAPIを公開するときはContractもはっきり示しておきたいものです。

C#での契約プログラミング

オブジェクト指向クラス設計原則のSOLIDのL(LSP:リスコフ置換原則)は、一言で言うなら 『サブクラスはスーパークラスと等価に扱えないといけない』 ということ。なんでそれが必要かというと、親クラスと子クラスで同じ関数に対する振る舞いが違うと、ポリモーフィックに扱えなくなり、その結果実行時の型情報(RTTI)を使った処理を書くハメになる、そうすると継承のメリット・必要性が失われるから。

インターフェースも継承も契約なため、静的な型付けをベースとするC#に「契約プログラミングは」は適していると思います。それが.NET4.0から標準に近い形で組込まれていたことを知りましたのでご紹介。

Code Contract for.NETをダウンロード & インストール。

codecontract_install

インストールはNext/Accept連打でOKです。これをインストールするとVisualStudioのプロジェクトのプロパティに [Code Contracts] タブが追加されます。ここで契約の度合いを設定します。

project_page

こちらは後述するとして、契約の例を見ていきます。契約には ①前提条件、②事後条件、③不変条件 の3種類があります。例を見ながら確認します。

1.前提条件を成立させる

PreconditionSample.cs

namespace CodeContractSample
{
    using System;
    using System.Diagnostics.Contracts;

    internal class PreconditiionSample
    {
        internal void ForbidNull(object arg)
        {
            Contract.Requires(arg != null);
            Console.WriteLine("ForbidNull called.");
        }

        internal void ForbidnullAndThrowsException(object arg)
        {
            Contract.Requires<ArgumentNullException>(arg != null);
            Console.WriteLine("ForbidnullAndThrowsException called.");
        }

        internal void ForbidNullWithLegacyCheck(object arg)
        {
            if (arg == null) throw new ArgumentNullException("arg");
            Contract.EndContractBlock(); // これより上をコントラクトと扱う.
            Console.WriteLine("ForbidNullWithLegacyCheck called.");
        }
    }
}

前提条件は関数を読みだしたときの条件で、Contract.Requires静的メソッドで表します。

1つ目はそのまま引数をコントラクトとして扱う最も単純な形です、この契約が守られていない場合ContractExceptionがスローされます。2つ目は同じく引数にコントラクトを取りますが、コントラクトが満たされなかった場合スローする例外の型を取ります。3つ目はCode Contractを使わない方法で、EndContractBlock()より上の内容をコントラクトとして扱います。上のようなガード節を使った書き方が決まっています、詳しくはDocmentを参照ください。

 

2.事後条件を成立させる

Postconditionample.cs

namespace CodeContractSample
{
    using System;
    using System.Diagnostics.Contracts;

    internal class PostconditionSample
    {
        private int testField = 0;
        internal void AssertZero()
        {
            Contract.Ensures(testField == 0);
            testField = 1;
        }

        internal void AssertZeroAndThrowsException()
        {
            Contract.EnsuresOnThrow<DivideByZeroException>(testField == 0);
            testField = 1;
            testField = testField / (testField - testField);
        }
    }
}

事後条件としては関数を抜けた時に満たしている条件で、Contract.Ensuresで表します。関数の先頭にありますが、関数を抜けるタイミングで評価されますのでご安心ください。

1つ目はtestFieldが0で関数が終わることをコントラクトにしていますが、1で抜けるのでContractExceptionがスローされます。2つ目は指定した例外がスローされた場合であっても守られる条件をコントラクトにしています。testFieldに1をセットした後0割で例外が投げられますが、その時でも0であることをコントラクトにしていますが満たしていないので同じくContractExceptionがスローされます。

 

3.不変条件を成立させる

 InvariantSample.cs

namespace CodeContractSample
{
    using System.Diagnostics.Contracts;

    internal class InvariantSample
    {
        public int TestProperty { get; set; }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(TestProperty > 0);
        }
    }
}

事前条件も事後条件もメソッド呼び出しが必要だったので、自動実装プロパティ等では上のようにContractInvariantMethod属性を使います。private void ObjectInvariantMethodは固定の関数で、コントラクト専用の関数です。

 

4.契約を使う

Contractは常に実行される訳ではありません。どのような運用をしたいかに対応できるよう、Contract違反しても何も起きないこともありますし、例外をスローさせることもできます。そこでコントラクトをどう設定したいかを決める必要があります。Help Documentによると以下のようなフローのようです。

flow

Code Contractをインストールすると出てくる、Visual Studioのプロジェクトプロパティに [Code Contracts] タブにAssembly Mode があります。ここで、上記用法①or②→[Standard Contract Requires]、 ③→[Custom Parameter Validation]  に設定します。

上の用法とも関連しますが、[Code Contracts]タブのPerform Runtime Contract Checkingの設定によって各種コントラクトがどうなるか、理解しておく必要があります。

dousajoukyou

Help Documentと私の確認結果で少し違いが…??上の表は参考程度に。Debugビルドではfullに設定して、ReleaseビルドではRelease Requiresがいいところかなと思います。

C#での画像処理 – 実践編

C#での画像処理、3回目の今回でおしまいです;オィ

今回のプログラム、構想はあるのですが作りこみが必要になるので概要だけさらって終わりにしようかと思います、万が一興味を持った方は作りこんでみてください;オィ

お題はASCIIアート。画像を適切なサイズにリサイズして2値化した上で画素値を元に文字に置き換えるという簡単な処理で実現できます。つまり、イメージはこういうこと。

taiou

ただ上のような3×3の画素値こんな簡単な対応付けでも2^9 = 512パターンを用意しなければなりません。これが厄介、文字にならない場合どうするんだってのもありますし。そこでここでは画素値に基づいて文字の濃さを割り当ててみます。

白は画素値でいうと255ですが文字の’ ’に割り当て、黒は画素値でいうと0ですが、文字の’轟’ というインクをいっぱい使いそうな文字に割り当て、その間の灰色は密度が合いそうな文字を選択する、という方式です。VGAの画像を1/16に圧縮した後で1画素ごとに文字に割り当ててみます。

ImageToChar.cs

namespace NormalizeCorrelation
{
    using System;
    using System.Text;

    internal class ImageToChar
    {
        MonochromeImage image;

        internal ImageToChar(MonochromeImage srcImage){
            // 適宜必要な変換
            image = srcImage.ScalingHalf().ScalingHalf().ScalingHalf().ScalingHalf();
        }

        public override string ToString()
        {
            StringBuilder builder = new StringBuilder();
            for (int y = 0; y < image.Height; y++)
            {
                for (int x = 0; x < image.Width; x++)
                {
                    builder.Append(ByteToChar(image[x, y]));
                }
                builder.Append(Environment.NewLine);
            }
            return builder.ToString();
        }

        private Char ByteToChar(Byte dat)
        {
            if (dat > 240)
            {
                return ' ';
            }
            if (dat > 200)
            {
                return '、';
            }
            if (dat > 150)
            {
                return '。';
            }
            if (dat > 125)
            {
                return 'ー';
            }
            if (dat > 100)
            {
                return 'い';
            }
            if (dat > 80)
            {
                return 'に';
            }
            if (dat > 65)
            {
                return 'た';
            }
            if (dat > 50)
            {
                return 'な';
            }
            if (dat > 25)
            {
                return '@';
            }
            if (dat > 10)
            {
                return 'ぜ';
            }
            if (dat > 0)
            {
                return '車';
            }
            return '轟';
        }
    }
}

image

動かすとこんな感じ。何が写ってるのかわかりにくいですが、手前にキーボードと私の手、奥にPCディスプレイがあります。TextBoxはMultilineをTrueにしたうえで、フォントを等幅フォントにしておきます。

ASCIIアートソフトを作りこんでみたい方はフォントを大きくしたり、プロポーショナルにしたりすると難易度はどんどん上がっていきます。

C#での画像処理 – 基本編

C#での画像処理、2回目は基本編と銘打ってよくある画像処理を用意します。

1回目のデータ構造を使った画像処理の例を出していませんでした。画像処理の基本としてよくある線形フィルタ等を用意してみました。

namespace NormalizeCorrelation
{
    using System;
    using System.Collections.Generic;

    internal static class MonochromeExtension
    {

        internal static MonochromeImage Invert(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    dstImage[x, y] = (byte)(~srcImage[x,y]);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage LowPassFilter(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    int sum = srcImage[x - 1, y - 1] + srcImage[x, y - 1] + srcImage[x + 1, y - 1] +
                              srcImage[x - 1, y] + srcImage[x, y] + srcImage[x + 1, y] +
                              srcImage[x - 1, y + 1] + srcImage[x, y + 1] + srcImage[x + 1, y + 1];
                    dstImage[x, y] = (byte)( sum / 9);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage ScalingHalf(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width / 2;
            int dstHeight = srcImage.Height/2;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    int sum = srcImage[2 * x, 2 * y] + srcImage[2 * x + 1, 2 * y] +
                              srcImage[2 * x, 2 * y + 1] + srcImage[2 * x + 1, 2 * y + 1];
                    dstImage[x, y] = (byte)(sum / 4);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage LaplacianFilter(this MonochromeImage srcImage )
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    int rowVal = srcImage[x - 1, y - 1] + srcImage[x, y - 1] + srcImage[x + 1, y - 1] +
                                 srcImage[x - 1, y] - 8 * srcImage[x, y] + srcImage[x + 1, y] +
                                 srcImage[x - 1, y + 1] + srcImage[x, y + 1] + srcImage[x + 1, y + 1];
                    int absVal = Math.Abs(rowVal);
                    dstImage[x, y] = (byte)((byte.MaxValue < absVal) ? byte.MaxValue : absVal);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage MedianFilter(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    var items = new List<byte>(){
                        srcImage[x - 1, y - 1],
                        srcImage[x, y - 1],
                        srcImage[x + 1, y - 1],
                        srcImage[x - 1, y],
                        srcImage[x, y],
                        srcImage[x + 1, y],
                        srcImage[x - 1, y + 1],
                        srcImage[x, y + 1],
                        srcImage[x + 1, y + 1]
                    };
                    items.Sort();
                    dstImage[x, y] = items[4];
                }
            }
            return dstImage;
        }
    }
}

上の通り、データ型に対する拡張メソッドとして定義すると次々と画像処理をさせたいときに以下のようなコードがかけます。

        private void UpdateDisplayImage(Bitmap img)
        {
            Bitmap capureImage = (Bitmap)img.Clone();
            MonochromeImage image = new MonochromeImage(capureImage);
            // ローパスフィルタかけて、1/2にダウンサンプリング、
            // その後ラプラシアンフィルタでエッジ抽出。
            var resultImage = image.LowPassFilter().ScalingHalf().LaplacianFilter();

            capturedPictureBox.Image = resultImage.GetAsBitmap();
        }

拡張メソッドばんざい。これは以前にUSBカメラをC#で使うのコードの一部です。組み合わせて動かすとこんな感じ。

imageprocessbycsharp

リアルタイムに更新される画像で画像処理を確認できるのはわかりやすくてGOOD。次回はもう少し自分でもやってみたくなる(?)内容に進む予定です;

C#での画像処理 – 土台編

今回から数回に分けてC#での画像処理で遊んでみようかと思います。第1回目となる今回は土台づくり。

画像といえばBitmap、非圧縮でRGBの色抽出もでき、Pixel単位で処理をするならこれ以上ないデータ形式です。ただこのBitmap、2, 3厄介なところがあります。それは

  • 画素ごとのSetPixelが遅いためLockBits, UnlockBitsが必要
  • その結果必要なデータがBitmapとBitmapDataに跨っている
  • さらにBitmapはIDisposableを実装するのでusingのたびにネストが1段深まる

あたりです。この辺りを忘れて処理ができる構造のほうが画像処理に向いている、と思うんです。私は。なので今回は画像処理の土台となるデータ構造を作りましょう。1つ大きな制限を課しておきます、それはモノクロ画像しか扱わないということです。(ちょっとお遊びするにはカラーは情報量が多すぎるのが一番の理由です。

 MonochromeImage.cs

namespace NormalizeCorrelation
{
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    internal class MonochromeImage
    {
        private byte[,] data;
        private int width;
        private int height;

        internal MonochromeImage(int width, int height)
        {
            data = new byte[width, height];
            this.width = width;
            this.height = height;
        }
        internal unsafe MonochromeImage(Bitmap srcImage)
            : this(srcImage, new Rectangle(Point.Empty, srcImage.Size))
        {
        }

        internal unsafe MonochromeImage(Bitmap srcImage, Rectangle targetRect)
        {
            if ((targetRect.X < 0) ||
                (srcImage.Width < (targetRect.X + targetRect.Width)) ||
                (targetRect.Y < 0) ||
                (srcImage.Height < (targetRect.Y + targetRect.Height)))
            {
                throw new ArgumentOutOfRangeException();
            }
            BitmapData bitmapData = srcImage.LockBits(new Rectangle(Point.Empty, srcImage.Size),
                ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
            data = new byte[srcImage.Width, srcImage.Height];
            byte* topAddress = (byte*)bitmapData.Scan0;
            width = targetRect.Width;
            height = targetRect.Height;
            int stride = bitmapData.Stride;

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    data[x, y] = (byte)(
                        (0.114478 * topAddress[y * stride + 3 * x]) +       // B
                        (0.586611 * topAddress[y * stride + 3 * x + 1]) +   // G
                        (0.298912 * topAddress[y * stride + 3 * x + 2])     // R
                        );
                }
            }
            srcImage.UnlockBits(bitmapData);
        }
        internal int Width
        {
            get { return width; }
            set { width = value; }
        }
        internal int Height
        {
            get { return height; }
            set { height = value; }
        }
        internal byte this[int x, int y]
        {
            // 3x3フィルタ処理などで画像領域外にアクセスしたときに
            // 自動的に折り返して境界を意識不要にしてやる。
            get {
                int roundedX = x < 0 ? 0 : ((width <= x) ? width - 1 : x);
                int roundedY = y < 0 ? 0 : ((height <= y) ? height - 1 : y);
                return data[roundedX, roundedY];
            }
            set {
                if (0 <= x && x < width && 0 <= y && y < height)
                {
                    data[x, y] = value;
                }
            }
        }
        internal unsafe Bitmap GetAsBitmap()
        {
            Bitmap saveImage = new Bitmap(Width, Height, PixelFormat.Format8bppIndexed);
            BitmapData bitmapData = saveImage.LockBits(new Rectangle(Point.Empty, new Size(Width, Height)),
                ImageLockMode.WriteOnly,
                PixelFormat.Format8bppIndexed);
            byte* topAddress = (byte*)bitmapData.Scan0;
            int stride = bitmapData.Stride;
            for(int y = 0; y < Height; y++){
                for (int x = 0; x < Width; x++)
                {
                    topAddress[y * stride + x] = data[x, y];
                }
            }
            saveImage.UnlockBits(bitmapData);

            // ColorPaletteはコンストラクタ公開なしなので参照コピーしてくる。
            ColorPalette palette = saveImage.Palette;
            for(int i = 0; i <= byte.MaxValue; i++){
                palette.Entries[i] = Color.FromArgb(i, i, i);
            }
            saveImage.Palette = palette;
            return saveImage;
        }

    }
}

こんな感じでいかがでしょうか。対象としたのは24bpp(RGB888)のBitmapです。α含めた32bppも対応しても良かったのですがやめときました。NTSCのグレー係数という重みをつけてモノクロ画素を算出・保持しています。データの並びは、B, G, Rなので注意。

画像アクセスにはインデクサを公開します。読み書き両方可能にしています。よくある画像処理フィルタに、オペレータとして3×3や5×5を取るものがあります。つまり対象とする画素を基準としてX, Y方向に±1画素の自分含めた周囲9画素や、±2画素の周囲25画素の画素値に対して演算した結果を自画素値とする処理です。この処理をする際、画像のフチでは領域外アクセスしないよう気をつけねばなりませんが、インデクサとしているので適宜内部で丸めたり例外スローしたりできて便利です。

出力結果確認のためBitmap出力をつけました。8bppのモノクロ画像にしていますが、24bppのほうが都合がよいかも。。?これは今後作りながら手を加えていくことになるかと思います。

WPFでNumericUpDownを使う

WPFの単項データバインドについて以前ブログに書きましたが、その時NumericUpDownの代わりにSliderを使いました、その理由はWPFにはNumericUpDownが存在しないためでした。そのためテンプレートを駆使してNumericUpDownを自作したり、ライブラリとして公開しているものが見受けられます。

ライブラリを使わず、あまり面倒なこともせず、NumericUpDownを使おうとするとWindowsFormsHostがあります。ただこれにも問題があり、WindowsFormsHostでNumericUpDownをホストすると、NumericUpDownはWindowsFormsのコントロールなのでよく使うValueプロパティが依存関係プロパティでなく、バインディングが効かないため他のWPFコントロール同様に扱えないという問題が生じます。そこで、バインド可能なNumericUpDownをライブラリを使わず、汎用的に作成することを考えてみます。

BindableNumericUpDown.xaml

<WindowsFormsHost x:Class="BindableWinFormsControl.BindableNumericUpDown"
             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:winform="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
             mc:Ignorable="d"
             d:DesignHeight="30" d:DesignWidth="300">
    <winform:NumericUpDown/>
</WindowsFormsHost>

BindableNumericUpDoiwn.xaml.cs (1) 不完全版

namespace BindableWinFormsControl
{
    using System;
    using System.Windows;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Windows.Forms.Integration;
    /// <summary>
    /// BindableNumericUpDown.xaml の相互作用ロジック
    /// </summary>
    public partial class BindableNumericUpDown : WindowsFormsHost
    {
        #region 依存関係プロパティ
        public static readonly DependencyProperty ValueProperty;
        static BindableNumericUpDown()
        {
            BindableNumericUpDown.ValueProperty = DependencyProperty.Register(
                "Value",
                typeof(Decimal),
                typeof(BindableNumericUpDown),
                new FrameworkPropertyMetadata(0m,
                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                              new PropertyChangedCallback(OnValueChanged)));
        }
        public Decimal Value
        {
            // このプロパティはコンパイル時に参照されるが、実行時には参照されない。
            // そのためこの.NETプロパティラッパにロジックを入れてはいけない。
            get { return (Decimal)GetValue(BindableNumericUpDown.ValueProperty); }
            set { SetValue(BindableNumericUpDown.ValueProperty, value); }
        }
        #endregion

        private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
        }
        public BindableNumericUpDown()
        {
            InitializeComponent();
        }
    }
}

WindowsFormsコントロールであるNumericUpDownを使うため、WindowsFormsHostを使うようにしました。ちゃんとバインドできるWPFコントロールとして扱いたいので、依存関係プロパティを定義する必要があります。依存関係プロパティは上を見た頂ければわかるとおり、定義にはstaticフィールドを使うため実行時になんとかして登録するのはダメです。となれば静的に型定義をする必要があり、ジェネリックな定義もできないので上のような基本構成となっています。

さて、これでバインドはできるのですが問題は値が同期していないことです。これは上記コントロールを使った簡単なコードを書くとよくわかります。

 TestBindObject.cs

namespace BindableWinFormsControl
{
    using System.ComponentModel;
    internal class TestBindObject : INotifyPropertyChanged
    {
        private int a;
        public int A {
            get { return a; }
            set {
                a = value;
                NotifyPropertyChanged("A");
            }
        }

        private void NotifyPropertyChanged(string parameter)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(parameter));
        }
        public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
    }
}

このテストデータをBindableNumericUpDownのValueプロパティとバインドさせたいとしましょう。

 MainWindow.xaml

<Window x:Class="BindableWinFormsControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:BindableWinFormsControl"
        Title="MainWindow" Height="350" Width="298.209">
    <StackPanel HorizontalAlignment="Left">
        <Button Height="20" Width="200" Click="ButtonDispClick">内部値の表示</Button>
        <Button Height="20" Width="200" Click="ButtonSetClick" >内部値を100に</Button>
        <my:BindableNumericUpDown Value="{Binding Path=A}" />
    </StackPanel>
</Window>

MainWindow.xaml.cs

namespace BindableWinFormsControl
{
    using System.Windows;
    /// <summary> MainWindow.xaml の相互作用ロジック </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new TestBindObject() { A = 10 };
        }
        private void ButtonDispClick(object sender, RoutedEventArgs e)
        {
            var bindingData = DataContext as TestBindObject;
            if (bindingData != null)
            {
                MessageBox.Show("内部値は : " + bindingData.A.ToString());
            }
        }
        private void ButtonSetClick(object sender, RoutedEventArgs e)
        {
            var bindingData = DataContext as TestBindObject;
            if (bindingData != null)
            {
                bindingData.A = 100;
            }
        }
    }
}

バインドNGの図

MainWindowのコンストラクタで10を設定していますが、0です。これは当然でBindableNumericUpDownコントロールのValueプロパティは10にバインドされていますが、BindableNumericUpDownコントロールのChildであるNumericUpDownコントロールのValueは初期値の0のままだからで、その0が表示されているためです。

ということでBindableNumericUpDownのValueとNumericUpDownのValueを同期させたいのですが、NumericUpDownはINotifyPropertyChangedを実装していないのでGUI上でマウスやキー操作で変更した数値がBindableNumericUpDown.Valueに反映できません。なので、BindableNumericUpDownにINotifyPropertyChangedを実装しましょう。

BindableNumericUpDown.xaml.cs(2) 完成版

namespace BindableWinFormsControl
{
    using System;
    using System.Windows;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Windows.Forms.Integration;
    /// <summary>
    /// BindableNumericUpDown.xaml の相互作用ロジック
    /// </summary>
    public partial class BindableNumericUpDown : WindowsFormsHost, INotifyPropertyChanged
    {
        #region 依存関係プロパティ
        public static readonly DependencyProperty ValueProperty;
        static BindableNumericUpDown()
        {
            BindableNumericUpDown.ValueProperty = DependencyProperty.Register(
                "Value",
                typeof(Decimal),
                typeof(BindableNumericUpDown),
                new FrameworkPropertyMetadata(0m,
                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                              new PropertyChangedCallback(OnValueChanged)));
        }
        public Decimal Value
        {
            // このプロパティはコンパイル時に参照されるが、実行時には参照されない。
            // そのためこの.NETプロパティラッパにロジックを入れてはいけない。
            get { return (Decimal)GetValue(BindableNumericUpDown.ValueProperty); }
            set {
                SetValue(BindableNumericUpDown.ValueProperty, value);
                NotifyPropertyChanged("Value");
            }
        }
        #endregion

        private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            BindableNumericUpDown control = sender as BindableNumericUpDown;
            if (control == null) {
                return;
            }
            if (e.Property == ValueProperty)
            {
                control.Value = (Decimal)e.NewValue;
            }
        }

        public BindableNumericUpDown()
        {
            InitializeComponent();
            SetUpBind();
        }

        #region 表示と依存関係プロパティの同期
        /// <summary>
        /// bypassData と NumericUpDown.Value のバインド
        /// </summary>
        private void SetUpBind()
        {
            var binding2 = new BindingSource();
            ((ISupportInitialize)binding2).BeginInit();
            NumericUpDown child = Child as NumericUpDown;
            child.DataBindings.Add(new System.Windows.Forms.Binding("Value", binding2, "Value", true, DataSourceUpdateMode.OnPropertyChanged));
            binding2.DataSource = typeof(BindableNumericUpDown);
            ((ISupportInitialize)binding2).EndInit();
            binding2.DataSource = this;
        }
        private void NotifyPropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
        #endregion
    }
}

これでちゃんとバインドができました。
バインドOKの図

今回はValueプロパティだけでしたが、ValueChangedイベントもバインドできるかなと。今回の方法ではChild要素は別にNumericUpDownじゃなくて、自作のWindowsFormsコントロールでも良い点を強調したいと思います。

さらにこれを汎用化するT4テンプレートを作る、という構想も膨らんでいますが今回はここまでで。

 

2014/8/7追記

WindowsFormsHostのの拡張方法としてこんな内容がmsdnにありますが、これでもやはりXAMLでバインドできないようなので上記の手法は使い道がありそうです。