wpf截屏系列
第一章 使用GDI实现截屏(本章)
第二章 使用GDI+实现截屏
第三章 使用DockPanel制作截屏框
第四章 实现截屏框热键截屏
第五章 实现截屏框实时截屏
第六章 使用ffmpeg命令行实现录屏
文章目录
前言
wpf截屏时通常可以采用gdi+,调用起来比较方便。使用gdi也能实现截屏,截屏数据也能转成BitmapSource对象,当然调用流程会复杂一些,而且需要引入win32方法,唯一比较容易的就是可以直接绘制异或鼠标。
一、导入gdi32
方法一、NuGet获取
这种方法好处是简单方便,缺点是增加了依赖dll,生成的程序容量大一些且附带一些dll。
(1)、获取gdi32
(2)、获取user32
方法二、Dllimport
使用DllImport将需要的win32 api导入。这样做工作量比较大,但是好处是无依赖,生成程序很小。
示例如下:
csharp
[DllImport(User32, SetLastError = false, ExactSpelling = true)]
public static extern IntPtr GetDC([In, Optional] IntPtr ptr);
完整的gdi需要导入的所有接口见附录。
二、实现步骤
1、创建兼容DC
csharp
IntPtr srcHdc = IntPtr.Zero;
IntPtr dstHdc = IntPtr.Zero;
srcHdc = GetDC(hWnd);
dstHdc = CreateCompatibleDC(srcHdc);
2、创建位图
csharp
BITMAPINFO bmi = new BITMAPINFO();
IntPtr hBitmap = IntPtr.Zero;
IntPtr oldBitmap = IntPtr.Zero;
bmi.bmiHeader.biSize = (uint)Marshal.SizeOf<BITMAPINFOHEADER>();
bmi.bmiHeader.biWidth = capWidth;
bmi.bmiHeader.biHeight = -capHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
bmi.bmiHeader.biCompression = BitmapCompressionMode.BI_RGB;
bmi.bmiHeader.biSizeImage = 0;
bmi.bmiHeader.biXPelsPerMeter = 0;
bmi.bmiHeader.biYPelsPerMeter = 0;
bmi.bmiHeader.biClrUsed = 0;
bmi.bmiHeader.biClrImportant = 0;
hBitmap = CreateDIBSection(dstHdc, in bmi, DIBColorMode.DIB_RGB_COLORS, out pvBits, IntPtr.Zero, 0);
oldBitmap = SelectObject(dstHdc, hBitmap);
3、获取位图信息
需要获取位图的行对齐stride
csharp
BITMAP bitmap;
temp = Marshal.AllocHGlobal(Marshal.SizeOf<BITMAP>());
if (GetObject(hBitmap, Marshal.SizeOf<BITMAP>(), temp) == 0)
{
throw new Exception("GetObject Failed");
}
bitmap = Marshal.PtrToStructure<BITMAP>(temp);
4、BitBlt
csharp
BitBlt(dstHdc, capX, capY, capWidth, capHeight, srcHdc, capX, capY, RasterOperationMode.SRCCOPY | RasterOperationMode.CAPTUREBLT)
5、获取数据
csharp
//行对齐
int stride=bitmap.bmWidthBytes;
//宽
int width=bitmap.bmWidth;
//高
int height=bitmap.bmHeight;
//位图数据
IntPtr pBuffer=bitmap.bmBits;
BITMAP转成WriteableBitmap(BitmapSource)
csharp
public static WriteableBitmap ToWriteableBitmap(this BITMAP bitmap)
{
var wb = new WriteableBitmap(bitmap.bmWidth, bitmap.bmHeight, 0, 0, bitmap.bmBitsPixel == 32 ? PixelFormats.Bgra32 : PixelFormats.Bgr24, null);
wb.WritePixels(new Int32Rect(0, 0, bitmap.bmWidth, bitmap.bmHeight), bitmap.bmBits, bitmap.bmHeight * bitmap.bmWidthBytes, bitmap.bmWidthBytes, 0, 0);
return wb;
}
6、销毁资源
csharp
if (dstHdc != IntPtr.Zero)
{
if (oldBitmap != IntPtr.Zero)
{
var ret = SelectObject(dstHdc, oldBitmap);
if (ret == IntPtr.Zero)
{
errors += "SelectObject Failed";
}
}
if (!DeleteDC(dstHdc))
{
errors += "DeleteDC Failed";
}
}
if (srcHdc != IntPtr.Zero)
{
if (!ReleaseDC(hWnd, srcHdc))
{
errors += "ReleaseDC Failed";
}
}
if (!DeleteObject(hBitmap))
{
errors += "DeleteObject Failed";
}
if (temp != IntPtr.Zero) Marshal.FreeHGlobal(temp);
三、封装成对象
csharp
/************************************************************************
* @Project: GdiGrabber
* @Decription: gdi图片采集
* 可以根据提供的句柄采集图片或者获取快照。提供了采集区域提供了实际值和比例值
* 两种接口。采集提供了同步和异步两种方式,在主线程或者UI线程建议用异步,在非
* UI线程建议用同步。
* @Verision: v1.0.0
* @Author: Xin Nie
* @Create: 2024/03/13 23:57:00
* @LastUpdate: 2024/03/13 23:57:00
************************************************************************
* Copyright @ 2024. All rights reserved.
************************************************************************/
public static class GdiGrabber
{
/// <summary>
/// 快照
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="hwnd"></param>
/// <returns></returns>
public static WriteableBitmap? SnapshotSync(int x, int y, int width, int height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 快照
/// 按比例,在任意分辨率,比如0,0,1,1就是全屏。
/// </summary>
/// <param name="x">比例,0-1</param>
/// <param name="y">比例,0-1</param>
/// <param name="width">比例,0-1</param>
/// <param name="height">比例,0-1</param>
/// <param name="hwnd"></param>
/// <returns></returns>
public static WriteableBitmap? SnapshotSync(double x, double y, double width, double height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 快照,异步
/// 一般情况用同步即可,截屏会阻塞几十毫秒,异步是不会阻塞到当前线程。
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="hwnd"></param>
/// <returns></returns>
public static async Task<WriteableBitmap?> Snapshot(int x, int y, int width, int height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 快照,异步
///一般情况用同步即可,截屏会阻塞几十毫秒,异步是不会阻塞到当前线程。
/// 按比例,在任意分辨率,比如0,0,1,1就是全屏。
/// </summary>
/// <param name="x">比例,0-1</param>
/// <param name="y">比例,0-1</param>
/// <param name="width">比例,0-1</param>
/// <param name="height">比例,0-1</param>
/// <param name="hwnd"></param>
/// <returns></returns>
public static async Task<WriteableBitmap?> Snapshot(double x, double y, double width, double height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 采集,异步
/// 按比例,在任意分辨率,比如0,0,1,1就是全屏。
/// 用法 await foreach(var i in GdiGrabber.Capture()){}
/// </summary>
/// <param name="x">比例,0-1</param>
/// <param name="y">比例,0-1</param>
/// <param name="width">比例,0-1</param>
/// <param name="height">比例,0-1</param>
/// <param name="hWnd">句柄,为0则采集桌面</param>
/// <param name="isPaintMouse">是否绘制鼠标</param>
/// <returns>采集的数据对象</returns>
public static IAsyncEnumerable<BITMAP> Capture(double x, double y, double width, double height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 采集,异步
/// 用法 await foreach(var i in GdiGrabber.Capture()){}
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="hWnd">句柄,为0则采集桌面</param>
/// <param name="isPaintMouse">是否绘制鼠标</param>
/// <returns>采集的数据对象</returns>
/// <exception cref="Exception"></exception>
public static async IAsyncEnumerable<BITMAP> Capture(int x, int y, int width, int height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 采集,同步
/// 用法 foreach(var i in GdiGrabber.CaptureSync()){}
/// </summary>
/// <param name="x">比例,0-1</param>
/// <param name="y">比例,0-1</param>
/// <param name="width">比例,0-1</param>
/// <param name="height">比例,0-1</param>
/// <param name="hWnd">句柄,为0则采集桌面</param>
/// <param name="isPaintMouse">是否绘制鼠标</param>
/// </param>
/// <returns>采集的数据对象</returns>
/// <exception cref="Exception"></exception>
public static IEnumerable<BITMAP> CaptureSync(double x, double y, double width, double height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 采集,同步
/// 用法 foreach(var i in GdiGrabber.CaptureSync()){}
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="hWnd">句柄,为0则采集桌面</param>
/// <param name="isPaintMouse">是否绘制鼠标</param>
/// </param>
/// <returns>采集的数据对象</returns>
/// <exception cref="Exception"></exception>
public static IEnumerable<BITMAP> CaptureSync(int x, int y, int width, int height, nint hWnd = 0, bool isPaintMouse = true);
/// <summary>
/// 将BITMAP转换成WriteableBitmap
/// 作者的设备测试此操作1080p耗时8ms
/// </summary>
/// <param name="bitmap">this</param>
/// <returns>WriteableBitmap</returns>
public static WriteableBitmap ToWirteableBitmap(this BITMAP bitmap);
/// <summary>
/// 将BITMAP数据拷贝到riteableBitmap
/// 作者的设备测试此操作1080p耗时2ms
/// </summary>
/// <param name="bitmap">this</param>
/// <param name="wb">WriteableBitmap</param>
public static void CopyToWriteableBitmap(this BITMAP bitmap, WriteableBitmap wb);
}
四、完整代码
vs2022 .net6.0 wpf项目,采用DllImport的方式无任何依赖。
https://download.csdn.net/download/u013113678/88983450
五、使用示例
1、快照
(1)比例值区域截取
截取全屏(任意分辨率)
csharp
WriteableBitmap? wb= GdiGrabber.SnapshotSync(0,0,1.0,1.0);
(2)实际值区域截取
csharp
WriteableBitmap? wb= GdiGrabber.SnapshotSync(0,0,600,600);
(3)WPF中使用
WpfGdiGrabber.xaml
xml
<Window x:Class="WpfGdiGrabber.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfGdiGrabber"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid >
<Image x:Name="img"></Image>
</Grid>
</Window>
WpfGdiGrabber.cs
csharp
using AC;
using System.Windows;
using System.Windows.Media.Imaging;
namespace WpfGdiGrabber
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
img.Source = GdiGrabber.SnapshotSync(0, 0, 1.0, 1.0);
}
}
}
效果预览
2、采集
(1)、异步
UI线程使用
csharp
await foreach (var i in GdiGrabber.Capture(0, 0, 1.0, 1.0, 0))
{
//img为Image控件
if (img.Source == null)
img.Source = i.ToWriteableBitmap();
else
i.CopyToWriteableBitmapp(img.Source as WriteableBitmap);
}
非UI线程使用,最好启动一个Dispatcher用于调度消息以及阻塞线程避免结束。
csharp
new Thread(async () =>
{
bool isExit = false;
var frame = new DispatcherFrame();
var func = async () =>
{
//循环采集
await foreach (var i in GdiGrabber.Capture(0, 0, 1920, 1080, 0))
{
//Dispatcher将操作切换到UI线程执行
Dispatcher.Invoke(() =>
{
//WriteableBitmap是和线程绑定的,需要在UI线程创建此对象。
WriteableBitmap? wb = i.ToWriteableBitmap();
});
//退出采集
if (isExit) break;
}
//退出消息循环
frame.Continue = false;
};
func();
//启动Dispatcher消息循环,此行阻塞
Dispatcher.PushFrame(frame);
})
{ IsBackground = true }.Start();
(2)、同步
同步的方式会阻塞,建议在非UI线程中使用。但要注意WriteableBitmap 需要在UI线程中创建才能被控件使用。
csharp
foreach (var i in GdiGrabber.CaptureSync(0, 0, 1920, 1080))
{
Dispatcher.Invoke(() =>
{
if (img.Source == null)
img.Source = i.ToWriteableBitmap();
else
i.CopyToWriteableBitmap(img.Source as WriteableBitmap);
});
}
(3)、WPF中使用(异步)
WpfGdiGrabber.xaml
xml
<Window x:Class="WpfGdiGrabber.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfGdiGrabber"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid >
<Image x:Name="img"></Image>
</Grid>
</Window>
WpfGdiGrabber.cs
csharp
using AC;
using System.Windows;
using System.Windows.Media.Imaging;
namespace WpfGdiGrabber
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Capture();
}
async void Capture()
{
await foreach (var i in GdiGrabber.Capture(0, 0, 1.0, 1.0, 0))
{
if (img.Source == null)
img.Source = i.ToWriteableBitmap();
else
i.CopyToWriteableBitmap(img.Source as WriteableBitmap);
}
}
}
}
效果预览
总结
以上就是今天要讲的内容,本文实现了的GDI截屏与GDI+对比性能略差一些,但也是可以一定程度满足使用,比如用来截屏或者制作放大镜。而且有一个好处是可以做到无额外依赖。总的来说,可以作为一种备选方案或者测试方案。
附录
DllImport
csharp
[StructLayout(LayoutKind.Sequential)]
public struct BITMAP
{
public int bmType;
public int bmWidth;
public int bmHeight;
public int bmWidthBytes;
public ushort bmPlanes;
public ushort bmBitsPixel;
public IntPtr bmBits;
}
class WinApiImport
{
const string Gdi32 = "gdi32.dll";
const string User32 = "user32.dll";
[StructLayout(LayoutKind.Sequential), Serializable]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential), Serializable]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[StructLayout(LayoutKind.Sequential, Size = 4)]
public struct RGBQUAD
{
public byte rgbBlue;
public byte rgbGreen;
public byte rgbRed;
public byte rgbReserved;
}
[StructLayout(LayoutKind.Sequential)]
public struct BITMAPINFO
{
public BITMAPINFOHEADER bmiHeader;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public RGBQUAD[] bmiColors;
}
public enum BitmapCompressionMode : uint
{
BI_RGB = 0,
BI_RLE8 = 1,
BI_RLE4 = 2,
BI_BITFIELDS = 3,
BI_JPEG = 4,
BI_PNG = 5
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct BITMAPINFOHEADER
{
public uint biSize;
public int biWidth;
public int biHeight;
public ushort biPlanes;
public ushort biBitCount;
public BitmapCompressionMode biCompression;
public uint biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public uint biClrUsed;
public uint biClrImportant;
}
public enum DIBColorMode : int
{
DIB_RGB_COLORS = 0,
DIB_PAL_COLORS = 1
}
public enum CursorState
{
CURSOR_HIDDEN = 0,
CURSOR_SHOWING = 0x00000001,
CURSOR_SUPPRESSED = 0x00000002,
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CURSORINFO
{
public uint cbSize;
public CursorState flags;
public IntPtr hCursor;
public POINT ptScreenPos;
}
[StructLayout(LayoutKind.Sequential)]
public sealed class ICONINFO
{
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}
public enum RasterOperationMode
{
SRCCOPY = 0x00CC0020,
SRCPAINT = 0x00EE0086,
SRCAND = 0x008800C6,
SRCINVERT = 0x00660046,
SRCERASE = 0x00440328,
NOTSRCCOPY = 0x00330008,
NOTSRCERASE = 0x001100A6,
MERGECOPY = 0x00C000CA,
MERGEPAINT = 0x00BB0226,
PATCOPY = 0x00F00021,
PATPAINT = 0x00FB0A09,
PATINVERT = 0x005A0049,
DSTINVERT = 0x00550009,
BLACKNESS = 0x00000042,
WHITENESS = 0x00FF0062,
NOMIRRORBITMAP = -2147483648,
CAPTUREBLT = 0x40000000
}
[DllImport(User32, CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
[System.Security.SecurityCritical]
public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
[DllImport(User32, SetLastError = false, ExactSpelling = true)]
public static extern IntPtr GetDesktopWindow();
[DllImport(User32, SetLastError = false, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[DllImport(User32, SetLastError = false, ExactSpelling = true)]
public static extern IntPtr GetDC([In, Optional] IntPtr ptr);
[DllImport(Gdi32, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr CreateCompatibleDC([Optional] IntPtr hDC);
[DllImport(Gdi32, SetLastError = false, ExactSpelling = true)]
public static extern IntPtr CreateDIBSection([In, Optional] IntPtr hdc, in BITMAPINFO pbmi, DIBColorMode usage, out IntPtr ppvBits, [In, Optional] IntPtr hSection, [In, Optional] uint offset);
[DllImport(Gdi32, SetLastError = false, CharSet = CharSet.Auto)]
public static extern int GetObject(IntPtr hgdiobj, int cbBuffer, IntPtr lpvObject);
[DllImport(Gdi32, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
[DllImport(Gdi32, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, RasterOperationMode dwRop);
[DllImport(Gdi32, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
[System.Security.SecurityCritical]
public static extern bool DeleteDC(IntPtr hdc);
[DllImport(User32, SetLastError = false, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport(Gdi32, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport(User32, SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorInfo(ref CURSORINFO pci);
[DllImport(User32, SetLastError = true, ExactSpelling = true)]
public static extern IntPtr CopyIcon(IntPtr hIcon);
[DllImport(User32, SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetIconInfo(IntPtr hIcon, [In, Out] ICONINFO piconinfo);
[DllImport(User32, SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DrawIcon(IntPtr hDC, int X, int Y, IntPtr hIcon);
[DllImport(User32, SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyCursor(IntPtr hCursor);
[DllImport(User32, SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr LoadCursor(IntPtr hInstance, string lpCursorName);
}