最近在折腾TouchGFX的时候,做了一个很典型的入门级小应用:界面设计里放一个光圈,用板子上的物理按键控制它在屏幕上移动。听起来挺简单,但真正做下来发现,它其实把TouchGFX里最常用的几个基础能力全部串起来了——控件布局、事件分发、按键注入、动画调度、坐标换算。而且这类“焦点移动”的交互在真实产品里非常常见,比如遥控器切台时的选中框、洗衣机面板上参数项的高亮圈、甚至是菜单里的光标移动,都是同一个套路。这篇文章就把我做这个“按键控制光圈移动”项目的完整过程记录下来,从需求拆解到环境搭建、界面制作、按键逻辑,再到调试踩坑,给想从“跑通示例”过渡到“自己写逻辑”的朋友一个能直接参考的路径。
1. 动手之前:先把“光圈移动”这件事想清楚
1.1 核心需求拆解:这不是一个“画圆”的活
先别急着开软件,把题目拆开看。“TouchGFX 简单界面设计_按键控制光圈移动”里面其实包含四层任务:
第一层,界面设计。需要在TouchGFX Designer里搭出一个屏幕布局,背景、控件、排版,让画面看起来像个正经的UI,不是随便堆几个框。
第二层,光圈。所谓“光圈”,在GUI里本质上就是一个高亮指示器。做的人通常有两种选择:要么用Circle控件直接绘制一个空心圆环,要么用一张PNG贴图当光圈素材。两者的做法我后面都会讲到,它们踩的坑还不一样。
第三层,按键控制。这里的按键不是触摸屏,而是物理按键,也就是GPIO输入。TouchGFX本身专注GUI渲染,物理按键的读取要靠STM32的HAL库去完成,然后再把按键事件“喂”给GUI层,这一步是很多新手卡住的地方。
第四层,光圈移动。移动不是“瞬间跳过去”,而是要做一个视觉上流畅的动画。你在界面上看到的是一个圆圈平滑地从一个目标点滑到另一个目标点,这背后要么用TouchGFX自带的动画机制,要么自己写一个简单的插值逻辑。
把这几层拆出来之后,整个项目的脉络就清楚了: 读GPIO -> 转换成按键事件 -> 传给GUI -> GUI里更新当前选中项 -> 控制光圈做位移动画 。
1.2 技术选型:为什么要选TouchGFX
很多朋友看到“界面设计”和“让控件动起来”的第一反应可能是LVGL、emWin,或者干脆自己在STM32上裸写2D库。我这次选TouchGFX,原因很实在:
一是TouchGFX和STM32的契合度非常高。它本身就是ST在推广HMI方案时的重头产品,在F4、F7、H7这些带屏幕控制器的芯片上有硬件加速,跑起来帧率稳定,动画不容易掉链子。
二是TouchGFX Designer这个可视化工具确实好用。拖拖拽拽就能完成界面布局,交互可以以“Interactions”的方式配置,生成C++代码,比纯手写布局效率高一大截。
三是MVP架构对写业务逻辑挺友好。TouchGFX把屏幕分成Model、View、Presenter三层,界面展示和业务处理分开,虽然学习要花一点时间,但一旦理顺了,在整个项目里找代码、改逻辑都非常清晰。
这里我也把主流方案放在一起对比一下,方便不同偏好的朋友参考:
| 对比项 | TouchGFX | LVGL | emWin |
|---|---|---|---|
| 主要硬件生态 | STM32生态最顺 | 各种MCU通吃,开源免费 | 老牌商用GUI,授权模式成熟 |
| 开发方式 | Designer可视化拖拽 + C++代码 | 纯代码为主,配合SquareLine Studio | AppWizard + 纯C代码 |
| 动画能力 | 内置多种动画,帧率稳定,硬件加速好 | 有动画API,自由度高 | 有动画支持,偏传统 |
| 学习曲线 | Designer易上手,MVP模式需要适应 | 轻量灵活,但UI调试不如拖拽直观 | 文档多,上手成本中等 |
| 资源占用 | 需要一定Flash/RAM,建议带LTDC和SDRAM | 占用低,适合资源紧张的MCU | 占用低,优化空间大 |
如果你手头只有一个小资源MCU、不需要复杂动画,LVGL或者emWin也很香。但如果你用的是ST芯片,手头又有带屏的评估板,TouchGFX确实是一条阻力最小的路。
1.3 项目最终的效果定义
再把这个项目要做成的效果描述清楚,避免做着做着跑偏:
屏幕尺寸我以480x272为例(这在很多4.3寸屏上很常见)。屏幕上放了4个目标点,分布在2x2的网格上,每个目标点可以理解成一个“槽位”。初始状态下,光圈停留在第一个目标点上。按下物理按键(上/下/左/右),光圈就朝对应方向移动一格。移动到边界时,再按相同方向则保持不动,光圈不会跑到屏幕外面去。
视觉上,光圈是一个带有明显颜色的圆环,目标点是四个灰色圆形或者方块,光圈移动时是平滑滑过去的,不是“瞬移”。这就够了,既覆盖了核心知识点,也不会把项目做复杂。
2. 环境准备:把TouchGFX开发链路跑起来
2.1 软件和硬件清单
我先列一下我自己用的环境,你可以根据手头东西对照着来:
- 硬件:STM32F429-DISCO开发板,板载4.3寸480x272屏。这块板是TouchGFX官方示例经常用的老面孔,资料多,出了任何问题都能搜到方案。如果你用F746G-DISCO或者自己画的板子,后面讲的原理都是一样的。
- 软件:STM32CubeMX(用来生成MCU初始化代码)、TouchGFX Designer(界面设计工具)、STM32CubeIDE或者IAR/Keil(编译烧录)。我这边用STM32CubeIDE,因为和CubeMX、TouchGFX Designer配合起来不需要来回切工程格式。
物理按键的问题说一下:F429-DISCO板载的用户按键只有一个,而我需要四个方向键,所以我在面包板上外接了三个按键,分别接到几个空闲GPIO上。如果你手上是别的板子,按键不够的话可以外接,或者临时把其中两个方向先做成“移动下一个”,只用一个按键来演示。
2.2 两种方式创建TouchGFX工程
创建工程无非两条路,我建议按你自己的情况选:
第一条路,用CubeMX创建。在CubeMX里选好自己的MCU型号,配置好时钟、LCD接口、SDRAM(如果用外部显存的话),然后在“Middleware and Software Packs”里勾选TouchGFX,生成一个带TouchGFX依赖的工程,再用TouchGFX Designer打开这个工程的
.touchgfx
文件。这条路适合从零开始做产品、硬件方案比较确定的情况。
第二条路,直接用TouchGFX Designer新建Demo工程。Designer里自带很多模板,比如“Empty Application”,直接基于这个模板改就行。生成的工程代码是完整的,编译烧录就能看到界面,代码里的Screen结构、MVP框架都是现成的。我看很多教程都喜欢用这条路,因为省去配置底层硬件的步骤,能更快把注意力放在界面逻辑上。
我这次用的是第二条路,因为目的是研究界面和按键逻辑,底层驱动不是重点。如果你用的是自己的板子,需要注意LCD的初始化代码和TouchGFX的HAL层要和你板子匹配,不然屏幕上就是白屏或者花屏。
2.3 TouchGFX工程里的目录结构:哪些代码能改
TouchGFX生成的工程初看会有点懵,目录比普通STM32工程多不少。但只要记住一个原则,就清楚了: generated目录下的东西不要手改,手写的业务逻辑都放在gui目录下 。
以4.x版本为例,核心目录大致是这样:
-
generated/:TouchGFX Designer自动生成的所有界面代码,包括每个Screen的基类、字体、图片缓存映射。这里面的代码在每次Designer重新生成之后会被覆盖。 -
gui/:你的主战场。每个Screen下面会有ScreenView、ScreenPresenter、ScreenViewBase这些文件。其中ViewBase是生成代码,View是你可以自由写逻辑的类。设计器里新增控件后,对应的成员变量会生成到ViewBase里,你在View里只要直接使用这些控件就行。 -
target/:平台相关的HAL层代码,比如触摸驱动、显示缓冲管理。如果你的板子显示器需要特殊时序,改这里的代码。
刚开始最容易犯的错误就是在ViewBase里手动添加成员变量或者改已有逻辑,结果Designer一保存就把你的改动冲掉了。更稳妥的做法是,在Designer里把所有控件都放好,然后切到IDE里,在
Screen1View.hpp
和自己的
Screen1View.cpp
里写扩展逻辑。这样重新生成也不会丢。
3. 界面制作:在Designer里把“光圈”安好家
3.1 创建空应用并设置背景
打开TouchGFX Designer,新建一个“Empty Application”,模板选择不带复杂示例的空白屏。分辨率我设置成480x272,屏幕背景我建议用深色,比如深灰或者纯黑。为什么?因为光圈是亮色的圆环,深色背景下对比度高,移动效果看得非常清楚,调试起来也直观。
具体操作:在右上角的Screen属性面板里找到
Canvas
配置,调整背景色。这里多说一句,Designer里的背景色和实际运行可能略有色差,但大体是准的,不影响开发。
背景色设置好之后,从左侧控件面板里拖一个
Box
放到屏幕上,把它铺满整个界面,这个Box就是背景板。为什么要用Box而不是直接用Screen背景?因为Box可以作为一个普通控件参与图层管理,后面如果想在背景上叠加半透明遮罩或者渐变效果,操作空间更大。
3.2 画光圈:Circle控件的参数陷阱
点击左侧控件栏中的Circle,然后在画布上拖出一个圆形。右侧属性栏会有几个和圆相关的参数:
- Radius(半径):这个直接决定光圈圆环的大小。我这边设置成60。
- Center X / Center Y(圆心坐标):这两个是圆心在父容器坐标系里的位置,不是控件的左上角。这里有一个我最初踩过的坑:Circle控件在布局系统里其实还是用一个矩形来定位的,这个矩形的左上角坐标和圆心坐标之间相差一个半径。也就是说,如果你想让圆心在屏幕坐标(120, 80)处,圆的左上角坐标应该是(120-60, 80-60),也就是(60, 20)。后面做移动动画的时候,移动的是控件的左上角,不是圆心,坐标换算很容易搞错。
- Line Color / Line Width:圆环的线条颜色和粗细。光圈要有“光”的质感,我会选一个明亮的青色或者橙黄色,线宽设置在6左右。
- Fill Color / Fill Alpha:填充颜色和透明度。做光圈的话,一般填充是透明的,也就是把Fill Alpha调成0,只保留外圈线条,这样看起来就是一个空心圆环。
这一步做完,你已经有了一个能显示的光圈。不过先别急着往下走,先在Designer里把它移动到屏幕中间偏左的位置,确认数据显示正常。
3.3 布置目标点:先确定“光圈要去哪儿”
目标点是光圈移动的锚点,我建议用圆形或者方形Box简单表示即可,优先级不高,重点是让位置关系清楚。
我放了4个目标点,坐标如下:
| 目标点索引 | 位置(左上角坐标) | 用途说明 |
|---|---|---|
| 0 | (120, 80) | 左上角目标点,光圈的初始位置 |
| 1 | (360, 80) | 右上角目标点 |
| 2 | (120, 200) | 左下角目标点 |
| 3 | (360, 200) | 右下角目标点 |
这4个点组成了2x2的网格,水平间距240像素,垂直间距120像素。你完全可以根据自己的屏幕重新设计网格间距,但网格逻辑建议保持不变,因为代码里“行号/列号 -> 屏幕坐标”的换算,依赖这样一个规律。
为了让光圈和目标点之间视觉上“套得准”,要注意圆环半径和目标点外沿的对应关系。我的目标点设计成半径40的实心圆,光圈半径60,这样光圈能正好把这些目标点包在里面,移动完成后视觉上是“光圈框住了目标点”,而不是重叠得乱七八糟。
3.4 图层顺序与命名规范
在Designer右下角的图层列表里,控件是有叠放顺序的。位于列表上方的控件,绘制时在更前面的层级。如果光圈和目标点叠在一起,你要保证光圈在目标点上面,否则视觉上就像目标点把光圈压住了。
我习惯把所有值得在代码里引用的控件都重命名:
-
光圈:
circleFocus -
四个目标点:
target0、target1、target2、target3(如果这一版只是示意,你也可以不引用它们)
命名这件事看着琐碎,但在写代码的时候特别重要。你如果在代码里看到
circleFocus
就知道这是焦点光圈,看到
circle3
这种名字还得回去查是哪个控件。这在TouchGFX里影响不大,但在稍微复杂一点的界面上,命名混乱会直接拖慢你的开发速度。
还需要强调一点:每次在Designer里添加完控件,点击右上角的“Generate Code”或者保存,Designer会把更新后的代码同步到工程里。然后切到IDE里编译,从ViewBase里就能看到新增的控件成员变量了。
4. 让光圈动起来:按键输入与动画实现的完整链路
4.1 TouchGFX里的两套输入:触摸和物理按键
TouchGFX框架默认处理的是触摸事件。你在Designer里给控件加点击交互,设计器和生成代码都会围绕触摸去构建。但物理按键是另一条路,它不会自动被当作触摸处理,需要你自己从GPIO读取,然后“注入”到事件系统里。
TouchGFX的事件分发机制大致是这样的:
-
触摸屏驱动在后台触发触摸事件,HAL层把事件交给
Application的事件队列,再由事件分发器派发给当前Screen的View。 -
物理按键更底层,它不经过触摸屏驱动,需要你自己写一小段代码读取GPIO状态,然后调用
Application::getInstance()->handleKeyEvent(key)这样的接口,把按键值派发给当前View的handleKeyEvent回调。
你可以把物理按键理解成“半外部输入源”:网络里传数据到CPU的GPIO,我读到GPIO电平变化,再转换成一张“事件票”递给GUI框架,之后所有GUI逻辑就都按你设定的规则运行了。
这样说吧,TouchGFX的
handleKeyEvent(uint8_t key)
是一个virtual函数,每个Screen的View都可以重写它,按键按下去之后,框架就会把对应的key值传给当前处于激活状态的View。你要做的,就是在这个回调里写“收到这个键该干嘛”的逻辑。
4.2 物理按键读取:从GPIO到事件注入
先定义按键键值。在
Screen1View.hpp
里我建议直接定义枚举或者宏:
// 自定义按键键值,可以放在View的头文件里
#define KEY_UP 1
#define KEY_DOWN 2
#define KEY_LEFT 3
#define KEY_RIGHT 4
然后在STM32的HAL层里,主循环或者一个定时任务中,周期扫描GPIO状态。以无RTOS的场景为例,在主循环里大概是这样一小段逻辑:
// main() 主循环中,伪代码示意
while (1)
{
uint8_t key = 0;
if (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin) == GPIO_PIN_RESET)
{
key = KEY_UP;
}
else if (HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port, KEY_DOWN_Pin) == GPIO_PIN_RESET)
{
key = KEY_DOWN;
}
else if (HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port, KEY_LEFT_Pin) == GPIO_PIN_RESET)
{
key = KEY_LEFT;
}
else if (HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port, KEY_RIGHT_Pin) == GPIO_PIN_RESET)
{
key = KEY_RIGHT;
}
if (key != 0)
{
// 可加简单消抖:确认电平稳定后再注入
Application::getInstance()->handleKeyEvent(key);
}
// TouchGFX渲染相关
touchgfx::HAL::getInstance()->backPorchExited();
}
注意几个细节:
第一,消抖。GPIO按键在按下和释放瞬间都会有机械抖动,如果不处理,一次按下的物理动作可能会产生多次
handleKeyEvent
。最简单的实用方案是“状态变化才触发”:记录上一次的键值,只有本次键值和上次不一样才去注入事件。更好的方案是延时20ms左右再读一次,确认电平稳定。相信我,这在按键控制里是必踩的坑,后面问题排查部分我会再提。
第二,
handleKeyEvent
的调用在主循环里的位置。理想情况下,它应该在TouchGFX的帧循环入口被调用,这样事件可以及时进入GUI事件队列。放了
backPorchExited()
调用之前,主循环一般都能工作。
第三,如果项目里用了FreeRTOS或者ThreadX,我更建议在任务里维护一个按键消息队列,GUI任务去读取队列,而不是在主循环里直接灌事件。这样输入和UI解耦,后面加按键、加编码器都容易。
4.3 View里处理按键:移动光圈的“状态机”
事件注入到GUI框架之后,当前激活Screen的View会收到
handleKeyEvent
回调。我的设计是把“按键”映射成“方向”,再把“方向”映射成“当前选中格的变化”。
由于目标点是2x2网格,我维护一个索引值
currentIndex
,它可以是0到3。行列换算关系是:
row = currentIndex / 2
col = currentIndex % 2
按下“右”键时,col加1;按下“下”键时,row加1。但要注意边界:col已经等于1时再按右,就不应该再变化。这个边界判断在
handleKeyEvent
里做,或者抽一个
moveFocus(dx, dy)
方法,传入行列偏移。
// Screen1View.cpp
void Screen1View::handleKeyEvent(uint8_t key)
{
if (key == KEY_UP)
{
moveFocus(0, -1);
}
else if (key == KEY_DOWN)
{
moveFocus(0, 1);
}
else if (key == KEY_LEFT)
{
moveFocus(-1, 0);
}
else if (key == KEY_RIGHT)
{
moveFocus(1, 0);
}
else
{
Screen1ViewBase::handleKeyEvent(key);
}
}
void Screen1View::moveFocus(int dx, int dy)
{
const int cols = 2;
const int rows = 2;
int row = currentIndex / cols;
int col = currentIndex % cols;
int newRow = row + dy;
int newCol = col + dx;
if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols)
{
return; // 已经到边界,不响应
}
currentIndex = newRow * cols + newCol;
// 更新目标坐标,动画将在 tick 中逐帧完成
targetX = targetPositions[currentIndex].x;
targetY = targetPositions[currentIndex].y;
}
targetPositions
是一个预定义的坐标数组,存放每个目标点处光圈左上角应处的位置。注意要提前减去半径偏移,因为前面说过Circle控件的定位是容器左上角。
// Screen1View.cpp 中定义
static const touchgfx::Point TARGET_POSITIONS[4] = {
{ 60, 20 }, // 目标点0,圆心120,80,半径60
{ 300, 20 }, // 目标点1,圆心360,80
{ 60, 140 }, // 目标点2,圆心120,200
{ 300, 140 } // 目标点3,圆心360,200
};
这样设计的好处是,目标点坐标改动只改一处数组,按键逻辑完全不用动。
4.4 平滑移动:两种动画方案的取舍
这是整个项目让视觉“像样”的关键一步。如果每次按键都把光圈直接
setXY
到新位置,效果是非常生硬的“瞬移”。要平滑移动,我试过两条路:
方案一:用TouchGFX的MoveAnimation机制。TouchGFX里有一个
MoveAnimator<T>
模板类,把控件包一层之后,可以直接调用
startMoveAnimation(newX, newY, duration)
,框架会在指定的毫秒数内自动做动画插值。如果以后项目里需要很多复杂的补间动画,走这条路最省事。
方案二:手写一个简单的逐帧插值,在
handleTickEvent
里做。这个方案更底层,但逻辑完全自己掌控,非常适合理解动画的本质。我这次在Demo里用的是方案二,因为代码量不大,而且不依赖额外的容器类型能避免版本差异。
手写插值的思想很朴素:每帧tick里,计算当前坐标和目标坐标的差值,如果差值大于步进速度,就向目标方向移动一个步长;如果差值很小,就直接定位到目标坐标。
// Screen1View::handleTickEvent
void Screen1View::handleTickEvent()
{
int16_t curX = circleFocus.getX();
int16_t curY = circleFocus.getY();
int dx = targetX - curX;
int dy = targetY - curY;
if (dx == 0 && dy == 0)
{
return;
}
const int16_t moveSpeed = 6; // 每帧最多移动6像素
int16_t stepX = dx;
if (stepX > moveSpeed) stepX = moveSpeed;
if (stepX < -moveSpeed) stepX = -moveSpeed;
int16_t stepY = dy;
if (stepY > moveSpeed) stepY = moveSpeed;
if (stepY < -moveSpeed) stepY = -moveSpeed;
circleFocus.setXY(curX + stepX, curY + stepY);
}
这样每次tick(TouchGFX一般以60fps运行,约16ms一帧)光圈会向目标靠近6个像素。当目标点之间距离240像素时,大概40帧能移动完,也就是0.6秒左右,肉眼看起来是平滑滑动,非常自然。
速度值
moveSpeed = 6
是调出来的,不是算出来的。步子太大了会像瞬移,太小了又显得拖沓。你在自己项目里可以根据目标点间距调整,一般间距100~200像素时,速度设4~8都是合理的。
4.5 初始状态与setupScreen的时机
还有一个容易忽略的小问题:刚进入界面时,光圈应该停在第一个目标点上,而不是从(0,0)一路飞过来。解决方法是把初始位置逻辑放到
setupScreen()
里。
void Screen1View::setupScreen()
{
Screen1ViewBase::setupScreen();
currentIndex = 0;
circleFocus.setXY(TARGET_POSITIONS[0].x, TARGET_POSITIONS[0].y);
targetX = TARGET_POSITIONS[0].x;
targetY = TARGET_POSITIONS[0].y;
}
setupScreen()
在每次界面切换进入当前Screen时都会被调用,所以回到这个界面时,光圈也会自动复位到初始位置,这是一个很自然的逻辑。比如你从设置界面返回主界面,光圈的记忆位置如果想保留,那就别在这里强制复位,这个可以根据自己的需求决定。
5. 实战踩坑:按键控制与动画调试的问题速查
5.1 按键按下没反应,或者移动了好几次
这是新手最容易遇到的两个问题,一个方向是“没反应”,一个方向是“反应过头”。
没反应的原因,90%出在按键事件没有真正传到View。我调试时常做三件事:
第一,确认GPIO配置正确。用HAL库的话,检查按键引脚有没有配置成输入模式,上拉/下拉是否正确。如果你的按键接法是一端接地、一端接引脚,那引脚要配置上拉,按下时读到低电平。如果你按下时读到的还是高电平,大概率是引脚配置反了。
第二,确认键值对应关系。
handleKeyEvent
里做一次
key
值的打印或者临时点亮一个LED,看看按键按下时
key
是不是你期望的值。这一步能快速把“按键没被读到”和“事件没进View”区分开。
第三,确认
Application::getInstance()->handleKeyEvent
是否被反复调用。如果你的主循环里没有做“状态变化才触发”,那每次循环都会注入一次事件,View里收到的就是连续多次移动。加上“边沿触发”或者消抖逻辑就好了。
反应过头还有一个隐蔽原因:按键按下持续时间太长,你的循环里每次扫描都注入了一遍事件,导致光圈来回移动。推荐用“下降沿”或者“短按一次只响应一次”的思路,最简单的实现就是记录上一个按键状态,只有
当前按下了且上次没按下
时才注入事件。
5.2 动画卡顿、掉帧、不够顺滑
动画卡顿的原因主要集中在渲染负载和内存访问上。
先看渲染负载。TouchGFX的透明混合、抗锯齿这些效果会占用大量CPU周期。如果你用了大面积的半透明填充或者频繁刷新整个屏幕,低主频MCU容易撑不住。我的做法是尽量让光圈动画期间,背景层保持不变,只有光圈在移动,这样TouchGFX只需重绘局部区域,性能压力小很多。
再看内存。如果你的板子没有额外SDRAM,显示缓冲通常放在内部SRAM里,容量有限。TouchGFX推荐双缓冲来实现流畅动画,但双缓冲对RAM要求更高。如果内存不足,会出现画面撕裂或闪烁。你可以在HAL配置里检查缓冲方案,能开双缓冲就开,不能的话至少保证单缓冲的帧率稳定。
最后看
moveSpeed
。如果每帧移动的像素太多,动画会表现为“跳格”,视觉不连续。如果每帧移动像素太少,动画虽然平滑但很慢,也容易让人觉得卡。我自己的经验是在60fps下,速度在4~8像素/帧之间比较舒服。如果慢得离谱,你可以考虑增大速度值。
5.3 修改代码后重新生成,改动全没了
这是一个关于TouchGFX工作流的经典坑,几乎每个人都会遇到一次。
在
Screen1View.cpp
里写的代码,如果写在Designer保留的自定义代码区域内,重新生成后不会丢。但如果改动在
Screen1ViewBase.cpp
、
Screen1ViewBase.hpp
这类
generated
目录或者
ViewBase
文件里,一旦Designer触发重新生成,改动会被冲掉。
我自己的习惯是:
- 控件布局、坐标、属性,全部通过Designer去改。
-
业务逻辑、按键回调、状态管理,全部写到
Screen1View和Screen1Presenter的自定义区域。 -
绝不手动修改
generated下的任何文件。
如果确实需要改底层HAL或者驱动,改成在
target
目录下进行,并保留修改记录。因为
target
目录虽然不是Designer直接生成的,但升级TouchGFX版本时也可能受到影响,最好固定版本后再动。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
| 按键按下无响应 | GPIO配置错误 | 检查输入模式和上下拉,打印键值确认 |
| 按键按下移动多次 | 无消抖/无边沿检测 | 加状态变化检测,或加20ms延时消抖 |
| 光圈移动方向反了 | 行/列方向和dx/dy映射搞反 | 检查moveFocus中dx/dy与上下左右的一致性 |
| 光圈没有套在目标点上 | Circle坐标换算错误 | 确认控件左上角=圆心-半径,检查TARGET_POSITIONS数组 |
| 动画卡顿掉帧 | 内存不足或全屏重绘过多 | 开双缓冲,减少透明混合区域,降低动画区域复杂度 |
| 图片/文字显示花屏 | LCD初始化或SDRAM配置错误 | 检查HAL层屏幕配置,确认显存地址正确 |
| 重新生成代码后功能丢失 | 改到了generated目录或ViewBase | 业务逻辑迁移到自定义View文件中,重新生成后复查 |
5.5 把项目扩展成更实用的东西
这个“按键控制光圈移动”的Demo,做完之后只是一个起点。我基于这套逻辑继续做过几个扩展,效果都很好,顺手推荐给你:
一是在每个目标点上挂一个菜单项,比如“温度”、“风速”、“模式”、“定时”,用光圈做选中指示,按键负责切换,这就是典型的家电UI菜单交互。
二是加上“确认键”。在目标网格之外,再加一个独立的按键作为“确认”,按下后跳转到一个新Screen或者弹出一个对话框。你只需要在
handleKeyEvent
里多判断一个键值,然后调用
presenter->okPressed()
之类的方法,逻辑完全复用。
三是在光圈的移动动画里加入缓动效果,让动画更像“甩过来”的感觉。TouchGFX本身有缓动函数库,比如
cubicEaseOut
,如果你用
MoveAnimator
方式,直接传一个缓动函数;如果用tick插值,可以通过一个时间比例来改变每帧步长。视觉档次会明显提升。
四是把4个方向键换成编码器或摇杆。编码器的脉冲计数读出来是一个方向增量,本质上和按键dx/dy是一样的,只要你把旋转方向转换成对应的“左/右”事件,整个框架不用改。
一点收尾的个人体会
这个项目做完,我个人最大的体会是: TouchGFX真正的门槛不在控件和API,而在“事件流经的路径” 。触摸事件是框架替你做好的,物理按键却需要你自己从底层一路接到GUI层,中间任何一环断了,表现出的现象都是“没反应”,但排查起来却要从硬件一路查到软件。如果以后再遇到类似现象,我会先确认“事件是否真的到达了View”这个边界问题,再去动具体业务代码。另外,坐标换算这种小细节,虽然不起眼,但在做焦点移动类UI时特别容易反复出错,最好在项目一开始就统一用“圆心坐标”或“左上角坐标”中的一种,并在代码注释里写清楚。
希望这篇笔记能帮你少走几步弯路。
120




被折叠的 条评论
为什么被折叠?



