[X]关闭

[米联客-XILINX-H3_CZ08_7100] FPGA_SDK高级篇连载-21 触摸液晶屏 UGUI 人机交互方案

文档创建者:FPGA课程
浏览次数:252
最后更新:2024-10-11
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-SOC » 1_SDK应用方案(仅旗舰型号) » 2-SDK高级应用方案
本帖最后由 FPGA课程 于 2024-10-11 17:29 编辑

​软件版本:VIVADO2021.1
操作系统:WIN10 64bit
硬件平台:适用 XILINX A7/K7/Z7/ZU/KU 系列 FPGA
实验平台:米联客-MLK-H3-CZ08-7100开发板
板卡获取平台:https://milianke.tmall.com/
登录“米联客”FPGA社区 http://www.uisrc.com 视频课程、答疑解惑!



1 概述
很多工程师都在 ZYNQ 上做过 LINUX 相关的应用和开发,在 ZYNQ 运行 LINUX 操作系统,可以实现非常细致的GUI 图形界面。用户可以通过鼠标、键盘等外部设备与操作系统通过 GUI 界面进行人机交互,与平时使用电脑的体验相当。但同时也有很用户只涉及 ZYNQ 的裸机开发,那么在无操作系统支持的情况下,是否可以在裸机环境中构建一个GUI 图形界面呢?答案是肯定的。本例程通过利用开源 GUI μGUI,在 ZYNQ 裸机环境下创建一组简单的 GUI 界面,并通过触摸液晶屏实现与 ZYNQ 的人机交互。
本例程通过开源的 uGUI v0.3 库设计了 1 GUI 界面,为该 GUI 界面设计了 4 个窗口,为每个窗口设计了标题、按键、文本、图片 logo 等元素,并给每个按键设定了相应的功能,如窗口间切换、LED 灯控制、屏幕亮度调节等,并在 1 个窗口中设计了绘图功能。然后,将设计的 GUI 窗口通过触摸屏显示。用户通过按下触摸屏中所显示的窗口对应的按键位置,便可以实现该按键所设定的相应功能,从而实现人机交互。
本文实验目的:
1:LCD 触摸屏的使用原理
2:移植开源 μGUI 库,利用 API 函数搭建一组 GUI 图形界面
3:通过 VDMAAXI-S Video OutVTC IP 实现 GUI 图形界面的显示
4:通过定时器中断实现 GUI 界面的周期性刷新
5:通过 PS 动态配置 AXI PWM 调节液晶屏亮度
6:通过 AXI GPIO 检查触摸屏的中断信号
7:通过 I2C 读取触摸屏的触摸信息
8:设计 GUI 界面的动态变化机制,将触摸信息反馈至 GUI 界面后,GUI 产生动态变化,形成人机交互。
2系统框图
8abdb1ed50134b909d981aaeb408d21b.jpg
3硬件电路分析
硬件接口和子卡模块请阅读“附录1”
配套工程的FPGA PIN脚定义路径为soc_prj/uisrc/04_pin/ fpga_pin.xdc。
本文内容仅给出RGB方案的说明,LVDS方案类似,也提供了配套源码和工程
4触摸液晶屏介绍
4.1液晶屏介绍
112a907048c24214867679314463789d.jpg
2020版本资料中使用触摸液晶屏是www.uisrc.com(米联客) 和www.crazyfpga.com(crazy bingo)联合开发。液晶屏参数:
                        
型号
                        
                        
VCLD-7TH
                        
                        
液晶显示屏型号
                        
                        
T HC63LVDM83D
                        
                        
液晶分辨率
                        
                        
1024*600
                        
                        
液晶屏幕比例
                        
                        
4:3
                        
                        
液晶驱动接口
                        
                        
RGB/VGA
                        
                        
触摸芯片型号
                        
                        
GP915
                        
                        
支持触点数量
                        
                        
5
                        
                        
支持最大分辨率
                        
                        
1024*600
                        
4.2触摸介绍
929825497c0d48cfb008cee042a145bb.jpg
GT915是专为 4.5”~6”设计的新一代 5 点电容触控方案,拥有26个驱动通道和14个感应通道,以满足更高的 touch 精度要求。GT915可同时识别5个触摸点位的实时准确位置,移动轨迹及触摸面积。并可根据主控需要,读取相应点数的触摸信息。
GT915 提供标准的 I 2C 通讯接口,由 SCL 和 SDA 与主 CPU 进行通讯。 在系统中 GT915 始终作为从设备,所有通讯都是由主 CPU 发起,建议通讯速度为 400Kbps 或 以下。
GT915上电顺序:
0a4084766ccf4177ab4ce43b41fe9717.jpg
INT T2 时间后,主控是要输出高,还是低,取决于主机要用何 I2C 从设备地址与 GT9 芯片通信,若用 地址 0x28/0x29,则输出高;若用地址 0xBA/0xBB,则输出低。
1:设置I2C地址为0x28/0x29
951a180332234f2e83fd1cb622cefcad.jpg
2:设置I2C地址为0xBA/0xBBz
dab81b0403944cbaa65422ee9bc958ca.jpg
3:寄存器配置
8b94d8a8e08241bfb9ba8035ca282aec.jpg
a83178d0335844eb99a6e1a1355128c9.jpg
默认的配置有可能不能满足要求,可以通过配置寄存器,修改默认的配置,我们重点关注0x8047、0x8048、0x8049、0x804A、0x804B、0x804C、0x804D这配置寄存器。
0x8047配置版本寄存器:该寄存器用于设置配置信息版本号,如果更新的版本号大于等于原来的版本,当配置不一样时才能更新到寄存器中。当写0x00,不管之前的版本号时说明,都会把这个寄存器初始化位0xa所以,一般我们都会写如0x00。
0x8048~0x804B用于设置触摸的分辨率。
0x804C用于设置触点上限,对于GT915支持的最大触点数量为5个。
另外配置寄存器后,还需要正确输入校验和寄存器0x8047以及0x80fe寄存器,更新配置。
4:读取坐标
14b536ca1ad9447e888ed9d177bb0f8b.jpg
我们可以读取坐标寄存器获取触摸芯片的ID、固件版本号、当前的分辨率、以及触摸状态、track id以及坐标信息。GT915支持5点触摸。以上表格只截取了第一个触点的寄存器,其他触点都一样,具体可以阅读uisrc/06_doc路径下的技术手册。
主控可以采取轮询或 INT 中断触发方式来读取坐标,采用轮询方式时可采取如下步骤读取:
1、先读取寄存器 0x814E,若当前 buffer(buffer status 为 1)数据准备好,则依据手 指个数读、按键状态取相应个数的坐标、按键信息。
2、若在 1 中发现 buffer 数据(buffer status 为 0)未准备好,则等待 1ms 再进行读取。
GT9 中断信号输出时序为(以输出上升沿为例,下降沿与此时序类同):
1、待机时 INT 脚输出低。
2、有坐标更新时,输出上升沿。
3、2 中输出上升沿后,INT 脚会保持高直到下一个周期(该周期可由配置 Refresh_Rate 决定)
5搭建SOC系统工程
5.1PL图形编程
273a3598041b436188a9585f35d2be02.jpg
5.2IP配置
本文中的大部分IP模块在前面的文章实验中都介绍过如何配置了,以下重点看下各个IP的参数设置
1:PWM IP设置
e3702ccec9484041aac101cf7a055e55.jpg
这重点看下pwm ip部分的源码:
以下代码支持设置PWM频率、占空比、以及输出极性
608e33879b6e44a9a326916ee9f1f3de.jpg
Pwm的参数通过AXI-Lite总线进行配置
6fde2c89294d481f96b257456f5e684d.jpg
2:axi-gpio设置
e4eff572013e4635820bc3e726e135e5.jpg
3:axis_subset_converter设置
35c15a3ff64744bcac924acb6dcc9da6.jpg
4:VDMA IP设置
c48c0204888e4f68a746145ee03b3419.jpg
4853e9a6d6ed47469260e3ac8e7edcc2.jpg
5:PLL时钟设置
8d55adc89946470fb0c20c9a0aa02517.jpg
29d12057cbdc450d8f52db5163d318f8.jpg
6:VTC视频时序IP设置
079eb33ff9d242b08374c3165388da99.jpg
00b466bdcaa2453ab6468480d4f17905.jpg
5.3编译并导出平台文件
1:单击Block文件à右键àGenerate the Output ProductsàGlobalàGenerate。
2:单击Block文件à右键à Create a HDL wrapper(生成HDL顶层文件)àLet vivado manager wrapper and auto-update(自动更新)。
3:生成Bit文件。
4:导出到硬件: FileàExport HardwareàInclude bitstream
5:导出完成后,对应工程路径的soc_hw路径下有硬件平台文件:system_wrapper.xsa的文件。根据硬件平台文件system_wrapper.xsa来创建需要Platform平台。
293de85a8d3c44ec8963a5a00bf8f09d.jpg
6搭建Vitis-sdk工程
创建zu_base sdk platform和APP工程的过程不再重复,可以阅读本章节01~05相关demo。以下给出创建好zu_base sdk platform的截图和对应工程APP的截图。
6.1创建SDK Platform工程
4c491dbc1e034587853c034575f6b16b.jpg
6.2创建SDK APP工程
5dfc4f8b79ce4b56886abfe08b8948b4.jpg
7程序分析
7.1GPIO输入
在SOC图形化编程中,我们使用了axi-gpio扩展了8个GPIO
570fa86b8b0447dcabe2ca53a5fba9aa.jpg
以下对gpio的功能做了一些定义:
gpio-bit4/gpio-bit5定义到开发板的2个led
gpio-bit6定义到触摸芯片GT9XX的RESET复位
gpio-bit7定义到触摸芯片GT9XX的INT中断
9199eb2bfadd4a85b94d255c96a5da8c.jpg
在C代码中定义如下宏定义,方便对GPIO进行操作
  1. #define LED0 0x10
  2. #define LED1 0x20
  3. #define LCD_RST 0x40
  4. #define LCD_INT 0x80
