В универе задали лабораторную: реализовать элемент интерфейса “всплывающее окно с сообщением” под DOS на C++, используя ООП подход. Ну спасибо тебе за актуальность, дорогой универ, нынче DOS и Turbo Vision наше всё, такой ценный опыт… К счастью, преподаватель отнёсся с пониманием и разрешил использовать современную кроссплатформенную библиотеку ncurses для рисования псевдографики, о ней я сегодня и расскажу.

Долой пиксели, даешь знакоместа!

Что такое ncurses и где оно обитает

Ncurses (произносится “энкёрсиз”) – это библиотека, которая существенно упрощает создание интерфейсов на псевдографике и позволяет при разработке вообще не думать о том, например, какую экранирующую последовательность символов нужно послать терминалу для того, чтобы текст выводился ^[[0;31;40mКРАСНЫМ цветом. Мало того, что эти закорючки тяжело осмыслить, так они еще и для разных терминалов разные бывают. Вообще, чтобы как-то облегчить жизнь, разработчики UNIX решили хранить в специальном файле termcap (а в будущем terminfo) все те возможности, которые предоставляет конкретный терминал. Этот файл позволяет приложениям смотреть, какие экранирующие последовательности можно посылать, в него смотрит и библиотека ncurses, избавляя пользователя-программиста от написания лишнего кода для, так скажем, кросс-терминальности. Пожалуй, самыми известными программами, в которых для интерфейса используется ncurses, являются htop, GNU Midnight Commander и инструмент графической настройки конфигурации ядра Linux, хоть с какой-то из них сталкивался каждый.

Как установить

Mac

    brew install ncurses

Linux

    sudo apt-get install libncurses5-dev libncursesw5-dev

Windows

Под Windows и DOS есть PDCurses, которая практически полностью совместима с ncurses. Одной строчкой не ставится, вот тут есть вся необходимая информация по ней.

Написание программ

При компиляции программ важно не забывать приписать флаг -lncurses, чтобы всё нормально залинковалось. Разберем простейшую программу с комментариями и пояснениями, и заглянем глубже.

Hello World!

    #include <ncurses.h>

    int main()
    {
        initscr();              // Инициализация и переход в curses режим
        printw("Hello World!"); // Напечатать строку в воображаемое окно
        refresh();              // Вывести на настоящий экран изменения
        getch();                // Ждать нажатие клавиши
        endwin();               // Освобождение памяти, переключение терминала в обычный режим

        return 0;
    }

Вот тот странный момент с printw и refresh, да, сейчас объясню. Дело в том, что для производительности будет лучше, если программист сначала сделает все необходимые операции по изменению данных на воображаемом экране (в буффере), и только потом (после refresh) на настоящем экране обновятся те участки, которые действительно изменились в конечном итоге.

Инициализация

Помимо initscr есть и другие функции, которые можно повызывать для кастомизации текущей сессии. Рассмотрим лишь некоторые из них, чтобы статья не превратилась в документацию.

raw и cbreak

Обычно, терминал буфферит символы, которые набирает пользователь до переноса строки, но большинству программ набираемые символы требуются сразу же во время их набора. Указанные выше функции позволяют управлять буфферизацией. Разница заключается в том, как будут обрабатывается управляющие последовательности вроде CTRL-Z и CTRL-C. В raw режиме они попадают в программу, не генерируя сигналов, а в cbreak режиме они интерпретируются драйвером терминала и генерируют соответствующие им сигналы.

echo и noecho

Эти функции контролируют вывод напечатанного на экран. noecho отключает вывод, echo наоборот включает. Обычно в программах используется noecho и вывод производится другими, более гибкими средствами.

keypad

Позволяет чтение клавиш F1, F2, стрелок и тд. Чтобы включить для стандартного экрана, вызывается как keypad(stdscr, TRUE).

curs_set;

Включает и отключает отображения курсора на экране, чтобы отключить курсор вызывается curs_set(FALSE)

Цвета

В ncurses есть удобный механизм для работы с цветами. В первую очередь, для того, чтобы проверить, поддерживается ли в текущем терминале функция цветов вообще, есть функция has_colors(), если вернулась истина, то start_color() и погнали. Важное понятие в ncurses – это цветовые пары, они определяют, каким цветом мы выводим и на каком фоне. Парам присваивают целочисленные значения:

  init_pair(1, COLOR_RED, COLOR_BLACK)

Мы сделали цветовую пару, которая пишет красным по черному, кстати, если лень писать константы, можно писать просто соответствующие им циферки:

Цвет Циферка
COLOR_BLACK 0
COLOR_RED 1
COLOR_GREEN 2
COLOR_YELLOW 3
COLOR_BLUE 4
COLOR_MAGENTA 5
COLOR_CYAN 6
COLOR_WHITE 7

Ну а если стандартные цвета недостаточно кислотные для вашей хакерской проги, то можно сделать свои! Вот так это делается:

/**
 * @param 1 как назовем цвет
 * @param 2 сколько красного
 * @param 3 сколько зеленого
 * @param 4 сколько синего
 */
init_color(COLOR_RED, 700, 0, 0);

RGB-значения лежат в интервале от 0 до 1000. Если что-то пошло не так, или ваш терминал не умеет работать с цветами, то функция init_color вернет ERR.

Окна

Окна – это воображаемые экраны, определенные в curses. Окно в данном контексте не означает то окно с рамочкой в Turbo Pascal, которое можно таскать по экрану. При инициализации ncurses, создается воображаемое окно stdscr, которое является представлением открытого терминала (настоящего экрана) размером с этот терминал. На это окно уже можно сразу выводить строки или другие окна, передавать его как параметр в функцию и тд.

  printw("Hi There !!!");
  refresh();

Данный код выведет строку в stdscr в текущей позиции курсора. Аналогично вызов refresh, работает только с stdscr.

После создания нового окна, для выполнения таких же действий с ним, а не с stdscr, вызывают функции, которые начинаются на w и принимают как параметр окно, с которым производятся действия, вот так для окна с именем win:

  wprintw(win, "Hi There !!!");
  wrefresh(win);

Окно с именем win имеется в виду, что это имя указателя на память данных с окном в куче. Такая конвенция именования соблюдается во всех функциях, которые можно применить и к stdscr, и к другим окнам.

Таким образом, библиотека предоставляет разработчику все необходимые механизмы для создания интерфейсов в терминале, код своей сделанной лабораторной с комментариями я привел ниже.

Лабораторная

Message.h

    #ifndef MESSAGE_H
    #define MESSAGE_H

    #include <ncurses.h>

    /**
     * Задача:
     * Разработать визуальную компоненту Message (окно с сообщением) с различными вариантами оформления: без pамки (Mode=0), одинаpная pамка (Mode=1).
     */

    class Message
    {
       private:
          WINDOW *mainWindow, *messageWindow;
          char *message;
          int height, width, top, left, mainCP, msgCP, bgCP;
          bool borders, hidden;
          void drawWindows();
          void eraseWindows();
       public:
          Message(int left, int top, char *message, int mainCP, int msgCP, int bgCP, bool borders);
          ~Message();
          void Show();
          void Hide();
          void moveUp();
          void moveDown();
          void moveLeft();
          void moveRight();
    };

    #endif

Message.cpp

    #include "Message.h"

    /**
     * Конструктор окна сообщения
     * @param left    координата левого верхнего угла по x
     * @param top     координата левого верхнего угла по y
     * @param message сообщение к выводу
     * @param borders флаг для полей вокруг сообщения
     */
    Message::Message(int left, int top, char *message, int mainCP, int msgCP, int bgCP, bool borders)
    {
       this->height  = 10;
       this->width   = 30;
       this->top     = top;
       this->left    = left;
       this->borders = borders;
       this->message = message;
       this->mainCP  = mainCP;
       this->msgCP   = msgCP;
       this->bgCP    = bgCP;
       this->drawWindows();
    }

    /**
     * Деструктор
     */
    Message::~Message()
    {
       this->eraseWindows();
       delwin(this->mainWindow);
       delwin(this->messageWindow);
    }

    /**
     * Метод для перерисовки окна сообщения
     */
    void Message::drawWindows()
    {
       if (!this->hidden)
       {
          this->mainWindow = newwin(this->height,
                                    this->width,
                                    this->top,
                                    this->left);
          if (this->borders)
             box(this->mainWindow, 0 , 0);
          wbkgd(this->mainWindow,COLOR_PAIR(this->mainCP));
          mvwaddstr(mainWindow, 1, 3, "You have got a message!");
          wrefresh(this->mainWindow);

          this->messageWindow = newwin(6,
                                       26,
                                       this->top + 3,
                                       this->left + 2);
          wbkgd(this->messageWindow,COLOR_PAIR(this->msgCP));
          mvwaddstr(messageWindow, 0, 1, this->message);
          wrefresh(this->messageWindow);

          refresh();
       }
    }

    /**
     * Метод для стирания окна сообщения
     */
    void Message::eraseWindows()
    {
       if (this->borders)
          wborder(this->mainWindow, ' ', ' ', ' ',' ',' ',' ',' ',' ');
       wbkgd(mainWindow, COLOR_PAIR(this->bgCP));
       wclear(this->mainWindow);
       wrefresh(this->mainWindow);
       refresh();
    }

    /**
     * Передвинуть окно сообщения на один знакосимвол влево
     */
    void Message::moveLeft()
    {
       this->left--;
       this->eraseWindows();
       this->drawWindows();
    }

    /**
     * Передвинуть окно сообщения на один знакосимвол вправо
     */
    void Message::moveRight()
    {
       this->left++;
       this->eraseWindows();
       this->drawWindows();
    }

    /**
     * Передвинуть окно сообщения на один знакосимвол вверх
     */
    void Message::moveUp()
    {
       this->top--;
       this->eraseWindows();
       this->drawWindows();
    }

    /**
     * Передвинуть окно сообщения на один знакосимвол вниз
     */
    void Message::moveDown()
    {
       this->top++;
       this->eraseWindows();
       this->drawWindows();
    }

    /**
     * Скрыть окно
     */
    void Message::Hide()
    {
       this->eraseWindows();
       this->hidden = true;
    }

    /**
     * Показать окно
     */
    void Message::Show()
    {
       this->hidden = false;
       this->drawWindows();
    }

main.cpp

    #include <ncurses.h>
    #include "Message.h"

    int main()
    {
       // Инициализация знакосимвольного пространства и цветовых пар
       initscr();
       start_color();
       init_pair(1,COLOR_YELLOW,COLOR_BLUE);
       init_pair(2,COLOR_RED,COLOR_GREEN);
       init_pair(3,COLOR_CYAN,COLOR_BLACK);

       // Отключение вывода с клавы и курсора, всякого буфферинга
       cbreak();
       keypad(stdscr, TRUE);
       noecho();
       curs_set(FALSE);

       // Задание размеров и цветов для окна сообщения
       int starty = 10, startx = 10;
       int messageColorPair = 2, mainColorPair = 1, bgColorPair = 3;
       bool needBorders = true;

       wbkgd(stdscr,COLOR_PAIR(bgColorPair)); // Цвет для фона применим сразу
       refresh();                             // Первоначальное обновление экрана

       printw("Made by Semyon Fomin using ncurses\nApril 13th 2017");

       char chr[] = "Press F9 to exit...";

       // Создаем экземпляр класса окна сообщения
       Message *myMessage = new Message(startx,
                                        starty,
                                        chr,
                                        mainColorPair,
                                        messageColorPair,
                                        bgColorPair,
                                        needBorders);

       int ch;
       while((ch = getch()) != KEY_F(9))
       {
          switch(ch)
          {
             case KEY_F(1):
                myMessage->Hide(); break;
             case KEY_F(2):
                myMessage->Show(); break;
             case KEY_LEFT:
                myMessage->moveLeft(); break;
             case KEY_RIGHT:
                myMessage->moveRight(); break;
             case KEY_UP:
                myMessage->moveUp(); break;
             case KEY_DOWN:
                myMessage->moveDown(); break;
          }
       }
       endwin();
    }