前言

Windows 上,屏幕截图一般是调用 win32 api 完成的,如果 C# 想实现截图功能,就需要封装相关 api。在 Windows 上,主要图形接口有 GDI 和 DirectX。GDI 接口比较灵活,可以截取指定窗口,哪怕窗口被遮挡或位于显示区域外,但兼容性较低,无法截取 DX 接口输出的画面。DirectX 是高性能图形接口(当然还有其他功能,与本文无关,忽略不计),主要作为游戏图形接口使用,灵活性较低,无法指定截取特定窗口(或者只是我不会吧),但是兼容性较高,可以截取任何输出到屏幕的内容,根据情况使用。

正文

以下代码使用了 C# 8.0 的新功能,只能使用 VS 2019 编译,如果需要在老版本 VS 使用,需要自行改造。

GDI

用静态类简单封装 GDI 接口并调用接口截图。

     public static class CaptureWindow
{
#region 类
/// <summary>
/// Helper class containing User32 API functions
/// </summary>
private class User32
{
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect); [DllImport("user32.dll", EntryPoint = "FindWindow", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
} private class Gdi32
{ public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter
[DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest,
int nWidth, int nHeight, IntPtr hObjectSource,
int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth,
int nHeight);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll")]
public static extern bool DeleteDC(IntPtr hDC);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
}
#endregion /// <summary>
/// 根据句柄截图
/// </summary>
/// <param name="hWnd">句柄</param>
/// <returns></returns>
public static Image ByHwnd(IntPtr hWnd)
{
// get te hDC of the target window
IntPtr hdcSrc = User32.GetWindowDC(hWnd);
// get the size
User32.RECT windowRect = new User32.RECT();
User32.GetWindowRect(hWnd, ref windowRect);
int width = windowRect.right - windowRect.left;
int height = windowRect.bottom - windowRect.top;
// create a device context we can copy to
IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);
// create a bitmap we can copy it to,
// using GetDeviceCaps to get the width/height
IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);
// select the bitmap object
IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);
// bitblt over
Gdi32.BitBlt(hdcDest, , , width, height, hdcSrc, , , Gdi32.SRCCOPY);
// restore selection
Gdi32.SelectObject(hdcDest, hOld);
// clean up
Gdi32.DeleteDC(hdcDest);
User32.ReleaseDC(hWnd, hdcSrc);
// get a .NET image object for it
Image img = Image.FromHbitmap(hBitmap);
// free up the Bitmap object
Gdi32.DeleteObject(hBitmap);
return img;
} /// <summary>
/// 根据窗口名称截图
/// </summary>
/// <param name="windowName">窗口名称</param>
/// <returns></returns>
public static Image ByName(string windowName)
{
IntPtr handle = User32.FindWindow(null, windowName);
IntPtr hdcSrc = User32.GetWindowDC(handle);
User32.RECT windowRect = new User32.RECT();
User32.GetWindowRect(handle, ref windowRect);
int width = windowRect.right - windowRect.left;
int height = windowRect.bottom - windowRect.top;
IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);
IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);
IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);
Gdi32.BitBlt(hdcDest, , , width, height, hdcSrc, , , Gdi32.SRCCOPY);
Gdi32.SelectObject(hdcDest, hOld);
Gdi32.DeleteDC(hdcDest);
User32.ReleaseDC(handle, hdcSrc);
Image img = Image.FromHbitmap(hBitmap);
Gdi32.DeleteObject(hBitmap);
return img;
}
}

Direct3D

安装 nuget 包 SharpDX.Direct3D11,简单封装。此处使用 D3D 11 接口封装,对多显卡多显示器的情况只能截取主显卡主显示器画面,如需截取其他屏幕,需稍微改造构造函数。截屏可能失败,也可能截取到黑屏,已经在返回值中提示。

