✍️

【ラッパー有】WriteConsoleOutputの使い方

2024/09/24に公開

はじめに

Windows APIのWriteConsoleOutputを使う機会があったので、備忘録を兼ねて記事にまとめます。

基本的にはQiitaのこちらの記事を参考にし、個人的な用途に合わせて簡易的なラッパーを作成しました。

動機

いくつかの選択肢がコンソールに表示され、ユーザの入力を方向キーで受け付ける場面を想定します。

  1. std::coutで選択肢を表示する。
  2. Windows APIのGetAsyncKeyStateで入力を受け付ける。
  3. エンターキーが押されたら終了、それ以外なら次へ。
  4. std::system("cls")でコンソールの表示を消去する。
  5. 現在地を示すもの(色・矢印など)を入力に合わせて変更した選択肢を、改めてstd::coutで表示する。
  6. 手順2に戻る。

以上のプログラムを実装すると、例えば次のようになります。

main.cpp
#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <Windows.h>

int inputSelection() {

    const int time = 150;

    while (true) {
        if (GetAsyncKeyState(VK_UP)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(time));
            return VK_UP;
        }
        if (GetAsyncKeyState(VK_DOWN)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(time));
            return VK_DOWN;
        }
        if (GetAsyncKeyState(VK_RETURN)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(time));
            return VK_RETURN;
        }
    }
}


int main() {

    std::vector<std::string> options = { "option 1", "option 2", "option 3", "option 4", "option 5" };
    const int size = options.size();
    int current_place = 0;

    while (true) {
        std::vector<std::string> msg(size);
        for (int i = 0; i < size; i++) {
            std::string prefix;
            if (i == current_place) {
                prefix = "⇒ ";
            }
            else {
                prefix = " ";
            }
            msg[i] = prefix + options[i];
        }

        std::system("cls");
        for (int i = 0; i < size; i++) {
            std::cout << msg[i] << std::endl;
        }

        int input = inputSelection();
        if (input == VK_UP) {
            if (current_place == 0) {
                current_place = size - 1;
            }
            else {
                current_place--;
            }
        }
        else if (input == VK_DOWN) {
            if (current_place == size - 1) {
                current_place = 0;
            }
            else {
                current_place++;
            }
        }
        else if (input == VK_RETURN) {
            break;
        }
    }

    std::cout << std::endl << options[current_place] << " is selected." << std::endl;
    return 0;
}

上記プログラムを実行してみると、表示された文字が点滅してしまうことが分かります。

これは、std::system("cls")で表示を消去しているのが原因です。

点滅を避けるためには、消去するのではなく、既存の表示に上書きする必要がありますが、
C++の標準ライブラリではそれができないようです。

そこで、より自由度が高く、既存の表示に上書きできるWindows APIのWriteConsoleOutputを使います。

WriteConsoleOutput関数

コンソールへの表示を便利にする

Microsoftのリファレンスも勉強になりますが、具体的なコードが示された記事がせっかくあるので、先人に感謝しながらこちらを参考にします。

WriteConsoleOutputは、HANDLECHAR_INFO、2つのCOORDSMALL_RECTを引数に取ります。

本記事ではクラスとしてラッパーを作るので、これらの変数をprivateメンバにしておきます。

console_util.h
#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <Windows.h>

constexpr int SCREEN_WIDTH = 100;
constexpr int SCREEN_HEIGHT = 100;

class Console {
public:
    Console();
    ~Console() {};

private:
    HANDLE _handle;
    COORD _buffer_size;
    COORD _start;
    SMALL_RECT _sr;
    CHAR_INFO _cell[SCREEN_HEIGHT][SCREEN_WIDTH];
};
console_util.cpp
Console::Console() {
    _handle = GetStdHandle(STD_OUTPUT_HANDLE);
    _buffer_size = { SCREEN_WIDTH, SCREEN_HEIGHT };
    _start = { 0, 0 };
    _sr = { 0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1 };
    ZeroMemory(_cell, sizeof(_cell));
}

表示のためのメンバ関数showを定義します。

引数のConsoleMessageConsoleStringについては次の項目で説明します。

console_util.cpp
void Console::show(const ConsoleMessage& msg) {
    const int size_x = msg.getSizeX();
    const int size_y = msg.getSizeY();

    for (int y = 0; y < size_y; y++) {
        int cell_x = 0;
        for (int x = 0; x < size_x; x++) {
            ConsoleString cs = msg.get(x, y);
            std::wstring wstr = cs.str;
            int color = cs.color_background * 16 + cs.color_str;
            for (int str_i = 0, sz = wstr.size(); str_i < sz; str_i++, cell_x++) {
                _cell[y][cell_x].Char.UnicodeChar = wstr[str_i];
                _cell[y][cell_x].Attributes = color;
            }
        }
    }
    WriteConsoleOutput(_handle, (CHAR_INFO*)_cell, _buffer_size, _start, &_sr);
    ZeroMemory(_cell, sizeof(_cell));
}

