简介:一份专门面向 Visual Basic 6 开发者的 SQLite 嵌入式数据库封装包(litex_sqlite),解决 VB6 程序在无独立服务器环境下的本地数据存储与管理需求,尤其适合桌面型 MIS、进销存、工具软件等对性能有较高要求的应用。压缩包共 134 个文件,约 1.5MB,包含 C/C++ 核心源码、编译好的 DLL/Lib 库、VB6 的模块与窗体文件、工程工作区与解决方案文件、注册脚本、测试示例、AutoIt 自动化脚本、头文件接口定义以及 SQLite 3.3.5 配套版本,还配有详细使用说明;既能直接集成进 VB6 工程,也可按需重新编译。已有 766 人学习下载,是 VB6 开发者接入 SQLite 的常用参考,特别适合需要快速落地的中小型项目。借助压缩包内的示例与文档,可完整了解从环境注册、工程引用到调用 API 执行建表、插入、查询、更新等操作的流程,同时解决 DLL 注册、库依赖等常见问题。头文件与源代码也便于深入理解封装机制,进行二次定制,从而让 VB6 程序直接获得 SQLite 的稳定存储能力。
1. 还在维护VB6老系统的兄弟,SQLite这条路怎么选
很多人以为VB6是上个世纪的古董,可进销存、工控上位机、老MIS系统里还跑着大量VB6程序。这些程序要加本地存储:Access文件几十MB后性能直线下降还容易损坏,SQL Server Server Compact早已停止维护,装完整版SQL Server又太重。单文件、零配置的SQLite成了最现实的选择。litex_sqlite就是“sqlite支持vb6的版本”这条路上常用的一套封装,它把SQLite的C接口翻译成VB6能直接调用的函数,老工程不用大改就能落库。这篇文章写给还在维护这类系统的人:接入路线怎么选、最小代码怎么跑通、参数怎么设、哪些坑绕不开。新项目就别折腾VB6了,下面这些经验只服务存量系统。
2. 为什么VB6跑SQLite这么别扭:三条路线与litex_sqlite的选型逻辑
VB6官方没有提供SQLite的ActiveX控件,也没有托管接口,SQLite官方只发布一组C语言API的DLL。这个前提决定了VB6接入SQLite必须走一层“翻译”。常见做法有三条:ADO配ODBC驱动、直接Declare sqlite3.dll、用litex_sqlite这类现成封装。先讲清楚三条路的差别,后面踩坑时你才知道问题到底出在哪一层。
2.1 ADO+ODBC这条最省事的路,为什么我最后没走
先抛结论:ADO+ODBC在VB6里操作SQLite,写代码确实像连Access一样舒服。不需要理解SQLite的C接口,打开Connection,拼SQL,Recordset查出来,老VB程序员闭着眼都能写。我第一次做SQLite改造就是这么干的,开发两天就完事。
部署时才开始翻车。SQLite的ODBC驱动不是Windows自带的,客户端每台机器都要装,而且驱动的位数和版本得跟系统里其他组件对齐。老运维机环境五花八门:有的装过旧版驱动,有的装的是另一个分支的SQLite ODBC,连接串参数不兼容,开发机上跑得好好的程序,拷到车间机器上报“找不到数据源”。更别扭的是,ODBC驱动对SQLite新特性的支持滞后,想开WAL、想设busy_timeout,驱动层面经常不暴露这些参数,最后只能退回去用默认的journal模式。这条路适合写一次性脚本、能接受每台机器装驱动的场景,我做产品化分发之后彻底放弃。
2.2 直接Declare sqlite3.dll:接口能用但处处是暗坑
第二条路是硬刚C接口。SQLite官方发布的sqlite3.dll导出函数齐全,VB6用Declare Function声明之后确实能调。但SQLite对外API是给C/C++用的,一堆指针、回调、编码转换。sqlite3_open_v2返回一个数据库句柄,查询要走prepare、step、column这一串操作,VB6处理指针本来就别扭,稍不留神就是内存泄漏。
更痛苦的是编码。SQLite内部存UTF-8,VB6的String是Unicode,Windows的ANSI编码又是中间层。直接传一个中文路径给sqlite3_open,顶层是VB6的BSTR,底层DLL按ANSI收,转成UTF-8时只要封装没处理好,路径就错了;读出来的sqlite3_column_text返回的是UTF-8字节,还得自己做多字节转宽字符。这套转换逻辑写一次能跑通,但每个工程复制一遍,出问题排查费大量时间。属于“能用但别碰”的路线,只适合拿来做学习实验。
2.3 litex_sqlite这类封装的价值:把C API收成了VB6函数
常见的做法是直接用社区里已经封装好的模块。litex_sqlite这类库,本质上是把2.2小节你自己要写的那层胶水代码写好,Declare sqlite3.dll的导出函数,再用VB6代码包一层,把指针、回调、UTF-8转换全部收进内部,对外暴露Open、Execute、Query这类符合VB6习惯的函数。传字符串进去,拿字符串出来,中间的黑匣子由封装负责。
我选型时一般看四个维度:部署成本——纯DLL还是要注册OCX;接口风格——函数式还是对象式;更新频率——封装有没有跟着SQLite核心版本走;排查深度——出错时给不给SQLite原生错误信息。在VB6这个生态里,litex_sqlite被讨论得多,是因为它的接口做得足够薄,接近直接操作SQLite,学过的SQL知识不浪费,排查时还能往下追到SQLite层。
| 接入路线 | 客户端部署 | 编码与指针 | 查询接口 | 适用场景 |
|---|---|---|---|---|
| ADO + ODBC | 每台机装驱动 | 驱动处理 | Recordset | 一次性脚本、内部工具 |
| 直接Declare sqlite3.dll | 只带DLL | 自己处理 | 指针硬调 | 学习验证,工程慎用 |
| litex_sqlite封装 | 只带DLL和模块 | 封装处理 | 函数式 | 存量VB6产品改造 |
结论很简单:产品化、要走多台机器,就用封装路线。下面几章全部按litex_sqlite的操作方式来写,你可以对照自己手头那套封装微调函数名。
3. 把litex_sqlite装进工程:DLL放置、注册与最小可用代码
从这一章开始动手。老工程改造顺序是:先把封装库放进工程目录,再写模块级声明,最后跑通最小用例。这步走通,后面所有查询、事务、排错都在这个骨架上加。
3.1 先搞清楚它是什么形态:标准DLL不需要注册,OCX才需要
拿到litex_sqlite压缩包,第一步看里面是DLL还是OCX,或者是纯.bas源码模块。如果是标准DLL,走Declare路线,不需要regsvr32注册,放进工程目录、在标准模块里声明函数就能用;如果是OCX控件,才需要regsvr32去注册,工程里还要勾选部件引用。判断方法很简单,看包里有没有.bas声明文件,或者有没有说明文档提到“regsvr32”。
我一般把DLL放在App.Path下的bin目录,跟EXE一起分发,不往System32里塞。原因是老机器上System32权限控制不一,而且不同版本的DLL混放容易互相覆盖,放在程序自己的目录里最省心。如果工程要打包成安装包,记得把bin目录加进去,安装时用相对路径定位。还有一个细节:VB6的IDE里调试时,App.Path是VB6.exe的目录,不是工程目录,所以调试阶段DLL要么放工程目录,要么把路径写成固定绝对路径,不然会报“无法加载DLL”。
3.2 最小可用代码:打开、建表、插入一条数据
下面这段是标准模块里的声明和测试过程。litex_sqlite的函数名在不同版本里可能有出入,以你手上的.bas声明文件为准,调用逻辑是一致的。
' 模块级:函数名和参数以你手上的.bas声明为准 Private Declare Function litex_open Lib "litex_sqlite.dll" _ (ByVal dbName As String) As Long Private Declare Function litex_exec Lib "litex_sqlite.dll" _ (ByVal dbHandle As Long, ByVal sql As String) As Long Private Declare Function litex_close Lib "litex_sqlite.dll" _ (ByVal dbHandle As Long) As Long Private Declare Function litex_errmsg Lib "litex_sqlite.dll" _ (ByVal dbHandle As Long) As String Sub TestInit() Dim hDB As Long Dim rc As Long Dim dbFile As String dbFile = App.Path & "\data\test.db" ' 确保data目录存在,SQLite不会自动创建父目录 On Error Resume Next MkDir App.Path & "\data" On Error GoTo 0 hDB = litex_open(dbFile) If hDB = 0 Then MsgBox "打开数据库失败" Exit Sub End If rc = litex_exec(hDB, "CREATE TABLE IF NOT EXISTS stock(" & _ "id INTEGER PRIMARY KEY AUTOINCREMENT, " & _ "name TEXT NOT NULL, " & _ "qty INTEGER DEFAULT 0)") If rc <> 0 Then MsgBox "建表失败: " & litex_errmsg(hDB) litex_close hDB Exit Sub End If rc = litex_exec(hDB, "INSERT INTO stock(name, qty) VALUES('螺丝钉', 200)") If rc <> 0 Then MsgBox "写入失败: " & litex_errmsg(hDB) End If litex_close hDB End Sub这段代码做了四件事:打开库、建表、插入、关闭。参数上注意几点:第一,litex_open返回0表示打开失败,SQLite句柄从1开始编号,所以判断用“= 0”而不是“< 0”;第二,返回码rc遵循SQLite约定,0表示成功,非0是错误码,具体含义用litex_errmsg取文本最直观;第三,CREATE TABLE IF NOT EXISTS是幂等操作,重复执行不会报错,适合程序每次启动都跑一遍建表语句;第四,MkDir外面包了On Error,因为目录已存在时会报错,这里故意忽略。
3.3 查询与update语句:结果集遍历与影响行数
插入跑通后,查询和更新是日常主力。litex_sqlite这类封装一般提供独立的查询句柄,遍历方式和Recordset不同,看下面这段:
Private Declare Function litex_query Lib "litex_sqlite.dll" _ (ByVal dbHandle As Long, ByVal sql As String) As Long Private Declare Function litex_next Lib "litex_sqlite.dll" _ (ByVal rsHandle As Long) As Long Private Declare Function litex_fieldText Lib "litex_sqlite.dll" _ (ByVal rsHandle As Long, ByVal colIndex As Long) As String Sub QueryStock() Dim hDB As Long Dim rs As Long Dim id As String, name As String, qty As String hDB = litex_open(App.Path & "\data\test.db") If hDB = 0 Then Exit Sub rs = litex_query(hDB, "SELECT id, name, qty FROM stock WHERE qty > 0 ORDER BY id") If rs = 0 Then litex_close hDB Exit Sub End If Do While litex_next(rs) = 0 id = litex_fieldText(rs, 0) name = litex_fieldText(rs, 1) qty = litex_fieldText(rs, 2) Debug.Print id, name, qty Loop litex_close hDB End Sublitex_next返回0表示还有下一行,返回非0表示遍历结束,这个约定和ODBC的SQLFetch相反,第一次用容易写反循环条件。字段索引从0开始,和SQL语句里的列顺序一致。前面说过,格式化输出之前最好先做数值处理,这里只用Debug.Print是为了看清楚原始值。
update语句在SQLite里的坑不多,核心是加WHERE条件,否则全表更新:
' 常见错误:不加WHERE会把全表qty改成0 ' rc = litex_exec(hDB, "UPDATE stock SET qty = 0") ' 正确做法: rc = litex_exec(hDB, "UPDATE stock SET qty = qty - 10 WHERE name = '螺丝钉'") If rc = 0 Then Debug.Print "更新成功" End If如果封装提供受影响行数函数,更新后取一下行数,能确认条件是否真的命中了数据;没有的话,更新后再SELECT一次比对更保险。SQLite的update语句本身不复杂,真正复杂的是“怎么判断更新了几行”——很多封装不暴露sqlite3_changes,我一般会在更新前后各查一次目标条件的数据,用数量差确认。
4. 三个必调参数与数据安全配置:并发、事务、加密
单机程序也要面对三个绕不开的问题:别的进程同时打开同一个库、循环写入慢、以及“sqlite数据库文件能否加密”这个几乎每个客户都会问的事。这些问题不解决,程序在客户机器上跑几天就会暴露。
4.1 busy timeout和WAL:把database is locked压下去
VB6程序里最常见的锁错误是“database is locked”,现象是连续快速写入时报错,或者程序开着多个窗口共用同一个数据库句柄时偶发。原因是SQLite默认的journal_mode是DELETE模式,写事务开始时拿EXCLUSIVE锁,另一个连接只要没等到锁就会立刻报错。
两个参数能解决大部分场景,直接通过litex_exec执行PRAGMA:
' 设置忙等待超时,单位毫秒 rc = litex_exec(hDB, "PRAGMA busy_timeout = 3000") ' 开启WAL日志模式,读写并发能力大幅提升 rc = litex_exec(hDB, "PRAGMA journal_mode = WAL")busy_timeout的意思是:拿不到锁时最多等3秒,而不是立刻报错。这个值对单机程序设1000到3000都合理,设太大用户在界面卡住,设太小等于没设。WAL模式把写操作改成追加到-wal文件,读操作不再被写锁阻塞,对VB6这种多窗口操作同一个库的场景改善明显。
注意两个点:journal_mode是持久化设置,执行一次之后,同一个数据库文件以后打开都保持WAL模式;但WAL会生成.db-wal和.db-shm两个临时文件,备份数据库时不能只拷.db文件,否则丢数据。老运维拷库的习惯得提前跟客户说清楚。如果客户环境的老旧杀毒软件对多文件特别敏感,可以考虑退回DELETE模式只设busy_timeout。
4.2 循环写入必须开事务:速度差一个量级
VB6里逐条INSERT,每条自带一个隐式事务,等于每插入一条就做一次磁盘同步,1万条数据可能要跑几十秒。包一层显式事务,同一批提交,速度快一个量级,这是SQLite性能优化里性价比最高的一项:
Dim i As Long Dim rc As Long Dim hDB As Long hDB = litex_open(App.Path & "\data\test.db") rc = litex_exec(hDB, "BEGIN IMMEDIATE") If rc <> 0 Then MsgBox "事务开启失败" litex_close hDB Exit Sub End If For i = 1 To 10000 rc = litex_exec(hDB, "INSERT INTO stock(name, qty) VALUES('批量', " & i & ")") If rc <> 0 Then ' 失败就回滚,不留半截数据 litex_exec hDB, "ROLLBACK" MsgBox "第" & i & "条写入失败: " & litex_errmsg(hDB) litex_close hDB Exit Sub End If Next i rc = litex_exec(hDB, "COMMIT") If rc <> 0 Then litex_exec hDB, "ROLLBACK" End If litex_close hDBBEGIN IMMEDIATE和普通BEGIN的区别在于:普通BEGIN是DEFERRED事务,第一条写语句执行时才拿写锁,中间如果别的连接已经写了,这个事务会拿不到锁;BEGIN IMMEDIATE在事务开始时就拿写锁,更早暴露锁冲突,反而容易处理。循环里每一条都得检查rc,你永远不知道哪一条会遇到约束冲突,事务回滚是唯一后悔药。写入密集的场景,把批量值放进Text文件逐行读再插入,比拼SQL字符串更可控。
4.3 sqlite数据库文件能否加密?VB6下的现实回答
这个问题几乎每个客户都会问。SQLite官方不提供免费的文件级加密,商业方案是SQLite Encryption Extension,但它是C接口,VB6直接调用很麻烦,license费用也不低。开源方案SQLCipher把整个库加密,但编译出来的DLL是给C/C++用的,VB6声明调用那套在它身上基本走不通,而且litex_sqlite这类封装底层连的是标准sqlite3.dll,换掉DLL后封装就废了。
我的做法是分两层处理。第一层是敏感字段加密:在VB6里用系统自带的CAPICOM或.NET写好的加密DLL,把身份证号、手机号这类字段单独加密成Base64字符串再入库,查询时解密用,其他字段明文存储。第二层是整库文件保护:数据库文件放在用户数据目录,用Windows的EFS加密文件夹,这个不需要改一行代码。
结论放在这里:如果你需要的是绕开商用授权、内置的公开标准加密,VB6+SQLite这个组合没有完美答案。真想整库加密,就得接受SQLCipher重写访问层,或者升级到.NET。老项目维护阶段,字段级加密足够应对绝大多数审计需求。
5. litex_sqlite排错与避坑:五条血泪经验
这一章写的是我把litex_sqlite用到生产环境后,遇到的最有代表性的五个问题。每一条都按现象、原因、解决的顺序写,排查时可以对照。
5.1 现象:程序启动报错“找不到DLL入口点”
这个错误出现的场景很典型:开发机Win7 32位一切正常,拷贝到新配的Win10电脑上,程序起来就报“无法定位程序输入点……于动态链接库sqlite3.dll上”。原因几乎都是64位DLL混入。VB6的EXE是32位进程,它只能加载32位的sqlite3.dll,而新机器上可能装了64位版本的DLL,或者安装包把64位版本覆盖了开发用的32位版本。
解决:检查程序目录下sqlite3.dll的位数,用Visual Studio自带的dumpbin或者简单的文件属性查看。注意,发版前把32位DLL和64位DLL分开命名,sqlite3.dll只放32位版,64位版放到sqlite3_x64.dll这种独立文件里,绝不能用同名文件覆盖。这个错浪费了我一下午,后来在构建脚本里加了一步位数检查,再没犯过。
5.2 现象:数据库文件路径带中文就打不开
客户机器用户名是“张三”,程序的数据目录在C:\Users\张三\AppData\Roaming\MyApp,运行后报“unable to open database file”。原因不是权限,是编码。VB6的String传给DLL时,默认按ANSI(其实就是本机代码页)转成字节,而SQLite的接口把传入路径当UTF-8解析。中文路径在ANSI和UTF-8之间来回转换,只要有一层不对就匹配不上。
解决思路有两个。第一个是给数据库文件换到纯英文路径,简单粗暴,但客户目录不受你控制;第二个是用Windows的GetShortPathName把长路径转成8.3短路径,短路径里不含中文,再传给litex_open。我用的就是第二种,而且把转换封装成一个独立函数:
Private Declare Function GetShortPathName Lib "kernel32" _ Alias "GetShortPathNameW" _ (ByVal lpszLongPath As Long, ByVal lpszShortPath As Long, _ ByVal cchBuffer As Long) As Long Function ShortPath(ByVal longPath As String) As String Dim buf As String Dim len As Long buf = String(260, 0) len = GetShortPathName(StrPtr(longPath), StrPtr(buf), 260) If len > 0 Then ShortPath = Left$(buf, len) Else ShortPath = longPath End If End Function注意GetShortPathName用的是W版本,参数传StrPtr取指针,否则长路径里有中文一样转不对。这个函数返回的短路径形如C:\Users\ZHANS~1\AppData...,虽然难看了点,但兼容性最好。如果litex_sqlite封装本身处理了Unicode路径,这步可以跳过,但我在几个版本里都遇到问题,干脆统一走短路径,眼不见心不烦。
5.3 现象:读出来的中文全是乱码
写入的时候一切正常,SELECT查出来,Debug.Print都是“鏉犱笣”,典型的UTF-8字节被按ANSI解码了。原因:SQLite里存的是UTF-8,litex_exec把SQL传进去时封装做了转换,但查询结果字段文本可能没转,或者转反了。
解决:确认你手上的litex_sqlite版本是否处理了字段编码。没有处理的话,查询结果拿到UTF-8字节数组,用StrConv转:
Private Declare Function litex_fieldBlob Lib "litex_sqlite.dll" _ (ByVal rsHandle As Long, ByVal colIndex As Long, _ ByRef bytes() As Byte) As Long Function FieldUTF8Text(rs As Long, col As Long) As String Dim bytes() As Byte Dim len As Long len = litex_fieldBlob(rs, col, bytes) If len > 0 Then FieldUTF8Text = StrConv(bytes, vbUnicode) End If End FunctionStrConv配合vbUnicode,是把UTF-8字节数组转成VB6 Unicode字符串的标准做法。但这个方法要小心:如果字段本身是纯ASCII,转出来没有异样;一旦混入中文,转错编码的乱码是不可逆的,数据本身没坏,坏的是显示层。我后来把查询字段统一走这个转换函数,不再直接用fieldText,乱码问题绝迹。
5.4 现象:程序退出后,数据库文件还被占用,无法拷贝备份
VB6程序关闭了主窗口,但备份数据库时报“文件正在被另一个进程使用”。原因几乎都是同一个:某个打开的数据库句柄没有关。VB6不像.NET有析构函数,全局变量里的hDB不会因为窗体Unload自动释放。
解决:在所有退出路径上补litex_close。特别是用了错误处理On Error Resume Next的地方,很容易跳过关闭代码。我的习惯是写一个统一的关闭函数,在Form_Unload、App.Terminate、甚至Err.Raise之后都调一遍:
Public Sub CloseDB(hDB As Long) If hDB <> 0 Then litex_close hDB hDB = 0 End If End Sub关键点是关闭后把句柄置0,防止重复关闭。另外,如果程序里开了多个查询结果集rs,也要逐个释放。这也是“玄学”味道最浓的一条,看不到报错,就是文件锁着,找到最后发现是最早测试时漏了close。建议在程序里保留一个调试菜单,能直接查看当前有几个连接句柄,排查起来省事很多。
5.5 现象:新电脑报错“缺少运行时”或“控件未注册”
把程序拷到新装的Windows电脑上,双击EXE报“运行时错误339”或“组件未正确注册”。原因不是litex_sqlite本身,而是VB6程序依赖的VB6运行时库MSVBVM60.dll在新系统里不一定预装。Windows 10之后,微软不再默认带VB6运行时,新装的精简版系统尤其常见。
解决:发布时把VB6运行库(VB6运行库下载包里那套)一起分发,安装程序里加一步静默安装,或者直接把MSVBVM60.dll放到程序目录。更隐蔽的是litex_sqlite依赖的VC运行时,如果封装DLL是用VC2015编译的,目标机器还得有对应的vcruntime140.dll。我的习惯是发版前在一台干净虚拟机里测安装包,保证从裸系统到程序可用,不依赖客户机器预装任何东西。这种坑不是代码问题,是分发打包的问题,但客户只会归咎于“程序装不上”。
6. 进阶:封装一个通用查询模块,并用DB Browser验证数据
如果你已经跑通上面这些,最后一步是把访问层收敛到一个公共模块里,让业务代码只拼SQL字符串,不直接碰句柄。最简单的封装就是两个函数:一个执行不返回结果的SQL,一个执行查询并返回结果集句柄。
' 通用执行函数 Public Function DBExec(dbPath As String, sql As String) As Long Dim hDB As Long hDB = litex_open(ShortPath(dbPath)) If hDB = 0 Then DBExec = -1 Exit Function End If DBExec = litex_exec(hDB, sql) litex_close hDB End Function ' 通用查询函数:调用方拿到rs后自己遍历,用完必须释放 Public Function DBQuery(dbPath As String, sql As String) As Long Dim hDB As Long hDB = litex_open(ShortPath(dbPath)) If hDB = 0 Then DBQuery = 0 Exit Function End If DBQuery = litex_query(hDB, sql) End Function这样业务层写起来很干净:DBExec "UPDATE stock SET qty=0 WHERE id=5",DBQuery取数据后逐行处理。缺点是一个查询开一次连接,但对VB6老系统来说,换来的是业务代码里没有一句Declare,后期找人维护时门槛低很多。实测下来,这种方式处理几千行数据毫无压力,日常单机业务足够。
写完代码,验证永远用DB Browser for SQLite。这个工具是SQLite生态里最常用的图形化客户端,可以打开你生成的.db文件,直接看表结构、浏览数据、执行任意SQL,还能检查WAL模式是否生效。程序生成数据后,我习惯用DB Browser打开看一眼,确认表存在、字段值对、没有多余的临时表,再交给客户。数据库文件用什么打开这个问题以后也不会再纠结——它就是SQLite的官方社区标配,免费,Windows和Linux都能跑。
说到教训,我最想重复的还是那句:老系统改造,稳定压倒一切,能少改就少改。用litex_sqlite把Access换成SQLite的那次改造,我前前后后花了三周,真正写代码只用四天,剩下时间全耗在DLL位数、中文路径和编码乱码这些环境问题上。如果你正在做同样的事,从最小用例开始,先跑通打开和建表,再写业务逻辑;DLL和运行库的版本号在发布清单里写清楚,别指望客户帮你排查环境问题。希望帮到你。
本文还有配套的精品资源,点击获取