
最近经常要在文档、网页和聊天记录之间搬运文字截图里的一段报错、一段代码、一个表格手动敲一遍效率太低用QQ截图自带的识别功能又总觉得差点意思识别结果偶尔还会自动带上表情或者多出换行。后来我索性用C# Winform调百度AI的OCR接口自己写了个文字提取小工具按快捷键框选屏幕区域松开鼠标文字就进剪贴板了整个过程不到一秒。这项目技术上不复杂但非常实用适合刚学完C#基础想找个完整练手项目的朋友也适合想解决日常办公痛点又不想用臃肿商业软件的人。这篇博文我会把整个工具的设计思路、百度AI接入流程、核心代码和踩坑记录都摊开讲完全按我实际开发的过程来写你照着敲一遍就能跑起来。1. 为什么要自己写一个文字提取工具1.1 现有截图工具到底缺了什么我不是说QQ截图和微信截图不好用它们胜在“随手就有”但真到了高频提取文字的场合几个问题就藏不住了第一是识别入口太深。QQ截图默认要按CtrlAltA截完图之后还要点一下“屏幕识文”按钮识别结果再弹一个小窗你再选中复制。这一套流程在偶尔用一次的时候能接受但在连续处理几十张截图的时候就显得特别啰嗦。第二是结果不可控。系统自带的OCR经常会智能有时候是自作聪明地把识别结果处理成“方便你直接发消息”的样子比如每一行自动加换行、繁体字切换、符号替换但我想拿到的是原始排版、原始字符尤其是代码片段和日志信息多一个空格都影响调试。第三是批量处理能力为零。截图工具的OCR一次只能处理当前截的这一张如果手头有几十张图片要统一提取文字那就得一张一张截、一张一张识没有任何扩展空间。想明白这几点之后我的目标就很清晰了做一个按快捷键就能框选屏幕、松开鼠标就完成OCR识别、结果自动进剪贴板的轻量工具。配合C# Winform做前端界面和交互百度AI开放平台做OCR识别引擎两个搭配起来正好覆盖我所有的需求。1.2 方案选型为什么是C# Winform加百度AI选C# Winform这个技术栈我自己权衡过两个备选方案。一个是WPF界面表现力确实比Winform强动画和自定义样式都方便但问题是很多老电脑、服务器环境跑WPF反而容易出现兼容问题Winform在Windows平台上是“万年稳”的存在。另一个是Python加PyQt写脚本确实快OCR这块Python生态也成熟但最终要打包成给同事用的exePyQt打包体积大、启动速度也一般而且很多公司内网机器的Python环境不全。C# Winform编译出来一个小体积exe拷过去就能跑不依赖任何运行时用.NET Framework的话对办公场景是最省心的。百度AI相比其他OCR方案的优势也很明显。本地OCR方案比如Tesseract识别中文效果一般尤其是手写体和带背景的字体而且配置引擎、训练字库的门槛不低。在线OCR方案里百度AI通用文字识别标准版每天有500次免费额度个人日常使用根本用不完识别速度和准确率在中文场景下实测是第一梯队。接口文档也做得清楚C#调用也就是几个HTTP请求的事。整体思路可以概括成一句话快捷键唤起截图遮罩层鼠标框选区域后截取屏幕图片交给百度AI识别返回的文本自动写入剪贴板并弹出结果窗口。接下来每一步我都会讲细节。2. 开发前准备环境和百度AI开放平台接入2.1 开发环境搭建与项目创建开发工具我用的是Visual Studio 2019你装2019或者2022都行社区版就够用。项目类型选择“Windows窗体应用(.NET Framework)”目标框架我选.NET Framework 4.7.2主要是考虑兼容性Win7及以上系统都能跑。你如果只想在Win10/11上用选.NET 6或.NET 8的“Windows窗体应用”也没问题代码逻辑基本一致就是包管理方式和入口函数略有不同。项目创建好之后先用NuGet装一个包Newtonsoft.Json后面解析百度返回的JSON数据要用的。操作很简单右键项目 → 管理NuGet程序包 → 搜索Newtonsoft.Json → 安装。如果你用的是.NET 5以上的版本也可以用System.Text.Json替代但Newtonsoft.Json在Windows老项目里更通用我习惯用它。界面设计上我保持极简主窗体只放一排按钮包括“开始识别”(按钮绑定快捷键)、一个文本框用于手工粘贴图片路径再加一个历史记录列表。真正的主角是一个全屏遮罩窗口这个窗口不显示标题栏背景半透明鼠标拖拽画一个矩形框这就是截图区域。界面美化的部分我建议第二版再做第一版先保证功能跑通。2.2 百度智能云OCR服务开通与凭证获取这一步是唯一需要联网操作的环节其实很简单。你先在浏览器打开百度智能云的控制台用百度账号登录然后按下面步骤操作在产品列表里找到“文字识别OCR”进入后选择“创建应用”。应用名称随便填比如“MyOCR”类型选“个人”接口选择默认通用文字识别/标准版。创建完成后在“应用列表”里能看到三个关键信息API Key、Secret Key、应用ID。这三个东西就是后续调接口的凭证。接下来要做的是“开通”OCR服务。百度的OCR是按接口单独开通的我们去“文字识别”页面找到“通用文字识别标准版”点“领取免费额度”或者“开通服务”即可。新用户一般有几百到几千次免费调用个人用完全足够。有个细节必须提醒一下API Key和Secret Key等同于你账户的钥匙千万别写死在代码里之后又随意把代码传到公开仓库。我见过有人把密钥提交到GitHub然后被刷爆额度的。可以把密钥放到一个单独的配置文件中并在.gitignore里忽略掉这个文件。2.3 认识AccessToken的获取逻辑百度OCR接口的鉴权方式不是直接拿API Key调识别接口而是要用API Key和Secret Key去换一个AccessToken然后每次识别请求都带上这个AccessToken。AccessToken有效期是30天过期之后需要重新获取。换取AccessToken的HTTP请求是标准的OAuth 2.0可以用浏览器直接验证https://aip.baidubce.com/oauth/2.0/token?grant_typeclient_credentialsclient_id你的APIKeyclient_secret你的SecretKey正常的话会返回一段JSON其中access_token字段就是我们要的东西。我在程序里封装了一个静态方法每次启动时检查缓存的AccessToken不存在或者过期就重新请求代码如下public static class BaiduAuth { private static string _accessToken ; private static DateTime _expireTime DateTime.MinValue; public static async Taskstring GetAccessTokenAsync(string apiKey, string secretKey) { if (!string.IsNullOrEmpty(_accessToken) DateTime.Now _expireTime) { return _accessToken; } string url $https://aip.baidubce.com/oauth/2.0/token?grant_typeclient_credentialsclient_id{apiKey}client_secret{secretKey}; using (HttpClient client new HttpClient()) { var resp await client.GetStringAsync(url); var json JObject.Parse(resp); _accessToken json[access_token]?.ToString(); int expiresIn json[expires_in]?.ToObjectint() ?? 2592000; _expireTime DateTime.Now.AddSeconds(expiresIn - 600); // 提前10分钟过期避免临界点 } return _accessToken; } }这里“提前10分钟过期”是我在实际使用中踩过坑之后加的逻辑。因为如果正好卡在过期时间点调用接口容易偶发401提前重置时间可以避开这个临界问题。3. 核心功能拆解与实现过程3.1 截图功能全屏遮罩与鼠标框选整个工具最核心的交互就是框选屏幕区域这一块用Winform实现其实很直白。第一步按快捷键唤起截图窗口。我使用的是全局快捷键这样即使工具主窗体在后台、最小化按下快捷键也能立刻响应。Winform里实现全局快捷键最简单的办法是调用Win32的RegisterHotKey函数。先在主窗体加载时注册一个组合键比如CtrlShiftZ[DllImport(user32.dll)] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); const int HOTKEY_ID 0x0001; const uint MOD_CONTROL 0x0002; const uint MOD_SHIFT 0x0004; const uint VK_Z 0x5A; protected override void WndProc(ref Message m) { base.WndProc(ref m); if (m.Msg 0x0312 m.WParam.ToInt32() HOTKEY_ID) // WM_HOTKEY { StartCapture(); } }这里0x0312是Windows系统定义的WM_HOTKEY消息当用户按下注册的快捷键时这个消息会发送到注册窗口的消息循环中。StartCapture方法就是创建并显示截图遮罩窗体。第二步截图遮罩窗体。其实是一个全屏无边框窗口背景色为黑色透明度设为0.3左右鼠标在上面按下并拖拽动态计算一个矩形区域并在窗体上把这个矩形画出来。为了让用户看清楚选中区域一般做法是把选中区域用白色边框框起来同时把选中区域之外的暗色加深。具体实现上我重写了遮罩窗体的OnMouseDown、OnMouseMove和OnMouseUp事件。拖拽过程中记录起点坐标和当前坐标通过Invalidate触发重绘在OnPaint事件里用Pen画矩形边框private Point _startPoint; private Rectangle _selectRect; protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (e.Button MouseButtons.Left) { _startPoint e.Location; } } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (e.Button MouseButtons.Left) { _selectRect GetRectangle(_startPoint, e.Location); Invalidate(); // 触发重绘 } } private Rectangle GetRectangle(Point p1, Point p2) { int x Math.Min(p1.X, p2.X); int y Math.Min(p1.Y, p2.Y); int width Math.Abs(p1.X - p2.X); int height Math.Abs(p1.Y - p2.Y); return new Rectangle(x, y, width, height); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); using (Pen pen new Pen(Color.White, 2f)) { e.Graphics.DrawRectangle(pen, _selectRect); } }鼠标松开时根据_selectRect区域调用截图方法把屏幕上对应矩形区域的内容抓成Bitmap然后关闭遮罩窗体。第三步抓取屏幕区域。这里用的是Graphics.CopyFromScreen方法private Bitmap CaptureScreen(Rectangle rect) { Bitmap bmp new Bitmap(rect.Width, rect.Height); using (Graphics g Graphics.FromImage(bmp)) { g.CopyFromScreen(rect.Location, Point.Empty, rect.Size); } return bmp; }这段代码在普通屏幕上没问题但在高分屏DPI缩放不是100%的系统上会有一个经典坑我放在后面的“避坑实录”里专门讲。3.2 图片预处理直接送识别还是先做加工截图拿到的Bitmap我建议先做一些预处理再送百度AI原因很简单识别率能明显提升尤其是屏幕上小字号字体和抗锯齿渲染出来的文字。我自己实验下来最优的预处理顺序是放大。如果框选的区域文字偏小比如代码编辑器里11号字体直接把原图送识别百度有时候会漏字。把图片放大2倍再识别准确率能上去不少。放大我直接用C#的Graphics类private Bitmap ScaleImage(Bitmap src, int scale) { int newWidth src.Width * scale; int newHeight src.Height * scale; Bitmap newBmp new Bitmap(newWidth, newHeight); using (Graphics g Graphics.FromImage(newBmp)) { g.InterpolationMode System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(src, 0, 0, newWidth, newHeight); } return newBmp; }灰度化。屏幕截图的背景如果有颜色渐变、阴影、暗色主题直接识别容易干扰。转成灰度图可以强化文字和背景的对比度。灰度化就是遍历像素用标准公式 R0.299 G0.587 B*0.114 计算灰度值。二值化可选。对于白底黑字的截图二值化效果很好但遇到深色主题的IDE截图二值化反而会让文字断裂。我的做法是让用户自己在设置里选“增强模式”和“原图模式”默认走灰度放大不强制二值化。这一步大家不要过度设计OCR识别的本质是图像理解和特征匹配太重的图像处理可能帮倒忙。实测下来对屏幕截图最有效的是“放大”其次是“灰度”其他边缘锐化、去噪这些对UI截图的收益很小。3.3 调用百度AI文字识别接口百度AI通用文字识别有几种接口通用文字识别标准版、通用文字识别高精度版、通用文字识别含位置信息版等。标准版免费额度多识别速度也快我就用它。接口地址https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token你的AccessToken请求方式为POSTContent-Type为application/x-www-form-urlencodedbody里带上image参数值是图片的Base64编码字符串。如果图片过大需要先压缩到一定大小百度要求Base64编码后的图片大小不超过4M我实际建议控制在1M以内速度更快。调用代码封装如下public static async Taskstring RecognizeTextAsync(Bitmap bmp, string accessToken) { using (MemoryStream ms new MemoryStream()) { bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png); byte[] imageBytes ms.ToArray(); string base64Image Convert.ToBase64String(imageBytes); string url $https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token{accessToken}; using (HttpClient client new HttpClient()) { var content new FormUrlEncodedContent(new Dictionarystring, string { { image, base64Image }, { detect_direction, false }, { paragraph, true }, { probability, false } }); var resp await client.PostAsync(url, content); string respBody await resp.Content.ReadAsStringAsync(); return respBody; } } }这里有几个参数值得说明一下detect_direction是否检测图像朝向默认false如果截的是手机竖屏长图或者扫描件建议设true让百度自动纠正方向。paragraph是否输出段落信息。默认false时只返回每行文字的坐标和内容设true后返回结果里多一个paragraphResult字段可以方便地把多行文字按段落合并。probability是否返回每行识别的置信度调试时开着方便定位问题正常使用建议关闭能省一点响应时间。3.4 解析返回结果并联动剪贴板百度OCR接口返回的JSON结构长这样{ words_result: [ { words: 你好世界 }, { words: 第二行文字 } ], words_result_num: 2, log_id: 1234567890 }用Newtonsoft.Json解析很简单var json JObject.Parse(respBody); var wordsList json[words_result]? .Select(t t[words]?.ToString()) .ToList(); string resultText string.Join(Environment.NewLine, wordsList);解析拿到纯文本后我做三件事把结果同步到剪贴板这样用户直接CtrlV就能粘贴到任何地方。弹出一个非模态的结果预览窗口里面放一个多行TextBox展示识别内容方便用户检查识别错误、手动编辑后再复制。把结果追加到历史记录列表里带时间戳方便回溯。剪贴板操作在Winform里有一个很需要注意的点Clipboard.SetText方法在个别环境下会因为剪贴板被其他程序占用而抛异常所以通常我会在调用时加上重试机制public static void SetClipboardText(string text) { for (int i 0; i 3; i) { try { Clipboard.SetText(text); return; } catch (Exception) { Thread.Sleep(100); } } }4. 高频问题与避坑实录4.1 高分屏截图偏移问题我在前文留了一个坑就是Graphics.CopyFromScreen在高分屏下截图会偏移、尺寸不对。这个问题非常典型尤其是在笔记本很多是150%缩放上跑这个工具截出来的图片往往只是屏幕左上角的一部分。原因在于Windows为了兼容老程序默认会对显示内容做缩放而GDI的CopyFromScreen使用的是物理像素坐标如果你的Winform进程不是DPI感知DPI Aware的系统会把坐标“虚拟化”导致获取到的屏幕区域和实际物理显示区域不一致。解决办法是在程序入口处、任何窗口创建之前调用SetProcessDPIAware[DllImport(user32.dll)] private static extern bool SetProcessDPIAware(); [STAThread] static void Main() { SetProcessDPIAware(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); }这样进程就按真实DPI来布局截图坐标和物理像素对齐问题就消失了。如果你用的是.NET Core 3.0以上版本还可以通过运行时配置application.manifest来声明DPI感知效果一样。4.2 识别结果中英文和数字混排不准百度AI标准版对纯中文和纯英文的识别都还不错但屏幕截图中经常出现源代码、日志信息这种“英文关键词中文注释数字”混排的情况偶尔识别结果里会出现把全角逗号识别成半角或者把0识别成O这类问题。我的经验是如果是代码块识别可以在调用时加参数language_typeCHN_ENG强制使用中英文混合模型比默认模型的准确率高一截。另外如果屏幕上的英文、数字特别多可以在设置里把“增强模式”打开也就是先做灰度放大再识别实测对代码类截图的误识别率能降低不少。4.3 网络请求超时和AccessToken过期在线接口不可避免会遇到网络波动和超时的场景。我开发的第一个版本没有做超时控制偶尔在弱网环境下界面就卡住了用户体验很差。后来我做了三个调整一是给HttpClient设置超时时间默认15秒超过就提示“识别超时请重试”并自动关闭截图等待状态。注意HttpClient的Timeout属性是针对整个请求周期的如果OCR响应慢这个值太短反而容易误判。二是增加简单的重试机制。对网络超时这类的偶发错误我会自动重试一次但如果是返回错误码比如110或111表示AccessToken无效就不重试而是提示用户重新获取。三是设计Token管理机制。每30天AccessToken过期一次很多用户当天用完第二天再用发现忽然不好使了其实就是Token过期了。我建议在启动时强制刷新一次Token或者把Token和过期时间保存到本地文件里启动时读取并判断是否过期这样能省一次HTTP请求也避免400错误的尴尬。4.4 全局快捷键被其他软件占用我用CtrlShiftZ作为默认快捷键但不少截图软件、输入法也会占用类似的组合注册失败时RegisterHotKey会返回false。所以初始化时如果发现注册失败我会弹一个提示并提供设置窗口让用户改成别的组合。比较稳妥的做法是使用F1到F12组合或者像QQ截图一样用“CtrlAlt字母”降低冲突的概率。另外全局快捷键消息是在程序的主窗口消息循环里接收的如果你的主窗体做了最小化到托盘的操作不要忘记在消息循环里依然处理WM_HOTKEY否则最小化后就唤不醒了。我最初踩过这个坑最小化到托盘后怎么按快捷键都没反应后来确认就是因为托盘化之后消息处理没有走WndProc的重载。5. 工具还能怎么扩展这个项目的框架搭好之后扩展方向非常多我目前加了几个比较实用的功能批量识别。在窗体上支持拖入多张图片自动批量调用接口识别把所有结果合并保存到txt文件适合处理拍照的文档或者PPT幻灯片截图。剪贴板监听。监听系统剪贴板当复制了图片时自动弹出提示“检测到剪贴板图片是否需要识别”这样连快捷键都不用按复制图片的动作直接就导向识别。表格识别。偶尔开会时要拍白板上的表格我用百度的表格文字识别接口替换通用接口返回的JSON里带表格结构可以在程序里渲染出表格预览图虽然格式复杂一点但确实能解决实际问题。识别结果翻译。识别完文字之后接一个翻译接口对英文文档、英文软件界面非常实用相当于截图即翻译省去切换软件的时间。离线识别兜底。因为在线接口受网络影响我也试过用PaddleOCR本地模型做离线识别。C#调用PaddleOCR需要用Python环境跑本地服务再通过HTTP调用架构上多一层但效果做个兜底方案还凑合。如果你对完全离线有刚性需求可以考虑这个方向。乐观点说这属于“平时用在线接口断网时降级离线”的双通道设计。我在实际使用中发现最顺手的工作流反而是“快捷键截图 → 自动识别 → 自动复制 → CtrlV黏贴”整个过程不需要看结果窗口识别错了再手动改一下。所以我强烈建议第一版把“自动复制到剪贴板”这个功能做扎实这才是提升效率的关键。写在最后的个人体会做这个工具最大的体会是技术难度真的不高但做完之后的成就感很实在。从“每次截图识别要点三次鼠标”变成“按一个快捷键文字就进剪贴板”这个效率提升是肉眼可见的。整个项目代码量不到500行核心逻辑就三块截图、HTTP调用、JSON解析对C#初学者来说正好是一次完整的综合练习——你会接触到Win32 API、GDI绘图、异步编程、HttpClient、JSON处理这些实际开发中躲不掉的技术点。如果你照着这篇文章实现了第一版我建议你写完之后给自己提两个问题第一识别结果怎么处理才能让用户少改一个字第二如果每天要识别几百张图程序的稳定性和容错要怎么提升。把这两个问题想清楚你就不只是抄了一套代码而是真正理解了一个工具类软件的设计逻辑。