单片机 GUI 设计(十五)- PC 仿真 GUI
单片机 GUI 设计(十五)- PC 仿真 GUI

单片机 GUI 设计(十五)- PC 仿真 GUI

基于 GD32F303ZET6 苹果派开发板

简介

主流的 GUI 开发平台,例如 EMWIN,大都有 PC 端的仿真。用户可以像 Qt 一样,在 PC 端完成界面,即 GUI 设计,最终在移植到单片机上。因为仿真可以脱离硬件而运行,所以我们可以随时随地的做开发、学习,只要手里有台电脑就行。本章中我们将介绍如何在 PC 端仿真出单片机下的 GUI。

windows 下 C 语言有个小巧的图形库,叫 EasyX。EasyX 提供了一些基本的 API 接口,例如画点、读点、获取鼠标输入等。虽然 EasyX 很原始,但我们只需要它的画点、读点函数即可。另外吐槽一下,EasyX 的官网在大陆上不去,导致好多资料无从查找,估计得翻墙才行。另外,EasyX 是用 C++ 写的,引用到 EasyX 的文件必须是 C++ 文件,不过还好,C++ 是向下兼容 C 的,所以我们完全可以用 C 的逻辑写代码。

移植

因为 EasyX 官网上不去,所以本文中提供了 EasyX 的安装包,安装包放在文章底部了。另外,推荐使用 Visual Studio 开发平台,EasyX 支持 VC++6.0、Visual Studio2008~2022。

在“相关资料/EasyX 图形库相关/图形库”文件夹下找到 EasyX.exe 文件,双击运行。在安装向导中选择对应的 IDE,即集成开发环境,如下所示。因为我的电脑中装了 Visual Studio 2013 和 Visual Studio 2019,所以我可以选择对这两个 IDE 安装 EasyX。然后,一路 Next 即可。

EasyX 的简单使用

因为 EasyX 使用到了 C++ 的特性,因此只有 C++ 文件才能调用它。EasyX 的头文件是 graphics.h,添加包含 graphics.h 后才能使用 EasyX 的 API 接口。

#include <graphics.h>

EasyX 通过 initgraph 函数创建窗口,具体如下所示。因为是 C++,所以 initgraph 函数支持为参数设定默认值。使能显示终端后,EasyX 会创建两个窗口,一个是应用窗口,另一个是终端窗口,即 C 语言中最常见的黑框。应用窗口用于显示用户界面,终端窗口用于输出一些调试信息,例如 printf 打印输出等。

initgraph(LCD_WIDTH, LCD_HEIGHT);                 //创建窗口,不显示终端
initgraph(LCD_WIDTH, LCD_HEIGHT, EW_SHOWCONSOLE); //创建窗口,显示终端

EasyX 的画点函数如下所示。经过实际测试,putpixel 函数效率太低,整屏刷很慢,不推荐使用。

putpixel(x, y);

因为 putpixel 函数效率太低,所以在本文提供的源码中,提供了新的思路。我们在 LCD 模块开辟了一段显存空间,也就是定义了一个大数组,打点、读点的对象都是这个显存数组。另外,我们还在 LCD 模块中开辟了一个线程,用于每隔 50ms 将显存数组更新到屏幕显示。具体代码如下所示。因为打点、读点操作的都是我们自己定义的显存缓冲区,所以访问速度会非常快。

本文提供的 LCD 驱动中,通过 LCD.h 中的 LCD_USE_RGB565 宏来配置是否要使用 RGB565格式。因为单片机平台大多使用的像素点格式为 RGB565,而 EasyX 只支持 RGB888,因此要做相应的转换。

static u32 s_arrLCDFrame[LCD_HEIGHT][LCD_WIDTH] = { 0 };
static void LCDReflashThread(void*)
{
  u32 x, y, color;
  DWORD* image;
  u32 i;

  while (1)
  {
    //获取当前显存首地址
    image = GetImageBuffer();

    //更新到屏幕显示
    i = 0;
    for (y = 0; y < LCD_HEIGHT; y++)
    {
      for (x = 0; x < LCD_WIDTH; x++)
      {
#if(0 != LCD_USE_RGB565)
        color = RGB565ToRGB888B(s_arrLCDFrame[y][x]);
#else
        color = s_arrLCDFrame[y][x];
#endif
        //更新到显存上
        image[i++] = color;
      }
    }

    //延时 50ms
    Sleep(50);
  }

  //线程返回
  _endthread();
}

EasyX 获取鼠标输入如下所示。通过 MOUSEMSG 获取到的鼠标信息,坐标系的原点就是应用窗口左上角,而不是电脑屏幕的绝对坐标。我们可以通过 MOUSEMSG 的成员变量 mkLButton 判断鼠标左键是否被按下。如果鼠标在应用窗口范围内按下了左键,那么 mkLButton 将会被置 1。

