俺と Win32 コンソール(1):オレオレ wcwidth

最近なんとなく Win32 のコンソール API をごにょごにょしたくなる気分だったのです。なんか微妙に説明不足感が漂うんですよね、このへんって…。

ということで(どういうことなんだろう)、コンソールに表示する文字の幅を調べる wcwidth 的なやつ。素直に WideCharToMultiByte とかでやりゃいい気もしますが、せっかくだからコンソールに直接訊いてみたりなんかして。

ということでそれらしいソース。

/*
    int wcwidth_win32(WCHAR)
    
    あらかじめ init_wcwidth_win32(FALSE) を実行しておくこと 
*/

static BOOL bAllocConsoleByMyself = FALSE;
static HANDLE hHiddenConsole = INVALID_HANDLE_VALUE;

static
int
init_wcwidth_win32(BOOL bForceAllocConsole)
{
  HANDLE h;
  
  if (bForceAllocConsole && !bAllocConsoleByMyself) {
    bAllocConsoleByMyself = AllocConsole();
  }
  
  if (hHiddenConsole != INVALID_HANDLE_VALUE)
    return hHiddenConsole;
  
  h = CreateConsoleScreenBuffer(
          GENERIC_READ|GENERIC_WRITE,
          FILE_SHARE_READ|FILE_SHARE_WRITE,
          NULL,
          CONSOLE_TEXTMODE_BUFFER,
          NULL);
  
  if (h != INVALID_HANDLE_VALUE) {
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    memset(&csbi, 0, sizeof(csbi));
    if (GetConsoleScreenBufferInfo(h, &csbi) && csbi.dwSize.X >= 2) {
      SetConsoleMode(h, 0);
      hHiddenConsole = h;
    } else {
      CloseHandle(h);
      h = INVALID_HANDLE_VALUE;
    }
  }
  return h;
}

static
void
exit_wcwidth_win32()
{
  HANDLE h = hHiddenConsole;
  if (h != INVALID_HANDLE_VALUE) {
    hHiddenConsole = INVALID_HANDLE_VALUE;
    CloseHandle(h);
  }
  if (bAllocConsoleByMyself) {
    bAllocConsoleByMyself = FALSE;
    FreeConsole();
  }
}


int
wcwidth_win32(WCHAR wc)
{
  int rc = -1;
  if (hHiddenConsole != INVALID_HANDLE_VALUE) {
    COORD pos;
    CONSOLE_CURSOR_INFO cci, cci_prev;
    DWORD dw;
    WCHAR w[2];
    if (wc == 0) return 0;
    pos.X = 0; pos.Y = 0;
    memset(&cci, 0, sizeof(cci));
    GetConsoleCursorInfo(hHiddenConsole, &cci_prev);
    cci = cci_prev;
    cci.bVisible = FALSE;
    SetConsoleCursorInfo(hHiddenConsole, &cci);
    SetConsoleCursorPosition(hHiddenConsole, pos);
    w[0] = w[1] = 0;
    WriteConsoleW(hHiddenConsole, w, 2, NULL, NULL); /* clear buffer (just for a proof) */
    SetConsoleCursorPosition(hHiddenConsole, pos);
    w[0] = wc;
    dw = 0;
    if (WriteConsoleW(hHiddenConsole, w, 1 /* sizeof(w)/sizeof(w[0]) */, &dw, NULL)) {
      CONSOLE_SCREEN_BUFFER_INFO csbi;
      if (GetConsoleScreenBufferInfo(hHiddenConsole, &csbi)) {
        rc = csbi.dwCursorPosition.X;
      }
    }
    SetConsoleCursorInfo(hHiddenConsole, &cci_prev);
  }
  return rc;
}


要するにやってることは、「指定された WCHAR 文字コードを隠しコンソールバッファに WriteConsoleW で書きこみ、その際のカーソルの移動量を文字幅とみなす」ということです。隠しコンソールなので書き込んでもメインコンソールに影響は出ません。そのはずなのになぜカーソルの一時消去・再表示を行っているのかというと、「そうしないとメインコンソール内のカーソル表示が微妙にバグったから」です。DOS/V の $DISP.SYS じゃないんだからさ、まったくもう…。

しかしサロゲートペアが必要な文字とか、コンソールバッファ中ではどういう扱いになってるんでしょうねえ。単にまったく考慮してないだけのような気もしますが。