复制代码
通过Gpiopl_Setup_Intr_System()初始化并使能AXI GPIO的输入中断
GpioplIntrHandler()函数:
判断GPIO[7]输入的触摸屏中断信号是否为0,若为0调用i2cps_rd16(&I2cInst0,&touch_rdata,9,GT9XX_ADDR);读取坐标和i2cps_wr16(&I2cInst0,&touch_wdata,1,GT9XX_ADDR)清楚触摸屏芯片的状态信息为下次读取做好准备,并触摸屏中断标志位btn_event置1,该信号将在定时器中断中使用。
7.2PWM信号输出
AXI PWM的驱动程序由pwm_config.cpwm_config.h组成。
本例程所使用的液晶屏建议输入PWM信号频率为100Hz~200K Hz。在main函数中通过PWM_Init()函数设置初始输出PWM信号的周期和占空比。
PWM_Init函数:
l 通过PWM_Set_Period函数设置PWM信号周期
l 通过PWM_Set_Duty函数设置PWM信号低电平的占空比
上述函数都是以时钟周期数来设置。在pwm_config.h包含了PWM信号周期和占空比的时钟周期数宏定义。如下:
  1. <b><font color="#000000">#define </font></b><font color="#000000">PERIOD_CLOCK_NUM 409600 </font>
  2. <b><font color="#000000">#define </font></b><font color="#000000">DUTY_CLOCK_NUM 204800</font>
复制代码

由于本例程中,PL部分AXI PWM的参考时钟频率为100MHz,一个时钟周期就是10ns。因此,PWM_Init所设置的PWM信号的周期为4.096ms,占空比为50%,频率约为250Hz
为了对PWM信号的占空比进行动态调节达到改变液晶屏背光源亮度的目的,又设计了PWM_increase_duty()PWM_decrease_duty()函数用于增大和减小PWM信号中低电平的占空比。
最后,在main函数中调用PWM_Enable()使能PWM信号输出。
7.3TTC定时器
PS定时器的驱动程序由ttc_intr.c 和ttc_intr.h 组成。 在本例程中,PS 定时器用于周期性产生中断来实现 GUI 界面的刷新,以固定的频率的调用 UG_Update()函数 实现 GUI 的动态特性。 首先,在 main 函数中调用 TTC_init ()函数对定时器进行初始化,其中断周期由以下宏定义决定,周期为 50ms, 即 GUI 的刷新频率为 20Hz,刷新频率越高,GUI 的动态变化越快,用户可以根据需求进行调整。
  1. <b><font color="#7f0055">#define</font></b> <font color="#000000">TICK_TIMER_FREQ_HZ</font> <font color="#000000">20  </font><font color="#3f7f5f">/* Tick timer counter's output frequency */</font>
  2. <div align="left"><b>#define</b> TICKS_PER_CHANGE_PERIOD TICK_TIMER_FREQ_HZ /* Tick signals per update */</div>
复制代码
在 main 函数中调用TTC_Setup_Intr_System ()函数使能定时器的中,将中断函数设置为TickHandler ()。 TickHandle()函数的过程如下:
1:关闭定时器中断,因为定时器中断函数的执行时间可能会超过定时器设置的中断周期,如 50ms。
2:若触摸屏中断标志位 btn_event 为 1,则对 GPIO 中断函数中读出的触摸信息进行判断,若读取寄存器知道有触点,并且通过判断btn_down知道之前的触电已经松开,则读出并计算出第一个触摸点的 x,y 坐标信息,通过 UG_TouchUpdate() 函数将该触摸坐标反馈至 GUI 界面,表示 GUI 上的该点被触摸。若第一个触摸点状态为抬起(put up), 则将无效坐标(-1,-1)通过 UG_TouchUpdate()反馈至 GUI 界面,表示 GUI 上的无任何点被触摸。目前μGUI 库的按键触摸功能只支持单点触摸,因此无法同时设计出两个按键同时被触摸的效果。
3:将变化后最新的 GUI 界面对应的图像通过 Xil_DCacheFlushRange()函数刷进 DDR 中,从而保证 VDMA 将最新的 GUI 界面读出,并显示在触摸屏上。
4:重新使能定时器中断。 最后,在 main 函数里调用 XTtcPs_Start ()函数启动定时器工作。
15f221dd72064843b3ff0dc4fc6b8c81.jpg
7.4AXI-VDMA初始化
GUI界面显示在PL部分由AXI VDMA,Video Timing Controller,AXI4-Sreamto Video Out三个IP协同完成。其中AXI VDMA需要PS进行设置,AXI VDMA完成PS端DDR3中GUI界面的读取,Video Timing Controller产生通过AXI4-Sreamto Video Out进行GUI界面显示的控制时序。
a88debb311154f77a574e7eec1886775.jpg
这里只设置1帧缓存,分辨率设置为1024*600
9dab1693461c4ec9902ef578e09346c8.jpg
这里设置让VDMA自动管理帧,设置circle模式,虽然这里只有一帧,由于人机交互界面要求不高,一帧缓存60帧刷新速度,不会感觉到卡顿撕裂。
以下是Video_Out_MM2Ssetup()函数完成VDMA的寄存器的初始化。
1c377efae5134b21a4d30a8e08742aff.jpg

7.5I2C读取GT9xx触摸信息
1:I2C器件地址设置
Gt9xx系列触控芯片需要通过Reset和INIT配合设置器件地址,我们这里设置0Xba/0xbb需要满足以下时序。
d12b15bc77544ce89a7481ebf3c8f5a1.jpg
以下是C代码部分完成对axi-gpio的控制完成触摸芯片的复位初始化,以设置器件地址为0Xba/0xbb,
0edb4bfae0b54a3e8e8d04df32565bd4.jpg
由于使用的ZYNQ/MPSOC的I2C控制器,所以控制器会对读写方向进行设置,所以给I2C用的器件地址应该左移1bit:
#define GT9XX_ADDR 0xba>>1
2:修改配置寄存器
当我们需要修改触摸液晶分辨率,就需要修改GT9XX的寄存器配置。GT915S_cfg_1024x600定义了186个寄存器配置。其中包括的校验和寄存器,和更新寄存器的值,但是很多时候,我们会自己用程序计算校验和寄存器。
e724f4da47f9467fab5469fcbd130b13.jpg
首先读取寄存器0x8047查看当前的配置版本是否是低于或者等于需要更新的设置版本,如果是则更新配置寄存器。
55369300efde4c72a09ef8a2f36bccd7.jpg
3:读取分辨率信息
为了验证我们写入的参数是否正确,我们这里读取从0x8140开始的10个寄存器,并且判断0x8147/0x8148/0x8149/0x8150这几个寄存器值。
24da8b811d78415a91e612808d3ac4bc.jpg
6f8e5ed5951d4a319e7bc99a26d641e0.jpg
4:读取触控坐标
之前刚刚分析的代码中,gpio-bit7定义到触摸芯片GT9XX的INT中断,因此我们定义了GPIO中断程序,在GPIO中断程序中对坐标进行读取。
5b28cecee6be4f088094bb14b5872e33.jpg
7.6GUI设计
GUI界面的驱动程序由gui_window.cgui_window.himage.hugui.cugui.h组成,其中ugui.cugui.h来自μGUI库。
在本例程中,一共设计了4个窗口,对应window1~window5。同时,也为每个窗口设计了对应的回调函数window_1_callback()~window_4_callback()
main函数中,调用gui_create()函数对GUI进行初始化,并创建所有的窗口及窗口所包含的所有对象。
gui_create()函数的流程如下:
1:调用UG_Init()函数初始化GUI
2:调用UG_FillScreen()函数设置所有GUI窗口的背景颜色
3:调用create_window1()~create_window4()函数依次创建4个窗口
4:调用UG_WindowShow()函数设置GUI显示的第一个窗口为window1
5:调用UG_WaitForUpdate()函数等待定时器中断到来,刷新并显示GUI界面
GUI界面设计以窗口为基础进行,每个界面对应一个窗口,但所有的窗口都对应同一片内存区域。μGUI库中有很多API函数,这里只介绍本例程所涉及的API函数,其余的API可参考μGUI使用手册。4个窗口的设计在create_window1()~create_window4()函数中完成
1:GUI初始化
通过UG_Init()函数设置GUI界面的长宽分辨率,并绑定用户自定义的像素值设置函数PixelSet()。通过UG_FillScreen()函数将所有GUI窗口的背景设置为浅灰色。
2:窗口1设计
窗口1为GUI的欢迎界面。如下图所示。
170bc831c66b4c8d928436dacc431d37.jpg
窗口1的设计在create_window1()函数中完成,流程如下:
1)窗口创建
1>调用UG_WindowCreate()函数绑定窗口1的回调函数为window_1_callback(),设置窗口可包含对象的最大个数为15。
2>调用UG_WindowSetTitleText()函数设置窗口1的标题内容。
3>调用UG_WindowSetTitleTextFont()函数设置标题字体的大小。
2)按键创建
1>调用UG_ButtonCreate()函数创建按键0,并设置按键0的坐标范围
2>调用UG_ButtonSetStyle()函数设置按键0的模式为3D,且按下后背景和字体的颜色会跳变
3>调用UG_ButtonSetAlternateForeColor函数设置按键0按下后改变的字体颜色
4>调用UG_ButtonSetAlternateBackColor函数设置按键0按下后改变的按键背景颜色
5>调用UG_ButtonSetFont函数设置按键0的字体大小
6>调用UG_ButtonSetText函数设置按键0显示的内容
3)文本框创建
文本框0
1>调用UG_TextboxCreate ()函数创建文本框0,并设置文本框0的坐标范围。
2>调用UG_TextboxSetFont()函数设置文本框0内容的字体大小。
3>调用UG_TextboxSetText()函数设置文本框0的内容。
4>调用UG_TextboxSetAlignment()函数设置文本框0中的内容在文本框中的对齐方式。
文本框1
1>调用UG_TextboxSetForeColor()函数设置文本框1内容的字体颜色
2>调用UG_TextboxSetHSpace()函数设置本文框1中每个字符之间的横向距离
3>其余函数调用与文本框0同理
4)图片创建
目前,最新版的μGUI v0.3仅支持在GUI界面中添加RGB565格式的BMP图像。由于本设计中所需添加的两个米联logo均为RGB888格式,为了让μGUI能支持24位RGB888的图片,需要对ugui.c文件中的UG_DrawBMP()函数进行修改,如下所示。其中通过阴影部分为添加的代码
  1. void UG_DrawBMP( UG_S16 xp, UG_S16 yp, UG_BMP* bmp )
  2. {
  3.    UG_S16 x,y,xs;
  4.    UG_U8 r,g,b;
  5.    UG_U16* p;
  6.    /*add by osrc 2017.2.18*/
  7.    UG_U32* p1;
  8.    UG_U16 tmp;
  9.    /*add by osrc 2017.2.18*/
  10.    UG_U32 tmp1;
  11.    UG_COLOR c;
  12.    if ( bmp->p == NULL ) return;
  13.    /* Only support 16 BPP so far */
  14.    if ( bmp->bpp == BMP_BPP_16 )
  15.    {
  16.       p = (UG_U16*)bmp->p;
  17.    }
  18.    /*add support for 32 BPP, by osrc 2017.2.18*/
  19.    else if(bmp->bpp == BMP_BPP_32)
  20.    {
  21.           p1 = (UG_U32*)bmp->p;
  22.    }
  23.    else
  24.    {
  25.       return;
  26.    }
  27.    xs = xp;
  28.    for(y=0;y<bmp->height;y++)
  29.    {
  30.       xp = xs;
  31.       for(x=0;x<bmp->width;x++)
  32.       {     
  33.                  /*add support for RGB888, by osrc 2017.2.18*/
  34.                  if(bmp->colors == BMP_RGB565)
  35.                  {
  36.                         tmp = *p++;
  37.          /* Convert RGB565 to RGB888 */
  38.             r = (tmp>>11)&0x1F;
  39.             r<<=3;
  40.             g = (tmp>>5)&0x3F;
  41.             g<<=2;
  42.             b = (tmp)&0x1F;
  43.             b<<=3;
  44.             c = ((UG_COLOR)r<<16) | ((UG_COLOR)g<<8) | (UG_COLOR)b;
  45.                  }
  46.                  else
  47.                  {
  48.                         tmp1 = *p1++;
  49.                         c = tmp1;
  50.                  }
  51.          UG_DrawPixel( xp++ , yp , c );
  52.       }
  53.       yp++;
  54.    }
  55. }
