TuringUi系列(四)
TuringUi系列(四)
**CzrTuringB:**在前几篇博客中,我们已经实现了基本元素的绘制和动画函数的编写,接下来我将着重讲解ui框架的实现构想和思路。
第一部分 TuringUi架构
第一章 文件目录
flowchart TB Root Root-->Folder1[Device] Folder1-->File1[Ssd1306] Root-->Folder2[Basic] Folder2-->File2[UiCore] Folder2-->File3[UiPlot] Folder2-->File4[UiPlot3D] Folder2-->File5[UiFontLibrary] Folder2-->File6[UiPictureLibrary] Folder2-->File7[UiAnimation] Folder2-->File8[UiEvent] Folder2-->File9[UiRender] Root-->Folder3[Widget] Folder3-->File10[UiNumberBox] Folder3-->File11[UiCheckBox] Folder3-->File12[UiLabel] Folder3-->File13[UiButton] Folder3-->File14[UiList] Folder3-->File15[……] Root-->Folder4[Application] Folder4-->File16[UiInit] Folder4-->File17[UiNavigation] Folder4-->File18[UiRootPage] Folder4-->File19[Ui……Page]
1 | flowchart TB |
-
设备目录:存放显示驱动文件
-
Basic目录:
- UiCore:Ui内核文件
- UiPlot:二维对象的绘制
- UiPlot3D:三维对象的绘制
- UiFontLibrary:字体库
- UiPictureLibrary:图标库
- UiAnimation:对象的各种变换与动画实现
- UiEvent:事件处理器
- UiRender:渲染器
-
Widget目录:存放UI部件
-
Application目录:
- UiInit:Ui初始化
- UiNavigation:Ui页面树的实现
- UiRootPage:TuringUi根页面
- ……:其他应用页面的实现
第二章 Ui内核的实现
第一节 硬件随机数生成
1 | /** |
第二节 UI元素
-
Ui元素是所有Ui部件必须包含的数据结构,其存放了各类部件的公有属性,通过使用函数指针和空指针实现了,各类Ui部件的事件处理、渲染、销毁、动画的函数重载(这个说法不是很准确)
-
数据结构:
1
2
3
4
5
6
7
8
9struct UiElement
{
Bool isHandle; // UI元素是否需要处理事件
void* widget; // 指向UI部件
void (*render)(UiElement* ele); // 渲染函数
void (*animation)(UiElement* ele); // 动画函数
void (*handleEvent)(UiElement* ele, void* value); // 事件处理函数
void (*destory)(UiElement* ele); // 析构函数
}; -
Ui元素的创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
*@ FunctionName: void UiElementCreate(void)
*@ Author: CzrTuringB
*@ Brief: 创建一个UI元素
*@ Time: Jan 9, 2025
*@ Requirement:
*/
UiElement* UiElementCreate(void)
{
UiElement* ele = pvPortMalloc(sizeof(UiElement));
if(ele == NULL) return NULL;
memset(ele, 0, sizeof(UiElement));
return ele;
} -
Ui元素的初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
*@ FunctionName: void UiElementInit(UiElement* ele,void* widget, void (*render)(UiElement*), void (*handle)(UiElement*, void*), void (*destory)(UiElement*))
*@ Author: CzrTuringB
*@ Brief: 初始化UI元素
*@ Time: Jan 2, 2025
*@ Requirement:
*@ 1、render即为UI元素渲染函数的指针
*@ 2、handle即为UI元素事件处理函数的指针
*/
void UiElementInit(UiElement* ele, void* widget, void (*render)(UiElement*), void (*handle)(UiElement*, void*), void (*destory)(UiElement*))
{
ele->widget = widget;
ele->isHandle = True;
ele->render = render;
ele->handleEvent = handle;
ele->destory = destory;
//带动画的UI部件需要单独配置
ele->animation = NULL;
} -
销毁:
1
2
3
4
5
6
7
8
9
10
11/**
*@ FunctionName: void UiElementDestroy(UiElement* ele)
*@ Author: CzrTuringB
*@ Brief: 销毁UI页面
*@ Time: Jan 2, 2025
*@ Requirement:
*/
void UiElementDestroy(UiElement* ele)
{
if (ele != NULL) vPortFree(ele);
}
第三节 Ui页面
-
Ui页面:TuringUi框架将应用程序视为由多个UI页面组成,在程序运行声明周期内,有且只有一个页面,即全局变量uiPage(指针),在发生页面切换时,销毁上个页面的内容,并分配新的内存给创建的页面。
-
数据结构:
1
2
3
4
5
6
7
8
9
10
11
12//ui页面
typedef struct
{
Bool haveOngoing; // Ui页面是否包含持续事件
pTreeNode base; // Ui页面的基类(指向索引树)
uint8_t index; // Ui页面的索引(用于子节点数组的索引)
uint8_t eleCount; // 当前页面包含的UI元素数量
Queue elements; // 存储页面的所有UI元素
Queue elesAnimation; // 当前页面的UI元素动画队列
Queue elesRender; // 当前页面的UI元素渲染队列
void (*handleEvent)(UiEvent* event); // 事件处理函数
}UiPage; -
页面的创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
*@ FunctionName: UiPage* UiPageCreate(void)
*@ Author: CzrTuringB
*@ Brief: 动态创建一个UI页面
*@ Time: Jan 2, 2025
*@ Requirement:
*/
UiPage* UiPageCreate(void)
{
//动态分配 UiPage 内存
UiPage* page = pvPortMalloc(sizeof(UiPage));
if (page == NULL)
{
//如果分配失败,返回 NULL
return NULL;
}
memset(page,0,sizeof(UiPage));
//返回创建的UI页面指针
return page;
} -
页面的初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/**
*@ FunctionName: void UiPageInit(UiPage* page, uint8_t count, pTreeNode node,Bool ongoing,void (*handleEvent)(UiEvent*))
*@ Author: CzrTuringB
*@ Brief: 初始化UI页面
*@ Time: Jan 2, 2025
*@ Requirement:
*@ 1、elements即为ui元素指针数组
*@ 2、count即为页面包含的ui元素的个数
*@ 3、node即为页面所在菜单索引树的节点
*/
void UiPageInit(UiPage* page, uint8_t count, pTreeNode node,Bool ongoing,void (*handleEvent)(UiEvent*))
{
page->eleCount = count; // 设置当前元素数量
page->index = 0; // 下一级页面的索引
page->base = node; // 指定页面所处菜单树的位置
page->handleEvent = handleEvent; // 指定页面的事件处理函数
page->haveOngoing = ongoing;
//创建一块内存用于存储页面所有元素队列
UiElement** eleBuffer = (UiElement**)pvPortMalloc(count * sizeof(UiElement*));
memset(eleBuffer,0,count * sizeof(UiElement*));
QueueInit(&page->elements, sizeof(UiElement*), count, eleBuffer);
//创建一块内存用于存储后续需要动画处理的元素
eleBuffer = NULL;
eleBuffer = (UiElement**)pvPortMalloc(count * sizeof(UiElement*));
memset(eleBuffer,0,count * sizeof(UiElement*));
QueueInit(&page->elesAnimation, sizeof(UiElement*), count, eleBuffer);
//创建一块内存用于存储后续需要渲染处理的元素
eleBuffer = NULL;
eleBuffer = (UiElement**)pvPortMalloc(count * sizeof(UiElement*));
memset(eleBuffer,0,count * sizeof(UiElement*));
QueueInit(&page->elesRender, sizeof(UiElement*), count, eleBuffer);
} -
页面的销毁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
*@ FunctionName: void UiPageDestroy(UiPage* page)
*@ Author: CzrTuringB
*@ Brief: 销毁UI页面
*@ Time: Jan 2, 2025
*@ Requirement:
*/
void UiPageDestroy(UiPage* page)
{
if (page != NULL)
{
if (page->elements.pdQueue != NULL)
{
vPortFree((UiElement**)page->elements.pdQueue);
}
if (page->elesAnimation.pdQueue != NULL)
{
vPortFree((UiElement**)page->elesAnimation.pdQueue);
}
if (page->elesRender.pdQueue != NULL)
{
vPortFree((UiElement**)page->elesRender.pdQueue);
}
//释放UiPage自身的内存
vPortFree(page);
}
} -
进入下一级页面【还未实现完全】:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
*@ FunctionName: pTreeNode UiPageEnter(UiPage* page)
*@ Author: CzrTuringB
*@ Brief: 进入下一级页面
*@ Time: Jan 2, 2025
*@ Requirement:
*/
pTreeNode UiPageEnter(UiPage* page)
{
pTreeNode node;
if(page->base->children == NULL) return NULL;
node = page->base->children[page->index];
UiPageDestroy(page);
return node;
} -
返回上一级页面【还未实现完全】:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
*@ FunctionName: pTreeNode UiPageQuit(UiPage* page)
*@ Author: CzrTuringB
*@ Brief: 返回上一级页面
*@ Time: Jan 2, 2025
*@ Requirement:
*/
pTreeNode UiPageQuit(UiPage* page)
{
pTreeNode node;
if(page->base->parent == NULL) return NULL;
node = page->base->parent;
UiPageDestroy(page);
return node;
}
第二部分 菜单树
第一章 N叉树的实现
-
C文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87/**
*@ FileName: nTree.c
*@ Author: CzrTuringB
*@ Brief: N叉树
*@ Time: Sep 22, 2024
*@ Requirement:
*/
/* Includes ------------------------------------------------------------------*/
/* Data ------------------------------------------------------------------*/
/* Functions ------------------------------------------------------------------*/
/**
*@ FunctionName: pTreeNode CreateNode(void* data, size_t dataSize, InitFunc initFunc)
*@ Author: CzrTuringB
*@ Brief: 动态创建一个节点
*@ Time: Jan 2, 2025
*@ Requirement:
*/
pTreeNode CreateNode(void* data, size_t dataSize, InitFunc initFunc)
{
//分配节点内存
pTreeNode node = (pTreeNode)pvPortMalloc(sizeof(TreeNode));
if (!node)
{
//内存分配失败,输出调试信息
return NULL;
}
//分配数据内存并复制数据
node->data = pvPortMalloc(dataSize);
if (!node->data)
{
//内存分配失败,输出调试信息
vPortFree(node);
return NULL;
}
memcpy(node->data, data, dataSize);
//初始化子节点信息
node->num = 0;
node->children = NULL;
node->parent = NULL;
//初始化节点的初始化函数指针
node->initFunc = initFunc;
//返回节点指针
return node;
}
/**
*@ FunctionName: void AddChild(pTreeNode parent, pTreeNode child)
*@ Author: CzrTuringB
*@ Brief: 添加子节点
*@ Time: Jan 2, 2025
*@ Requirement:
*/
void AddChild(pTreeNode parent, pTreeNode child)
{
//如果形参指针为空,返回或输出打印信息
if (!parent || !child) return;
//调整子节点数组大小
parent->children = (pTreeNode*)pvPortMalloc(sizeof(pTreeNode) * (parent->num + 1));
if (!parent->children)
{
//内存分配失败,输出打印信息
return;
}
// 添加新子节点
parent->children[parent->num++] = child;
child->parent = parent;
}
/**
*@ FunctionName: void freeTree(pTreeNode node)
*@ Author: CzrTuringB
*@ Brief: 释放树
*@ Time: Jan 2, 2025
*@ Requirement:
*/
void FreeTree(pTreeNode node)
{
if (!node) return;
//递归释放所有子节点
for (size_t i = 0; i < node->num; i++)
{
FreeTree(node->children[i]);
}
//释放子节点数组和节点数据
vPortFree(node->children);
vPortFree(node->data);
vPortFree(node);
} -
H文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/**
*@ FileName: nTree.h
*@ Author: CzrTuringB
*@ Brief: N叉树
*@ Time: Sep 22, 2024
*@ Requirement:
*/
/* Includes ------------------------------------------------------------------*/
/* Data ------------------------------------------------------------------*/
//Function Pointer
typedef void (*InitFunc)(void* data, void* params);
//Struct
//定义树节点
typedef struct TreeNode
{
void* data; //指向树节点数据
InitFunc initFunc; //存储初始化函数的指针
size_t num; //树节点个数
struct TreeNode** children; //存储一个指向结构体数组的指针
struct TreeNode* parent; //存储父节点指针
}TreeNode, *pTreeNode;
/* Functions------------------------------------------------------------------*/
pTreeNode CreateNode(void* data, size_t dataSize, InitFunc initFunc);
void AddChild(pTreeNode parent, pTreeNode child);
void FreeTree(pTreeNode node);
第二章 菜单树的实现
-
原理:
1
2
3
4
5
6
7
8typedef struct TreeNode
{
void* data; //指向树节点数据
InitFunc initFunc; //存储初始化函数的指针
size_t num; //树节点个数
struct TreeNode** children; //存储一个指向结构体数组的指针
struct TreeNode* parent; //存储父节点指针
}TreeNode, *pTreeNode;-
一个树节点对应一个Ui页面
-
data指针指向一个字符串,用于表明页面的名称
-
initFunc函数指针指向应用页面的初始化函数,不同的页面需要加载不同的UI元素,因此指向页面的初始化函数方便调用。
-
num存储子节点个数,方便动态创建页面节点。
-
children是一个存储树节点指针的数组的指针。
-
parent指向页面的父节点
-
-
菜单树初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
*@ FunctionName: void MenuTreeInit(void)
*@ Author: CzrTuringB
*@ Brief: 初始化菜单页面索引树
*@ Time: Jan 2, 2025
*@ Requirement:
*@ 1、这个页面索引树贯穿整个程序生命周期,所以不用销毁分配的空间
*@ 2、在程序编写时就要确定好整个UI框架的目录结构,不要再程序运行期间添加节点
*/
void MenuTreeInit(void)
{
//根页面节点的创建
rootNode = CreateNode("root", sizeof("root"), RootPageInit);
//向跟页面添加子节点
// for(uint8_t i=0; i<rootPage->num; i++)
// {
// AddChild(rootPage, menu1[i]);
// }
}
第三部分 Ui初始化
**CzrTuringB:**TuringUi基于FreeRTOS实时操作系统,在程序刚开始运行时会创建一个OledAPP任务,其执行TuringUI的初始化函数后便会被删除。
-
OledApp任务:
1
2
3
4
5
6
7
8
9
10
11void OledApp(void)
{
//TuringUi初始化
UiInit();
//删除当前任务
vTaskDelete(NULL);
while(1)
{
;
}
} -
初始化流程:
- 延时一会,防止刚开始上电SSD1306来不及初始化
- 初始化SSD1306
- 创建开机动画任务,开机动画播放期间,同步进行后续的初始化
- 初始化菜单树
- 初始化根页面
- 初始化完成后,删除开机动画任务
- 创建Ui事件处理任务和Ui渲染任务
-
初始化函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32/**
*@ FunctionName: BspState UiInit(UiPage* page)
*@ Author: CzrTuringB
*@ Brief: 初始化UI框架
*@ Time: Oct 30, 2024
*@ Requirement:
*/
BspState UiInit()
{
vTaskDelay(pdMS_TO_TICKS(50)); //延时50ms,避免重新上电导致OLED初始化失败
SSDInit(); //初始化SSD1306
//创建开机动画任务
xTaskHandle StartupAnimationHandle;
xTaskCreate((void*)StartupAnimation, "StartupAnimation", 128 * 4, NULL, 24, &StartupAnimationHandle);
TickType_t time = xTaskGetTickCount();
//初始化UI菜单
MenuTreeInit();
//1、初始化根页面
RootPageInit((void*)rootNode, (void*)NULL);
//2、开机动画播放时间设置为4s
TickType_t nowTime = xTaskGetTickCount();
if(nowTime - time < pdMS_TO_TICKS(4000)) vTaskDelayUntil(&time, pdMS_TO_TICKS(4000));
vTaskDelete(StartupAnimationHandle);
PlotFillScreen(uiDevice->disBuffer, Fill0);
SSDUpdateScreen();
//3、初始化事件管理器
xTaskHandle UiEventTaskHandle;
xTaskCreate((void*)UiEventHandle, "UiEvent", 128 * 4, NULL, 24, &UiEventTaskHandle);
//4、初始化屏幕渲染器
xTaskCreate((void*)UiRenderHandle, "UiRender", 128 * 4, NULL, 24, &UiRenderTaskHandle);
return BspOk;
} -
开机动画的设计[1]:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37/**
*@ FunctionName: void StartupAnimation(void)
*@ Author: CzrTuringB
*@ Brief: 开机动画
*@ Time: Oct 30, 2024
*@ Requirement:
*/
void StartupAnimation(void)
{
float attribute[3]; //创建动作因子
uint8_t fillMode[11] = {8,3,5,2,6,3,4,2,7,3,1}; //外边框填充模式的变化规律
AnimType anit; //创建动画类型
AnimTypeInit(&anit,aEasingInOut); //动画类型设置为缓入换出
MoveScale scale[3] = {{0,60},{0,160},{0,10}}; //设置动画的起始位置和结束位置:内圆弧旋转角度、外圆弧旋转角度,边框动作模式
AnimElement anie[3];
for(uint8_t i=0;i<3;i++)
{
//初始化动画元素
AnimElementInit(&anie[i], &anit, &scale[i], &attribute[i]);
}
AnimSet anis;
AnimSetInit(&anis, &anie[0], 3, pdMS_TO_TICKS(1500));//设置动画时间为1.5s
while(1)
{
PlotCleanBuffer(uiDevice->disBuffer);
PlotString((Point2D){46,28}, "Turing", C6x8, uiDevice->disBuffer);
PlotArcRectangle((Point2D){0,0}, 127, 63, 8, uiDevice->disBuffer, (FillMode)fillMode[(uint8_t)attribute[2]]);
PlotRingWithRoundedEnds((Point2D){64,32}, 22, 20, uiDevice->disBuffer, 0+attribute[1], 150+attribute[1], FillB);
PlotRingWithRoundedEnds((Point2D){64,32}, 22, 20, uiDevice->disBuffer, 180+attribute[1], 330+attribute[1], FillB);
PlotRingWithRoundedEnds((Point2D){64,32}, 28, 24, uiDevice->disBuffer, 0+attribute[0], 90+attribute[0], Fill5);
PlotRingWithRoundedEnds((Point2D){64,32}, 28, 24, uiDevice->disBuffer, 120+attribute[0], 210+attribute[0], Fill5);
PlotRingWithRoundedEnds((Point2D){64,32}, 28, 24, uiDevice->disBuffer, 240+attribute[0], 330+attribute[0], Fill5);
Animation(&anis);
SSDUpdateScreen();
AnimBidirectionalLoop(&anis, 3);
}
}
第四部分 Ui事件管理器【还未实现加锁】
-
事件管理机制:创建了一个以UiEvent结构体变量为元素的FreeRTOS队列,其他外设任务可以向队列中发送UiEvent,事件处理任务在接收到队列中的事件信号后就会进行相应的处理。
-
数据类型:
1
2
3
4
5
6
7
8
9
10
11
12typedef enum
{
EventOngoing, //持续触发事件
EventButton, //按键事件
EventTerminal, //终端事件
}EventType;
typedef struct
{
EventType type; //存储事件类型
uint8_t id; //存储事件ID,用于分辨不同外设的同类型事件,如多个按键的单击事件;
void* data; //存储事件携带的数据
}UiEvent; -
事件队列初始化:
1
2
3
4
5
6
7
8
9
10
11
12/**
*@ FunctionName: void EventManagerInit(void)
*@ Author: CzrTuringB
*@ Brief: 事件管理器初始化
*@ Time: Jan 10, 2025
*@ Requirement:
*/
void EventManagerInit(void)
{
//创建事件消息队列
eventQueue = xQueueCreate(EventQueueSize, sizeof(UiEvent));
} -
事件的投递:
1
2
3
4
5
6
7
8
9
10
11/**
*@ FunctionName: BaseType_t PostUiEvent(UiEvent* event)
*@ Author: CzrTuringB
*@ Brief: 事件的发送
*@ Time: Jan 10, 2025
*@ Requirement:
*/
BaseType_t PostUiEvent(UiEvent* event)
{
return xQueueSendToFront(eventQueue, event, portMAX_DELAY);
} -
事件任务函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
*@ FunctionName: void UiEventHandle(void)
*@ Author: CzrTuringB
*@ Brief: TuringUi的事件管理任务函数
*@ Time: Jan 6, 2025
*@ Requirement:
*/
void UiEventHandle(void)
{
EventManagerInit();
UiEvent event;
while(1)
{
//接收其他任务发送的事件
if (xQueueReceive(eventQueue, &event, portMAX_DELAY) == pdTRUE)
{
//执行页面的事件处理函数
uiPage->handleEvent(&event);
//每执行完一个事件,释放事件数据
vPortFree(event.data);
}
}
}
第六部分 Ui渲染器
-
渲染任务处理函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38/**
*@ FunctionName: void UiEventHandle(void)
*@ Author: CzrTuringB
*@ Brief: TuringUi的事件管理任务函数
*@ Time: Jan 6, 2025
*@ Requirement:
*/
void UiRenderHandle(void)
{
UiElement* ele;
while(1)
{
//动画计算
while(uiPage->elesAnimation.count)
{
QueueOutElement(&uiPage->elesAnimation, &ele);
//调用UI部件的动画处理函数进行计算
ele->animation(ele);
}
//渲染绘制
while(uiPage->elesRender.count)
{
QueueOutElement(&uiPage->elesRender, &ele);
//调用UI部件的渲染处理函数进行计算
ele->render(ele);
}
//页面上的持续事件
if(uiPage->haveOngoing == True)
{
UiEvent event;
event.type = EventOngoing;
event.id = 0; //表示持续动画事件
event.data = NULL;
PostUiEvent(&event); //确保事件被发送
}
SSDUpdateScreen();
}
}
第七部分 页面设计
第一章 页面初始化示例
1 | /** |
第二章 页面事件处理函数
1 | /** |
第三节 页面销毁
1 | /** |