俺と Win32 コンソール(3):とりあえず IME のオン/オフとかやってみる

まあ Alt+漢字のキーストロークを keybd_event で叩き込むだけでいけるんですが。

void ToggleIMEStateByKeyStroke(void)
{
   /* キー注入結果を反映させるための待ち(手元の環境では単なる Yield 扱いの 
      0 でも問題なさそうだった。環境によってはもっと大きめの値がいるかも) */
  const DWORD dwYieldMSecForKey = 1;
  
  keybd_event(VK_MENU, 0 /* 0x38 */, 0, 0);                 /* down Alt */
  keybd_event(VK_KANJI, 0 /* 0x29 */, 0, 0);                /* down Kanji */
  keybd_event(VK_MENU, 0 /* 0x38 */, KEYEVENTF_KEYUP, 0);   /* up Alt */
  keybd_event(VK_KANJI, 0 /* 0x29 */, KEYEVENTF_KEYUP, 0);  /* up Kanji */
  Sleep(dwYieldMSecForKey);  /* wait for a proof */
}

ただ、コンソールウィンドウ自身がキー入力のフォーカスを持っていないと、よそのウィンドウの IME が開いたり閉じたりしちゃう罠。とりあえず GetConsoleWindow() == GetForegroundWindow() の確認が必要です。

あともうひとつの問題は、コンソールウィンドウの場合、現在の IME のモードを確実に判定する方法がどうも存在しないっぽいことです。IME 系の API で問い合わせてもダメだしなあ(実は Win9x なら取れるのだが)…

いろいろ悪あがきしてみたけど「確実」にはほど遠い。一応それらしいソースも晒してみます。

#include <windows.h>

#include <malloc.h>
#include <stdio.h>
#include <string.h>


static BOOL CALLBACK iter_find_console_win9x(HWND hwnd, LPARAM lprm)
{
  /* コンソールクラス名: NT 系は "ConsoleWindowClass" */
  const TCHAR szConClass[] = TEXT("tty");
  HWND *hwndResult = (void *)lprm;
  TCHAR szClass[128];
  
  szClass[sizeof(szClass)/sizeof(szClass[0])-1] = 0;
  if (GetClassName(hwnd, szClass, sizeof(szClass)/sizeof(szClass[0])-1) > 0 &&
      lstrcmpi(szClass, szConClass) == 0)
  {
    DWORD pcur, tcur;
    /*
      Win9x: ウィンドウが現行プロセスに属するか調べる。 
      コンソールウィンドウの pid と tid は拡張ウィンドウメモリにも 
      入っているので、GetWindowLong で取り出してもよい。 
      pid = GetWindowLong(hwnd, 0);
      tid = GetWindowLong(hwnd, 4);
      NT 系の場合、コンソールウィンドウはコンソールプロセスに属していない 
      ため、この方法では検出できない 
    */
    tcur = GetWindowThreadProcessId(hwnd, &pcur);
    if (tcur != 0 && pcur == GetCurrentProcessId()) {
      *hwndResult = hwnd;
      return FALSE;
    }
  }
  return TRUE;
}


static HWND WINAPI myGetConsoleWindowForWin9x(void)
{
  HWND hwnd = NULL;
  EnumWindows(iter_find_console_win9x, (LPARAM)&hwnd);
  return hwnd;
}

static HWND WINAPI myGetConsoleWindowStub(void)
{
#if 0
  SetLastError(ERROR_NOT_SUPPORTED);
#endif
  return NULL;
}

/*
  コンソールウィンドウのハンドルを得る。 
  Win9x (95/98/Me), Win2000以上兼用。 
  バージョン 4.0 以下の Windows NT は未対応。無念。 
*/

HWND WINAPI myGetConsoleWindow(void)
{
  static HWND (WINAPI *lpfn_getconsolewindow)(void) = NULL;

  if (!lpfn_getconsolewindow) {
    DWORD dwVer = GetVersion();
    HMODULE hm = GetModuleHandle(TEXT("KERNEL32"));
    FARPROC lpfnGetConWnd;
    
    if (hm != NULL && (lpfnGetConWnd = GetProcAddress(hm, TEXT("GetConsoleWindow"))) != NULL) {
      /* use API (Windows2000 or above) */
      lpfn_getconsolewindow = (HWND (WINAPI *)(void))lpfnGetConWnd;
    } else if ((dwVer & 0x80000000U) != 0 && (dwVer & 0xff) >= 4) {
      /* Win95, 98, 98SE, Me */
      lpfn_getconsolewindow = myGetConsoleWindowForWin9x;
    } else {
      /* oh poor NT version 4.0 (or below) */
      lpfn_getconsolewindow = myGetConsoleWindowStub;
    }
  }
  return (*lpfn_getconsolewindow)();
}