私は日本語を表示したかったのでstd::wstringを使いました。
_cellの各要素に文字を一つずつ入れていきますが、std::wstringの場合はUnicodeCharに情報を格納します。

また、WriteConsoleOutputでは、文字とその背景に色をつけることができます。
文字色と背景色をそれぞれ0~15の数値で指定し、上記プログラム中の式によってその状態を一意に表す数値を計算します。
数値と色の対応については、下図をご覧ください。(0は黒です)

これで、表示したい文字と色の情報をConsoleMessageに入れるだけでWriteConsoleOutputを使えるようになりました。

表示する情報の管理を便利にする

WriteConsoleOutput関数に渡す情報として、ユーザは以下の設定をする必要があります。

  • 文字情報std::string or std::wstring
  • 文字の色int
  • 背景の色int

どれも簡単な情報なので、構造体にまとめておけば十分だと思います。

色のデフォルト値は、文字が白、背景が黒です。
より厳密には、定数宣言したものを代入するほうが良いでしょう。

console_util.h
struct ConsoleString {
    std::wstring str;
    int color_str = 15;
    int color_background = 0;
};

上記の構造体を要素に持つstd::vectorで情報を管理します。

私はConsoleMessageというクラスを作成しましたが、今振り返ってみると、そのままのstd::vectorでも良かったと思います。

console_util.h
class ConsoleMessage {
public:
    ConsoleMessage() {};
    ConsoleMessage(int size_x, int size_y);
    ConsoleMessage(const ConsoleMessage& msg);
    ~ConsoleMessage() {};

    void resize(int size_x, int size_y);

    void set(int x, int y, const ConsoleString& cs);
    void set(int x, int y, const std::wstring& str, int color_str = 15, int color_background = 0);

    ConsoleString get(int x, int y) const;
    int getSizeX() const;
    int getSizeY() const;

private:
    int _size_x = 0;
    int _size_y = 0;
    std::vector<std::vector<ConsoleString>> _msg;
};

ラッパーを使ってみる

最終的なラッパーを以下に示します。

上記のプログラムに機能を少し足しています。

console_util.h
#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <Windows.h>

constexpr int SCREEN_WIDTH = 100;
constexpr int SCREEN_HEIGHT = 100;

int inputSelection();

struct ConsoleString {
    std::wstring str;
    int color_str = 15;
    int color_background = 0;
};

class ConsoleMessage {
public:
    ConsoleMessage() {};
    ConsoleMessage(int size_x, int size_y);
    ConsoleMessage(const ConsoleMessage& msg);
    ~ConsoleMessage() {};

    void resize(int size_x, int size_y);

    void set(int x, int y, const ConsoleString& cs);
    void set(int x, int y, const std::wstring& str, int color_str = 15, int color_background = 0);

    ConsoleString get(int x, int y) const;
    int getSizeX() const;
    int getSizeY() const;

private:
    int _size_x = 0;
    int _size_y = 0;
    std::vector<std::vector<ConsoleString>> _msg;
};

class Console {
public:
    Console();
    ~Console() {};

    static const int COLOR_BLACK = 0; static const int COLOR_BLUE = 3;  static const int COLOR_RED = 4; static const int COLOR_GRAY = 7;
    static const int COLOR_GREEN = 10; static const int COLOR_LIGHT_BLUE = 11; static const int COLOR_WHITE = 15;

    void show(const ConsoleMessage& msg);
    void showUsingCache(const ConsoleMessage& msg);

private:
    ConsoleMessage _msg_cache;
    HANDLE _handle;
    COORD _buffer_size;
    COORD _start;
    SMALL_RECT _sr;
    CHAR_INFO _cell[SCREEN_HEIGHT][SCREEN_WIDTH];
};
console_util.cpp
//////////////////////////////////////////////////////////////////////////////////////////////
int inputSelection() {

    const int time = 150;

    while (true) {
        if (GetAsyncKeyState(VK_UP)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(time));
            return VK_UP;
        }
        if (GetAsyncKeyState(VK_DOWN)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(time));
            return VK_DOWN;
        }
        if (GetAsyncKeyState(VK_RETURN)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(time));
            return VK_RETURN;
        }
    }
}

//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
ConsoleMessage::ConsoleMessage(int size_x, int size_y) {
    resize(size_x, size_y);
}

//////////////////////////////////////////////////////////////////////////////////////////////
ConsoleMessage::ConsoleMessage(const ConsoleMessage& msg) {
    const int size_x = msg.getSizeX();
    const int size_y = msg.getSizeY();
    resize(size_x, size_y);
    for (int y = 0; y < size_y; y++) {
        for (int x = 0; x < size_x; x++) {
            ConsoleString cs = msg.get(x, y);
            set(x, y, cs);
        }
    }
}

//////////////////////////////////////////////////////////////////////////////////////////////
void ConsoleMessage::resize(int size_x, int size_y) {
    _size_x = size_x;
    _size_y = size_y;
    _msg.resize(size_y, std::vector<ConsoleString>(size_x));
}

