基于 GD32F303ZET6 苹果派开发板
简介
摇杆控件是一个输入控件,常常用于游戏开发,或者用于控制遥控车、无人机等设备,具体如下图所示。下图中,左右两个控件即为摇杆控件。

首先放一张结果图,本章中实现的摇杆效果如下所示。与上图不一样的是,本文中实现的摇杆控件,不管是背景区,还是滑块部分,都是带透明度的,这样显示的效果会更好一些。在单片机中,透明度叠加会带来算力上的巨大消耗,如果处理的的性能不够,也可以直接绘制实心圆,不考虑透明度。当然,如果资源充裕,也可以选择直接贴图。
-摇杆-实验结果-20230322.gif)
控件设备结构体和 API 接口设计
为了方便用户移植和使用,我们使用一个结构体来管理摇杆控件,这样用户就可以创建任意多的摇杆控件在项目中,具体如下述代码所示。初始化时,用户需要提供控件的中心坐标、背景/滑块的半径、颜色以及透明度。至于后边的成员变量,例如当前角度、距离值等,都是运行过程中自动计算得出的。
为了驱动摇杆控件运行,需要为摇杆控件定义一个轮询函数,每隔一段时间扫描一下摇杆控件,更新控件显示。
//摇杆控制结构体
typedef struct
{
unsigned int x, y, rBg, rButton; //中心坐标、背景半径、滑块半径
unsigned int bgColor, buttonColor; //颜色
unsigned char bgAlpha, buttonAlpha; //透明度
unsigned int curAngle; //当前角度,角度制,以垂直向上为零,按顺时针方向增涨
unsigned int curDistance; //当前滑块与控件中心的距离
unsigned int lastX, lastY; //上一次按下的坐标
unsigned short* buttonBg; //按键的背景,用于按键的移动
unsigned int isPress; //是否被按下标志位
}StructRockerDev;
//API 接口
void CreateRocker(StructRockerDev* widget); //创建摇杆控件
void RockerPoll(StructRockerDev* widget); //轮询函数
int GetRockResult(StructRockerDev* widget, unsigned int* angle, unsigned int* distance); //获取摇杆检测结果
源码中需要包含的头文件
摇杆驱动中,需要使用到三角函数、动态内存分配等接口,需要包含的头文件如下所示。
#include "Rocker.h"
#include "./LCD/LCD.h"
#include "math.h"
#include "malloc.h"
#include "stdio.h"
绘制透明的实心圆
摇杆驱动中,不论是背景还是滑块,都是一个透明的实心圆,透明实心圆的驱动代码如下所示。
绘制实心圆时,不能使用环形进度条中的驱动,因为 Bresenham 算法绘制实心圆时,会导致一些点被重复绘制,而透明度叠加具有累加效应,所以使用 Bresenham 算法会造成某些区域颜色更深。为了避免这种状况,我们采用了老方法,先计算实心圆坐标范围,然后扫描这篇区域内所有的点,计算这些点与圆心的距离。如果距离小于等于半径,那么就绘制该点。
/*********************************************************************************************************
* 函数名称: DrawPoint
* 函数功能: 画点
* 输入参数: x、y:坐标,color:颜色,alpha:透明度
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年03月21日
* 注 意: 只能用于 RGB565
*********************************************************************************************************/
static void DrawPoint(unsigned int x, unsigned int y, unsigned int color, unsigned int alpha)
{
unsigned int foreColor, backColor;
unsigned int backR, backG, backB, foreR, foreG, foreB, colorAlpha;
unsigned int resultR, resultG, resultB;
unsigned int result;
//获取前景色
foreColor = color;
//获取背景色
backColor = LCDReadPoint(x, y);
//提前前景 RGB 通道数据
foreR = ((foreColor >> 11) & 0x1F);
foreG = ((foreColor >> 5) & 0x3F);
foreB = ((foreColor >> 0) & 0x1F);
//提前背景 RGB 通道数据
backR = ((backColor >> 11) & 0x1F);
backG = ((backColor >> 5) & 0x3F);
backB = ((backColor >> 0) & 0x1F);
//获取透明度
colorAlpha = alpha;
//RGB通道透明度叠加
resultR = (foreR * colorAlpha + backR * (0xFF - colorAlpha)) >> 8;
resultG = (foreG * colorAlpha + backG * (0xFF - colorAlpha)) >> 8;
resultB = (foreB * colorAlpha + backB * (0xFF - colorAlpha)) >> 8;
//组合成RGB565格式
result = (((u16)resultR) << 11) | (((u16)resultG) << 5) | (((u16)resultB) << 0);
//画点
LCDFastDrawPoint(x, y, result);
}
/*********************************************************************************************************
* 函数名称: DrawCircle
* 函数功能: 绘制实心圆
* 输入参数: x,y:圆心坐标,r:半径,color:颜色,alpha:透明度
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年03月21日
* 注 意:
*********************************************************************************************************/
static void DrawCircle(unsigned int x, unsigned int y, unsigned int r, unsigned int color, unsigned int alpha)
{
unsigned int xCir, yCir, x0, y0, x1, y1;
double dx, dy, distance;
//记录圆心坐标
xCir = x;
yCir = y;
//计算显示区域大小
x0 = xCir - r;
x1 = xCir + r;
y0 = yCir - r;
y1 = yCir + r;
//依次扫描所有的点
for (y = y0; y <= y1; y++)
{
for (x = x0; x <= x1; x++)
{
//计算当前坐标与圆心的距离
dx = abs(x - xCir);
dy = abs(y - yCir);
distance = sqrt(dx * dx + dy * dy);
//距离圆心的距离小于等于半径,绘制该点
if (distance <= r)
{
DrawPoint(x, y, color, alpha);
}
}
}
}
更新滑块显示
在摇杆控件的设备结构体中,成员变量 buttonBg 表示滑块的背景缓冲区,创建摇杆控件时,背景缓冲区所需的内存由动态内存分配。
更新滑块显示时,首先要将当前滑块所在位置的背景填回去,即背景重绘,这样就可以擦除滑块所在区域。然后,在目的地绘制滑块之前,首先将该区域的像素点数据预先保存下来,为下次背景重绘做准备。最后才是绘制滑块,将滑块显示到屏幕上。经过这一系列复杂的操作后,才能将滑块从一个位置移动到另一个位置。具体实现代码如下所示。
/*********************************************************************************************************
* 函数名称: DrawButton
* 函数功能: 绘制滑块
* 输入参数: widget:控件首地址
* x,y:新的坐标
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年03月21日
* 注 意:
*********************************************************************************************************/
static void DrawButton(StructRockerDev* widget, unsigned int x, unsigned int y)
{
unsigned int x0, y0, x1, y1, i, targetX, targetY;
//保存目标位置信息
targetX = x;
targetY = y;
//新的坐标与旧的坐标相同
if ((targetX == widget->lastX) && (targetY == widget->lastY))
{
return;
}
//计算背景所在位置
x0 = widget->lastX - widget->rButton;
x1 = widget->lastX + widget->rButton;
y0 = widget->lastY - widget->rButton;
y1 = widget->lastY + widget->rButton;
//背景重绘
i = 0;
for (y = y0; y <= y1; y++)
{
for (x = x0; x <= x1; x++)
{
LCDFastDrawPoint(x, y, widget->buttonBg[i++]);
}
}
//计算新的显示区域
x0 = targetX - widget->rButton;
x1 = targetX + widget->rButton;
y0 = targetY - widget->rButton;
y1 = targetY + widget->rButton;
//保存背景
i = 0;
for (y = y0; y <= y1; y++)
{
for (x = x0; x <= x1; x++)
{
widget->buttonBg[i++] = LCDReadPoint(x, y);
}
}
//绘制滑块
DrawCircle(targetX, targetY, widget->rButton, widget->buttonColor, widget->buttonAlpha);
//保存当前位置
widget->lastX = targetX;
widget->lastY = targetY;
}
创建函数
创建函数中,主要工作就是为滑块的背景缓冲区申请动态内存,具体如下所示。绘制出背景圆后,首先要将滑块背景像素点数据保存到背景缓冲区,为第一次背景重绘做准备,然后才能绘制滑块。
/*********************************************************************************************************
* 函数名称: CreateRocker
* 函数功能: 创建摇杆控件
* 输入参数: widget: 控件首地址
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年03月21日
* 注 意: 只能用于 RGB565
*********************************************************************************************************/
void CreateRocker(StructRockerDev* widget)
{
unsigned int x, y, x0, y0, x1, y1, width, height, i;
//画背景圆
DrawCircle(widget->x, widget->y, widget->rBg, widget->bgColor, widget->bgAlpha);
//计算滑块显示区域
x0 = widget->x - widget->rButton;
x1 = widget->x + widget->rButton;
y0 = widget->y - widget->rButton;
y1 = widget->y + widget->rButton;
width = x1 - x0 + 1;
height = y1 - y0 + 1;
//为滑块背景申请动态内存
widget->buttonBg = (unsigned short*)malloc(width * height * 2);
if (NULL == widget->buttonBg)
{
printf("CreateRocker: Fail to malloc\r\n");
while (1) {}
}
//保存滑块背景区域
i = 0;
for (y = y0; y <= y1; y++)
{
for (x = x0; x <= x1; x++)
{
widget->buttonBg[i++] = LCDReadPoint(x, y);
}
}
//绘制滑块
DrawCircle(widget->x, widget->y, widget->rButton, widget->buttonColor, widget->buttonAlpha);
widget->lastX = widget->x;
widget->lastY = widget->y;
//标记尚未被按下
widget->isPress = 0;
}
轮询函数
轮询函数中,只需要不停的调用 DrawButton 函数刷新滑块显示位置即可。绘制滑块时,有显示区域限制,不能绘制到背景圆外边,因此调用 DrawButton 函数之前,还得校验当前触点坐标是否越界,如果越界,则需要通过几何关系,计算出滑块在边界处的坐标。具体实现代码如下所示。
/*********************************************************************************************************
* 函数名称: RockerPoll
* 函数功能: 摇杆控件轮询函数
* 输入参数: widget: 控件首地址
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年03月21日
* 注 意: 只能用于 RGB565
*********************************************************************************************************/
void RockerPoll(StructRockerDev* widget)
{
const double pi = 3.1415926535;
unsigned int tx, ty, tflag;
double x, y, dx, dy, distance, angle;
//获取当前触屏坐标信息
tflag = GetTouch(&tx, &ty);
//未检测到按下
if (0 == tflag)
{
widget->curAngle = 0;
widget->curDistance = 0;
DrawButton(widget, widget->x, widget->y);
widget->isPress = 0;
return;
}
//正好落在控件中心,滑块归位(定义落到原点上的角度值为零)
if ((tx == widget->x) && (ty == widget->y))
{
widget->curAngle = 0;
widget->curDistance = 0;
DrawButton(widget, widget->x, widget->y);
widget->isPress = 1;
return;
}
//计算当前坐标值与控件中心的距离
x = tx; y = ty;
dx = x - widget->x;
dy = y - widget->y;
dy = -dy;
distance = sqrt(dx * dx + dy * dy);
//按照第一象限求角度
angle = atan(fabs(dx) / fabs(dy));
//角度补偿,补偿到 4 个象限
if ((dx >= 0) && (dy >= 0)) //第一象限
{
angle = angle;
}
else if ((dx >= 0) && (dy <= 0)) //第二象限
{
angle = 0.5 * pi + (0.5 * pi - angle);
}
else if ((dx <= 0) && (dy <= 0)) //第三象限
{
angle = angle + pi;
}
else
{
angle = angle + pi + 2 * (0.5 * pi - angle);
}
//还在控件范围内
if (distance <= widget->rBg)
{
//更新显示
DrawButton(widget, tx, ty);
//标记被按下
widget->isPress = 1;
//保存当前角度值和距离值
widget->curAngle = 180.0 * angle / pi;
widget->curDistance = distance;
}
//超出了控件范围,需要把坐标缩小到边界上
else
{
//如果上一次检测时,控件有被按下
if (0 != widget->isPress)
{
//计算目的坐标
x = widget->x + widget->rBg * sin(angle);
y = widget->y - widget->rBg * cos(angle);
//更新显示
DrawButton(widget, x, y);
//保存当前角度值和距离值
widget->curAngle = 180.0 * angle / pi;
widget->curDistance = distance;
}
}
}
获取摇杆输入
其实,用户可以直接访问摇杆控制结构体获取当前角度、距离以及是否被按下等信息;但是为了规范性,在这里还是提供了一个 API 函数,专门用于获取摇杆输入,具体如下所示。
/*********************************************************************************************************
* 函数名称: GetRockResult
* 函数功能: 获取摇杆检测结果
* 输入参数: widget: 控件首地址
* angle:用于输出当前角度,角度制,以垂直向上为零,顺时针方向为正方向
* distance:手指与控件中心的距离
* 输出参数: void
* 返 回 值: 0-未检测到输入,其它-检测到输入
* 创建日期: 2023年03月21日
* 注 意: 只能用于 RGB565
*********************************************************************************************************/
int GetRockResult(StructRockerDev* widget, unsigned int* angle, unsigned int* distance)
{
*angle = widget->curAngle;
*distance = widget->curDistance;
return widget->isPress;
}
测试代码
测试代码如下所示,为了方便显示结果,这里使用了两个 Text 控件,分别用于显示当前角度值和距离值。角度值范围是 0~359,为角度制,以垂直上升为零度,顺时针为正方向;距离值则是当前触点坐标与摇杆控件中心的距离值,以像素点为单位。
#include <stdio.h>
#include "./LCD/LCD.h"
#include "./Picture/BMP.h"
#include <windows.h>
#include "Rocker.h"
#include "Text.h"
int main(void)
{
//相关变量
static StructRockerDev s_structRocker;
static StructText s_structText1;
static StructText s_structText2;
static char s_arrStringBuf[64];
unsigned int angle, distance;
//初始化
InitLCD();
InitBMP();
//显示背景
extern const unsigned char g_arrBmpBgImage800x480[768068];
DrawBMP(g_arrBmpBgImage800x480, 0, 0);
//创建摇杆控件
s_structRocker.x = 400;
s_structRocker.y = 240;
s_structRocker.rBg = 100;
s_structRocker.rButton = 25;
s_structRocker.bgColor = LCD_COLOR_BLACK;
s_structRocker.buttonColor = LCD_COLOR_YELLOW;
s_structRocker.bgAlpha = 75;
s_structRocker.buttonAlpha = 200;
CreateRocker(&s_structRocker);
//创建 Text1
s_structText1.x = 10;
s_structText1.y = 10;
s_structText1.width = 12 * 20;
s_structText1.size = 24;
s_structText1.height = s_structText1.size;
TextInit(&s_structText1);
//创建 Text2
s_structText2.x = 10;
s_structText2.y = 40;
s_structText2.width = 12 * 20;
s_structText2.size = 24;
s_structText2.height = s_structText2.size;
TextInit(&s_structText2);
//速度仪表盘显示
while (1)
{
//摇杆轮询
RockerPoll(&s_structRocker);
//获取摇杆输入
if (0 != GetRockResult(&s_structRocker, &angle, &distance))
{
//设置画笔颜色
g_iLCDPointColor = LCD_COLOR_WHITE;
//显示角度
sprintf(s_arrStringBuf, "Angle: %d", angle);
TextShow(&s_structText1, s_arrStringBuf);
//显示距离
sprintf(s_arrStringBuf, "Distance: %d", distance);
TextShow(&s_structText2, s_arrStringBuf);
}
else
{
//设置画笔颜色
g_iLCDPointColor = LCD_COLOR_WHITE;
//显示角度
sprintf(s_arrStringBuf, "Angle: --");
TextShow(&s_structText1, s_arrStringBuf);
//显示距离
sprintf(s_arrStringBuf, "Distance: --");
TextShow(&s_structText2, s_arrStringBuf);
}
//延时 25ms
Sleep(25);
}
return 0;
}
源码
本章节中的源码请参考《单片机 GUI 设计(零)- 大纲》