当前位置: 首页 > news >正文

嵌入式GUI数据可视化:emWin GRAPH控件核心API与实战应用

1. 嵌入式GUI数据可视化的基石:GRAPH控件深度解析

在嵌入式系统开发中,尤其是涉及工业控制、医疗设备、仪器仪表等领域,将传感器采集的数据、系统运行状态以直观的图形方式呈现给用户,是一项核心且高频的需求。你不可能总指望用户去解读一串串冰冷的十六进制数或日志文件,一个实时的波形图、一条平滑的趋势曲线,往往能更高效地传递信息。这正是嵌入式图形用户界面(GUI)库的价值所在,而emWin作为业界广泛使用的解决方案,其内置的GRAPH控件,就是我们实现这类数据可视化功能的“瑞士军刀”。

很多刚接触emWin的开发者,可能会觉得GRAPH控件用起来有点“绕”:为什么要先创建数据对象,再附加到控件?标尺又该怎么配置才能和我的数据对齐?这些疑惑的背后,其实是对GRAPH控件设计哲学的理解不足。简单来说,GRAPH控件采用了典型的“模型-视图”分离架构。控件本身(GRAPH)只是一个“画布”和“视图管理器”,它负责定义绘图区域、网格、边框等视觉框架。真正的数据源,则由独立的数据对象(GRAPH_DATA_YT/GRAPH_DATA_XY)来管理,这就像Excel图表和数据表的关系。而标尺对象(GRAPH_SCALE)则是独立的“坐标轴标注器”。这种解耦设计带来了极大的灵活性:你可以动态更换数据源、为同一组数据配置不同的显示样式,或者在一个GRAPH上叠加多个数据曲线。

本文将聚焦于GRAPH控件的三大核心构件——控件本身、数据对象和标尺——的关键API,并结合我多年在STM32、NXP等MCU平台上的实战经验,不仅告诉你每个函数怎么用,更会深入剖析其设计意图、使用场景以及那些手册里不会写的“坑”。无论你是在开发一个电机转速监控界面,还是绘制心电图波形,掌握这些API的奥义,都能让你事半功倍。

2. GRAPH控件本体:创建、管理与视觉定制

GRAPH控件是数据展示的舞台,所有的数据对象和标尺都将在其上绘制。理解如何搭建和配置这个舞台,是第一步。

2.1 控件的创建与初始化

创建GRAPH控件主要有两种方式:直接创建和间接创建。对于大多数应用,我们使用GRAPH_CreateEx函数,因为它提供了最直接的控制。