复制代码

图片0
添加的图片0如下所示,为米联的logo。图片各像素点的值包含在image.h的logo1_bmp数组中。
638a631a0ef74e2c8123ab9bcb92e8b4.jpg
定义该图片在GUI中的结构体,如下所示。图片的指针logo1_bmp,为图片的长、宽均为65个像素,每个像素点占32bit,图片格式为RGB888。
  1. const UG_BMP logo1 =
  2. {
  3.    (void*)logo1_bmp,
  4.    65,
  5.    65,
  6.    BMP_BPP_32,
  7.    BMP_RGB888
  8. };
复制代码

1>调用UG_ImageCreate函数在窗口1中创建该图片对象,并设置图片的坐标位置。
2>调用UG_ImageSetBMP函数将上述的logo图像与创建的图片对象绑定。
图片1
7e1fb736a3a24ca489219914ad59312d.jpg
定义该图片在GUI中的结构体,如下所示。图片的指针logo2_bmp,为图片的长位259像素,宽为105个像素,每个像素点占32bit,图片格式为RGB888。
  1. const UG_BMP logo2 =
  2. {
  3.    (void*)logo2_bmp,
  4.    259,
  5.    105,
  6.    BMP_BPP_32,
  7.    BMP_RGB888
  8. };
复制代码

图片对象创建添加方式与图片0相同。
5)回调函数
1个窗口的回调函数当该窗口中的按键的触摸状态发生改变时,会在UG_Update()函数中被调用。在窗口1的回调函数window_1_callback()中设置了按键0的功能,当在触摸屏中按下start application now按键便会使window_1_callback()被调用,通过UG_WindowShow函数,让GUI界面刷新后切换到窗口2。在按下按键的同时,可以观察到按键0字体和背景颜色产生的变化。
2:窗口2设计
窗口2为LED灯控制界面,如下图所示。在该窗口中,用户可以通过触摸LED1~LED5按键来控制开发板上的标识为LD1~LD5的LED灯。
42e4e24858e544ba8c7891c1c00b5fd4.jpg
窗口2的设计在create_window2()函数中完成,窗口2中的标题、按键、文本框的创建流程与create_window1()原理相同,详细可参阅工程代码,此处不作赘述。
1)回调函数
窗口2的回调函数window_2_callback()中设置了7个按键0~6的功能,其中LED1~LED5对应5个LED灯的控制功能,通过控制GPIO[3]~GPIO[7]的输出来实现LED灯的开关。当LED灯亮时,对应按键的背景颜色为红色,当LED熄灭时,对应按键的背景颜色还原为绿色。(注意部分开发板只有LED1和LED2可以使用)另外,按下Previous Page按键让GUI界面切换至窗口1,按下Next Page按键让GUI界面切换至窗口3。在按下按键的同时,可以观察到各按键字体和背景颜色产生的变化。
3:窗口3设计
窗口3为液晶屏亮度调节界面,如下图所示。在该窗口中,用户可以通过触摸加、减按键来调节液晶屏背光源的亮度。
433916cdecea4351adeb32a52613bf20.jpg
窗口3的设计在create_window3()函数中完成,窗口3中的标题、按键、文本框的创建流程与create_window1()原理相同,详细可参阅工程代码,此处不作赘述。
1)回调函数
窗口3的回调函数window_3_callback()中设置了4个按键0~3的功能,其中“-”按键控制PWM信号低电平的占空比减小,从而降低液晶屏亮度;“+”按键控制PWM信号低电平的占空比增大,从而提高液晶屏亮度。另外,按下“Previous Page”按键让GUI界面切换至窗口2,按下“Next Page”按键让GUI界面切换至窗口4。在按下按键的同时,可以观察到各按键字体和背景颜色产生的变化。
4:窗口4设计
窗口4为退出界面,如下图所示。在该窗口中,用户可以通过触摸“YES”、“NO”按键来选择是否退出回到窗口1的欢迎界面。由于窗口5的尺寸小于其他窗口,因此当GUI界面从窗口4切换到窗口5时,可以出两个窗口叠加的效果,并且当窗口5出现时,窗口4的标题栏会由蓝变灰。
69fd2d22442648019cfd9e9c125b22bf.jpg
窗口5的设计在create_window4()函数中完成,由于窗口5的尺寸小于1024×600,因此需要调用UG_WindowResize()函数重新设置窗口4的尺寸大小及位置。窗口4中的标题、按键、文本框的创建流程与create_window1()原理相同,详细可参阅工程代码,此处不作赘述。
1)回调函数
窗口4的回调函数window_4_callback()中设置了2个按键0~1的功能,按下“YES”按键让GUI界面切换至窗口1,按下“NO”按键让GUI界面切换至窗口4。在按下按键的同时,可以观察到各按键字体和背景颜色产生的变化。
6.7 UGUI 演示程序绘制过程分析
ugui 源码的注释很少,但是作为程序开发者需要有基本的学习分析代码能力。如果这点能力都没有,很难成为优秀的程序员。ugui 图形界面创建过程对学习编程者来说具有非常好的学习参考价值,阅读这种优秀的代码也让作为开发者的本人非常兴奋。
1:初始化包括,触控液晶屏、GPIO、定时器、中断这个环节本文实验中,关键是理解创建 ugui 窗口的环节,包括窗口、控件、TextBox、Image 的创建过程。窗口、控件、TextBox、Image 都是窗口类别中不同 type(类别)。每个 type 都有自己的 ID,自己的状态等。
1:UG_GUI 结构体定义如下
  1. typedef struct
  2. {
  3. void (*pset)(UG_S16,UG_S16,UG_COLOR); /* 指向像素绘制函数,这个函数定义在*gui_window.c中*/
  4. UG_S16 x_dim; /*界面的X方向尺寸*/
  5. UG_S16 y_dim; /*界面的Y方向尺寸*/
  6. UG_TOUCH touch; /*触控对象*/
  7. UG_WINDOW* next_window; /*下一个窗口*/
  8. UG_WINDOW* active_window; /*活动的窗口*/
  9. UG_WINDOW* last_window; /*上一个窗口*/
  10. struct
  11. {
  12. UG_S16 x_pos; /* X位置*/
  13. UG_S16 y_pos; /* y位置*/
  14. UG_S16 x_start; /* X起始*/
  15. UG_S16 y_start; /* Y起始*/
  16. UG_S16 x_end; /* X结束*/
  17. UG_S16 y_end; /* Y结束*/
  18. UG_COLOR fore_color; /* 前色*/
  19. UG_COLOR back_color; /* 背景色*/
  20. } console; /*控制台参数*/
  21. struct
  22. {
  23. unsigned char* p;
  24. UG_S16 char_width; /* */
  25. UG_S16 char_height; /* */
  26. UG_S8 char_h_space; /* */
  27. UG_S8 char_v_space; /* */
  28. } font; /* 字体*/
  29. UG_COLOR fore_color; /* 前色*/
  30. UG_COLOR back_color; /* 背景色*/
  31. UG_COLOR desktop_color; /* 桌面颜色*/
  32. UG_U8 state; /*状态*/
  33. UG_DRIVER driver[NUMBER_OF_DRIVERS];
  34. } UG_GUI;