void ToggleIMEStateByKeyStroke(void)
{
   /* キー注入結果を反映させるための待ち(手元の環境では単なる Yield 扱いの 
      0 でも問題なさそうだった。環境によってはもっと大きめの値がいるかも) */
  const DWORD dwYieldMSecForKey = 1;
  
  keybd_event(VK_MENU, 0 /* 0x38 */, 0, 0);                 /* down Alt */
  keybd_event(VK_KANJI, 0 /* 0x29 */, 0, 0);                /* down Kanji */
  keybd_event(VK_MENU, 0 /* 0x38 */, KEYEVENTF_KEYUP, 0);   /* up Alt */
  keybd_event(VK_KANJI, 0 /* 0x29 */, KEYEVENTF_KEYUP, 0);  /* up Kanji */
  Sleep(dwYieldMSecForKey);  /* wait for a proof */
}


static BOOL peek_console_key_state_from_buffer(DWORD *dwState, const INPUT_RECORD *ir, DWORD ir_count)
{
  BOOL rc = FALSE;
  DWORD i;
  for(i=0; i<ir_count; ++i) {
    if (ir[i].EventType == KEY_EVENT) {
      if (dwState) *dwState = ir[i].Event.KeyEvent.dwControlKeyState;
      rc = TRUE;
      /* do not break */
    }
  }
  return rc;
}

DWORD PeekLatestConsoleKeyStateForNT(HANDLE hConin)
{
  const DWORD dwIRRESERVED = 2;
  const DWORD dwYieldMSecForKey = 1;
  DWORD dwKeyState;
  INPUT_RECORD *ir;
  DWORD dwIRMAX, dwIRRead;
  BOOL b;
  
  dwIRMAX = 0;
  if (!GetNumberOfConsoleInputEvents(hConin, &dwIRMAX)) return 0;
  ir = alloca((dwIRMAX + dwIRRESERVED) * sizeof(INPUT_RECORD));
  b = PeekConsoleInput(hConin, ir, dwIRMAX+dwIRRESERVED, &dwIRRead);
  if (b) {
    b = peek_console_key_state_from_buffer(&dwKeyState, ir, dwIRRead);
    if (!b) {
      /* KeyStroke not exist - send dummy keystate and try again */
      keybd_event(0 /*VK_NULL*/, 0, 0, 0);
      keybd_event(0 /*VK_NULL*/, 0, KEYEVENTF_KEYUP, 0);
      Sleep(dwYieldMSecForKey); /* and yield (for a proof) */
      if (PeekConsoleInput(hConin, ir, dwIRMAX+dwIRRESERVED, &dwIRRead)) {
        b = peek_console_key_state_from_buffer(&dwKeyState, ir, dwIRRead);
      }
    }
  }
  if (!b) dwKeyState = 0;
  
  return dwKeyState;
}


BOOL
IsWin32Console(HANDLE h)
{
  DWORD dw;
  /* if (GetFileType(h) != FILE_TYPE_CHAR) return FALSE; */
  return GetConsoleMode(h, &dw);
}




#ifdef TEST

#ifndef NLS_DBCSCHAR
# define NLS_ALPHANUMERIC   0x00000000
# define NLS_DBCSCHAR       0x00010000
# define NLS_KATAKANA       0x00020000
# define NLS_HIRAGANA       0x00040000
# define NLS_ROMAN          0x00400000
# define NLS_IME_CONVERSION 0x00800000
# define NLS_IME_DISABLE    0x20000000
#endif

