嵌入式GUI开发:emWin GRAPH控件从入门到精通
1. 项目概述为什么嵌入式系统需要GRAPH控件在嵌入式开发领域尤其是涉及人机交互HMI的工业控制、医疗设备、智能家居中控屏等场景数据的直观呈现至关重要。想象一下一个温控器的屏幕上如果只有不断跳动的数字操作员很难快速判断温度变化的趋势而如果有一条平滑的曲线实时描绘温度变化那么过热预警、恒温控制等状态就一目了然了。这就是图表控件的核心价值——将抽象的数据序列转化为直观的视觉图形。emWin作为一款在嵌入式领域久经考验的GUI库其内置的GRAPH控件正是为此而生。它并非一个简单的“画线工具”而是一个完整的、面向对象的图表绘制引擎。很多刚从裸机绘图或简单LCD驱动转向emWin的开发者可能会觉得GRAPH控件API繁多配置复杂远不如自己用GUI_DrawLine画个折线图来得直接。但当你真正处理多曲线、实时滚动、动态缩放、带网格和刻度的专业图表时自己从头实现不仅工作量巨大而且性能、内存管理都会成为噩梦。GRAPH控件将这些复杂性封装起来提供了从数据管理、坐标变换、到渲染优化的一整套解决方案。简单来说GRAPH控件解决了嵌入式GUI数据可视化的三个核心痛点一是性能在有限的CPU和内存资源下高效绘制二是功能集成网格、刻度、滚动、多曲线叠加等专业特性三是可维护性通过对象化的接口使图表逻辑与业务逻辑清晰分离。接下来我将结合自己多年在STM32、NXP等MCU平台上使用emWin的经验带你从零开始彻底吃透GRAPH控件让你不仅能“用起来”更能“用得巧”。2. GRAPH控件核心架构与设计哲学要熟练使用GRAPH控件绝不能仅仅停留在调用API的层面必须理解其内部的对象模型和设计思路。这就像开车只知道踩油门和刹车也能开但了解发动机和变速箱的原理才能应对复杂的路况。2.1 控件结构一个精密的协作系统根据官方手册一个GRAPH控件由三大核心对象构成控件本身Widget、数据对象Data Object和刻度对象Scale Object。这种松耦合的设计是emWin控件体系的精髓。GRAPH控件本体你可以把它理解为一个“画布”或“舞台”。它定义了图表的物理显示区域Data Area、边框Border、背景网格Grid以及滚动条Scrollbar的视觉属性和行为。它负责整体的坐标系管理、窗口消息处理和最终图像的合成。创建控件时你通过GRAPH_CreateEx指定其位置、大小和父窗口。数据对象GRAPH_DATA_YT/XY这是图表的“灵魂”。它独立于控件存在专门负责存储和管理要绘制的数据点。GRAPH_DATA_YT用于最常见的时间序列图Y值随时间T变化每个X坐标位置对应一个Y值而GRAPH_DATA_XY则用于绘制任意坐标点的散点图或函数曲线例如正弦波、轨迹图。数据对象可以被创建、填充数据然后“附着”Attach到GRAPH控件上。一个GRAPH控件可以附着多个数据对象从而实现多条曲线的叠加显示。这种设计的好处是数据更新和图表渲染可以解耦你可以在后台线程准备数据然后通知GUI线程刷新显示。刻度对象GRAPH_SCALE这是图表的“标尺”。它也是一个独立对象用于在图表边缘绘制带有刻度和数值的坐标轴。你可以创建水平或垂直的刻度并附着到GRAPH控件上。刻度对象可以灵活设置字体、颜色、刻度间隔以及数值转换因子例如将像素值转换为实际的电压值“2.5V”。它们之间的关系GRAPH控件作为容器和管理者。你创建好数据对象和刻度对象后通过GRAPH_AttachData和GRAPH_AttachScale将其“挂载”到控件上。控件在绘制时会依次调用这些附着对象的绘制逻辑。当控件被删除时它会自动清理所有附着的对象这大大简化了内存管理。这种“控件-子对象”的模型使得图表的每个部分都可以独立配置和复用非常灵活。2.2 坐标系与渲染流程像素背后的数学理解GRAPH控件的坐标系是精准控制显示效果的关键。这里有两个核心概念虚拟大小Virtual Size和数据区域Data Area。数据区域这是GRAPH控件内部用于绘制曲线和网格的矩形区域其大小由控件创建时的尺寸减去边框Border决定。所有数据点的坐标最终都要映射到这个区域。虚拟大小这是逻辑上的绘图范围。默认情况下虚拟大小等于数据区域的物理像素尺寸。例如数据区域宽300像素那么X轴的虚拟大小就是300数据点的X坐标范围就是0到299。当你有一个包含1000个数据点的序列但只想在300像素宽的区域内显示其中一段时就需要用到滚动。这时你可以通过GRAPH_SetVSizeX将X轴的虚拟大小设置为1000。控件会发现虚拟大小1000大于数据区域宽度300于是自动生成水平滚动条。滚动操作改变的是数据区域相对于整个虚拟画布的“视口”位置。渲染流程是固定的理解它有助于我们使用自定义绘制回调GRAPH_SetUserDraw在合适的时机添加自定义元素用背景色填充数据区域。调用第一次用户绘制回调GRAPH_DRAW_FIRST。此时裁剪区被限制在数据区域内适合绘制自定义背景或底层网格。绘制控件自带的网格如果启用。绘制所有附着的数据对象即曲线。绘制所有附着的刻度对象。调用第二次用户绘制回调GRAPH_DRAW_LAST。此时裁剪区是整个控件区域除边框外适合绘制浮在最上层的标注、标题或自定义刻度。这个流程决定了绘制元素的上下层关系比如你无法在用户绘制回调的第一阶段去覆盖后来绘制的曲线。3. 从零到一创建并配置一个基础图表理论说得再多不如动手写一行代码。我们从一个最经典的需求开始在嵌入式设备上绘制一条实时更新的温度曲线。3.1 环境准备与控件创建首先确保你的emWin库已正确移植到你的目标平台如STM32LTDC或任何带LCD的MCU。通常你需要完成GUI_Init()的初始化。假设我们有一个320x240的屏幕。// 定义句柄 static WM_HWIN hGraph; static GRAPH_DATA_Handle hDataTemp; // 创建GRAPH控件 // 参数x, y, width, height, parent, winflags, exflags, id hGraph GRAPH_CreateEx(10, 10, 300, 150, WM_HBKWIN, WM_CF_SHOW, 0, GUI_ID_GRAPH0);这行代码在坐标(10,10)处创建了一个300x150像素的图表控件并立即显示。WM_HBKWIN是桌面窗口句柄。此时你运行程序会看到一个空白矩形因为还没有数据和任何装饰。3.2 创建并绑定YT数据对象对于温度这种随时间等间隔采样的数据我们使用GRAPH_DATA_YT对象。#define TEMP_DATA_MAX_POINTS 300 // 图表最多显示300个历史点 static I16 s_aTemperatureData[TEMP_DATA_MAX_POINTS]; // 存储原始数据 static int s_DataIndex 0; // 当前数据索引 // 创建YT数据对象 // 参数曲线颜色最大数据点数初始数据数组初始数据个数 hDataTemp GRAPH_DATA_YT_Create(GUI_GREEN, TEMP_DATA_MAX_POINTS, s_aTemperatureData, 0); // 将数据对象附着到图表控件 GRAPH_AttachData(hGraph, hDataTemp);这里有几个关键点颜色GUI_GREEN定义了曲线的颜色。emWin使用24位RGB格式你也可以用GUI_RGB()宏自定义。最大点数这个参数决定了数据对象内部环形缓冲区的大小。当数据点超过这个数量时旧数据会被自动挤出FIFO。这里设为300意味着图表始终显示最新的300个采样点。初始数据我们传入空数组和0表示开始时图表是空的。你也可以预填充一些数据。3.3 配置视觉元素网格、边框与滚动一个光秃秃的曲线可读性很差。我们来添加网格和边框并配置滚动行为。// 1. 启用并设置网格 GRAPH_SetGridVis(hGraph, 1); // 1启用网格 GRAPH_SetGridDistX(hGraph, 50); // 水平网格线间隔50像素 GRAPH_SetGridDistY(hGraph, 20); // 垂直网格线间隔20像素 GRAPH_SetColor(hGraph, GUI_DARKGRAY, GRAPH_CI_GRID); // 设置网格线为深灰色 // 2. 设置边框和背景色 GRAPH_SetBorder(hGraph, 2, 2, 2, 2); // 设置左、上、右、下边框均为2像素 GRAPH_SetColor(hGraph, GUI_BLACK, GRAPH_CI_BK); // 数据区域背景设为黑色 GRAPH_SetColor(hGraph, GUI_LIGHTGRAY, GRAPH_CI_BORDER); // 边框区域背景设为浅灰色 GRAPH_SetColor(hGraph, GUI_WHITE, GRAPH_CI_FRAME); // 数据区域边缘的细框设为白色 // 3. 配置滚动条当数据点超过显示宽度时自动出现 // 设置X轴虚拟大小为500像素。由于数据区域宽度可能小于300当数据点超过显示范围时会自动显示水平滚动条。 GRAPH_SetVSizeX(hGraph, 500); // 启用自动滚动条功能通常默认就是启用的显式设置是个好习惯 GRAPH_SetAutoScrollbar(hGraph, GUI_COORD_X, 1);通过以上设置你的图表已经有了专业的外观深灰色网格衬于黑色背景上一条绿色曲线将绘制其中四周有浅灰色的边框和白色的细线勾勒。当曲线点数超过水平可视范围时底部会出现滚动条。3.4 动态更新数据让图表“活”起来静态图表意义不大GRAPH控件的强大之处在于能高效处理动态数据。我们模拟一个每100ms采样一次温度的线程或定时器中断服务程序。// 假设此函数在定时器中断或任务中被周期调用 void TemperatureSensor_Task(void) { I16 newTemp ReadTemperatureSensor(); // 读取传感器返回原始ADC值或实际温度值 // 将新数据添加到数据对象 GRAPH_DATA_YT_AddValue(hDataTemp, newTemp); // 可选通知窗口管理器进行局部重绘效率高于全屏刷新 WM_InvalidateWindow(hGraph); }GRAPH_DATA_YT_AddValue函数是核心。它会将新值newTemp追加到数据对象中。如果数据已满达到创建时设定的300点它会自动将最旧的一个数据点丢弃左移然后将新点放在末尾。这种“滚动窗口”式的数据管理对于实时波形显示来说非常高效你无需自己管理数据队列。这里有一个非常重要的细节GRAPH_DATA_YT的Y值范围默认映射到数据区域的整个高度。假设数据区域高150像素那么Y值0对应底部Y值149对应顶部。如果你的温度传感器原始ADC值范围是0~4095那么直接添加进去曲线可能会压缩在底部的一小段。因此通常需要进行值域映射。// 更健壮的数据添加函数 void AddTemperatureValue(I16 rawAdcValue) { // 假设ADC范围0-4095对应温度-20°C到80°C我们希望显示在图表上 // 图表数据区域高度为150像素我们想用其中140像素来显示温度上下留5像素边距 #define TEMP_RANGE_MIN (-20) #define TEMP_RANGE_MAX (80) #define CHART_Y_RANGE 140 // 可用像素高度 #define CHART_Y_OFFSET 5 // 顶部偏移 // 将ADC值转换为实际温度假设线性 float temp (rawAdcValue / 4095.0f) * (TEMP_RANGE_MAX - TEMP_RANGE_MIN) TEMP_RANGE_MIN; // 将实际温度映射到像素Y坐标0像素对应TEMP_RANGE_MAX, 140像素对应TEMP_RANGE_MIN // 因为屏幕坐标Y轴向下为正而图表Y轴向上为正 I16 yPixel (I16)((TEMP_RANGE_MAX - temp) / (TEMP_RANGE_MAX - TEMP_RANGE_MIN) * CHART_Y_RANGE) CHART_Y_OFFSET; // 添加映射后的像素值 GRAPH_DATA_YT_AddValue(hDataTemp, yPixel); }这个映射过程是使用GRAPH控件时必须掌握的技巧。你也可以通过GRAPH_DATA_YT_SetOffY来整体平移曲线或者后续通过刻度对象来显示原始物理值。4. 高级功能与深度配置实战基础图表跑通后我们可以探索更高级的功能以满足复杂的项目需求。4.1 添加专业刻度Scale让Y轴显示实际的温度值如“25.5°C”而不是像素坐标。static GRAPH_SCALE_Handle hScaleY; // 创建一个垂直刻度位于控件左侧10像素处文字右对齐 hScaleY GRAPH_SCALE_Create(10, GUI_TA_RIGHT, GRAPH_SCALE_CF_VERTICAL, 30); // 刻度间隔30像素 // 设置刻度因子将像素值转换回温度值 // 根据之前的映射yPixel (TEMP_RANGE_MAX - temp) / ΔTemp * CHART_Y_RANGE OFFSET // 反推temp TEMP_RANGE_MAX - (yPixel - OFFSET) * ΔTemp / CHART_Y_RANGE // 刻度因子Factor表示显示值 像素值 * Factor Offset // 我们需要 显示值 - (像素值 - OFFSET) * (ΔTemp/CHART_Y_RANGE) TEMP_RANGE_MAX // 这可以拆解为 Factor -(ΔTemp/CHART_Y_RANGE), 再结合GRAPH_SCALE_SetOff float tempRange TEMP_RANGE_MAX - TEMP_RANGE_MIN; float factor - (tempRange / CHART_Y_RANGE); // 负号是因为像素增加温度值减少 GRAPH_SCALE_SetFactor(hScaleY, factor); // 设置偏移使得像素值0对应TEMP_RANGE_MAX // 显示值 (0 Off) * factor TEMP_RANGE_MAX Off TEMP_RANGE_MAX / factor int scaleOff (int)(TEMP_RANGE_MAX / factor); GRAPH_SCALE_SetOff(hScaleY, scaleOff); // 设置显示一位小数 GRAPH_SCALE_SetNumDecs(hScaleY, 1); // 设置字体和颜色 GRAPH_SCALE_SetFont(hScaleY, GUI_Font8x16); GRAPH_SCALE_SetTextColor(hScaleY, GUI_WHITE); // 将刻度附着到图表 GRAPH_AttachScale(hGraph, hScaleY);刻度配置是GRAPH控件中最需要数学思维的部分。核心是理解因子Factor和偏移Off的作用显示值 (像素坐标 Off) * Factor。通过巧设Factor和Off可以将内部的像素坐标系映射到任何你想要的工程单位如°C, V, kPa。4.2 实现多曲线对比显示在监控系统中常常需要同时显示多条曲线比如温度、湿度和压力。static GRAPH_DATA_Handle hDataTemp, hDataHumi, hDataPress; // 创建三条不同颜色的数据对象 hDataTemp GRAPH_DATA_YT_Create(GUI_RED, 300, NULL, 0); hDataHumi GRAPH_DATA_YT_Create(GUI_CYAN, 300, NULL, 0); hDataPress GRAPH_DATA_YT_Create(GUI_YELLOW, 300, NULL, 0); // 全部附着到同一个GRAPH控件 GRAPH_AttachData(hGraph, hDataTemp); GRAPH_AttachData(hGraph, hDataHumi); GRAPH_AttachData(hGraph, hDataPress); // 分别更新数据 void UpdateSensorData(I16 temp, I16 humi, I16 press) { GRAPH_DATA_YT_AddValue(hDataTemp, MapToPixel(temp, TEMP_MIN, TEMP_MAX)); GRAPH_DATA_YT_AddValue(hDataHumi, MapToPixel(humi, HUMI_MIN, HUMI_MAX)); GRAPH_DATA_YT_AddValue(hDataPress, MapToPixel(press, PRESS_MIN, PRESS_MAX)); WM_InvalidateWindow(hGraph); }GRAPH控件会自动处理多条曲线的绘制叠加无需开发者干预。为了区分务必使用对比度明显的颜色。如果值域相差很大可能需要为每条曲线配置不同的Y轴刻度这可以通过创建多个垂直刻度对象并设置不同的位置和映射因子来实现但这会稍微复杂一些通常更常见的做法是归一化到同一值域或用双Y轴需要更高级的自定义绘制。4.3 使用XY数据对象绘制函数曲线GRAPH_DATA_XY对象更适合绘制数学函数或非均匀采样的数据。例如绘制一个正弦波#define POINT_NUM 100 static GUI_POINT aSinWave[POINT_NUM]; GRAPH_DATA_Handle hDataSin; // 生成正弦波点集范围在数据区域内 for(int i 0; i POINT_NUM; i) { float x (2 * GUI_PI * i) / (POINT_NUM - 1); // 0到2π aSinWave[i].x i * 3; // X轴拉伸 aSinWave[i].y (I16)(50 * sin(x) 60); // Y轴偏移到中心 } // 创建XY数据对象 hDataSin GRAPH_DATA_XY_Create(GUI_MAGENTA, POINT_NUM, aSinWave, POINT_NUM); GRAPH_AttachData(hGraph, hDataSin); // 可以设置线条样式和笔触大小 GRAPH_DATA_XY_SetLineStyle(hDataSin, GUI_LS_DOT); // 设置为点线 GRAPH_DATA_XY_SetPenSize(hDataSin, 2); // 设置线宽为2像素注意仅对实线有效GRAPH_DATA_XY对象存储的是绝对的(X,Y)坐标点控件会按顺序将它们用直线连接起来。你可以通过GRAPH_DATA_XY_SetOffX/Y来整体平移整个曲线图形。4.4 利用用户绘制回调进行自定义当内置功能无法满足时GRAPH_SetUserDraw提供了强大的扩展能力。比如我们想在图表背景上添加一个代表“安全阈值”的红色水平带状区域。static void _cbUserDraw(WM_HWIN hWin, int Stage) { switch (Stage) { case GRAPH_DRAW_FIRST: // 阶段1在网格和曲线绘制之前在数据区域内绘制 { GUI_RECT Rect; int yTop, yBottom; // 假设安全温度范围是20-30度映射到像素坐标 yTop MapValueToPixel(30, TEMP_RANGE_MAX, TEMP_RANGE_MIN, CHART_Y_RANGE, CHART_Y_OFFSET); yBottom MapValueToPixel(20, TEMP_RANGE_MAX, TEMP_RANGE_MIN, CHART_Y_RANGE, CHART_Y_OFFSET); // 获取数据区域矩形 WM_GetInsideRectEx(hWin, Rect); // 注意这里获取的是窗口客户区数据区可能需要减去边框 // 更准确的做法是计算数据区坐标这里简化处理 Rect.y0 yTop; Rect.y1 yBottom; GUI_SetColor(GUI_RED); GUI_SetAlpha(0x80); // 设置半透明如果底层驱动支持Alpha混合 GUI_FillRectEx(Rect); GUI_SetAlpha(0xFF); // 恢复不透明 GUI_SetColor(GUI_WHITE); } break; case GRAPH_DRAW_LAST: // 阶段2在所有元素绘制之后在整个控件区域绘制 // 可以在这里添加标题文本 GUI_SetFont(GUI_Font16_ASCII); GUI_SetTextMode(GUI_TM_TRANS); // 透明背景模式 GUI_DispStringHCenterAt(Temperature Monitor, 150, 5); // 在控件上方居中显示标题 break; } } // 在创建图表后设置回调 GRAPH_SetUserDraw(hGraph, _cbUserDraw);自定义绘制回调让你几乎可以在图表的任何位置、任何图层添加图形元素极大地提升了灵活性。5. 性能优化与常见问题排查在资源紧张的嵌入式设备上使用GRAPH控件性能和内存是需要重点关注的问题。5.1 内存与性能优化策略数据点数量与刷新率这是最直接的影响因素。GRAPH_DATA_YT_Create中分配的内存与MaxNumItems成正比。不要盲目设置一个很大的值比如10000点除非你真的需要显示这么长的历史数据。对于实时波形300-500点通常足以形成平滑曲线。同时高刷新率如60FPS会持续触发重绘消耗CPU。应根据实际需求降低刷新率例如只有在新数据到达时才调用WM_InvalidateWindow。关闭不必要的功能网格、特别是非实线样式的网格如虚线GUI_LS_DASH会显著增加绘制时间。在性能敏感的场合考虑关闭网格GRAPH_SetGridVis(hGraph, 0)或增大网格间距。边框和用户自定义绘制回调也会增加开销。虚拟大小与滚动优化如果设置了很大的虚拟大小GRAPH_SetVSizeX但只显示一小部分控件内部仍需要管理整个逻辑空间。只设置必要的虚拟大小。对于无限滚动的实时数据可以采用“视口跟随”策略当数据添加到末尾时自动调整滚动值让视图始终锁定在最新数据。void AddValueAndScroll(GRAPH_Handle hGraph, GRAPH_DATA_Handle hData, I16 val) { GRAPH_DATA_YT_AddValue(hData, val); int vSizeX GRAPH_GetVSizeX(hGraph); int dataWidth ... // 获取数据区域宽度 // 如果数据点数超过显示宽度则滚动到最右端 if (vSizeX dataWidth) { GRAPH_SetScrollValue(hGraph, GUI_COORD_X, vSizeX - dataWidth); } WM_InvalidateWindow(hGraph); }使用内存设备Memory Device如果图表区域频繁更新且闪烁严重可以考虑使用emWin的内存设备WM_CreateMemoryDevice进行双缓冲。将GRAPH控件创建在内存设备窗口上所有绘制先在内存中完成然后一次性刷到屏幕可以完全消除闪烁。5.2 常见问题与解决方案速查表下表总结了我在实际项目中踩过的一些“坑”及其解决方法问题现象可能原因解决方案曲线不显示或显示不全1. 数据未正确映射到数据区域像素范围。2. 数据对象的Y值超出了数据区域高度0 到 ySize-1。3. 数据对象未成功附着GRAPH_AttachData调用失败或未调用。1. 检查数据映射公式使用GUI_DispDecAt等函数打印几个关键数据点的像素坐标进行调试。2. 确保Y值在有效范围内。对于GRAPH_DATA_YTY0对应底部YySize-1对应顶部。3. 检查GRAPH_AttachData的返回值并确保在WM_PAINT消息能到达之前完成附着。网格或刻度不显示1. 网格可见性未启用GRAPH_SetGridVis。2. 网格颜色与背景色相同。3. 刻度对象创建失败或未附着。4. 刻度位置Pos设置不当导致刻度画在控件外部。1. 确认调用GRAPH_SetGridVis(hGraph, 1)。2. 使用GRAPH_SetColor设置GRAPH_CI_GRID为对比明显的颜色。3. 检查GRAPH_SCALE_Create的返回值并调用GRAPH_AttachScale。4. 调整GRAPH_SCALE_Create的Pos参数或后续使用GRAPH_SCALE_SetPos。滚动条不出现或无法滚动1. 虚拟大小GRAPH_SetVSizeX/Y未设置或设置得不大于数据区域尺寸。2. 自动滚动条功能被关闭GRAPH_SetAutoScrollbar(..., GUI_COORD_X, 0)。3. 数据对象的数据量确实未超过可视范围。1. 确保虚拟大小大于数据区域的实际像素尺寸。例如数据区宽300要滚动需设置GRAPH_SetVSizeX(hGraph, 500)。2. 确认自动滚动条启用。3. 检查添加到数据对象的数据点数量是否足够多。屏幕闪烁严重1. 数据更新和重绘频率过高。2. 未使用任何双缓冲机制直接绘制到显存。1. 限制刷新频率例如每收集到10个新点才重绘一次。2. 启用窗口管理器自动重绘WM_SetCreateFlags(WM_CF_MEMDEV)或为GRAPH控件单独使用内存设备。多条曲线重叠无法区分1. 曲线颜色太接近。2. 曲线值域相差巨大一条曲线被压缩成直线。1. 为每条曲线选择差异明显的颜色红、绿、蓝、黄、青、紫。2. 考虑对每条曲线进行归一化处理使其适应同一Y轴范围或使用多个Y轴刻度需要复杂自定义。自定义绘制的内容被覆盖GRAPH_SetUserDraw回调的绘制阶段Stage选择错误。记住绘制顺序FIRST阶段在背景之后、网格之前绘制适合画背景色块LAST阶段在所有元素网格、曲线、刻度之后绘制适合画前景文本和标记。使用GRAPH_DATA_XY时线段连接顺序错乱GUI_POINT数组中的点未按X坐标排序。GRAPH_DATA_XY按照点数组中的顺序依次连接。确保你的点集是按X坐标或你希望的连接顺序排列的。对于函数图先生成有序的点数组再创建对象。5.3 调试技巧与心得分步构建法不要试图一次性配置完所有功能。先从创建一个空白GRAPH控件开始运行看看窗口是否出现。然后添加一条静态曲线再添加动态更新接着加网格、刻度最后加自定义绘制。每步都验证能快速定位问题。善用GUI_DispStringAt调试在怀疑数据映射出错时直接在屏幕固定位置打印出原始传感器值、计算后的像素值、虚拟大小等关键变量是最直接的调试手段。理解坐标系统时刻牢记两个坐标系屏幕坐标系原点在左上角和图表数据坐标系原点在数据区域左下角Y轴向上。GRAPH_DATA_YT_SetOffY和GRAPH_SCALE_SetOff的偏移操作都是在数据坐标系内进行的。内存泄漏检查虽然GRAPH控件在删除时会自动删除附着的子对象但如果你手动GRAPH_DetachData或GRAPH_DetachScale了就必须手动调用对应的Delete函数来释放内存。在长时间运行的系统里这可能是内存缓慢泄漏的根源。