C#ライブラリ(DLL)の作成方法

C#から使う、C#で書かれたライブラリ(*.dll)の作成方法です。
C#から使う、C言語ライブラリの作成方法はこちらを参照ください。

 

まず、ライブラリを呼び出す側のプロジェクトを作成します。

 

ここでは、Windowsフォームアプリケーションを作成するのに、

テンプレート→Visual C#→Windows→Windowsフォームアプリケーション

と選択し、名前を適当に付けてOKボタンをクリックします。

 

これで、呼び出し側のプロジェクトが作成されました。

 

次に、C#ライブラリ用のプロジェクトを作成します。

今、作成したプロジェクトの1つ上の階層に作成されている ソリューション の文字を右クリックし、

追加→新しいプロジェクト

を選択します。

 

今度はライブラリを作成するので、

  Visual C#→Windows→クラスライブラリ

と選択し、名前を適当に付けてOKボタンをクリックします。

 

すると、最小限のコードが生成されます。

 

通常は、Class1とかいうクラス名ではなく、もっとわかりやすい名前に変えますが、とりあえず、このまま次に進みます。

 

次に、2つの値を足すだけの簡単なメソッドを追加してみます。

 

この状態で、ビルドすると、ライブラリのプロジェクトフォルダのbinフォルダのDebugもしくはReleaseフォルダの中にライブラリファイル(CSharpDll.dll)が作成されます。

 

しかし、それだけだと、ライブラリを呼び出す元のプロジェクト(ここではWindowsFormsApplication1)からは使う事ができないため、参照の設定を行います。

 

参照は呼び出す元の方のプロジェクトにある、参照の文字を右クリックし、参照の追加を選択します。

 

ここで、ライブラリファイルを選択する方法もあるのですが、プログラム作成中はライブラリのプロジェクトを参照した方がデバッグ等が便利なので、プロジェクトの参照を行います。

 

  プロジェクト→ソリューション

 

と選択すると、ライブラリのプロジェクト名が表示されているので、プロジェクト名の左側にあるチェックボックスにチェックを入れ、OKボタンをクリックします。

 

これで、参照元に参照先のライブラリ名(ここではCSharpDll)が追加され、ライブラリが使用できる状態になります。

 

 

試しにフォーム上にボタンを配置します。

 

 

ボタンをダブルクリックし、作成されたクリックイベント内に、今、作成したライブラリのメソッドを追加してみます。

 

これで、ボタンをクリックした時に、以下のように表示されれば、C#ライブラリの作成は成功です。

 

ここでは、ライブラリのプロジェクトの参照として行いましたが、こうすることで、ライブラリのメソッド内へのステップイン実行もできるようになるので、かなり便利です。

 

一つポイントとしては、デフォルトのまま、ライブラリを作成すれば特に問題は無いのですが、ライブラリのプロジェクトのプラットフォームはAnyCPUにしておくのをお勧めします。

 

こうしておく事で、呼び出し元のプロジェクトがx86だろうが、x64であっても、同一のライブラリファイル(ここではCSharpDll.dll)を使用することができるようになります。

これを間違うと、x86用のdllファイル、x64用のdllファイルのそれぞれを作成する必要が出てくるので、少々面等になります。

【Visual Studio】同一ファイルを横に分割して表示

Visual Studioで同一のファイルを二分割して表示するには、これまで

ウィンドウ→分割

 

と選択して、コードを上下に表示していました。

 

 

しかしながら、今どきはモニタも横長なので、コードを上下に表示するよりは、横に並べて表示したくなります。

横にウィンドウを並べて表示するには、

  ウィンドウ→垂直タブグループの新規作成

というのがあるのですが、これだけだと、同一ファイルを横に並べて表示する事ができません。

 

そこで、

  ウィンドウ→新規ウィンドウ

 

と選択し、同一ファイルを2つのタブで表示させます。

 

この状態で、

  ウィンドウ→垂直タブグループの新規作成

 

と選択すると、同一ファイルが横に並んで表示されます。

 

参考

https://blogs.msdn.microsoft.com/zainnab/2011/03/01/split-code-windows-vertically/

 

【C#】文字列の回転描画

文字列を回転して描画するのはGraphicsオブジェクトをワールド変換して描画することも可能ですが、ワールド変換はGraphicsオブジェクト全体の座標系が変換されてしまうため、少々使いづらく感じます。