将 DX 截屏转换成 C# 图像使用了指针操作,一方面可以提升性能,一方面也是因为都用 DX 了,基本上是很难避免底层操作了,那就一不做二不休,多利用一下。

     public class DirectXScreenCapturer : IDisposable
{
private Factory1 factory;
private Adapter1 adapter;
private SharpDX.Direct3D11.Device device;
private Output output;
private Output1 output1;
private Texture2DDescription textureDesc;
//2D 纹理,存储截屏数据
private Texture2D screenTexture; public DirectXScreenCapturer()
{
// 获取输出设备(显卡、显示器),这里是主显卡和主显示器
factory = new Factory1();
adapter = factory.GetAdapter1();
device = new SharpDX.Direct3D11.Device(adapter);
output = adapter.GetOutput();
output1 = output.QueryInterface<Output1>(); //设置纹理信息,供后续使用(截图大小和质量)
textureDesc = new Texture2DDescription
{
CpuAccessFlags = CpuAccessFlags.Read,
BindFlags = BindFlags.None,
Format = Format.B8G8R8A8_UNorm,
Width = output.Description.DesktopBounds.Right,
Height = output.Description.DesktopBounds.Bottom,
OptionFlags = ResourceOptionFlags.None,
MipLevels = ,
ArraySize = ,
SampleDescription = { Count = , Quality = },
Usage = ResourceUsage.Staging
}; screenTexture = new Texture2D(device, textureDesc);
} public Result ProcessFrame(Action<DataBox, Texture2DDescription> processAction, int timeoutInMilliseconds = )
{
//截屏,可能失败
using OutputDuplication duplicatedOutput = output1.DuplicateOutput(device);
var result = duplicatedOutput.TryAcquireNextFrame(timeoutInMilliseconds, out OutputDuplicateFrameInformation duplicateFrameInformation, out SharpDX.DXGI.Resource screenResource); if (!result.Success) return result; using Texture2D screenTexture2D = screenResource.QueryInterface<Texture2D>(); //复制数据
device.ImmediateContext.CopyResource(screenTexture2D, screenTexture);
DataBox mapSource = device.ImmediateContext.MapSubresource(screenTexture, , MapMode.Read, SharpDX.Direct3D11.MapFlags.None); processAction?.Invoke(mapSource, textureDesc); //释放资源
device.ImmediateContext.UnmapSubresource(screenTexture, );
screenResource.Dispose();
duplicatedOutput.ReleaseFrame(); return result;
} public (Result result, bool isBlackFrame, Image image) GetFrameImage(int timeoutInMilliseconds = )
{
//生成 C# 用图像
Bitmap image = new Bitmap(textureDesc.Width, textureDesc.Height, PixelFormat.Format24bppRgb);
bool isBlack = true;
var result = ProcessFrame(ProcessImage); if (!result.Success) image.Dispose(); return (result, isBlack, result.Success ? image : null); void ProcessImage(DataBox dataBox, Texture2DDescription texture)
{
BitmapData data = image.LockBits(new Rectangle(, , texture.Width, texture.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); unsafe
{
byte* dataHead = (byte*)dataBox.DataPointer.ToPointer(); for (int x = ; x < texture.Width; x++)
{
for (int y = ; y < texture.Height; y++)
{
byte* pixPtr = (byte*)(data.Scan0 + y * data.Stride + x * ); int pos = x + y * texture.Width;
pos *= ; byte r = dataHead[pos + ];
byte g = dataHead[pos + ];
byte b = dataHead[pos + ]; if (isBlack && (r != || g != || b != )) isBlack = false; pixPtr[] = b;
pixPtr[] = g;
pixPtr[] = r;
}
}
} image.UnlockBits(data);
}
} #region IDisposable Support
private bool disposedValue = false; // 要检测冗余调用 protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: 释放托管状态(托管对象)。
factory.Dispose();
adapter.Dispose();
device.Dispose();
output.Dispose();
output1.Dispose();
screenTexture.Dispose();
} // TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
// TODO: 将大型字段设置为 null。
factory = null;
adapter = null;
device = null;
output = null;
output1 = null;
screenTexture = null; disposedValue = true;
}
} // TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
// ~DirectXScreenCapturer()
// {
// // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
// Dispose(false);
// } // 添加此代码以正确实现可处置模式。
public void Dispose()
{
// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
Dispose(true);
// TODO: 如果在以上内容中替代了终结器,则取消注释以下行。
// GC.SuppressFinalize(this);
}
#endregion
}

