简介:一款基于C#与WinForm开发的截图识别OCR桌面程序源码,面向希望在Windows桌面端实现“截图→识别→复制”完整流程的开发者,也适合对OCR接入、屏幕区域选择、结果编辑等场景做二次开发。程序支持点击识别并显示标注、选择屏幕区域截取、编辑修正识别结果、复制结果到剪贴板等基础功能,可直接编译运行,便于快速验证效果。需注意中文识别率不高,部分结果需结合编辑功能手动修正。资源包共124个文件,主要包含22个.cs源码文件、31个dll依赖库、6个traineddata识别训练数据,以及配置、资源与项目工程文件,整体约174.4MB,使用Visual Studio打开即可查看和修改工程结构。目前已有465人浏览学习。对于正在开发截图工具或研究WinForm界面交互的开发者,可借鉴其整体工程划分、OCR识别调用流程和结果处理逻辑,在此基础上扩展区域标注、批量识别、剪贴板集成等功能,减少从零搭建的成本。
1. OCR截图识别工具:WinForm桌面程序源码拆开讲
"截图识别"这个需求在桌面工具开发里出现频率比我预想的高得多。给内部业务系统做个截图取数的小工具,或者帮用户把图片里的文字批量转成文本,最省事的做法不是去调现成的截图软件,而是直接做一个WinForm窗口。
框选屏幕任意区域,调用OCR引擎识别文字,再把结果用标注框标回截图上,支持手动修改和复制到剪贴板——这份源码就是这样一个C#写的、可直接编译运行的WinForm桌面程序,用Visual Studio打开就能看到完整实现,改完能直接编译跑。
它适合两类人:想给现有工具加截图识字功能的WinForm开发者,以及需要一个完整参考案例、想把屏幕选区加OCR这条路走通的新手。识别逻辑跑在本地,不依赖在线服务,代码量不大,读一遍不吃力。
2. 源码结构解析:先分清哪些文件能删、哪些是核心
2.1 文件清单:一堆.cache文件不是源码
解压下载的压缩包后,第一眼会看到很多文件名带cache的文件,比如DesignTimeResolveAssemblyReferencesInput.cache、WinChopScreenToolOCR.csproj.GenerateResource.cache、CoreCompileInputs.cache、AssemblyReference.cache,还有applicationhost.config。第一次接触这套源码的人容易懵:这么多文件到底哪些是代码?我的建议是:除了.cs、.csproj、.sln、app.config,其他先当作"编译器产生的临时文件"处理,不必逐个研究。
这些.cache文件是Visual Studio编译过程中的缓存,记录程序集引用解析、资源生成等中间结果,每次编译都会重新生成,手工删除不影响任何功能。applicationhost.config是VS调试Web项目时用的IIS Express配置,WinForm程序运行时根本不会读它,它出现在项目里多半是建项目时VS顺手生成的,删掉也不影响编译。真正要保住的只有.csproj工程文件、.sln解决方案文件、.cs源文件,以及app.config。
| 文件 | 性质 | 处理方式 |
|---|---|---|
| *.cs | 源码 | 保留,核心逻辑 |
| *.csproj / *.sln | 工程文件 | 保留,双击打开 |
| app.config | 运行配置 | 保留,OCR语言和路径在这改 |
| *.cache | 编译缓存 | 可删除 |
| applicationhost.config | VS生成 | 运行不需要 |
| bin / obj 目录 | 编译输出 | 可删除重建 |
提示:把项目拷给别人前,先清掉bin、obj、*.cache,压缩包会小很多,对方打开也不会因为缓存里记录的旧路径报奇怪的问题。
2.2 屏幕选区:全屏遮罩窗体加鼠标框选
截图的入口一般是一个全屏半透明窗体。用户在这个窗体上按下鼠标左键,拖出一个矩形区域,松开后这个矩形就是要截取的区域。这个交互在WinForm里是很标准的写法:一个无边框、全屏、置顶的Form,把DoubleBuffered设为true避免拖动时闪烁,在OnMouseDown记录起点,在OnMouseMove里实时更新矩形并调用Invalidate触发重绘,在OnPaint里用红色画笔把当前选框画出来。
public class SelectForm : Form { private Point _startPoint; private Rectangle _selectRect; public SelectForm() { FormBorderStyle = FormBorderStyle.None; // 无边框 WindowState = FormWindowState.Maximized; // 全屏,别用普通大小 TopMost = true; // 置顶,盖住其他窗口 DoubleBuffered = true; // 双缓冲,选框拖动不闪烁 BackColor = Color.FromArgb(60, 0, 0, 0); // 半透明遮罩,A通道控制透明度 } protected override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _startPoint = e.Location; } base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _selectRect = GetRectangle(_startPoint, e.Location); // 规范化矩形 Invalidate(); // 触发OnPaint重绘 } base.OnMouseMove(e); } protected override void OnPaint(PaintEventArgs e) { if (_selectRect.Width > 0 && _selectRect.Height > 0) { using (Pen pen = new Pen(Color.Red, 2f)) { e.Graphics.DrawRectangle(pen, _selectRect); } } base.OnPaint(e); } private Rectangle GetRectangle(Point a, Point b) { // 鼠标往左/往上拖时,起点在终点右下,直接new会得到负宽高 int x = Math.Min(a.X, b.X); int y = Math.Min(a.Y, b.Y); int w = Math.Abs(a.X - b.X); int h = Math.Abs(a.Y - b.Y); return new Rectangle(x, y, w, h); } }几个容易忽略的参数:BackColor的A通道决定遮罩透过程度,太小看不清选框,太大看不到后面内容,50~80之间比较合适。DrawRectangle的Pen宽度用2f,在高DPI屏幕上看起来才不至于细成一条线。GetRectangle里对坐标做规范化是必须的,否则鼠标向左上方拖拽时矩形宽高会是负数,选框直接画不出来。这个窗体本身不负责截屏,它只负责输出一个Rectangle,截屏动作在用户松开鼠标后由主窗体触发。
2.3 OCR识别调用:从屏幕截取到文字输出
拿到Rectangle之后,主窗体用Graphics.CopyFromScreen把屏幕上对应区域取成Bitmap,然后交给OCR引擎处理。这种工具最常用的本地OCR方案是Tesseract引擎,C#里通过Tesseract的.NET封装包调用,语言包放在tessdata目录下,中文用chi_sim,英文用eng。
public string RecognizeRegion(Rectangle region) { using (Bitmap bitmap = new Bitmap(region.Width, region.Height)) { // 第一个参数是屏幕绝对坐标,第二个是画布起点,第三个是截取尺寸 using (Graphics g = Graphics.FromImage(bitmap)) { g.CopyFromScreen(region.Location, Point.Empty, region.Size); } // tessdata目录要放在exe同级,chi_sim+eng表示中英文混合识别 using (var engine = new TesseractEngine(@"tessdata", "chi_sim+eng", EngineMode.Default)) { engine.SetVariable("preserve_interword_spaces", "1"); using (var page = engine.Process(bitmap)) { string text = page.GetText(); return text; } } } }CopyFromScreen三个参数的含义:region.Location是源点(屏幕坐标系,单位像素),Point.Empty是目标画布左上角,region.Size是复制多少像素。如果你发现截出来的图和实际框选区域有偏移,多半和后面避坑部分讲到的DPI问题有关。TesseractEngine构造参数里,"chi_sim+eng"表示中英文混合识别,两个语言用加号连接,语言包必须已经下载并放到tessdata目录里,缺了会直接抛异常。preserve_interword_spaces这个变量控制是否保留词间空格,对含英文和数字的识别结果影响较大,建议设为1。engine.Process接收Bitmap对象,识别完成后page.GetText()返回整段文本。
2.4 结果标注与复制:把识别框画回截图
识别出文字之后,较好的效果是在截图预览上把每个词用红框标出来,就像主流截图工具那样。Tesseract的ResultIterator可以拿到每个词的坐标矩形,遍历后画到PictureBox上。同时结果文本放进一个TextBox里,用户可以直接修改,改完一键复制到剪贴板。
private List<Rect> GetWordBoxes(Page page) { List<Rect> boxes = new List<Rect>(); using (IResultIterator iter = page.GetIterator()) { iter.Begin(); do { if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out Rect box)) { boxes.Add(box); } } while (iter.Next(PageIteratorLevel.Word)); } return boxes; }遍历词矩形的写法基本固定:先Begin,再do-while循环,Next接收PageIteratorLevel作为粒度,Word表示按词取框,想按行取就传TextLine,按段落就传Block。TryGetBoundingBox返回的Rect是相对截取区域的坐标,画回同一张Bitmap上,标注框和文字本身就能对齐。有个细节容易漏:标注框坐标是相对截取区域的,不是相对整个屏幕的。如果你要把标注画到其他画布上,记得加上截取区域左上角的偏移量。复制逻辑用Clipboard.SetText就能满足,但偶尔会遇到剪贴板被占用的诡异问题,这个在避坑部分单独讲。
3. 编译与运行:Visual Studio里从打开到跑起来
3.1 环境准备:版本和依赖一次到位
这个项目是WinForm桌面程序,目标框架是.NET Framework,不是.NET Core。你得用支持WinForm的Visual Studio来打开,我用的是VS 2019社区版,VS 2017和2022也能打开。如果你用的是VS 2022,安装时记得勾选".NET 桌面开发"工作负载,否则打开WinForm工程会提示缺少组件。C#语言本身不用额外装什么,运行时框架用.NET Framework 4.x就行,项目属性里能看到具体版本。
Tesseract的封装包是通过NuGet引用的,打开工程后Visual Studio会在首次生成前自动还原。如果还原失败,常见原因是NuGet源被公司代理挡住,或者离线环境下没有本地包缓存。离线场景下我会手动下载对应的.nupkg文件放到本地源,或者干脆把另一台机器上还原好的packages目录一起拷过来。这里多说一句:别在NuGet里看到新版本就顺手升级,OCR封装包新旧版本之间的原生DLL可能不兼容,升级后编译通过但运行时报错,反而浪费时间。
3.2 编译步骤:从打开工程到看到主窗口
拿到源码后我习惯先清理再生成,避免上一台机器留下的缓存干扰。命令行方式如下,但你直接在VS里操作更省事,后面说:
# 进入源码根目录,先还原NuGet包 nuget restore WinChopScreenToolOCR.sln # 生成Debug版,平台用x64 msbuild WinChopScreenToolOCR.csproj /p:Configuration=Debug /p:Platform=x64nuget restore只做包还原,在VS里右键解决方案选"还原NuGet程序包"效果一样。msbuild的/p:Configuration和/p:Platform指定配置和平台,这里用x64是因为Tesseract的原生DLL有架构区分,AnyCPU在运行时可能踩到"未能加载文件或程序集"的坑。命令行编译的好处是能看到完整错误列表,缺点是要先配好msbuild环境变量;大多数情况直接在VS里Ctrl+Shift+B生成更方便。
VS里双击.sln打开,确认活动配置是Debug+x64,然后按F5。第一次运行如果窗口一闪而过就退出了,看输出窗口的错误信息,最常见的是tessdata目录缺失。正常情况主窗口出来,点击"识别文字"按钮后程序切到全屏遮罩状态,这时候拖出选区,松开鼠标经过短暂的识别过程,窗口里出现识别文本和标注框。
3.3 关键配置项:app.config里能改什么
OCR语言、语言包路径这类运行参数不会写死在代码里,而是放在app.config的appSettings节点,编译后变成exe同级的config文件,改起来不用重新编译。
<configuration> <appSettings> <!-- 识别语言:chi_sim中文简体,eng英文,加号组合 --> <add key="OcrLanguage" value="chi_sim+eng" /> <!-- 语言包目录,相对exe路径 --> <add key="TessDataPath" value="tessdata" /> <!-- 选区最小宽高,小于这个值提示重新框选 --> <add key="MinSelectSize" value="10" /> </appSettings> </configuration>OcrLanguage对应TesseractEngine构造函数的第二个参数,改成eng只识别英文,识别速度会快一些,误识别也少一些;TessDataPath决定语言包加载目录,默认是exe同级的tessdata;MinSelectSize是我后加的校验,用户不小心点了一下鼠标松开也算一个选区,不加上这个参数就会把1像素宽的矩形也丢给识别引擎。这几个参数都属于"运行期常改的东西",放在配置文件里比每次重新编译项目省事。
编译成功后,bin.x64.Debug目录下除了exe,还应该有Tesseract相关的DLL(封装包自带)、tessdata目录(含语言包)、exe.config。很多人自己机器上能跑,拷给同事就报错,十有八九是tessdata没跟着exe走。我一般会在工程文件里把语言包设为"如果较新则复制":
<Content Include="tessdata\chi_sim.traineddata"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content>真正交付时,连同exe、DLL、tessdata整体打包才有完整功能。还有一种交付坑:目标机缺.NET Framework运行库,双击exe报"未安装.NET Framework",要么给目标机装对应版本,要么打包时把运行库一起带上。选型阶段先问清楚运行环境,能省掉后面来回改的麻烦。
4. 避坑指南:五个常见翻车点排查记录
4.1 中文识别率低,常见字也认错
现象:截一张清晰的中文网页文字,识别结果里错字不少,有的段落完全没法用。这个项目简介里也提过中文识别率不高,不是个例。原因:本地OCR对中文的支持很大程度上取决于traineddata语言包质量,官方默认的chi_sim数据对非标准字体、低对比度、复杂背景都很敏感。解决:先从源头优化,截图前把目标区域放大再截,或者把图片做灰度和二值化预处理;其次给引擎设置白名单,如果场景固定是数字和字母,用engine.SetVariable("tessedit_char_whitelist", "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")能明显降低误识别;最后实在不行换训练质量更高的社区语言包,识别率比官方默认包高一截,但体积也更大,要实测后再决定。
4.2 编译报错:找不到Tesseract DLL或版本冲突
现象:生成时报错提示找不到Tesseract程序集,或者运行时报"未能加载文件或程序集"、"BadImageFormatException"。原因:NuGet包版本不统一,多个封装包混用,或者原生DLL没复制到输出目录。解决:卸载重装统一版本的Tesseract包,然后删掉bin和obj目录再重新生成。我习惯每次换NuGet版本后手动清一次这两个目录,Cache文件残留导致旧版本引用被复用的情况很常见。另一个案例是64位系统装了只支持32位的封装包,运行时抛BadImageFormatException,这种要把项目平台改成x86或x64,跟包的要求对齐,别让它停留在AnyCPU。
4.3 多显示器或高分屏:选区位置和实际识别区域错位
现象:在150%缩放的屏幕上框选一段文字,识别出来的内容和框选区域完全对不上,偏移量还和屏幕缩放比例成正比。原因:程序没声明DPI感知,Windows把坐标按缩放系数做了虚拟化,CopyFromScreen拿到的是虚拟化后的坐标,实际像素位置偏了。解决:给程序声明DPI感知,两种途径——在app.manifest里加PerMonitorV2的dpiAwareness声明,或者在Main入口调用SetProcessDPIAware()。我实际测试下来PerMonitorV2方式更稳,切换窗口时字体也不会突然模糊。如果是多显示器且分辨率不同,还要处理负坐标,CopyFromScreen支持负坐标源点,但要确认region.Location用的是屏幕坐标系绝对坐标,而不是窗体坐标。
4.4 复制到剪贴板偶发失败
现象:识别结果第一次复制正常,第二次点复制没反应或抛异常。原因:一是剪贴板被其他程序占用,OpenClipboard冲突;二是复制操作所在线程上下文不对。解决:用Clipboard.SetDataObject(text, true)代替SetText,第二个参数true表示退出时把数据转成可持久化格式,避免进程结束后内容丢失;再包一层重试逻辑,失败时等一两百毫秒重试两三次。如果异常信息里有"当前线程不在单线程单元中"字样,检查Main方法上有没有[STAThread]特性,WinForm程序的入口必须带这个特性,忘了会导致剪贴板相关的很多诡异问题。
4.5 识别结果为空,但截图看起来正常
现象:选区没错、图片也截出来了,但识别的文字是空字符串。原因:图片本身没问题,问题出在识别前没做最基本的图像处理。深色背景白字就是典型的翻车场景,Tesseract默认按黑字白底设计,直接丢进去大概率一个字符都出不来。解决:识别前先做反色判断。统计图片平均亮度,背景偏暗说明大概率是白字黑底,遍历像素做反色处理再识别:
// 反色:遍历像素点,RGB三通道都取 255 - value for (int y = 0; y < gray.Height; y++) { for (int x = 0; x < gray.Width; x++) { Color c = gray.GetPixel(x, y); int v = 255 - c.R; // 灰度图三通道值相同 gray.SetPixel(x, y, Color.FromArgb(v, v, v)); } }这个逻辑不复杂但非常关键,很多OCR工具实际使用中"识别不出来"的反馈,最后查下来都是卡在这。GetPixel和SetPixel逐像素操作在小图上没压力,图大了会慢,生产环境建议换LockBits。另外选区里混着大量非文字元素也会导致结果为空,这种只能靠用户框选时尽量圈住文字区域,程序侧用MinSelectSize校验拦截无效小矩形。
5. 在源码基础上做扩展:快捷键、自动保存、换OCR引擎
5.1 注册全局快捷键,一键触发截图
现在的流程是点按钮再框选,实际场景里用户更习惯按个快捷键直接呼出工具。Windows下用RegisterHotKey注册系统级热键,程序收到WM_HOTKEY消息后触发截图动作。
[DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); // Alt+1触发截图,0x0001是MOD_ALT RegisterHotKey(this.Handle, 1, 0x0001, (uint)Keys.D1);注册热键要在窗体句柄创建之后,一般放在OnShown里。id参数是自定义的,回调时靠它区分不同热键。WM_HOTKEY消息需要重写WndProc处理,在消息号0x0312的分支里启动截图流程:
protected override void WndProc(ref Message m) { // 0x0312 是 WM_HOTKEY 的消息号 if (m.Msg == 0x0312 && m.WParam.ToInt32() == 1) { StartCapture(); // 触发截图 } base.WndProc(ref m); }RegisterHotKey注册的是系统级热键,和其他软件冲突时返回false,所以注册后一定要检查返回值并提示用户换键。关闭程序时记得用UnregisterHotKey注销,不然热键残留会导致下次注册失败。
5.2 识别结果自动保存PNG和TXT
工具自己用还好,给别人用加自动保存会很实用。截图和识别结果按时间戳命名存到指定目录,一份原始截图PNG,一份结果TXT。路径用Environment.GetFolderPath拿用户目录下的固定文件夹,避免写死当前路径导致权限问题。
string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string basePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "OCRResult"); Directory.CreateDirectory(basePath); // 目录不存在会抛异常,先建 bitmap.Save(Path.Combine(basePath, $"shot_{stamp}.png"), ImageFormat.Png); File.WriteAllText(Path.Combine(basePath, $"text_{stamp}.txt"), recognizedText, Encoding.UTF8);几个细节:时间戳精确到秒,连续识别两次也能区分;TXT用UTF8写入,避免默认编码在系统区域设置改动后乱码。想保留标注痕迹的话,把GetWordBoxes拿到的词矩形列表画到Bitmap上再Save一次,就能存一张"哪些字被认出来"的预览图。
5.3 换识别引擎:本地Tesseract换云端OCR的改造点
本地OCR的优点是离线可用、不花钱、数据不外传,缺点是中文识别率上限有限。如果你对准确率要求高,常见做法是把识别方法整体换成云端OCR接口。改造点其实不大,前提是识别逻辑收敛成了统一方法,换引擎时窗体和选区代码完全不用动,只改方法内部:
public string RecognizeRegion(Rectangle region) { using (Bitmap bitmap = new Bitmap(region.Width, region.Height)) { // 截屏逻辑略,与前面一致 return CloudOcrRecognize(bitmap); // 换成云端接口 } }云端接口的典型套路是:Bitmap转Base64,POST到识别URL,解析返回JSON里的文本字段,按顺序拼接。AccessToken或API Key要配置到app.config里,别硬编码在代码中。要注意的是本地引擎和云端引擎返回的坐标体系不一样,如果你依赖Tesseract返回的词矩形做标注框,切换后渲染标注的代码也得跟着改。
| 对比项 | 本地Tesseract | 云端OCR |
|---|---|---|
| 联网依赖 | 无需联网 | 必须联网 |
| 中文识别率上限 | 中上,受语言包限制 | 高,训练数据更充分 |
| 数据安全 | 图片不出本机 | 图片需上传 |
| 调用成本 | 无 | 按次计费 |
| 集成工作量 | 引包即可 | 申请Key、处理HTTP |
我个人建议在封装层定义一个统一识别结果模型,包含文本和词框列表,本地和云端各写一个适配器,以后换引擎只加适配器不动窗体逻辑。识别是个阻塞操作,期间UI会卡住,顺手加个进度条提示或者用Task.Run放到后台线程,体验会好很多,这也是WinForm项目里常见的界面细节改造点。
6. 验证方法:三个固定测试场景确认工具能交付
6.1 三个测试场景
我拿到任何OCR源码都会先跑三个固定场景再谈优化。第一个是白底黑字的纯文本截图,比如在记事本里打一段话再截它,最基础,识别率应该接近全对。第二个是软件界面截图,带菜单、按钮、深浅不一的背景,用来验证复杂背景下的检出能力。第三个是中英文混合文本,带英文缩写和数字,验证语言包混合识别效果和空格处理。
6.2 准确率怎么估
小样本不用写脚本统计,肉眼对比即可。先数源文本总字数,再数识别结果里错误或缺失的字数,正确率约等于1减错字比例。低于85%直接进入预处理调试,不做参数微调,因为根源大概率在图像质量而不是引擎参数。
6.3 性能与内存
本地OCR的耗时主要在语言包加载,第一次识别慢很正常,引擎初始化结束后后面就快了。嫌初始化慢可以把引擎实例做成单例复用,而不是每次识别都重新new。连续识别几十张图后出现卡顿,检查有没有及时释放Bitmap和Page对象,using包裹是最省心的写法。
从那以后我每次拿到这类OCR源码,第一件事就是先跑一遍这三个场景再动手改代码,识别率实测过才敢给需求方回复交付时间。改OCR参数也是个玄学活,语言包、预处理、白名单、DPI四件事先排掉,再谈准确率,顺序反了容易白调半天。希望帮到你。
本文还有配套的精品资源,点击获取