そこで、文字だけを回転するGraphicsPathを使った方法で関数にまとめてみました。

この関数で、文字の描画位置、回転、回転の基準位置(左上、中心など)を設定できるようにしています。

 

実行結果の画面

 

サンプルプログラム

private void Form1_Paint(object sender, PaintEventArgs e)
{
    var g = e.Graphics;
    // アンチエイリアスの設定
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    // グリッド線の描画(50画素ピッチ)
    for (int i = 0; i < this.Width; i += 50)
    {
        g.DrawLine(Pens.Black, i, 0, i, this.Height);
    }
    for (int j = 0; j < this.Height; j += 50)
    {
        g.DrawLine(Pens.Black, 0, j, this.Width, j);
    }

    var font = new Font("Arial", 20);

    // 文字の中心の座標が(100, 100)を中心に-30度回転
    var format = new StringFormat();
    format.Alignment = StringAlignment.Center;      // 左右方向は中心寄せ
    format.LineAlignment = StringAlignment.Center;  // 上下方向は中心寄せ
    DrawString(
        g,
        "[文字の中心が基準]",
        font,
        new SolidBrush(Color.Blue),
        100, 100,
        -30,
        format
        );

    // 文字の左上の座標が(200, 100)を中心に45度回転
    format.Alignment = StringAlignment.Near;      // 左右方向は左寄せ
    format.LineAlignment = StringAlignment.Near;  // 上下方向は上寄せ
    DrawString(
        g,
        "[文字の左上が基準]",
        font,
        new SolidBrush(Color.Red),
        200, 100,
        45,
        format
        );
}

