Как рисовать в консоли: курсор, цвет, вывод блоком, второй буфер и клавиши
CHAR_INFO и выводить её одним вызовомКонсоль Windows хранит текст в буфере экрана — прямоугольной таблице ячеек: столбцов обычно 120, строк — до нескольких тысяч (это и есть «прокрутка назад»). Каждая ячейка — символ и его атрибуты: цвет символа и цвет фона. Окно — видимая часть буфера. Курсор — ячейка, куда попадёт следующий символ вывода. По сути буфер экрана — матрица из раздела 8, а вывод std::cout — запись в её ячейки начиная с курсора.
// Буфер экрана и окно консоли: размеры, положение курсора, текущие атрибуты
#include <windows.h>
#include <iostream>
int main()
{
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
if (!GetConsoleScreenBufferInfo(out, &info)) {
std::cout << "Not a console: output is redirected\n";
return 2;
}
std::cout << "Buffer: " << info.dwSize.X << " x " << info.dwSize.Y << '\n';
std::cout << "Window: columns " << info.srWindow.Left << ".." << info.srWindow.Right
<< ", rows " << info.srWindow.Top << ".." << info.srWindow.Bottom << '\n';
std::cout << "Cursor: column " << info.dwCursorPosition.X << ", row " << info.dwCursorPosition.Y << '\n';
std::cout << "Attributes: " << info.wAttributes << '\n';
return 0;
}
> bufinfo
Buffer: 120 x 9001
Window: columns 0..119, rows 0..29
Cursor: column 0, row 1
Attributes: 7
GetStdHandle(STD_OUTPUT_HANDLE) — дескриптор, в который пишет std::cout; STD_INPUT_HANDLE — откуда читает std::cin. Эти дескрипторы программа получает от системы готовыми и не закрывает.COORD с полями X (столбец) и Y (строка), тип SHORT; отсчёт с нуля от левого верхнего угла буфера, а не окна.bufinfo > out.txt), дескриптор — уже не консоль, и функции консоли вернут FALSE. Поэтому результат GetConsoleScreenBufferInfo проверяют.// Позиция курсора и цвет: рамка с надписью в заданном месте консоли
#include <windows.h>
#include <iostream>
void moveTo(HANDLE out, int column, int row);
void setColor(HANDLE out, WORD attributes);
void drawFrame(HANDLE out, int left, int top, int width, int height, WORD attributes);
int main()
{
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
if (!GetConsoleScreenBufferInfo(out, &info)) {
std::cout << "Not a console\n";
return 2;
}
const WORD original = info.wAttributes; // вернуть в конце
const int top = info.dwCursorPosition.Y; // рисуем от текущей строки вниз
const WORD yellowOnBlue = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE;
const WORD whiteOnBlue = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY
| BACKGROUND_BLUE;
drawFrame(out, 4, top + 1, 30, 5, yellowOnBlue);
moveTo(out, 11, top + 3);
setColor(out, whiteOnBlue);
std::cout << "Hello, console!";
setColor(out, original);
moveTo(out, 0, top + 7); // курсор — под рамку
return 0;
}
// Курсор в столбец column строки row; накопленный вывод сначала выталкивается на экран
void moveTo(HANDLE out, int column, int row)
{
std::cout.flush();
const COORD position = {static_cast<SHORT>(column), static_cast<SHORT>(row)};
SetConsoleCursorPosition(out, position);
}
// Цвет следующего вывода; накопленный вывод — старым цветом
void setColor(HANDLE out, WORD attributes)
{
std::cout.flush();
SetConsoleTextAttribute(out, attributes);
}
// drawFrame — рамка + - | с заливкой внутри: в каждой строке moveTo и вывод символов; полностью — в примерах раздела
+----------------------------+
| |
| Hello, console! |
| |
+----------------------------+
std::cout.flush() перед каждой функцией консоли. std::cout копит вывод в своём буфере в памяти программы и отдаёт системе порциями. Если между << и SetConsoleCursorPosition не вытолкнуть накопленное, текст может появиться уже после перемещения курсора — не там — или не тем цветом. Поэтому moveTo и setColor сначала вызывают flush. Вывод блоком из пункта 04 этой проблемы не имеет.Программа, которая меняет цвет, возвращает исходный — info.wAttributes, запомненный в начале: иначе после неё вся консоль останется жёлтой на синем. Структура COORD из переменных int собирается только с static_cast<SHORT>: инициализация списком не разрешает сужения типа — narrowing conversion of 'column' from 'int' to 'SHORT'.
Атрибут ячейки — число WORD, в котором младшие 4 бита — цвет символа, следующие 4 — цвет фона. Каждый цвет собирается из трёх основных и яркости, как флаги из раздела 3:
| Бит | Символ | Значение | Фон | Значение |
|---|---|---|---|---|
| синий | FOREGROUND_BLUE | 1 | BACKGROUND_BLUE | 16 |
| зелёный | FOREGROUND_GREEN | 2 | BACKGROUND_GREEN | 32 |
| красный | FOREGROUND_RED | 4 | BACKGROUND_RED | 64 |
| яркость | FOREGROUND_INTENSITY | 8 | BACKGROUND_INTENSITY | 128 |
Красный + зелёный = жёлтый (6, яркий — 14), красный + зелёный + синий = светло-серый (7, яркий белый — 15), ничего = чёрный. Атрибут консоли по умолчанию — 7: светло-серый на чёрном. Число атрибута = фон · 16 + цвет символов; яркий жёлтый на синем — 1 · 16 + 14 = 30. Программа выводит все 256 сочетаний:
// Таблица атрибутов: число = фон · 16 + цвет символов, от 0 до 255
for (int background = 0; background < 16; ++background) {
for (int foreground = 0; foreground < 16; ++foreground) {
const int attributes = background * 16 + foreground;
std::cout.flush();
SetConsoleTextAttribute(out, static_cast<WORD>(attributes));
std::cout << std::setw(4) << attributes;
}
std::cout.flush();
SetConsoleTextAttribute(out, info.wAttributes); // перевод строки — обычным цветом
std::cout << '\n';
}
Цвет символа из атрибута — a & 0x0F, цвет фона — (a & 0xF0) >> 4. Инверсия (поменять цвет символа и фона местами — так выделяют пункт меню) — ((a & 0x0F) << 4) | ((a & 0xF0) >> 4).
WriteConsoleOutputAВывод по одному символу с перемещением курсора — сотни вызовов системы на картинку, и зритель видит, как она «рисуется». Быстрее подготовить всю картинку в памяти — в матрице ячеек CHAR_INFO, по ячейке на символ экрана, — и отдать системе одним вызовом:
// Вывод блоком: матрица ячеек CHAR_INFO готовится в памяти и выводится одним вызовом WriteConsoleOutputA
#include <windows.h>
#include <iostream>
constexpr int WIDTH = 32;
constexpr int HEIGHT = 8;
constexpr int SQUARE = 4; // ширина клетки в символах
void fillBoard(CHAR_INFO cells[][WIDTH]);
void putText(CHAR_INFO cells[][WIDTH], int row, int column, const char text[], WORD attributes);
bool drawCells(HANDLE out, const CHAR_INFO cells[][WIDTH], int left, int top);
int main()
{
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
if (!GetConsoleScreenBufferInfo(out, &info)) {
std::cout << "Not a console\n";
return 2;
}
CHAR_INFO cells[HEIGHT][WIDTH];
fillBoard(cells);
putText(cells, 3, 10, " BLOCK ", FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_GREEN
| BACKGROUND_BLUE);
const int top = info.dwCursorPosition.Y;
if (!drawCells(out, cells, 2, top)) {
std::cout << "WriteConsoleOutput failed, error " << GetLastError() << '\n';
return 2;
}
const COORD below = {0, static_cast<SHORT>(top + HEIGHT)};
SetConsoleCursorPosition(out, below);
return 0;
}
// Шахматная раскраска клетками SQUARE × 1: тёмно-серые и белые пробелы
void fillBoard(CHAR_INFO cells[][WIDTH])
{
for (int i = 0; i < HEIGHT; ++i) {
for (int j = 0; j < WIDTH; ++j) {
const bool dark = (i + j / SQUARE) % 2 == 0;
cells[i][j].Char.AsciiChar = ' ';
cells[i][j].Attributes = dark ? BACKGROUND_INTENSITY
: BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY;
}
}
}
// Строка text в ячейки строки row с позиции column; что не помещается — отбрасывается
void putText(CHAR_INFO cells[][WIDTH], int row, int column, const char text[], WORD attributes)
{
for (int k = 0; text[k] != '\0' && column + k < WIDTH; ++k) {
cells[row][column + k].Char.AsciiChar = text[k];
cells[row][column + k].Attributes = attributes;
}
}
// Вся матрица — в прямоугольник экранного буфера с левым верхним углом (left, top)
bool drawCells(HANDLE out, const CHAR_INFO cells[][WIDTH], int left, int top)
{
const COORD size = {WIDTH, HEIGHT}; // размер матрицы
const COORD from = {0, 0}; // с какой её ячейки брать
SMALL_RECT region = {static_cast<SHORT>(left), static_cast<SHORT>(top),
static_cast<SHORT>(left + WIDTH - 1), static_cast<SHORT>(top + HEIGHT - 1)};
return WriteConsoleOutputA(out, &cells[0][0], size, from, ®ion) != FALSE;
}
Параметр WriteConsoleOutputA | Смысл |
|---|---|
&cells[0][0] | адрес первой ячейки матрицы — строки лежат в памяти подряд (раздел 9, пункт 09). Сама матрица cells не подходит по типу: cannot convert 'CHAR_INFO (*)[10]' to 'const CHAR_INFO*' |
size | размер матрицы: столбцы и строки |
from | левая верхняя ячейка матрицы, с которой брать данные; обычно (0, 0) |
®ion | прямоугольник буфера экрана, куда писать: Left, Top, Right, Bottom включительно. На выходе функция записывает туда, что реально выведено (часть за краем буфера отрезается) |
Функции-помощники putText и drawCells из этого примера пригодятся в практической работе: всё рисование — это запись символов и атрибутов в матрицу, а показ — один вызов. Это та же обработка матрицы, что в разделе 8, и та же передача матрицы в функцию, что в разделе 10.
Заполнить подряд идущие ячейки одним символом или одним атрибутом — начиная с заданной ячейки и дальше по строкам — умеют две функции. Так очищают экран:
// Очищает весь буфер экрана пробелами цвета attributes и ставит курсор в левый верхний угол
void clearScreen(HANDLE out, WORD attributes)
{
CONSOLE_SCREEN_BUFFER_INFO info;
if (!GetConsoleScreenBufferInfo(out, &info)) {
return;
}
const DWORD cells = static_cast<DWORD>(info.dwSize.X) * info.dwSize.Y;
const COORD origin = {0, 0};
DWORD written = 0;
FillConsoleOutputCharacterA(out, ' ', cells, origin, &written);
FillConsoleOutputAttribute(out, attributes, cells, origin, &written);
SetConsoleCursorPosition(out, origin);
}
Эти функции не двигают курсор и не меняют текущий цвет вывода — только ячейки. Раскрасить готовую строку, не трогая текст, — FillConsoleOutputAttribute с её началом и длиной.
У консоли может быть несколько буферов экрана, а показывается один — активный. Если рисовать кадр прямо в видимом буфере, зритель видит промежуточные состояния: стёртый экран, половину картинки. Решение, как в играх: рисовать в невидимом буфере, а готовый кадр показывать переключением.
// Фрагмент backbuffer.cpp: мяч отскакивает от рамки, кадры чередуются между двумя буферами
HANDLE original = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE back[2];
for (int k = 0; k < 2; ++k) {
back[k] = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr);
if (back[k] == INVALID_HANDLE_VALUE) {
std::cout << "CreateConsoleScreenBuffer failed, error " << GetLastError() << '\n';
return 2;
}
}
CHAR_INFO cells[HEIGHT][WIDTH];
int x = 1;
int y = 1;
int dx = 1;
int dy = 1;
for (int frame = 0; frame < FRAMES; ++frame) {
HANDLE target = back[frame % 2]; // рисуем в тот, что сейчас не виден
drawField(cells, x, y);
drawCells(target, cells);
SetConsoleActiveScreenBuffer(target); // показать готовый кадр
Sleep(FRAME_MS);
if (x + dx < 1 || x + dx > WIDTH - 2) { // отскок от рамки
dx = -dx;
}
if (y + dy < 1 || y + dy > HEIGHT - 2) {
dy = -dy;
}
x += dx;
y += dy;
}
SetConsoleActiveScreenBuffer(original); // вернуть исходный буфер со всем его текстом
CloseHandle(back[0]);
CloseHandle(back[1]);
CreateConsoleScreenBuffer закрывают. Ошибка — INVALID_HANDLE_VALUE.SetConsoleActiveScreenBuffer(original) перед выходом — иначе пользователь останется с последним кадром.std::cout всегда пишет в исходный буфер. Во второй буфер пишут функциями консоли с его дескриптором: WriteConsoleOutputA, SetConsoleCursorPosition(back, …), WriteConsoleA.Sleep(миллисекунды) задерживает поток — скорость анимации. Кадр в 40 мс — 25 кадров в секунду.std::cin ждёт Enter и не видит стрелок. Консоль отдаёт каждое событие ввода — нажатие и отпускание клавиши, действие мышью, изменение размера окна — в структуре INPUT_RECORD. Её читают ReadConsoleInputA с дескриптора ввода:
// Ждёт нажатия клавиши и возвращает её виртуальный код (VK_UP, VK_RETURN…); 0 — ошибка чтения
WORD readKey(HANDLE in)
{
INPUT_RECORD record;
DWORD read = 0;
while (ReadConsoleInputA(in, &record, 1, &read)) {
if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) {
return record.Event.KeyEvent.wVirtualKeyCode;
}
}
return 0;
}
| Клавиша | Код | Клавиша | Код |
|---|---|---|---|
| стрелки | VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT | Enter, Esc, пробел | VK_RETURN, VK_ESCAPE, VK_SPACE |
| Home, End | VK_HOME, VK_END | цифры и буквы | код символа: '0'…'9', 'A'…'Z' (заглавная) |
ReadConsoleInputA ждёт, пока событие появится. Для анимации ждать нельзя — кадры должны идти сами, а клавиша только прерывает. Тогда сначала спрашивают, есть ли события, и читают только если есть:
// Последняя нажатая клавиша из уже накопленных событий ввода; 0 — нажатий нет. Не ждёт
WORD pollKey(HANDLE in)
{
WORD key = 0;
DWORD pending = 0;
INPUT_RECORD record;
DWORD read = 0;
while (GetNumberOfConsoleInputEvents(in, &pending) && pending > 0 && ReadConsoleInputA(in, &record, 1, &read)) {
if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) {
key = record.Event.KeyEvent.wVirtualKeyCode;
}
}
return key;
}
Цикл анимации: while (pollKey(in) != VK_ESCAPE) { кадр; Sleep; }. Меню — наоборот, с ожиданием: нарисовать, readKey, изменить выбор, перерисовать. Полная программа меню со стрелками, выделением инверсией и кодом завершения — menu.cpp в примерах раздела. Мигающий курсор на время рисования прячут: GetConsoleCursorInfo, bVisible = FALSE, SetConsoleCursorInfo — и возвращают перед выходом.
В примерах курса рамки рисуются символами + - |: они есть в любой кодовой странице. Красивые линии ┌─┐│└┘ в кодовой странице 1251 (решение курса Д2, раздел 2) отсутствуют — в ней на их местах русские буквы. Они есть в странице 866 — «досовской» кодировке консоли: '\xDA' — ┌, '\xC4' — ─, '\xBF' — ┐, '\xB3' — │, '\xC0' — └, '\xD9' — ┘, '\xDB' — █. Чтобы ими рисовать, программа ставит SetConsoleOutputCP(866), а русский текст тогда пишет в 866. Для практической работы достаточно + - | #; псевдографика — по желанию.
| Ошибка | Признак | Сообщение g++ | Как избежать |
|---|---|---|---|
COORD из int списком | не собирается | narrowing conversion of 'column' from 'int' to 'SHORT' | static_cast<SHORT> |
| матрица вместо адреса первой ячейки | не собирается | cannot convert 'CHAR_INFO (*)[10]' to 'const CHAR_INFO*' | &cells[0][0] |
нет flush перед функцией консоли | текст не там или не тем цветом | нет | std::cout.flush() в moveTo и setColor |
| цвет не возвращён | консоль после программы — чужого цвета | нет | запомнить info.wAttributes, вернуть в конце |
| не возвращён исходный буфер | после выхода — последний кадр, текста консоли нет | нет | SetConsoleActiveScreenBuffer(original) |
std::cout во второй буфер | вывод не виден, пока активен второй | нет | во второй буфер — функциями консоли с его дескриптором |
| перепутаны строка и столбец | картинка повёрнута или обрезана | нет | X — столбец, Y — строка; cells[строка][столбец] |
Right/Bottom как размер, а не последняя ячейка | лишний столбец и строка мусора | нет | left + WIDTH - 1 |
в цикле анимации readKey | кадр меняется только при нажатии | нет | pollKey — без ожидания |
| курсор не показан обратно | после программы курсора в консоли нет | нет | bVisible = TRUE перед выходом |
COORD: X — столбец, Y — строкаGetStdHandle; результат функций консоли проверяютSetConsoleCursorPosition, SetConsoleTextAttribute; перед ними — flush; цвет возвращаютCHAR_INFO, показ — один WriteConsoleOutputA с адресом &cells[0][0]SetConsoleActiveScreenBuffer; исходный буфер возвращаютReadConsoleInputA; без ожидания — после GetNumberOfConsoleInputEventsprintError. Нужен для: раздела 16 — поток рисует в консоли, пока другой считает; итогового проекта — «Жизнь» Конвея и консольный чат с окном во втором буфере.