GRAPH_Handle hGraph; hGraph = GRAPH_CreateEx(50, // x0: 控件左上角X坐标(相对于父窗口) 30, // y0: 控件左上角Y坐标 400, // xSize: 控件宽度 300, // ySize: 控件高度 WM_HBKWIN, // hParent: 父窗口句柄,这里用桌面窗口 WM_CF_SHOW, // WinFlags: 窗口创建标志,立即显示 0, // ExFlags: GRAPH特定标志,通常为0或使用GRAPH_CF_XXX系列 GUI_ID_GRAPH0 // Id: 控件ID,用于消息回调识别 ); if (hGraph == 0) { // 创建失败处理,通常是内存不足 }

参数详解与避坑指南:

  • 坐标与尺寸 (x0, y0, xSize, ySize):这些值是在父窗口坐标系下的。如果你的GRAPH放在另一个容器窗口(如对话框)里,务必计算好相对位置。一个常见的错误是直接使用屏幕绝对坐标,导致控件位置错乱。
  • WinFlags:WM_CF_SHOW是最常用的,表示创建后立即显示。如果你需要先创建控件,配置好所有属性(如数据、颜色)后再一次性显示以避免闪烁,可以先不用这个标志,最后调用WM_ShowWindow(hGraph)
  • ExFlags:这是GRAPH控件的专属创建标志。例如,GRAPH_CF_AUTOSCROLLBAR可以启用自动滚动条(但更推荐用GRAPH_SetAutoScrollbar动态控制)。在创建时确定是否需要滚动条,能避免后续的布局问题。
  • 控件ID:在回调函数中,通过pMsg->Id可以识别是哪个GRAPH控件产生了消息(如触摸事件)。如果不需要消息区分,可以设为0。

GRAPH_CreateIndirectGRAPH_CreateUser则用于更复杂的场景,比如通过GUI Builder工具生成的代码,或者需要深度自定义窗口过程时使用。对于手动编码,GRAPH_CreateEx足矣。

2.2 视觉属性配置:边框、网格与颜色

一个专业的图表,其可读性很大程度上取决于背景、网格等视觉元素的配置。

设置边框 (GRAPH_SetBorder):边框定义了数据绘制区域(Data Area)与控件边缘之间的留白。这个留白区域可以用来放置标尺文本。

GRAPH_SetBorder(hGraph, 40, 20, 10, 30); // 左、上、右、下边框宽度(像素)

实操心得:左边的边框(BorderL)通常需要设置得宽一些,为Y轴标尺的文本留出空间。下边的边框(BorderB)则为X轴标尺留空间。上边和右边的边框可以小一些,主要用于美观和防止曲线贴边。

显示与配置网格 (GRAPH_SetGridVis, GRAPH_SetGridDistX/Y):网格是辅助读数的重要工具。

GRAPH_SetGridVis(hGraph, 1); // 1=显示网格,0=隐藏 GRAPH_SetGridDistX(hGraph, 50); // 设置X方向网格线间距为50像素 GRAPH_SetGridDistY(hGraph, 30); // 设置Y方向网格线间距为30像素
  • 网格偏移 (GRAPH_SetGridOffX/Y):当你的数据零点不在绘图区左下角时(例如波形图零点在中间),网格线可能不对齐。这时可以用GRAPH_SetGridOffY(hGraph, -150)将网格线上移150像素,使其穿过零点。
  • 固定网格 (GRAPH_SetGridFixedX):在实时滚动图表中(如心电图),数据在滚动,但背景网格通常是固定的。设置GRAPH_SetGridFixedX(hGraph, 1)可以实现这个效果。
  • 网格线样式 (GRAPH_SetLineStyleH/V):可以设置为虚线 (GUI_LS_DOT)、点划线等。但要注意手册的警告:非实线样式 (GUI_LS_SOLID) 的绘制会消耗更多CPU时间,在低性能MCU上需谨慎使用。

颜色管理 (GRAPH_SetColor):GRAPH控件有多种颜色索引,用于设置不同部分的颜色。

GUI_COLOR oldBgColor = GRAPH_SetColor(hGraph, GUI_DARKGRAY, GRAPH_CI_BK); GRAPH_SetColor(hGraph, GUI_LIGHTGRAY, GRAPH_CI_GRID); // 网格线颜色 GRAPH_SetColor(hGraph, GUI_BLUE, GRAPH_CI_FRAME); // 数据区边框颜色

常见的颜色索引包括GRAPH_CI_BK(背景色)、GRAPH_CI_GRID(网格色)、GRAPH_CI_FRAME(边框色)。通过GRAPH_GetColor可以查询当前设置。

2.3 滚动与用户自定义绘制

虚拟尺寸与滚动 (GRAPH_SetVSizeX/Y):这是实现大数据集或实时滚动图表的关键。控件的物理尺寸是固定的(如400x300),但你可以定义一个更大的“虚拟画布”。

// 假设我们有1000个数据点,每个点希望在X方向占2像素宽度 GRAPH_SetVSizeX(hGraph, 1000 * 2); // 虚拟宽度为2000像素 // 如果2000 > 控件数据区实际宽度,水平滚动条会自动出现(需确保自动滚动条已启用) GRAPH_SetAutoScrollbar(hGraph, GUI_COORD_X, 1); // 启用X轴自动滚动条

通过GRAPH_SetScrollValueGRAPH_GetScrollValue可以编程控制或读取滚动位置。GRAPH_InvertScrollbar可以反转滚动方向,这在某些特定场景下有用(比如从右向左生长的时序图)。

用户自定义绘制 (GRAPH_SetUserDraw):这是GRAPH控件最强大的扩展功能之一。它允许你在控件绘制的特定阶段插入自己的绘图代码。

static void _MyUserDraw(WM_HWIN hWin, int Stage) { switch (Stage) { case GRAPH_DRAW_FIRST: // 阶段1:在网格和數據绘制之前,但在背景之后。 // 适合绘制自定义的背景色块、区域标记等。 GUI_SetColor(GUI_RED); GUI_FillRect(10, 10, 50, 50); // 在数据区(10,10)位置画个红色方块 break; case GRAPH_DRAW_LAST: // 阶段2:在所有标准元素(数据线、网格、标尺)绘制之后。 // 适合绘制高亮的参考线、文本注释、峰值标记等。 GUI_SetColor(GUI_GREEN); GUI_DrawHLine(100, 0, 399); // 在Y=100像素处画一条绿色参考线 GUI_SetTextMode(GUI_TM_TRANS); GUI_DispStringHCenterAt("Threshold", 200, 95); break; } } // 在初始化时设置回调函数 GRAPH_SetUserDraw(hGraph, _MyUserDraw);

核心技巧:GRAPH_DRAW_FIRST阶段的裁剪区域被限制在数据区内,而GRAPH_DRAW_LAST阶段则是整个控件区域(除效果边框外)。这意味着在LAST阶段你可以在边框上绘图。利用这个特性,可以实现非常灵活的图表注解功能。

3. 数据对象:GRAPH_DATA_YT与GRAPH_DATA_XY详解

数据对象是GRAPH控件的灵魂,它决定了数据如何被存储、组织和渲染。emWin主要提供了两种类型的数据对象,对应两种最常用的图表类型。

3.1 YT图数据对象 (GRAPH_DATA_YT)

YT图,即Y值-时间图,是嵌入式系统中最常见的图表类型。其特点是X轴代表均匀的时间序列或顺序索引,Y轴代表对应的测量值。例如,温度随时间的变化、ADC采样值的实时显示。

创建与初始化:

#define MAX_DATA_POINTS 500 static I16 s_aTemperatureData[MAX_DATA_POINTS] = {0}; // 静态数组存储 GRAPH_DATA_Handle hDataTemp; // 创建数据对象,并关联初始数据 hDataTemp = GRAPH_DATA_YT_Create(GUI_GREEN, // 曲线颜色 MAX_DATA_POINTS, // 对象能存储的最大数据点数 s_aTemperatureData, // 初始数据数组指针 0); // 初始添加的数据个数,这里为0 if (hDataTemp == 0) { // 错误处理 } // 将数据对象附加到GRAPH控件 GRAPH_AttachData(hGraph, hDataTemp);

关键参数解析:

  • MaxNumItems: 这是数据对象的“容量”。一旦数据点数量达到这个值,再添加新数据时,最旧的数据会被挤出(FIFO队列)。这个机制非常适合实现固定长度的实时滚动波形。
  • pItemsNumItems: 允许在创建时传入一批历史数据。如果是从零开始实时添加,可以传入NULL和0。

动态数据操作:

// 模拟获取一个新的温度值(例如从ADC) I16 newTemp = Read_Temperature_Sensor(); // 将新值添加到数据对象 GRAPH_DATA_YT_AddValue(hDataTemp, newTemp); // 此时,GRAPH控件会自动重绘,显示新的曲线点

GRAPH_DATA_YT_AddValue是实时数据流的核心。它会自动处理队列的移位。特殊值0x7FFF被定义为无效数据,绘制时会产生断点,这在传感器偶尔失效时非常有用。

数据变换与对齐:

  • 垂直偏移 (GRAPH_DATA_YT_SetOffY):如果你的ADC原始值范围是0-4095,但你想显示为-100°C到100°C,就需要进行偏移和缩放。偏移是直接的像素平移。
    // 假设数据区高度300像素,我们希望0对应-100°,4095对应100°。 // 这需要先缩放,但偏移可以处理零点平移。例如,想让零点在中间(150像素处): // 如果原始值2048对应0°,那么添加值时需要先转换:(adc_value - 2048) * scale_factor // 但更简单的思路:设置偏移,让计算后的Y=0点位于150像素。 // 通常偏移和因子配合标尺的GRAPH_SCALE_SetFactor使用更直观。
  • 对齐方式 (GRAPH_DATA_YT_SetAlign):默认情况下,数据点位于网格线的“右侧”。你可以设置为GRAPH_ALIGN_LEFT让点位于网格线左侧,这对某些柱状图效果有用。
  • 镜像 (GRAPH_DATA_YT_MirrorX):默认数据从右向左绘制(最新数据在右)。GRAPH_DATA_YT_MirrorX(hDataTemp, 1)会改为从左向右绘制,符合一些从左向右时间流的习惯。

数据管理:

  • GRAPH_DATA_YT_Clear: 清空所有数据点。
  • GRAPH_DATA_YT_GetValue: 按索引读取数据点,用于数据导出或分析。
  • GRAPH_DATA_YT_Delete:重要!仅当数据对象已从GRAPH控件分离 (GRAPH_DetachData) 后,才需要手动删除。如果数据对象仍附着在GRAPH上,GRAPH被删除时会自动清理所有附着的数据对象,此时再手动删除会导致野指针或内存错误。

3.2 XY图数据对象 (GRAPH_DATA_XY)

XY图用于描述两个变量之间的关系,X和Y坐标都是自由值。例如,电机扭矩-转速曲线、阻抗特性曲线、任意形状的轨迹绘制。

创建与操作:

#define MAX_XY_POINTS 100 static GUI_POINT s_aMotorCurve[MAX_XY_POINTS]; GRAPH_DATA_Handle hDataCurve; // 创建空的XY数据对象 hDataCurve = GRAPH_DATA_XY_Create(GUI_RED, MAX_XY_POINTS, NULL, 0); // 添加数据点(例如,来自计算或测量) GUI_POINT aPoint; aPoint.x = 100; // 转速 aPoint.y = 50; // 扭矩 GRAPH_DATA_XY_AddPoint(hDataCurve, &aPoint); // ... 添加更多点 GRAPH_AttachData(hGraph, hDataCurve);

高级样式控制:XY数据对象提供了更丰富的绘制选项,这是YT对象所不具备的。

  • 线条可见性 (GRAPH_DATA_XY_SetLineVis):可以隐藏连接线,只显示数据点 (GRAPH_DATA_XY_SetPointVis)。
  • 线条样式与粗细 (GRAPH_DATA_XY_SetLineStyle, GRAPH_DATA_XY_SetPenSize):可以设置为虚线、点线,并调整线条粗细。请注意一个关键限制:只有当线条样式为GUI_LS_SOLID(默认)时,才能设置大于1的画笔尺寸 (PenSize)。如果你想画粗的虚线,需要自己用GRAPH_SetUserDraw实现。
  • 数据点可见性 (GRAPH_DATA_XY_SetPointVis):设置为1后,每个数据点处会绘制一个小标记(通常是矩形或十字),便于识别离散数据点。

偏移设置 (GRAPH_DATA_XY_SetOffX/Y):XY图的偏移设置比YT图更直观,因为它分别控制X和Y方向。例如,你的数据坐标系原点在(100, -200),而GRAPH数据区原点在(0,0)。为了让数据正确显示,你需要设置:

GRAPH_DATA_XY_SetOffX(hDataCurve, -100); // 将所有点左移100像素 GRAPH_DATA_XY_SetOffY(hDataCurve, 200); // 将所有点上移200像素

所有者绘制 (GRAPH_DATA_XY_SetOwnerDraw):这是比GRAPH控件级别的SetUserDraw更细粒度的回调。它专属于某个数据对象,允许你自定义该数据对象上每个数据点或线段的绘制方式。回调函数会收到WIDGET_ITEM_DRAW_INFO结构体,里面包含了当前绘制的命令、坐标等信息。你可以用它来绘制特殊的数据点图标(如三角形、圆形),或者实现自定义的线型效果。

3.3 数据对象管理的最佳实践

  1. 生命周期管理:牢记“谁创建,谁销毁”的原则。但GRAPH控件提供了便利:通过GRAPH_AttachData附加的对象,其生命周期将由GRAPH控件托管。只有当需要提前销毁数据对象,或者GRAPH销毁后你还需要数据对象时,才先调用GRAPH_DetachData,再调用GRAPH_DATA_YT/XY_Delete
  2. 多曲线显示:一个GRAPH控件可以附加多个数据对象,实现多条曲线的叠加显示。只需创建多个数据对象,分别设置不同的颜色,然后依次附加即可。
  3. 性能考量:数据对象容量 (MaxNumItems) 不宜盲目设置过大。尤其是在低RAM的MCU上,每个I16数据点占2字节,每个GUI_POINT占4字节(两个I16)。一个1000点的XY数据对象就需要4KB RAM。同时,重绘大量数据点也会消耗CPU时间。需要根据显示区域大小和刷新率权衡。
  4. 无效数据的使用:对于YT数据,善用0x7FFF表示无效点来创建曲线的中断,比用两个独立的数据对象更高效。

4. 标尺对象 (GRAPH_SCALE):为图表注入灵魂

没有坐标刻度的图表就像没有刻度的尺子,几乎无法读数。GRAPH_SCALE对象就是为GRAPH控件添加坐标刻度的专用工具。

4.1 标尺的创建与附着

标尺的创建比数据对象稍复杂,因为需要指定其位置、对齐方式和刻度间隔。

GRAPH_SCALE_Handle hScaleX, hScaleY; // 创建X轴标尺(水平标尺,通常放在底部) hScaleX = GRAPH_SCALE_Create(GRAPH_SCALE_CF_HORIZONTAL, // 标志:水平标尺 10, // Pos: 距离GRAPH顶部的距离(对于水平标尺) GUI_TA_LEFT | GUI_TA_TOP, // TextAlign: 文本左对齐、顶部对齐(位于刻度线上方) 50); // TickDist: 刻度间隔50像素 if (hScaleX) { GRAPH_AttachScale(hGraph, hScaleX); } // 创建Y轴标尺(垂直标尺,通常放在左侧) hScaleY = GRAPH_SCALE_Create(GRAPH_SCALE_CF_VERTICAL, // 标志:垂直标尺 40, // Pos: 距离GRAPH左侧的距离(对于垂直标尺) GUI_TA_RIGHT | GUI_TA_TOP, // TextAlign: 文本右对齐、顶部对齐(位于刻度线左侧) 30); // TickDist: 刻度间隔30像素 if (hScaleY) { GRAPH_AttachScale(hGraph, hScaleY); }

参数深度解读:

  • Flags:GRAPH_SCALE_CF_HORIZONTALGRAPH_SCALE_CF_VERTICAL决定了标尺的方向。这是一个关键但易错点:标尺的“位置”参数Pos的含义取决于方向。对于水平标尺,Pos是从GRAPH顶部向下的距离;对于垂直标尺,Pos是从GRAPH左侧向右的距离。你需要根据你为GRAPH设置的边框 (BorderL,BorderT) 来调整这个值,让标尺文本显示在理想的留白区域内。
  • TextAlign:文本对齐方式决定了刻度数字相对于刻度线的位置。对于底部水平标尺,通常用GUI_TA_HCENTER | GUI_TA_TOP(水平居中、上方)让数字在刻度线中间上方。对于左侧垂直标尺,常用GUI_TA_RIGHT | GUI_TA_TOP(右对齐、上方)让数字在刻度线左侧。
  • TickDist:这是像素距离。它定义了屏幕上每隔多少像素画一个刻度并标一个数字。这个值需要和你数据的物理意义通过GRAPH_SCALE_SetFactor关联起来。

4.2 标尺与数据的映射:因子 (Factor) 的设置

这是标尺配置中最核心的一步。默认情况下,标尺的数字就是像素坐标。但在实际应用中,我们需要显示具有物理意义的单位,如“秒(s)”、“电压(V)”、“温度(°C)”。

设置因子 (GRAPH_SCALE_SetFactor):

// 假设我们的GRAPH数据区X方向(宽度)虚拟尺寸是1000像素,代表100秒的时间。 // 那么,1像素 = 0.1秒。 // 我们希望标尺显示的是“秒”,所以需要设置因子。 GRAPH_SCALE_SetFactor(hScaleX, 0.1f); // 因子 = 期望单位 / 像素 // 现在,如果TickDist=50像素,那么刻度数字间隔就是 50px * 0.1 s/px = 5秒。 // 假设Y方向数据区高300像素,对应ADC值0-4095,我们想显示为电压0.0V-3.3V。 // 那么,Y方向每像素代表的电压值是 3.3V / 300px = 0.011 V/px。 // 但我们通常更习惯从像素换算到物理量:物理量 = 像素坐标 * 因子。 // 所以因子应该是 0.011 V/px。 // 然而,注意:标尺的零点在数据区底部。如果ADC值0对应0V,在像素坐标0处,那么显示正确。 // 如果我们设置了 GRAPH_DATA_YT_SetOffY(hData, -100),让ADC值0显示在Y=100像素处(中间), // 那么标尺的零点也对应像素坐标100处。此时,像素坐标0处显示的电压值是 (0-100)*0.011 = -1.1V。 // 因此,因子和偏移需要协同考虑。 GRAPH_SCALE_SetFactor(hScaleY, 0.011f); // 假设无偏移或偏移已考虑

核心逻辑:标尺显示的数字 = 像素坐标 × 因子。你需要根据你的数据到像素的映射关系,计算出这个因子。如果数据本身还有偏移,需要确保标尺的零点与数据的零点在像素位置上对齐。

4.3 字体与更多控制

  • 字体 (GRAPH_SCALE_SetFont):你可以为标尺设置特定的字体,通常比默认字体小一些以节省空间,例如&GUI_Font8x16
  • 动态更新:因子和字体可以在运行时动态修改。例如,当用户切换量程时(从显示mV切换到V),只需要更新因子并触发重绘即可。
  • 多标尺:你可以在同一侧附加多个标尺。例如,在左侧放置两个垂直标尺,分别用不同的颜色和因子,来对应GRAPH上两条不同量纲的曲线(比如一条是温度(°C),一条是压力(kPa))。这需要精心计算位置(Pos)以避免重叠。

5. 综合实战:构建一个实时温度监测曲线图

让我们将上述所有知识点串联起来,实现一个经典的嵌入式应用:实时温度监测曲线图。假设我们从DS18B20传感器每秒读取一个温度值,并在480x272的LCD上显示最近5分钟(300秒)的历史曲线。

5.1 系统设计与参数计算

  1. 需求定义:
    • 显示区域:GRAPH控件大小设为400像素宽,200像素高。
    • 时间范围:X轴显示最近300秒。
    • 温度范围:Y轴显示-10°C 到 50°C。
    • 刷新率:1秒添加一个数据点,曲线向左滚动。
  2. GRAPH布局规划:
    • 控件位置:GRAPH_CreateEx(40, 20, 400, 200, ...)
    • 边框:左留50像素给Y轴标尺,下留30像素给X轴标尺。GRAPH_SetBorder(hGraph, 50, 10, 10, 30)
    • 网格:显示网格,间距设为X=40像素(对应约30秒),Y=25像素(对应约5°C)。GRAPH_SetGridDistX(hGraph, 40); GRAPH_SetGridDistY(hGraph, 25);
  3. 数据对象规划:
    • 容量:每秒1点,显示300秒,需要300个点。但为了平滑滚动和一点余量,我们设置容量为320。GRAPH_DATA_YT_Create(..., 320, ...)
    • 数据映射:温度范围-10~50°C,共60°C跨度。数据区高200像素。因此,每像素代表 60°C / 200px = 0.3 °C/px。
    • 偏移计算:我们希望-10°C对应像素坐标0(底部),50°C对应像素坐标200(顶部)。那么,温度值T对应的像素Y = (T - (-10)) / 0.3 = (T + 10) / 0.3。或者,我们可以设置数据偏移GRAPH_DATA_YT_SetOffY(hData, - ( -10 / 0.3) )?不,更简单的方法是在添加数据时进行转换。我们选择在添加值时转换。
  4. 标尺规划:
    • X轴标尺:因子 = 300秒 / 400像素 = 0.75 秒/像素。刻度间隔40像素对应 40 * 0.75 = 30秒。
    • Y轴标尺:因子 = 0.3 °C/像素(与数据映射一致)。刻度间隔25像素对应 25 * 0.3 = 7.5°C。我们希望显示为整数,可以调整网格间距为26.666...像素,或者接受小数显示。

5.2 代码实现

// 定义与全局变量 #define TEMP_GRAPH_WIDTH 400 #define TEMP_GRAPH_HEIGHT 200 #define TEMP_HISTORY_SECONDS 300 #define TEMP_DATA_CAPACITY 320 #define TEMP_Y_RANGE_MIN (-10) #define TEMP_Y_RANGE_MAX 50 static GRAPH_Handle hTempGraph; static GRAPH_DATA_Handle hTempData; static GRAPH_SCALE_Handle hScaleX, hScaleY; static I16 s_aTempDataBuffer[TEMP_DATA_CAPACITY]; static U32 s_ulDataIndex = 0; // 温度值到像素Y坐标的转换函数 static I16 _TempToPixel(float fTemp) { float fPixelY; // 映射: fTemp从[-10, 50] 线性映射到 [0, GRAPH_HEIGHT-1] // 公式: Y = (fTemp - Y_MIN) / (Y_MAX - Y_MIN) * (Height-1) // 但GRAPH的Y坐标原点在顶部,值增大向下。而温度值增大应向上。 // 所以需要反转: Y = (Height-1) - (fTemp - Y_MIN) / (Y_MAX - Y_MIN) * (Height-1) fPixelY = (TEMP_Y_RANGE_MAX - fTemp) / (TEMP_Y_RANGE_MAX - TEMP_Y_RANGE_MIN) * (TEMP_GRAPH_HEIGHT - 1); // 加上边框偏移?不,GRAPH_DATA_YT的数据是相对于数据区内部的。 // 我们创建GRAPH时设置了Border,数据区是内部的区域。 // 所以这里的计算只针对数据区高度。 return (I16)(fPixelY + 0.5f); // 四舍五入 } // 初始化GRAPH、数据对象和标尺 void TEMP_GRAPH_Init(void) { // 1. 创建GRAPH控件 hTempGraph = GRAPH_CreateEx(40, 20, TEMP_GRAPH_WIDTH, TEMP_GRAPH_HEIGHT, WM_HBKWIN, WM_CF_SHOW, 0, GUI_ID_GRAPH0); // 2. 设置视觉属性 GRAPH_SetBorder(hTempGraph, 50, 10, 10, 30); // 左、上、右、下 GRAPH_SetColor(hTempGraph, GUI_BLACK, GRAPH_CI_BK); GRAPH_SetColor(hTempGraph, GUI_GRAY, GRAPH_CI_GRID); GRAPH_SetGridVis(hTempGraph, 1); GRAPH_SetGridDistX(hTempGraph, 40); // 约30秒间隔 GRAPH_SetGridDistY(hTempGraph, 25); // 约7.5°C间隔 // 3. 创建并附加YT数据对象 hTempData = GRAPH_DATA_YT_Create(GUI_GREEN, TEMP_DATA_CAPACITY, NULL, 0); if (hTempData) { GRAPH_AttachData(hTempGraph, hTempData); // 设置数据颜色(也可以在创建时指定) GRAPH_DATA_YT_SetColor(hTempData, GUI_GREEN); } // 4. 创建并附加X轴标尺(时间轴) hScaleX = GRAPH_SCALE_Create(GRAPH_SCALE_CF_HORIZONTAL, TEMP_GRAPH_HEIGHT + 10, // 位于底部边框内,距顶部210像素(200+10) GUI_TA_HCENTER | GUI_TA_TOP, 40); // 刻度间隔40像素 if (hScaleX) { GRAPH_AttachScale(hTempGraph, hScaleX); // 设置因子:像素 -> 秒。总时间300秒,数据区宽400像素(假设无水平滚动,虚拟尺寸=物理尺寸) // 但注意:我们的GRAPH宽度是400,但左border=50,右border=10,数据区实际宽=400-50-10=340像素。 // 标尺是基于数据区宽度计算的!我们需要用数据区宽度。 int dataAreaWidth = TEMP_GRAPH_WIDTH - 50 - 10; // 340像素 float factorX = (float)TEMP_HISTORY_SECONDS / dataAreaWidth; // 300/340 ≈ 0.882秒/像素 GRAPH_SCALE_SetFactor(hScaleX, factorX); GRAPH_SCALE_SetFont(hScaleX, &GUI_Font8x16); // 用小字体 } // 5. 创建并附加Y轴标尺(温度轴) hScaleY = GRAPH_SCALE_Create(GRAPH_SCALE_CF_VERTICAL, 5, // 位于左侧边框内,距左边缘5像素 GUI_TA_RIGHT | GUI_TA_TOP, 25); // 刻度间隔25像素 if (hScaleY) { GRAPH_AttachScale(hTempGraph, hScaleY); // 设置因子:像素 -> °C。温度范围60°C,数据区高200-10-30=160像素? // 仔细计算:GRAPH高200,上border=10,下border=30,数据区高=200-10-30=160像素。 int dataAreaHeight = TEMP_GRAPH_HEIGHT - 10 - 30; // 160像素 float factorY = (float)(TEMP_Y_RANGE_MAX - TEMP_Y_RANGE_MIN) / dataAreaHeight; // 60/160 = 0.375 °C/像素 GRAPH_SCALE_SetFactor(hScaleY, factorY); // 注意:标尺的0点对应数据区像素坐标的0点(底部)。我们的温度-10°C对应像素坐标160(顶部),50°C对应0(底部)。 // 因此,标尺显示的数字 = (像素坐标) * 0.375。当像素坐标=0时,显示0°C;像素坐标=160时,显示60°C。 // 但我们的温度范围是-10到50,跨度60。所以我们需要让标尺显示-10到50。 // 这可以通过设置标尺的“偏移”吗?GRAPH_SCALE没有直接偏移API。 // 正确做法:让数据映射时,将-10°C映射到像素160,50°C映射到像素0。这样标尺因子0.375,在像素0处显示0,在像素160处显示60。 // 但我们想要-10和50。所以需要修改转换函数_TempToPixel,让-10对应像素160,50对应像素0。 // 同时,标尺的因子需要反映从像素到“显示值”的映射。显示值 = 50 - 像素坐标 * 0.375。 // 这无法通过简单因子实现。一个更实用的方法是:接受标尺显示0-60,然后在标尺文本旁通过静态文本标注“°C”,用户知道需要减去10。 // 或者,使用更高级的技巧:自定义用户绘制函数(GRAPH_SetUserDraw)来覆盖绘制标尺数字。 // 这里为了简化,我们调整因子和转换,让显示值正确。 // 重新定义:像素坐标y (0在底部,160在顶部) 对应的温度 T = Y_MAX - y * (Y_RANGE/Height) // T = 50 - y * (60/160) = 50 - y * 0.375 // 因此,因子是 -0.375,并且需要设置一个“参考点偏移”?GRAPH_SCALE不支持。 // 鉴于复杂度,一个常见的工程妥协是:调整Y轴显示范围从0到60,并在旁边注明“T(°C)-10”。 // 或者,使用两个标尺,一个在左显示-10..50,一个在右显示0..60。 // 本例中,我们采用简化方案,显示0-60,并在初始化时添加一个静态文本说明。 GRAPH_SCALE_SetFont(hScaleY, &GUI_Font8x16); } // 6. (可选)添加静态文本说明 GUI_DispStringAt("Temp (C) -10", 5, 25); GUI_DispStringAt("Time (s)", 200, 225); } // 每秒调用一次,添加新的温度数据 void TEMP_GRAPH_AddNewValue(float fTemperature) { I16 yPixel; if (hTempData == 0) return; // 将温度值转换为像素Y坐标(基于数据区高度160像素) // 公式:y = (TEMP_Y_RANGE_MAX - fTemperature) / (TEMP_Y_RANGE_MAX - TEMP_Y_RANGE_MIN) * (dataAreaHeight - 1) // dataAreaHeight = 160 float range = (float)(TEMP_Y_RANGE_MAX - TEMP_Y_RANGE_MIN); // 60.0 int dataAreaHeight = TEMP_GRAPH_HEIGHT - 10 - 30; // 160 yPixel = (I16)( (TEMP_Y_RANGE_MAX - fTemperature) / range * (dataAreaHeight - 1) + 0.5f ); // 添加数据 GRAPH_DATA_YT_AddValue(hTempData, yPixel); // (可选)实现滚动:当数据点超过一定数量后,可以设置虚拟尺寸并启用滚动条 // 但本例要求固定显示300秒,每秒1点,共300点。数据区宽度340像素,足以显示300点(每点>1像素)。 // 如果希望每点占更宽像素,可以设置虚拟尺寸大于物理尺寸。 // 例如,让每点占2像素:GRAPH_SetVSizeX(hTempGraph, TEMP_DATA_CAPACITY * 2); // 并启用自动滚动条:GRAPH_SetAutoScrollbar(hTempGraph, GUI_COORD_X, 1); // 这样,当数据超过170个点后,会出现水平滚动条。 }

5.3 性能优化与常见问题排查

1. 画面闪烁问题:

  • 现象:曲线更新时,整个GRAPH区域有明显闪烁。
  • 原因:默认情况下,每次添加数据点 (GRAPH_DATA_YT_AddValue) 都会导致GRAPH控件自动重绘。如果重绘区域大或MCU性能低,就会闪烁。
  • 解决方案:
    • 局部更新:在添加大量历史数据初始化时,可以先调用WM_DisableWindow(hTempGraph)禁用控件更新,所有数据添加完毕后再调用WM_EnableWindow(hTempGraph)WM_InvalidateWindow(hTempGraph)触发一次重绘。
    • 双缓冲:如果emWin配置支持窗口管理器(WM)的双缓冲,可以启用它。这通常在GUI_Conf.h中配置WM_SUPPORT_MEMDEV
    • 手动控制重绘:对于极高频的实时数据(如音频波形),可以积累一定数量的点(比如10个)再调用一次GRAPH_DATA_YT_AddValue,并配合定时器控制刷新率,而不是每来一个点就刷新。

2. 内存占用过高:

  • 分析:每个GRAPH控件、数据对象、标尺对象都需要内存。特别是大数据容量的GRAPH_DATA_YT_Create
  • 优化:
    • 精确计算所需的数据点容量,不要盲目设置过大。
    • 及时销毁不再需要的控件和数据对象。如果一个界面有多个图表,切换界面时务必用WM_DeleteWindow删除整个GRAPH控件(它会自动删除附加的数据和标尺)。
    • 考虑使用GRAPH_DATA_XY_Create时,如果点集是静态的,可以传入指向常量数组的指针,避免额外拷贝。

3. 标尺数字显示不全或重叠:

  • 现象:标尺数字被截断或挤在一起。
  • 原因:字体太大、刻度间隔(TickDist)太小、或者标尺位置(Pos)设置不当导致文本跑到控件外或被边框裁剪。
  • 排查:
    • 使用更小的字体GRAPH_SCALE_SetFont(hScale, &GUI_Font6x8)
    • 增大刻度间隔,让数字有足够空间。
    • 调整标尺的Pos值,确保文本在预留的边框区域内。可以通过临时设置一个明显的背景色来调试边框区域。
    • 检查文本对齐方式(TextAlign),确保其适合标尺位置(如左侧标尺用右对齐)。

4. 曲线绘制不正确(直线、错位):

  • 现象:曲线没有按预期连接,或者画在了奇怪的位置。
  • 排查步骤:
    1. 检查数据值:使用GRAPH_DATA_YT_GetValue或调试器,确认添加到数据对象的值是否在预期范围内(0到数据区高度-1)。超出范围的值会被裁剪,但可能导致奇怪图形。
    2. 检查偏移(OffY):确认GRAPH_DATA_YT_SetOffY的设置。一个正的偏移会将整个曲线上移。
    3. 检查数据对象是否成功附加:确保GRAPH_AttachData调用成功,且句柄有效。
    4. 检查GRAPH虚拟尺寸:如果设置了GRAPH_SetVSizeX并启用了滚动,但滚动值不正确,可能导致显示的数据段不对。
    5. 无效数据点:确认数据中是否意外包含了无效值0x7FFF,这会导致曲线中断。

5. 触摸滚动不流畅:

  • 现象:在触摸屏上拖动滚动条时,图表响应迟滞。
  • 优化:
    • 确保在系统定时器中断或高优先级任务中,不要执行长时间的GRAPH重绘操作。
    • 检查是否在重绘过程中进行了复杂的计算(如在UserDraw回调中做浮点运算)。尽量将计算提前。
    • 如果图表非常复杂,考虑在滚动时暂时关闭网格绘制 (GRAPH_SetGridVis(hGraph, 0)) 或提高网格间距,滚动停止后再恢复。

通过以上系统的学习、实战和问题排查经验的积累,你应该能够游刃有余地运用emWin的GRAPH控件,为你的嵌入式产品打造出专业、流畅的数据可视化界面。记住,关键是将“数据-像素”映射关系理清,并善用数据对象和标尺对象的分离式设计,这能让你的图表代码更加模块化和可维护。

http://www.jsqmd.com/news/1050275/

相关文章:

  • 佛山桂城川菜性价比测评榜单|4家夜宵门店实测,好吃实惠不坑人推荐 - 资讯速览
  • Postman+Newman+GitLab+Jenkins接口自动化测试流水线搭建指南
  • 百考通智能化AI,论文降重与去AI痕迹,让学术成果更合规
  • 上海低层临街路面噪音怎么隔音?|静华轩隔音窗|1-6楼直面路面车流人声、临街尘土入户阻隔,洋房自建房低层降噪改造 - 维小达科技
  • 跨省搬家寄大件选哪家?2026省钱攻略来了 - 快递物流资讯
  • 深圳低层临街路面噪音灰尘怎么隔音防尘?|静华轩隔音窗|1-6楼低层车流胎噪、沿街人声阻隔,一楼洋房自建房降噪防尘改造 - 维小达科技
  • 毕业证丢了登报怎么线上办理?正规办理渠道与流程 - 资讯速览
  • 2026年宁波高复学校推荐|TOP5名单,宁波舟山提分首选在这 - 资讯速览
  • 深度解析Audiveris音乐识别:企业级部署完整指南
  • 北京房山离婚律所哪家靠谱:房山区5家专业离婚律所排名榜 - 品牌2026
  • 终极指南:5分钟用游戏手柄控制电脑,Gopher360让您彻底告别键盘鼠标
  • 如何解决BepInEx IL2CPP启动失败:新手必看的完整指南
  • 卫生许可证丢了登报怎么线上办理?合规登报办理方法 - 资讯速览
  • 不动产产权证丢了登报怎么线上办理?登报完整办理流程 - 资讯速览
  • 2026芜湖奢侈品名包名表回收哪家不坑人?全城诚信奢品商家深度对比 - 鸿运名品
  • 2026重庆消防材料供应链企业核心能力评估标准|Top 10排行榜 - 资讯速览
  • 2026年成都小程序定制市场全景解读:头部公司技术实力与服务能力深度对比 - 软件测评师
  • 2026年6月最新爱彼中国官方售后服务热线客服中心地址及网点 - 亨得利官方服务中心
  • 嵌入式GUI开发利器:SEGGER emWin字体转换器实战指南
  • 2026芜湖正规靠谱的奢侈品名包名表回收店推荐:十年口碑老店,闲置奢品回收好评不断 - 鸿运名品
  • 2026寄摩托车哪个物流便宜?跨省机车托运安全又省钱渠道推荐 - 快递物流资讯
  • 嵌入式GUI开发:emWin皮肤定制与多缓冲技术实战解析
  • 2026年众智商学院CPPM试听课适合先看什么?采购基础薄弱怎么入门和8800元费用说明 - 众智商学院官方
  • 道路运输经营许可证丢了登报怎么线上办理?合规登报办理方法 - 资讯速览
  • 吉州大道永新土菜哪家正宗?4家本地人实测 - 资讯速览
  • 汕头旅游选正宗牛肉火锅:杏花吴记的硬核标准解析 - 起跑123
  • 简单量子协议
  • 闲置伯爵首饰怎么变现?上海2026最新回收行情测评 - 奢侈品交易观察员
  • 墙面砖体裂缝剥落砖头墙壁缺陷识别分割数据集labelme格式1300张5类别
  • 2026年6月最新爱彼中国官方售后服务热线客服网点地址电话 - 亨得利官方服务中心