/// <summary>
/// 文字列の描画、回転、基準位置指定
/// </summary>
/// <param name="g">描画先のGraphicsオブジェクト</param>
/// <param name="s">描画する文字列</param>
/// <param name="f">文字のフォント</param>
/// <param name="brush">描画用ブラシ</param>
/// <param name="x">基準位置のX座標</param>
/// <param name="y">基準位置のY座標</param>
/// <param name="deg">回転角度(度数、時計周りが正)</param>
/// <param name="format">基準位置をStringFormatクラスオブジェクトで指定します</param>
public void DrawString(Graphics g, string s, Font f, SolidBrush brush, float x, float y, float deg, StringFormat format)
{
    using (var pathText = new System.Drawing.Drawing2D.GraphicsPath())  // パスの作成
    using (var mat = new System.Drawing.Drawing2D.Matrix())             // アフィン変換行列
    {
        // 描画用Format
        var formatTemp = (StringFormat)format.Clone();
        formatTemp.Alignment = StringAlignment.Near;        // 左寄せに修正
        formatTemp.LineAlignment = StringAlignment.Near;    // 上寄せに修正

        // 文字列の描画
        pathText.AddString(
                s,
                f.FontFamily,
                (int)f.Style,
                f.SizeInPoints,
                new PointF(0, 0),
                format);
        formatTemp.Dispose();

        // 文字の領域取得
        var rect = pathText.GetBounds();

        // 回転中心のX座標
        float px;
        switch (format.Alignment)
        {
            case StringAlignment.Near:
                px = rect.Left;
                break;
            case StringAlignment.Center:
                px = rect.Left + rect.Width / 2f;
                break;
            case StringAlignment.Far:
                px = rect.Right;
                break;
            default:
                px = 0;
                break;
        }
        // 回転中心のY座標
        float py;
        switch (format.LineAlignment)
        {
            case StringAlignment.Near:
                py = rect.Top;
                break;
            case StringAlignment.Center:
                py = rect.Top + rect.Height / 2f;
                break;
            case StringAlignment.Far:
                py = rect.Bottom;
                break;
            default:
                py = 0;
                break;
        }

        // 文字の回転中心座標を原点へ移動
        mat.Translate(-px, -py, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 文字の回転
        mat.Rotate(deg, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 表示位置まで移動
        mat.Translate(x, y, System.Drawing.Drawing2D.MatrixOrder.Append);

        // パスをアフィン変換
        pathText.Transform(mat);

        // 描画
        g.FillPath(brush, pathText);
    }
}

プログラムの解説

回転中心の座標(表示位置の座標)をx, y で指定します。

文字の回転角度(deg)は時計方向が正となります。

文字列の回転の基準位置(回転の中心の位置)はStringFormatクラスで指定します。

StringFormat.Alignmentプロパティが左右方向の基準位置で、

StringAlignment.Near 左寄せ
StringAlignment.Center 中心寄せ
StringAlignment.Far 右寄せ

となります。

同様にStringFormat.LineAlignmentプロパティが上下方向の基準位置で、

StringAlignment.Near 上寄せ
StringAlignment.Center 中心寄せ
StringAlignment.Far 下寄せ

となります。

 

これを使うと、こんな時計の文字盤のように文字を配置するのも簡単に書くことができます。

 

関連記事

【C#.NET】マイクロソフト仕様のアフィン変換

【C#】GraphicsPath

【C#】GraphicsPathの描画

【C#】寸法線の描画

 

画像処理のためのC#テクニックへ戻る

【C#】メモリの値コピー、ポインタ、1次元、2次元、3次元配列間

C#でメモリのポインタ(IntPtr)と一次元配列間の値のコピーにはMarshal.Copyメソッド(名前空間:System.Runtime.InteropServices)を用います。

Marshal.Copyにはポインタから一次元配列へのコピー および 一次元配列からポインタへのコピーが用意されています。

一次元配列の型は、Single[], IntPtr[], Byte[], Single[], Int64[] , Int32[] , Int32[] , Int16[] , Double[]が指定可能です。

Mershal.Copyが可能なのは、あくまでも一次元配列が対象となります。

 

多次元配列(1次元、2次元、3次元…)と多次元配列間の値のコピーにはBuffer.BlockCopyメソッド(名前空間:System)を用います。

このメソッドでは、異なる次元の配列間、異なる型の配列間でメモリをコピーしてくれます。

異なる型の場合は、値のコピーではありません。

どちらかというと、C言語のCopyMemoryの挙動に似ています。

 

サンプルプログラム

// 一次元配列、二次元配列、三次元配列を確保(全て24バイト)
var arr1D = new byte[24] ;
var arr2D = new byte[6, 4];
var arr3D = new byte[2, 3, 4];
var arrUS = new ushort[12];

// メモリの確保(24バイト)
IntPtr ptr = Marshal.AllocHGlobal(24);

// 値の代入
for (byte i = 0; i < 24; i++)
{
    Marshal.WriteByte(ptr, i, i);
}
// ポインタから一次元配列へコピー
Marshal.Copy(ptr, arr1D, 0, arr1D.Length);

// 一次元配列から二次元配列へコピー
Buffer.BlockCopy(arr1D, 0, arr2D, 0, arr1D.Length * sizeof(byte));
// 二次元配列から三次元次元配列へコピー
Buffer.BlockCopy(arr2D, 0, arr3D, 0, arr2D.Length * sizeof(byte));
// byte配列からushort配列へコピー
Buffer.BlockCopy(arr1D, 0, arrUS, 0, arr1D.Length * sizeof(byte));

// メモリの解放
Marshal.FreeHGlobal(ptr);

 

実行後の配列の値は以下のようになります。

 

ushort配列の結果が少しわかりづらいですが、byte配列の2バイト分が1つのushortの値として格納されます。

 

 

参考ページ

【C#】配列の中身をメモリで確認する方法

 

【C#】配列の中身をメモリで確認する方法

C言語の時は、ポインタの中身をメモリで参照する事が多かったのですが、C#をメインで触るようになってからは、メモリの中身を参照することは無くなってしまい、いざ、やってみようとすると、やり方がわかりづらかったので記事にしました。

まず、Visual Studio にメモリのウィンドウを表示するのですが、ウィンドウ表示メニューがステップ実行中にしか表示してくれないため、メモリを見たい場所でブレークポイントを置いて停止させます。

 

この状態で、Visual Studio のメニューの デバッグ→ウィンドウ→メモリ→メモリ1(1) と選択するとメモリウィンドウが表示されます。

 

 

さらにメモリで表示させたい変数をメモリウィンドウへDrag&Dropすることで、配列の中身をメモリで参照することができます。

 

 

と、本当にやりたかったのは、二次元配列の時に、A[x, y]のように、左側の添え字の方からメモリに値が格納されていくのか? それともA[y, x]のように右側の添え字の方から格納されるのか?を確認したかったのですが、確認のため、

var arr2D = new byte[16, 16];

for (int j = 0; j < 16; j++)
{
    for (int i = 0; i < 16; i++)
    {
        arr2D[j, i] = (byte)((j << 4) + i);
    }
}

のようなソースコードで、8ビット(1バイト)中の下位4ビットに i の値を、上位4ビットに j の値を格納するプログラムをステップ実行で確認したところ、メモリへは、以下のように格納されていました。

 

 

ということで、多次元配列の時、配列の値は右側の添え字の方から格納される事がわかります。

 

こっち↓の方が並びは分かりやすいですね。

 

C# へ戻る

【C#】アフィン変換を用いて画像ビューアを作ろう!

C#にはアフィン変換に用いる行列用の

Matrixクラス(名前空間:System.Drawing.Drawing2D)

というクラスがあり、これを駆使すると、以下のようなプログラムを比較的簡単に作成することができます。

 

 

プログラムの実行ファイルはこちらより→ AffineImage101.zip

ソースコードはGitHubで公開しています。

https://github.com/ImagingSolution/AffineImage

 

プログラムの機能としては

  • 画像ファイルのDrag&Dropで画像をピクチャボックス全体に表示
  • マウスのドラッグで表示画像の移動
  • マウスホイールの回転でマウスポインタを中心に画像の拡大縮小
  • マウスホイールの回転 + Shiftキー でマウスポインタを中心に画像の回転
  • マウスのダブルクリックで画像をピクチャボックス全体に表示
  • ステータスバーにマウスポインタのピクチャボックス座標と、画像上の座標を表示

となります。

 

ここで用いていたMatrixクラスの主なメソッドは

Translate 平行移動
Scale 拡大縮小
RotateAt 指定した点を中心に回転
Reset リセット、単位行列の代入
Invert 逆行列を求める
TransformPoints 点の配列をアフィン変換する

となります。

ただ、これだけだと少し物足りないので、以下のメソッドを拡張メソッドとして追加しています。

ScaleAt 指定した点を中心に拡大縮小
ZoomFit 画像をピクチャボックスの大きさに合わせる

 

以下、ソースコードの解説です。

 

アフィン変換行列に基づいて画像の表示

DrawImageメソッド(名前空間:System.Drawing)には、描画する画像の領域と、描画先の座標を画像の左上、右上、左下の順でPointFの配列で指定することで、画像を任意場所や大きさで表示することが可能となります。

この描画先の座標をアフィン変換行列で求めて画像を表示しています。


public static void DrawImage(this Graphics g, Bitmap bmp, System.Drawing.Drawing2D.Matrix mat)
{
    if ((g == null) || (bmp == null))return;

    // 画像の画素の外側の領域
    var srcRect = new RectangleF(
            -0.5f,
            -0.5f,
            bmp.Width,
            bmp.Height
        );

    // 画像の左上、右上、左下の座標
    var points = new PointF[] {
            new PointF(srcRect.Left, srcRect.Top),  // 左上
            new PointF(srcRect.Right, srcRect.Top), // 右上
            new PointF(srcRect.Left, srcRect.Bottom)// 左下
    };

    // アフィン変換で描画先の座標に変換する
    mat.TransformPoints(points);

    // 描画
    g.DrawImage(
        bmp,
        points,
        srcRect,
        GraphicsUnit.Pixel
        );
}

 

ただし、画像の左上の座標は(-0.5, -0.5)となるのでご注意下さい。

(参考)

【C#】画像の座標系

 

 

指定した点を中心に拡大縮小

アフィン変換で行う拡大縮小の変換行列は座標の原点を中心に拡大縮小するため、単に拡大縮小のアフィン変換行列を掛けただけでは、意図しない拡大縮小となります。

そこで、アフィン変換を組み合わせて、

中心座標を原点へ移動→拡大縮小→原点から元の位置へ移動

の変換を行うと、指定して点中心の拡大縮小となります。

 


public static void ScaleAt(this System.Drawing.Drawing2D.Matrix mat, float scale, PointF center)
{
    // 原点へ移動
    mat.Translate(-center.X, -center.Y, System.Drawing.Drawing2D.MatrixOrder.Append);

    // 拡大縮小
    mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);

    // 元へ戻す
    mat.Translate(center.X, center.Y, System.Drawing.Drawing2D.MatrixOrder.Append);

}

(参考)

任意点周りの回転移動(アフィン変換)

 

画像をピクチャボックスに合わせて表示する

ここでのアフィン変換の考え方は、まず、画像の左上の座標をピクチャボックスの原点に合わせるため、(+0.5, +0.5)だけ平行移動(Translate)します。

↓ (+0.5, +0.5)だけ平行移動

 

次に、画像の縦横比とピクチャボックスの縦横比を比較して、画像が縦長の場合、画像の縦の長さとピクチャボックスの縦の長さが揃うように画像を拡大縮小(Scale)します。

画像が横長の場合は、横の長さが合うように画像を拡大縮小(Scale)します。

最後に、画像がピクチャボックスの中心に来るように平行移動(Translate)します。


public static void ZoomFit(this System.Drawing.Drawing2D.Matrix mat, PictureBox pic, Bitmap bmp)
{
    if (bmp == null) return;

    // アフィン変換行列の初期化(単位行列へ)
    mat.Reset();

    // 0.5画素分移動
    mat.Translate(0.5f, 0.5f, System.Drawing.Drawing2D.MatrixOrder.Append);

    int srcWidth = bmp.Width;
    int srcHeight = bmp.Height;
    int dstWidth = pic.Width;
    int dstHeight = pic.Height;

    float scale;

    // 縦に合わせるか?横に合わせるか?
    if (srcHeight * dstWidth > dstHeight * srcWidth)
    {
        // ピクチャボックスの縦方法に画像表示を合わせる場合
        scale = dstHeight / (float)srcHeight;
        mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 中央へ平行移動
        mat.Translate((dstWidth - srcWidth * scale) / 2f, 0f, System.Drawing.Drawing2D.MatrixOrder.Append);
    }
    else
    {
        // ピクチャボックスの横方法に画像表示を合わせる場合
        scale = dstWidth / (float)srcWidth;
        mat.Scale(scale, scale, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 中央へ平行移動
        mat.Translate(0f, (dstHeight - srcHeight * scale) / 2f, System.Drawing.Drawing2D.MatrixOrder.Append);
    }
}

 

ピクチャボックスの座標から、元の画像の座標を求める

これまで説明してきたアフィン変換の行列は、画像の座標をピクチャボックスの座標へ変換するためのアフィン変換行列を求めています。

それでは、その逆のピクチャボックスの座標を元の画像の座標へ変換するには、アフィン変換行列の逆行列を求めて座標変換することで、元の画像を求める事ができます。

この処理をサンプルプログラムでは、MouseMoveイベントで行っています。

その一部抜粋です。


private void picImage_MouseMove(object sender, MouseEventArgs e)
{
    ////////////////////////////
    // マウスポインタの座標と画像の座標を表示する

    // マウスポインタの座標
    lblMousePointer.Text = $"Mouse {e.Location}";

    // アフィン変換行列(画像座標→ピクチャボックス座標)の逆行列(ピクチャボックス座標→画像座標)を求める
    // Invertで元の行列が上書きされるため、Cloneを取ってから逆行列
    var invert = _matAffine.Clone();
    invert.Invert();

    var pf = new PointF[] { e.Location };

    // ピクチャボックス座標を画像座標に変換する
    invert.TransformPoints(pf);

    // 画像の座標
    lblImagePointer.Text = $"Image {pf[0]}";

}

(参考)

【C#】アフィン変換の相互座標変換

【C#.NET】マイクロソフト仕様のアフィン変換

 

以上、如何だったでしょうか?

「アフィン変換」と言うと、「えっ?行列の計算??」「なんか難しそう」と言われて何かと敬遠されがちなのですが、アフィン変換に少し慣れると、計算も比較的スッキリとするかと思います。

私からすると、ここでやっている計算を幾何学的に求めようとすると、そっちの方がはるかに大変。。

 

画像をピクチャボックスに合わせて表示するで紹介しているようにアフィン変換行列を一発で求めようとするのではなく、変換前の状態から変換後の状態へと変換するまでの手順を考え、その手順を行列で表して掛けていく部分がポイントとなります。

アフィン変換の行列の計算部分では、画像そのものを拡大縮小/平行移動している訳ではなく、たかだか3x3の行列の積を計算しているだけなので、計算量としても少なくて済みます。

 

画像処理のためのC#テクニックへ戻る