InputBox на С++ и SDL
На этот раз создаем класс InputBox, который представляет из себя поле для ввода текста. Класс обрабатывает события клавиатуры и вызывает пользовательский метод (CALLBACK) при нажатии клавиши Enter.
Принцип работы InputBox предельно прост. Внутри класса определена публичная переменная text типа string. А при обработке нажатия клавиш мы получаем код соответствующего символа и присоединяем его к строке text посредством оператора + класса string. Чтобы добавление символа стало возможным, необходимо включить нужную опцию: SDL_EnableUNICODE(SDL_ENABLE).
Если пользователь нажимает backspace и строка не пуста, то удаляем последний символ строки с помощью метода erase класса string. Детально этот процесс описан на сайте Lazyfoo.net.
Мой вклад в данном случае заключается в применении графического шрифта без дополнительного модуля SDL. Все спрайты мы храним с помощью структуры SpriteBank, а все подмножества спрайтов концентрируем в структурах SpriteSet. Тут обойдемся без подробностей, так как данная часть приложения уже рассматривалась в предыдущей статье.
Кроме вышеперечисленного на форме присутствует кнопка, создание класса для которой было описано тут.
Описание класса и некоторых корректировок
Изменения в SpriteString
Для данного проекта был изменен исходник SpriteString.cpp, так как возникла необходимость ограничивать видимость печатаемого текста. Пользовательская строка не должна быть видна за пределами InputBox.
void SpriteStringDraw(SpriteString* sstr, unsigned char* str, int x, int y, int w, int h) { int i=0; int offsetx = x; int offsety = y; while(str[i] != 0) { if(str[i] != '\n') { SpriteBankDrawSetId(sstr->sb, sstr->ss, sstr->charset[str[i]], offsetx, offsety); offsetx += sstr->sb->spnodes[sstr->ss->ids[sstr->charset[str[i]]].id].w; if(offsetx > w) { offsetx = x; offsety += sstr->sb->spnodes[sstr->ss->ids[sstr->charset[str[i]]].id].h+1; }; if(offsety > h) return; } else { offsetx = x; offsety += sstr->sb->spnodes[sstr->ss->ids[sstr->charset[str[i]]].id].h+1; }; i++; }; };
Кроме того, данный метод позволяет размещать текст на некоторой прямоугольной области, а также использовать переносы строк. В отличие от предыдущей версии, учитывается ширина спрайта каждого символа. Таким образом мы избавились от необходимости использовать шрифт с фиксированной шириной символа (fixed-width font).
Класс InputBox
Заголовочный файл выглядит так:
/* InputBox header */ #include <string> #include "SpriteBank.h" #include "SpriteString.h" #ifndef INPUTBOX_H #define INPUTBOX_H using namespace std; class InputBox { public: InputBox(); ~InputBox(); int x, y, w, h; string text; int text_x, text_y; SpriteBank* sb; SpriteString* sstr; SpriteSet* ss; void* userData; void (*onEnter)(void*, void*); bool visible; bool enabled; void handle_events(SDL_Event& event); void draw(); }; #endif
Вся отрисовка спрайтов производиться через SpriteBank, поэтому подключаем соответствующие заголовочные файлы. Далее объявляем класс с конструктором без аргументов и деструктором. Следующий блок переменных содержит координаты InputBox с шириной и высотой соответственно. Переменная text будет хранить содержимое поля ввода, а переменные text_x и text_y — координаты первого символа в этом поле, поскольку содержимое не должно накладываться на элементы оформления.
Фактически, наш объект представляет из себя прямоугольную область с текстом, расположенным поверх графической подложки. Данная подложка должна быть описана в подмножестве спрайтов ss банка спрайтов sb.
Информация о спрайтах символов должна быть сосредоточена в переменной sstr. Так как шрифт может быть общим для нескольких элементов управления, то при удалении экземпляра класса из памяти, никаких действий с данными по указателю sstr не выполняем.
Ещё одна переменная-указатель создана для ссылки на пользовательские данные (userData). При нажатии клавиши Enter будет вызван метод, на который ссылается переменная onEnter. В качестве аргументов передаются userData и указатель на экземпляр данного класса. Так как процедура-обработчик может быть одной и той же для разных InputBoxов, полезно знать — какой из экземпляров вызвал процедуру.
Логические переменные enabled и visible определяют необходимость обработки событий клавиатуры и видимость поля ввода соответственно. То есть при enabled = false InputBox не реагирует на события SDL. Если полей ввода текста будет несколько, важно знать — какое из них мы в данный момент заполняем.
Последними объявлены методы обработки событий SDL и отрисовки соответственно. Назначение то же, что и в классе Button.
Реализация методов класса InputBox выглядит так:
/* InputBox source */ #include <string> #include "SpriteBank.h" #include "SpriteString.h" #include "InputBox.h" #ifndef INPUTBOX_CPP #define INPUTBOX_CPP using namespace std; InputBox::InputBox() { this->x = 0; this->y = 0; this->w = 10; this->h = 10; this->text_x = 2; this->text_y = 2; this->sb = NULL; this->ss = NULL; this->sstr = NULL; this->userData = NULL; this->onEnter = NULL; this->visible = true; this->enabled = true; }; InputBox::~InputBox() { if(this->ss) SpriteSetFree(this->ss); }; void InputBox::draw() { if(this->visible) { SpriteBankDrawSetId(sb, ss, 0, x, y); SpriteStringDraw(sstr, (unsigned char*)text.c_str(), x+text_x, y+text_y, w-text_x, h-text_y); }; }; void InputBox::handle_events(SDL_Event& event) { if(this->visible && this->enabled && (event.type == SDL_KEYDOWN)) { if((event.key.keysym.sym == SDLK_BACKSPACE) && (this->text.length() != 0)) this->text.erase(this->text.length() - 1); else if(event.key.keysym.unicode >= (Uint16)' ' && event.key.keysym.unicode <= (Uint16)'~') this->text += (char)event.key.keysym.unicode; else if(event.key.keysym.sym == SDLK_RETURN) { if(onEnter) onEnter(userData, this); }; }; }; #endif
В методе draw при условии видимости (visible = true) рисуем подложку в позиции (x, y), а затем сам текст в позиции (x+text_x, y+texty).
Метод handle_events работает так: если наше поле ввода видимо и активно, то обрабатываем нажатия клавиш. Причем, в случае нажатия backspace удаляем последний символ (если он есть), а в случае нажатия Enter вызываем пользовательский метод с аргументами userData и this соответственно.
Тут следует отметить, что обрабатываются только печатаемые символы, то есть те, что расположены на отрезке таблицы ASCII от ‘ ‘ до ‘~’ включительно.
Применение
Приводить здесь весь исходник main.cpp не стану, поскольку многое взято из исходника для класса Button. Укажу только то, что касается InputBox.
Для отображения текста внутри окна, объявим следующую структуру:
struct Parchment { string text; int x,y,w,h; };
Это всего-навсего строка с описанием прямоугольной области для расположения. Внутри функции main объявим наш класс и инициируем его:
InputBox inpBox; inpBox.sstr = sstr; inpBox.sb = sb; setInputBox(cr, inpBox); inpBox.onEnter = addTextToParchment; inpBox.userData = &parchment;
Переменные для банка спрайтов (sb) и шрифта (sstr) объявлены и инициированы раннее, поскольку являются общими для всех задействованных объектов. Процедура setInputBox задает координаты, размеры и спрайтовые подмножества для нашего класса исходя из данных конфигурационного файла, который, в свою очередь, загружен экземпляром класса ConfReader (переменная cr). Если пользователь нажмет клавишу Enter, то будет вызвана процедура addTextToParchment. Следовательно, в качестве пользовательских данных передаем указатель на переменную parchment, тип которой описан выше.
И последнее — обработчик нажатия клавиши Enter:
void addTextToParchment(void* p1, void* p2) { Parchment* p = (Parchment*)p1; InputBox* ibox = (InputBox*)p2; p->text += ibox->text; p->text += '\n'; ibox->text = ""; };
Как было сказано ранее: первый указатель — это пользовательские данные, а второй — экземпляр класса InputBox, вызвавший эту процедуру. Здесь мы приводим указатели к нужным типам, добавляем к тексту parchmentа текст из поля ввода, добавляем туда же перенос строки, очищаем содержимое поля ввода.
Осталось добавить в основной цикл необходимые процедуры:
while(working) { start = SDL_GetTicks(); while(SDL_PollEvent(&event)) { if(event.type == SDL_QUIT) { working = false; }; btnQuit.handle_events(event); inpBox.handle_events(event); }; SpriteBankDrawSetId(sb, ssBack, 0, 0, 0); btnQuit.draw(); inpBox.draw(); SpriteStringDraw(sstr, (unsigned char*)(parchment.text.c_str()), parchment.x, parchment.y, parchment.w, parchment.h); SDL_Flip(screen); end = SDL_GetTicks(); if(end - start < cdelay) SDL_Delay(cdelay - end + start); };
Выделенные строки — это изменения по сравнению с вариантом для Button.
Графические материалы
Все изображения, кроме шрифта я взял с ресурса opengameart.org и немного доработал. Фон, кнопка, поле ввода.
Несмотря на то, что SpriteStringDraw работает с изображениями символов разной ширины, используемый шрифт всё-таки имеет фиксированную ширину символа. В таком случае сформировать набор спрайтов можно гораздо быстрее.
Например, можно напечатать все необходимые символы на рисунке в GIMP, а потом сформировать набор спрайтов, сдвигая координаты прямоугольной области на фиксированную величину. Ведь ширина символа одна и та же.
Добавить комментарий