复制代码

2:UG_WINDOW 结构体用于定义窗体相关的参数
  1. typedef struct S_WINDOW UG_WINDOW;
  2. struct S_WINDOW
  3. {
  4. UG_U8 objcnt; /*窗体可以支持的包括控件、文本等对象的最大数量*/
  5. UG_OBJECT* objlst; /*包括控件、文本等对象的列表,这个会在_UG_GetFreeObject 函数中自动分配*/
  6. UG_U8 state; /*窗体的状态*/
  7. UG_COLOR fc; /*前景色(fc)*/
  8. UG_COLOR bc; /*窗体的背景色*/
  9. UG_S16 xs; /*窗体的窗体开始的起始坐标*/
  10. UG_S16 ys; /*窗体的窗体开始的起始坐标*/
  11. UG_S16 xe; /*窗体的结束坐标*/
  12. UG_S16 ye; /*窗体的结束坐标*/
  13. UG_U8 style;
  14. UG_TITLE title;
  15. void (*cb)( UG_MESSAGE* );
  16. };
复制代码

3:UG_OBJECT 用于定义窗体中的对象的相关参数
  1. typedef struct S_OBJECT UG_OBJECT;
  2. struct S_OBJECT
  3. {
  4. UG_U8 state; /* object state */
  5. UG_U8 touch_state; /* object touch state */
  6. void (*update) (UG_WINDOW*,UG_OBJECT*); /* pointer to object-specific update function */
  7. UG_AREA a_abs; /* absolute area of the object */
  8. UG_AREA a_rel; /* relative area of the object */
  9. UG_U8 type; /* object type */
  10. UG_U8 id; /* object ID */
  11. UG_U8 event; /* object-specific events */
  12. void* data; /* pointer to object-specific data */
  13. };
复制代码

4:UG_BUTTON 用于定义控件的对象
  1. /* -------------------------------------------------------------------------------- */
  2. /* -- BUTTON OBJECT -- */
  3. /* -------------------------------------------------------------------------------- */
  4. /* Button structure */
  5. typedef struct
  6. {
  7. UG_U8 state;
  8. UG_U8 style;
  9. UG_COLOR fc;
  10. UG_COLOR bc;
  11. UG_COLOR afc;
  12. UG_COLOR abc;
  13. const UG_FONT* font;
  14. char* str;
  15. }UG_BUTTON;
复制代码

5:UG_MESSAGE 消息结构体
该函数中的回调函数参数为一个结构体指针,UG_MESSAGE
  1. /* Message structure */
  2. /* Message types */
  3. #define MSG_TYPE_NONE 0
  4. #define MSG_TYPE_WINDOW 1
  5. #define MSG_TYPE_OBJECT 2
  6. typedef struct
  7. {
  8. UG_U8 type; /* 消息类型 */
  9. UG_U8 id; /* 消息ID */
  10. UG_U8 sub_id; /* 子ID */
  11. UG_U8 event; /* 事件 */
  12. void* src; /* void指针*/
  13. } UG_MESSAGE;
复制代码

6:UG_TITLE 标题结构体
下面再分析下 BOTTON 控件对象的创建,至于 TEXT 文本,BMP 图片对象的创建过程读者可以根据这里的分析方
法自行分析代码。
先看下 UG_TITLE 的结构体
  1. /* Title structure */
  2. typedef struct
  3. {
  4. char* str;
  5. const UG_FONT* font;
  6. UG_S8 h_space;
  7. UG_S8 v_space;
  8. UG_U8 align;
  9. UG_COLOR fc;
  10. UG_COLOR bc;
  11. UG_COLOR ifc;
  12. UG_COLOR ibc;
  13. UG_U8 height;
  14. } UG_TITLE;
复制代码

7:其他控件对象请自行阅读源码
其他控件分析请阅读 ugui.c ugui.h 源码这里,仅仅以分析 BOTTON 控件为例。
8:UG_Init 用于初始化 GUI 界面参数
  1. UG_S16 UG_Init( UG_GUI* g, void (*p)(UG_S16,UG_S16,UG_COLOR), UG_S16 x, UG_S16 y )
  2. {
  3. UG_U8 i;
  4. g->pset = (void(*)(UG_S16,UG_S16,UG_COLOR))p;
  5. g->x_dim = x;
  6. g->y_dim = y;
  7. g->console.x_start = 4;
  8. g->console.y_start = 4;
  9. g->console.x_end = g->x_dim - g->console.x_start-1;
  10. g->console.y_end = g->y_dim - g->console.x_start-1;
  11. g->console.x_pos = g->console.x_end;
  12. g->console.y_pos = g->console.y_end;
  13. g->font.char_h_space = 1;
  14. g->font.char_v_space = 1;
  15. g->font.p = NULL;
  16. g->desktop_color = 0x5E8BEf;
  17. g->fore_color = C_WHITE;
  18. g->back_color = C_BLACK;
  19. g->next_window = NULL;
  20. g->active_window = NULL;
  21. g->last_window = NULL;
  22. /* Clear drivers */
  23. for(i=0;i<NUMBER_OF_DRIVERS;i++)
  24. {
  25. g->driver[i].driver = NULL;
  26. g->driver[i].state = 0;
  27. }
  28. gui = g;
  29. return 1;
  30. }
复制代码

UG_Init(GuiPtr, PixelSet, GUI_WIDTH, GUI_HEIGHT);用于初始化 GUI 界面参数
9:UG_FillScreen ( C_GAINSBORO )绘制 GUI
之后通过 UG_FillScreen ( C_GAINSBORO );函数绘制 gui
  1. void UG_FillScreen( UG_COLOR c )
  2. {
  3. UG_FillFrame(0,0,gui->x_dim-1,gui->y_dim-1,c);
  4. }
  5. void UG_FillFrame( UG_S16 x1, UG_S16 y1, UG_S16 x2, UG_S16 y2, UG_COLOR c )
  6. {
  7. UG_S16 n,m;
  8. if ( x2 < x1 )
  9. {
  10. n = x2;
  11. x2 = x1;
  12. x1 = n;
  13. }
  14. if ( y2 < y1 )
  15. {
  16. n = y2;
  17. y2 = y1;
  18. y1 = n;
  19. }
  20. /* Is hardware acceleration available? */
  21. if ( gui->driver[DRIVER_FILL_FRAME].state & DRIVER_ENABLED )
  22. {
  23. if( ((UG_RESULT(*)(UG_S16 x1, UG_S16 y1, UG_S16 x2, UG_S16 y2, UG_COLOR
  24. c))gui->driver[DRIVER_FILL_FRAME].driver)(x1,y1,x2,y2,c) == UG_RESULT_OK ) return;
  25. }
  26. for( m=y1; m<=y2; m++ )
  27. {
  28. for( n=x1; n<=x2; n++ )
  29. {
  30. gui->pset(n,m,c);
  31. }
  32. }
  33. }
复制代码

gui->pset是一个指针函数,指向了 gui_window.c中的 PixelSetPixelSet函数绘制点在内存中,这个内存也是显存,
后面的所有绘制图形最终都要绘制到这个缓存。所以当 UG_FillScreen 执行后,输出界面就能看到界面的背景色了。
10:PixelSet 绘制内存函数
  1. void PixelSet(UG_S16 x, UG_S16 y, UG_COLOR c)
  2. {
  3. u32 iPixelAddr;
  4. iPixelAddr = y * GUI_WIDTH + x;
  5. BufferPtr[0][iPixelAddr] = c;
  6. }
复制代码