//////////////////////////////////////////////////////////////////////////////////////////////
void ConsoleMessage::set(int x, int y, const ConsoleString& cs) {
    set(x, y, cs.str, cs.color_str, cs.color_background);
}

//////////////////////////////////////////////////////////////////////////////////////////////
void ConsoleMessage::set(int x, int y, const std::wstring& str, int color_str, int color_background) {
    _msg[y][x].str = str;
    _msg[y][x].color_str = color_str;
    _msg[y][x].color_background = color_background;
}

//////////////////////////////////////////////////////////////////////////////////////////////
int ConsoleMessage::getSizeX() const {
    return _size_x;
}

//////////////////////////////////////////////////////////////////////////////////////////////
int ConsoleMessage::getSizeY() const {
    return _size_y;
}

//////////////////////////////////////////////////////////////////////////////////////////////
ConsoleString ConsoleMessage::get(int x, int y) const {
    return _msg[y][x];
}


//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
Console::Console() {
    _handle = GetStdHandle(STD_OUTPUT_HANDLE);
    _buffer_size = { SCREEN_WIDTH, SCREEN_HEIGHT };
    _start = { 0, 0 };
    _sr = { 0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1 };
    ZeroMemory(_cell, sizeof(_cell));
}

//////////////////////////////////////////////////////////////////////////////////////////////
void Console::show(const ConsoleMessage& msg) {
    const int size_x = msg.getSizeX();
    const int size_y = msg.getSizeY();
    _msg_cache.resize(size_x, size_y);

    for (int y = 0; y < size_y; y++) {
        int cell_x = 0;
        for (int x = 0; x < size_x; x++) {
            ConsoleString cs = msg.get(x, y);
            std::wstring wstr = cs.str;
            int color = cs.color_background * 16 + cs.color_str;
            for (int str_i = 0, sz = wstr.size(); str_i < sz; str_i++, cell_x++) {
                _cell[y][cell_x].Char.UnicodeChar = wstr[str_i];
                _cell[y][cell_x].Attributes = color;
            }
            // set cache
            _msg_cache.set(x, y, cs);
        }
    }
    WriteConsoleOutput(_handle, (CHAR_INFO*)_cell, _buffer_size, _start, &_sr);
    ZeroMemory(_cell, sizeof(_cell));
}

//////////////////////////////////////////////////////////////////////////////////////////////
void Console::showUsingCache(const ConsoleMessage& msg) {
    if (_msg_cache.getSizeX() < msg.getSizeX()) {// can't show all msg
        return;
    }

    ConsoleMessage new_msg(_msg_cache);

    const int cache_size_x = _msg_cache.getSizeX();
    const int cache_size_y = _msg_cache.getSizeY();
    const int msg_size_x = msg.getSizeX();
    const int msg_size_y = msg.getSizeY();
    const int new_msg_size_y = cache_size_y + msg_size_y;

    new_msg.resize(cache_size_x, new_msg_size_y);

    for (int new_y = cache_size_y, msg_y = 0; new_y < new_msg_size_y; new_y++, msg_y++) {
        for (int x = 0; x < msg_size_x; x++) {
            new_msg.set(x, new_y, msg.get(x, msg_y));
        }
    }
    show(new_msg);
}

ラッパーを使ってはじめのプログラムを改善すると、次のようになります。

これを実行してみると、はじめのプログラムにはあった点滅がないことが確認できます。

main.cpp
int main() {

    Console console;

    const int size_x = 1;
    const int size_y = 5;
    ConsoleMessage options(size_x, size_y);
    for (int y = 0; y < size_y; y++) {
        options.set(0, y, L"option " + std::to_wstring(y + 1));
    }

    int current_place = 0;
	
    while (true) {
        ConsoleMessage msg(size_x, size_y);
        for (int y = 0; y < size_y; y++) {
            std::wstring prefix;
            if (y == current_place) {
                prefix = L"⇒ ";
            }
            else {
                prefix = L" ";
            }
            ConsoleString cs = options.get(0, y);
            cs.str = prefix + cs.str;
            msg.set(0, y, cs);
        }

        console.show(msg);

        int input = inputSelection();
        if (input == VK_UP) {
            if (current_place == 0) {
                current_place = size_y - 1;
            }
            else {
                current_place--;
            }
        }
        else if (input == VK_DOWN) {
            if (current_place == size_y - 1) {
                current_place = 0;
            }
            else {
                current_place++;
            }
        }
        else if (input == VK_RETURN) {
            break;
        }
    }
	
    ConsoleMessage add_msg(1, 2);
    add_msg.set(0, 1, options.get(0, current_place).str + L" is selected.");
	
    console.showUsingCache(add_msg);
    std::this_thread::sleep_for(std::chrono::seconds(3));

    return 0;
}

おわりに

本記事では、WriteConsoleOutputで簡単に文字を表示するためのラッパーを紹介しました。

質問や疑問などは、コメント欄かX(Twitter)でご連絡ください。

本記事が少しでもお役に立てば幸いです。

Discussion