WPF + C#でSetPixel、GetPixcelする

WinFromの時代には、System.Drawing.Bitmapクラスがあってそのクラスには、1ドットごとに色を指定して絵を描くことができるSetPixel関数が付いていました。

WPFになって以降、クラス群が更に高級化したのでBitmapImageやImageSourceではそのような操作は提供されていません。

が、大変稀に、自分でBitmapを作成して1ドットずつ点描したくなる時があります。そこで、そういった操作が実現できるようにクラスを作成したいと思います。

ビットマップを管理するクラスを作成する

とりあえずRGB24という形式でビットマップを管理する「Rgb24ImageBuffer」クラスを作成したいと思います。

RGB24は名前の通り、R → G → B が8bitずつStreamに並んでいる形式です。

// RGB24形式の画像バッファーを表します。
public class Rgb24ImageBuffer
{
    private byte[] buffer;
    private int x;
    private int y;
    private int rawStride;

    public int RawStride { get { return this.rawStride; } }

    /// <summary>
    /// 画像バッファーのサイズを指定してオブジェクトを初期化します。
    /// </summary>
    public Rgb24ImageBuffer(int x, int y)
    {
        this.x = x;
        this.y = y;
        this.rawStride = this.calculateRawStride();
        this.buffer = new byte[this.rawStride * y];
    }

    /// <summary>
    /// 指定した位置へ色を設定します。
    /// </summary>
    public void SetPixel(int x, int y, Color c)
    {
        this._getIndex(x, y, out int xIndex, out int yIndex);
        this.buffer[xIndex + yIndex] = c.R;
        this.buffer[xIndex + yIndex + 1] = c.G;
        this.buffer[xIndex + yIndex + 2] = c.B;
    }

    /// <summary>
    /// 指定した位置へ色を設定します。
    /// </summary>
    public Color GetPixcel(int x, int y)
    {
        this._getIndex(x, y, out int xIndex, out int yIndex);

        return new Color()
        {
            R = this.buffer[xIndex + yIndex],
            G = this.buffer[xIndex + yIndex + 1],
            B = this.buffer[xIndex + yIndex + 2],
        };
    }

    /// <summary>
    /// 現在のバッファーを取得します。
    /// </summary>
    public byte[] GetBuffer()
    {
        byte[] _tempBuffer = new byte[this.buffer.Length];
        for (int i = 0; i < this.buffer.Length; i++)
        {
            _tempBuffer[i] = this.buffer[i];
        }
        return _tempBuffer;
    }

    // --- Privates ---

    // RGB24のRawStrideを計算する
    private int calculateRawStride() => (this.x * PixelFormats.Rgb24.BitsPerPixel + 7) / 8;

    // ユーザー値 → bute[]のR, G, Bへアクセスするための位置計算
    private void _getIndex(int x, int y, out int xIndex, out int yIndex)
    {
        xIndex = x * 3;
        yIndex = y * this.rawStride;
    }
}

使い方

上記のバッファークラスにSetPixelメソッドが付いているので1ドットずつX,Y座標と色を指定してきます。

試しに全面を黒に塗りつぶして灰色の縦線を描画したBitmapを表示したいと思います。

先に実行結果を張っておきます。

f:id:Takachan:20171013011037p:plain

コードですが、ImageクラスのSourceプロパティにSetPixelしたBitmapを指定する場合の使い方は以下の通りです。

<!-- 画面のコード -->
<Controls:MetroWindow x:Class="ElmosAdControlClinet.Test.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:ElmosAdControlClinet.Test"
                      xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
                      mc:Ignorable="d"
                      Title="MainWindow"
                      Height="650"
                      Width="900"
                      BorderThickness="1"
                      GlowBrush="{DynamicResource AccentColorBrush}"
                      WindowTransitionsEnabled="False"
                      Loaded="MetroWindow_Loaded">
    <StackPanel>
        <Image x:Name="img"
               Height="593"
               Width="850"               
               Source="{Binding ImageSource}"/>
    </StackPanel>
</Controls:MetroWindow>

コードビハインド側

using MahApps.Metro.Controls;
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public partial class MainWindow : MetroWindow
{
    public const int ImageWidth = 850;
    public const int ImageHeight = 593;

    public ImageSource ImageSource { get; set; }
    
    public MainWindow()
    {
        this.InitializeComponent();
        this.DataContext = this;

        var buffer = new Rgb24ImageBuffer(850, 600);

        for (int y = 0; y < ImageHeight; y++)
        {
            for (int x = 0; x < ImageWidth; x++)
            {
                buffer.SetPixel(x, y, Color.FromRgb(40, 40, 40));
            }
        }

        for (int y = 0; y < ImageHeight; y++)
        {
            for (int x = 0; x < ImageWidth; x++)
            {
                if (x % 10 > 6)
                {
                    buffer.SetPixel(x, y, Color.FromRgb(160, 160, 160));
                }
            }
        }

        this.ImageSource = 
            BitmapSource.Create(ImageWidth, ImageHeight, 
                96, 96, PixelFormats.Rgb24, null, buffer.GetBuffer(), buffer.RawStride);
    }
}

マジで肝心なのが、バッファーからBitmapSourceを作成して、ImageSourceに設定する箇所です。このコードだけで値千金だと思います。

ちなみに、バッファーからブラシを作成して、MainWindowの背景に設定する場合以下のように記述します。

var brush = new ImageBrush()
{
    ImageSource =
        BitmapSource.Create((int)this.mandel.ScreenSizeX, (int)this.mandel.ScreenSizeY, 96, 96,
            PixelFormats.Rgb24, null, this.buffer.GetBuffer(), this.buffer.RawStride);
}

this.Background = brush;

おまけ

これをやっちゃあ、上記クラスを自作した意味がかなり薄くなるのですが、このバッファーをファイルパスから作成するユーティリティです。

System.Drawing.Bitmapを使ってファイルから画像を読み取ってバッファーに移し替えています。WPFのImageSourceやImageBrushと、SystemDrawing.Bitmapの間を埋めるためにバッファーを使うという意味では正しいのかもしれませんが、、、あとRgb24BufferクラスのSetPixcelhは余計な処理がないためBitmapの操作より結構軽いと思います。

/// <summary>
/// 画像操作に関する汎用処理を提供します。
/// </summary>
public static class Rgb24ImageBufferUtility
{
    // needs "System.Drawing.dll"

    /// <summary>
    /// 指定したファイルパスからバッファーを作成します。
    /// </summary>
    public static Rgb24ImageBuffer CreateBuffer(string path)
    {
        var _bitmap = new System.Drawing.Bitmap(path);
        var buffer = new Rgb24ImageBuffer(_bitmap.Width, _bitmap.Height);

        for(int y = 0; y < _bitmap.Height; y++)
        {
            for (int x = 0; x < _bitmap.Width; x++)
            {
                System.Drawing.Color color = _bitmap.GetPixel(x, y);
                buffer.SetPixel(x, y, new Color() { R = color.R, G = color.G, B = color.B});
            }
        }

        return buffer;
    }
}

あとは、自分で好きなようにSetPixelして画像を書いてください。