11:_UG_GetFreeObject( UG_WINDOW* wnd )函数分配资源
一 个 窗 体 会 有 多 种 类 型 的 多 种 对 象 , 每 次 创 建 对 象 都 会 调 用 UG_OBJECT*
_UG_GetFreeObject( UG_WINDOW* wnd )来自动分配序号,其中 bj->state 的初始状态由 UG_WindowCreate 函数中
设置为 OBJ_STATE_INIT,当某个对象被初始化为初始状态,说明该对象可以被使用和创建,否则代表了该对象已
经被创建,则返回 NULL
  1. /* Object states */
  2. #define OBJ_STATE_FREE (1<<0)
  3. #define OBJ_STATE_VALID (1<<1)
  4. #define OBJ_STATE_BUSY (1<<2)
  5. #define OBJ_STATE_VISIBLE (1<<3)
  6. #define OBJ_STATE_ENABLE (1<<4)
  7. #define OBJ_STATE_UPDATE (1<<5)
  8. #define OBJ_STATE_REDRAW (1<<6)
  9. #define OBJ_STATE_TOUCH_ENABLE (1<<7)
  10. #define OBJ_STATE_INIT (OBJ_STATE_FREE | OBJ_STATE_VALID)
  11. UG_OBJECT* _UG_GetFreeObject( UG_WINDOW* wnd )
  12. {
  13. UG_U8 i;
  14. UG_OBJECT* obj=(UG_OBJECT*)wnd->objlst;
  15. for(i=0;i<wnd->objcnt;i++)
  16. {
  17. obj = (UG_OBJECT*)(&wnd->objlst[i]);
  18. if ( (obj->state & OBJ_STATE_FREE) && (obj->state & OBJ_STATE_VALID) )
  19. {
  20. /* Free object found! */
  21. return obj;
  22. }
  23. }
  24. return NULL;
  25. }<i><i><i> <img width="15" _height="15" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" border="0" alt=""></i></i></i>
复制代码
12:create_window1 函数分析
create_window1();create_window2();create_window3();create_window4();用于创建4个界面窗口,创建的过程都是类似,以create_window1()来说。
  1. void create_window1(void)
  2. {
  3. /* Create Window 1 */
  4. UG_WindowCreate( &window_1, obj_buff_wnd_1, MAX_OBJECTS, window_1_callback );
  5. UG_WindowSetTitleText( &window_1, "礕UI For Milianke Board" );
  6. UG_WindowSetTitleTextFont( &window_1, &FONT_12X20 );
  7. /* Create Button 1 */
  8. UG_ButtonCreate( &window_1, &button1_1, BTN_ID_0, 100,
  9. UG_WindowGetInnerHeight(&window_1)-100,
  10. UG_WindowGetInnerWidth( &window_1 )-100,
  11. UG_WindowGetInnerHeight(&window_1)-50 );
  12. UG_ButtonSetStyle( &window_1 , BTN_ID_0 , BTN_STYLE_3D | BTN_STYLE_USE_ALTERNATE_COLORS);
  13. UG_ButtonSetAlternateForeColor(&window_1 , BTN_ID_0 , C_RED);
  14. UG_ButtonSetAlternateBackColor(&window_1 , BTN_ID_0 , C_BLUE);
  15. UG_ButtonSetFont( &window_1, BTN_ID_0, &FONT_22X36 );
  16. UG_ButtonSetText( &window_1, BTN_ID_0, "Start Application Now" );
  17. /* Create Textbox 1 */
  18. UG_TextboxCreate( &window_1, &textbox1_1, TXB_ID_0, 10, 10, UG_WindowGetInnerWidth( &window_1 )-10, 40 );
  19. UG_TextboxSetFont( &window_1, TXB_ID_0, &FONT_12X20 );
  20. UG_TextboxSetText( &window_1, TXB_ID_0, "Welcome to Milianke 礕UI v1.0" );
  21. UG_TextboxSetAlignment( &window_1, TXB_ID_0, ALIGN_TOP_CENTER );
  22. /* Create Textbox 2 */
  23. UG_TextboxCreate( &window_1, &textbox1_2, TXB_ID_1, 10, 125, UG_WindowGetInnerWidth( &window_1 )-10, 155 );
  24. UG_TextboxSetFont( &window_1, TXB_ID_1, &FONT_16X26 );
  25. UG_TextboxSetText( &window_1, TXB_ID_1, "www.uisrc.com" );
  26. UG_TextboxSetAlignment( &window_1, TXB_ID_1, ALIGN_BOTTOM_CENTER );
  27. UG_TextboxSetForeColor( &window_1, TXB_ID_1, C_BLUE );
  28. UG_TextboxSetHSpace( &window_1, TXB_ID_1, 1 );
  29. /* Create Image 1 */
  30. UG_ImageCreate( &window_1, &image1_1, IMG_ID_0, (UG_WindowGetInnerWidth( &window_1 )>>1) - (logo1.width>>1), 50, 0, 0 );
  31. UG_ImageSetBMP( &window_1, IMG_ID_0, &logo1 );
  32. /* Create Image 2 */
  33. UG_ImageCreate( &window_1, &image1_2, IMG_ID_1, (UG_WindowGetInnerWidth( &window_1 )>>1) - (logo2.width>>1), 180, 0,
  34. 0 );
  35. UG_ImageSetBMP( &window_1, IMG_ID_1, &logo2 );
  36. }
复制代码

13:UG_WindowCreate 窗口创建
UG_WindowCreate( &window_1, obj_buff_wnd_1, MAX_OBJECTS, window_1_callback );函数主要完成窗口结构体参数的设置,这里不绘制窗口,窗口
的绘制在后面完成
  1. /* -------------------------------------------------------------------------------- */
  2. /* -- WINDOW FUNCTIONS -- */
  3. /* -------------------------------------------------------------------------------- */
  4. UG_RESULT UG_WindowCreate( UG_WINDOW* wnd, UG_OBJECT* objlst, UG_U8 objcnt, void (*cb)( UG_MESSAGE* ) )
  5. {
  6. UG_U8 i;
  7. UG_OBJECT* obj=NULL;
  8. if ( (wnd == NULL) || (objlst == NULL) || (objcnt == 0) ) return UG_RESULT_FAIL;
  9. /* Initialize all objects of the window */
  10. for(i=0; i<objcnt; i++)
  11. {
  12. obj = (UG_OBJECT*)&objlst[i];
  13. obj->state = OBJ_STATE_INIT; /* #define OBJ_STATE_INIT (OBJ_STATE_FREE |OBJ_STATE_VALID)*/
  14. obj->data = NULL;
  15. }
  16. /* Initialize window */
  17. wnd->objcnt = objcnt;
  18. wnd->objlst = objlst;
  19. wnd->state = WND_STATE_VALID;
  20. wnd->fc = 0x000000;
  21. wnd->bc = 0xF0F0F0;
  22. wnd->xs = 0;
  23. wnd->ys = 0;
  24. wnd->xe = UG_GetXDim()-1;
  25. wnd->ye = UG_GetYDim()-1;
  26. wnd->cb = cb; /* 设置消息的回调函数指针,回调函数等后面消息回调的时候介绍 */
  27. wnd->style = WND_STYLE_3D | WND_STYLE_SHOW_TITLE;
  28. /* Initialize window title-bar */
  29. wnd->title.str = NULL;
  30. wnd->title.font = NULL;
  31. wnd->title.h_space = 2;
  32. wnd->title.v_space = 2;
  33. wnd->title.align = ALIGN_CENTER_LEFT;
  34. wnd->title.fc = C_WHITE;
  35. wnd->title.bc = C_BLUE;
  36. wnd->title.ifc = C_WHITE;
  37. wnd->title.ibc = C_GRAY;
  38. wnd->title.height = 15;
  39. return UG_RESULT_OK;
  40. }
复制代码

14:UG_WindowSetTitleText 设置窗体标题字符
UG_WindowSetTitleText( &window_1, "ugUI For Milianke Board" );用于设置窗体的Title显示字符
  1. UG_RESULT UG_WindowSetTitleText( UG_WINDOW* wnd, char* str )
  2. {
  3. if ( (wnd != NULL) && (wnd->state & WND_STATE_VALID) )
  4. {
  5. wnd->title.str = str;
  6. wnd->state |= WND_STATE_UPDATE | WND_STATE_REDRAW_TITLE;
  7. return UG_RESULT_OK;
  8. }
  9. return UG_RESULT_FAIL;
  10. }
复制代码

15:UG_WindowSetTitleTextFont 设置窗体标题字体
UG_WindowSetTitleTextFont( &window_1, &FONT_12X20 );该函数用于设置窗口 title 的字符大小
  1. UG_RESULT UG_WindowSetTitleTextFont( UG_WINDOW* wnd, const UG_FONT* font )
  2. {
  3. if ( (wnd != NULL) && (wnd->state & WND_STATE_VALID) )
  4. {
  5. wnd->state |= WND_STATE_UPDATE | WND_STATE_REDRAW_TITLE;
  6. wnd->title.font = font;
  7. if ( wnd->title.height <= (font->char_height + 1) )
  8. {
  9. wnd->title.height = font->char_height + 2;
  10. wnd->state &= ~WND_STATE_REDRAW_TITLE;
  11. }
  12. return UG_RESULT_OK;
  13. }
  14. return UG_RESULT_FAIL;
  15. }
复制代码

FONT_12X20 的定义如下:
  1. 在 ugui.h 文件中
  2. #define USE_FONT_12X20
  3. #ifdef USE_FONT_12X20
  4. extern const UG_FONT FONT_12X20;
  5. #endif
  6. 在 ugui.c 文件中
  7. #ifdef USE_FONT_12X20
  8. const UG_FONT FONT_12X20 = {(unsigned char*)font_12x20,12,20};
  9. #endif
复制代码

