俺と Win32 コンソール(2):半分冗談で msvc/mingw32 用のオレオレ getch を模索してみた
いわゆる「澤」問題。よりくわしく言うと「getch でカーソルキーとか押すと 0xE0 のプレフィックスがついちゃって DBCS 入力にものすごくさしさわりがってゆうかこれじゃカーソルだか 2byte 文字だか判別しようがねーだろゴルァ」問題。(詳しいの?それって詳しいの?)。
ちなみにプレフィクスが 0xE0 になるのって実はマイクロソフトのランタイムだけっぽいのさ…そもそも <conio.h> の関数って dos 時代に作られた dos 特化のコンソール入出力関数なのに、この件に限って言えばマイクロソフトだけが dos 時代の Microsoft C/C++ と挙動が違うんですよ(dos 時代の _getch はカーソルキー押してもプレフィクスは 0xE0 にならない。0 のまま)。ちなみに Borland とかの他社製は dos 時代の仕様に合わせてたりする。テンキー統合のカーソルキーと独立のカーソルキーを別個に判別したいという需要があったのかもしれないけど、それならそれ相応の関数を別に作っても良かったじゃん…。
つまり何が言いたいかと申しますと、MS の _getch は DBCS 入力のことなんてなーんも考えておらんということです。ここで採りうるソリューション:
- _getch をあきらめる。Win32 のコンソールAPI を自前で使う(_getch 代替関数の作成も含めて)
- Microsoft や mingw をあきらめる。Borland, DigitalMars, OpenWatcom の検討
- DBCS をあきらめる(その1)。コンソールで日本語なんて入力しませんよJK
- DBCS をあきらめる(その2)。_getwch で Unicode 受け(さようなら Win9x)。
いちばん正しい道は 1. だと思うのですが、個人的な趣味により、もっとも変態的なアプローチを考えてみました。
ということでいきなりソース:
/* 変態 getch() test その1 int __cdecl my_kludge_getch(void) */ #include <windows.h> #include <conio.h> #include <stdio.h> int __cdecl my_kludge_getch(void); int __cdecl my_kludge_kbhit(void); int __cdecl my_kludge_ungetch(int); static HMODULE hm_crtdll; static int (__cdecl *lpfn_getch)(void); static int (__cdecl *lpfn_getwch)(void); static int (__cdecl *lpfn_kbhit)(void); static int (__cdecl *lpfn_ungetch)(int); #define COUNT_OF_CONBUF 16 static unsigned char conbuf[COUNT_OF_CONBUF]; static int in_conbuf, out_conbuf; #define ENTER_THREAD_ATOMIC #define LEAVE_THREAD_ATOMIC static void load_crtdll(void) { if (!hm_crtdll) { HMODULE hm; hm = LoadLibrary(TEXT("MSVCR71.dll")); /* VS2003 extra runtime DLL */ if (hm) { hm_crtdll = hm; lpfn_getch = (int (__cdecl*)(void))GetProcAddress(hm,TEXT("_getch")); lpfn_getwch = (int (__cdecl*)(void))GetProcAddress(hm,TEXT("_getwch")); lpfn_kbhit = (int (__cdecl*)(void))GetProcAddress(hm,TEXT("_kbhit")); lpfn_ungetch = (int (__cdecl*)(int))GetProcAddress(hm,TEXT("_ungetch")); } } return; } static int push_conbuf(int k) { int rc; int next_conbuf; ENTER_THREAD_ATOMIC; next_conbuf = in_conbuf + 1; if (next_conbuf == out_conbuf) { rc = -1; /* buffer full */ } else { conbuf[in_conbuf] = (unsigned char)(k & 255); if (next_conbuf >= COUNT_OF_CONBUF) next_conbuf = 0; in_conbuf = next_conbuf; rc = k; } LEAVE_THREAD_ATOMIC; return rc; } #if 0 static int peek_conbuf(void) { return (in_conbuf == out_conbuf) ? -1 : conbuf[out_conbuf]; } #endif static int get_conbuf(void) { int k; if (in_conbuf == out_conbuf) k = -1; else { ENTER_THREAD_ATOMIC; k = conbuf[out_conbuf++]; if (out_conbuf >= COUNT_OF_CONBUF) out_conbuf = 0; LEAVE_THREAD_ATOMIC; } return k; } static int push_wchar_into_conbuf(WCHAR wc) { int pushcnt; if (wc == 0xe0) wc = 0; /* dike enhanced key sign. */ if (wc >= 0x20) { unsigned char mbs[8]; int i; pushcnt = WideCharToMultiByte(CP_OEMCP,0,&wc,1,mbs,sizeof(mbs),NULL,NULL); for(i=0; i<pushcnt; ++i) push_conbuf(mbs[i]); } else { pushcnt = 1; push_conbuf(wc); } return pushcnt; } int __cdecl my_kludge_getch(void) { int k; if (!hm_crtdll) load_crtdll(); #if 0 if (!lpfn_getwch) { return lpfn_getch ? (*lpfn_getch)() : -1; } #endif for(;;) { k = get_conbuf(); if (k != -1) break; k = (*lpfn_getwch)(); if (k == -1) break; push_wchar_into_conbuf(k); } return k; } #ifdef TEST int main(int argc, char **argv) { int c; printf("Press Esc (or Ctrl-[) to exit.\n"); while(1) { c = my_kludge_getch(); if (c == -1 || c == 0x1b) break; printf("%c", c); } return 0; } #endif
_getwch でキー入力を受けて、0xe0 だったら 0 にする(0xe0 が拡張キーのプレフィクスではなく、ほんとうの文字 à という可能性もあるが、すくなくとも日本語じゃねーしというわけでひとまず黙殺)。それから MBCS に変換。msvcrt.dll には _getwch がないので、とりあえず VS2003 の dll から引っ張ってきています。
…自分でやっといて「何だかなあ」という感じではある。