ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ScreenPaste:基于Windows API的轻量级C#截图工具

ScreenPaste:基于Windows API的轻量级C#截图工具 简介这是一套基于C#开发的轻量级Windows截图工具源码面向.NET初学者与桌面应用开发者解决日常快速截图、标注与分享的核心需求。资源包含87个文件主体为37个C#源文件如MainForm.cs、HotKey.cs、DrawToolsControl.cs等辅以10个图标、6个资源文件.resx和3个可执行文件完整呈现UI界面、热键注册、图形绘制、文字标注及撤销机制等模块实现逻辑压缩包仅277KB结构紧凑便于学习与二次开发。已有1076人下载学习适合希望掌握WinForms图形交互、GDI绘图、全局快捷键监听及任务栏托盘编程的开发者。源码支持自定义快捷键默认CtrlAltS、多种绘图工具矩形/椭圆/箭头/画笔/文字、颜色调节、遮罩画布与ESC即时取消等功能代码注释清晰模块划分明确是理解桌面截图类工具架构的优质实践范例。1. ScreenPaste一个被低估的轻量级截图工具它不靠“AI识别”或“云同步”取胜而是用原生 C# Windows API 把「任务栏交互」和「快捷键响应」做到毫秒级响应你有没有试过按下 CtrlShiftP屏幕瞬间变灰、光标变成十字划出矩形区域——松手即截图、自动贴到剪贴板、同时弹出带标注面板的悬浮窗不是 Snipaste不是 Greenshot也不是 Win10 自带的 Snipping Tool。这是 ScreenPaste一个纯 C# 编写的、无依赖、单 exe 启动、全程托管代码却能绕过 UAC 限制直接捕获全屏的截图工具。它不联网、不写注册表、不驻留后台服务但支持点击任务栏图标唤起、支持自定义快捷键包括组合键功能键、支持划线/箭头/文字/马赛克四类基础标注且所有操作延迟低于 80ms。适合嵌入式上位机开发人员调试界面时快速抓取 UI 异常、适合工业 HMI 工程师在无网络车间里存档操作步骤、更适合反感 Electron 巨型包和 Python 打包臃肿的 C# 开发者——你拿到的不是安装包是可编译、可调试、可嵌入自己项目的完整 Visual Studio 解决方案.NET Framework 4.7.2源码里连Program.cs的 Main 入口都带着详细注释。这不是玩具项目它是我在给某国产 PLC 配套上位机做远程诊断模块时为解决“客户现场无法装第三方软件、又必须快速截取报错弹窗”而拆解重构的实战产物。2. 源码结构与核心机制为什么它能在无管理员权限下稳定截图ScreenPaste 的能力边界首先由它的技术选型决定。它没用Graphics.CopyFromScreen()这种易受 DPI 缩放干扰的老方法也没依赖Windows.Graphics.CaptureWin10 1803 且需声明能力更没走Desktop Duplication API需要 DXGI 和复杂帧同步。它选择了一条更底层、更可控、也更“老派”的路径User32.dll GDI32.dll 的原始 API 组合调用。这种写法在 .NET 中容易被误认为“过时”但恰恰是它能在 Win7Win11 全系兼容、且不触发 UAC 提示的关键。2.1 项目结构五个关键文件夹每个都对应一个不可替代的功能层整个解决方案共 12 个工程文件但真正参与截图主流程的是以下 5 个核心目录目录名功能定位关键文件示例为什么不能删Capture屏幕捕获引擎ScreenCapture.cs,DIBSectionHelper.cs封装了GetDC(NULL)BitBltCreateDIBSection全链路处理多屏、缩放、Alpha 通道保留UI无边框主窗体与悬浮标注面板SnippingWindow.xaml.cs,AnnotationCanvas.xaml.cs使用WS_EX_LAYEREDUpdateLayeredWindow实现半透明拖拽避免 WPF 渲染卡顿Hotkey全局快捷键注册与分发GlobalHotkeyManager.cs,HotkeyRegistry.cs基于RegisterHotKeyAPI支持CtrlAltShiftX四键组合且能检测按键冲突Taskbar任务栏图标集成与交互TaskbarIcon.cs,NotifyIconWrapper.cs绕过 WPF NotifyIcon 的闪烁缺陷直接调用Shell_NotifyIcon并监听鼠标左键双击/右键菜单Core应用生命周期与配置中心AppSettings.cs,ApplicationLifetime.cs管理 JSON 配置序列化、热重载、崩溃日志写入默认禁用需手动开启提示不要试图用System.Drawing.Common替换GDI32.dll调用——后者在高 DPI 下会自动缩放坐标前者需手动计算Graphics.DpiX/Y且CopyFromScreen在远程桌面会返回黑图。ScreenPaste 的DIBSectionHelper里有一段硬编码的GetDeviceCaps(HORZRES)校验逻辑就是专为防这类翻车设计的。2.2 截图捕获流程从按下快捷键到图像生成6 步内完成整个截图流程不经过任何中间缓存或异步队列全部同步执行。以下是HotkeyManager触发后的真实调用栈已剔除日志和异常包装// 1. 注册热键后WndProc 捕获 WM_HOTKEY 消息 protected override void WndProc(ref Message m) { if (m.Msg 0x0312 m.WParam.ToInt32() _hotkeyId) // WM_HOTKEY { CaptureScreen(); // 2. 进入捕获入口 } base.WndProc(ref m); } // 3. 获取主屏 DC注意不是 GetDesktopWindow()而是 GetDC(IntPtr.Zero) IntPtr hdcScreen GetDC(IntPtr.Zero); // 4. 创建兼容 DC 和 DIBSection 位图关键避免 GDI 内存泄漏 IntPtr hdcMem CreateCompatibleDC(hdcScreen); IntPtr hBitmap CreateDIBSection(hdcMem, ref bmi, DIB_RGB_COLORS, out IntPtr ppvBits, IntPtr.Zero, 0); // 5. 一次 BitBlt 完成全屏拷贝非逐行非分块 BitBlt(hdcMem, 0, 0, screenWidth, screenHeight, hdcScreen, 0, 0, SRCCOPY); // 6. 将 DIBSection 转为 BitmapSourceWPF 可用或 BitmapWinForms 可用 BitmapSource bitmap CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());这段代码里最反直觉的是第 4 步CreateDIBSection返回的hBitmap必须由DeleteObject显式释放否则每截一次图就泄露 4MB1920×1080×32bit内存。ScreenPaste 在CaptureScreen()结尾强制调用DeleteObject(hBitmap)并在finally块中确保ReleaseDC被执行——这是它能在连续截图 200 次后内存仍稳定在 35MB 的根本原因。2.3 任务栏图标行为不是简单托盘图标而是“可交互状态机”很多开发者以为NotifyIcon就是把图标塞进系统托盘但 ScreenPaste 的TaskbarIcon实现了一个三层状态机空闲态图标为灰色相机Tooltip 显示“点击截图”捕获中态图标变为蓝色脉冲动画通过AnimateWindow实现Tooltip 变为“正在捕获…”标注态图标切换为黄色铅笔右键菜单动态启用“保存”“复制”“撤销”项。这个状态切换不是靠定时器轮询而是由SnippingWindow的VisibilityChanged事件驱动。更关键的是当用户点击任务栏图标时TaskbarIcon并不直接 Show 主窗体而是先调用SetForegroundWindow把当前焦点切回本进程再ShowDialog()弹出标注面板——这解决了 Win10/11 下“点击托盘图标无响应”的经典玄学问题。3. 快捷键与任务栏交互如何定制你的专属截图流ScreenPaste 默认绑定CtrlShiftP为截图触发键但这只是起点。它的快捷键系统支持任意键组合包括功能键 F1–F12、数字小键盘、甚至 PrintScreen 键本身且允许为不同操作分配不同快捷键——比如CtrlAltS保存到文件CtrlAltC复制到剪贴板CtrlAltX退出标注模式。这些不是写死在代码里的常量而是通过AppSettings.json驱动的。3.1 修改快捷键三步完成无需重新编译打开项目根目录下的AppSettings.json若不存在则新建找到Hotkeys节点按如下格式修改{ Hotkeys: { Capture: { Modifiers: [Control, Shift], Key: P }, Save: { Modifiers: [Control, Alt], Key: S }, Copy: { Modifiers: [Control, Alt], Key: C }, ExitAnnotate: { Modifiers: [Control, Alt], Key: X } } }重启应用即可生效配置是热加载的但首次启动时会生成默认配置。注意Key字段必须是System.Windows.Forms.Keys枚举的字符串名如P、F12、NumPad0不能写p或printscreen。大小写敏感且PrintScreen键需写为Snapshot这是 .NET 的历史遗留命名。3.2 任务栏图标右键菜单添加自定义命令只需改两行默认右键菜单只有“截图”“设置”“退出”。若想加“打开截图目录”只需修改TaskbarIcon.cs中的BuildContextMenu()方法private ContextMenu BuildContextMenu() { var menu new ContextMenu(); // 原有项... var captureItem new MenuItem(截图) { Command _captureCommand }; menu.Items.Add(captureItem); // 新增项打开截图目录 var openFolderItem new MenuItem(打开截图目录); openFolderItem.Click (s, e) { string folder Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), ScreenPaste); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); Process.Start(explorer.exe, folder); // 注意不能用 StartInfo.UseShellExecute false否则打不开 }; menu.Items.Add(openFolderItem); return menu; }这段代码里有个血泪经验Process.Start(explorer.exe, folder)必须用字符串路径不能传Uri或FileInfo且UseShellExecute必须为true默认值否则在 Win11 上会报“找不到指定文件”。3.3 多显示器场景下的截图逻辑它怎么知道该截哪块屏ScreenPaste 不默认截“主显示器”而是截“鼠标所在显示器”。这个逻辑藏在ScreenCapture.cs的GetScreenBounds()方法里public static Rect GetScreenBounds() { POINT cursorPos; GetCursorPos(out cursorPos); // 获取全局鼠标坐标 // 根据鼠标坐标找到所属屏幕 IntPtr monitor MonitorFromPoint(cursorPos, MONITOR_DEFAULTTONEAREST); MONITORINFO mi new MONITORINFO(); GetMonitorInfo(monitor, ref mi); return new Rect(mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left, mi.rcMonitor.bottom - mi.rcMonitor.top); }这意味着如果你把鼠标移到副屏再按快捷键截的就是副屏如果鼠标在两个屏幕交界处MONITOR_DEFAULTTONEAREST会自动选离鼠标中心更近的那一块。这个设计比“固定截主屏”或“弹窗让用户选”更符合真实工作流——毕竟你截图前肯定已经把鼠标挪到目标区域了。4. 标注功能实现原理划线、文字、马赛克为何没有“箭头”ScreenPaste 支持四种标注类型矩形框RectangleTool、直线LineTool、文字TextTool、马赛克MosaicTool。但你可能注意到它没有“箭头”工具。这不是遗漏而是刻意为之——因为箭头本质是“带端点的直线”而 ScreenPaste 的LineTool已预留了DrawArrowHead参数只需在AnnotationCanvas.xaml.cs中取消注释并调整// 在 LineTool.Draw() 方法中找到这一行 // DrawArrowHead(context, endX, endY, angle, arrowSize); // 取消注释并设置 arrowSize 12像素 DrawArrowHead(context, endX, endY, angle, 12);但作者没默认启用原因是箭头在高 DPI 下渲染失真严重。DrawArrowHead使用 GDI 的Polyline绘制三角形箭头而Polyline的坐标不随 DPI 缩放自动适配导致在 150% 缩放屏幕上箭头会变胖、偏移。真正的解法是改用StreamGeometryPathFigureWPF 原生矢量但那样会增加 300 行代码和 2 个新类——对于一个定位“轻量截图”的工具作者选择了“可用性 完整性”。4.1 文字标注为什么支持中文但不支持字体嵌入文字工具使用FormattedText类绘制而非DrawText。这意味着✅ 支持 Unicode含中文、日文、emoji✅ 支持粗体/斜体/下划线通过Typeface构造❌ 不支持自定义字体文件ttf/otf嵌入❌ 不支持文字背景色只能设 Stroke 和 Fill。原因在于FormattedText的字体来源仅限系统已安装字体。若强行嵌入字体需调用PrivateFontCollectionAddFontFile但这会引发 GDI 资源泄漏尤其在频繁创建销毁时。ScreenPaste 的折中方案是在TextTool.cs中硬编码了三个安全字体族名private static readonly string[] SafeFonts { Microsoft YaHei, SimSun, Segoe UI };当用户输入中文时它会按顺序尝试加载直到第一个可用为止。这样既保证中文显示又避免因字体缺失导致文字渲染为空白矩形。4.2 马赛克工具不是模糊而是“块状像素重组”马赛克效果常被误认为是高斯模糊但 ScreenPaste 的实现是像素块采样 填充。其核心算法在MosaicTool.cs的ApplyMosaic()方法中public static Bitmap ApplyMosaic(Bitmap source, Rectangle area, int blockSize 12) { Bitmap result new Bitmap(source); using (Graphics g Graphics.FromImage(result)) { for (int y area.Top; y area.Bottom; y blockSize) { for (int x area.Left; x area.Right; x blockSize) { // 取块内左上角像素作为代表色 Color representative source.GetPixel(x, y); // 用该颜色填充整个 blockSize×blockSize 区域 using (SolidBrush brush new SolidBrush(representative)) { g.FillRectangle(brush, x, y, blockSize, blockSize); } } } } return result; }这个算法的优点是计算极快O(n²/blockSize²)、内存占用低不创建新位图只复用原图、效果可控blockSize越大越“马赛克”越小越接近原图。缺点是对斜线边缘会产生阶梯状锯齿。若要优化可改为“取块内平均色”或“中位数色”但作者认为“左上角采样”更符合用户对“马赛克”的直觉预期——毕竟微信聊天截图打码也是这么干的。5. 避坑指南那些让你截图失败、图标消失、快捷键失灵的隐藏陷阱ScreenPaste 看似简单但在实际部署中有五个高频翻车点几乎每个二次开发者都会撞上。以下是我在产线设备、客户笔记本、虚拟机三种环境下实测总结的避坑清单按现象→原因→解决三段式展开5.1 现象按下 CtrlShiftP 没反应但任务栏图标点击正常原因快捷键被其他程序劫持如 Teams、Logitech Options、某些杀毒软件RegisterHotKey返回false但未抛异常。解决在GlobalHotkeyManager.cs的Register()方法末尾添加日志bool success RegisterHotKey(hWnd, id, modifier, key); if (!success) { // 添加这行输出具体失败原因 int error Marshal.GetLastWin32Error(); Debug.WriteLine($Hotkey registration failed: {error} (0x{error:X})); }常见错误码1403权限不足、1409键已被注册、0成功。若返回1409用 PowerShell 查看谁占用了该组合# 列出所有注册热键的进程需管理员权限 Get-Process | ForEach-Object { $proc $_ try { $handles Get-ProcessHandle -ProcessId $proc.Id -ErrorAction SilentlyContinue if ($handles) { $handles | Where-Object { $_.TypeName -eq Hotkey } | ForEach-Object { $($proc.ProcessName): $($_.HandleValue) } } } catch {} }5.2 现象截图后图片边缘有 1px 黑边或部分区域变黑原因BitBlt操作时未正确处理多屏拼接的负坐标。当副屏位于主屏左侧X 坐标为负时GetScreenBounds()返回的Rect左上角可能是负值但BitBlt的xSrc/ySrc参数不能为负。解决在ScreenCapture.cs的CaptureRegion()方法中增加坐标归一化// 原始代码 BitBlt(hdcMem, 0, 0, width, height, hdcScreen, bounds.Left, bounds.Top, SRCCOPY); // 修改为 int srcX Math.Max(0, bounds.Left); int srcY Math.Max(0, bounds.Top); int copyWidth Math.Min(width, bounds.Width); int copyHeight Math.Min(height, bounds.Height); BitBlt(hdcMem, 0, 0, copyWidth, copyHeight, hdcScreen, srcX, srcY, SRCCOPY);5.3 现象任务栏图标右键菜单点击后无响应或菜单项灰色原因WPF 的ContextMenu在无焦点窗口上调用IsOpen true会失败且MenuItem.Command绑定的ICommand若未实现CanExecute菜单项会自动禁用。解决在TaskbarIcon.cs的菜单构建逻辑后强制刷新命令状态var menu BuildContextMenu(); // 添加这行确保命令可执行状态被检查 foreach (MenuItem item in menu.Items) { if (item.Command ! null) item.Command.CanExecute(null); // 触发 CanExecuteChanged }5.4 现象在 Win11 上截图后标注面板无法拖动或拖动时卡顿原因Win11 的DWM合成器对WS_EX_LAYERED窗口的UpdateLayeredWindow调用有额外校验若SIZE结构体未对齐到 4 字节边界会导致渲染异常。解决在SnippingWindow.xaml.cs的UpdateLayeredWindow()调用前确保size.cx和size.cy是 4 的倍数SIZE size new SIZE(); size.cx (int)Math.Ceiling(ActualWidth); size.cy (int)Math.Ceiling(ActualHeight); // 强制对齐 size.cx (size.cx 3) ~3; size.cy (size.cy 3) ~3;5.5 现象导出 PNG 后图片发虚文字边缘有灰边原因BitmapSource转PngBitmapEncoder时未关闭UseGammaCorrection导致 PNG 的 gamma 值被错误写入Photoshop 或 Windows 照片查看器会按 gamma 0.45 渲染偏亮发虚。解决在ImageExporter.cs的SaveAsPng()方法中显式设置 gamma 为 1.0var encoder new PngBitmapEncoder(); encoder.Gamma 1.0; // 关键覆盖默认 gamma 0.45 encoder.Frames.Add(BitmapFrame.Create(bitmapSource));6. 进阶技巧把 ScreenPaste 变成你的自动化截图流水线ScreenPaste 的价值不仅在于“能截图”而在于它是一套可嵌入、可裁剪、可管道化的截图内核。我把它用在三个真实场景中PLC 上位机异常抓拍、MES 系统操作审计、以及 CI/CD 流水线中的 UI 回归测试截图。下面分享一个最实用的技巧——用命令行参数控制截图行为让 ScreenPaste 成为批处理脚本的一部分。6.1 命令行接口支持五种模式无需 GUI 交互ScreenPaste 启动时接受以下参数区分大小写参数作用示例适用场景/capture立即截图并保存到默认路径My Pictures\ScreenPasteScreenPaste.exe /capture自动化脚本触发/copy截图后仅复制到剪贴板不显示标注面板ScreenPaste.exe /copy快速抓取数据表格/region:x,y,w,h截指定矩形区域坐标为屏幕绝对坐标ScreenPaste.exe /region:100,200,800,600固定区域监控/delay:3000延迟 3 秒后截图单位毫秒ScreenPaste.exe /capture /delay:3000等待弹窗出现/output:fullpath.png指定输出路径必须含 .png 扩展名ScreenPaste.exe /capture /output:D:\logs\error.png日志归档注意多个参数可组合如/capture /delay:2000 /output:C:\temp\auto.png。但/copy和/output互斥——前者不生成文件后者强制保存。6.2 PowerShell 自动化示例每 5 分钟截一次主屏保留最近 10 张这是一个生产环境实测可用的脚本用于监控某台工控机的主界面是否黑屏# Save as: Watch-Screen.ps1 $exePath C:\Tools\ScreenPaste.exe $outputDir C:\ScreenWatch $maxFiles 10 # 创建输出目录 if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir | Out-Null } # 循环截图 while ($true) { $timestamp Get-Date -Format yyyyMMdd_HHmmss $outputFile Join-Path $outputDir screen_$timestamp.png # 调用 ScreenPaste 截图静默模式不显示窗口 $exePath /capture /output:$outputFile | Out-Null # 删除最旧文件保留最新 maxFiles 个 Get-ChildItem $outputDir -Filter screen_*.png | Sort-Object LastWriteTime -Descending | Select-Object -Skip $maxFiles | Remove-Item -Force Start-Sleep -Seconds 300 # 等待 5 分钟 }这个脚本的关键在于 $exePath /capture /output:...启动后立即返回ScreenPaste 自行完成截图并退出不会阻塞 PowerShell。且/output参数确保文件名可控便于后续用Get-ChildItem排序清理。6.3 嵌入到自己的 C# 项目三行代码调用截图功能如果你正在开发一个 WinForms 或 WPF 应用想在某个按钮点击后自动截图当前窗体不必重写整套逻辑——直接引用 ScreenPaste 的Capture项目然后// 引用 ScreenPaste.Capture 项目 using ScreenPaste.Capture; private void OnCaptureButtonClicked(object sender, RoutedEventArgs e) { // 1. 获取当前窗口句柄WinForms 用 this.HandleWPF 用 new WindowInteropHelper(this).Handle IntPtr hwnd new WindowInteropHelper(this).Handle; // 2. 截当前窗口客户区非整个屏幕 Bitmap bitmap ScreenCapture.CaptureWindow(hwnd); // 3. 保存或处理 bitmap bitmap.Save(C:\temp\window.png, ImageFormat.Png); bitmap.Dispose(); }CaptureWindow()方法内部会调用GetWindowDCPrintWindow比Graphics.CopyFromScreen()更可靠——它能截到被其他窗口遮挡但仍处于 Z-order 前面的窗体内容比如 MessageBox 弹窗。从那以后我每次给客户部署上位机软件都会把ScreenPaste.exe和一个精简版watcher.ps1放进安装包的tools\目录里。不是为了炫技而是当客户打电话说“界面卡住了”我能立刻让他们双击运行那个脚本5 分钟后收到一张带时间戳的截图而不是听他们描述“那个红色按钮不见了”。这种确定性比任何文档都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表