nkf を用いた文字コードの判別

オープンソースである nkf を借用して,テキストファイルの文字コードを簡単に判別できます。バージョン 2.0.9 以降の nkf のライセンスは zlib/libpng License です (極めて良心的なライセンスです)。対応文字コードは次の通りです。

nkf32.dll を用意し,実行ファイルと同じディレクトリに置く必要があります。nkf32.dll は Vector からダウンロードできます。元々の nkf とは異なるライセンスですので,配布条件を確認してください (とは言っても緩い条件です)。

nkf.exe nkf32.dll Windows
http://www.vector.co.jp/soft/win95/util/se295331.html

文字コードの判別を行うには,私が作った EncodingUtilities クラスを利用してください。まずは使い方の見本から。

using System;
using System.Text;

class Program
{
    static void Main()
    {
        string path = @"c:\works\hoge.txt";

        // 文字コードを判別
        Encoding enc = EncodingUtilities.DetectEncoding(path);

        // テキストエディタを作るならこうしとくといい
        if (enc == null || enc == Encoding.ASCII)
        {
            enc = Encoding.Default;
        }

        // 処理...
    }
}

次のリンクからダウンロードできます。断りなく自由に使っていただいて結構です。

ソースコード: EncodingUtilities.cs

// EncodingUtilities.cs

using System;
using System.IO;
using System.Text;

/// <summary>
/// エンコーディングを取り扱うユーティリティを提供します。
/// </summary>
public static class EncodingUtilities
{
    /// <summary>
    /// テキストファイルで使われている文字コードを判別し,対応する Encoding オブジェクトを返します。
    /// </summary>
    /// <param name="path">テキストファイルへのパス。</param>
    /// <returns>Shift_JIS, EUC-JP, ISO-2022-JP, UTF-8, UTF-16, UTF-16BE, US-ASCII, null のいずれかが返ります。</returns>
    public static Encoding DetectEncoding(string path)
    {
        // -g: 自動判別の結果を出力する。
        // -t: 何もしない。
        SetNkfOption("-gt");

        byte[] bytes;
        
        using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            bytes = new byte[fs.Length];
            fs.Read(bytes, 0, bytes.Length);
        }

        unsafe
        {
            fixed (byte* pbs = bytes)
            {
                StringBuilder strBldr = new StringBuilder(1);
                NkfConvert(strBldr, (char*) pbs);
            }
        }

        int nEnc = NkfGetKanjiCode();

        // US-ASCII も ISO-2022-JP と判別されるので,更に篩にかける。
        if (nEnc == nJIS)
        {
            nEnc = nASCII;  // US-ASCII と仮定する
            for (int i = 0; i < bytes.Length; i++)
            {
                if (bytes[i] == 0x1b) // ISO-2022-JP であれば ESC (0x1B) が含まれる
                {
                    nEnc = nJIS;
                    break;
                }
            }
        }

        switch (nEnc)
        {
            case nSJIS:
                return Encoding.GetEncoding("shift_jis");
            case nEUC:
                return Encoding.GetEncoding("euc-jp");
            case nJIS:
                return Encoding.GetEncoding("iso-2022-jp");
            case nUTF8:
                return Encoding.GetEncoding("utf-8");
            case nUTF16LE:
                return Encoding.GetEncoding("utf-16");
            case nUTF16BE:
                return Encoding.GetEncoding("utf-16BE");
            case nASCII:
                return Encoding.ASCII;
            default:
                return null;
        }
    }

    #region nkf functions

    [System.Runtime.InteropServices.DllImport("nkf32.dll")]
    static extern int SetNkfOption(string optStr);

    [System.Runtime.InteropServices.DllImport("nkf32.dll")]
    unsafe static extern void NkfConvert(StringBuilder outStr, char* inStr);

    [System.Runtime.InteropServices.DllImport("nkf32.dll")]
    static extern int NkfGetKanjiCode();

    #endregion

    #region Fields

    // nSJIS, nEUC, nJIS, nUTF8, nUTF16LE, nUTF16BE の値は NkfGetKanjiCode() の戻り値に対応
    const int nSJIS = 0;
    const int nEUC = 1;
    const int nJIS = 2;
    const int nUTF8 = 3;
    const int nUTF16LE = 4;
    const int nUTF16BE = 5;

    const int nASCII = 1001;

    #endregion
}

「null 以外を返した」という結果は「文字コードを完全に特定できた」という意味を持つ訳ではありません。その点だけ注意してください。これは nkf の仕様に因ります。

char は全ての Unicode 文字を表せる訳ではない

System.Char のドキュメント (*1) を参照して「Unicode 文字を表します。」と書いてあるのを鵜呑みにし,第 4 水準の「&#140062」(廴+囘,U+2231E) という字を char に入れようとして失敗した。