u32 GetTouch(u32* tx, u32* ty)
{
  //获取鼠标信息
  MOUSEMSG mouse;

  //获取鼠标信息
  mouse = GetMouseMsg();

  //如果左键被按下
  if(1 == mouse.mkLButton)
  {
    *tx = mouse.x;
    *ty = mouse.y;
    return 1;
  }
  else
  {
    *tx = 0xFFFFFFFF;
    *ty = 0xFFFFFFFF;
    return 0;
  }
}

LCD 底层驱动

为了更贴近于单片机的 GUI 开发,本文模仿裸机下的 GUI 编程,提供了 LCD 底层驱动,源码文件如下所示。LCD 文件对就是 LCD 驱动的顶层文件。为了方便用户使用,我们还将 GBK 字库以常量数组的形式内嵌到了 LCD 驱动中,对应 HzFontxxx.c 文件,也就是说 LCD 驱动是支持中文显示的。注意:因为 GBK 字库很大,所以打开 HzFont64x64.c 文件时,可能会出现卡顿,但不影响编译。

LCD.h 提供的 API 函数如下所示,可以看出,与之前的实验里的 LCD 驱动 API 接口相似的,唯一不同的是,此处将触屏检测也放到了 LCD.c/.h 中,主要是懒得再创建文件对。如果可以的话,我巴不得所有的东西都塞到 LCD 文件对中,这样移植时,只需要添加 LCD 文件对即可。

void InitLCD(void);                                                        //初始化 LCD 驱动
void DeInitLCD(void);                                                      //注销 LCD 模块
void LCDClear(u32 color);                                                  //LCD 清屏
void LCDFill(u32 x0, u32 y0, u32 x1, u32 y1, u32 color);                   //LCD 填充
void LCDFillColor(u32 x0, u32 y0, u32 x1, u32 y1, u32* color);             //LCD 填充
void LCDDrawPoint(u32 x, u32 y);                                           //LCD 画点
void LCDFastDrawPoint(u32 x, u32 y, u32 color);                            //LCD 快速画点
u32  LCDReadPoint(u32 x, u32 y);                                           //LCD 读点
void LCDDrawLine(u32 x0, u32 y0, u32 x1, u32 y1, u32 color);               //LCD 画线
void LCDDrawRectangle(u32 x1, u32 y1, u32 x2, u32 y2, u32 color);          //LCD 画矩形
void LCDDrawCircle(u32 x0, u32 y0, u32 r, u32 color);                      //LCD 画圆
void LCDShowChar(u32 x, u32 y, u32 code, u8 size, u8 mode);                //LCD 显示字符
void LCDShowString(u32 x, u32 y, u32 width, u32 height, u8 size, char* p); //LCD 显示字符串
u32  GetTouch(u32* tx, u32* ty);                                           //获取触屏输入,即鼠标输入

LCD 驱动中,屏幕的尺寸由 LCD_WIDTH 和 LCD_HEIGHT 两个宏决定,如下所示。

//LCD 长宽定义
#define LCD_WIDTH  (800)
#define LCD_HEIGHT (480)

测试代码

测试代码可以如下所示。

int main(void)
{
  char string[64];
  u32 x, y;

  //初始化 LCD
  InitLCD();

  //清屏
  LCDClear(LCD_COLOR_WHITE);

  //字符串显示测试
  g_iLCDPointColor = LCD_COLOR_BLACK;
  g_iLCDBackColor = LCD_COLOR_WHITE;
  LCDShowString(10, 0, LCD_WIDTH, LCD_HEIGHT, 12, (char*)"hello world, 你好世界");
  LCDShowString(10, 15, LCD_WIDTH, LCD_HEIGHT, 16, (char*)"hello world, 你好世界");
  LCDShowString(10, 30, LCD_WIDTH, LCD_HEIGHT, 24, (char*)"hello world, 你好世界");
  LCDShowString(10, 55, LCD_WIDTH, LCD_HEIGHT, 32, (char*)"hello world, 你好世界");
  LCDShowString(10, 90, LCD_WIDTH, LCD_HEIGHT, 36, (char*)"hello world, 你好世界");
  LCDShowString(10, 125, LCD_WIDTH, LCD_HEIGHT, 64, (char*)"hello world, 你好世界");
  
  //填充测试
  LCDFill(10, 200, 790, 230, LCD_COLOR_RED);

  //画线测试
  LCDDrawLine(10, 240, 790, 470, LCD_COLOR_GREEN);

  //画圆测试
  LCDDrawCircle(400, 355, 50, LCD_COLOR_BLUE);

  //输出鼠标信息
  while (1)
  {
    if (0 != GetTouch(&x, &y))
    {
      LCDFill(10, 450, 240, 479, g_iLCDBackColor);
      sprintf(string, "place: %d %d", x, y);
      LCDShowString(10, 450, LCD_WIDTH, LCD_HEIGHT, 12, string);
    }
  }
  
  return 0;
}

最终的效果如下所示。

注意事项

请将项目配置中的“字符集”项目修改为“使用多字节字符集”,否则项目中的字符串编码有可能不是 GBK,造成中文显示乱码。

安装包

源码

本章节中的源码请参考《单片机 GUI 设计(零)- 大纲