WPF 实现调用 WindowsAPI 实现屏幕录制
- 框架使用
.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(); } } }
效果图
以上就是WPF调用WindowsAPI实现屏幕录制的详细内容,更多关于WPF WindowsAPI屏幕录制的资料请关注其它相关文章!