Графический интерфейс пользователя. Кнопка

Электроника

На этом занятии мы разберемся с функциональным программированием графических интерфейсов на примере построения кнопки и изменения ее состояния
Глоссарий
Для успешного освоения материала рекомендуем вам изучить следующие понятия:
Кнопка
один из элементов интерфейса пользователя компьютерной программы, «нажатие» на который приводит к некоторому действию, заложенному в программе
SPI
SPI (англ. Serial Peripheral Interface, SPI bus — последовательный периферийный интерфейс, шина SPI) — последовательный синхронный стандарт передачи данных в режиме полного дуплекса, предназначенный для обеспечения простого и недорогого высокоскоростного сопряжения микроконтроллеров и периферии
Видеолекция
Конспект
Разберемся с функциональным программированием графических интерфейсов на примере построения кнопки и изменения ее состояния.

Встраиваемые системы при взаимодействии с пользователем стремятся использовать модель, характерную для мобильных устройств и компьютеров. Это означает, что пользователь взаимодействует с определенной графической абстракцией, например, кнопкой. Кнопка — это квадрат или прямоугольник, внутри которого располагается текст. У нее могут быть два состояния: «нажата» или «отжата». Когда пользователь взаимодействует с кнопкой, нажимая на нее на тач-панели или кликая курсором мыши, она меняет свое графическое состояние, и происходит вызов какого-либо действия. То есть пользователь взаимодействует с системой.

Задача
  1. Построить абстракцию простой кнопки в программе
  2. Нарисовать на графическом экране несколько состояний кнопки
  3. По таймеру изменить состояние кнопки. Нажата кнопка — вывод на индикатор pushed, отжата — unpushed
Информация для кнопки
1. Координаты левого верхнего угла в пикселях экрана

2. Ширина-высота в пикселях экрана

3. Текст кнопки, например, «ОК»

4. Идентификатор объекта — некоторое число, которое будет использоваться для поиска в массиве объектов

5. Флаг «необходимо перерисовать»

6. Текущее состояние («нажата», «отжата»)

7. Предыдущее состояние («нажата», «отжата»)

8. Счетчик тиков программного таймера при нажатом состоянии. Используется для обработки длительного нажатия

9. Метод отрисовки состояния, которое вызывается для отрисовки этого состояния

10. Метод-событие состояния, которое вызывается, когда кнопку только что «нажали» — переход от «отжата» к «нажата»

11. Метод-событие состояния, которое вызывается, когда кнопку только что «отжали» — переход от «нажата» к «отжата»

12. Метод-событие состояния, которое вызывается, когда кнопка все еще «нажата». Используется для непрерывной генерации события — предыдущее состояние «нажата»/ текущее «нажата»
Структуры
Хранение информации для кнопки удобно организовать в виде структур, т.к. несколько структур можно записать в массив и изменять их состояние в цикле.
Работа на компьютере
1. Копируем предыдущий проект, переименовываем и запускаем

2. Заходим в main.c и заполняем структуру по объектам, то есть по кнопке
struct Button_Data
{
	enum Object id; //идентификатор объекта
	char *button_text; 	//текст кнопки
	uint8_t y;		   	//координаты верхнего левого угла кнопки
	uint8_t x;			//координаты верхнего левого угла кнопки
	uint8_t	height;		//высота кнопки
	uint8_t width;		//ширина кнопки
	uint8_t to_redraw;	//флаг "отрисовать"
	Button_State current_state;  	//текущее состояние кнопки
	Button_State previous_state;	//предыдущее состояние кнопки
	uint32_t pushed_counter;			//счетчик тиков сколько кнопка нажата
    void (*Draw_Button)(Button_Data *btn_struct, uint8_t clear);
    void (*HasPushed)(Button_Data *btn_struct);
    void (*HasUnPushed)(Button_Data *btn_struct);
    void (*StillPushed)(Button_Data *btn_struct);
};
3. Создаем перечисления, которые используем в структуре для кнопки:
  • идентификаторы объекта
  • состояние кнопки
//состояние кнопки
typedef enum
{
	NO_PRESS = 0,
	PRESS
}
Button_State;

//идентификаторы объектов
enum Object
{
	Button1 = 1,
};

//описание кнопки
4. Описываем массив объектов. В данном случае объект будет один — кнопка
#define NUM_OBJECTS_BTN 1
//массив объектов
struct Button_Data Object[NUM_OBJECTS_BTN]=
{
		{
				.id = Button1,
				.button_text = "OK",
				.y = 30,
				.x = 10,
				.height = 24,
				.width = 32,
				.to_redraw = 1,
				.current_state = NO_PRESS,
				.previous_state = NO_PRESS,
				.pushed_counter = 0,
				.Draw_Button = &draw_btn,
				.HasPushed = &draw_haspushed,
				.HasUnPushed = &draw_hasunpushed,
				.StillPushed = &draw_stillpushed
		}
};
5. Формируем функции:
  • в функцию передается указатель на массив
  • если использовать draw_btn, то применяем дополнительный параметр clear. Если он равен нулю, то рисуем обычные примитивы, а если — 1, то все примитивы с черным цветом
