14.5 Текстовый интерфейс — curses

Предыдущая  Содержание  Следующая V*D*V

curses — древняя библиотека, которая управляет символьно-ориентированными терминалами и скрывает индивидуальные особенности различных терминалов при помощи баз данных termcap или terminfo [Sch90]. Первоначально Кен Арнольд в Беркли извлёк эти функции из редактора vi Билли Джоя. Между тем есть несколько оптимизированных реализаций, даже для DOS; некоторые находятся в свободном доступе.

Если мы хотим соединить наш калькулятор с curses, то должны реализовать замены для LineOut и Button и соединить их с объектом Calc. curses предоставляют тип данных WINDOW и оказывается, что лучший выбор состоит в использовании WINDOW для каждого графического объекта. Оригинальная версия curses не использует мышь или даже не предоставляет функций для перемещения курсора под управлением чего-то подобного клавишам со стрелками. Поэтому мы должны будем реализовать другой объект, который контролирует курсор и отправляет кнопкам позиции курсора как события.

Похоже, что у нас есть два выбора. Можно определить подкласс Ic для управления курсором и основным циклом приложения, и подклассы Button и LineOut, чтобы предоставить графические объекты. Каждый из этих трёх классов будут владеть его собственным WINDOW. Альтернативно, как показано ниже, можно запустить новую иерархию с помощью подкласса Ic, который владеет WINDOW и может управлять курсором. Дополнительно создаётся ещё два подкласса, которым затем, вероятно, придётся владеть Button и LineOut, соответственно.

 

Object

  Ic

    Button

      CButton

    LineOut

      CLineOut

    Crt

Object

  Ic

    Button

 

    LineOut

 

    Crt

      CButton

      ClineOut

 

Ни одно из решений не выглядит совершенно правильным. Первое кажется, вероятно, ближе к нашему приложению, но мы не инкапсулируем существование типа данных WINDOW в одном классе и не похоже, что мы упаковываем curses способом, который может быть снова использован для следующего проекта. Второе решение, похоже, инкапсулирует curses главным образом в Crt; однако, подклассы должны содержать объекты, которые очень близки данному приложению, то есть, вероятно, мы получим всё же одноразовое решение.

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

 

КЛАСС

ДАННЫЕ

МЕТОДЫ

 

Object

 

 

базовый класс

 

 

 


  Ic

 

 

базовый класс приложения





    Crt

 

 

базовый класс для управления экраном

 

window

 

WINDOW библиотеки curses

 

rows, cols

 

 размер

 

 

makeWindow

 создаёт своё окно

 

 

addStr

 выводит на экран строку в своё окно

 

 

crtBox

 управляет фреймом вокруг своего окна

 

 

gate

 управляет курсором, отсылает текст или позиции





      CLineOut

 

 

окно вывода

 

 

gate

 выводит на экран текст в своё окно

 

 

 


      CButton

 

 

прямоугольник с метком для нажатия

 

button

 

Button для принятия и перенаправления сообщений

 

x, y

 

 своя позиция

 

 

gate

 если сообщение - текст, отсылает его button
 если позиция сообщения совпадает,
 отсылает нулевой указатель в button

 

Реализация этих классов требует хорошего знания curses; поэтому, мы не будем рассматривать здесь детали кода. Пакет curses должен быть проинициализирован; это выполняется через ловушку в Crt_ctor(): необходимые функции curses вызываются, когда создаётся первый объект Crt.

Crt_gate() содержит основной цикл программы. Он игнорирует свое входящее событие и читает данные из клавиатуры, пока это не увидит control-D в качестве индикатора завершения ввода. В этой точке он вернёт reject и основная программа завершится.

Для управления курсором используется несколько символов ввода. Если нажат return, Crt_gate() вызывает super_gate(), чтобы отослать событие с kind, равным единице, и с целочисленным массивом с текущей позицией курсора в виде строки и столбца. Все другие символы отсылаются наружу как события с kind, равным нулю, и символом в качестве строки.

Интересный класс является CButton. Когда объект сконструирован, на экране появляется прямоугольник с названием кнопки внутри.

 

