PG日誌

受託系 PG が C# の事を書いています

WPF + C#でSetPixelする

はるか昔、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)
    {
        int xIndex = x * 3;
        int yIndex = y * this.rawStride;
        this.buffer[xIndex + yIndex] = c.R;
        this.buffer[xIndex + yIndex + 1] = c.G;
        this.buffer[xIndex + yIndex + 2] = c.B;
    }

    /// <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;
    }

    // RGB24のRawStrideを計算します。
    private int calculateRawStride()
    {
        return (x * PixelFormats.Rgb24.BitsPerPixel + 7) / 8;
    }
}

使い方

上記のバッファークラスに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;

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