使用示例

其中使用了窗口枚举辅助类,详细代码请看文章末尾的 Github 项目。支持 .Net Core。

         static async Task Main(string[] args)
{
Console.Write("按任意键开始DX截图……");
Console.ReadKey(); string path = @"E:\截图测试"; var cancel = new CancellationTokenSource();
await Task.Run(() =>
{
Task.Run(() =>
{
Thread.Sleep();
cancel.Cancel();
Console.WriteLine("DX截图结束!");
});
var savePath = $@"{path}\DX";
Directory.CreateDirectory(savePath); using var dx = new DirectXScreenCapturer();
Console.WriteLine("开始DX截图……"); while (!cancel.IsCancellationRequested)
{
var (result, isBlackFrame, image) = dx.GetFrameImage();
if (result.Success && !isBlackFrame) image.Save($@"{savePath}\{DateTime.Now.Ticks}.jpg", ImageFormat.Jpeg);
image?.Dispose();
}
}, cancel.Token); var windows = WindowEnumerator.FindAll();
for (int i = ; i < windows.Count; i++)
{
var window = windows[i];
Console.WriteLine($@"{i.ToString().PadLeft(3, ' ')}. {window.Title}
{window.Bounds.X}, {window.Bounds.Y}, {window.Bounds.Width}, {window.Bounds.Height}");
} var savePath = $@"{path}\Gdi";
Directory.CreateDirectory(savePath);
Console.WriteLine("开始Gdi窗口截图……"); foreach (var win in windows)
{
var image = CaptureWindow.ByHwnd(win.Hwnd);
image.Save($@"{savePath}\{win.Title.Substring(win.Title.LastIndexOf(@"\") < 0 ? 0 : win.Title.LastIndexOf(@"\") + 1).Replace("/", "").Replace("*", "").Replace("?", "").Replace("\"", "").Replace(":", "").Replace("<", "").Replace(">", "").Replace("|", "")}.jpg", ImageFormat.Jpeg);
image.Dispose();
}
Console.WriteLine("Gdi窗口截图结束!"); Console.ReadKey();
}

结语

这个示例代码中的 DX 截图只支持 win7 以上版本,xp 是时候退出历史舞台了。代码参考了网上大神的文章,并根据实际情况进行改造,尽可能简化实现和使用代码,展示最简单情况下所必须的代码。如果实际需求比较复杂,可以以这个为底版进行改造。

转载请完整保留以下内容并在显眼位置标注,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!

  本文地址:https://www.cnblogs.com/coredx/p/12422559.html

  完整源代码:Github

  里面有各种小东西,这只是其中之一,不嫌弃的话可以Star一下。

