编辑
2025-09-25
C#
00

目录

🔍 痛点分析:为什么要自己造轮子?
现有方案的局限性
🎯 核心技术架构解析
多线程并发设计
🚀 性能优化:屏幕捕获算法
🎵 音频同步录制:多源混音
🎬 视频编码:FFMpegCore集成
🎨 用户界面:WPF现代化设计
⚠️ 关键问题与解决方案
内存泄漏防护
线程安全保障
异常处理机制
🔧 部署与扩展
NuGet包依赖
性能监控
💡 核心收获总结

在数字化办公时代,屏幕录制已成为开发者、产品经理、教育工作者的刚需工具。市面上的录屏软件要么功能单一,要么性能不佳,要么收费昂贵。作为C#开发者,为什么不自己打造一个功能强大、性能卓越的录屏神器呢?

本文将手把手带你用C#构建一个完整的屏幕录制应用,涵盖视频捕获、音频同步、多线程优化等核心技术。通过2000+行实战代码,你将掌握系统级编程的精髓,提升你的C#技术水平。

🔍 痛点分析:为什么要自己造轮子?

现有方案的局限性

性能瓶颈:大多数录屏工具在高帧率录制时会出现卡顿、掉帧问题,影响用户体验。

功能限制:无法满足特定需求,如自定义录制区域、多音频源混合、实时压缩等。

成本考虑:商业软件授权费用高昂,开源方案往往缺乏维护。

技术提升:对于C#开发者而言,这是一个绝佳的系统编程实战项目。

🎯 核心技术架构解析

多线程并发设计

C#
// 高精度帧捕获线程 private void CaptureScreenFrames(CancellationToken cancellationToken) { Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; while (_isRecording && !cancellationToken.IsCancellationRequested) { var currentTime = _stopwatch.ElapsedTicks; if (currentTime >= _nextFrameTime) { var bitmap = CaptureScreenOptimized(_captureArea); _frameQueue.Enqueue(new FrameData { Bitmap = bitmap, Index = _frameIndex++, Timestamp = DateTime.Now }); _nextFrameTime += _frameInterval; } else { // 精确等待,避免CPU空转 var waitMs = Math.Max(1, (_nextFrameTime - currentTime) / TimeSpan.TicksPerMillisecond); Thread.Sleep((int)waitMs); } } }

核心要点

  • 使用ThreadPriority.AboveNormal确保帧捕获优先级
  • 高精度计时器避免帧率不稳定
  • 并发队列(ConcurrentQueue)实现生产者-消费者模式

🚀 性能优化:屏幕捕获算法

