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

📅 2026/6/20 19:34:01
嵌入式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_CreateIndirect和GRAPH_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_SetScrollValue和GRAPH_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); // 在Y100像素处画一条绿色参考线 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队列。这个机制非常适合实现固定长度的实时滚动波形。pItems和NumItems: 允许在创建时传入一批历史数据。如果是从零开始实时添加可以传入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 // 但更简单的思路设置偏移让计算后的Y0点位于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 数据对象管理的最佳实践生命周期管理牢记“谁创建谁销毁”的原则。但GRAPH控件提供了便利通过GRAPH_AttachData附加的对象其生命周期将由GRAPH控件托管。只有当需要提前销毁数据对象或者GRAPH销毁后你还需要数据对象时才先调用GRAPH_DetachData再调用GRAPH_DATA_YT/XY_Delete。多曲线显示一个GRAPH控件可以附加多个数据对象实现多条曲线的叠加显示。只需创建多个数据对象分别设置不同的颜色然后依次附加即可。性能考量数据对象容量 (MaxNumItems) 不宜盲目设置过大。尤其是在低RAM的MCU上每个I16数据点占2字节每个GUI_POINT占4字节两个I16。一个1000点的XY数据对象就需要4KB RAM。同时重绘大量数据点也会消耗CPU时间。需要根据显示区域大小和刷新率权衡。无效数据的使用对于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_HORIZONTAL和GRAPH_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); // 因子 期望单位 / 像素 // 现在如果TickDist50像素那么刻度数字间隔就是 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显示在Y100像素处中间 // 那么标尺的零点也对应像素坐标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 系统设计与参数计算需求定义显示区域GRAPH控件大小设为400像素宽200像素高。时间范围X轴显示最近300秒。温度范围Y轴显示-10°C 到 50°C。刷新率1秒添加一个数据点曲线向左滚动。GRAPH布局规划控件位置GRAPH_CreateEx(40, 20, 400, 200, ...)。边框左留50像素给Y轴标尺下留30像素给X轴标尺。GRAPH_SetBorder(hGraph, 50, 10, 10, 30)。网格显示网格间距设为X40像素对应约30秒Y25像素对应约5°C。GRAPH_SetGridDistX(hGraph, 40); GRAPH_SetGridDistY(hGraph, 25);。数据对象规划容量每秒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) )不更简单的方法是在添加数据时进行转换。我们选择在添加值时转换。标尺规划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像素(20010) GUI_TA_HCENTER | GUI_TA_TOP, 40); // 刻度间隔40像素 if (hScaleX) { GRAPH_AttachScale(hTempGraph, hScaleX); // 设置因子像素 - 秒。总时间300秒数据区宽400像素假设无水平滚动虚拟尺寸物理尺寸 // 但注意我们的GRAPH宽度是400但左border50右border10数据区实际宽400-50-10340像素。 // 标尺是基于数据区宽度计算的我们需要用数据区宽度。 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-30160像素 // 仔细计算GRAPH高200上border10下border30数据区高200-10-30160像素。 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映射到像素16050°C映射到像素0。这样标尺因子0.375在像素0处显示0在像素160处显示60。 // 但我们想要-10和50。所以需要修改转换函数_TempToPixel让-10对应像素16050对应像素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. 曲线绘制不正确直线、错位现象曲线没有按预期连接或者画在了奇怪的位置。排查步骤检查数据值使用GRAPH_DATA_YT_GetValue或调试器确认添加到数据对象的值是否在预期范围内0到数据区高度-1。超出范围的值会被裁剪但可能导致奇怪图形。检查偏移(OffY)确认GRAPH_DATA_YT_SetOffY的设置。一个正的偏移会将整个曲线上移。检查数据对象是否成功附加确保GRAPH_AttachData调用成功且句柄有效。检查GRAPH虚拟尺寸如果设置了GRAPH_SetVSizeX并启用了滚动但滚动值不正确可能导致显示的数据段不对。无效数据点确认数据中是否意外包含了无效值0x7FFF这会导致曲线中断。5. 触摸滚动不流畅现象在触摸屏上拖动滚动条时图表响应迟滞。优化确保在系统定时器中断或高优先级任务中不要执行长时间的GRAPH重绘操作。检查是否在重绘过程中进行了复杂的计算如在UserDraw回调中做浮点运算。尽量将计算提前。如果图表非常复杂考虑在滚动时暂时关闭网格绘制 (GRAPH_SetGridVis(hGraph, 0)) 或提高网格间距滚动停止后再恢复。通过以上系统的学习、实战和问题排查经验的积累你应该能够游刃有余地运用emWin的GRAPH控件为你的嵌入式产品打造出专业、流畅的数据可视化界面。记住关键是将“数据-像素”映射关系理清并善用数据对象和标尺对象的分离式设计这能让你的图表代码更加模块化和可维护。