Skip to main content

C# 截取指定应用窗口图像

效果图

前言

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

正文

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

GDI

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

CaptureWindow.cs
using System;
using System.Drawing;
using System.Runtime.InteropServices;

namespace AIYoloV8
{
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 = 1067;//windowRect.right - windowRect.left;
int height = 600;//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, 0, 0, width, height, hdcSrc, 0, 0, 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, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.SRCCOPY);
Gdi32.SelectObject(hdcDest, hOld);
Gdi32.DeleteDC(hdcDest);
User32.ReleaseDC(handle, hdcSrc);
Image img = Image.FromHbitmap(hBitmap);
Gdi32.DeleteObject(hBitmap);
return img;
}

public static Image ScaleImage(Image image, int maxWidth, int maxHeight, Color backColor)
{
var ratioX = (double)maxWidth / image.Width;
var ratioY = (double)maxHeight / image.Height;
var ratio = Math.Min(ratioX, ratioY);

var newWidth = (int)(image.Width * ratio);
var newHeight = (int)(image.Height * ratio);

var newImage = new Bitmap(maxWidth, maxHeight);
using (var graphics = Graphics.FromImage(newImage))
{
// 使用指定的背景色填充整个图像
graphics.Clear(backColor);

// 计算新图像中心的位置
var x = (maxWidth - newWidth) / 2;
var y = (maxHeight - newHeight) / 2;

graphics.DrawImage(image, x, y, newWidth, newHeight);
}

return newImage;
}
}
}

Direct3D

安装 nuget 包 SharpDX.Direct3D11,简单封装。此处使用 D3D 11 接口封装,对多显卡多显示器的情况只能截取主显卡主显示器画面,如需截取其他屏幕,需稍微改造构造函数。截屏可能失败,也可能截取到黑屏,已经在返回值中提示。将 DX 截屏转换成 C# 图像使用了指针操作,一方面可以提升性能,一方面也是因为都用 DX 了,基本上是很难避免底层操作了,那就一不做二不休,多利用一下。

DirectXScreenCapturer.cs
using SharpDX;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;

namespace AIYoloV8
{
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(0);
device = new SharpDX.Direct3D11.Device(adapter);
output = adapter.GetOutput(0);
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 = 1,
ArraySize = 1,
SampleDescription = { Count = 1, Quality = 0 },
Usage = ResourceUsage.Staging
};

screenTexture = new Texture2D(device, textureDesc);
}

public Result ProcessFrame(Action<DataBox, Texture2DDescription> processAction, int timeoutInMilliseconds = 5)
{
//截屏,可能失败
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, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None);

processAction?.Invoke(mapSource, textureDesc);

//释放资源
device.ImmediateContext.UnmapSubresource(screenTexture, 0);
screenResource.Dispose();
duplicatedOutput.ReleaseFrame();

return result;
}

public (Result result, bool isBlackFrame, Image image) GetFrameImage(int timeoutInMilliseconds = 5)
{
//生成 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(0, 0, texture.Width, texture.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

unsafe
{
byte* dataHead = (byte*)dataBox.DataPointer.ToPointer();

for (int x = 0; x < texture.Width; x++)
{
for (int y = 0; y < texture.Height; y++)
{
byte* pixPtr = (byte*)(data.Scan0 + y * data.Stride + x * 3);

int pos = x + y * texture.Width;
pos *= 4;

byte r = dataHead[pos + 2];
byte g = dataHead[pos + 1];
byte b = dataHead[pos + 0];

if (isBlack && (r != 0 || g != 0 || b != 0)) isBlack = false;

pixPtr[0] = b;
pixPtr[1] = g;
pixPtr[2] = 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
}
}
WindowEnumerator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace AIYoloV8
{
/// <summary>
/// 包含枚举当前用户空间下所有窗口的方法。
/// </summary>
public class WindowEnumerator
{
/// <summary>
/// 查找当前用户空间下所有符合条件的窗口(仅查找顶层窗口)。如果不指定条件,将返回所有窗口。
/// </summary>
/// <param name="match">过滤窗口的条件。</param>
/// <returns>找到的所有窗口信息。</returns>
public static IReadOnlyList<WindowInfo> FindAll(Predicate<WindowInfo> match = null)
{
var windowList = new List<WindowInfo>();
User32.EnumWindows(OnWindowEnum, 0);
return match == null ? windowList : windowList.FindAll(match);

bool OnWindowEnum(IntPtr hWnd, int lparam)
{
// 仅查找顶层窗口。
if (User32.GetParent(hWnd) == IntPtr.Zero)
{
var windowDetail = GetWindowDetail(hWnd);
// 添加到已找到的窗口列表。
windowList.Add(windowDetail);
}

return true;
}
}

private static WindowInfo GetWindowDetail(IntPtr hWnd)
{
// 获取窗口类名。
var lpString = new StringBuilder(512);
User32.GetClassName(hWnd, lpString, lpString.Capacity);
var className = lpString.ToString();

// 获取窗口标题。
var lptrString = new StringBuilder(512);
User32.GetWindowText(hWnd, lptrString, lptrString.Capacity);
var title = lptrString.ToString().Trim();

// 获取窗口可见性。
var isVisible = User32.IsWindowVisible(hWnd);

// 获取窗口位置和尺寸。
User32.LPRECT rect = default;
User32.GetWindowRect(hWnd, ref rect);
var bounds = new Rect(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);

return new WindowInfo(hWnd, className, title, isVisible, bounds);
}
public static List<WindowInfo> GetAllAboveWindows(IntPtr hwnd)
{
var windowInfos = new List<WindowInfo>();
var intPtr = User32.GetWindow(hwnd, 3);
if (intPtr == IntPtr.Zero)
{
return windowInfos;
}
var windowDetail = GetWindowDetail(intPtr);
windowInfos.AddRange(GetAllAboveWindows(intPtr));
windowInfos.Add(windowDetail);
return windowInfos;
}
public static List<WindowInfo> GetAllBelowWindows(IntPtr hwnd)
{
var windowInfos = new List<WindowInfo>();
var intPtr = User32.GetWindow(hwnd, 2);
if (intPtr == IntPtr.Zero)
{
return windowInfos;
}
var windowDetail = GetWindowDetail(intPtr);
windowInfos.AddRange(GetAllBelowWindows(intPtr));
windowInfos.Add(windowDetail);
return windowInfos;
}
}
}

测试
  Image image = CaptureWindow.ByHwnd(keyValuePairs[listBox1.SelectedItem.ToString()]);
image = CaptureWindow.ScaleImage(image, 640, 640, Color.Black);
pictureBox1.Image = image;
image.Save($@"dnf.jpg", ImageFormat.Jpeg);

总结

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

友情推荐:源码地址 文章来源:https://www.cnblogs.com/coredx/p/12422559.html