资讯动态

MFC连接MySQL数据库:ODBC配置与增删改查实战指南

发布时间:2026/10/9 15:42:46 来源:尧图企业网站定制
简介面向需要在MFC应用中集成MySQL数据库的C开发者这份压缩包以ODBC方式打通数据库连接链路围绕驱动安装、系统DSN创建、CDatabase/CRecordset封装、SQL执行与结果集遍历展开并给出异常处理与事务管理思路适合初学MFC数据库编程或需要快速搭建数据库交互界面的读者参考。资源共38个文件包含头文件与实现文件.h/.cpp、界面资源.rc/.ico、工程配置信息.dsp/.dsw以及可直接运行的Debug版exe压缩包约3.54MB整体目录层次清楚可在Visual C环境中对照源码理解连接与查询细节。已有1664人学习下载尤其可从InfomationDlg等对话框模块看到UI与数据库操作的整合思路帮助读者绕过常见配置坑减少调试时间。1. MFC连接MySql数据库先把ODBC这条数据通路彻底摸清MFC连接MySql数据库说得直白一点就是把ODBC这条数据通路彻底吃透。很多桌面项目在升级数据库时卡住的往往不是SQL本身而是MFC这边看不见的小问题程序编的是x86DSN却建在64位管理器里MySQL 8.0默认的caching_sha2_password认证老版驱动压根不认连接串少写一个CHARSET中文就变成一串问号。这篇文章按我从模拟项目X的Access迁移到MySQL时实际踩过的顺序把环境搭建、CDatabase连接、增删改查和常见故障一条条过一遍给正在用MFC维护业务系统的开发者一个能直接照做的参考。2. 环境选型MySQL版本、ODBC驱动与32/64位一致性老规矩先把环境装对再谈代码。MFC连接MySQL的失败有一半发生在写代码之前。下面这组环境决策直接影响后续所有代码能不能在客户机器上跑起来。2.1 为什么首选ODBC三种接入方式对比MFC要访问MySQL常见做法有三种官方Connector/C、MySQL C API、ODBC。Connector/C在MFC里用起来比较别扭因为接口风格偏现代CCString和std::string之间的来回转换容易出问题尤其当字段带中文时转完再转回来就变成一串乱码。MySQL C API功能最全但所有结果集都要自己管理内存和游标对对话框程序来说太底层写增删改查变成纯粹造轮子。ODBC是我这几年最顺手的方案原因就两条第一MFC的CDatabase和CRecordset已经把连接、查询、字段绑定封装好了读写字段比裸API少写一半代码第二ODBC是通用接口以后数据库要从MySQL换成SQL Server或者恢复成Access多半只需要改连接串和少量SQL方言业务代码不用重写。我们组维护的某跨平台系统数据层就是靠这层ODBC抽象在Access和MySQL之间来回切换过两次UI层一行没动。当然ODBC也有它的代价多一层驱动转发性能比直接C API稍微慢一点。对每秒几万次写入的采集场景不合适但桌面对接MySQL做增删改查性能瓶颈从来不在这一层。如果你维护的是订单管理、进销存这类MFC业务系统ODBC是风险最低的选型。2.2 版本组合怎么选MySQL 5.7还是8.0选版本时先看一件事现有代码里有没有用Connector/ODBC 3.51的痕迹。如果有直接换驱动也救不了MySQL 8.0因为8.0把默认认证插件改成了caching_sha2_password5.x驱动在连接时会直接报认证插件不可加载。我的建议是MySQL服务端用8.0.x配套驱动用Connector/ODBC 8.0.x的Unicode版本。如果公司规定必须沿用5.7那驱动可以用5.3或8.0连接串里仍然要写CHARSETutf8mb4。为什么要强调Unicode驱动MFC工程在VS里默认字符集是UnicodeCString实际存的是UTF-16宽字符。ANSI驱动会把数据以系统本地代码页转来转去字段里的中文就容易在转换中损坏。选带Unicode字样的驱动ODBC驱动自己处理UTF-8和UTF-16的互转应用程序这一层只需要负责传入传出。多花一分钟选对驱动后面能省下一整天的乱码排查。2.3 MySQL服务端和ODBC驱动安装注意点安装MySQL本身不多说MySQL Installer选Developer Default即可。如果你在安装时没看到Develop选项说明安装包可能选了自定义组件模式切回Full或者重新下载完整安装包就能解决。真正要小心的三步如下。驱动安装时勾选32位和64位两个版本。很多安装包默认只装当前系统位数你运行一个x86编译的MFC程序时就会找不到驱动。装完驱动后打开ODBC数据源管理器。64位用C:\Windows\System32\odbcad32.exe32位用C:\Windows\SysWOW64\odbcad32.exe这两个路径极其容易弄混混了就是第5章说的“找不到数据源”。添加系统DSN时驱动名选择“MySQL ODBC 8.0 Unicode Driver”TCP/IP Server填127.0.0.1Port填3306User填rootDatabase填demo_db。设置界面里如果带Character Set选项直接选utf8mb4。环境变量层面没有额外要求但建议把MySQL的bin目录加进PATH方便后面用mysql命令验证数据库状态。VS编译平台和驱动位数必须一一对应我习惯在VS里把解决方案平台明确成x64再用64位DSN彻底避开混用。还有一个经验DSN名字用全英文不要带空格和中文否则在连接串里转义很麻烦。2.4 建一个测试库表结构和字符集从开始就定好有了DSN不代表能写业务代码先把测试库准备好。下面建库和测试数据的SQL直接粘贴到mysql命令行或MySQL Workbench执行即可。CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE demo_db; CREATE TABLE user_info ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT DEFAULT 0, score DECIMAL(10, 2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; INSERT INTO user_info(name, age, score) VALUES (张三, 23, 88.50), (李四, 20, 76.00);库名和表名都用小写字段用英文避免在MySQL大小写敏感配置下翻车。utf8mb4是必须的MySQL 8.0把utf8保留成utf8mb3存不了表情符号连接串和表字符集都对齐到utf8mb4后乱码问题能少一半。后面所有C代码都针对这个user_info表字段类型和MFC的RFX映射有个对应关系INT对应RFX_LongVARCHAR对应RFX_TextDECIMAL对应RFX_Double。2.5 验证环境三个命令确认没装错装完别急着写代码先做一轮快速验证。打开命令提示符依次执行下面三条能返回正常结果再继续。mysql -uroot -p -e SHOW VARIABLES LIKE character_set_server; mysql -uroot -p -e SELECT VERSION(); mysql -uroot -p -e USE demo_db; SELECT COUNT(*) FROM user_info;第一条看服务端默认字符集正常应该是utf8mb4或utf8mb3第二条确认版本是否与驱动匹配第三条能返回2说明库和表都没问题。如果第二个命令返回的版本是8.0.x而驱动装的是5.3后面连接一定会被认证插件卡住趁早回头改驱动版本。这套验证流程也是排查环境问题的第一站比在VS里断点调试快得多。3. 连接串与CDatabase不弹窗、可重连的接线方式环境就绪后进入真正写代码的部分。MFC里连接MySQL最常用的类是CDatabase但能不能一次连上取决于连接串怎么写、异常怎么接住、连接对象怎么管理。3.1 两种连接串写法DSN和DRIVER二选一打开数据库连接最常见的是CDatabase::OpenEx。第一个参数是连接字符串有DSN和DRIVER两种写法。第一种依赖刚才配置的系统DSNCDatabase db; CString connStr; connStr.Format( _T(DSNdemo_dsn;UIDroot;PWD123456;CHARSETutf8mb4;)); db.OpenEx(connStr, CDatabase::noOdbcDialog);第二种不依赖DSN直接把驱动名、服务器地址写全CDatabase db2; CString connStr2 _T( DRIVER{MySQL ODBC 8.0 Unicode Driver}; SERVER127.0.0.1;PORT3306; DATABASEdemo_db;UIDroot;PWD123456; CHARSETutf8mb4;OPTION3;); db2.OpenEx(connStr2, CDatabase::noOdbcDialog);两种方式区别在维护成本用DSN换服务器只需要改ODBC管理器程序不用重编译用DRIVER所有信息都在代码里部署时少一个配置项但换库就要改源码。我一般项目内用DSN分发工具类程序用DRIVER。参数解释如下表。参数含义典型取值DSN数据源名称需在ODBC管理器里预先配置demo_dsnDRIVERODBC驱动名要和已安装的驱动完全一致MySQL ODBC 8.0 Unicode DriverSERVERMySQL服务器地址127.0.0.1PORT端口3306UID / PWD用户名、密码root / 123456CHARSET连接字符集utf8mb4OPTION连接标志3表示启用CLIENT_FOUND_ROWS等3连接串里的CHARSET用小写开头那个写法MySQL ODBC对大小写不敏感但全篇统一能减少后续字符集排查的麻烦。UID和PWD只是测试环境正式项目别把密码写死在代码里至少要放到配置文件或注册表里并做加密。如果嫌每次OpenEx要传长串也可以把连接串存到ini文件里启动时读入这样换环境不需要重新编译。3.2 OpenEx的第二个参数为什么必须传noOdbcDialogCDatabase::Open如果不传参数会弹一个选择数据源的对话框。MFC新手很容易在这里卡住程序跑到Open就往出弹ODBC管理窗口点确定后才能继续。生产环境绝不能出现这个弹窗所以OpenEx的第二个参数要传CDatabase::noOdbcDialog。这个参数的意思是禁止ODBC管理器显示连接对话框连接失败时直接抛CDBException把错误抛到你的catch里。参考代码CDatabase db; CString connStr _T(DSNdemo_dsn;UIDroot;PWD123456;CHARSETutf8mb4;); try { if (!db.OpenEx(connStr, CDatabase::noOdbcDialog)) { AfxMessageBox(_T(数据库连接失败)); return FALSE; } } catch (CDBException* e) { CString msg; msg.Format(_T(连接异常%s), e-m_strError); AfxMessageBox(msg); e-Delete(); return FALSE; }这里要留意MFC异常对象的释放规则catch里拿到的是CDBException指针用完后必须调用Delete这是MFC的异常宏机制不是普通的delete。如果漏掉Debug模式会报内存泄漏。e-m_strError里包含ODBC驱动返回的原始错误描述联调时比只看“连接失败”四个字管用得多。3.3 封装一个连接管理类避免每次查询重连桌面程序常见错误是每次执行SQL都new一个CDatabase用完再关。这样虽然能跑但MySQL服务端会频繁看到TCP连接的建立和断开在局域网环境下慢得明显。正确做法是把连接对象放到对话框或数据层对象的生命周期里初始化时建立一次窗口销毁时关闭。我写过一层很薄的封装实际项目里一直这么用class CDbConn { public: CDbConn() : m_bOpened(FALSE) {} virtual ~CDbConn() { Close(); } BOOL Connect(const CString strConn) { if (m_bOpened) return TRUE; try { m_bOpened m_db.OpenEx(strConn, CDatabase::noOdbcDialog); } catch (CDBException* e) { m_strError e-m_strError; e-Delete(); m_bOpened FALSE; } return m_bOpened; } void Close() { if (m_bOpened m_db.IsOpen()) { m_db.Close(); } m_bOpened FALSE; } CDatabase Get() { return m_db; } BOOL IsOpen() const { return m_bOpened m_db.IsOpen(); } CString GetError() const { return m_strError; } private: CDatabase m_db; BOOL m_bOpened; CString m_strError; };这个封装的核心是把Open和Close收到同一个类里析构时自动关闭避免忘记Close导致连接句柄泄漏。IsOpen方法用来判断当前连接对象是否可用。注意CDatabase::IsOpen只能反映对象内部状态如果MySQL服务端主动断开了连接这个返回值并不一定可靠所以下一节还要加一层重连策略。3.4 连接被MySQL断开后的重连策略MySQL服务端wait_timeout默认是8小时桌面程序开着过夜第二天再来操作经常会弹“连接不可用”。常见做法是执行查询前先检查IsOpen如果关闭则重新Connect。更省事的办法是记录上次操作时间超过阈值就主动Close再重连。下面这段放在查询入口处BOOL EnsureAvailable(CDbConn conn, const CString strConn) { if (conn.IsOpen()) { return TRUE; } conn.Close(); if (!conn.Connect(strConn)) { AfxMessageBox(conn.GetError()); return FALSE; } return TRUE; }这套做法同样适合开发期调试程序崩溃重启后不需要重启MySQL服务端下一次查询会自动把连接建起来。我习惯在CDbConn里加一个连接计数器连接成功一次就加一连不上时看计数就能判断是密码错误、主机拒绝还是驱动问题。计数器加进日志后排障会变得非常直观。4. 增删改查CRecordset字段绑定与ExecuteSQL事务环境通了以后核心业务就是增删改查。MFC里查询和写操作走两条不同的路线查询用CRecordset写操作用ExecuteSQL两条线配合好桌面数据库模块就稳了。4.1 用CRecordset读出数据GetFieldValue最快RFX绑定最稳查询可以走两条线快速验证用GetFieldValue反复读的表用RFX绑定。先用GetFieldValue做一个快速的查询验证适合联调阶段确认SQL和字段名CDatabase db conn.Get(); CRecordset rs(db); BOOL bOK rs.Open(CRecordset::dynaset, _T(SELECT id, name, age FROM user_info ORDER BY id)); if (!bOK) { AfxMessageBox(_T(查询失败)); return; } while (!rs.IsEOF()) { CString strName; long nAge 0; rs.GetFieldValue(_T(name), strName); rs.GetFieldValue(_T(age), nAge); TRACE(_T(name%s age%ld\n), (LPCTSTR)strName, nAge); rs.MoveNext(); } rs.Close();GetFieldValue的重载支持把字段值读入CString、long等类型但遇到NULL字段会直接抛DBException所以这里先把nAge初始化为0。CRecordset的打开方式dynaset在MySQL ODBC 8.0下可用它允许在结果集打开期间看到其他会话已提交的改动如果驱动不支持会返回错误那就换成CRecordset::snapshot它是快照式打开后结果固定不变。GetFieldValue适合联调业务代码推荐RFX映射因为访问字段就像访问struct成员一样直接而且对NULL字段有默认值兜底。下面这个CUserSet子类把user_info的四个字段绑到成员变量class CUserSet : public CRecordset { public: CUserSet(CDatabase* pDb NULL) : CRecordset(pDb), m_id(0), m_age(0), m_score(0.0) {} virtual CString GetDefaultSQL() { return _T(SELECT id, name, age, score FROM user_info); } void DoFieldExchange(CFieldExchange* pFX) { pFX-SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T(id), m_id); RFX_Text(pFX, _T(name), m_name); RFX_Long(pFX, _T(age), m_age); RFX_Double(pFX, _T(score), m_score); } long m_id; CString m_name; long m_age; double m_score; };DoFieldExchange里的列顺序要和SELECT顺序一致。RFX宏的类型要与MySQL列类型对应INT到RFX_LongVARCHAR到RFX_TextDECIMAL到RFX_Double。如果对不上Open时会报“无法转换字段”。m_score初始化为0.0是因为score列允许NULL查询时碰到NULLRFX_Double会把缓冲区值置为0而不是抛异常。使用时代码干净很多CUserSet rs(db); if (rs.Open()) { while (!rs.IsEOF()) { CString line; line.Format(_T(%ld, %s, %ld, %.2f), rs.m_id, rs.m_name, rs.m_age, rs.m_score); m_list.AddString(line); rs.MoveNext(); } rs.Close(); } else { AfxMessageBox(rs.m_strError); }如果Open失败CRecordset对象里m_strError会有原始错误描述我习惯先把它打到日志里再给用户弹窗。这个习惯帮我解决过很多字段名拼错的低级问题。4.2 插入、更新、删除ExecuteSQL和事务控制查询走CRecordset写操作我一般直接用CDatabase::ExecuteSQL。它没有结果集适合INSERT、UPDATE、DELETE和DDL。基本写法CString sql; sql.Format(_T( INSERT INTO user_info(name, age, score) VALUES(%s, %ld, %.2f)), _T(王五), 30, 92.5); try { db.ExecuteSQL(sql); } catch (CDBException* e) { CString msg e-m_strError; e-Delete(); AfxMessageBox(_T(插入失败) msg); }这里有个安全隐患如果VALUES里的name由用户输入直接把字符串拼进SQL会形成注入口或者单引号把SQL语法弄破。第6章会给转义和参数化方案。另外ExecuteSQL里不能执行SELECT如果误传SELECT语句会返回错误因为CDatabase::ExecuteSQL不处理结果集。多行写操作需要保证原子性时必须用事务包起来BOOL BatchUpdate(CDatabase db) { db.BeginTrans(); try { db.ExecuteSQL(_T( UPDATE user_info SET score score 5 WHERE age 25)); db.ExecuteSQL(_T( DELETE FROM user_info WHERE score 60)); db.CommitTrans(); } catch (CDBException* e) { db.RollbackTrans(); CString msg e-m_strError; e-Delete(); AfxMessageBox(_T(事务回滚) msg); return FALSE; } return TRUE; }事务有几个硬前提表存储引擎必须是InnoDBMyISAM不参与事务RollbackTrans不会撤销MyISAM的写入另外BeginTrans之后一定要保证CommitTrans或RollbackTrans成对出现不然连接状态会卡在半开事务里后续所有SQL都表现为“不生效”。写代码时保持这个成对习惯能省掉5.4节说的那个经典故障。4.3 MFC里拼排序、分页和模糊查询SQL的小套路桌面端最常见的三类SQL模板都基于user_info表。排序用ORDER BY分页用LIMIT模糊查询用LIKE。直接看模板SELECT id, name, score FROM user_info ORDER BY score DESC, id ASC LIMIT 10; SELECT id, name, score FROM user_info ORDER BY id LIMIT 20, 10; SELECT id, name FROM user_info WHERE name LIKE 李% OR name LIKE %张%;MFC里分页一般这样拼偏移量由界面页码决定int nPage 0; // 当前页从0开始 int nPageSize 20; // 每页20条 CString sql; sql.Format(_T( SELECT id, name, score FROM user_info ORDER BY id LIMIT %d, %d), nPage * nPageSize, nPageSize);ORDER BY后面接LIMIT是固定顺序排序字段最好不要用中文列名否则容易踩字符集和排序规则不一致的坑。需要统计总行数就用SELECT COUNT() FROM user_info但COUNT()在小数据量没问题数据量大时应该基于主键或加条件避免全表扫描拖慢界面。模糊查询有一个隐形问题用户输入里的%和_会被当成通配符如果用户搜索内容里带%而你想按字面量搜必须转义。MySQL里用LIKE ... ESCAPE \\可以解决但MFC拼SQL时反斜杠本身又需要转义这点容易写错建议封装一个专门函数去处理。5. 避坑排查DSN位数、认证插件与乱码问题环境最折磨人的不是原理看不懂而是“明明照着教程写了还是不行”。现场最容易翻车的四个点下面按现象、原因、解决办法过一遍。5.1 找不到数据源先问编译平台是x86还是x64现象程序在开发机上运行正常换一台机器或打包发布后一执行到OpenEx就弹“找不到数据源名称”或“默认驱动程序不存在”。打开ODBC管理器看DSN明明存在。原因开发机同时装了32位和64位驱动ODBC数据源管理器也分两套。如果用System32下的odbcad32.exe建的是64位DSNMFC工程编译成x86时读不到反过来x64工程读不到SysWOW64建的32位DSN。发布机只装了单一版本驱动时这个错暴露得更彻底。解决先确认VS里的解决方案平台再打开对应位数的管理器建同名DSN。32位入口是C:\Windows\SysWOW64\odbcad32.exe64位是C:\Windows\System32\odbcad32.exe。更稳妥的方案是不依赖DSN把连接串写成DRIVER完整形式这样只要驱动版本统一DSN是否存在不影响。我现在的新项目发布包安装脚本直接把驱动名和连接串写进配置文件从源头去掉DSN这个变量。5.2 MySQL 8.0默认认证插件不识别现象连接串没问题DSN测试也通过但程序运行时报“Authentication plugin caching_sha2_password cannot be loaded”或者“Client does not support authentication protocol requested by server”。原因MySQL 8.0安装时默认给root用户使用caching_sha2_password认证而Connector/ODBC 5.x或更早版本只认识mysql_native_password。老驱动握手时拿不到新插件支持的认证消息连接就被拒绝。解决优先升级驱动到Connector/ODBC 8.0.x。如果必须保留老驱动就手工把认证插件降级ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES; SELECT user, host, plugin FROM mysql.user WHERE user root;注意host字段程序用127.0.0.1连接时对应rootlocalhost或root127.0.0.1用局域网IP访问时别改错host创建一个只给应用用的账号比直接动root更安全。还有一点容易忽略DSN配置界面里也有一套账号密码如果DSN里填的和代码里不一样测试时通过、运行时失败两边不一致会让人绕很多弯路。5.3 中文乱码和NULL字段读取和写入各一个坑现象查询出来的中文变成??????或乱码插入的中文进表后变成问号。MFC界面用CString显示正常一写进库就坏。原因三层里至少有一层不一致。第一层是表字符集第二层是连接字符集第三层是驱动是ANSI还是Unicode。MySQL服务端character_set_results默认跟随全局配置如果全局是latin1连接串又没写CHARSET驱动就会按latin1把UTF-16转码中文就丢了。解决建表指定utf8mb4连接串加CHARSETutf8mb4DSN配置界面里连接字符集选utf8mb4驱动选Unicode版。MFC工程里中文字面量直接用中文但源文件保存编码必须是UTF-8 with BOMVS默认就是。这三处对齐后乱码基本绝迹。有一个快速验证方法插入一条带中文和表情符号的记录再用mysql命令行查如果显示正常说明库这边没问题。现象查询某字段抛DBException错误提示“无法从SQL语句中读取指定的字段值”或字段明明有值GetFieldValue返回空串。原因字段类型是NULL或类型不匹配。GetFieldValue遇到NULL字段会抛异常而不是返回空字符串。另一个可能原因是SELECT里列别名和表结构不一致比如SELECT name AS n FROM user_info然后GetFieldValue(_T(name))当然找不到。解决SQL里给列起别名查询端也用别名字段可能为NULL时先判断IsFieldNullable或者在SELECT时用IFNULL(age,0) AS age给NULL一个默认值。更稳妥是用4.1的RFX绑定方式RFX对NULL会写入成员变量默认值不用在每次取值处加判断业务代码会干净很多。5.4 记录集类型和事务状态两个容易迷惑的点现象CRecordset::Open(CRecordset::snapshot)在MySQL上偶尔报错换dynaset后正常或者反过来有的机器上dynaset报错。原因MFC的snapshot和dynaset在不同ODBC驱动下的支持程度不一样。MySQL ODBC 8.0对dynaset支持较好snapshot有时会退化成只读快照老驱动对dynaset的支持则不稳定。解决按驱动文档范围选优先dynaset驱动不支持再退回snapshot最后用forwardOnly。代码里最好把记录集类型定义成可配置常量换驱动只改一个地方。现象执行INSERT或UPDATE后程序没报错但数据没变。原因十有八九是前面BeginTrans后没CommitTrans。CDatabase对象还停留在事务中第二次ExecuteSQL实际还在同一事务里事务没提交别的会话看不到数据。另一种可能是操作了MyISAM表UPDATE语句执行成功但表引擎不支持事务。解决在写操作函数入口检查是否有未提交事务有则先RollbackTrans再重新Begin。我习惯用CDatabase::CanTransact()判断当前连接是否支持事务写操作前后加日志打印事务状态。这个习惯能快速发现半开事务问题不至于当场抓瞎。6. 进阶实战状态栏显示数据库连接状态与参数化防注入最后这部分是实际项目里一定会用到的两组技巧。第一组把数据库状态放到状态栏上程序跑起来一眼能看到连接是否还活着第二组是参数化查询给SQL注入收尾。6.1 用状态栏实时显示数据库连接状态状态栏不是只能显示菜单提示。把连接状态放上去调试和现场运维都会轻松很多。先看MainFrame的OnCreate里怎么写int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) -1) return -1; if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators) / sizeof(UINT))) { TRACE0(状态栏创建失败\n); return -1; } // 把第一个窗格设为可拉伸用来显示连接信息 m_wndStatusBar.SetPaneInfo(0, ID_SEPARATOR, SBPS_STRETCH, 200); // 每3秒检查一次数据库连接状态 SetTimer(1, 3000, NULL); return 0; }状态栏窗格定义沿用MFC默认的indicators数组第一个是ID_SEPARATOR。SetPaneInfo第三个参数传SBPS_STRETCH让它自动填充剩余空间第四个参数是宽度如果只显示“数据库已连接”几个字默认宽度就够要显示较长错误信息时需要把这个值调大。然后在OnTimer里更新文字void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { CString strState; if (g_pDbConn g_pDbConn-IsOpen()) strState _T(数据库已连接); else strState _T(数据库断开); m_wndStatusBar.SetPaneText(0, strState); } CFrameWnd::OnTimer(nIDEvent); }SetPaneText的第一个参数是窗格索引0表示第一个窗格。如果状态栏还有其它自定义窗格索引要对齐。定时器周期设为3秒比较合适太短会无谓消耗资源太长让人误以为程序卡住。这个功能在现场调试帮过大忙服务端断网、用户改密码都能第一时间在状态栏看到变化而不是等用户操作时弹一个错误框。6.2 参数化查询让SQL注入在MFC里无从下手第4章的直接拼接SQL有注入风险。MFC ODBC没有那种一行式的参数化API但CRecordset支持参数绑定把用户输入作为参数传入SQL驱动会负责转义。参考这个子类class CUserSearchSet : public CRecordset { public: CUserSearchSet(CDatabase* pDb, LPCTSTR strName) : CRecordset(pDb), m_strName(strName), m_id(0), m_age(0) {} virtual CString GetDefaultSQL() { return _T(SELECT id, name, age FROM user_info WHERE name ?); } void DoFieldExchange(CFieldExchange* pFX) { pFX-SetFieldType(CFieldExchange::param); RFX_Text(pFX, _T(param_name), m_strName); pFX-SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T(id), m_id); RFX_Text(pFX, _T(name), m_name); RFX_Long(pFX, _T(age), m_age); } CString m_strName; // 查询参数 long m_id; CString m_name; long m_age; };这里有一个容易踩的坑参数名和外层列名不要撞车我这里把参数名写成param_name实际起作用的是SQL里?的位置不是参数名文本。DoFieldExchange里参数段必须在输出段之前顺序不能乱。执行Open前先给m_strName赋值CUserSearchSet rs(db, strKeyword); if (rs.Open()) { while (!rs.IsEOF()) { CString line; line.Format(_T(%ld, %s, %ld), rs.m_id, rs.m_name, rs.m_age); m_list.AddString(line); rs.MoveNext(); } rs.Close(); }CRecordset里的参数个数由DoFieldExchange中param类型的RFX数量决定多一个少一个都会在运行时报“参数数量错误”。UPDATE和DELETE没有原生参数化手段我用一个转义函数兜底CString EscapeSql(const CString strInput) { CString str strInput; str.Replace(_T(\\), _T(\\\\)); str.Replace(_T(), _T()); str.Replace(_T(\), _T(\\\)); return str; }写完增删改查后一定要拿一个字段做特殊字符测试输入its ok试试。能正常写入且不漏SQL这一层才算真的抗住了。从那以后我每次新建MFC数据库模块都会强制自己按三步验收先用64位DSN和x64工程跑通连接再检查中文和NULL字段读写最后把用户输入字段全部改成参数或转义。希望这组经验能帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