//отрисовка кнопок примитивами
//clear: 0 - рисуем нормально, 1 - рисуем цветом фона (BLACK)
void draw_btn(Button_Data *btn_struct, uint8_t clear)
{
	uint16_t Color;
	if(btn_struct->current_state == NO_PRESS)
		Color = WHITE;
	else
		Color = RED;

	if(clear!=0)
		Color= BLACK; //очистка примитивами
}
6. Используя отрисовку линий, рисуем границы кнопки и оставляем место для закруглений

7. Рисуем закругления в библиотеке SSD1331:
  • находим функцию для отрисовки окружности
  • копируем и оставляем отрисовку только одной из 4 частей окружности
  • повторяем действия для 2, 3, 4 сегментов
  • переносим заголовки этих функций в заголовочный файл библиотеки
//отрисовка кнопок примитивами
//clear: 0 - рисуем нормально, 1 - рисуем цветом фона (BLACK)
void draw_btn(Button_Data *btn_struct, uint8_t clear)
{
	uint16_t Color;
	if(btn_struct->current_state == NO_PRESS)
		Color = WHITE;
	else
		Color = RED;

	if(clear!=0)
		Color= BLACK; //очистка примитивами


	//вывод прямых участков границ кнопки
	ssd1331_draw_line(btn_struct->x+10, btn_struct->y,btn_struct->x + btn_struct->width-10, btn_struct->y, Color); //верхний горизонт
	ssd1331_draw_line(btn_struct->x+10, btn_struct->y+btn_struct->height,btn_struct->x + btn_struct->width-10, btn_struct->y+btn_struct->height, Color); //нижний горизонт
	ssd1331_draw_line(btn_struct->x, btn_struct->y+10, btn_struct->x, btn_struct->y+btn_struct->height-10, Color); //левая граница
	ssd1331_draw_line(btn_struct->x+btn_struct->width, btn_struct->y+10, btn_struct->x+btn_struct->width, btn_struct->y+btn_struct->height-10, Color); //правая граница

	//вывод скругления углов кнопки
	ssd1331_draw_circle_seg4(btn_struct->x+10, btn_struct->y+10, 10, Color);//левый верхний
	ssd1331_draw_circle_seg3(btn_struct->x+10, btn_struct->y+btn_struct->height-10, 10, Color); //левый нижний
	ssd1331_draw_circle_seg2(btn_struct->x+btn_struct->width-10, btn_struct->y+btn_struct->height-10, 10, Color);//правый нижний
	ssd1331_draw_circle_seg1(btn_struct->x+btn_struct->width-10, btn_struct->y+10, 10, Color);//правый верхний
8. Работаем с центром кнопки:
  • вычисляем центр
  • вычисляем размер текста
  • рассчитываем координаты верхнего левого угла первого символа так, чтобы текст был отцентрован
  • с помощью вызова рисуем текст внутри кнопки
//центр кнопки
	uint8_t x_c = btn_struct->x + btn_struct->width/2;
	uint8_t y_c = btn_struct->y + btn_struct->height/2;

	uint8_t size_txt = strlen(btn_struct->button_text); //размер текста

	//координаты текста - центрирование внутри кнопки
	uint8_t y_t = y_c - FONT_1206/2;
	uint8_t x_t = x_c - ( (FONT_1206/2) * size_txt)/2;

	//вывод текста кнопки
	ssd1331_display_string(x_t, y_t, btn_struct->button_text, FONT_1206, Color);
}
9. Расписываем все состояния кнопки
//метод-событие "кнопка только что нажата"
void draw_haspushed(Button_Data *btn_struct)
{
	print2LCD("               ", 0, 0);
	print2LCD("pushed", 0, 3);

	btn_struct->pushed_counter = 0; //сброс счетчика все еще нажатой кнопки

	print2LCD("               ", 1, 0);
	char str_sec[6];
	sprintf(str_sec, "%04d", btn_struct->pushed_counter);
	print2LCD(str_sec, 1, 1);
}

//метод-событие "кнопка только что отжата"
void draw_hasunpushed(Button_Data *btn_struct)
{
	print2LCD("               ", 0, 0);
	print2LCD("unpushed", 0, 3);
}

//метод-событие "кнопка все еще нажата"
void draw_stillpushed(Button_Data *btn_struct)
{
	static uint8_t tick=5;

	tick--;
	if(tick==0)
		{//прошло 0.5 секунд
			tick=5;
			btn_struct->pushed_counter++; //считаем автоповторы пока все еще нажата кнопка
			char str_sec[6];
			sprintf(str_sec, "%04d", btn_struct->pushed_counter);
			print2LCD(str_sec, 1, 1);
		}
}

10. Генерируем два новых события:

  • «раз в 100 мс» — проверка объектов
  • «раз в 3 с» — нажатие кнопки
Главный цикл программы
1. Закомментируем работу с индикатором до главного цикла, оставляем инициализацию дисплея

2. Убираем лишние обработчики событий из главного цикла