public class Program
{
    public static void Main()
    {
        string s = char.ConvertFromUtf32(0x2231e);
        char c = s[0];

        MessageBox.Show(s.ToString(), "s"); // 表示される
        MessageBox.Show(c.ToString(), "c"); // 文字化けする
    }
}

U_2231E_MsgBox.png

Microsoft の言う「Unicode」とは Unicode ではなく UTF-16LE のことである。ConvertFromUtf32() の返り値が string 型なのを怪しく思いながら,なかなかその理由に気付けなかったことが敗因だった。


(*1) 古いバージョンを見たのがいけなかった。その後「文字をUTF-16 コード単位で表します。」と訂正されている。

SubItems[0] がなぜか埋まってる

ListViewItemSubItemsAdd でサブ項目を追加しようとすると,なぜか SubItems[0] に入れられない。

// 失敗例

ListViewItem item = new ListViewItem();
item.SubItems.Add("hoge");
item.SubItems.Add("piyo");

Console.WriteLine(item.SubItems[0].Text);  // 
Console.WriteLine(item.SubItems[1].Text);  // hoge

最初は Visual C# のバグかと思ったが,MSDN を読んだらそれらしいことが書いてあった。解決法もわかった。サブ項目をコンストラクタに渡せばいい。

// 成功例

ListViewItem item = new ListViewItem(new[] { "hoge", "piyo" });

Console.WriteLine(item.SubItems[0].Text);  // hoge
Console.WriteLine(item.SubItems[1].Text);  // piyo

ListView.Columns は普通に 0 から Add できるのに,ListViewItem.SubItems はできない。結果ずれる。こういう仕様ってどうなんだろ。

IME

IME の変換結果を勝手に書き換えてしまうようなアプリケーションを作ろうとして頓挫した途中結果。ImmSetCompositionString がどうしても動かない。

EXPORT LRESULT CALLBACK ImeHookProc(
    int nCode, WPARAM wParam, LPARAM lParam)
{
    /* 前略 */

    // 
    if (pmsg->lParam & GCS_RESULTSTR && wParam == 1)
    {
        HWND hWndActive = GetForegroundWindow();
        HWND hIMEWnd = ImmGetDefaultIMEWnd(hWndActive);
        HIMC hImc = ImmGetContext(hIMEWnd);

        char szBuf[1024];
        memset(szBuf, '\0', sizeof szBuf);

        // これは動く
        ImmGetCompositionString(
            hImc, GCS_RESULTSTR, szBuf, sizeof szBuf);

        /* 中略 (置換処理) */

        // 動かない!
        ImmSetCompositionString(
            hImc, SCS_SETSTR, szBuf, sizeof szBuf, NULL, 0);

        ImmReleaseContext(hIMEWnd, hImc);
    }

    return CallNextHookEx(NULL, code, wParam, lParam);
}

フックプロシージャの部分はこちらを参考にさせていただいた: http://win32.fc2web.com/win32tips/ime_kakutei.html

C/C++ で作った DLL を C# で使う

この記事の目的は「アンマネージド DLL に親しむ」です。

  1. C/C++ で DLL を作る
  2. C# で DLL 関数を呼び出す
  3. 文字列を扱う

C/C++ で DLL を作る

C で書かれた次の資産 arithmetic.c を,C# のプログラムから使いたいとします。

// arithmetic.c

int add(int a, int b)
{
    return a + b;
}

これを EXE ではなく DLL としてコンパイルすれば,C# のプログラムから DLL を読み込むことで,add 関数を呼び出すことができます。ただし,正しく DLL 化するには少々の細工が必要です。実際に,正しく細工を施した次のコードを DLL としてコンパイルしてみてください。

// arithmetic.c

__declspec(dllexport) int __stdcall add(int a, int b);

__declspec(dllexport) int __stdcall add(int a, int b)
{
    return a + b;
}

VC++ で DLL としてコンパイルするには,[表示] - [(プロジェクト名) のプロパティ] から,[構成プロパティ] - [全般] - [構成の種類] を [ダイナミックライブラリ (.dll)] に設定してください。

BCC で DLL としてコンパイルするには,-WD オプションを指定してください。

C++ としてコンパイルする場合には,extern "C" を付加する必要があります。関数のオーバーロードの関係で,関数名がめちゃくちゃに変えられてしまうのを防ぐためです。

// arithmetic.cpp

extern "C" __declspec(dllexport) int __stdcall add(int a, int b);

extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
{
    return a + b;
}

__declspec(dllexport) は,関数のエクスポートに必要なキーワードです。関数のエクスポートとは,関数を他のプログラムから利用できるように公開することです。

