在数字化时代,屏幕录制工具已成为教育培训、软件演示和内容创作的必备工具。本文将详细讲解如何使用C#开发一款功能完善的屏幕录制工具,从界面设计到核心功能实现,一步步带您构建自己的屏幕捕获应用。
我们将开发一个Windows窗体应用程序,具备以下核心功能:
开发此应用需要以下环境和工具:
首先,创建一个Windows窗体应用程序,并通过NuGet包管理器安装SharpAvi库:
PowerShellInstall-Package SharpAvi
一个直观的用户界面对于屏幕录制工具至关重要。我们的界面包含以下关键元素:
界面初始化代码如下:
C#private void Form1_Load(object sender, EventArgs e)
{
// 初始化录制区域下拉菜单
cmbRecordingArea.Items.Clear();
cmbRecordingArea.Items.Add("全屏");
cmbRecordingArea.Items.Add("当前窗口");
cmbRecordingArea.SelectedIndex = 0;
// 初始化状态标签
lblStatus.Text = "准备就绪";
// 初始化帧率
numFrameRate.Value = 30;
// 默认设置
chkRecordMouse.Checked = true;
}
C#using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using SharpAvi;
using SharpAvi.Output;
using SharpAvi.Codecs;
namespace AppAvi
{
public partial class Form1 : Form
{
private bool _isRecording = false; // 录制状态标志
private string _outputPath; // 视频输出路径
private Task _recordingTask; // 录制任务
private CancellationTokenSource _cancellationTokenSource; // 取消令牌源
private bool _processingComplete = false; // 处理完成标志
/// <summary>
/// 窗口矩形结构体
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
/// <summary>
/// 导入获取窗口矩形的Windows API
/// </summary>
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
/// <summary>
/// 构造函数
/// </summary>
public Form1()
{
InitializeComponent();
}
/// <summary>
/// 开始录制按钮点击事件处理
/// </summary>
private void btnRecord_Click(object sender, EventArgs e)
{
if (_isRecording)
return;
try
{
// 禁用开始按钮,启用停止按钮
btnRecord.Enabled = false;
btnStop.Enabled = true;
// 设置录制状态
_isRecording = true;
lblStatus.Text = "正在录制中...";
// 创建输出文件路径
_outputPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
$"ScreenCapture_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.avi");
// 获取录制参数 - 在UI线程上捕获这些值
int frameRate = (int)numFrameRate.Value;
bool recordMouse = chkRecordMouse.Checked;
int selectedAreaIndex = cmbRecordingArea.SelectedIndex;
// 开始录制
_cancellationTokenSource = new CancellationTokenSource();
_recordingTask = StartRecordingAsync(frameRate, recordMouse, selectedAreaIndex, _cancellationTokenSource.Token);
}
catch (Exception ex)
{
MessageBox.Show($"录制失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
ResetUI();
}
}
/// <summary>
/// 停止录制按钮点击事件处理
/// </summary>
private async void btnStop_Click(object sender, EventArgs e)
{
if (!_isRecording)
return;
try
{
lblStatus.Text = "正在保存录制文件...";
// 停止录制
_cancellationTokenSource.Cancel();
await _recordingTask;
_processingComplete = true;
// 重置UI状态
ResetUI();
// 询问是否打开录制文件
if (File.Exists(_outputPath))
{
var result = MessageBox.Show($"录制已完成并保存至:\n{_outputPath}\n\n是否立即查看视频?",
"录制完成", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
if (result == DialogResult.Yes)
{
System.Diagnostics.Process.Start(_outputPath);
}
}
}
catch (Exception ex)
{
MessageBox.Show($"停止录制过程失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
ResetUI();
}
}
/// <summary>
/// 重置UI状态
/// </summary>
private void ResetUI()
{
btnRecord.Enabled = true;
btnStop.Enabled = false;
_isRecording = false;
if (_processingComplete)
lblStatus.Text = "处理完成,准备就绪";
else
lblStatus.Text = "准备就绪";
}
/// <summary>
/// 异步录制方法
/// </summary>
private async Task StartRecordingAsync(int frameRate, bool recordMouse,
int selectedAreaIndex, CancellationToken cancellationToken)
{
await Task.Run(() =>
{
Rectangle bounds;
// 根据选择的区域确定录制区域
switch (selectedAreaIndex)
{
case 0: // 全屏
bounds = Screen.PrimaryScreen.Bounds;
break;
case 1: // 当前窗口
IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;
GetWindowRect(handle, out RECT rect);
bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
break;
default:
bounds = Screen.PrimaryScreen.Bounds;
break;
}
// 创建一个MJPEG编码器,设置质量为75%
var encoder = new MJPEGEncoder(bounds.Width, bounds.Height, 75);
try
{
using (var writer = new AviWriter(_outputPath)
{
FramesPerSecond = frameRate,
EmitIndex1 = true
})
{
// 创建视频流
var videoStream = writer.AddVideoStream();
videoStream.Width = bounds.Width;
videoStream.Height = bounds.Height;
videoStream.Codec = new FourCC("MJPG");
videoStream.BitsPerPixel = BitsPerPixel.Bpp24;
using (var screenCapture = new ScreenCapture(bounds, recordMouse))
{
var frameInterval = TimeSpan.FromSeconds(1.0 / frameRate);
var watch = new Stopwatch();
watch.Start();
var frameCount = 0;
var lastFrameTime = TimeSpan.Zero;
while (!cancellationToken.IsCancellationRequested)
{
var timestamp = watch.Elapsed;
// 确保按照指定的帧率捕获
if (timestamp - lastFrameTime >= frameInterval)
{
using (var bitmap = screenCapture.Capture())
{
try
{
// 使用MJPEG编码器编码图像
byte[] compressedFrame = encoder.EncodeFrame(bitmap);
// 写入编码后的视频帧
videoStream.WriteFrame(true, compressedFrame, 0, compressedFrame.Length);
frameCount++;
lastFrameTime = timestamp;
// 在UI线程上更新状态
if (frameCount % 30 == 0) // 每30帧更新一次UI
{
this.Invoke((MethodInvoker)delegate {
lblStatus.Text = $"正在录制中... 已录制 {frameCount} 帧";
});
}
}
catch (Exception ex)
{
Debug.WriteLine($"编码/写入视频帧时出错: {ex.Message}");
}
}
}
else
{
// 如果时间间隔不够,让线程休眠一小段时间以减少CPU使用
Thread.Sleep(1);
}
}
// 录制完成后更新UI
this.Invoke((MethodInvoker)delegate {
lblStatus.Text = $"录制已完成,共 {frameCount} 帧";
});
}
}
// 确认文件是否写入成功
if (File.Exists(_outputPath))
{
FileInfo fi = new FileInfo(_outputPath);
Debug.WriteLine($"AVI文件大小: {fi.Length / (1024 * 1024):F2}MB");
}
}
catch (Exception ex)
{
Debug.WriteLine($"录制过程中发生严重错误: {ex.Message}");
Debug.WriteLine($"错误堆栈: {ex.StackTrace}");
}
}, cancellationToken);
}
/// <summary>
/// 窗体关闭事件处理
/// </summary>
protected override void OnFormClosing(FormClosingEventArgs e)
{
// 如果正在录制,确保停止
if (_isRecording)
{
try
{
_cancellationTokenSource.Cancel();
_recordingTask.Wait(1000); // 等待录制任务结束,最多等待1秒
}
catch { /* 忽略关闭时的异常 */ }
}
base.OnFormClosing(e);
}
/// <summary>
/// 屏幕捕获类
/// </summary>
private class ScreenCapture : IDisposable
{
private readonly Rectangle _bounds; // 捕获区域
private readonly bool _captureMouse; // 是否捕获鼠标
private readonly Graphics _graphics; // 绘图对象
private readonly Bitmap _bitmap; // 位图对象
/// <summary>
/// 创建屏幕捕获实例
/// </summary>
/// <param name="bounds">捕获区域</param>
/// <param name="captureMouse">是否捕获鼠标</param>
public ScreenCapture(Rectangle bounds, bool captureMouse)
{
_bounds = bounds;
_captureMouse = captureMouse;
_bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb); // 改为24bpp以匹配MJPEG
_graphics = Graphics.FromImage(_bitmap);
}
/// <summary>
/// 捕获屏幕内容
/// </summary>
/// <returns>捕获的屏幕图像</returns>
public Bitmap Capture()
{
// 捕获屏幕
_graphics.CopyFromScreen(
_bounds.Left, _bounds.Top,
0, 0,
_bounds.Size,
CopyPixelOperation.SourceCopy);
// 如果需要,绘制鼠标光标
if (_captureMouse)
{
DrawMouseCursor(_graphics);
}
return (Bitmap)_bitmap.Clone();
}
/// <summary>
/// 绘制鼠标光标
/// </summary>
/// <param name="g">绘图对象</param>
private void DrawMouseCursor(Graphics g)
{
// 获取鼠标位置
var cursorPosition = Cursor.Position;
// 如果鼠标在捕获区域内
if (_bounds.Contains(cursorPosition))
{
// 获取当前鼠标光标
var cursor = Cursors.Default;
// 计算相对于捕获区域的位置
var x = cursorPosition.X - _bounds.Left;
var y = cursorPosition.Y - _bounds.Top;
// 绘制光标
cursor.Draw(g, new Rectangle(x, y, cursor.Size.Width, cursor.Size.Height));
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_graphics.Dispose();
_bitmap.Dispose();
}
}
/// <summary>
/// MJPEG编码器类 - 用于将Bitmap编码为MJPEG格式
/// </summary>
private class MJPEGEncoder
{
private readonly int _width; // 图像宽度
private readonly int _height; // 图像高度
private readonly long _quality; // JPEG质量 (1-100)
private readonly ImageCodecInfo _jpegCodecInfo; // JPEG编码器信息
private readonly EncoderParameters _encoderParams; // 编码器参数
/// <summary>
/// 创建MJPEG编码器
/// </summary>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="quality">JPEG质量 (1-100)</param>
public MJPEGEncoder(int width, int height, long quality)
{
_width = width;
_height = height;
_quality = quality;
// 获取JPEG编码器
_jpegCodecInfo = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
// 设置JPEG压缩质量
_encoderParams = new EncoderParameters(1);
_encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, _quality);
}
/// <summary>
/// 编码视频帧为JPEG格式
/// </summary>
/// <param name="bitmap">要编码的位图</param>
/// <returns>编码后的JPEG字节数组</returns>
public byte[] EncodeFrame(Bitmap bitmap)
{
using (var memoryStream = new MemoryStream())
{
// 将Bitmap保存为JPEG到内存流
bitmap.Save(memoryStream, _jpegCodecInfo, _encoderParams);
// 返回JPEG压缩后的字节数组
return memoryStream.ToArray();
}
}
}
/// <summary>
/// 窗体加载事件
/// </summary>
private void Form1_Load(object sender, EventArgs e)
{
// 初始化录制区域下拉菜单
cmbRecordingArea.Items.Clear();
cmbRecordingArea.Items.Add("全屏");
cmbRecordingArea.Items.Add("当前窗口");
cmbRecordingArea.SelectedIndex = 0;
// 初始化状态标签
lblStatus.Text = "准备就绪";
// 初始化帧率
numFrameRate.Value = 15;
// 默认设置
chkRecordMouse.Checked = true;
}
}
}

这个项目展示了如何从零开始构建一个功能完整的屏幕录制工具,结合了UI设计、图形处理、多线程编程和视频编码等多项技术,是C#桌面应用开发的一个很好的实例。
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!