WPF 实现调用 WindowsAPI 实现屏幕录制
控件名:DesktopRecord
作 者:WPFDevelopersOrg - 驚鏵
原文链接[1]:https://github.com/yanjinhuagood/DesktopRecord
框架使用
.NET4
Visual Studio 2022
接着上一篇做一个不依赖
ffmpeg
实现屏幕录制1000
毫秒调用WindowsAPI
进行截取屏幕获取图像,并保存jpg
文件到指定路径(保存的文件从0.jpg
至n.jpg
)。
1)获取屏幕图片并保存为 jpg
代码如下:
private static BitmapSource CaptureScreen()
{
IntPtr desk = GetDesktopWindow();
IntPtr dc = GetWindowDC(desk);
IntPtr memdc = CreateCompatibleDC(dc);
IntPtr bitmap = CreateCompatibleBitmap(dc, screenWidth, screenHeight);
SelectObject(memdc, bitmap);
BitBlt(memdc, 0, 0, screenWidth, screenHeight, dc, 0, 0, 0xCC0020);
BitmapSource source = Imaging.CreateBitmapSourceFromHBitmap(bitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
ReleaseDC(desk, dc);
return source;
}
Task.Factory.StartNew(() =>
{
while (IsRunning)
{
Thread.Sleep(1000);
num += 1;
Application.Current.Dispatcher.Invoke(new Action(() =>
{
var drawingVisual = new DrawingVisual();
POINT mousePosition;
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(CaptureScreen(),
new Rect(new Point(),
new Size(screenWidth, screenHeight)));
if (GetCursorPos(out mousePosition))
{
var cursorSize = 30;
var cursorHalfSize = cursorSize / 2;
var cursorCenterX = mousePosition.X - SystemParameters.VirtualScreenLeft;
var cursorCenterY = mousePosition.Y - SystemParameters.VirtualScreenTop;
drawingContext.DrawImage(GetCursorIcon(),
new Rect(new Point(cursorCenterX, cursorCenterY),
new Size(cursorSize, cursorSize)));
}
}
var png = Path.Combine(tempDir, $"{num}.jpg");
using (FileStream stream = new FileStream(png, FileMode.Create))
{
var bitmap = new RenderTargetBitmap((int)screenWidth, (int)screenHeight, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(drawingVisual);
var bitmapEncoder = BitmapFrame.Create(bitmap);
bitmapEncoder.Freeze();
var encoder = new JpegBitmapEncoder();
encoder.QualityLevel = 50;
encoder.Frames.Add(bitmapEncoder);
encoder.Save(stream);
encoder.Frames.Clear();
GC.Collect();
}
}));
}
});
当点击开始录制按钮时将窗体最小化,停止录制时通过循环之前保存的文件夹地址排序循环添加每一帧图像到
GifBitmapEncoder.Frames
中,但是在使用自带的GifBitmapEncoder
发现内存占用很高,当使用完成后没有释放GC
,所以放弃了使用它。哪位大佬有好的方式欢迎分享使用了
GifEncoder
自己写入GIF
文件。保存
gif
文件可以使用以下库FreeImage.NET
WpfAnimatedGif
ImageTools
Magick.NET
GifRenderer
2) MainWindow.xaml
代码如下:
<wd:Window
x:Class="DesktopRecord.View.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:vm="clr-namespace:DesktopRecord.ViewModel"
xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"
Title="屏幕录制"
Width="525"
Height="200"
Icon="/screen.ico"
ResizeMode="CanMinimize"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<wd:Window.DataContext>
<vm:MainVM />
</wd:Window.DataContext>
<Grid>
<TabControl>
<TabItem Header="ffmpeg 录制">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button
Margin="0,0,5,0"
Command="{Binding MyStart}"
Content="{Binding MyTime}"
Style="{StaticResource WD.SuccessPrimaryButton}" />
<Button
Margin="5,0,0,0"
Command="{Binding MyStop}"
Content="停止录制"
Style="{StaticResource WD.DangerPrimaryButton}" />
</StackPanel>
</TabItem>
<TabItem Header="WindowsAPI 录制">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button
Margin="0,0,5,0"
Command="{Binding RecordCommand}"
Content="开始录制"
Style="{StaticResource WD.SuccessPrimaryButton}" />
<Button
Margin="5,0,0,0"
wd:Loading.Child="{x:Static wd:NormalLoading.Default}"
wd:Loading.IsShow="{Binding IsShow}"
Command="{Binding RecordStopCommand}"
Content="停止录制"
Style="{StaticResource WD.DangerPrimaryButton}" />
</StackPanel>
</TabItem>
</TabControl>
</Grid>
</wd:Window>
3)创建 MainVM.cs
代码如下:
using DesktopRecord.Helper;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using WPFDevelopers.Controls;
using WPFDevelopers.Helpers;
namespace DesktopRecord.ViewModel
{
public class MainVM : ViewModelBase
{
private DispatcherTimer tm = new DispatcherTimer();
public int currentCount = 0;
private string myTime = "开始录制";
public string MyTime
{
get { return myTime; }
set
{
myTime = value;
NotifyPropertyChange("MyTime");
}
}
private bool isStart = true;
public bool IsStart
{
get { return isStart; }
set
{
isStart = value;
NotifyPropertyChange("IsStart");
}
}
private bool _isShow;
public bool IsShow
{
get { return _isShow; }
set
{
_isShow = value;
NotifyPropertyChange("IsShow");
}
}
private ICommand myStart;
public ICommand MyStart
{
get
{
return myStart ?? (myStart = new RelayCommand(p =>
{
App.Current.MainWindow.WindowState = System.Windows.WindowState.Minimized;
if (!FFmpegHelper.Start())
{
App.Current.MainWindow.WindowState = System.Windows.WindowState.Normal;
MessageBox.Show("未找到 【ffmpeg.exe】,请下载", "错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
return;
}
tm.Tick += tm_Tick;
tm.Interval = TimeSpan.FromSeconds(1);
tm.Start();
IsStart = false;
}, a =>
{
return IsStart;
}));
}
}
private void tm_Tick(object sender, EventArgs e)
{
currentCount++;
MyTime = "录制中(" + currentCount + "s)";
}
/// <summary>
/// 获取或设置
/// </summary>
private ICommand myStop;
/// <summary>
/// 获取或设置
/// </summary>
public ICommand MyStop
{
get
{
return myStop ?? (myStop = new RelayCommand(p =>
{
var task = new Task(() =>
{
FFmpegHelper.Stop();
MyTime = "开始录制";
tm.Stop();
currentCount = 0;
IsShow = true;
});
task.ContinueWith(previousTask =>
{
IsShow = false;
IsStart = true;
Process.Start(AppDomain.CurrentDomain.BaseDirectory);
}, TaskScheduler.FromCurrentSynchronizationContext());
task.Start();
}, a =>
{
return !IsStart;
}));
}
}
public ICommand RecordCommand { get; }
public ICommand RecordStopCommand { get; }
public MainVM()
{
RecordCommand = new RelayCommand(Record, CanExecuteRecordCommand);
RecordStopCommand = new RelayCommand(RecordStop);
}
void Record(object parameter)
{
App.Current.MainWindow.WindowState = System.Windows.WindowState.Minimized;
Win32.Start();
IsStart = false;
}
private bool CanExecuteRecordCommand(object parameter)
{
return IsStart;
}
void RecordStop(object parameter)
{
var task = new Task(() =>
{
Win32.Stop();
IsShow = true;
Win32.Save($"DesktopRecord_{DateTime.Now.ToString("yyyyMMddHHmmss")}.gif");
});
task.ContinueWith(previousTask =>
{
IsShow = false;
IsStart = true;
Process.Start(AppDomain.CurrentDomain.BaseDirectory);
}, TaskScheduler.FromCurrentSynchronizationContext());
task.Start();
}
}
}
4)创建 Win32.cs
代码如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DesktopRecord.Helper
{
public class Win32
{
[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("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, System.Int32 dwRop);
[DllImport("user32.dll")]
private static extern bool GetCursorInfo(out CURSORINFO pci);
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct CURSORINFO
{
public Int32 cbSize;
public Int32 flags;
public IntPtr hCursor;
public POINT ptScreenPos;
}
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
public static extern bool DestroyIcon(IntPtr handle);
private static string basePath = AppDomain.CurrentDomain.BaseDirectory;
private static string tempDir = Path.Combine(Path.GetTempPath(), "DesktopRecord");
private static Thread _thread = null;
public static bool IsRunning = false;
static int screenWidth = Convert.ToInt32(SystemParameters.PrimaryScreenWidth);
static int screenHeight = Convert.ToInt32(SystemParameters.PrimaryScreenHeight);
private static BitmapSource GetCursorIcon()
{
var cursorInfo = new CURSORINFO { cbSize = Marshal.SizeOf(typeof(CURSORINFO)) };
if (GetCursorInfo(out cursorInfo) && cursorInfo.hCursor != IntPtr.Zero)
{
try
{
return Imaging.CreateBitmapSourceFromHIcon(cursorInfo.hCursor, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}
finally
{
DestroyIcon(cursorInfo.hCursor);
}
}
return null;
}
private static BitmapSource CaptureScreen()
{
IntPtr desk = GetDesktopWindow();
IntPtr dc = GetWindowDC(desk);
IntPtr memdc = CreateCompatibleDC(dc);
IntPtr bitmap = CreateCompatibleBitmap(dc, screenWidth, screenHeight);
SelectObject(memdc, bitmap);
BitBlt(memdc, 0, 0, screenWidth, screenHeight, dc, 0, 0, 0xCC0020);
BitmapSource source = Imaging.CreateBitmapSourceFromHBitmap(bitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
ReleaseDC(desk, dc);
return source;
}
public static void Start()
{
if (_thread == null)
{
IsRunning = true;
_thread = new Thread(Record);
_thread.Start();
}
}
public static void Stop()
{
if (_thread != null)
{
IsRunning = false;
_thread = null;
}
}
private static void Record()
{
if (!Directory.Exists(tempDir))
Directory.CreateDirectory(tempDir);
else
{
foreach (string file in Directory.GetFiles(tempDir))
File.Delete(file);
}
int num = 0;
Task.Factory.StartNew(() =>
{
while (IsRunning)
{
Thread.Sleep(20);
num += 1;
Application.Current.Dispatcher.Invoke(new Action(() =>
{
var drawingVisual = new DrawingVisual();
POINT mousePosition;
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(CaptureScreen(),
new Rect(new Point(),
new Size(screenWidth, screenHeight)));
if (GetCursorPos(out mousePosition))
{
var cursorSize = 30;
var cursorHalfSize = cursorSize / 2;
var cursorCenterX = mousePosition.X - SystemParameters.VirtualScreenLeft;
var cursorCenterY = mousePosition.Y - SystemParameters.VirtualScreenTop;
drawingContext.DrawImage(GetCursorIcon(),
new Rect(new Point(cursorCenterX, cursorCenterY),
new Size(cursorSize, cursorSize)));
}
}
var png = Path.Combine(tempDir, $"{num}.jpg");
using (FileStream stream = new FileStream(png, FileMode.Create))
{
var bitmap = new RenderTargetBitmap((int)screenWidth, (int)screenHeight, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(drawingVisual);
var bitmapEncoder = BitmapFrame.Create(bitmap);
bitmapEncoder.Freeze();
var encoder = new JpegBitmapEncoder();
encoder.QualityLevel = 50;
encoder.Frames.Add(bitmapEncoder);
encoder.Save(stream);
encoder.Frames.Clear();
GC.Collect();
}
}));
}
});
}
public static void ClearRecording()
{
if (Directory.Exists(tempDir))
Directory.Delete(tempDir, true);
Directory.CreateDirectory(tempDir);
}
public static void Save(string output)
{
try
{
output = Path.Combine(basePath, output);
var imagePaths = Directory.GetFiles(tempDir, "*.jpg", SearchOption.TopDirectoryOnly);
if (imagePaths.Length == 0) return;
#region GC不释放,暂时弃用
//using (var gifFileStream = new FileStream(Output, FileMode.Create))
//{
// var gifBitmapEncoder = new GifBitmapEncoder();
// var jpgs = Directory.GetFiles(tempDir, "*.jpg", SearchOption.TopDirectoryOnly);
// if (jpgs.Length == 0) return;
// foreach (string file in jpgs)
// {
// using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read))
// {
// var bitmapDecoder = new JpegBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
// var bitmapFrame = bitmapDecoder.Frames[0];
// bitmapDecoder.Frames[0].Freeze();
// gifBitmapEncoder.Frames.Add(bitmapFrame);
// bitmapFrame = null;
// bitmapDecoder = null;
// GC.Collect();
// stream.Dispose();
// }
// }
// gifBitmapEncoder.Save(gifFileStream);
// gifBitmapEncoder.Frames.Clear();
// gifBitmapEncoder = null;
// GC.Collect();
// GC.WaitForPendingFinalizers();
//}
#endregion
var bitmapFrames = new List<BitmapFrame>();
foreach (string imagePath in imagePaths)
{
var frame = BitmapFrame.Create(new Uri(imagePath, UriKind.RelativeOrAbsolute));
bitmapFrames.Add(frame);
}
using (var gifStream = new MemoryStream())
{
using (var encoder = new GifEncoder(gifStream))
{
foreach (var imagePath in imagePaths)
{
var image = System.Drawing.Image.FromFile(imagePath);
encoder.AddFrame(image, 0, 0, TimeSpan.FromSeconds(0));
}
}
gifStream.Position = 0;
using (var fileStream = new FileStream(output, FileMode.Create))
{
fileStream.Write(gifStream.ToArray(), 0, gifStream.ToArray().Length);
}
}
}
catch
{
throw;
}
}
}
}
5)创建 GifEncoder.cs
代码如下:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
namespace DesktopRecord.Helper
{
public class GifEncoder : IDisposable
{
#region Header Constants
private const string FileType = "GIF";
private const string FileVersion = "89a";
private const byte FileTrailer = 0x3b;
private const int ApplicationExtensionBlockIdentifier = 0xff21;
private const byte ApplicationBlockSize = 0x0b;
private const string ApplicationIdentification = "NETSCAPE2.0";
private const int GraphicControlExtensionBlockIdentifier = 0xf921;
private const byte GraphicControlExtensionBlockSize = 0x04;
private const long SourceGlobalColorInfoPosition = 10;
private const long SourceGraphicControlExtensionPosition = 781;
private const long SourceGraphicControlExtensionLength = 8;
private const long SourceImageBlockPosition = 789;
private const long SourceImageBlockHeaderLength = 11;
private const long SourceColorBlockPosition = 13;
private const long SourceColorBlockLength = 768;
#endregion
private bool _isFirstImage = true;
private int? _width;
private int? _height;
private int? _repeatCount;
private readonly Stream _stream;
public TimeSpan FrameDelay { get; set; }
/// <summary>
/// Encodes multiple images as an animated gif to a stream. <br />
/// ALWAYS ALWAYS ALWAYS wire this in a using block <br />
/// Disposing the encoder will complete the file. <br />
/// Uses default .net GIF encoding and adds animation headers.
/// </summary>
/// <param name="stream">The stream that will be written to.</param>
/// <param name="width">Sets the width for this gif or null to use the first frame's width.</param>
/// <param name="height">Sets the height for this gif or null to use the first frame's height.</param>
public GifEncoder(Stream stream, int? width = null, int? height = null, int? repeatCount = null)
{
_stream = stream;
_width = width;
_height = height;
_repeatCount = repeatCount;
}
/// <summary>
/// Adds a frame to this animation.
/// </summary>
/// <param name="img">The image to add</param>
/// <param name="x">The positioning x offset this image should be displayed at.</param>
/// <param name="y">The positioning y offset this image should be displayed at.</param>
public void AddFrame(Image img, int x = 0, int y = 0, TimeSpan? frameDelay = null)
{
using (var gifStream = new MemoryStream())
{
img.Save(gifStream, ImageFormat.Gif);
if (_isFirstImage) // Steal the global color table info
{
InitHeader(gifStream, img.Width, img.Height);
}
WriteGraphicControlBlock(gifStream, frameDelay.GetValueOrDefault(FrameDelay));
WriteImageBlock(gifStream, !_isFirstImage, x, y, img.Width, img.Height);
}
_isFirstImage = false;
}
private void InitHeader(Stream sourceGif, int w, int h)
{
// File Header
WriteString(FileType);
WriteString(FileVersion);
WriteShort(_width.GetValueOrDefault(w)); // Initial Logical Width
WriteShort(_height.GetValueOrDefault(h)); // Initial Logical Height
sourceGif.Position = SourceGlobalColorInfoPosition;
WriteByte(sourceGif.ReadByte()); // Global Color Table Info
WriteByte(0); // Background Color Index
WriteByte(0); // Pixel aspect ratio
WriteColorTable(sourceGif);
// App Extension Header
WriteShort(ApplicationExtensionBlockIdentifier);
WriteByte(ApplicationBlockSize);
WriteString(ApplicationIdentification);
WriteByte(3); // Application block length
WriteByte(1);
WriteShort(_repeatCount.GetValueOrDefault(0)); // Repeat count for images.
WriteByte(0); // terminator
}
private void WriteColorTable(Stream sourceGif)
{
sourceGif.Position = SourceColorBlockPosition; // Locating the image color table
var colorTable = new byte[SourceColorBlockLength];
sourceGif.Read(colorTable, 0, colorTable.Length);
_stream.Write(colorTable, 0, colorTable.Length);
}
private void WriteGraphicControlBlock(Stream sourceGif, TimeSpan frameDelay)
{
sourceGif.Position = SourceGraphicControlExtensionPosition; // Locating the source GCE
var blockhead = new byte[SourceGraphicControlExtensionLength];
sourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE
WriteShort(GraphicControlExtensionBlockIdentifier); // Identifier
WriteByte(GraphicControlExtensionBlockSize); // Block Size
WriteByte(blockhead[3] & 0xf7 | 0x08); // Setting disposal flag
WriteShort(Convert.ToInt32(frameDelay.TotalMilliseconds / 10)); // Setting frame delay
WriteByte(blockhead[6]); // Transparent color index
WriteByte(0); // Terminator
}
private void WriteImageBlock(Stream sourceGif, bool includeColorTable, int x, int y, int h, int w)
{
sourceGif.Position = SourceImageBlockPosition; // Locating the image block
var header = new byte[SourceImageBlockHeaderLength];
sourceGif.Read(header, 0, header.Length);
WriteByte(header[0]); // Separator
WriteShort(x); // Position X
WriteShort(y); // Position Y
WriteShort(h); // Height
WriteShort(w); // Width
if (includeColorTable) // If first frame, use global color table - else use local
{
sourceGif.Position = SourceGlobalColorInfoPosition;
WriteByte(sourceGif.ReadByte() & 0x3f | 0x80); // Enabling local color table
WriteColorTable(sourceGif);
}
else
{
WriteByte(header[9] & 0x07 | 0x07); // Disabling local color table
}
WriteByte(header[10]); // LZW Min Code Size
// Read/Write image data
sourceGif.Position = SourceImageBlockPosition + SourceImageBlockHeaderLength;
var dataLength = sourceGif.ReadByte();
while (dataLength > 0)
{
var imgData = new byte[dataLength];
sourceGif.Read(imgData, 0, dataLength);
_stream.WriteByte(Convert.ToByte(dataLength));
_stream.Write(imgData, 0, dataLength);
dataLength = sourceGif.ReadByte();
}
_stream.WriteByte(0); // Terminator
}
private void WriteByte(int value)
{
_stream.WriteByte(Convert.ToByte(value));
}
private void WriteShort(int value)
{
_stream.WriteByte(Convert.ToByte(value & 0xff));
_stream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
}
private void WriteString(string value)
{
_stream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length);
}
public void Dispose()
{
// Complete File
WriteByte(FileTrailer);
// Pushing data
_stream.Flush();
}
}
}
参考资料
[1]
原文链接: https://github.com/yanjinhuagood/DesktopRecord