资讯动态

ChartCtrl老图表控件VS2015升级与MFC集成实战

发布时间:2026/10/17 16:21:24 来源:尧图企业网站定制
简介ChartCtrl 是早期经典 MFC 图表控件源码经作者优化后可在 VS2015 下直接编译使用适合需要快速集成曲线、柱状、甘特等图表的桌面软件开发者也适合学习 GDI 绘图与自定义控件封装的中级 MFC 程序员。压缩包共 86 个文件其中 41 个头文件与 32 个 cpp 源文件构成完整控件库另有 inl 内联实现、工程配置与示例程序等整体仅 154KB轻量易读。源码涵盖坐标轴、游标、滚动条、图例、多种序列类型以及渐变、气球提示等辅助组件结构清晰且模块划分明确便于按需裁剪或继续美化界面。演示工程 ChartDemo 展示了基本调用方式可直接运行验证效果工程配置同时保留了新旧两种格式对照学习颇为方便。该资源已有 1007 人学习适合作为二次开发基础或 MFC 绘图控件的参考实现。1. ChartCtrl一个老图表控件的价值比你想的更大接手一个维护了十几年的工业上位机项目时你会发现图表显示永远是最难动的部分。客户要实时曲线老板不让换框架而你面前是一套VC6时代就写好的ChartCtrl源码。这套控件确实“老”——代码风格停留在#ifdef和new满飞的年代但它能跑、稳定、逻辑完整而且已经被前人优化到VS2015能直接编译通过。这篇文章就围绕这套优化后的ChartCtrl源码展开它是什么、怎么编译、怎么集成、有什么坑、还能怎么改。适合两类人一类是要在老项目上做新功能的工程师另一类是初学MFC绘图机制、想读一套完整图表代码的开发者。把这个老控件吃透你在曲线绘制这条路上能少走很多弯路。2. 拿到源码后的第一件事目录结构与工程升级2.1 源码目录里都有什么先看清再动手老款ChartCtrl的源码通常是一组互相依赖的类而不是一个单文件控件。核心文件包括ChartCtrl.h/cpp负责控件整体ChartAxis.h/cpp管坐标轴ChartBase是基类ChartLineSerie和ChartPointSerie负责曲线和散点还有一些辅助文件比如ChartElement、ChartTitle。你打开工程目录后别急着编译先把文件依赖理清楚。常见的结构是这样ChartCtrl/ ├── ChartCtrl.h ├── ChartCtrl.cpp ├── ChartBase.h ├── ChartBase.cpp ├── ChartAxis.h ├── ChartAxis.cpp ├── ChartLineSerie.h ├── ChartLineSerie.cpp ├── ChartPointSerie.h ├── ChartPointSerie.cpp ├── ChartRes.h └── readme.txt这里的ChartRes.h里可能定义控件消息、颜色常量、默认字体。老代码喜欢把所有资源相关的东西集中在一个头文件里方便#include。你需要注意ChartCtrl.cpp里通常会#include ChartCtrl.h以及其他几个 Serie 头文件这些#include的顺序不能乱因为老代码里可能存在隐式依赖——比如ChartAxis类里调用了ChartCtrl的成员函数但并没有 include 完整定义而是靠前向声明。我一般建议先完整编译一遍再看代码。如果编译通过说明你拿到的已经是“优化过”的版本如果报错大概率是工程文件里的平台工具集和 Windows SDK 版本不对这一步会在下一节处理。编译之前先确认你的 VS2015 已经安装了“VC 工具集”和“MFC 支持和 ATL 支持”这两个可选组件缺了直接编译不过。2.2 从 VC6/2005 工程升级到 VS2015向导与手动改法老版 ChartCtrl 的工程文件可能是.dspVC6或.vcprojVS2005/2008。用 VS2015 直接双击打开.dspIDE 会提示你转换为.vcxproj。这个转换向导大多数时候能成功但转换后经常出现两个问题一是平台工具集变成了v140但某个源文件的编辑模式还带着旧编译器指令二是 Windows SDK 版本默认选成 8.1而老代码里调用了微软已废弃的 APISDK 版本不对直接爆一堆C2065未声明标识符。所以更可靠的做法不是我懒得点向导而是直接新建一个 MFC 动态链接库工程把.cpp和.h手动加进去。这样做的好处是工程文件完全受控不会带出一堆历史垃圾配置。新建工程的配置如下// 新建工程向导里的关键选项 // 应用程序类型DLL // MFC 使用在共享 DLL 中使用 MFC // 项目样式使用 MFC 的 DLL不选 Automation // 附加选项不推荐加保持空工程建好后把源文件添加现有项加进来。先别点生成你要做三件事。第一在项目属性 - 常规 - 字符集中把“字符集”改为“使用多字节字符集”。老控件里大量使用char和CString的隐式转换如果你用 Unicode 字符集CString的Format和sprintf混用会翻车。第二在 C/C - 预处理器 - 预处理器定义中加上_CRT_SECURE_NO_WARNINGS老代码里strcpy、sprintf在 VS2015 下会警告加了这个可以让你把精力放到真正影响功能的错误上。第三查看ChartCtrl.cpp顶部是否有#pragma comment(lib, gdi32.lib)之类的语句如果没有在项目属性 - 链接器 - 附加依赖项里补上gdi32.lib。代码层面最常见的改造是头文件的#include路径。老代码可能会写#include afxwin.h这在 VS2015 里没问题。问题出在一些过时的写法比如#include afxcmn.h不是必需的。而ChartAxis.cpp里如果用了GetEnhMetaFile这类 GDI 函数记得在stdafx.h里包含wingdi.h否则编译器认为GetEnhMetaFile未声明。我见过有人把这些函数用::前缀重写其实没必要加个头文件就行。2.3 编译通过的标准Debug/Release 下零警告的配置“优化为 VS2015”不是能编译就算完你要追求 Debug 和 Release 两个配置下都零警告。老代码里常见的警告有两类一类是C4244类型转换丢精度比如把double赋给int另一类是C4100未引用的形参。处理这两类问题有个原则能通过显式转换解决的就用显式转换不要用#pragma warning(disable)一关了之。比如这段典型代码// ChartAxis.cpp 里常见的计算 double dStep (max - min) / m_nTickCount; int nPixel (int)(dStep * m_pCtrl-GetRect().Width()); // C4244 警告这里dStep是 double乘了Width()返回的 int 后结果赋给 int会警告。正确的改法是int nPixel static_castint(dStep * static_castdouble(m_pCtrl-GetRect().Width()));把乘数先转成 double 再乘最后统一 cast 成 int这样警告消失逻辑不变。顺便说一句如果你在代码里看到一堆(int)旧式 C 风格转换可以顺手改成static_cast但要改完立刻编译别大面积重构。Release 配置下的特殊点是要关闭/ZI调试信息并开启/O2优化。老代码可能在#pragma optimize里做了特殊处理比如为了配合断点而#pragma optimize(g, off)。如果你拿到了带这种 pragma 的源码建议把它注释掉否则 Release 版性能会被拖后腿。让编译输出窗口里 0 warning、0 error再到 Debug 和 Release 下跑一个最小绘制测试确认控件能弹出窗口、能画线这一章才算过了。3. 把 ChartCtrl 集成进自己的 MFC 程序最小接入步骤3.1 静态链接还是动态链接两种方式的选择你手头有 ChartCtrl 源码集成方式有两种常见做法。第一种是把 ChartCtrl 的.cpp文件直接加入你的应用程序工程这样源码和你的程序一起编译调试最方便但是每次都要带这些文件。第二种是把 ChartCtrl 编译成一个静态库.lib然后在你的程序里链接它。静态库更干净你只关心暴露出来的接口但需要单独维护库工程。我一般建议如果 ChartCtrl 只在一个程序里用而且你可能要修改它的绘图逻辑比如加十字光标、改颜色直接加入工程。如果多个系统都用它建成静态库。动态链接 DLL 其实不太推荐因为 MFC 的 DLL 规则多CChartCtrl 类导出需要AFX_EXT_CLASS宏老代码里未必写全。无论哪种方式你需要知道CChartCtrl的构造和析构。老控件通常不需要你管理内存它内部维护一个序列数组析构时自动 delete。你只需要在对话框类里声明一个CChartCtrl成员变量即可。注意CChartCtrl不是CWnd派生的吗它是。所以它可以直接作为对话框上的控件也可以通过Create动态创建。3.2 在对话框上放 ChartCtrl 的完整步骤最常见的是在对话框模板里放一个自定义控件然后把控件类绑定为CChartCtrl。假设你的对话框资源里有一个 ID 为IDC_CHART的 Custom Control你在对话框的头文件里声明class CMyDlg : public CDialogEx { public: CChartCtrl m_chart; // 成员控件对象 };然后在DoDataExchange里绑定void CMyDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_CHART, m_chart); }DDX_Control会让m_chart和资源上的控件窗口句柄关联。这里有个关键点CChartCtrl能不能直接用 DDX取决于它的类是否实现了PreSubclassWindow或SubclassWindow。老 ChartCtrl 一般是可以的因为它继承了CWnd而且Create用的类名已经在GetSuperWndProcAddr里注册。如果你发现DDX_Control后控件没有绘制那就改用CChartCtrl自己提供的Create方法// 在 OnInitDialog 里手动创建 CRect rc(10, 10, 500, 300); m_chart.Create(WS_CHILD | WS_VISIBLE, rc, this, 1001);注意Create参数里的控件 ID。如果你用 DDXID 就是资源里的 ID如果手动创建ID 随便给一个不和现有控件冲突的整数。创建完控件后下一步是设置坐标轴和加入曲线。3.3 第一张曲线SetDataCore 与刷新流程ChartCtrl 的接口命名有点像老式 C曲线的载体叫CChartLineSerie。你先创建序列然后设置它的数据点。典型代码// 往 m_chart 里添加一条折线序列 CChartLineSerie* pLine new CChartLineSerie(); m_chart.AddLineSerie(pLine); // 控件接管该指针 pLine-SetName(_T(温度曲线)); pLine-SetColor(RGB(255, 0, 0)); // 设置数据点坐标 pLine-SetData(0, atof(10.5)); // x0, y10.5 pLine-SetData(1, atof(12.3)); pLine-SetData(2, atof(11.8));SetData有两个参数索引和值。这个索引是点序号不是 x 坐标。如果你要自定义 x 轴刻度需要再调用SetXData。很多第一次用的同学都栽在这里以为第一个参数是 x实际它是点的顺序索引x 坐标默认是从 0 增长的整数。数据填充完后调用刷新m_chart.ResetAll(); // 重新计算坐标轴范围如果之前有旧数据 m_chart.Invalidate();Invalidate会触发OnDraw控件内部会遍历所有序列的每个点计算最小最大值然后绘制。注意老控件的刷新是全量刷新数据点上千时会明显卡顿。后面第 6 章我会讲怎么改成局部刷新区。如果你要显示实时曲线常见做法是启动一个定时器每次追加数据后只改最后一个点的值或者追加新点然后调用Invalidate。但这里有个坑老 ChartCtrl 在实时模式下会始终重算坐标轴范围导致曲线左右跳动。解决办法是固定坐标轴范围代码里调SetFixedXAxis之类的函数具体名称要看你的源码版本。我拿到的这个优化版里CChartCtrl提供了SetXAxisPos和SetXAxisRange可以手动锁定范围。4. ChartCtrl 的绘图机制坐标映射与双缓冲4.1 坐标系统数据坐标到窗口坐标的换算理解 ChartCtrl你绕不开坐标映射。内部最核心的函数是GetDataRect()它返回的是窗口内真正用来画图的矩形区域默认是控件客户区减去四周留白。数据坐标到窗口像素的换算就在CChartCtrl::DrawData里大致逻辑是// 伪代码实际在 ChartCtrl.cpp 内部 double dX (x - m_dMinX) / (m_dMaxX - m_dMinX); long nLeft rcData.left (long)(dX * rcData.Width());这里的m_dMinX和m_dMaxX是当前 x 轴的最小最大值。每次ResetAll或添加新数据控件都会重新计算它们。所以在写扩展功能时你要读取这两个值而不是自己在外部重新算。不少老代码的坐标变换用的是浮点乘再加这个没问题但要注意rcData.Width()的返回值类型是int和浮点混合时容易出C4244警告。我建议把矩形宽高先存成doubledouble nW static_castdouble(rcData.Width()); double nH static_castdouble(rcData.Height());然后在做像素坐标时先算浮点结果最后long转换。这样既避免警告也避免大数值时浮点溢出导致画线错位。坐标映射的边界坑是“零范围”。当你的数据点全是同一个 y 值时m_dMaxY - m_dMinY等于 0除法就会产生无穷大。老控件里有对这情况的处理通常是强制设一个最小范围。你可以自己加一段保护代码比如if (m_dMaxY - m_dMinY 1e-10) { m_dMinY - 1.0; m_dMaxY 1.0; }否则你的图像会在垂直方向“消失”。4.2 双缓冲与 OnDraw 的刷新陷阱老版本 ChartCtrl 最初是直接往窗口 DC 绘图的导致曲线在刷新时严重闪烁。优化到 VS2015 时的最大改动之一就是加入了双缓冲。双缓冲的标准做法是在OnDraw函数里创建一个内存 DC把景象先画到内存位图再一次BitBlt到窗口。你看到的优化版源码里应该有一个CMemDC类或者直接用了 GDI 的CreateCompatibleDC。void CChartCtrl::OnDraw(CDC* pDC) { // 双缓冲绘制 CRect rcClient; GetClientRect(rcClient); CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); // 先填充背景 memDC.FillSolidRect(rcClient, m_crBackColor); // 绘制坐标轴、网格、曲线 DrawAxis(memDC); DrawGrid(memDC); DrawSeries(memDC); // 一次性拷贝到窗口 pDC-BitBlt(rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); }这段代码逻辑清晰但有几个隐藏问题。第一内存位图每次OnDraw都重新创建频繁刷新时会反复分配释放内存性能并不好。优化做法是把位图作为控件成员变量只在OnSize里重新创建。第二如果DrawSeries里调用了任何触发Invalidate或UpdateWindow的代码就会产生递归绘制严重时导致堆栈溢出。需要注意CChartLineSerie的SetColor之类的函数里如果带了Invalidate就不要在DrawSeries过程中调用。4.3 曲线、坐标轴、网格的绘制优先级绘制顺序决定视觉层次。ChartCtrl 的绘制顺序一般先画背景再画坐标轴和网格最后画序列。这个顺序不能乱否则网格线会盖在曲线上面。我拿到的优化版里DrawGrid有一个m_bShowGrid开关默认是关的。如果你开了网格注意网格线的颜色要和背景有对比但不要太深。代码里通常写memDC.SelectObject(penGrid); memDC.MoveTo(nGridX, nTop); memDC.LineTo(nGridX, nBottom);画完网格后DrawSeries里遍历每个序列。每一种序列类型都有自己的绘制函数比如CChartLineSerie::Draw它内部由pDC-Polyline把点连接起来。这里有第三个坑Polyline要求点数至少 2如果序列里只有 1 个点老代码可能什么都不画而你的用户只会看到一条空曲线。解决办法是在单点情况下画一个点标记比如Ellipse。优先级还有一个体现序列之间可能重叠。比如你加了两个CChartLineSerie后面加载的颜色和前面可能相互遮挡。老控件没有 Z 序管理只能按加入顺序后画的覆盖先画的。所以你需要在设置序列时就安排好绘制顺序别指望控件提供BringToFront方法。5. 常见问题与排查老代码在新编译器下的 5 个坑5.1 error C2440static_cast 无法从 void* 转换这是升级到 VS2015 碰到的第一类高频错误。现象编译ChartCtrl.cpp时报错error C2440: static_cast: 无法从 void * 转换为 CChartLineSerie *。原因是老代码里用CChartLineSerie *pLine static_castCChartLineSerie*(GetItemData(index))之类的方式取序列指针而GetItemData返回void*老编译器允许这种强转新编译器在严格模式下不允许。原因在于void*到具体类型指针的static_cast是隐式转换的新版本要求必须先转成DWORD_PTR或使用安全类型。解决方法是分两步// 先取 DWORD_PTR再转指针 DWORD_PTR dwPtr GetItemData(index); CChartLineSerie* pLine reinterpret_castCChartLineSerie*(dwPtr);或者干脆用CChartLineSerie**的二级指针存数组很多老代码原本就是这么设计的。你在改的时候要检查所有GetItemData和SetItemData的使用点统一修正不要只改报错的那一行。5.2 未声明的标识符 GetEnhMetaFile 和 DeleteEnhMetaFile现象编译ChartAxis.cpp时出现error C2065: GetEnhMetaFile: 未声明的标识符。原因VS2015 的 Windows SDK 默认将WINVER设置为0x0601Win7而GetEnhMetaFile在wingdi.h里被#if (WINVER 0x0400)保护照理说应该声明。但问题是老代码可能在stdafx.h里没有包含wingdi.h而afxwin.h不一定传递包含它。另一个原因是WINVER被工程设置得低于 0x0400。解决在stdafx.h里显式包含#define WINVER 0x0601 #define _WIN32_WINNT 0x0601 #include afxwin.h // 或按原顺序保留 #include wingdi.h // 补充如果.cpp文件里直接用了GetEnhMetaFile而不想改stdafx.h也可以在这个.cpp文件顶部加#include wingdi.h。这类 GDI 函数在老控件里主要是为了加载曲线图元文件而这种功能实际用的人很少如果找不到合理用处直接注释掉相关调用也可以。5.3 中文乱码或文字不显示现象坐标轴标题或者序列名称明明是中文绘制出来却是乱码或者干脆空白。原因老控件绘制文字时用的字体是创建好的CFont但创建时没有指定字符集。VS2015 默认工程字符集是 Unicode而老 ChartCtrl 可能用CString的宽字符转多字节再传给 GDI导致乱码。解决方法是统一使用CString和 GDI 的宽字符版本// 创建字体时指定字符集 CFont font; font.CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T(宋体));同时确认在DrawTitle或DrawAxisLabel里使用的是pDC-TextOut而不是pDC-ExtTextOut后手动转换编码。如果CChartCtrl::SetTitle接受的是const char*而你的程序传的是CStringW需要先转成多字节。一个稳妥办法是程序中统一用CStringA来传标题给老控件但这样英文没问题中文就要保证源代码文件保存为 UTF-8 且带 BOM。我踩过的教训是VS2015 会自动把无 BOM 的源文件按系统代码页GBK读取所以直接用简体中文写死的字符串反而没问题如果你从远程拉下来的源码是 UTF-8 无 BOM中文就会乱码。解决方案是用记事本把源文件转为带 UTF-8 BOM 格式#pragma execution_character_set(utf-8)虽然有用但不如改文件编码省心。5.4 控件无响应或者不刷新现象控件在对话框上显示出来但拖动窗口或改变大小时图形不更新或者鼠标悬停时控件变成一块白板。原因绝大多数是消息映射缺失。老 ChartCtrl 的OnPaint里没有调用OnDraw或者OnEraseBkgnd返回了 TRUE 但OnPaint没有被正确触发。另一个常见原因是控件窗口类被错误注册导致OnSize消息没有传到控件。解决检查ChartCtrl.cpp的消息映射BEGIN_MESSAGE_MAP(CChartCtrl, CWnd) ON_WM_PAINT() ON_WM_ERASEBKGND() ON_WM_SIZE() END_MESSAGE_MAP()如果少了ON_WM_PAINT()那 Windows 会调用默认的DefWindowProc不触发你的绘制逻辑。另外如果你把控件放在对话框上但对话框的OnInitDialog里调用了m_chart.SetBackColor(...)之后立即Invalidate而控件窗口还没创建完成就会失效。确保所有对控件的操作都在DDX_Control或Create之后最好在OnInitDialog的末尾统一初始化。5.5 性能卡顿数据点上千后拖动鼠标延迟明显现象加了一个 10000 点的曲线每次刷新都卡上半秒CPU 占用率 100%。原因老控件没有增量绘制机制每次Invalidate全窗口重绘Polyline用 GDI 把每个点都画一遍再加上坐标轴重算和网格重绘计算量很大。解决思路有三个。第一减少无效刷新用InvalidateRect只刷新需要变化的区域比如新增数据点的局部区域而不是整个控件。第二减点采样在数据量超过阈值比如 5000 点时强制对数据进行等间距抽稀只保留趋势特征点这能让绘制速度成倍提升。第三把网格绘制和坐标轴绘制从每帧重绘中剔出来只画一次到背景位图曲线单独一层内存位图绘制曲线时只BitBlt曲线层。这个分层技巧是老 ChartCtrl 能做但默认没做的我在第 6 章会展开讲。6. 让老控件继续进化扩展功能与性能调优技巧6.1 加一个十字光标跟踪老 ChartCtrl 没有鼠标交互功能但它提供了坐标变换函数你可以自己加。在控件类里捕获WM_MOUSEMOVE拿鼠标像素坐标反算出数据坐标再画一条十字线// 在 ChartCtrl.h 中添加消息处理声明 afx_msg void OnMouseMove(UINT nFlags, CPoint point); // 在 ChartCtrl.cpp 中 void CChartCtrl::OnMouseMove(UINT nFlags, CPoint point) { // 先算数据坐标 CRect rcData GetDataRect(); double dX m_dMinX (point.x - rcData.left) * (m_dMaxX - m_dMinX) / rcData.Width(); double dY m_dMaxY - (point.y - rcData.top) * (m_dMaxY - m_dMinY) / rcData.Height(); m_dCrossX dX; // 保存十字光标的数据坐标 m_dCrossY dY; // 只刷新一个小区域避免全图闪烁 InvalidateRect(rcData); // 这里可以优化为局部刷新十字线区域 CWnd::OnMouseMove(nFlags, point); }然后在OnDraw的双缓冲绘制代码末尾、BitBlt之前加一段画十字线的代码。注意画线时要考虑坐标轴边界限制在rcData范围。这个功能加完后你就能看到鼠标移动时实时显示数据坐标项目演示效果会好很多。6.2 用双缓冲 局部刷新增强实时曲线实时曲线每秒钟刷新 10 次如果每次全量重绘CPU 一定吃紧。局部刷新的思路是曲线动画新增点通常只影响最右侧一列像素所以你可以把客户区划分成“历史区”和“新点区”。每次刷新时先用BitBlt把历史区位图整体左移一像素然后在最右侧新点亮一个点。这个过程需要把ChartCtrl的绘制拆成两段背景和坐标轴只画一次到静态位图曲线部分单独一层。你可以在类里加一个成员位图CBitmap m_bmpBackground; // 只画背景和网格 int m_nCurRight; // 记录当前右边界刷新时把背景位图直接贴到内存 DC再依次画每条曲线的最后一段。这种做法能把实时刷新的 CPU 占用降 60% 以上。我测试过一个场景1000 点实时曲线每秒刷新 20 次原来全量重绘需要 50ms局部优化后只要 8ms。要点是只有在新增点时用局部刷新一旦坐标轴范围发生整体变化仍要回退到全量重绘否则会出现拖影。6.3 从 ChartCtrl 迁移到现代图表库之前值得抄什么如果你未来打算把老 ChartCtrl 替换成更现代的图表库先别急着丢掉它。ChartCtrl 里最值得借鉴的是它的坐标轴管理和序列组织方式它把数据窗口DataRect和坐标映射算法单独封装这个设计很干净很多现代库反而把坐标映射写死在渲染循环里。第二个值得抄的是它的字体资源管理CChartElement基类里维护了一份全局字体命名数组避免重复创建 GDI 对象。第三个是它有“自动范围”和“固定范围”两种模式切换逻辑清晰这是实时监控系统的刚需。我的习惯是保留 ChartCtrl 的坐标映射算法把它提炼成独立的数学工具类然后在新的绘图框架里直接复用。这样即使 UI 层换掉底层的数据变换逻辑也经过了老项目的千锤百炼。这个思路在我在某大型设备监控系统里验证过——老控件的数据核心用了一个星期就稳定跑起来了而新界面花了三个月。最后说个踩过的具体教训调 ChartCtrl 时不要盲目打开“优化”选项VS2015 的-fp:fast浮点优化会让坐标映射出现微小偏移在长时间运行后数据轴位置漂移几个像素。正确做法是保持-fp:precise默认值。把这些技巧都实践一遍后你会发现这个老控件不但能用而且好用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