nkf を用いた文字コードの判別
オープンソースである nkf を借用して,テキストファイルの文字コードを簡単に判別できます。バージョン 2.0.9 以降の nkf のライセンスは zlib/libpng License です (極めて良心的なライセンスです)。対応文字コードは次の通りです。
- Shift_JIS
- EUC-JP
- ISO-2022-JP
- UTF-8
- UTF-16LE
- UTF-16BE
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 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 水準の「𢌞」(廴+囘,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"); // 文字化けする } }
Microsoft の言う「Unicode」とは Unicode ではなく UTF-16LE のことである。ConvertFromUtf32()
の返り値が string
型なのを怪しく思いながら,なかなかその理由に気付けなかったことが敗因だった。
(*1) 古いバージョンを見たのがいけなかった。その後「文字をUTF-16 コード単位で表します。」と訂正されている。
SubItems[0] がなぜか埋まってる
ListViewItem
の SubItems
に Add
でサブ項目を追加しようとすると,なぜか 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 に親しむ」です。
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
を修飾する関数はプロトタイプ宣言が必要とのことです。
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.StringBuilder
,const 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;