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&amp; 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&amp; event) {
	if(this->visible &amp;&amp; this->enabled &amp;&amp; (event.type == SDL_KEYDOWN)) {
		if((event.key.keysym.sym == SDLK_BACKSPACE) &amp;&amp; (this->text.length() != 0))
			this->text.erase(this->text.length() - 1);
		else if(event.key.keysym.unicode >= (Uint16)' ' &amp;&amp; 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(&amp;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, а потом сформировать набор спрайтов, сдвигая координаты прямоугольной области на фиксированную величину. Ведь ширина символа одна и та же.

Исходники

Скачать

99

Добавить комментарий

Ваш e-mail не будет опубликован.