void
print_keystate(DWORD dw)
{
  const char *spc = "";
  const char *spc2 = " ";
  BOOL bKanaLck;
  
  if (dw & LEFT_ALT_PRESSED) { printf("%sAlt-L", spc); spc = spc2; }
  if (dw & RIGHT_ALT_PRESSED) { printf("%sAlt-R", spc); spc = spc2; }
  if (dw & LEFT_CTRL_PRESSED) { printf("%sCtrl-L", spc); spc = spc2; }
  if (dw & RIGHT_CTRL_PRESSED) { printf("%sCtrl-R", spc); spc = spc2; }
  if (dw & SHIFT_PRESSED) { printf("%sShift", spc); spc = spc2; }
  if (dw & NUMLOCK_ON) { printf("%sNumLk", spc); spc = spc2; }
  if (dw & SCROLLLOCK_ON) { printf("%sScrLk", spc); spc = spc2; }
  if (dw & CAPSLOCK_ON) { printf("%sCapsLk", spc); spc = spc2; }
  if (dw & SCROLLLOCK_ON) { printf("%sScrLk", spc); spc = spc2; }
  
  bKanaLck = GetKeyState(VK_KANA);
  if (bKanaLck) { printf("%sカナ", spc); spc = spc2; }

  /*
    IME オン時の入力モード(不完全版) 
    ぶっちゃけ「(ローマ字/カナ的な意味での)カナ入力」と 
    「(ひらがな/カタカナモード的な意味での)カナモード」とが微妙に 
    ごっちゃになっていて、正確な判定が不可能となっている。 
    
    たとえば、IME2000 のツールバーで日本語入力時のみカナロックを
    有効にすると NLS_KATAKANA がセットされるが、実際はひらがな入力のまま。 
    そして、カナロックを解除して、"Shift+ひらがな"でカタカナモードに 
    してもやはり NLS_KATAKANA がセットされる(ローマ字入力モード)。 
    
    IME オン時の「カタカナ/ひらがな」モードを正確に取得する方法って 
    あるんですかねえ… 
    
    (Win9x の場合は INPUT_RECORD のEvent.KeyEvent.dwControlKeyState に 
    何も情報が帰らないので全く無意味。tty クラスのウィンドウに IME 方面の 
    API で問い合わせをすればいいような気がする。) 
  */

  if (dw & NLS_IME_CONVERSION) {
    printf("%s%s", spc, (dw & NLS_DBCSCHAR) ? "全" : "半");
    
#ifdef dont_need_kanalock_kludge
    if (dw & NLS_HIRAGANA) printf("あ");
#else
    if ((dw & NLS_HIRAGANA) || (bKanaLck && (dw & NLS_KATAKANA)))
      printf("あ");
#endif
    else if (dw & NLS_KATAKANA)
      printf("%s", (dw & NLS_DBCSCHAR) ? "カ" : "カ ");
    else
      printf("%s", (dw & NLS_DBCSCHAR) ? "A" : "A ");
    
    if (dw & NLS_ROMAN) printf("ローマ");
    spc = spc2;
  }
}


int main(void)
{
  HWND hwndFG, hwndCon;
  HANDLE hConin;
  DWORD dw;
  
  hwndFG = GetForegroundWindow();
  hwndCon = myGetConsoleWindow();
  
  printf("GetForegroundWindow 0x%x\n", hwndFG);
  printf("GetConsoleWindow 0x%x\n", hwndCon);
  
  if (!hwndCon || hwndFG != hwndCon) {
    printf("not foreground window.\n");
    return 1;
  }
  
  hConin = GetStdHandle(STD_INPUT_HANDLE);
  if (!IsWin32Console(hConin)) {
    printf("Console is redirected.\n");
    return 1;
  }
  
  FlushConsoleInputBuffer(hConin);
  dw = PeekLatestConsoleKeyStateForNT(hConin);
  printf("Previous Control State 0x%x", dw);
  printf(" [");
  print_keystate(dw);
  printf("]\n");
  
  ToggleIMEStateByKeyStroke();
  
  dw = PeekLatestConsoleKeyStateForNT(hConin);
  printf("Currect Control State  0x%x", dw);
  printf(" [");
  print_keystate(dw);
  printf("]\n");
  
  
  return 0;
}
#endif

…余談ですが、「コンソールウィンドウでも(Alt+漢字じゃなくて)漢字キー一発で IME を制御したい」みたいな場合はスキャンコードを書き換えちゃうのがいちばん確実だと思います(最近の Windows でこの技が使えるのか分かりませんが…)。