酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

UIElementをWriteableBitmapに変換する

Windows Phone Advent Calendar "ひとり" 2011」第2日目。

UIElementをWriteableBitmapに変換する

UI要素はUIElementクラスを継承しており、WriteableBitmapのコンストラクタ引数に指定することが出来ます。

public WriteableBitmap(
    UIElement element,
    Transform transform
)

|element|ビットマップに描画するUI要素| |transform|ビットマップへ描画前の要素の変形を指定させるかを指定|

transformは、ビットマップへの描画前の最後のステップとして要素に変換が適用されます。nullを指定した場合は変換を行いません。

以下にミニマムで実行できるサンプルコードを用意しました。ページをWriteableBitmapに変換し、分離ストレージにJPEGデータとして保存しています。

// ページのをWriteableBitmapに変換する
var bitmap = new WriteableBitmap(this, null);
// 分離ストレージにページの画像を描画
using (var store = IsolatedStorageFile.GetUserStoreForApplication())
using (var strm = store.CreateFile("page.jpg")) {
  bitmap.SaveJpeg(strm, bitmap.PixelWidth, bitmap.PixelHeight, 0, 80);
}

円形グラデーションの四角形をWriteableBitmapに変換する

下図のような円形グラデーションのRectangleコントロールをWriteableBitmapに変換したいと考えています。

コードで書いても良いのですが、実装の難易度が高いことと速度が出ないことが予想できましたので、前記したUIElementからWriteableBitmapへ変換する方法を上手く利用していみましょう。

XAMLで円形グラデーションを書いた場合

先ほどの画像はXAMLで書くと以下の通りです。XAMLだとものすごくシンプルです。

    <Rectangle StrokeThickness="1">
        <Rectangle.Fill>
            <RadialGradientBrush>
                <GradientStop Color="Yellow" Offset="0"/>
                <GradientStop Color="Black" Offset="1"/>
            </RadialGradientBrush>
        </Rectangle.Fill>
    </Rectangle>

プログラムで円形グラデーションを書いた場合

四角形要素のインスタンスの作成した後に、どんなグラデーションにしたいかをGradientStopsプロパティに指定したのちに、四角形要素をコンストラクタ引数にWriteableBitmapインスタンスを生成します。

WriteableBitmapインスタンスにはSaveJpegメソッドが存在しているので、JPEG画像にエンコード後に分離ストレージへの保存も簡単にできますし、グラデーションさせた画像のピクセル操作をさせることも出来るようになります。

    // 四角形要素のインスタンスの作成
    var rect = new Rectangle() { Width = 404, Height = 571 };

    // グラデーション画像の作成
    var brush = new RadialGradientBrush();
    brush.GradientStops.Add(new GradientStop() { Color = Colors.Yellow, Offset = 0 });
    brush.GradientStops.Add(new GradientStop() { Color = Colors.Black, Offset = 1 });
    rect.Fill = brush;

    // 画像のキワが分かりにくいので外枠の線を引く
    rect.Stroke = new SolidColorBrush() { Color = Colors.Purple };

    // 分離ストレージにグラデーション画像を描画
    var bmp = new WriteableBitmap(rect, null);
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    using (var strm = store.CreateFile("rectGradation.jpg")) {
        bmp.SaveJpeg(strm, 404, 571, 0, 80);
    }

    // 画像としてImage要素のソースに設定
    image.Source = bmp;

動的なグラデーションを用意するのは少し厳しいかもしれませんが、固定のグラデーションを使用するのであれば簡単に使えると思います。ただしWriteableBitampを使用している関係でUIスレッド上でしか処理が出来ません。

ここに関しては非同期処理化は難しそうですね。

Shape以外の要素をWriteableBitmapへ変換する

先ほどUIElementからWriteableBitmap(UIElement, Transform)を使ってWriteableBitmapへ変換を行いました。しかしストレートに変換可能なのは一部の要素だけです。

例えばGridを変換しようとすると、エラーコードだけの理由が分かりにくいExceptionが発生してしまいます。

何故か?理由は単純でサイズが分からないのでビットマップが作れないからです。下記のコードを実行するとWriteableBitmapのインスタンス生成時にExceptionが発生します。

    var grid = new Grid() { Width = 456, Height = 624 };
    var size = new Size(grid.Width, grid.Height);
    var bmp = new WriteableBitmap(grid, null); // ここで落ちる

Rectangleを代表とするShapeの場合は、描画時のサイズRectangle.ActualHeightとRectangle.ActualWidthの値が決定しているため、問題なくWriteableBitmapに変換が可能です。

しかし、Gridの場合は描画時に初めて決定するのでWriteableBitmapへの変換が不可能です。逆に言えばUI上に描画しないまでも同様のプロセスを踏むことによって、ActualHeight、ActualWidthプロパティが更新されてWriteableBitmapへの変換が可能になるということです。

先ほどのコードに2行追加しただけですが、これで問題なくWriteableBitmapへ変換することが出来るようになっているはずです。

    var grid = new Grid() { Width = 456, Height = 624 };
    var size = new Size(grid.Width, grid.Height);
    grid.Arrange(new Rect(new Point(0, 0), size));
    grid.UpdateLayout();

    var bmp = new WriteableBitmap(grid, null);