16:UG_ButtonCreate 控件-Button 创建
创建控件函数主要对控件的结构体参数设置,本文这里仅仅以创建 Button 为例,创建后也不是立马完成绘制
UG_ButtonCreate( &window_1, &button1_1, BTN_ID_0, 100,
UG_WindowGetInnerHeight(&window_1)-100,
UG_WindowGetInnerWidth( &window_1 )-100,
UG_WindowGetInnerHeight(&window_1)-50 );
  1. /* -------------------------------------------------------------------------------- */
  2. UG_RESULT UG_ButtonCreate( UG_WINDOW* wnd, UG_BUTTON* btn, UG_U8 id, UG_S16 xs, UG_S16 ys, UG_S16 xe, UG_S16 ye )
  3. {
  4. UG_OBJECT* obj;
  5. obj = _UG_GetFreeObject( wnd );
  6. if ( obj == NULL ) return UG_RESULT_FAIL;常州一二三/常州米联客电子科技有限公司 米联客(MILIANKE)www.uisrc.com www.milianke.com ZYNQ SOC SDK Advanced Course (2024)
  7. 364
  8. /* Initialize object-specific parameters */
  9. btn->state = BTN_STATE_RELEASED;
  10. btn->bc = wnd->bc; /* 按键正常显示的前景颜色 */
  11. btn->fc = wnd->fc; /* 按键正常显示的背景颜色 */
  12. btn->abc = wnd->bc; /*按键按下显示的前景颜色 */
  13. btn->afc = wnd->fc; /*按键按下显示的背景颜色 */
  14. btn->style = BTN_STYLE_3D;
  15. btn->font = NULL;
  16. btn->str = "-";
  17. /* Initialize standard object parameters */
  18. obj->update = _UG_ButtonUpdate; /*这是一个指针函数,用于更新控件 */
  19. obj->touch_state = OBJ_TOUCH_STATE_INIT;
  20. obj->type = OBJ_TYPE_BUTTON;
  21. obj->event = OBJ_EVENT_NONE;
  22. obj->a_rel.xs = xs;
  23. obj->a_rel.ys = ys;
  24. obj->a_rel.xe = xe;
  25. obj->a_rel.ye = ye;
  26. obj->a_abs.xs = -1;
  27. obj->a_abs.ys = -1;
  28. obj->a_abs.xe = -1;
  29. obj->a_abs.ye = -1;
  30. obj->id = id;
  31. obj->state |= OBJ_STATE_VISIBLE | OBJ_STATE_REDRAW | OBJ_STATE_VALID | OBJ_STATE_TOUCH_ENABLE;
  32. obj->data = (void*)btn;
  33. /* Update function: Do your thing! */
  34. obj->state &= ~OBJ_STATE_FREE;
  35. return UG_RESULT_OK;
  36. }
复制代码


以上函数中 obj = _UG_GetFreeObject( wnd );函数用于设置未被使用的 object 列表参数
  1. UG_OBJECT* _UG_GetFreeObject( UG_WINDOW* wnd )
  2. {
  3. UG_U8 i;
  4. UG_OBJECT* obj=(UG_OBJECT*)wnd->objlst;
  5. for(i=0;i<wnd->objcnt;i++)
  6. {
  7. obj = (UG_OBJECT*)(&wnd->objlst[i]);
  8. if ( (obj->state & OBJ_STATE_FREE) && (obj->state & OBJ_STATE_VALID) )
  9. {
  10. /* Free object found! */
  11. return obj;
  12. }
  13. }
  14. return NULL;
  15. }
复制代码

17:UG_ButtonSetStyle 函数设置控件的 Style
  1. UG_RESULT UG_ButtonSetStyle( UG_WINDOW* wnd, UG_U8 id, UG_U8 style )
  2. {
  3. UG_OBJECT* obj=NULL;
  4. UG_BUTTON* btn=NULL;
  5. obj = _UG_SearchObject( wnd, OBJ_TYPE_BUTTON, id );
  6. if ( obj == NULL ) return UG_RESULT_FAIL;
  7. btn = (UG_BUTTON*)(obj->data);
  8. /* Select color scheme */
  9. btn->style &= ~(BTN_STYLE_USE_ALTERNATE_COLORS | BTN_STYLE_TOGGLE_COLORS);
  10. btn->state |= BTN_STATE_ALWAYS_REDRAW;
  11. if ( style & BTN_STYLE_TOGGLE_COLORS )
  12. {
  13. btn->style |= BTN_STYLE_TOGGLE_COLORS;
  14. }
  15. else if ( style & BTN_STYLE_USE_ALTERNATE_COLORS )
  16. {
  17. btn->style |= BTN_STYLE_USE_ALTERNATE_COLORS;
  18. }
  19. else
  20. {
  21. btn->state &= ~BTN_STATE_ALWAYS_REDRAW;
  22. }
  23. /* 3D or 2D */
  24. if ( style & BTN_STYLE_3D )
  25. {
  26. btn->style |= BTN_STYLE_3D;
  27. }
  28. else
  29. {
  30. btn->style &= ~BTN_STYLE_3D;
  31. }
  32. obj->state |= OBJ_STATE_UPDATE | OBJ_STATE_REDRAW;
  33. return UG_RESULT_OK;
  34. }
复制代码

18:UG_SearchObject 判断控件是否有效
以上函数中 obj = _UG_SearchObject( wnd, OBJ_TYPE_BUTTON, id );用于判断当前对象是否有效
  1. UG_OBJECT* _UG_SearchObject( UG_WINDOW* wnd, UG_U8 type, UG_U8 id )
  2. {
  3. UG_U8 i;
  4. UG_OBJECT* obj=(UG_OBJECT*)wnd->objlst;
  5. for(i=0;i<wnd->objcnt;i++)
  6. {
  7. obj = (UG_OBJECT*)(&wnd->objlst[i]);
  8. if ( !(obj->state & OBJ_STATE_FREE) && (obj->state & OBJ_STATE_VALID) )
  9. {
  10. if ( (obj->type == type) && (obj->id == id) )
  11. {
  12. /* Requested object found! */
  13. return obj;
  14. }
  15. }
  16. }
  17. return NULL;
  18. }
复制代码

19:其他相关 Button 函数不详细介绍
UG_ButtonSetAlternateForeColor(&window_1 , BTN_ID_0 , C_RED);设置当触控按下前景颜色(字体颜色)
UG_ButtonSetAlternateBackColor(&window_1 , BTN_ID_0 , C_BLUE); 设置当触控按下背景颜色(背景颜色)
UG_ButtonSetFont( &window_1, BTN_ID_0, &FONT_22X36 );设置字体
UG_ButtonSetText( &window_1, BTN_ID_0, "Start Application Now" );设置文本
20:UG_WindowShow 设置需要显示的窗体
以上函数执行完成后并不会立马完成窗体的绘制,而是先调用UG_WindowShow( &window_1 )函数,该函数设置窗体的
状态,以及设置 ui->next_window为当前窗体。但是这个函数也是设置窗体的相关结构体参数,为显示做好准备,如
下设置两个关键参数WND_STATE_VISIBLE | WND_STATE_UPDATE;
  1. UG_RESULT UG_WindowShow( UG_WINDOW* wnd )
  2. {
  3. if ( wnd != NULL )
  4. {
  5. /* Force an update, even if this is the active window! */
  6. wnd->state |= WND_STATE_VISIBLE | WND_STATE_UPDATE;
  7. wnd->state &= ~WND_STATE_REDRAW_TITLE;
  8. gui->next_window = wnd;
  9. return UG_RESULT_OK;
  10. }
  11. return UG_RESULT_FAIL;
  12. }
复制代码

21:UG_WaitForUpdate 等待窗体更新
调用UG_WaitForUpdate()函数等待窗体更新,这个函数是等待窗口更新,也不具体实现窗口的绘制。
  1. void UG_WaitForUpdate( void )
  2. {
  3. gui->state |= UG_SATUS_WAIT_FOR_UPDATE;
  4. while ( (volatile UG_U8)gui->state & UG_SATUS_WAIT_FOR_UPDATE ){};
  5. }
复制代码

22:那么问题来了,什么时候绘制窗口呢?
每间隔 250ms,定时器中断一次,在定时器中断中,读取触控芯片的寄存器,判断是否有触控按下。更新按键状态;同时,会更新窗口状态。所以关键是 timer_callback 函数中断中的三个函数:
UG_TouchUpdate(*px_int[0], *py_int[0], TOUCH_STATE_PRESSED);该函数用于更新按键状态为按下状态,并且设置触控的坐标,后面的识别对应控件是否按下就是通过识别触控的坐标范围是否在绘制的触控按键坐标范围实现。
23:UG_TouchUpdate 函数用于更新触控状态
UG_TouchUpdate(-1, -1, TOUCH_STATE_RELEASED); 该函数坐标设置-1 的时候代表释放,因为显示屏幕中没有负坐标
  1. void UG_TouchUpdate( UG_S16 xp, UG_S16 yp, UG_U8 state )
  2. {
  3. gui->touch.xp = xp;
  4. gui->touch.yp = yp;
  5. gui->touch.state = state;
  6. }
复制代码