stdcall は呼び出し規約といい,関数の呼び出し方を決定するために必要なキーワードです。よく使われる呼び出し規約には stdcall 以外に cdecl などがあります。stdcall を修飾する関数はプロトタイプ宣言が必要とのことです。

__stdcall (MSDN)
http://msdn.microsoft.com/ja-jp/library/zxk0tw93.aspx

C でも C++ でも通用する書き方として,次のようなマクロ定義をしたコードをお勧めします。ここで __cplusplus は,C++ モードでコンパイラが自動的に定義するマクロ定数です。

// arithmetic.c または arithmetic.cpp

#ifdef __cplusplus
#define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllexport)
#endif

DLLEXPORT int __stdcall add(int a, int b);

DLLEXPORT int __stdcall add(int a, int b)
{
    return a + b;
}

C# で DLL 関数を呼び出す

さて,一方の DLL を呼び出す側ですが,C# のコードは次のようにします。

// Program.cs

using System;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("arithmetic.dll")]
    static extern int add(int a, int b);

    static void Main()
    {
        Console.WriteLine("2 + 3 = {0}", add(2, 3));
        Console.WriteLine("4 + 5 = {0}", add(4, 5));
    }
}

arithmetic.dll 内にある add() を関数を読み込むために,次の 2 行からなる宣言を行っています。

    [DllImport("arithmetic.dll")]
    static extern int add(int a, int b);

DllImport 属性 (System.Runtime.InteropServices 名前空間) は,関数が指定したアンマネージド DLL から読み込まれることを示します。続く修飾子 static extern は決まり文句です。戻り値,関数名,引数リストは,型が C と C# に共通である限り,元の関数プロトタイプをそのまま写せば OK です。

さて,このプログラムをビルドして,生成された実行ファイルと同じディレクトリに DLL ファイルを置けば準備完了です。プログラムをコマンドプロンプトで走らせてみてください。

文字列を扱う

文字列を受け渡しする次の mystrcpy() 関数を DLL 化します。

// strutils.c

#include <string.h>

#ifdef __cplusplus
#define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllexport)
#endif

DLLEXPORT void __stdcall mystrcpy(char *dest, const char *src);

DLLEXPORT void __stdcall mystrcpy(char *dest, const char *src)
{
    strcpy(dest, src);
}

これを C# から呼び出すためには,次のようにします。

// Program.cs

using System;
using System.Runtime.InteropServices;
using System.Text;

class Program
{
    [DllImport("strutils.dll")]
    static extern void mystrcpy(StringBuilder dest, string src);

    static void Main()
    {
       StringBuilder dest= new StringBuilder(1024);
       string src = "this is a test.";
       mystrcpy(dest, src);
       Console.WriteLine(dest.ToString());
    }
}

char System.Text.StringBuilderconst char string に対応付けられます。StringBuilder を使用する際は,new StringBuilder(1024) のように,十分な領域を確保する必要があります (*1)。

C の型C# の型
const char *string
char *System.Text.StringBuilder

関数宣言の頭に unsafe を指定すれば,ポインタをポインタのまま残すことも可能です。

その他の型の対応については,次のサイトが参考になります。

.NET TIPS Win32 APIやDLL関数を呼び出すには? - @IT
http://www.atmarkit.co.jp/fdotnet/dotnettips/024w32api/w32api.html

(*1) コメントでご指摘いただき,new StringBuilder()new StringBuilder(1024) に訂正,その旨補足しました (2012-08-20)。

new による多次元配列の動的作成

2 次元配列を作ろうとして double **arr = new double[size_x][size_y]; とか書いたらコンパイラに怒られたのでメモ。

1 次元配列

double *array = new double[size_x];

delete[] array;

2 次元配列

ダメな例

double **array = new double[size_x][size_y];  // これはダメ

new により確保できる配列は 1 次元までなので,次のように 1 次元ずつ確保していく必要があります。delete による解放も同様に 1 次元ずつ行います。

正しい例

double **array = new double*[size_x];
for (int i = 0; i < size_x; i++) {
    array[i] = new double[size_y];
}

for (int i = 0; i < size_x; i++) {
    delete[] array[i];
}
delete[] array;

3 次元配列

3 次元以上の配列でも同じ手法で解決できます。

ダメな例

double ***array = new double[size_x][size_y][size_z]; // ダメ

正しい例

double ***array = new double**[size_x];
for (int i = 0; i < size_x; i++) {
    array[i] = new double*[size_y];
    for (int j = 0; j < size_y; j++) {
        array[i][j] = new double[size_z];
    }
}

for (int i = 0; i < size_x; i++) {
    for (int j = 0; j < size_y; j++) {
        delete[] array[i][j];
    }
    delete[] array[i];
}
delete[] array;