Раздел 15 — Буфер экрана консоли

Как рисовать в консоли: курсор, цвет, вывод блоком, второй буфер и клавиши

Прогресс курса Раздел 15 из 20

Что вы освоите в этом разделе

2 академических часа теории и 4 часа практики: практическая работа №16 «Работа с буфером экрана» — на портале. Это первая практика на Windows API. Программы раздела запускаются в обычной консоли Windows (cmd, PowerShell). В Windows 11 по умолчанию открывается Терминал Windows — он поддерживает все функции раздела, но если что-то рисуется не так, запустите программу в классической консоли: Параметры → Конфиденциальность и защита → Для разработчиков → Терминал → «Узел консоли Windows».
01

Консоль изнутри: буфер и окно

Консоль 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
02

Курсор и цвет

// Позиция курсора и цвет: рамка с надписью в заданном месте консоли
#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'.

03

Атрибуты: биты цвета

Атрибут ячейки — число WORD, в котором младшие 4 бита — цвет символа, следующие 4 — цвет фона. Каждый цвет собирается из трёх основных и яркости, как флаги из раздела 3:

БитСимволЗначениеФонЗначение
синийFOREGROUND_BLUE1BACKGROUND_BLUE16
зелёныйFOREGROUND_GREEN2BACKGROUND_GREEN32
красныйFOREGROUND_RED4BACKGROUND_RED64
яркостьFOREGROUND_INTENSITY8BACKGROUND_INTENSITY128

Красный + зелёный = жёлтый (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).

04

Вывод блоком: 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, &region) != FALSE;
}
Параметр WriteConsoleOutputAСмысл
&cells[0][0]адрес первой ячейки матрицы — строки лежат в памяти подряд (раздел 9, пункт 09). Сама матрица cells не подходит по типу: cannot convert 'CHAR_INFO (*)[10]' to 'const CHAR_INFO*'
sizeразмер матрицы: столбцы и строки
fromлевая верхняя ячейка матрицы, с которой брать данные; обычно (0, 0)
&regionпрямоугольник буфера экрана, куда писать: Left, Top, Right, Bottom включительно. На выходе функция записывает туда, что реально выведено (часть за краем буфера отрезается)

Функции-помощники putText и drawCells из этого примера пригодятся в практической работе: всё рисование — это запись символов и атрибутов в матрицу, а показ — один вызов. Это та же обработка матрицы, что в разделе 8, и та же передача матрицы в функцию, что в разделе 10.

05

Очистка и заливка

Заполнить подряд идущие ячейки одним символом или одним атрибутом — начиная с заданной ячейки и дальше по строкам — умеют две функции. Так очищают экран:

// Очищает весь буфер экрана пробелами цвета 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 с её началом и длиной.

06

Второй буфер: анимация без мерцания

У консоли может быть несколько буферов экрана, а показывается один — активный. Если рисовать кадр прямо в видимом буфере, зритель видит промежуточные состояния: стёртый экран, половину картинки. Решение, как в играх: рисовать в невидимом буфере, а готовый кадр показывать переключением.

// Фрагмент 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]);
07

Клавиши

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_RIGHTEnter, Esc, пробелVK_RETURN, VK_ESCAPE, VK_SPACE
Home, EndVK_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 — и возвращают перед выходом.

08

Рамки и кодовые страницы

В примерах курса рамки рисуются символами + - |: они есть в любой кодовой странице. Красивые линии ┌─┐│└┘ в кодовой странице 1251 (решение курса Д2, раздел 2) отсутствуют — в ней на их местах русские буквы. Они есть в странице 866 — «досовской» кодировке консоли: '\xDA' — ┌, '\xC4' — ─, '\xBF' — ┐, '\xB3' — │, '\xC0' — └, '\xD9' — ┘, '\xDB' — █. Чтобы ими рисовать, программа ставит SetConsoleOutputCP(866), а русский текст тогда пишет в 866. Для практической работы достаточно + - | #; псевдографика — по желанию.

09

Ловушки раздела

ОшибкаПризнакСообщение 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; цвет возвращают
  • Атрибут = фон · 16 + цвет символов; цвет — сумма бит синего, зелёного, красного, яркости
  • Картинка — матрица CHAR_INFO, показ — один WriteConsoleOutputA с адресом &cells[0][0]
  • Анимация — во втором буфере и переключение SetConsoleActiveScreenBuffer; исходный буфер возвращают
  • Клавиши — ReadConsoleInputA; без ожидания — после GetNumberOfConsoleInputEvents
Связи раздела. Опирается на раздел 3 — побитовые операции для атрибутов; раздел 8 — матрицы, обход вложенными циклами; раздел 9 — адрес первого элемента, строки матрицы подряд в памяти; раздел 10 — матрица в функции; раздел 14 — дескрипторы, проверка результатов, printError. Нужен для: раздела 16 — поток рисует в консоли, пока другой считает; итогового проекта — «Жизнь» Конвея и консольный чат с окном во втором буфере.
Практическая работа №16 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 14: Ресурсы ОС и процессы Практика на портале