24:UG_Update()函数用于更新窗口状态
UG_Update()函数用于更新窗口状态,首先如果 gui 处于等待更新(在前面的 UG_WaitForUpdate 函数中等待更新完),则设置 gui->state &= ~UG_SATUS_WAIT_FOR_UPDATE ,表示已经更新。
  1. void UG_Update( void )
  2. {
  3. UG_WINDOW* wnd;
  4. /* Is somebody waiting for this update? */
  5. if ( gui->state & UG_SATUS_WAIT_FOR_UPDATE ) gui->state &= ~UG_SATUS_WAIT_FOR_UPDATE;
  6. /* Keep track of the windows */
  7. if ( gui->next_window != gui->active_window ) /*当窗口切换的时候,记录上一个窗口,以及更新新的活动窗口 */
  8. {
  9. if ( gui->next_window != NULL )
  10. {
  11. gui->last_window = gui->active_window; /* 记录上一个窗口 */
  12. gui->active_window = gui->next_window; /* 记录上当前窗口*/
  13. /* Do we need to draw an inactive title? 是否绘制未激活的上一个串口的TITLE*/
  14. if ( (gui->last_window->style & WND_STYLE_SHOW_TITLE) && (gui->last_window->state & WND_STATE_VISIBLE) )
  15. {
  16. /* Do both windows differ in size */
  17. if ( (gui->last_window->xs != gui->active_window->xs) || (gui->last_window->xe != gui->active_window->xe) ||
  18. (gui->last_window->ys != gui->active_window->ys) || (gui->last_window->ye != gui->active_window->ye) )
  19. {
  20. /* Redraw title of the last window */
  21. _UG_WindowDrawTitle( gui->last_window );
  22. }
  23. }
  24. gui->active_window->state &= ~WND_STATE_REDRAW_TITLE;
  25. gui->active_window->state |= WND_STATE_UPDATE | WND_STATE_VISIBLE; /* 需要更新显示 */
  26. }
  27. }
  28. /* Is there an active window */
  29. if ( gui->active_window != NULL )
  30. {
  31. wnd = gui->active_window;
  32. /* Does the window need to be updated? ,更新*/
  33. if ( wnd->state & WND_STATE_UPDATE )
  34. {
  35. /* Do it! */
  36. _UG_WindowUpdate( wnd );
  37. }
  38. /* Is the window visible? 显示*/
  39. if ( wnd->state & WND_STATE_VISIBLE )
  40. {
  41. _UG_ProcessTouchData( wnd );//update touch check state
  42. _UG_UpdateObjects( wnd );
  43. _UG_HandleEvents( wnd );
  44. }
  45. }
  46. }
复制代码

25:_UG_WindowUpdate( wnd )更新窗口绘制
更新窗口的同时会设置,更新窗口控件对象的状态。
  1. void _UG_WindowUpdate( UG_WINDOW* wnd )
  2. {
  3. UG_U16 i,objcnt;
  4. UG_OBJECT* obj;
  5. UG_S16 xs,ys,xe,ye;
  6. xs = wnd->xs;
  7. ys = wnd->ys;
  8. xe = wnd->xe;
  9. ye = wnd->ye;
  10. wnd->state &= ~WND_STATE_UPDATE;
  11. /* Is the window visible? 如果窗口需要显示,则绘制窗口*/
  12. if ( wnd->state & WND_STATE_VISIBLE )
  13. {
  14. /* 3D style? */
  15. if ( (wnd->style & WND_STYLE_3D) && !(wnd->state & WND_STATE_REDRAW_TITLE) )
  16. {
  17. _UG_DrawObjectFrame(xs,ys,xe,ye,(UG_COLOR*)pal_window);
  18. xs+=3;
  19. ys+=3;
  20. xe-=3;
  21. ye-=3;
  22. }
  23. /* Show title bar? */
  24. if ( wnd->style & WND_STYLE_SHOW_TITLE )
  25. {
  26. _UG_WindowDrawTitle( wnd );
  27. ys += wnd->title.height+1;
  28. if ( wnd->state & WND_STATE_REDRAW_TITLE )
  29. {
  30. wnd->state &= ~WND_STATE_REDRAW_TITLE;
  31. return;
  32. }
  33. }
  34. /* Draw window area? */
  35. UG_FillFrame(xs,ys,xe,ye,wnd->bc);
  36. /* Force each object to be updated! 当新窗口更新后,更新窗口包含的object(对象)*/
  37. objcnt = wnd->objcnt;
  38. for(i=0; i<objcnt; i++)
  39. {
  40. obj = (UG_OBJECT*)&wnd->objlst[i];
  41. if ( !(obj->state & OBJ_STATE_FREE) && (obj->state & OBJ_STATE_VALID) && (obj->state & OBJ_STATE_VISIBLE) )
  42. obj->state |= (OBJ_STATE_UPDATE | OBJ_STATE_REDRAW);
  43. }
  44. }
  45. else
  46. {
  47. UG_FillFrame(wnd->xs,wnd->xs,wnd->xe,wnd->ye,gui->desktop_color);
  48. }
  49. }
复制代码

26:_UG_DrawObjectFrame 更新窗口绘制
一下绘制窗口的边框,使用 UG_DrawLine 函数
  1. void _UG_DrawObjectFrame( UG_S16 xs, UG_S16 ys, UG_S16 xe, UG_S16 ye, UG_COLOR* p )
  2. {
  3. // Frame 0
  4. UG_DrawLine(xs, ys , xe-1, ys , *p++);
  5. UG_DrawLine(xs, ys+1, xs , ye-1, *p++);
  6. UG_DrawLine(xs, ye , xe , ye , *p++);
  7. UG_DrawLine(xe, ys , xe , ye-1, *p++);
  8. // Frame 1
  9. UG_DrawLine(xs+1, ys+1, xe-2, ys+1, *p++);
  10. UG_DrawLine(xs+1, ys+2, xs+1, ye-2, *p++);
  11. UG_DrawLine(xs+1, ye-1, xe-1, ye-1, *p++);
  12. UG_DrawLine(xe-1, ys+1, xe-1, ye-2, *p++);
  13. // Frame 2
  14. UG_DrawLine(xs+2, ys+2, xe-3, ys+2, *p++);
  15. UG_DrawLine(xs+2, ys+3, xs+2, ye-3, *p++);
  16. UG_DrawLine(xs+2, ye-2, xe-2, ye-2, *p++);
  17. UG_DrawLine(xe-2, ys+2, xe-2, ye-3, *p);
  18. }
复制代码

