
简介C#实现的Windows截屏功能源码面向Windows桌面开发初学者及需要集成截屏能力的开发者。项目借助系统绘图类库中的位图、画布等对象完成屏幕图像捕获先获取屏幕尺寸并创建同尺寸位图再用画布绘制方法将屏幕内容复制到位图支持鼠标拖拽自定义区域截图监听鼠标移动消息计算选区矩形并可使用画笔绘制线条做标注。生成的图片能保存为JPEG或PNG格式同时通过HTTP客户端示例演示了上传图片到服务器的流程。资源包共36个文件以C#源码为核心辅以资源文件、可执行程序、调试文件另含解决方案、配置文件与动态库引用压缩后仅820KB工程结构清晰。目前已有1660人学习下载。项目包含主窗体、剪切窗体、模式窗口等多个窗体及图像处理类直观展示选区、绘制、保存与网络请求的完整衔接适合逐段学习图形编程和网络通信技巧也方便在此基础上扩展出个性化截屏工具。1. C# 截屏工具的日常一个桌面小脚本能解决什么需要每天上午十点把内部报表看板的某个区域截图存档还要按日期命名、传到共享文件夹。手动截图一个月之后某开发者实在忍不了用 C# 写了一个常驻托盘的截屏小工具支持全屏、区域、窗口三种方式注册全局热键保存成 PNG/JPEG也可以直接复制到剪贴板交互方式类似常见截屏软件。这个项目把 Windows 截屏要用到的关键技术点都串了起来GDI 的 CopyFromScreen、全屏遮罩窗体、Win32 热键、剪贴板和 DPI 处理。如果你正在用 C# 做桌面工具或者想给自己的业务系统加一个“截图上报”功能这份源码可以直接抄作业按自己的需求改改就能用。2. 架构与技术选型为什么是 C# GDI程序怎么跑起来2.1 三种截屏方案为什么先选 CopyFromScreenWindows 平台上做截屏我通常会在三种方案里选。第一种是Graphics.CopyFromScreen这是 .NET 里最直接的 API从屏幕设备上下文直接拷贝一块矩形区域到位图适合固定桌面、全屏、窗口可见区域的截图。第二种是PrintWindow通过给目标窗口发送 WM_PRINT 消息让窗口自己把内容画到指定的 HDC 上适合窗口被其他窗口遮挡时也能截到完整内容的场景。第三种是 DXGI Desktop Duplication直接复制桌面纹理性能最好但代码复杂度明显更高适合录屏和高帧率采集。这个项目选 CopyFromScreen 不是因为它最强而是因为它对 90% 的桌面截图场景足够稳定代码量只有几十行。对于“每天定时截一块区域存下来”这种需求没必要把桌面采集那套搬进来。技术栈就是 C# .NET Framework 4.8 Windows FormsSystem.Drawing 负责图像处理System.Windows.Forms 提供窗体和剪贴板。工程默认用 .NET Framework 4.8原因是 Windows Forms 在这个版本下最成熟不依赖额外的运行时如果换成 .NET 6/8需要处理 System.Drawing.Common 在非 Windows 平台下的兼容问题Windows 上问题不大。2.2 工程结构与主流程截图动作的完整链路拿到源码包之后先看目录结构整个工程的布局是这样的ScreenCapDemo/ ├── ScreenCapDemo.sln ├── ScreenCapDemo/ │ ├── App.config │ ├── Program.cs │ ├── MainForm.cs │ ├── MainForm.Designer.cs │ ├── HotKeyManager.cs │ ├── ScreenCapture.cs │ ├── RegionSelector.cs │ ├── SaveHelper.cs │ └── Properties/ │ ├── AssemblyInfo.cs │ └── Settings.settings主流程非常直接程序启动后Program.cs创建主窗体但默认隐藏MainForm里注册全局热键并准备系统托盘图标。用户按下热键时HotKeyManager通过 Win32 回调把事件抛给MainForm此时根据当前模式来决定动作区域截图会先弹出一个全屏遮罩窗体RegionSelector用户在遮罩上拖拽出矩形区域全屏模式直接用Screen.PrimaryScreen.Bounds拿到整个屏幕的矩形窗口模式则通过鼠标取点定位窗口句柄再换算成屏幕坐标。拿到矩形后ScreenCapture.CaptureRegion执行Graphics.CopyFromScreen把像素拷贝到位图。最后SaveHelper负责写文件或放到剪贴板。这个设计里每个类职责都很单一ScreenCapture不关心 UIRegionSelector不关心图像编码SaveHelper只负责持久化。后续如果想把截屏能力集成到别的程序直接调用ScreenCapture.CaptureRegion就够了。2.3 快速跑起来从打开工程到按下第一个热键假设你机器上已经装了 Visual Studio 2022 或者较新的 .NET SDK双击ScreenCapDemo.sln在解决方案管理器里把启动项目设为 ScreenCapDemo然后直接 F5。第一次运行会看到右下角任务栏出现一个托盘图标主窗口是隐藏的。默认热键是Ctrl Alt A按下之后屏幕会蒙上一层半透明遮罩鼠标拖拽选出要截的区域松开后图片会自动保存到系统图片目录的 Screenshots 文件夹文件名带时间戳比如screenshot_20250601_143512.png。如果想立刻该改保存位置不用动代码直接打开App.config把SaveFolder的值改成你想要的路径比如D:\Work\Captures重新运行即可。路径支持环境变量写法代码内部会调用Environment.ExpandEnvironmentVariables做展开。如果热键跟系统或其他软件冲突把HotKey配置项改成类似CtrlAltShiftA这样的组合再重启程序。第一次跑建议先用全屏模式验证环境再做区域截图的联动调试。3. 核心模块拆解一个截图请求从热键到落盘的完整链路3.1 ScreenCapture最核心的几十行代码所有截屏逻辑最终都收敛到这一个方法上。核心代码看起来很短但每个细节都有讲究using System; using System.Drawing; using System.Drawing.Imaging; using System.Windows.Forms; namespace ScreenCapDemo { public static class ScreenCapture { public static Bitmap CaptureRegion(Rectangle region) { if (region.IsEmpty) throw new ArgumentException(截图区域不能为空, nameof(region)); var bmp new Bitmap(region.Width, region.Height, PixelFormat.Format32bppArgb); using (var g Graphics.FromImage(bmp)) { g.CopyFromScreen( region.Location, Point.Empty, region.Size, CopyPixelOperation.SourceCopy); } return bmp; } public static Bitmap CaptureFullScreen() { var bounds Screen.PrimaryScreen?.Bounds ?? throw new InvalidOperationException(当前没有可用的屏幕); return CaptureRegion(bounds); } } }CaptureRegion接收一个Rectangle这个矩形用的是屏幕坐标左上角为原点向右向下递增。方法先做空值检查然后创建等宽高的位图再用 Graphics.Draw 的方式把屏幕像素拷贝到位图。PixelFormat.Format32bppArgb是为了统一颜色格式避免系统在缩放或进行图片处理时因为格式不一致出现颜色偏差。g.CopyFromScreen有四个关键参数第一个region.Location是屏幕源区域的左上角坐标第二个Point.Empty是目标位图的写入起点第三个region.Size是拷贝的宽高最后一个CopyPixelOperation.SourceCopy表示直接覆盖而不是混合。调用方拿到Bitmap之后必须自己负责释放这里故意不写using因为截图结果要跨方法传递由最终持有者决定什么时候释放。一个小习惯是谁创建位图谁就不负责销毁销毁责任交给消费方避免“提前释放导致后续画图异常”这类问题。3.2 区域选择全屏遮罩窗体是怎么交互的区域截图的交互不能靠主窗体需要一个独立的全屏窗体盖住桌面让用户在上面拖拽出一个矩形。RegionSelector 的核心实现思路是窗体设置FormBorderStyle.None、Bounds Screen.PrimaryScreen.Bounds、TopMost true、WindowState Maximized然后通过鼠标事件记录起止坐标重绘时把选区外的区域涂半透明黑选区内部保持透明。public partial class RegionSelector : Form { private Point _start; private Rectangle _rect; public Rectangle SelectedRegion { get; private set; } public RegionSelector() { SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); FormBorderStyle FormBorderStyle.None; TopMost true; ShowInTaskbar false; Cursor Cursors.Cross; } protected override void OnMouseDown(MouseEventArgs e) { _start e.Location; _rect Rectangle.Empty; base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { if (e.Button MouseButtons.Left) { int x Math.Min(_start.X, e.X); int y Math.Min(_start.Y, e.Y); int w Math.Abs(e.X - _start.X); int h Math.Abs(e.Y - _start.Y); _rect new Rectangle(x, y, w, h); Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { if (_rect.Width 2 _rect.Height 2) { SelectedRegion _rect; DialogResult DialogResult.OK; } base.OnMouseUp(e); } protected override void OnPaint(PaintEventArgs e) { var bounds ClientRectangle; var fillBrush new SolidBrush(Color.FromArgb(100, Color.Black)); if (_rect.IsEmpty) { e.Graphics.FillRectangle(fillBrush, bounds); } else { var outside new Region(bounds); outside.Exclude(_rect); e.Graphics.FillRegion(fillBrush, outside); e.Graphics.DrawRectangle(Pens.Red, _rect); } fillBrush.Dispose(); } }OnMouseMove里的计算逻辑很直白Math.Min保证无论从哪个方向拖拽矩形坐标都归一化成左上角原点的表示宽高取绝对值这样最后拿到的_rect才能直接传给ScreenCapture.CaptureRegion。OnPaint里用Region做“选区外部排除”比手动计算四个矩形要边界安全得多不会有缝隙或重叠。拖拽过程中调用Invalidate()触发重绘配合双缓冲可以避免闪烁。释放遮罩前千万记得把窗体隐藏不然CopyFromScreen会把遮罩和红色边框一起截进去这一条在避坑章节详细讲。窗口模式截图比区域模式多一个步骤先让遮罩消失然后在鼠标位置调用WindowFromPoint取到窗口句柄再用GetWindowRect拿到窗口矩形最后把这个矩形当作区域交给CaptureRegion。注意GetWindowRect拿到的矩形包含窗口阴影和边框如果只想截客户区需要再调GetClientRect加ClientToScreen换算。3.3 保存、剪贴板与定时截图结果怎么落到磁盘截图结果拿到后保存逻辑集中在SaveHelper。它负责两件事按配置生成文件路径以及处理剪贴板复制。这里把保存和复制分开用户按CtrlS存文件按CtrlC直接复制到剪贴板互不干扰。public static class SaveHelper { public static string GeneratePath(string folder, ImageFormat format) { string ext format.Equals(ImageFormat.Png) ? png : jpg; string fileName DateTime.Now.ToString(yyyyMMdd_HHmmss_fff) . ext; string fullFolder Environment.ExpandEnvironmentVariables(folder); Directory.CreateDirectory(fullFolder); return Path.Combine(fullFolder, fileName); } public static string SaveBitmap(Bitmap bmp, ImageFormat format, string folder) { string path GeneratePath(folder, format); bmp.Save(path, format); return path; } public static void CopyToClipboard(Bitmap bmp) { Clipboard.SetImage(bmp); // 复制成功后顺便保存一份临时文件防止剪贴板数据在程序退出后失效 } }DateTime.Now.ToString(yyyyMMdd_HHmmss_fff)里的毫秒位fff很重要不带上它两次连续截图间隔小于 1 秒时会出现文件名冲突。Environment.ExpandEnvironmentVariables负责把配置文件里的%USERPROFILE%展开成真实路径这是新手最容易漏的一步。Directory.CreateDirectory是幂等操作目录不存在就创建存在则不报错可以放心调用。定时截图是另一个隐藏入口。在托盘菜单里勾选“定时截图”后主窗体启动一个System.Windows.Forms.Timer把Interval设为配置里的秒数乘以 1000然后在Tick事件里调用CaptureFullScreen并保存。定时截图一般配全屏模式如果配合区域截图需要提前把区域坐标写死或者存一个“上次使用区域”的持久化文件下次直接复用这比每次弹遮罩要顺手得多。4. 配置与运行改哪里、怎么跑、失败时看什么4.1 App.config 里到底能改什么整个程序不提供设置界面所有参数集中在App.config这样做的好处是部署到别的机器时不用重新编译。配置结构分成三块热键、保存策略、截屏行为。典型配置如下configuration appSettings add keySaveFolder value%USERPROFILE%\Pictures\Screenshots / add keyHotKey valueCtrlAltA / add keyDefaultFormat valuePNG / add keyRegionMode valueRegion / add keyTimerIntervalSeconds value0 / add keyIncludeCursor valuefalse / add keyMinWidth value2 / add keyMinHeight value2 / /appSettings startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.8 / /startup /configurationHotKey的值解析规则是用CtrlAlt、CtrlShift、AltShift这类组合作为前缀加号后面是大写字母键。解析代码里维护一个字典把字符串映射到 Win32 的修饰键常量MOD_CONTROL|MOD_ALT和虚拟键码(uint)Keys.A。DefaultFormat支持 PNG 和 JPEG两种格式对应的ImageFormat实例不同保存路径里的扩展名也会跟着变。TimerIntervalSeconds设为 0 表示不启用定时任务改成 5 就是每 5 秒自动截一次全屏这对电脑监控类需求很有用。改配置之后有两种生效方式改完直接重启程序或者在托盘菜单里点“重载配置”后者内部其实还是重新读取一次ConfigurationManager.AppSettings再把全局的单例配置对象替换成新实例。这个细节比每次都读配置更高效因为截屏路径和热键注册不希望在每次截图时都重新解析。4.2 命令行参数与系统托盘不弹窗口的常驻方案托盘程序最忌讳的就是主窗口弹出来打断用户操作。MainForm的构造函数里有一个关键动作this.WindowState FormWindowState.Minimized; this.ShowInTaskbar false;这样程序启动后只在NotifyIcon上显示一个气泡提示“截屏工具已启动按 CtrlAltA 截图”。命令行参数我预留了几个调试入口ScreenCapDemo.exe --once ScreenCapDemo.exe --savefolder D:\tmp ScreenCapDemo.exe --region 0,0,1920,1080--once表示启动后立即执行一次截图然后退出适合在任务计划程序里配合定时任务使用--region后面跟四个数字逗号分隔表示 x、y、宽、高这样不用弹遮罩直接截指定区域。实现方式是Program.cs里先解析Environment.GetCommandLineArgs()把参数填到一个启动配置对象里再决定是否跳过托盘流程直接进入截图逻辑。这里没有做太过复杂的参数校验数字解析失败就落回默认配置保证程序不会因为命令行写错而崩溃。4.3 日志和错误处理失败时不要只给用户一个黑盒子截屏失败时最怕程序默默吞掉异常用户只看到没反应还以为是热键没触发。我习惯在Program.cs里挂全局异常钩子同时给托盘菜单加一个“查看日志”入口。日志文件写到%LOCALAPPDATA%\ScreenCapDemo\logs\error.log每行记录带时间戳的异常信息。热键触发失败会记录热键 id、修饰键和虚拟键码保存失败会记录路径和完整堆栈。关键位置不用日志框架直接File.AppendAllLines就够了避免引第三方依赖。运行中经常出问题的还有两类一类是剪贴板被其他程序占用Clipboard.SetImage抛ExternalException另一类是没有权限写指定的保存目录。剪贴板异常的处理方式是捕获后重试一次再失败就只保存文件不复制剪贴板并且提示用户“复制失败文件已保存到默认目录”。保存目录异常则在初始化阶段就做一次写测试程序启动时生成一个 1x1 像素的测试图片再删除如果写不了直接托盘气泡提醒不要把问题拖到用户真正截图那一刻。5. 避坑与排查DPI 偏移、句柄泄漏、热键冲突与黑屏5.1 DPI 缩放导致的截屏偏移看起来是玄学其实有规律现象在一块 150% 缩放的屏幕上用遮罩精确选了一个 100x100 的区域保存出来的图片却是 150x150而且内容整体向右下偏移一大截。原因进程默认不是 DPI-Aware系统会把传入CopyFromScreen的坐标和尺寸按照缩放比例重新解释导致区域被放大。解决程序入口处调用SetProcessDPIAware告诉系统“我自己处理缩放”之后再传入的坐标就都是物理像素。[DllImport(user32.dll)] private static extern bool SetProcessDPIAware(); [STAThread] static void Main() { SetProcessDPIAware(); Application.EnableVisualStyles(); Application.Run(new MainForm()); }另外注意Screen.PrimaryScreen.Bounds在 DPI-Aware 和 DPI-Unaware 两种模式下返回的值不一样这也解释了为什么同一个工程在 100% 缩放的机器上一切正常换到高分辨率笔记本上就翻车。血泪经验是任何涉及屏幕坐标的 Windows 程序都必须在程序入口做这一行调用别指望用户自己去改兼容性设置。5.2 句柄泄漏跑一天后内存暴涨甚至报 OutOfMemoryException现象定时截屏跑了几个小时之后内存占用一路爬升最终弹出一个“创建 Graphics 对象失败”的异常。原因在循环里创建了大量Bitmap和Graphics对象没有及时调用Dispose()释放GDI 句柄池被耗尽。解决每次截图只创建一张位图用完立刻释放保存和剪贴板都做完之后调用bmp.Dispose()。在定时截图的Tick事件里建议用try-finally保证异常时也能释放或者用using包住整个处理流程。还要注意一个不明显的坑把Bitmap对象塞进剪贴板之后剪贴板不会立即释放这个位图的句柄它在剪贴板内容被替换或程序退出前都可能占着资源。所以复制到剪贴板后不要马上Dispose可以等到下次截图时再处理或者直接复制一个MemoryStream序列化的副本到剪贴板。实战中我通常是“保存文件和复制剪贴板二选一优先保存剪贴板交给系统管理”。5.3 全局热键注册失败组合键没反应也不报错现象CtrlAltA毫无反应托盘菜单的“测试热键”功能却能正常截图说明 UI 逻辑没问题只是热键没注册成功。原因该组合键被其他程序占用或者RegisterHotKey的句柄、id、参数设置不对函数返回 false。解决RegisterHotKey声明时加上SetLastError true注册失败时立刻检查返回值和错误码。常见错误码是 1409表示热键已经被占用这种情况需要自动降级到备选组合键或提示用户改配置。注册时hWnd要传主窗体的句柄id 是程序内唯一即可用来区分不同热键。5.4 把遮罩窗体截进图里选区中间有半透明黑块现象区域截图保存出来的图片里有一块颜色变暗的区域红框也印在图片上刚好是被拖拽选区的位置。原因CopyFromScreen执行时遮罩窗体还停留在屏幕上桌面上实际是一片“半透明黑 红框”的画面截图把整个桌面都拍下来自然带上了遮罩。解决在开始截图前先隐藏遮罩窗体等桌面重绘完成后再拷贝。我一般会在RegionSelector的确认事件里先执行this.Hide()然后调一个Application.DoEvents()或者Thread.Sleep(50)确保屏上不再残留窗体痕迹再调用CaptureRegion。不要把这 50 毫秒省掉尤其有些屏幕刷新率低隐藏窗体到画面真正刷新之间有明显延迟。5.5 游戏、视频播放器截出来是黑屏或空白现象截全屏视频播放区域或游戏画面时图片是黑色、空白或者内容缺失。原因视频播放器和多数游戏都用了硬件加速覆盖层或 DirectX 独占模式CopyFromScreen走的 GDI 合成通道拿不到覆盖层的内容只能看到桌面背景。解决这个坑在 GDI 方案里无解需要换成 DXGI Desktop Duplication 或者 PrintWindow 绕过合成。PrintWindow 对某些视频窗口有效但它要求目标窗口不处于硬件独占模式。做项目定位时要想清楚如果你的目标用户主要是浏览器页面和办公软件CopyFromScreen 完全够用如果要做游戏直播、录屏工具一开始就放弃这个方案直接上桌面复制 API。6. 二次开发把截屏能力抽象成可接入的服务接下来是工程实践部分。如果不想只做一个托盘工具而是把截屏能力塞进现有的业务系统比如考勤打卡附带人脸截图、客服工单附带报错截图我建议先做两个改造定义一个ICaptureSource接口把 CopyFromScreen 和 PrintWindow 分别封装成实现把热键、托盘、遮罩这些 UI 相关的东西从核心逻辑里彻底剥离。public interface ICaptureSource { Bitmap Capture(Rectangle region); bool IsSupported { get; } } public class GdiCaptureSource : ICaptureSource { public Bitmap Capture(Rectangle region) ScreenCapture.CaptureRegion(region); public bool IsSupported true; } public class PrintWindowCaptureSource : ICaptureSource { public Bitmap Capture(Rectangle region) { // 通过窗口句柄获取目标窗口的屏幕坐标区间 IntPtr hWnd WindowFromPoint(new Point(region.X, region.Y)); if (hWnd IntPtr.Zero) return null; return PrintWindowHelper.CaptureWindow(hWnd); } public bool IsSupported true; }这样原来的CaptureRegion变成具体实现上层服务只需依赖接口。做定时任务时这个接口尤其有价值你可以为同一个业务系统写一个“区域截图 水印 上传”的流水线截图模块只是其中一个步骤。验证方法也简单写一个单元测试工程分别用 100% 和 150% 缩放的虚拟机跑一遍针对ICaptureSource的矩形边界测试把 DPI Awareness 初始化、对象释放和热键冲突三项列成回归清单。从那以后我每次接截屏功能都强制先过一遍多显示器加缩放回归列表再把用例补进自动化这套流程帮我省下很多来自测试小姐姐的“截图又偏了”的反馈。希望帮到你。本文还有配套的精品资源点击获取