C#
private Bitmap CaptureScreenOptimized(Rectangle area) { var bitmap = new Bitmap(area.Width, area.Height, PixelFormat.Format24bppRgb); using (var graphics = Graphics.FromImage(bitmap)) { // 关键优化设置 graphics.CompositingMode = CompositingMode.SourceCopy; graphics.CompositingQuality = CompositingQuality.HighSpeed; graphics.InterpolationMode = InterpolationMode.NearestNeighbor; graphics.SmoothingMode = SmoothingMode.None; graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed; graphics.CopyFromScreen(area.X, area.Y, 0, 0, area.Size, CopyPixelOperation.SourceCopy); } return bitmap; }

性能提升技巧

  1. 选择Format24bppRgb减少内存占用
  2. 关闭图形渲染质量提升速度
  3. 使用SourceCopy模式避免像素混合计算

🎵 音频同步录制:多源混音

C#
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.VisualBasic; using NAudio.Wave; namespace AppScreenRecorder { public class AudioRecorder : IDisposable { private WasapiLoopbackCapture _loopbackCapture; private WaveInEvent _micCapture; private string _systemAudioPath; private string _micAudioPath; private WaveFileWriter _audioWriter; private WaveFileWriter _micWriter; private bool _isRecording = false; private bool _isPaused = false; public string SystemAudioPath => _systemAudioPath; public string MicAudioPath => _micAudioPath; public void StartRecording(string basePath, bool recordMic = true, bool recordSystem = true) { _isRecording = true; _isPaused = false; if (recordSystem) { _systemAudioPath = Path.ChangeExtension(basePath, "_system.wav"); _loopbackCapture = new WasapiLoopbackCapture(); _loopbackCapture.DataAvailable += OnSystemAudioDataAvailable; _loopbackCapture.RecordingStopped += OnRecordingStopped; _audioWriter = new WaveFileWriter(_systemAudioPath, _loopbackCapture.WaveFormat); _loopbackCapture.StartRecording(); } if (recordMic) { _micAudioPath = Path.ChangeExtension(basePath, "_mic.wav"); _micCapture = new WaveInEvent(); _micCapture.WaveFormat = new WaveFormat(44100, 16, 2); _micCapture.DataAvailable += OnMicDataAvailable; _micWriter = new WaveFileWriter(_micAudioPath, _micCapture.WaveFormat); _micCapture.StartRecording(); } } public void StopRecording() { // 立即设置停止标志 _isRecording = false; _isPaused = false; try { // 立即停止录制设备 _loopbackCapture?.StopRecording(); _micCapture?.StopRecording(); // 立即刷新并关闭写入器 _audioWriter?.Flush(); _audioWriter?.Dispose(); _micWriter?.Flush(); _micWriter?.Dispose(); // 释放捕获设备 _loopbackCapture?.Dispose(); _micCapture?.Dispose(); _audioWriter = null; _micWriter = null; _loopbackCapture = null; _micCapture = null; } catch (Exception ex) { Console.WriteLine($"停止音频录制时出错: {ex.Message}"); } } public void PauseRecording() { _isPaused = true; } public void ResumeRecording() { _isPaused = false; } private void OnSystemAudioDataAvailable(object sender, WaveInEventArgs e) { if (_isRecording && !_isPaused && _audioWriter != null) { try { _audioWriter.Write(e.Buffer, 0, e.BytesRecorded); } catch (ObjectDisposedException) { } } } private void OnMicDataAvailable(object sender, WaveInEventArgs e) { if (_isRecording && !_isPaused && _micWriter != null) { try { _micWriter.Write(e.Buffer, 0, e.BytesRecorded); } catch (ObjectDisposedException) { } } } private void OnRecordingStopped(object sender, StoppedEventArgs e) { if (e.Exception != null) { Console.WriteLine($"音频录制出错: {e.Exception.Message}"); } } public void Dispose() { StopRecording(); } } }

关键技术点

  • WasapiLoopbackCapture实现系统音频捕获
  • 双音频流独立录制,后期FFmpeg合并
  • 异步写入避免音频丢失

🎬 视频编码:FFMpegCore集成

C#
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using FFMpegCore; using FFMpegCore.Enums; namespace AppScreenRecorder { public class ScreenRecorder : IDisposable { private bool _isRecording = false; private bool _isPaused = false; private string _outputPath; private Rectangle _captureArea; private string _tempDirectory; private int _frameRate = 30; private int _quality = 720; private string _tempVideoPath; // 使用并发队列来缓存帧 private readonly ConcurrentQueue<FrameData> _frameQueue = new ConcurrentQueue<FrameData>(); private Task _captureTask; private Task _saveTask; private CancellationTokenSource _cancellationTokenSource; // 高精度计时器 private System.Diagnostics.Stopwatch _stopwatch; private long _nextFrameTime; private long _frameInterval; private int _frameIndex = 0; public string TempVideoPath => _tempVideoPath; private struct FrameData { public Bitmap Bitmap; public int Index; public DateTime Timestamp; } public ScreenRecorder() { _tempDirectory = Path.Combine(Path.GetTempPath(), "ScreenRecorder_" + Guid.NewGuid().ToString("N")[..8]); Directory.CreateDirectory(_tempDirectory); } public async Task StartRecording(string outputPath, Rectangle? captureArea = null, int frameRate = 30, int quality = 720) { _outputPath = outputPath; _captureArea = captureArea ?? Screen.PrimaryScreen.Bounds; _frameRate = frameRate; _quality = quality; _isRecording = true; _isPaused = false; _frameIndex = 0; // 初始化高精度计时器 _stopwatch = System.Diagnostics.Stopwatch.StartNew(); _frameInterval = TimeSpan.TicksPerSecond / _frameRate; _nextFrameTime = _frameInterval; _cancellationTokenSource = new CancellationTokenSource(); // 启动捕获和保存任务 _captureTask = Task.Run(() => CaptureScreenFrames(_cancellationTokenSource.Token)); _saveTask = Task.Run(() => SaveFramesToDisk(_cancellationTokenSource.Token)); await Task.CompletedTask; } public async Task StopRecording() { // 立即停止录制标志 _isRecording = false; _isPaused = false; Console.WriteLine("开始停止录制..."); if (_cancellationTokenSource != null) { // 立即取消捕获任务 _cancellationTokenSource.Cancel(); try { // 等待捕获任务完成 if (_captureTask != null) { var timeoutTask = Task.Delay(3000); // 3秒超时 var completedTask = await Task.WhenAny(_captureTask, timeoutTask); if (completedTask == timeoutTask) { Console.WriteLine("警告: 捕获任务超时"); } else { Console.WriteLine("捕获任务已完成"); } } // 等待保存任务处理完所有帧 if (_saveTask != null) { Console.WriteLine($"等待保存任务完成,队列剩余: {_frameQueue.Count} 帧"); var timeoutTask = Task.Delay(10000); // 10秒超时,给保存任务更多时间 var completedTask = await Task.WhenAny(_saveTask, timeoutTask); if (completedTask == timeoutTask) { Console.WriteLine("警告: 保存任务超时"); // 即使超时也继续,清理剩余帧 while (_frameQueue.TryDequeue(out var frame)) { frame.Bitmap?.Dispose(); } } else { Console.WriteLine("保存任务已完成"); } } } catch (Exception ex) { Console.WriteLine($"停止录制任务时出错: {ex.Message}"); } } // 停止计时器 _stopwatch?.Stop(); // 此时所有帧应该都已保存完毕,生成视频 if (_frameIndex > 0) { Console.WriteLine($"开始生成视频,总帧数: {_frameIndex}"); var tempVideoPath = Path.Combine(_tempDirectory, "temp_video.mp4"); await GenerateVideoOnly(tempVideoPath); _tempVideoPath = tempVideoPath; Console.WriteLine("视频生成完成"); } } public Task PauseRecording() { _isPaused = true; return Task.CompletedTask; } public Task ResumeRecording() { _isPaused = false; // 重置计时器以避免时间跳跃 if (_stopwatch != null) { _stopwatch.Restart(); _nextFrameTime = _frameInterval; } return Task.CompletedTask; } private void CaptureScreenFrames(CancellationToken cancellationToken) { Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; Console.WriteLine("捕获任务开始"); while (_isRecording && !cancellationToken.IsCancellationRequested) { if (_isPaused) { // 暂停时重置计时器 _stopwatch.Restart(); _nextFrameTime = _frameInterval; Thread.Sleep(50); continue; } var currentTime = _stopwatch.ElapsedTicks; if (currentTime >= _nextFrameTime) { // 最后一次检查录制状态 if (!_isRecording) { break; } try { var bitmap = CaptureScreenOptimized(_captureArea); // 检查队列大小,避免内存溢出 if (_frameQueue.Count < 500) { _frameQueue.Enqueue(new FrameData { Bitmap = bitmap, Index = _frameIndex++, Timestamp = DateTime.Now }); } else { bitmap?.Dispose(); Console.WriteLine("队列满,丢弃一帧"); } _nextFrameTime += _frameInterval; if (currentTime > _nextFrameTime + _frameInterval * 2) { _nextFrameTime = currentTime + _frameInterval; } } catch (Exception ex) { Console.WriteLine($"捕获帧时出错: {ex.Message}"); _nextFrameTime += _frameInterval; } } else { // 精确等待 var waitTicks = _nextFrameTime - currentTime; var waitMs = Math.Max(1, Math.Min(waitTicks / TimeSpan.TicksPerMillisecond, 15)); Thread.Sleep((int)waitMs); } } Console.WriteLine($"捕获任务结束,总共捕获 {_frameIndex} 帧,队列剩余: {_frameQueue.Count}"); } private void SaveFramesToDisk(CancellationToken cancellationToken) { Thread.CurrentThread.Priority = ThreadPriority.Normal; var processedFrames = 0; Console.WriteLine("保存任务开始"); while (true) { bool hasMoreFrames = _frameQueue.TryDequeue(out var frameData); if (hasMoreFrames) { try { var frameFile = Path.Combine(_tempDirectory, $"frame_{frameData.Index:D6}.png"); using (frameData.Bitmap) { frameData.Bitmap.Save(frameFile, ImageFormat.Png); } processedFrames++; if (processedFrames % 100 == 0) { Console.WriteLine($"已保存 {processedFrames} 帧,队列剩余: {_frameQueue.Count}"); } } catch (Exception ex) { Console.WriteLine($"保存帧时出错: {ex.Message}"); frameData.Bitmap?.Dispose(); } } else { if (cancellationToken.IsCancellationRequested && !_isRecording) { break; } Thread.Sleep(10); } } Console.WriteLine($"保存任务结束,总共保存 {processedFrames} 帧"); } private Bitmap CaptureScreenOptimized(Rectangle area) { var bitmap = new Bitmap(area.Width, area.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb); using (var graphics = Graphics.FromImage(bitmap)) { graphics.CompositingMode = CompositingMode.SourceCopy; graphics.CompositingQuality = CompositingQuality.HighSpeed; graphics.InterpolationMode = InterpolationMode.NearestNeighbor; graphics.SmoothingMode = SmoothingMode.None; graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed; graphics.CopyFromScreen(area.X, area.Y, 0, 0, area.Size, CopyPixelOperation.SourceCopy); } return bitmap; } private async Task GenerateVideoOnly(string outputPath) { try { var inputPattern = Path.Combine(_tempDirectory, "frame_%06d.png"); await FFMpegArguments .FromFileInput(inputPattern, false, options => options .WithFramerate(_frameRate)) .OutputToFile(outputPath, true, options => options .WithVideoCodec(VideoCodec.LibX264) .WithCustomArgument("-preset ultrafast") .WithCustomArgument("-tune zerolatency") .WithVideoBitrate(GetBitrateForQuality(_quality)) .WithFramerate(_frameRate) .WithVideoFilters(filterOptions => filterOptions .Scale(GetVideoSize(_quality)))) .ProcessAsynchronously(); } catch (Exception ex) { throw new Exception($"视频生成失败: {ex.Message}", ex); } } public static async Task MergeVideoAndAudio(string videoPath, string audioPath, string outputPath) { try { await FFMpegArguments .FromFileInput(videoPath) .AddFileInput(audioPath) .OutputToFile(outputPath, true, options => options .WithCustomArgument("-c:v copy") .WithAudioCodec(AudioCodec.Aac) .WithAudioBitrate(128) .WithCustomArgument("-shortest")) .ProcessAsynchronously(); } catch (Exception ex) { throw new Exception($"视频音频合并失败: {ex.Message}", ex); } } public static async Task MergeVideoWithMultipleAudio(string videoPath, string systemAudioPath, string micAudioPath, string outputPath) { try { bool hasMicAudio = !string.IsNullOrEmpty(micAudioPath) && File.Exists(micAudioPath); if (hasMicAudio) { await FFMpegArguments .FromFileInput(videoPath) .AddFileInput(systemAudioPath) .AddFileInput(micAudioPath) .OutputToFile(outputPath, true, options => options .WithCustomArgument("-c:v copy") .WithAudioCodec(AudioCodec.Aac) .WithAudioBitrate(128) .WithCustomArgument("-filter_complex [1:a][2:a]amix=inputs=2:duration=shortest[a]") .WithCustomArgument("-map 0:v") .WithCustomArgument("-map [a]")) .ProcessAsynchronously(); } else { await MergeVideoAndAudio(videoPath, systemAudioPath, outputPath); } } catch (Exception ex) { throw new Exception($"视频多音频合并失败: {ex.Message}", ex); } } private int GetBitrateForQuality(int quality) { return quality switch { 1080 => 4000, 720 => 2000, 480 => 1000, _ => 2000 }; } private VideoSize GetVideoSize(int quality) { return quality switch { 1080 => VideoSize.FullHd, 720 => VideoSize.Hd, 480 => VideoSize.Ed, _ => VideoSize.Hd }; } private void CleanupTempFiles() { try { if (Directory.Exists(_tempDirectory)) { Directory.Delete(_tempDirectory, true); } } catch (Exception ex) { Console.WriteLine($"清理临时文件时出错: {ex.Message}"); } } public void Dispose() { if (_isRecording) { _isRecording = false; } _cancellationTokenSource?.Cancel(); while (_frameQueue.TryDequeue(out var frame)) { frame.Bitmap?.Dispose(); } _stopwatch?.Stop(); _cancellationTokenSource?.Dispose(); CleanupTempFiles(); } } }

编码优化策略

  • ultrafast预设平衡质量与速度
  • amix滤镜实现音频混合
  • 视频流复制(-c:v copy)避免重复编码

🎨 用户界面:WPF现代化设计

XML
<Window x:Class="AppScreenRecorder.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:AppScreenRecorder" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" KeyDown="MainWindow_KeyDown" Focusable="True"> <Window.Resources> <!-- 样式定义 --> <Style x:Key="ModernButtonStyle" TargetType="Button"> <Setter Property="Background" Value="#FF2196F3"/> <Setter Property="Foreground" Value="White"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Padding" Value="20,10"/> <Setter Property="Margin" Value="5"/> <Setter Property="Cursor" Value="Hand"/> <Setter Property="FontSize" Value="14"/> <Setter Property="FontWeight" Value="Medium"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Border Background="{TemplateBinding Background}" CornerRadius="5" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#FF1976D2"/> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Background" Value="#FF0D47A1"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="StopButtonStyle" TargetType="Button" BasedOn="{StaticResource ModernButtonStyle}"> <Setter Property="Background" Value="#FFF44336"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#FFD32F2F"/> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Background" Value="#FFB71C1C"/> </Trigger> </Style.Triggers> </Style> <Style x:Key="GroupBoxStyle" TargetType="GroupBox"> <Setter Property="BorderBrush" Value="#FFCCCCCC"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Margin" Value="10"/> <Setter Property="Padding" Value="10"/> <Setter Property="FontWeight" Value="Medium"/> </Style> <Style x:Key="StatusLabelStyle" TargetType="Label"> <Setter Property="FontSize" Value="16"/> <Setter Property="FontWeight" Value="Bold"/> <Setter Property="Margin" Value="10"/> </Style> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- 标题栏 --> <Border Grid.Row="0" Background="#FF2196F3" Padding="20,15"> <StackPanel Orientation="Horizontal"> <TextBlock Text="🎬" FontSize="24" Margin="0,0,10,0"/> <TextBlock Text="C# 录屏神器" FontSize="20" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/> <TextBlock Text="轻松实现视频音频同步录制" FontSize="12" Foreground="#FFBBDEFB" VerticalAlignment="Center" Margin="20,0,0,0"/> </StackPanel> </Border> <!-- 主要内容区域 --> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"> <StackPanel Margin="20"> <!-- 录制控制区域 --> <GroupBox Header="🎥 录制控制" Style="{StaticResource GroupBoxStyle}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="0,0,0,15"> <Button Name="RecordButton" Content="开始录制" Style="{StaticResource ModernButtonStyle}" Click="RecordButton_Click" MinWidth="120"/> <Button Name="StopButton" Content="停止录制" Style="{StaticResource StopButtonStyle}" Click="StopButton_Click" MinWidth="120" IsEnabled="False"/> <Button Name="PauseButton" Content="暂停录制" Style="{StaticResource ModernButtonStyle}" Click="PauseButton_Click" MinWidth="120" IsEnabled="False"/> </StackPanel> <Label Name="StatusLabel" Content="就绪" Grid.Row="1" Grid.Column="0" Style="{StaticResource StatusLabelStyle}" Foreground="#FF4CAF50"/> <StackPanel Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Orientation="Vertical"> <TextBlock Text="录制时长:" FontWeight="Medium" Margin="0,0,0,5"/> <Label Name="RecordTimeLabel" Content="00:00:00" FontSize="18" FontFamily="Consolas" Background="#FFF5F5F5" Padding="10,5" HorizontalContentAlignment="Center"/> </StackPanel> </Grid> </GroupBox> <!-- 录制设置区域 --> <GroupBox Header="⚙️ 录制设置" Style="{StaticResource GroupBoxStyle}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- 视频设置 --> <TextBlock Text="视频质量:" Grid.Row="0" Grid.Column="0" Margin="0,0,0,5" FontWeight="Medium"/> <ComboBox Name="VideoQualityComboBox" Grid.Row="0" Grid.Column="1" Margin="10,0,0,10"> <ComboBoxItem Content="高质量 (1080p)" IsSelected="True"/> <ComboBoxItem Content="标准质量 (720p)"/> <ComboBoxItem Content="低质量 (480p)"/> </ComboBox> <TextBlock Text="帧率 (FPS):" Grid.Row="1" Grid.Column="0" Margin="0,0,0,5" FontWeight="Medium"/> <ComboBox Name="FrameRateComboBox" Grid.Row="1" Grid.Column="1" Margin="10,0,0,10"> <ComboBoxItem Content="60 FPS"/> <ComboBoxItem Content="30 FPS" IsSelected="True"/> <ComboBoxItem Content="24 FPS"/> <ComboBoxItem Content="15 FPS"/> </ComboBox> <!-- 音频设置 --> <CheckBox Name="RecordSystemAudioCheckBox" Content="录制系统音频" Grid.Row="2" Grid.Column="0" IsChecked="True" Margin="0,10"/> <CheckBox Name="RecordMicAudioCheckBox" Content="录制麦克风" Grid.Row="2" Grid.Column="1" IsChecked="True" Margin="10,10,0,10"/> <!-- 录制区域 --> <TextBlock Text="录制区域:" Grid.Row="3" Grid.Column="0" Margin="0,0,0,5" FontWeight="Medium"/> <ComboBox Name="RecordAreaComboBox" Grid.Row="3" Grid.Column="1" Margin="10,0,0,10"> <ComboBoxItem Content="全屏录制" IsSelected="True"/> <ComboBoxItem Content="选择窗口"/> <ComboBoxItem Content="自定义区域"/> </ComboBox> <!-- 输出路径 --> <TextBlock Text="保存路径:" Grid.Row="4" Grid.Column="0" Margin="0,10,0,5" FontWeight="Medium"/> <StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" Margin="10,10,0,0"> <TextBox Name="OutputPathTextBox" Width="200" Margin="0,0,10,0" Text="C:\录屏文件\录屏_" IsReadOnly="True"/> <Button Content="浏览..." Click="BrowseButton_Click" Style="{StaticResource ModernButtonStyle}" Padding="10,5"/> </StackPanel> </Grid> </GroupBox> <!-- 快捷键设置 --> <GroupBox Header="⌨️ 快捷键" Style="{StaticResource GroupBoxStyle}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Text="开始/停止录制:" Grid.Row="0" Grid.Column="0" Margin="0,0,20,5" FontWeight="Medium"/> <TextBlock Text="F9" Grid.Row="0" Grid.Column="1" FontFamily="Consolas" Background="#FFF0F0F0" Padding="5,2"/> <TextBlock Text="暂停/恢复:" Grid.Row="1" Grid.Column="0" Margin="0,0,20,5" FontWeight="Medium"/> <TextBlock Text="F10" Grid.Row="1" Grid.Column="1" FontFamily="Consolas" Background="#FFF0F0F0" Padding="5,2"/> <TextBlock Text="截图:" Grid.Row="2" Grid.Column="0" Margin="0,0,20,5" FontWeight="Medium"/> <TextBlock Text="F11" Grid.Row="2" Grid.Column="1" FontFamily="Consolas" Background="#FFF0F0F0" Padding="5,2"/> </Grid> </GroupBox> </StackPanel> </ScrollViewer> <!-- 底部状态栏 --> <Border Grid.Row="2" Background="#FFF5F5F5" BorderBrush="#FFDDDDDD" BorderThickness="0,1,0,0"> <Grid Margin="20,10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0" Orientation="Horizontal"> <TextBlock Text="状态:" Margin="0,0,10,0" VerticalAlignment="Center"/> <Ellipse Name="StatusIndicator" Width="12" Height="12" Fill="#FF4CAF50" Margin="0,0,10,0" VerticalAlignment="Center"/> <TextBlock Name="DetailStatusLabel" Text="系统就绪" VerticalAlignment="Center"/> </StackPanel> <StackPanel Grid.Column="1" Orientation="Horizontal"> <Button Content="📁 打开输出文件夹" Click="OpenOutputFolderButton_Click" Style="{StaticResource ModernButtonStyle}" Padding="10,5" Margin="0,0,10,0"/> </StackPanel> </Grid> </Border> </Grid> </Window>
C#
using System; using System.IO; using System.Windows; using System.Windows.Threading; using System.Threading.Tasks; using Microsoft.Win32; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; using MessageBox = System.Windows.MessageBox; using System.Windows.Input; using KeyEventArgs = System.Windows.Input.KeyEventArgs; namespace AppScreenRecorder { public partial class MainWindow : Window { private ScreenRecorder _screenRecorder; private AudioRecorder _audioRecorder; private DispatcherTimer _recordingTimer; private DateTime _startTime; private bool _isRecording = false; private bool _isPaused = false; public MainWindow() { InitializeComponent(); InitializeTimer(); SetInitialState(); this.Focusable = true; this.Focus(); } private void InitializeTimer() { _recordingTimer = new DispatcherTimer(); _recordingTimer.Interval = TimeSpan.FromSeconds(1); _recordingTimer.Tick += UpdateRecordingTime; } private void SetInitialState() { RecordButton.IsEnabled = true; StopButton.IsEnabled = false; PauseButton.IsEnabled = false; StatusLabel.Content = "就绪"; StatusLabel.Foreground = System.Windows.Media.Brushes.Green; RecordTimeLabel.Content = "00:00:00"; StatusIndicator.Fill = System.Windows.Media.Brushes.Green; DetailStatusLabel.Text = "系统就绪"; // 设置默认输出路径 string defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "录屏文件"); if (!Directory.Exists(defaultPath)) { Directory.CreateDirectory(defaultPath); } OutputPathTextBox.Text = Path.Combine(defaultPath, "录屏_"); } private async void RecordButton_Click(object sender, RoutedEventArgs e) { try { if (!_isRecording) { await StartRecording(); } } catch (Exception ex) { MessageBox.Show($"开始录制时出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); SetInitialState(); } } private async void StopButton_Click(object sender, RoutedEventArgs e) { try { await StopRecording(); } catch (Exception ex) { MessageBox.Show($"停止录制时出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } private void PauseButton_Click(object sender, RoutedEventArgs e) { try { if (_isPaused) { ResumeRecording(); } else { PauseRecording(); } } catch (Exception ex) { MessageBox.Show($"暂停/恢复录制时出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } private async Task StartRecording() { // 生成输出文件路径 string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string finalOutputPath = OutputPathTextBox.Text + timestamp + ".mp4"; string audioBasePath = OutputPathTextBox.Text + timestamp; // 获取录制设置 var quality = GetSelectedQuality(); var frameRate = GetSelectedFrameRate(); var recordArea = GetRecordArea(); // 更新UI状态 _isRecording = true; _isPaused = false; _startTime = DateTime.Now; RecordButton.IsEnabled = false; StopButton.IsEnabled = true; PauseButton.IsEnabled = true; PauseButton.Content = "暂停录制"; StatusLabel.Content = "录制中..."; StatusLabel.Foreground = System.Windows.Media.Brushes.Red; StatusIndicator.Fill = System.Windows.Media.Brushes.Red; DetailStatusLabel.Text = "正在录制屏幕..."; // 启动计时器 _recordingTimer.Start(); try { // 初始化录制器 _screenRecorder = new ScreenRecorder(); _audioRecorder = new AudioRecorder(); // 启动音频录制(如果需要) bool recordMic = RecordMicAudioCheckBox.IsChecked ?? false; bool recordSystem = RecordSystemAudioCheckBox.IsChecked ?? false; if (recordMic || recordSystem) { _audioRecorder.StartRecording(audioBasePath, recordMic, recordSystem); } // 启动屏幕录制 await _screenRecorder.StartRecording(finalOutputPath, recordArea, frameRate, quality); } catch (Exception ex) { MessageBox.Show($"录制失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); await StopRecording(); throw; } } private async Task StopRecording() { if (!_isRecording) return; _isRecording = false; _recordingTimer.Stop(); // 更新UI状态 StatusLabel.Content = "正在停止录制..."; StatusLabel.Foreground = System.Windows.Media.Brushes.Orange; StatusIndicator.Fill = System.Windows.Media.Brushes.Orange; DetailStatusLabel.Text = "正在停止录制,请稍候..."; try { string tempVideoPath = null; string systemAudioPath = null; string micAudioPath = null; // 1. 立即停止屏幕录制 if (_screenRecorder != null) { await _screenRecorder.StopRecording(); tempVideoPath = _screenRecorder.TempVideoPath; } // 2. 立即停止音频录制 if (_audioRecorder != null) { _audioRecorder.StopRecording(); systemAudioPath = _audioRecorder.SystemAudioPath; micAudioPath = _audioRecorder.MicAudioPath; } // 3. 减少等待时间,只等待文件系统同步 await Task.Delay(200); // 更新UI状态 StatusLabel.Content = "处理中..."; DetailStatusLabel.Text = "正在合并视频和音频..."; // 4. 生成最终输出路径 string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string finalOutputPath = OutputPathTextBox.Text + timestamp + ".mp4"; // 5. 合并视频和音频 await MergeVideoAndAudio(tempVideoPath, systemAudioPath, micAudioPath, finalOutputPath); // 6. 清理资源 _screenRecorder?.Dispose(); _audioRecorder?.Dispose(); _screenRecorder = null; _audioRecorder = null; // 恢复UI状态 SetInitialState(); MessageBox.Show($"录制完成!\n文件保存至:{finalOutputPath}", "成功", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { _screenRecorder?.Dispose(); _audioRecorder?.Dispose(); _screenRecorder = null; _audioRecorder = null; MessageBox.Show($"停止录制时出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); SetInitialState(); } } private async Task MergeVideoAndAudio(string videoPath, string systemAudioPath, string micAudioPath, string outputPath) { try { if (string.IsNullOrEmpty(videoPath) || !File.Exists(videoPath)) { throw new Exception("视频文件不存在"); } if (string.IsNullOrEmpty(systemAudioPath) && string.IsNullOrEmpty(micAudioPath)) { File.Copy(videoPath, outputPath, true); return; } if (!string.IsNullOrEmpty(systemAudioPath) && File.Exists(systemAudioPath) && !string.IsNullOrEmpty(micAudioPath) && File.Exists(micAudioPath)) { await ScreenRecorder.MergeVideoWithMultipleAudio(videoPath, systemAudioPath, micAudioPath, outputPath); } else if (!string.IsNullOrEmpty(systemAudioPath) && File.Exists(systemAudioPath)) { await ScreenRecorder.MergeVideoAndAudio(videoPath, systemAudioPath, outputPath); } else if (!string.IsNullOrEmpty(micAudioPath) && File.Exists(micAudioPath)) { await ScreenRecorder.MergeVideoAndAudio(videoPath, micAudioPath, outputPath); } else { File.Copy(videoPath, outputPath, true); } CleanupTempFiles(videoPath, systemAudioPath, micAudioPath); } catch (Exception ex) { throw new Exception($"合并视频音频失败: {ex.Message}", ex); } } // 清理临时文件的方法 private void CleanupTempFiles(params string[] filePaths) { foreach (var filePath in filePaths) { if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) { try { File.Delete(filePath); } catch (Exception ex) { Console.WriteLine($"删除临时文件失败: {ex.Message}"); } } } } private void PauseRecording() { if (!_isRecording) return; _isPaused = true; _recordingTimer.Stop(); _screenRecorder?.PauseRecording(); _audioRecorder?.PauseRecording(); PauseButton.Content = "恢复录制"; StatusLabel.Content = "已暂停"; StatusLabel.Foreground = System.Windows.Media.Brushes.Orange; StatusIndicator.Fill = System.Windows.Media.Brushes.Orange; DetailStatusLabel.Text = "录制已暂停"; } private void ResumeRecording() { if (!_isRecording) return; _isPaused = false; _recordingTimer.Start(); _screenRecorder?.ResumeRecording(); _audioRecorder?.ResumeRecording(); PauseButton.Content = "暂停录制"; StatusLabel.Content = "录制中..."; StatusLabel.Foreground = System.Windows.Media.Brushes.Red; StatusIndicator.Fill = System.Windows.Media.Brushes.Red; DetailStatusLabel.Text = "正在录制屏幕..."; } private void UpdateRecordingTime(object sender, EventArgs e) { if (_isRecording && !_isPaused) { TimeSpan elapsed = DateTime.Now - _startTime; RecordTimeLabel.Content = elapsed.ToString(@"hh\:mm\:ss"); } } private int GetSelectedQuality() { var selectedItem = VideoQualityComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem; return selectedItem?.Content.ToString() switch { "高质量 (1080p)" => 1080, "标准质量 (720p)" => 720, "低质量 (480p)" => 480, _ => 720 }; } private int GetSelectedFrameRate() { var selectedItem = FrameRateComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem; return selectedItem?.Content.ToString() switch { "60 FPS" => 60, "30 FPS" => 30, "24 FPS" => 24, "15 FPS" => 15, _ => 30 }; } private Rectangle? GetRecordArea() { var selectedItem = RecordAreaComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem; return selectedItem?.Content.ToString() switch { "全屏录制" => Screen.PrimaryScreen.Bounds, "选择窗口" => null, // 这里可以添加窗口选择逻辑 "自定义区域" => null, // 这里可以添加区域选择逻辑 _ => Screen.PrimaryScreen.Bounds }; } private void BrowseButton_Click(object sender, RoutedEventArgs e) { var dialog = new System.Windows.Forms.FolderBrowserDialog(); dialog.Description = "选择录屏文件保存路径"; if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { OutputPathTextBox.Text = Path.Combine(dialog.SelectedPath, "录屏_"); } } private void OpenOutputFolderButton_Click(object sender, RoutedEventArgs e) { try { string folderPath = Path.GetDirectoryName(OutputPathTextBox.Text); if (Directory.Exists(folderPath)) { Process.Start("explorer.exe", folderPath); } else { MessageBox.Show("输出文件夹不存在!", "错误", MessageBoxButton.OK, MessageBoxImage.Warning); } } catch (Exception ex) { MessageBox.Show($"打开文件夹失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } private async void MainWindow_KeyDown(object sender, KeyEventArgs e) { try { switch (e.Key) { case Key.F9: await HandleF9KeyPress(); e.Handled = true; break; case Key.F10: HandleF10KeyPress(); e.Handled = true; break; case Key.F11: HandleF11KeyPress(); e.Handled = true; break; } } catch (Exception ex) { MessageBox.Show($"快捷键操作出错:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } // F9 - 开始/停止录制 private async Task HandleF9KeyPress() { if (_isRecording) { await StopRecording(); } else { await StartRecording(); } } // F10 - 暂停/恢复录制 private void HandleF10KeyPress() { if (!_isRecording) { MessageBox.Show("请先开始录制后再使用暂停功能", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (_isPaused) { ResumeRecording(); } else { PauseRecording(); } } // F11 - 截图功能 private void HandleF11KeyPress() { try { TakeScreenshot(); } catch (Exception ex) { MessageBox.Show($"截图失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } // 截图功能实现 private void TakeScreenshot() { try { var bounds = Screen.PrimaryScreen.Bounds; using (var bitmap = new Bitmap(bounds.Width, bounds.Height)) { using (var graphics = System.Drawing.Graphics.FromImage(bitmap)) { graphics.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size); } // 生成截图文件名 string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string screenshotPath = Path.Combine( Path.GetDirectoryName(OutputPathTextBox.Text), $"截图_{timestamp}.png"); // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(screenshotPath)); // 保存截图 bitmap.Save(screenshotPath, System.Drawing.Imaging.ImageFormat.Png); // 显示成功消息 MessageBox.Show($"截图已保存:\n{screenshotPath}", "截图成功", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (Exception ex) { throw new Exception($"截图操作失败:{ex.Message}"); } } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { this.Focus(); } private void MainWindow_Activated(object sender, EventArgs e) { this.Focus(); } protected override void OnClosed(EventArgs e) { // 确保在关闭窗口时停止录制 if (_isRecording) { _screenRecorder?.Dispose(); _audioRecorder?.Dispose(); } _recordingTimer?.Stop(); base.OnClosed(e); } } }

image.png

UI设计亮点

  • 全键盘快捷键支持,提升操作效率
  • 实时状态反馈,用户体验友好
  • 响应式布局适配不同屏幕尺寸

⚠️ 关键问题与解决方案

内存泄漏防护

C#
public void Dispose() { if (_isRecording) { _isRecording = false; } _cancellationTokenSource?.Cancel(); // 清理帧缓存 while (_frameQueue.TryDequeue(out var frame)) { frame.Bitmap?.Dispose(); } _stopwatch?.Stop(); _cancellationTokenSource?.Dispose(); CleanupTempFiles(); }

线程安全保障

C#
// 使用并发集合确保线程安全 private readonly ConcurrentQueue<FrameData> _frameQueue = new ConcurrentQueue<FrameData>(); // 取消令牌统一管理 private CancellationTokenSource _cancellationTokenSource;

异常处理机制

C#
private async Task StopRecording() { try { // 设置超时机制,避免无限等待 var timeoutTask = Task.Delay(3000); var completedTask = await Task.WhenAny(_captureTask, timeoutTask); if (completedTask == timeoutTask) { Console.WriteLine("警告: 捕获任务超时"); } } catch (Exception ex) { Console.WriteLine($"停止录制时出错: {ex.Message}"); } }

🔧 部署与扩展

NuGet包依赖

XML
<PackageReference Include="FFMpegCore" Version="4.8.0" /> <PackageReference Include="NAudio" Version="2.1.0" /> <PackageReference Include="System.Drawing.Common" Version="7.0.0" />

性能监控

C#
// 队列监控 if (_frameQueue.Count > 500) { bitmap?.Dispose(); Console.WriteLine("队列满,丢弃一帧"); } // 处理进度反馈 if (processedFrames % 100 == 0) { Console.WriteLine($"已保存 {processedFrames} 帧,队列剩余: {_frameQueue.Count}"); }

💡 核心收获总结

通过这个屏幕录制器项目,你掌握了:

系统级编程:Windows API调用、GDI+图形处理、音频设备访问等底层技术

多线程并发:生产者-消费者模式、线程优先级管理、并发集合使用

性能优化:内存管理、CPU密集型任务优化、I/O操作异步化

这不仅仅是一个录屏工具,更是一个完整的C#系统编程实战案例。代码中的设计模式、性能优化技巧、异常处理机制都可以应用到其他项目中。


你在C#项目中遇到过哪些性能瓶颈?在多媒体处理方面有什么经验分享? 欢迎在评论区交流讨论!

如果这篇文章对你的C#学习有帮助,记得收藏转发给更多需要的同行!我们下期见! 🚀

相关信息

通过网盘分享的文件:AppScreenRecorder.zip 链接: https://pan.baidu.com/s/1EuhSA9lX7lYGLc_k6d8Yww?pwd=wed9 提取码: wed9 --来自百度网盘超级会员v9的分享

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!