27:UG_DrawLine 绘制线条
  1. void UG_DrawLine( UG_S16 x1, UG_S16 y1, UG_S16 x2, UG_S16 y2, UG_COLOR c )
  2. {
  3. UG_S16 n, dx, dy, sgndx, sgndy, dxabs, dyabs, x, y, drawx, drawy;
  4. if ( x2 < x1 )
  5. {
  6. n = x2;
  7. x2 = x1;
  8. x1 = n;
  9. }
  10. if ( y2 < y1 )
  11. {
  12. n = y2;
  13. y2 = y1;
  14. y1 = n;
  15. }
  16. /* Is hardware acceleration available? */
  17. if ( gui->driver[DRIVER_DRAW_LINE].state & DRIVER_ENABLED )
  18. {
  19. if( ((UG_RESULT(*)(UG_S16 x1, UG_S16 y1, UG_S16 x2, UG_S16 y2, UG_COLOR
  20. c))gui->driver[DRIVER_DRAW_LINE].driver)(x1,y1,x2,y2,c) == UG_RESULT_OK ) return;
  21. }
  22. dx = x2 - x1;
  23. dy = y2 - y1;
  24. dxabs = (dx>0)?dx:-dx;
  25. dyabs = (dy>0)?dy:-dy;
  26. sgndx = (dx>0)?1:-1;
  27. sgndy = (dy>0)?1:-1;
  28. x = dyabs >> 1;
  29. y = dxabs >> 1;
  30. drawx = x1;
  31. drawy = y1;
  32. gui->pset(drawx, drawy,c);
  33. if( dxabs >= dyabs )
  34. {
  35. for( n=0; n<dxabs; n++ )
  36. {
  37. y += dyabs;
  38. if( y >= dxabs )
  39. {
  40. y -= dxabs;
  41. drawy += sgndy;
  42. }
  43. drawx += sgndx;
  44. gui->pset(drawx, drawy,c);
  45. }
  46. }
  47. else
  48. {
  49. for( n=0; n<dyabs; n++ )
  50. {
  51. x += dxabs;
  52. if( x >= dyabs )
  53. {
  54. x -= dyabs;
  55. drawx += sgndx;
  56. }
  57. drawy += sgndy;
  58. gui->pset(drawx, drawy,c);
  59. }
  60. }
复制代码

UG_DrawLine 函数具体的实现过程如下,给出起始坐标,终点坐标即可完成绘制,到这里 gui->pset 才是完成了具体的绘制工作。
28:_UG_ProcessTouchData( wnd )用于更新触控的状态
  1. void _UG_ProcessTouchData( UG_WINDOW* wnd )
  2. {
  3. UG_S16 xp,yp;
  4. UG_U16 i,objcnt;
  5. UG_OBJECT* obj;
  6. UG_U8 objstate;
  7. UG_U8 objtouch;
  8. UG_U8 tchstate;
  9. xp = gui->touch.xp;
  10. yp = gui->touch.yp;
  11. tchstate = gui->touch.state;
  12. objcnt = wnd->objcnt;
  13. for(i=0; i<objcnt; i++)
  14. {
  15. obj = (UG_OBJECT*)&wnd->objlst[i];
  16. objstate = obj->state;
  17. objtouch = obj->touch_state;
  18. if ( !(objstate & OBJ_STATE_FREE) && (objstate & OBJ_STATE_VALID) && (objstate & OBJ_STATE_VISIBLE) && !(objstate &
  19. OBJ_STATE_REDRAW))
  20. {
  21. /* Process touch data */
  22. if ( (tchstate) && xp != -1 )
  23. {
  24. if ( !(objtouch & OBJ_TOUCH_STATE_IS_PRESSED) )
  25. {
  26. objtouch |= OBJ_TOUCH_STATE_PRESSED_OUTSIDE_OBJECT | OBJ_TOUCH_STATE_CHANGED;
  27. objtouch &= ~(OBJ_TOUCH_STATE_RELEASED_ON_OBJECT | OBJ_TOUCH_STATE_RELEASED_OUTSIDE_OBJECT |
  28. OBJ_TOUCH_STATE_CLICK_ON_OBJECT);
  29. }
  30. objtouch &= ~OBJ_TOUCH_STATE_IS_PRESSED_ON_OBJECT;
  31. if ( xp >= obj->a_abs.xs )
  32. {
  33. if ( xp <= obj->a_abs.xe )
  34. {
  35. if ( yp >= obj->a_abs.ys )
  36. {
  37. if ( yp <= obj->a_abs.ye )
  38. {
  39. objtouch |= OBJ_TOUCH_STATE_IS_PRESSED_ON_OBJECT;
  40. if ( !(objtouch & OBJ_TOUCH_STATE_IS_PRESSED) )
  41. {
  42. objtouch &= ~OBJ_TOUCH_STATE_PRESSED_OUTSIDE_OBJECT;
  43. objtouch |= OBJ_TOUCH_STATE_PRESSED_ON_OBJECT;
  44. }
  45. }
  46. }
  47. }
  48. }
  49. objtouch |= OBJ_TOUCH_STATE_IS_PRESSED;
  50. }
  51. else if ( objtouch & OBJ_TOUCH_STATE_IS_PRESSED )
  52. {
  53. if ( objtouch & OBJ_TOUCH_STATE_IS_PRESSED_ON_OBJECT )
  54. {
  55. if ( objtouch & OBJ_TOUCH_STATE_PRESSED_ON_OBJECT ) objtouch |= OBJ_TOUCH_STATE_CLICK_ON_OBJECT;
  56. objtouch |= OBJ_TOUCH_STATE_RELEASED_ON_OBJECT;
  57. }
  58. else
  59. {
  60. objtouch |= OBJ_TOUCH_STATE_RELEASED_OUTSIDE_OBJECT;
  61. }
  62. if ( objtouch & OBJ_TOUCH_STATE_IS_PRESSED )
  63. {
  64. objtouch |= OBJ_TOUCH_STATE_CHANGED;
  65. }
  66. objtouch &= ~(OBJ_TOUCH_STATE_PRESSED_OUTSIDE_OBJECT | OBJ_TOUCH_STATE_PRESSED_ON_OBJECT |
  67. OBJ_TOUCH_STATE_IS_PRESSED);
  68. }
  69. }
  70. obj->touch_state = objtouch;
  71. }
  72. }
复制代码

29:_UG_UpdateObjects( wnd )函数负责更新控件对象
  1. void _UG_UpdateObjects( UG_WINDOW* wnd )
  2. {
  3. UG_U16 i,objcnt;
  4. UG_OBJECT* obj;
  5. UG_U8 objstate;
  6. UG_U8 objtouch;
  7. /* Check each object, if it needs to be updated? ,这个检查是否需要更新实在_UG_WindowUpdate( wnd )绘制窗口中设置*/
  8. objcnt = wnd->objcnt;
  9. for(i=0; i<objcnt; i++)
  10. {
  11. obj = (UG_OBJECT*)&wnd->objlst[i];
  12. objstate = obj->state;
  13. objtouch = obj->touch_state; /*把控件的触控状态复制给objtouch */
  14. if ( !(objstate & OBJ_STATE_FREE) && (objstate & OBJ_STATE_VALID) )
  15. {
  16. if ( objstate & OBJ_STATE_UPDATE )
  17. {
  18. obj->update(wnd,obj); /*更新控件 */
  19. }
  20. if ( (objstate & OBJ_STATE_VISIBLE) && (objstate & OBJ_STATE_TOUCH_ENABLE) )
  21. {
  22. if ( (objtouch & (OBJ_TOUCH_STATE_CHANGED | OBJ_TOUCH_STATE_IS_PRESSED)) )
  23. {
  24. obj->update(wnd,obj);
  25. }
  26. }
  27. }
  28. }
  29. }
复制代码
在创建控件的时候已经设置好了指针函数,obj->update(wnd,obj)指针函数会调用_UG_ButtonUpdate 函数,以下函
数最终会调用 UG_DrawLine 函数完成绘制。这个函数也会对按键的状态进行更新
  1. void _UG_ButtonUpdate(UG_WINDOW* wnd, UG_OBJECT* obj)
  2. {
  3. UG_BUTTON* btn;
  4. UG_AREA a;
  5. UG_TEXT txt;
  6. UG_U8 d;
  7. /* Get object-specific data */
  8. btn = (UG_BUTTON*)(obj->data);
  9. /* -------------------------------------------------- */
  10. /* Object touch section */
  11. /* -------------------------------------------------- */
  12. if ( (obj->touch_state & OBJ_TOUCH_STATE_CHANGED) )
  13. {
  14. /* Handle 'click' event */
  15. if ( obj->touch_state & OBJ_TOUCH_STATE_CLICK_ON_OBJECT )
  16. {
  17. obj->event = BTN_EVENT_CLICKED;
  18. obj->state |= OBJ_STATE_UPDATE;
  19. }
  20. /* Is the button pressed down? */
  21. if ( obj->touch_state & OBJ_TOUCH_STATE_PRESSED_ON_OBJECT )
  22. {
  23. btn->state |= BTN_STATE_PRESSED;
  24. obj->state |= OBJ_STATE_UPDATE;
  25. }
  26. /* Can we release the button? */
  27. else if ( btn->state & BTN_STATE_PRESSED )
  28. {
  29. btn->state &= ~BTN_STATE_PRESSED;
  30. obj->state |= OBJ_STATE_UPDATE;
  31. }
  32. obj->touch_state &= ~OBJ_TOUCH_STATE_CHANGED;
  33. }
  34. /* -------------------------------------------------- */
  35. /* Object update section */
  36. /* -------------------------------------------------- */
  37. if ( obj->state & OBJ_STATE_UPDATE )
  38. {
  39. if ( obj->state & OBJ_STATE_VISIBLE )
  40. {
  41. /* Full redraw necessary? */
  42. if ( (obj->state & OBJ_STATE_REDRAW) || (btn->state & BTN_STATE_ALWAYS_REDRAW) )
  43. {
  44. UG_WindowGetArea(wnd,&a);
  45. obj->a_abs.xs = obj->a_rel.xs + a.xs;
  46. obj->a_abs.ys = obj->a_rel.ys + a.ys;
  47. obj->a_abs.xe = obj->a_rel.xe + a.xs;
  48. obj->a_abs.ye = obj->a_rel.ye + a.ys;
  49. if ( obj->a_abs.ye >= wnd->ye ) return;
  50. if ( obj->a_abs.xe >= wnd->xe ) return;
  51. /* 3D or 2D style? */
  52. d = ( btn->style & BTN_STYLE_3D )? 3:1;
  53. txt.bc = btn->bc;
  54. txt.fc = btn->fc;
  55. if( btn->state & BTN_STATE_PRESSED )
  56. {
  57. /* "toggle" style? */
  58. if( btn->style & BTN_STYLE_TOGGLE_COLORS )
  59. {
  60. /* Swap colors */
  61. txt.bc = btn->fc;
  62. txt.fc = btn->bc;
  63. }
  64. /* Use alternate colors? */
  65. else if ( btn->style & BTN_STYLE_USE_ALTERNATE_COLORS )
  66. {
  67. txt.bc = btn->abc;
  68. txt.fc = btn->afc;
  69. }
  70. }
  71. UG_FillFrame(obj->a_abs.xs+d, obj->a_abs.ys+d, obj->a_abs.xe-d, obj->a_abs.ye-d, txt.bc);
  72. /* Draw button text */
  73. txt.a.xs = obj->a_abs.xs+d;
  74. txt.a.ys = obj->a_abs.ys+d;
  75. txt.a.xe = obj->a_abs.xe-d;
  76. txt.a.ye = obj->a_abs.ye-d;
  77. txt.align = ALIGN_CENTER;
  78. txt.font = btn->font;
  79. txt.h_space = 2;
  80. txt.v_space = 2;
  81. txt.str = btn->str;
  82. _UG_PutText( &txt );
  83. obj->state &= ~OBJ_STATE_REDRAW;
  84. }
  85. /* Draw button frame */
  86. if ( btn->style & BTN_STYLE_3D )
  87. { /* 3D */
  88. _UG_DrawObjectFrame(obj->a_abs.xs,obj->a_abs.ys,obj->a_abs.xe,obj->a_abs.ye,
  89. (btn->state&BTN_STATE_PRESSED)?(UG_COLOR*)pal_button_pressed:(UG_COLOR*)pal_button_released);
  90. }
  91. else
  92. { /* 2D */
  93. UG_DrawFrame(obj->a_abs.xs,obj->a_abs.ys,obj->a_abs.xe,obj->a_abs.ye,(btn->state&BTN_STATE_PRESSED)?btn->abc:btn->afc);
  94. }
  95. }
  96. else
  97. {
  98. UG_FillFrame(obj->a_abs.xs, obj->a_abs.ys, obj->a_abs.xe, obj->a_abs.ye, wnd->bc);
  99. }
  100. obj->state &= ~OBJ_STATE_UPDATE;
  101. }
  102. }
复制代码

8硬件接线

下图中 TF 卡不是必须的
259cf1adf222443995e7526fbace0dd7.jpg
9实验结果
在配套工程的uisrc/06_doc路径下有录制的测试视频

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则