
简介这份源码资源面向需要在 Windows 平台下开发扫描仪控制程序的 VC 开发者基于 TWAIN 协议实现完整的图像采集流程涵盖从初始化数据源管理器、枚举并选择扫描设备到设置分辨率与扫描区域、启动扫描、处理位图数据以及关闭会话释放资源的全过程。资源包共 66 个文件约 21.79MB以 h 头文件、cpp 源文件、obj 编译中间文件、rc 资源脚本、vcxproj 工程配置及 sln 解决方案为主另含 manifest、pdb、ico 等辅助文件工程结构完整可直接在 Visual Studio 中打开编译调试。目前已有 1574 人学习下载。通过研读源码读者能够掌握 TWAIN 接口的调用方式、扫描参数的用户界面控制逻辑以及扫描结果的处理思路对图像处理、文档数字化等方向的 Windows 应用开发具有实际参考价值。1. 扫描仪对接这件事为什么TWAIN到今天还在用接手一个文档电子化项目时最容易被低估的环节就是扫描仪对接。业务方觉得“点一下扫描按钮”就完事实际动手才发现驱动型号五花八门、厂商SDK各不相同、有的设备只认TWAIN、有的只认WIA还有的两种都支持但行为不一致。VC 使用TWAIN协议实现扫描仪完整源码这个方向解决的正是“不依赖厂商私有SDK、用一套通用接口把扫描仪接进自己程序”的问题。TWAIN是一套跨厂商的图像采集接口标准Windows上绝大多数平板扫描仪、馈纸式扫描仪都支持它。它适合谁适合需要在MFC或Win32桌面程序里内嵌扫描功能、又不想为每个品牌单独适配的开发者。核心思路是你的程序作为“数据源管理器”的客户端通过一个动态库加载目标扫描仪的数据源用消息回调驱动整个扫描流程。听起来绕但拆开看就是几个固定步骤。2. TWAIN的三层结构和消息驱动模型2.1 数据源管理器、数据源、应用程序各管什么TWAIN的架构是三层应用程序你的VC程序、数据源管理器DSM通常是一个叫TWAIN_32.DLL或TWAINDSM.DLL的动态库、数据源DS每个扫描仪厂商提供的驱动。应用程序不直接跟扫描仪对话而是先加载DSM由DSM去枚举和打开具体的DS。这个设计的好处是应用程序只需要认识DSM这一套接口换扫描仪时只要换DS就行。三层之间的通信靠两种机制一种是调用DSM导出的函数比如DSM_Entry另一种是DS向应用程序窗口发送消息。消息驱动是TWAIN最容易让人懵的地方——扫描不是“调用一个函数然后等返回”而是“发一个命令然后等DS往你的窗口发消息你在消息处理里回应”。整个流程像一场来回传球而不是一次直线调用。我一般会把状态机画在纸上再写代码。TWAIN定义了7个状态从状态1DSM未加载到状态7数据传输中。每个操作都要求当前状态匹配状态不对就返回失败。很多新手翻车就翻在这里没打开DS就直接调能力设置或者传输没结束就关DS结果程序卡死或者扫描仪报错。2.2 从状态1到状态7一次扫描要经过哪些状态状态1是初始态DSM还没加载。调用DSM_Entry的MSG_OPENDSM后进入状态2此时DSM已加载但没打开任何DS。接着MSG_OPENDS打开指定扫描仪进入状态3。状态3是“DS已打开但未启用”这时可以查询和设置能力比如分辨率、色彩模式、扫描区域。调用MSG_ENABLEDS后进入状态4DS开始工作。如果DS需要显示自己的配置界面会进入状态5如果直接开始传输图像进入状态6。状态6是数据传输中DS会不断发消息告诉你有一张图传过来了。传输完成后回到状态5或状态4最后MSG_DISABLEDS回到状态3MSG_CLOSEDS回到状态2MSG_CLOSEDSM回到状态1。这套状态机是TWAIN的骨架。写代码时我会用一个变量记录当前状态每次调用DSM_Entry前先检查状态是否允许。下面这张表是我常用的状态与操作对照放在手边随时查。当前状态允许的操作典型调用1加载DSMMSG_OPENDSM2打开DS、查询DSM能力MSG_OPENDS3设置能力、启用DSMSG_ENABLEDS、MSG_GET/SET4传输图像、禁用DSMSG_ENABLEDS(带传输)、MSG_DISABLEDS5传输图像、关闭UIMSG_ENABLEDS6接收数据、结束传输处理MSG_XFERREADY7结束传输MSG_ENDXFER2.3 消息回调为什么你的窗口过程要处理TWAIN消息DS向应用程序发消息时用的是你注册时提供的窗口句柄。所以你的主窗口或一个隐藏窗口的WindowProc里必须处理几个TWAIN专用消息。最核心的是MSG_XFERREADY表示DS已经准备好一张图像等你来取。还有MSG_CLOSEDSREQ表示DS请求关闭MSG_DEVICEEVENT表示设备事件比如扫描仪上的按钮被按下。处理这些消息时不能做耗时操作否则会阻塞DS。我一般只做标记把实际的数据获取放到主循环或单独线程里。下面是一个窗口过程里处理TWAIN消息的骨架// 假设已经定义了全局的TWAIN会话结构 LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_TWAIN_XFERREADY: // 自定义消息由DS发来 // 只做标记不在这里做耗时传输 g_bXferReady TRUE; PostMessage(hWnd, WM_APP 1, 0, 0); // 投递到主循环处理 return 0; case WM_TWAIN_CLOSEDSREQ: // DS请求关闭需要走禁用和关闭流程 g_bCloseRequest TRUE; PostMessage(hWnd, WM_APP 2, 0, 0); return 0; case WM_TWAIN_DEVICEEVENT: // 设备事件比如扫描仪按钮 // 一般记录事件类型按需处理 return 0; default: return DefWindowProc(hWnd, msg, wParam, lParam); } }这段代码的关键点是窗口过程只做消息分发不直接调用DSM_Entry去取图像。因为取图像可能触发DS再次发消息如果在窗口过程里递归调用容易造成死锁或状态混乱。用PostMessage投递到主消息队列让主循环在合适的状态下处理是更稳的做法。参数方面WM_TWAIN_XFERREADY这类消息的数值需要根据TWAIN头文件里的定义来不同版本的DSM可能略有差异但核心几个是固定的。3. 用VC把TWAIN会话跑起来的最小实现3.1 加载DSM和打开数据源的完整代码第一步是加载DSM。TWAIN的DSM通常以动态库形式存在32位程序用TWAIN_32.DLL64位程序用TWAINDSM.DLL。加载后拿到DSM_Entry函数指针后续所有操作都通过它。下面是我常用的加载和打开DS的代码// 定义DSM_Entry函数指针类型 typedef TW_UINT16 (FAR PASCAL *DSMENTRYPROC)(pTW_IDENTITY, pTW_IDENTITY, TW_UINT32, TW_UINT16, TW_UINT16, TW_MEMREF); DSMENTRYPROC g_pDSMEntry nullptr; TW_IDENTITY g_AppIdentity {0}; TW_IDENTITY g_SrcIdentity {0}; TW_UINT16 g_CurrentState 1; // 初始状态 bool LoadDSM() { // 根据程序位数选择DSM库名 const char* dsmName TWAINDSM.DLL; // 64位 HMODULE hDSM LoadLibraryA(dsmName); if (!hDSM) { // 尝试32位库名 hDSM LoadLibraryA(TWAIN_32.DLL); if (!hDSM) return false; } g_pDSMEntry (DSMENTRYPROC)GetProcAddress(hDSM, DSM_Entry); if (!g_pDSMEntry) return false; // 填充应用程序标识 g_AppIdentity.Id 0; g_AppIdentity.Version.MajorNum 1; g_AppIdentity.Version.MinorNum 0; g_AppIdentity.Version.Language TWLG_CHINESE_SIMPLIFIED; g_AppIdentity.Version.Country TWCY_CHINA; strcpy_s(g_AppIdentity.Version.Info, 1.0.0); strcpy_s(g_AppIdentity.ProductName, MyScanApp); strcpy_s(g_AppIdentity.Manufacturer, MyCompany); strcpy_s(g_AppIdentity.ProductFamily, Document); g_AppIdentity.ProtocolMajor TWON_PROTOCOLMAJOR; g_AppIdentity.ProtocolMinor TWON_PROTOCOLMINOR; g_AppIdentity.SupportedGroups DG_IMAGE | DG_CONTROL; // 打开DSM状态1 - 状态2 TW_UINT16 rc g_pDSMEntry(g_AppIdentity, nullptr, DG_CONTROL, DAT_PARENT, MSG_OPENDSM, (TW_MEMREF)g_hWnd); if (rc ! TWRC_SUCCESS) return false; g_CurrentState 2; return true; } bool OpenDataSource() { if (g_CurrentState ! 2) return false; // 打开默认数据源状态2 - 状态3 TW_UINT16 rc g_pDSMEntry(g_AppIdentity, nullptr, DG_CONTROL, DAT_IDENTITY, MSG_OPENDS, (TW_MEMREF)g_SrcIdentity); if (rc ! TWRC_SUCCESS) return false; g_CurrentState 3; return true; }逻辑说明LoadDSM先尝试加载64位DSM库失败再试32位。拿到DSM_Entry后填充应用程序标识这个标识告诉DSM是谁在调用。MSG_OPENDSM需要传入一个窗口句柄DS后续的消息会发到这个窗口。OpenDataSource用MSG_OPENDS打开默认数据源如果机器上有多个扫描仪可以先枚举再选择。参数方面g_AppIdentity里的ProtocolMajor和ProtocolMinor要和DSM支持的版本匹配一般用头文件里的宏。SupportedGroups声明你支持的能力组DG_IMAGE和DG_CONTROL是必须的。3.2 设置分辨率、色彩模式和扫描区域打开DS后状态3下可以设置能力。TWAIN的能力用“能力容器”管理每个能力有一个ID比如ICAP_XRESOLUTION是水平分辨率ICAP_PIXELTYPE是像素类型。设置能力用MSG_SET查询用MSG_GET。下面是一个设置分辨率和色彩模式的例子bool SetResolution(TW_UINT16 xRes, TW_UINT16 yRes) { if (g_CurrentState ! 3) return false; // 设置水平分辨率 TW_UINT16 rc g_pDSMEntry(g_AppIdentity, g_SrcIdentity, DG_IMAGE, DAT_IMAGEINFO, MSG_SET, (TW_MEMREF)xRes); // 注意实际设置分辨率用的是ICAP_XRESOLUTION这里简化示意 // 正确做法是构造TW_CAPABILITY结构 return rc TWRC_SUCCESS; }上面这段是简化示意实际设置能力需要构造TW_CAPABILITY结构指定能力ID、容器类型和值。我一般封装一个SetCapability函数把能力ID和值传进去内部处理容器分配和释放。色彩模式常用的是TWPT_RGB彩色、TWPT_GRAY灰度、TWPT_BW黑白。扫描区域用ICAP_FRAMES设置单位是英寸的千分之一。这些参数设不好扫出来的图要么太大要么太小要么颜色不对。提示设置能力前先用MSG_GET查询当前值和支持范围不要盲目设一个超出范围的值否则DS可能返回失败或者用默认值覆盖。3.3 触发扫描并接收图像数据设置完能力后调用MSG_ENABLEDS进入状态4或5。如果传入的TW_USERINTERFACE结构里ShowUI为TRUEDS会弹出自己的配置界面用户点“扫描”后开始传输。如果ShowUI为FALSEDS直接开始传输。传输过程中DS会发MSG_XFERREADY消息你在窗口过程里标记后在主循环里调用MSG_GET获取图像数据。获取图像数据用DG_IMAGE / DAT_IMAGENATIVEXFER拿到的是一个句柄里面是设备无关位图DIB格式的数据。你可以把它保存成BMP文件或者转成其他格式。下面是一个获取并保存BMP的简化流程void OnXferReady() { if (g_CurrentState ! 6 g_CurrentState ! 5) return; HANDLE hBitmap nullptr; TW_UINT16 rc g_pDSMEntry(g_AppIdentity, g_SrcIdentity, DG_IMAGE, DAT_IMAGENATIVEXFER, MSG_GET, (TW_MEMREF)hBitmap); if (rc TWRC_XFERDONE hBitmap) { // hBitmap是DIB数据可以保存为BMP SaveDIBToBmp(hBitmap, scan_output.bmp); GlobalFree(hBitmap); } // 结束传输状态6 - 状态5或4 g_pDSMEntry(g_AppIdentity, g_SrcIdentity, DG_CONTROL, DAT_PENDINGXFERS, MSG_ENDXFER, nullptr); }逻辑说明MSG_GET拿到的是DIB句柄不是HBITMAP需要按DIB格式解析文件头和信息头。SaveDIBToBmp是我自己封装的函数把DIB数据加上BMP文件头写盘。传输结束后必须调MSG_ENDXFER否则DS会一直停在传输状态。如果有多页MSG_ENDXFER后DS可能再次发MSG_XFERREADY循环处理即可。4. 多页扫描、内存管理和UI线程的避坑记录4.1 现象扫第二页时程序卡死原因第一页传输结束后没有正确调用MSG_ENDXFER或者调用后没有检查返回值。DS在等待应用程序确认传输结束应用程序却在等DS发下一张的消息双方互相等死锁。解决每次MSG_GET成功后无论是否还有下一页都要调MSG_ENDXFER。如果返回值是TWRC_SUCCESS且后续还有页DS会再发MSG_XFERREADY如果返回值里带TWRC_XFERDONE说明没有更多页了。4.2 现象扫描大图时内存暴涨原因每次MSG_GET返回的DIB句柄需要应用程序自己释放。如果只保存不释放或者保存后忘记GlobalFree内存会持续增长。另外如果开了多页扫描每页都保留在内存里也会爆。解决每页处理完立即释放句柄需要多页合并的话只保留必要的数据或者直接写盘。我一般扫一页存一页不在内存里堆。4.3 现象DS的配置界面弹不出来原因调用MSG_ENABLEDS时传入的窗口句柄不对或者ShowUI设了FALSE。另外如果DSM加载的是32位版本而程序是64位或者反过来也会导致界面无法显示。解决确认程序位数和DSM库位数一致。ShowUI根据需求设置需要用户选参数就设TRUE。窗口句柄用主窗口的不要用临时窗口。4.4 现象设置分辨率后扫描仪没反应原因能力设置失败但没检查返回值。有些扫描仪不支持某些分辨率或者支持的范围和你想的不一样。解决设置前先MSG_GET查询支持的范围在范围内取值。设置后检查返回值失败就回退到默认值。不要假设所有扫描仪都支持300dpi或600dpi。4.5 现象程序退出时扫描仪灯不灭原因没有按顺序关闭DS和DSM。正确的关闭顺序是MSG_DISABLEDS状态4/5到3、MSG_CLOSEDS状态3到2、MSG_CLOSEDSM状态2到1。漏掉任何一步DS可能保持打开状态。解决在程序退出或扫描完成时按状态机顺序逐步关闭每步检查返回值。5. 把TWAIN封装成可复用类的几个技巧5.1 用状态机类管理会话生命周期把状态变量、DSM句柄、DS标识封装到一个类里所有操作作为成员函数每次调用前检查状态。这样调用方不需要关心状态转换细节只调Open、SetCap、Scan、Close几个方法。我一般会写一个CTwainSession类构造函数里加载DSM析构函数里确保关闭。状态转换用私有方法失败时记录错误码。5.2 异步扫描与进度反馈TWAIN的消息机制天然适合异步。把扫描放在工作线程里主线程处理UI。工作线程调用MSG_ENABLEDS后进入消息循环收到MSG_XFERREADY就取数据取完发通知给主线程更新进度。注意DSM_Entry不是线程安全的所有TWAIN调用要在同一个线程里。我一般把TWAIN会话固定在一个线程其他线程通过消息和它通信。5.3 兼容不同DSM版本的注意事项不同版本的DSM对某些能力的支持不一样。比如老版本可能不支持ICAP_AUTODISCARDBLANKPAGES新版本支持。调用前先MSG_GET查询返回TWRC_FAILURE就跳过。另外32位和64位DSM的库名不同加载时要根据编译目标选择。如果程序需要同时支持两种可以用条件编译或者运行时判断。5.4 一个可复用的能力设置封装下面这个函数封装了能力设置传入能力ID和值内部处理容器分配和释放bool SetCapability(TW_UINT16 capId, TW_UINT16 type, void* pValue, TW_UINT32 size) { if (g_CurrentState ! 3) return false; TW_CAPABILITY cap {0}; cap.Cap capId; cap.ConType TWON_ONEVALUE; cap.hContainer GlobalAlloc(GHND, sizeof(TW_ONEVALUE) size); if (!cap.hContainer) return false; pTW_ONEVALUE pVal (pTW_ONEVALUE)GlobalLock(cap.hContainer); pVal-ItemType type; memcpy(pVal-Item, pValue, size); GlobalUnlock(cap.hContainer); TW_UINT16 rc g_pDSMEntry(g_AppIdentity, g_SrcIdentity, DG_CONTROL, DAT_CAPABILITY, MSG_SET, (TW_MEMREF)cap); GlobalFree(cap.hContainer); return rc TWRC_SUCCESS; }逻辑说明TW_CAPABILITY结构里ConType指定容器类型TWON_ONEVALUE表示单值。hContainer是全局内存句柄里面放TW_ONEVALUE结构加实际值。设置完必须释放容器否则内存泄漏。参数capId是能力IDtype是值类型如TWTY_UINT16pValue指向实际值size是值的大小。这个封装把容器管理藏起来调用方只需要关心能力ID和值。5.5 验证扫描结果是否正确的几个检查点扫出来的图不对先查这几个地方分辨率设置是否生效看图像像素尺寸和预期是否一致、色彩模式是否正确彩色图是不是灰度了、扫描区域是否覆盖了文档有没有截断。我一般会在保存图像后打印图像宽高和位深和预期对比。如果DS支持还可以用ICAP_PIXELTYPE查询当前像素类型确认设置生效。这套TWAIN方案我用了几年最大的教训是不要假设扫描仪会按你想的行为工作。每台设备的能力支持范围、默认值、对异常状态的反应都不一样。写代码时多查返回值多打日志遇到问题先看状态机走到哪一步了。希望帮到你。本文还有配套的精品资源点击获取