在C#中利用Graphics类完成屏幕截图,本质上是把屏幕设备上下文中的像素复制到内存中的Bitmap画布上。系统提供的System.Drawing命名空间已经封装了足够底层的API,开发者只需组合Screen、Bitmap、Graphics三个类型即可实现全屏或指定区域抓图,整个过程不依赖任何外部组件。

一、全屏截图的基本实现
全屏截图的逻辑非常直观:首先获取主屏幕的边界尺寸,然后创建一个同样大小的Bitmap对象作为目标画布,通过Graphics.FromImage方法得到与该画布关联的绘图表面,最后调用CopyFromScreen把屏幕左上角(0,0)开始的全部像素拷贝过来。这种写法在单显示器环境下完全够用,而且资源占用极低。
需要注意,Graphics和Bitmap都实现了IDisposable接口,必须使用using语句或者手动调用Dispose,否则频繁截图会造成GDI对象泄漏。下面的示例演示了最基础的全屏保存为PNG文件的做法,路径部分可按需修改。
using System;
using System.Drawing;
using System.Drawing.Imaging;
class ScreenShotDemo
{
static void CaptureFullScreen()
{
// 获取主屏幕的宽高
int width = Screen.PrimaryScreen.Bounds.Width;
int height = Screen.PrimaryScreen.Bounds.Height;
// 创建内存画布
using (Bitmap bmp = new Bitmap(width, height))
{
using (Graphics g = Graphics.FromImage(bmp))
{
// 从屏幕(0,0)拷贝到画布(0,0),尺寸为width x height
g.CopyFromScreen(0, 0, 0, 0, new Size(width, height));
}
// 保存为文件
bmp.Save("fullscreen.png", ImageFormat.Png);
}
}
}
1.1 关于Screen类的选择
上面的代码只使用了PrimaryScreen,也就是主显示器。如果用户接了双屏,可能需要遍历Screen.AllScreens来合并所有屏幕区域。每个Screen对象的Bounds属性都带有X、Y偏移量,拼接多屏位图时要按偏移绘制,不能直接从(0,0)拷贝。
另外,Screen类位于System.Windows.Forms程序集中,如果是控制台项目需要手动引用该程序集并添加using System.Windows.Forms;,否则编译时会找不到类型。这也是很多新手第一次写截图代码报错的主要原因。
二、区域截图的实现方式
区域截图和全屏截图的区别仅在于参数:不再从(0,0)取整个屏幕,而是取用户指定的起点坐标以及宽高。常见做法是提供一个Rectangle结构,里面包含X、Y、Width、Height,然后把这个矩形传给CopyFromScreen的源点和目标点。比如想截取屏幕上从(100,100)开始、宽300高200的一块,就写成g.CopyFromScreen(100, 100, 0, 0, new Size(300, 200))。
为了让用户可视化选择区域,通常会配合Mouse事件在窗体上拖出虚线框,再把框的矩形传进截图方法。下面给出一个简化版的区域截图函数,入参是Rectangle,返回Bitmap,调用方自行决定保存还是显示。
using System;
using System.Drawing;
class RegionCapture
{
static Bitmap CaptureRectangle(Rectangle rect)
{
if (rect.Width <= 0 || rect.Height <= 0)
throw new ArgumentException("截取区域宽高必须大于0");
Bitmap bmp = new Bitmap(rect.Width, rect.Height);
using (Graphics g = Graphics.FromImage(bmp))
{
// 源起点为rect.X, rect.Y,目标起点为0,0
g.CopyFromScreen(rect.X, rect.Y, 0, 0, rect.Size);
}
return bmp;
}
}
2.1 坐标与DPI缩放问题
在Windows高DPI设置下,窗体如果使用感知模式(Per Monitor DPI Aware),鼠标得到的客户坐标和屏幕物理像素并不一致。这时直接把窗体坐标传给CopyFromScreen会导致截偏或截小。解决办法是在截图前把逻辑坐标乘以当前屏幕的DPI缩放比,或者把应用程序清单设为不感知DPI,由系统自动拉伸。
还有一个隐蔽坑:CopyFromScreen使用的是屏幕设备坐标,而不是窗体客户区坐标。如果区域框是在某个子控件上画的,必须用PointToScreen把局部坐标转换成屏幕坐标,否则截出来的图会是错位的。
三、完整可运行的控制台示例
下面把全屏和区域两种能力合并成一个简单的控制台程序,通过命令行参数决定模式。虽然控制台没有界面,但足以验证Graphics截图的可用性,你也可以把方法搬到WinForms项目里做界面化工具。
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
class Program
{
static void Main(string[] args)
{
if (args.Length == 0)
{
// 默认全屏
CaptureFull();
}
else if (args.Length == 4)
{
// 传入 x y width height 做区域
int x = int.Parse(args[0]);
int y = int.Parse(args[1]);
int w = int.Parse(args[2]);
int h = int.Parse(args[3]);
CaptureRegion(new Rectangle(x, y, w, h));
}
}
static void CaptureFull()
{
Rectangle bounds = Screen.PrimaryScreen.Bounds;
using (Bitmap bmp = new Bitmap(bounds.Width, bounds.Height))
{
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size);
}
bmp.Save("full.png", ImageFormat.Png);
Console.WriteLine("全屏截图已保存");
}
}
static void CaptureRegion(Rectangle rect)
{
using (Bitmap bmp = new Bitmap(rect.Width, rect.Height))
{
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(rect.X, rect.Y, 0, 0, rect.Size);
}
bmp.Save("region.png", ImageFormat.Png);
Console.WriteLine("区域截图已保存");
}
}
}
3.1 性能与格式选择
截图本身耗时极短,瓶颈通常在保存阶段。PNG无损但体积大,JPEG体积小但丢失文字清晰度。如果是做屏幕录制类工具,建议用Bitmap直接扔进内存流再编码,不要每次都落盘。此外,CopyFromScreen内部调用了BitBlt,在显卡驱动异常时可能返回空白图,生产环境应加try-catch并提示用户。
Graphics类方案胜在零依赖、代码少,但在截取DirectX全屏游戏画面时往往只能得到黑屏,因为游戏使用的是独立渲染表面。这种场景要改用DirectShow或桌面 duplication API,已经超出Graphics类的能力范围。
四、小结与避坑清单
用C#的Graphics做截图,核心就是Bitmap加CopyFromScreen,全屏和区域只是参数不同。真正写进项目时,记得处理多屏边界、DPI缩放、资源释放三件事,基本就不会出大问题。
- 始终用using包裹Bitmap和Graphics,防止GDI泄漏
- 区域坐标必须是屏幕绝对坐标,不是控件本地坐标
- 高DPI下自行换算缩放比,或关闭DPI感知
- 需要截游戏画面时放弃Graphics,换系统级捕获接口
只要理清上面几点,你就能在十行内写出一个实用的C#截屏模块,后续接上鼠标框选或者快捷键监听,就是一个完整的桌面抓图小工具。
C#Graphicsscreen_capture修改时间:2026-08-09 15:42:41