Windows GDI 窗口与 Direct3D 屏幕截图的更多相关文章

  1. [转]Windows的窗口刷新机制

    1.Windows的窗口刷新管理 窗口句柄(HWND)都是由操作系统内核管理的,系统内部有一个z-order序列,记录着当前窗口从屏幕底部(假象的从屏幕到眼睛的方向),到屏幕最高层的一个窗口句柄的排序 ...

  2. 【Visual C++】Windows GDI贴图闪烁解决方法

    一般的windows 复杂的界面需要使用多层窗口而且要用贴图来美化,所以不可避免在窗口移动或者改变大小的时候出现闪烁. 先来谈谈闪烁产生的原因 原因一:如果熟悉显卡原理的话,调用GDI函数向屏幕输出的 ...

  3. Delphi利用Windows GDI实现文字倾斜

    Delphi利用Windows GDI实现文字倾斜 摘要 Delphi利用Windows GDI实现文字倾斜 procedure TForm1.FormPaint(Sender: TObject);v ...

  4. 用 windows GDI 实现软光栅化渲染器--gdi3d(开源)

    尝试用windows GDI实现了一个简单的软光栅化渲染器,把OpenGL渲染管线实现了一遍,还是挺有收获的,搞清了以前一些似是而非的疑惑. ----更新2015-10-16代码已上传.gihub地址 ...

  5. 眼见为实(2):介绍Windows的窗口、消息、子类化和超类化

    眼见为实(2):介绍Windows的窗口.消息.子类化和超类化 这篇文章本来只是想介绍一下子类化和超类化这两个比较“生僻”的名词.为了叙述的完整性而讨论了Windows的窗口和消息,也简要讨论了进程和 ...

  6. C# windows GDI+仿画图 绘图程序设计

    C# windows GDI+仿画图 绘图程序设计 1.介绍 这里分享一个简单的画图程序 原作者:author: ping3108@163.com 2.程序主窗体设计 3.程序设计 本程序工程使用VS ...

  7. Windows定位窗口对应的exe文件

    一.说明 以下两种情况我们会想要定位窗口是由哪个exe文件,或者什么命令启用 第一种是:广告窗口,现在经常时不时冒出一个广告窗口,要么是完全看不出哪个程序启动,要么是虽然大概知道是哪个应用启动(比如w ...

  8. windows cmd窗口提示“telnet”命令不能内部或外部命令,也不是可运行的程序

    windows cmd窗口提示“telnet”命令不能内部或外部命令,也不是可运行的程序 原因:C:\Windows\System32目录下没有telnet.exe,path系统变量的值包含了C:\W ...

  9. 自定义HttpModule,用于未登录用户,不弹出Windows认证窗口,而是跳转回SSO站点

    2012年的一篇随笔记录,可以学习到如何自定义HttpModule,而具体里面针对需求开发的代码,可能未必能让大伙了解到什么,可快速扫描而过. using System; using System.W ...

随机推荐

  1. 金蝶CLOUD消息队列服务

  2. RDD(七)——依赖

    概述 RDD只支持粗粒度转换,即在大量记录上执行的单个操作.将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区.RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD ...

  3. 2. Unconstrained Optimization(2th)

    2.1 Basic Results on the Existence of Optimizers 2.1. DefinitionLet $f:U->\mathbb{R}$ be a functi ...

  4. k8s中command、args和dockerfile中的entrypoint、cmd之间的关系

    当用户同时写了command和args的时候自然是可以覆盖DockerFile中ENTRYPOINT的命令行和参数,那么对于具体情况呢,比如仅仅写了command或者args的时候呢?完整的情况分类如 ...

  5. 量化预测质量之分类报告 sklearn.metrics.classification_report

    classification_report的调用为:classification_report(y_true, y_pred, labels=None, target_names=None, samp ...

  6. 74)PHP,Session的一些属性

    (1) (2)有效期在  会话周期结束(就是将浏览器关闭前) (3)有效路径:  整站都有效 (4)有效域:当前域 (5)是否安全传输:否 (6)以上的session数据的特征都是由一个问题导致的,就 ...

  7. MOOC(9)- 登录接口返回的cookie中有多个token

  8. 双因子方差分析|adjusted R|强度|SSA|SSE|SST|

    应用统计学 方差分析的基本假设: 组间组平均与总平均的不同是由treatment引发的.单个值与组平均的不同是由组内error引发的. 如果没有处理误差SSA=SSE,所以右尾假设如果F>1则处 ...

  9. 遍历一个文件夹,打印出该文件夹下所有的py文件

    import os def iterbrowse(path): for home, dirs, files in os.walk(path): for filename in files: yield ...

  10. 基于ci框架 修改出来了一个带农历的万年历。

    1这里没有写model:代码一看就懂,没什么负杂地方,就是麻烦一点. 直接control模块的代码: <?php if ( ! defined('BASEPATH')) exit('No dir ...