從零到一:C#實現(xiàn)高性能屏幕錄制器完全指南
數(shù)字化辦公時代,屏幕錄制已成為開發(fā)者、產(chǎn)品經(jīng)理、教育工作者的剛需工具。市面上的錄屏軟件要么功能單一,要么性能不佳,要么收費昂貴。作為C#開發(fā)者,為什么不自己打造一個功能強大、性能卓越的錄屏神器呢?
本文將手把手帶你用C#構(gòu)建一個完整的屏幕錄制應(yīng)用,涵蓋視頻捕獲、音頻同步、多線程優(yōu)化等核心技術(shù)。通過2000+行實戰(zhàn)代碼,你將掌握系統(tǒng)級編程的精髓,提升你的C#技術(shù)水平。
痛點分析:為什么要自己造輪子?
現(xiàn)有方案的局限性
性能瓶頸:大多數(shù)錄屏工具在高幀率錄制時會出現(xiàn)卡頓、掉幀問題,影響用戶體驗。
功能限制:無法滿足特定需求,如自定義錄制區(qū)域、多音頻源混合、實時壓縮等。
成本考慮:商業(yè)軟件授權(quán)費用高昂,開源方案往往缺乏維護。
技術(shù)提升:對于C#開發(fā)者而言,這是一個絕佳的系統(tǒng)編程實戰(zhàn)項目。
核心技術(shù)架構(gòu)解析
多線程并發(fā)設(shè)計
// 高精度幀捕獲線程
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空轉(zhuǎn)
var waitMs = Math.Max(1, (_nextFrameTime - currentTime) / TimeSpan.TicksPerMillisecond);
Thread.Sleep((int)waitMs);
}
}
}核心要點:
- 使用
ThreadPriority.AboveNormal確保幀捕獲優(yōu)先級 - 高精度計時器避免幀率不穩(wěn)定
- 并發(fā)隊列(
ConcurrentQueue)實現(xiàn)生產(chǎn)者-消費者模式
?? 性能優(yōu)化:屏幕捕獲算法
private Bitmap CaptureScreenOptimized(Rectangle area)
{
var bitmap = new Bitmap(area.Width, area.Height, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(bitmap))
{
// 關(guān)鍵優(yōu)化設(shè)置
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;
}性能提升技巧:
- 選擇
Format24bppRgb減少內(nèi)存占用 - 關(guān)閉圖形渲染質(zhì)量提升速度
- 使用
SourceCopy模式避免像素混合計算
音頻同步錄制:多源混音
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
{
publicclass AudioRecorder : IDisposable
{
private WasapiLoopbackCapture _loopbackCapture;
private WaveInEvent _micCapture;
privatestring _systemAudioPath;
privatestring _micAudioPath;
private WaveFileWriter _audioWriter;
private WaveFileWriter _micWriter;
privatebool _isRecording = false;
privatebool _isPaused = false;
publicstring SystemAudioPath => _systemAudioPath;
publicstring 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()
{
// 立即設(shè)置停止標(biāo)志
_isRecording = false;
_isPaused = false;
try
{
// 立即停止錄制設(shè)備
_loopbackCapture?.StopRecording();
_micCapture?.StopRecording();
// 立即刷新并關(guān)閉寫入器
_audioWriter?.Flush();
_audioWriter?.Dispose();
_micWriter?.Flush();
_micWriter?.Dispose();
// 釋放捕獲設(shè)備
_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();
}
}
}關(guān)鍵技術(shù)點:
WasapiLoopbackCapture實現(xiàn)系統(tǒng)音頻捕獲- 雙音頻流獨立錄制,后期FFmpeg合并
- 異步寫入避免音頻丟失
視頻編碼:FFMpegCore集成
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
{
publicclass ScreenRecorder : IDisposable
{
privatebool _isRecording = false;
privatebool _isPaused = false;
privatestring _outputPath;
private Rectangle _captureArea;
privatestring _tempDirectory;
privateint _frameRate = 30;
privateint _quality = 720;
privatestring _tempVideoPath;
// 使用并發(fā)隊列來緩存幀
private readonly ConcurrentQueue<FrameData> _frameQueue = new ConcurrentQueue<FrameData>();
private Task _captureTask;
private Task _saveTask;
private CancellationTokenSource _cancellationTokenSource;
// 高精度計時器
private System.Diagnostics.Stopwatch _stopwatch;
privatelong _nextFrameTime;
privatelong _frameInterval;
privateint _frameIndex = 0;
publicstring TempVideoPath => _tempVideoPath;
privatestruct FrameData
{
public Bitmap Bitmap;
publicint 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();
// 啟動捕獲和保存任務(wù)
_captureTask = Task.Run(() => CaptureScreenFrames(_cancellationTokenSource.Token));
_saveTask = Task.Run(() => SaveFramesToDisk(_cancellationTokenSource.Token));
await Task.CompletedTask;
}
public async Task StopRecording()
{
// 立即停止錄制標(biāo)志
_isRecording = false;
_isPaused = false;
Console.WriteLine("開始停止錄制...");
if (_cancellationTokenSource != null)
{
// 立即取消捕獲任務(wù)
_cancellationTokenSource.Cancel();
try
{
// 等待捕獲任務(wù)完成
if (_captureTask != null)
{
var timeoutTask = Task.Delay(3000); // 3秒超時
var completedTask = await Task.WhenAny(_captureTask, timeoutTask);
if (completedTask == timeoutTask)
{
Console.WriteLine("警告: 捕獲任務(wù)超時");
}
else
{
Console.WriteLine("捕獲任務(wù)已完成");
}
}
// 等待保存任務(wù)處理完所有幀
if (_saveTask != null)
{
Console.WriteLine($"等待保存任務(wù)完成,隊列剩余: {_frameQueue.Count} 幀");
var timeoutTask = Task.Delay(10000); // 10秒超時,給保存任務(wù)更多時間
var completedTask = await Task.WhenAny(_saveTask, timeoutTask);
if (completedTask == timeoutTask)
{
Console.WriteLine("警告: 保存任務(wù)超時");
// 即使超時也繼續(xù),清理剩余幀
while (_frameQueue.TryDequeue(out var frame))
{
frame.Bitmap?.Dispose();
}
}
else
{
Console.WriteLine("保存任務(wù)已完成");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"停止錄制任務(wù)時出錯: {ex.Message}");
}
}
// 停止計時器
_stopwatch?.Stop();
// 此時所有幀應(yīng)該都已保存完畢,生成視頻
if (_frameIndex > 0)
{
Console.WriteLine($"開始生成視頻,總幀數(shù): {_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("捕獲任務(wù)開始");
while (_isRecording && !cancellationToken.IsCancellationRequested)
{
if (_isPaused)
{
// 暫停時重置計時器
_stopwatch.Restart();
_nextFrameTime = _frameInterval;
Thread.Sleep(50);
continue;
}
var currentTime = _stopwatch.ElapsedTicks;
if (currentTime >= _nextFrameTime)
{
// 最后一次檢查錄制狀態(tài)
if (!_isRecording)
{
break;
}
try
{
var bitmap = CaptureScreenOptimized(_captureArea);
// 檢查隊列大小,避免內(nèi)存溢出
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($"捕獲任務(wù)結(jié)束,總共捕獲 {_frameIndex} 幀,隊列剩余: {_frameQueue.Count}");
}
private void SaveFramesToDisk(CancellationToken cancellationToken)
{
Thread.CurrentThread.Priority = ThreadPriority.Normal;
var processedFrames = 0;
Console.WriteLine("保存任務(wù)開始");
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($"保存任務(wù)結(jié)束,總共保存 {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)
{
thrownew 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)
{
thrownew 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)
{
thrownew 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();
}
}
}編碼優(yōu)化策略:
ultrafast預(yù)設(shè)平衡質(zhì)量與速度amix濾鏡實現(xiàn)音頻混合- 視頻流復(fù)制(
-c:v copy)避免重復(fù)編碼
用戶界面:WPF現(xiàn)代化設(shè)計
<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>
<!-- 標(biāo)題欄 -->
<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="輕松實現(xiàn)視頻音頻同步錄制" FontSize="12" Foreground="#FFBBDEFB" VerticalAlignment="Center" Margin="20,0,0,0"/>
</StackPanel>
</Border>
<!-- 主要內(nèi)容區(qū)域 -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="20">
<!-- 錄制控制區(qū)域 -->
<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>
<!-- 錄制設(shè)置區(qū)域 -->
<GroupBox Header="?? 錄制設(shè)置" 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>
<!-- 視頻設(shè)置 -->
<TextBlock Text="視頻質(zhì)量:" 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="高質(zhì)量 (1080p)" IsSelected="True"/>
<ComboBoxItem Content="標(biāo)準(zhǔn)質(zhì)量 (720p)"/>
<ComboBoxItem Content="低質(zhì)量 (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>
<!-- 音頻設(shè)置 -->
<CheckBox Name="RecordSystemAudioCheckBox" Content="錄制系統(tǒng)音頻"
Grid.Row="2" Grid.Column="0" IsChecked="True" Margin="0,10"/>
<CheckBox Name="RecordMicAudioCheckBox" Content="錄制麥克風(fēng)"
Grid.Row="2" Grid.Column="1" IsChecked="True" Margin="10,10,0,10"/>
<!-- 錄制區(qū)域 -->
<TextBlock Text="錄制區(qū)域:" 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="自定義區(qū)域"/>
</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>
<!-- 快捷鍵設(shè)置 -->
<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="暫停/恢復(fù):" 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>
<!-- 底部狀態(tài)欄 -->
<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="狀態(tài):" 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="系統(tǒng)就緒" 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>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;
privatebool _isRecording = false;
privatebool _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 = "系統(tǒng)就緒";
// 設(shè)置默認輸出路徑
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($"暫停/恢復(fù)錄制時出錯:{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;
// 獲取錄制設(shè)置
var quality = GetSelectedQuality();
var frameRate = GetSelectedFrameRate();
var recordArea = GetRecordArea();
// 更新UI狀態(tài)
_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狀態(tài)
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. 減少等待時間,只等待文件系統(tǒng)同步
await Task.Delay(200);
// 更新UI狀態(tài)
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;
// 恢復(fù)UI狀態(tài)
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))
{
thrownew 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);
}
elseif (!string.IsNullOrEmpty(systemAudioPath) && File.Exists(systemAudioPath))
{
await ScreenRecorder.MergeVideoAndAudio(videoPath, systemAudioPath, outputPath);
}
elseif (!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)
{
thrownew 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 = "恢復(fù)錄制";
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
{
"高質(zhì)量 (1080p)" => 1080,
"標(biāo)準(zhǔn)質(zhì)量 (720p)" => 720,
"低質(zhì)量 (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, // 這里可以添加窗口選擇邏輯
"自定義區(qū)域" => null, // 這里可以添加區(qū)域選擇邏輯
_ => 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 - 暫停/恢復(fù)錄制
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);
}
}
// 截圖功能實現(xiàn)
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)
{
thrownew 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)
{
// 確保在關(guān)閉窗口時停止錄制
if (_isRecording)
{
_screenRecorder?.Dispose();
_audioRecorder?.Dispose();
}
_recordingTimer?.Stop();
base.OnClosed(e);
}
}
}
圖片
UI設(shè)計亮點:
- 全鍵盤快捷鍵支持,提升操作效率
- 實時狀態(tài)反饋,用戶體驗友好
- 響應(yīng)式布局適配不同屏幕尺寸
關(guān)鍵問題與解決方案
內(nèi)存泄漏防護
public void Dispose()
{
if (_isRecording)
{
_isRecording = false;
}
_cancellationTokenSource?.Cancel();
// 清理幀緩存
while (_frameQueue.TryDequeue(out var frame))
{
frame.Bitmap?.Dispose();
}
_stopwatch?.Stop();
_cancellationTokenSource?.Dispose();
CleanupTempFiles();
}線程安全保障
// 使用并發(fā)集合確保線程安全
private readonly ConcurrentQueue<FrameData> _frameQueue = new ConcurrentQueue<FrameData>();
// 取消令牌統(tǒng)一管理
private CancellationTokenSource _cancellationTokenSource;異常處理機制
private async Task StopRecording()
{
try
{
// 設(shè)置超時機制,避免無限等待
var timeoutTask = Task.Delay(3000);
var completedTask = await Task.WhenAny(_captureTask, timeoutTask);
if (completedTask == timeoutTask)
{
Console.WriteLine("警告: 捕獲任務(wù)超時");
}
}
catch (Exception ex)
{
Console.WriteLine($"停止錄制時出錯: {ex.Message}");
}
}部署與擴展
NuGet包依賴
<PackageReference Include="FFMpegCore" Version="4.8.0" />
<PackageReference Include="NAudio" Version="2.1.0" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />性能監(jiān)控
// 隊列監(jiān)控
if (_frameQueue.Count > 500)
{
bitmap?.Dispose();
Console.WriteLine("隊列滿,丟棄一幀");
}
// 處理進度反饋
if (processedFrames % 100 == 0)
{
Console.WriteLine($"已保存 {processedFrames} 幀,隊列剩余: {_frameQueue.Count}");
}核心收獲總結(jié)
通過這個屏幕錄制器項目,你掌握了:
系統(tǒng)級編程:Windows API調(diào)用、GDI+圖形處理、音頻設(shè)備訪問等底層技術(shù)
多線程并發(fā):生產(chǎn)者-消費者模式、線程優(yōu)先級管理、并發(fā)集合使用
性能優(yōu)化:內(nèi)存管理、CPU密集型任務(wù)優(yōu)化、I/O操作異步化
這不僅僅是一個錄屏工具,更是一個完整的C#系統(tǒng)編程實戰(zhàn)案例。代碼中的設(shè)計模式、性能優(yōu)化技巧、異常處理機制都可以應(yīng)用到其他項目中。

