3. Добавляем обработчики двух новых событий
if(event_100ms_flag_obj==1)
	  	  {//наступило событие "раз в 100 мс проверка объектов"

		  	  //проверка всех объектов
		  	  check_objects(Object, NUM_OBJECTS_BTN);

			  //перерисовка всех объектов при необходимости
			  redraw_objects(Object, NUM_OBJECTS_BTN);

			  event_100ms_flag_obj=0; //сбросим флаг - обработали событие
	  	  }

	  if(event_3s_flag_pushbtn==1)
	  {//наступило событие "раз в 3 сек нажимаем кнопку"
		  static uint8_t pushed_unpushed=0;

		  if(pushed_unpushed==0)
		  {
			  //нажимаем все кнопки
			  to_push_btns(Object, NUM_OBJECTS_BTN);
			  pushed_unpushed=1;
		  }
		  else
		  {
			  //отжимаем все кнопки
			  to_unpush_btns(Object, NUM_OBJECTS_BTN);
			  pushed_unpushed=0;
		  }

		  event_3s_flag_pushbtn=0; //сбросим флаг - обработали событие
	  }
4. Формируем функции
Важно
Добавим функцию, которая потребует перерисовать все объекты. Это необходимо сделать в самом начале работы программы, когда событий еще нет, но отрисовка уже должна быть
// нажатие всех кнопок
void to_push_btns(struct Button_Data Object[], uint32_t num_objs)
{
	for(uint32_t i=0; i< num_objs; i++)
		Object[i].current_state = PRESS;
}

// отжатие всех кнопок
void to_unpush_btns(struct Button_Data Object[], uint32_t num_objs)
{
	for(uint32_t i=0; i< num_objs; i++)
			Object[i].current_state = NO_PRESS;
}

//проверка всех объектов
void check_objects(struct Button_Data Object[], uint32_t num_objs)
{
	for(uint32_t i=0; i< num_objs; i++) //перебор всех объектов
	{
		if(Object[i].current_state == PRESS && Object[i].previous_state == NO_PRESS)
		{//только что нажали
			Object[i].HasPushed(&Object[i]); //вызов обработчика "только что нажали"
			Object[i].Draw_Button(&Object[i],1);//очистка примитивами объекта- рисуем примитивы фоном
			Object[i].to_redraw = 1; //надо потом нарисовать объект с новым состоянием
			Object[i].previous_state = Object[i].current_state; //меняем предыдущее состояние
		}

		if(Object[i].current_state == PRESS && Object[i].previous_state == PRESS)
		{//все еще нажата
			Object[i].StillPushed(&Object[i]); //вызов обработчика "все еще нажата"
		}

		if(Object[i].current_state == NO_PRESS && Object[i].previous_state == PRESS)
		{//только что отжали
			Object[i].HasUnPushed(&Object[i]); //вызов обработчика "только что отжали"
			Object[i].Draw_Button(&Object[i],1);//очистка примитивами объекта- рисуем примитивы фоном
			Object[i].to_redraw = 1; //надо потом нарисовать объект с новым состоянием
			Object[i].previous_state = Object[i].current_state; //меняем предыдущее состояние
		}

	}
}
5. Формируем перерисовку всех объектов
//перерисовка всех объектов
void redraw_objects(struct Button_Data Object[], uint32_t num_objs)
{
	for(uint32_t i=0; i< num_objs; i++) //перебор всех объектов
	{
		if(Object[i].to_redraw!=0)
		{
			Object[i].Draw_Button(&Object[i], 0); //рисуем с новым состоянием
			Object[i].to_redraw=0;//сброс флага отрисовки объекта
		}
	}
}

//все объекты на перерисовку
void to_redraw_all_objects(struct Button_Data Object[], uint32_t num_objs)
{
	for(uint32_t i=0; i< num_objs; i++) //перебор всех объектов
	{
		Object[i].to_redraw=1;//устанвока флага отрисовки объекта
	}
}
6. Компилируем проект
Основные ошибки
1. Не находит функцию draw_btn
Исправление: переносим заголовки функций в начало программы
2. Использование неверных типов
Исправление: производим переопределение типов
Тестирование
На индикаторе отображается кнопка со скругленными углами и текстом «ОК». При этом программа генерирует событие нажатия кнопки каждые три секунды, и в это время кнопка меняет цвет с красного на белый
Итак, мы создали графическую абстракцию взаимодействия с пользователем, а именно кнопку, и написали для нее функции. Давайте попробуем проверить, как вы разобрались в этом!
Интерактивное задание
Тест
Для закрепления полученных знаний пройдите тест
Стартуем!
Как сделать у кнопки закругленные края?
Дальше
Проверить
Узнать результат
Для чего нужно событие «раз в 100 мс»?
Дальше
Проверить
Узнать результат
Когда используется функция, которая требует перерисовать все объекты?
Дальше
Проверить
Узнать результат
Как исправить ошибку «Не находит функцию draw_btn»?
Дальше
Проверить
Узнать результат
К сожалению, вы ответили неправильно
Прочитайте лекцию и посмотрите видео еще раз
Пройти еще раз
Неплохо!
Но можно лучше. Прочитайте лекцию и посмотрите видео еще раз
Пройти еще раз
Отлично!
Вы отлично справились. Теперь можете ознакомиться с другими компетенциями
Пройти еще раз