% CButton ctor { // new(CButton(), "text", row, col)

    struct CButton * self = super_ctor(CButton(), _self, app);

 

    self -> button =

                new(Button(), va_arg(* app, const char *));

    self -> y = va_arg(* app, int);

    self -> x = va_arg(* app, int);

 

    makeWindow(self, 3, strlen(text(self -> button)) + 4,

                        self -> y, self -> x);

    addStr(self, 1, 2, text(self -> button));

    crtBox(self);

    return self;

}

 

Окно создаётся достаточно большим для текста, окружённого пробелами и фреймом. wire() должен быть перезаписан так, чтобы внутренний объект "кнопка" button получил соединение:

 

% CButton wire {

%casts

    wire(to, self —> button);

}

 

Наконец, CButton_gate() передаёт текстовые события непосредственно внутренней кнопке. Для события "позиция" мы проверяем, находится ли курсор в нашем прямоугольнике:

 

% CButton gate {

%casts

    if (kind(item) == 1) {       // если kind == 1, это событие щелчок

        int * v = data(item);    // данные в массиве [x, y]

 

        if (v[0] >= self —> x && v[0] < self —> x + cols(self)

        && v[1] >= self —> y && v[1] < self —> y + rows(self))

            return gate(self —> button, 0);

        return reject;

    }

    return gate(self —> button, item);

}

 

Если да, мы отправляем нулевой указатель во внутреннюю кнопку, которая отвечает, отправляя дальше свой текст.

И снова мы можем проверить новые классы с помощью простой программой cbutton, прежде чем соберём приложение калькулятора целиком.

 

int main () {

    void * crt = new(Crt());

    void * lineOut = new(CLineOut(), 5, 10, 40);

    void * button = new(CButton(), "a", 10, 40);

 

    makeWindow(crt, 0, 0, 0, 0);    /* весь экран */

    gate(lineOut, "hello, world");

 

    wire(lineOut, button), wire(button, crt);

    gate(crt, 0);                   /* главный цикл */

 

    return 0;

}

 

Эта программа выводит на экран hello, world в пятой строке и маленькую кнопку с меткой a вблизи середины нашего окна в терминале. Если мы переместим курсор на кнопку и нажмём return, или если нажмём a, изображение изменится и будет показана a. cbutton завершается, если прерывается или если мы вводим control-D.

Как только это заработает, наш калькулятор тоже будет работать. Просто у него больше кнопок и микросхема для вычислений:

 

int main () {

    void * calc = new(Calc());

    void * crt = new(Crt());

    void * lineOut = new(CLineOut(), 1, 1, 12);

    void * mux = new(Mux());

 

    static const struct tbl { const char * nm; int y, x; }

        tbl [] = { "C", 0, 15,

            "1", 3, 0, "2", 3, 5, "3", 3, 10, "+", 3, 15,

            "4", 6, 0, "5", 6, 5, "6", 6, 10, "-", 6, 15,

            "7", 9, 0, "8", 9, 5, "9", 9, 10, "*", 9, 15,

            "Q", 12, 0, "0", 12, 5, "=", 12, 10, "/", 12, 15,

        0 };

    const struct tbl * tp;

 

    makeWindow(crt, 0, 0, 0, 0);

    wire(lineOut, calc);

    wire(mux, crt);

 

    for (tp = tbl; tp -> nm; ++ tp) {

        void * o = new(CButton(), tp -> nm, tp -> y, tp -> x);

 

        wire(calc, o), wire(o, mux);

    }

 

    gate(crt, 0);

    return 0;

}

 

Это решение очень похоже на последнее. Объект CButton нуждается в координатах; поэтому мы расширяем таблицу из которой создаются кнопки. Мы добавляем объект Crt, соединяем его с мультиплексором и позволяем ему выполнять основной цикл.

Выводы

Повторное использование класса Calc не должно вызывать большого удивления. Это наименьшее, чего можно ожидать, неважно какая техника проектирования используется для приложения. Однако мы также снова использовали Button, а базовый класс Ic помог нам концентрироваться полностью на взаимодействии с curses, а не на адаптации микросхемы для вычислений к различному множеству типов ввода.

Незначительный недостаток в том, что у нас нет чистого разделения между curses и классом Ic. Наша иерархия классов заставляет нас идти на компромисс и (более или менее) использовать в CButton два объекта Ic. Если следующий проект не использует класс Ic, мы не можем снова использовать этот код, разработанный, чтобы скрыть детали curses.

 

Предыдущая  Содержание  Следующая