
简介面向 C# 桌面开发者与视频处理学习者的完整 Winform 视频压缩源码包基于 C# 与 Windows Forms 构建 GUI解决了从视频读取、编码压缩到参数调节与实时预览的一整套流程问题适合希望掌握桌面多媒体应用开发或研究 H.264/FFmpeg 集成方案的读者。压缩包共 31 个文件大小 11.22MB其中包含 10 个 .cs 源码文件、Visual Studio 解决方案与工程文件、窗口布局及资源文件另有 3 个编译好的 exe 和对应 pdb 调试文件便于直接运行、阅读和二次修改。已有 73 人学习。项目以 MyVideoCompress 为主窗口围绕 CompressForm 提供压缩交互界面并通过 VideoEncoder 中的 VideoFile、EncodedVideo、Encoder、FileSizeFormatProvider 等类实现文件读取、编码封装、输出大小格式化与日志记录同时保留了 cache、settings 等辅助文件目录结构清晰适合作为 Winform 工程组织、C# 事件处理与多媒体 API 调用的实战范本。1. 先拆开看一套自带 GUI 的 C# WinForms 视频压缩源码值不值得一行行读完做 C# 桌面应用的人遇到的最尴尬的需求就是“在窗口上加个视频压缩功能”。这不是丢一个第三方控件就能完事的它牵扯到编码器的初始化、输入输出的数据模型、界面上的进度反馈还有最容易被忽略的文件大小格式化。这份基于 C# 的 WinForms 框架 GUI 界面的视频压缩源码正好把这些问题拆成了可读性很高的工程一个 WinForms 主窗体 CompressForm 负责交互一个 VideoEncoder 文件夹装四个核心类负责压缩业务。我拆完第一遍的感觉是它没有堆任何炫技代码结构特别适合拿来做“视频压缩桌面工具”的模板甚至你手头有上位机项目、WinForms 管理工具要做视频导出也能直接借鉴这一层。适合刚学完 C# 想上手真实项目的初学者也适合有桌面开发经验、想快速参考编码器封装思路的从业者。2. 工程骨架从 .sln 到 VideoEncoder 类库的层次拆解打开这个压缩包第一步不是急着按 F5 跑起来而是先把解决方案的物理结构读明白。编码器这种东西最怕的就是逻辑全堆在窗体按钮事件里看起来能跑改起来要命。这个工程好在从文件组织上就已经把界面和业务切开了。2.1 先看目录这份源码包里到底放了什么东西在 Visual Studio 里打开以前值得先把文件清单跟实际目录对应一遍。这套工程的根目录是一份 MyVideoCompress.sln 解决方案文件往下是项目主体 MyVideoCompress/里面既有典型的 WinForms 工程文件CompressForm.cs、Program.cs、Properties 资源与设置也单独开了一个 VideoEncoder/ 文件夹来放编码器相关的类文件。bin/Release、bin/Debug、obj/ 这些是编译输出目录网上下到的源码包里往往还带着上一次编译的 exe但别迷信那个产物自己重新编译一遍更稳因为编译机器上的运行库版本未必跟你的环境一致。文件或目录职责MyVideoCompress.sln解决方案入口双击能从 VS 打开整个工程CompressForm.cs Designer.cs主窗体的业务逻辑与控件布局CompressForm.resx窗体的资源文件存图标、字符串这类内容Program.cs程序入口Main 函数里启动窗体MyVideoCompress.csproj项目文件目标框架和引用的程序集都在这里声明Properties/程序集信息、图标资源、用户设置VideoEncoder/Encoder.cs压缩逻辑的封装类核心中的核心VideoEncoder/VideoFile.cs输入视频模型VideoEncoder/EncodedVideo.cs压缩输出结果模型VideoEncoder/FileSizeFormatProvider.cs文件大小格式化器给 UI 层显示用的bin/、obj/编译产物与中间文件把这个结构对应到脑子里你就能得到第一层判断作者刻意把“界面”和“编码逻辑”分开了。CompressForm 只管按钮、输入框、进度条这些交互元素Encoder 和两个数据模型组成的 VideoEncoder 文件夹才是整个工程真正值钱的部分。这样的划分对后来者非常友好——想调压缩参数不用去翻窗体代码想换界面风格也不用碰编码器。2.2 界面层、编码层、输出模型层三层各管什么把上面的目录对应到职责上可以分三层看。界面层是以 CompressForm 为中心的 WinForms 部分负责让用户选输入视频、填输出路径、点开始、看进度和最终结果这也是这套源码里 winform 应用体验的直接体现。编码层是 Encoder.cs它接收输入视频按调用方给的比特率、帧率参数调用 Windows 自带的多媒体接口去做真正的转码。输出模型层是 VideoFile 和 EncodedVideo 两个类它们不写任何业务逻辑只把输入侧和输出侧的数据装好。为什么这么分因为视频压缩是一个“输入不确定、输出可预期”的事情。输入的视频可能来自手机、相机、录屏软件格式五花八门而输出我们通常想要的是一个规格固定的 MP4 文件。把输入数据用 VideoFile 包一层把压缩结果用 EncodedVideo 包一层Encoder 夹在中间做转化未来底层接口换成 FFmpeg 还是系统编码器界面代码一行都不用动。这种数据模型的隔离比什么都往窗体里塞要省心得多。2.3 设计器文件为什么不该手动改CompressForm.Designer.cs 是 Visual Studio 设计器自动生成的文件新手最容易在这上面翻车。有人觉得手工调整控件坐标更快直接在 Designer.cs 里改了一行布局代码结果再打开设计器的时候提示“文件已修改请重新加载”或者改好的属性又被设计器覆盖回去。原因很简单设计器维护的是控件和属性的同步关系手工改出来的代码一旦和窗口状态不匹配VS 就会判定文件被外部改动。这个文件也不是不能看它其实是很好的学习材料。控件大小、Anchor 锚点、按钮事件绑定在 InitializeComponent 方法里都写得明明白白。但改布局永远要从设计器图形界面下手IDE 生成的代码交给 IDE 维护就好这是我在这个项目上最想强调的第一条纪律。顺手看一下项目里 Program.cs 的写法这个文件很短却是整个应用能跑起来的前提// Program.cs —— WinForms 应用的统一入口 [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new CompressForm()); }逻辑说明WinForms 应用里 Main 函数这三行几乎成了模板缺了会直接影响界面表现。第一行启用视觉样式让按钮和进度条跟随系统主题第三行把主窗体交给消息循环后程序才开始真正响应鼠标和键盘事件。参数说明SetCompatibleTextRenderingDefault(false) 使用默认的 GDI 文本渲染跟设计器里看到的字体效果保持一致。别手痒改成 true字号和间距在运行时会跟设计器预览对不上追查起来很费时间。3. 编码核心Encoder、VideoFile 与 EncodedVideo 如何串起一条压缩链路中间这一层是整个源码包的含金量所在。前两章解决了“界面和业务分没分开”的问题这一章看的是“编码器到底怎么设计”也就是 Encoder.cs 这个黑匣子里面的结构逻辑。3.1 视频压缩为什么需要一个独立的 Encoder 封装类视频压缩的过程本质上就是把一个视频文件的像素数据和音频数据按照目标编码格式重新写一遍。Windows 平台上做这件事传统有两条路一条是调系统自带的多媒体转码接口另一条是封装 FFmpeg 命令行或者动态库。这个工程从文件结构看并没有引入 FFmpeg 依赖Encoder.cs 更接近把 Windows 多媒体接口包了一层向外界暴露一个简单方法你给我一个 VideoFile我给你一个 EncodedVideo。这样做的好处很直接使用这套源码的人不需要额外配置 FFmpeg 环境拿到工程能编译就能跑。代价是编码能力的上限受限于系统接口常见的 H.264 级别压缩没问题想压 AV1 或者设置 10-bit 色深就力不从心了。我一般把这种封装叫“够用型封装”——它服务的目标是 GUI 工具不是专业转码流水线。只要你界面里放几个预设档位它能稳稳完成任务。3.2 VideoFile 与 EncodedVideo把输入和输出做成两个纯净的模型VideoFile.cs 这个类代表输入端它不关心输出结果只记录源视频的基本信息。最常见的设计是这样// VideoFile.cs —— 描述一个待压缩的输入视频 public class VideoFile { public string FilePath { get; set; } public long FileSize { get; set; } public double? DurationSeconds { get; set; } public int? Width { get; set; } public int? Height { get; set; } public VideoFile(string filePath) { FilePath Path.GetFullPath(filePath); var info new FileInfo(FilePath); if (info.Exists Info.Length 0) { FileSize info.Length; } } }逻辑说明构造函数里只做两件事规范化路径和读取文件大小。DurationSeconds、Width、Height 这些字段先留空等编码器真正打开源文件探测后再填充不在构造阶段做重 IO 操作。这么做是为了让界面层拿到 VideoFile 时可以先用 FileSize 做体积预判不必一上来就解码整个文件。参数说明FilePath 用 Path.GetFullPath 处理过后面不管遇到相对路径还是带短横线的目录名至少路径这一环是稳的。FileSize 用 long 而不用 int是刻意避坑的写法超过 2GB 的视频在 int 下会溢出成负数显示出来就成了“压缩前 -876MB”这种笑话。EncodedVideo.cs 是输出端的模型它把“压缩任务的结果”打包成一个对象// EncodedVideo.cs —— 一次压缩任务的完整输出结果 public class EncodedVideo { public string OutputPath { get; set; } public long OutputSize { get; set; } public bool IsSuccess { get; set; } public string ErrorMessage { get; set; } public DateTime CompletedAt { get; set; } DateTime.Now; }逻辑说明输出模型里绑定是否成功和错误信息是这类工具很值得学习的点。调用方拿到 EncodedVideo哪怕失败也能从 ErrorMessage 直接看到原因而不是靠 catch 异常后返回 null。这样界面层拿一个返回值就能覆盖成功和失败两种展示逻辑。参数说明OutputSize 存压缩后文件实际字节数CompletedAt 默认赋值时间戳方便压缩完立即在界面上显示“完成于 xx:xx”不用额外再取一次当前时间。3.3 一条完整调用链读取、编码、落盘以及默认参数怎么设把两个模型串起来的是 Encoder.cs。它的主方法应该长这样// Encoder.cs —— 压缩入口输入源视频输出编码结果 public class Encoder { public EncodedVideo Compress(VideoFile source, string outputPath, int videoBitrateKbps, int audioBitrateKbps, double frameRate, bool useFastPreset) { var result new EncodedVideo { OutputPath outputPath }; try { // 1. 校验输入文件是否存在输出目录是否可写 // 2. 通过 Windows 多媒体接口打开源文件探测分辨率、时长 // 3. 按 videoBitrateKbps / audioBitrateKbps 创建编码会话 // 4. 逐帧读取写入编码器循环到文件结束 // 5. 关闭会话取输出文件大小写入结果模型 result.OutputSize new FileInfo(outputPath).Length; result.IsSuccess true; } catch (Exception ex) { result.IsSuccess false; result.ErrorMessage ex.Message; } return result; } }逻辑说明try/catch 放在方法入口处把任何异常都转换成 EncodedVideo 里的错误信息这是典型的薄封装思路。注意步骤 4 强调“逐帧读取”而不是整个视频一次性加载进内存视频文件动辄几百 MB 甚至几个 GB内存里扛不住。参数说明videoBitrateKbps 是视频比特率单位是 kbps界面给三个常用档位就够2500流畅清晰、3500默认、5000高画质。audioBitrateKbps 一般固定在 128对视频工具来说足够。frameRate 别胡乱抬高源视频 30 帧就写 30抬高了体积上去画质并没有提升。这段调用链真正的坑在步骤 2 和步骤 3打开源文件探测信息时多媒体接口的初始化代码要写在 try 块的最前面一旦探测失败就别往后走直接返回。我见过不少初学者把探测放在构造函数里结果弹窗还没出来就先报一遍错用户完全不知道发生了什么。4. GUI 交互CompressForm 的控件布局与 FileSizeFormatProvider 的格式化技巧工程外观部分看 CompressForm。这一章说三件事窗体上到底需要哪些控件、文件大小格式化为什么值得单独做成一个类、压缩任务怎么从 UI 线程里挪出去。4.1 WinForms 主窗口需要多少控件才算够用看这套源码的窗体设计没有花哨的皮肤也没有奇特的布局就是最朴素的 winform 编排一个输入文件路径文本框加浏览按钮一个输出路径文本框加浏览按钮一个压缩参数区域比特率下拉框或滑块一个开始按钮一条进度条最后是一行结果文本。这个控件事务清单适合绝大多数视频压缩场景不是功能少而是视频压缩本来就不需要太多开关。控件命名这块值得照着习惯走btnSelectFile、btnCompress、txtInputPath、txtOutputPath、progressBar、lblResult从名字就能猜到用途。这比 Form1、button1、button2 的命名方式维护成本低得多特别是窗口上控件一多默认命名会让你在事件绑定里找半天。这里要特别提一句 winform 界面美化的话题。很多人拿到工程第一件事就是想把界面换成深色主题、圆角按钮这个思路没错但别动骨架。界面美化和业务逻辑是两码事先确认压缩功能稳定了再用自定义绘制或第三方皮肤控件去改外观顺序不能反。4.2 FileSizeFormatProvider 的实现把字节数变成“1.2 GB”的人话这个类在整个工程里看起来最小但用起来最顺手。它的作用是把长整型字节数直接格式化成“1.2 GB”这种容易读的字符串实现方式是 C# 的 IFormatProvider 加 ICustomFormatter 组合// FileSizeFormatProvider.cs —— 让 string.Format 直接输出可读文件大小 public class FileSizeFormatProvider : IFormatProvider, ICustomFormatter { public object GetFormat(Type formatType) { return formatType typeof(ICustomFormatter) ? this : null; } public string Format(string format, object arg, IFormatProvider formatProvider) { if (!string.Equals(format, FS, StringComparison.OrdinalIgnoreCase)) return string.Format(new CultureInfo(zh-CN), {0: format }, arg); double size Convert.ToDouble(arg, CultureInfo.InvariantCulture); string[] units { B, KB, MB, GB, TB }; int unit 0; while (size 1024 unit units.Length - 1) { size / 1024; unit; } return ${size:0.##} {units[unit]}; } } // 使用方式把 1,863,000,000 字节显示成 1.86 GB不用自己写除法 var sizeText string.Format(new FileSizeFormatProvider(), {0:FS}, result.OutputSize);逻辑说明这个类的巧妙之处在于它挂在 string.Format 上调用处不用写“if 大于 1GB 就除以 1024 再判断一次单位”这种重复分支。GetFormat 方法让格式化器被 string.Format 识别Format 方法内部判断格式化串是不是 “FS”是就走进文件大小换算逻辑。注意 Convert.ToDouble 用了 InvariantCulture避免个别本机区域设置把小数点当成分隔符。参数说明格式串“FS”是自定义标识你可以改成任何不跟内置格式冲突的字母组合。单位数组从 B 到 TB循环退出的条件是 size 小于 1024 或者到了最后一个单位所以超过 TB 的文件不会进位到 PB但会保留两位小数显示这是工程里能接受的边界。这段代码放进界面层之后压缩结果展示就变成一行lblResult.Text 压缩完成输出体积 string.Format(new FileSizeFormatProvider(), {0:FS}, result.OutputSize);逻辑说明调用处干净到不能再干净所有换算逻辑都在格式化类里。如果你在 bin/Release 里跑过这个 exe看到界面输出 3.2 GB、845 MB 这类字符串背后就是这段代码在干活。4.3 压缩任务放后台进度更新回 UI 线程一套干净的写法WinForms 最经典的翻车现场就是把耗时任务直接写在按钮点击事件里。压缩视频是典型的 CPU 密集任务一发编码会话跑出去要是占着 UI 线程窗口立刻变白、拖不动、按钮点了没反应。正确做法是让压缩跑在后台线程进度和结果通过 Invoke 封回 UI 线程// CompressForm.cs 中的压缩按钮事件 private void btnCompress_Click(object sender, EventArgs e) { string inputFile txtInputPath.Text.Trim(); string outputFile txtOutputPath.Text.Trim(); if (!File.Exists(inputFile)) { MessageBox.Show(输入文件不存在); return; } var encoder new Encoder(); var videoFile new VideoFile(inputFile); btnCompress.Enabled false; // Task.Run 把压缩放到后台UI 线程只负责刷新界面 Task.Run(() { var result encoder.Compress(videoFile, outputFile, 3500, 128, 30, true); // 回 UI 线程更新控件WinForms 控件不是线程安全的 this.Invoke((Action)(() { btnCompress.Enabled true; if (result.IsSuccess) { lblResult.Text 压缩完成 string.Format(new FileSizeFormatProvider(), {0:FS}, result.OutputSize); } else { lblResult.Text 压缩失败 result.ErrorMessage; } })); }); }逻辑说明压缩开始前先禁用按钮防止用户重复点击导致多个编码会话同时跑。Task.Run 把 Compress 调用丢给线程池UI 线程立即返回消息循环继续处理界面重绘。压缩结束后用 this.Invoke 把结果写回控件这是 WinForms 跨线程更新的标准写法——直接在线程池线程里改 label.Text十有八九会抛“跨线程操作无效”的异常。参数说明这里的 Compress 参数沿用了上一章的五个参数3500kbps 视频码率、128kbps 音频码率、30 帧输出。如果你是在环境较老、用 vs2015 打开的工程注意确认项目目标框架支持 Task.Run.NET 4.5 以上都没问题实在跑不了就退回 BackgroundWorker逻辑一样。5. 避坑指南WinForms 视频压缩项目最常见的五个坑这个工程的结构不难难的是在调试里碰到的各种“玄学”问题。我拆过不少同类源码也见过很多人卡在同一类错误上下面五条是按出现频率排出来的每一条都是血泪经验。5.1 现象压缩到一半界面整个卡死压缩启动之后窗口白屏标题栏显示“未响应”等几分钟才恢复。原因压缩逻辑直接被放在按钮点击事件里UI 线程忙于解码和编码循环没空处理重绘和鼠标消息。解决按 4.3 的写法把压缩放到 Task.Run 或 BackgroundWorker 里UI 线程只接收完成状态。检查方法很简单进度条如果不是持续刷新而是压缩过程不动、结束后突然跳满那基本就是 UI 线程被占住了。5.2 现象压缩完成输出文件却被锁定压缩成功后紧接着去删除或覆盖输出的视频文件系统提示“文件正在被另一进程使用”。原因十有八九是编码会话或文件流的句柄没释放有些多媒体接口的写入句柄必须显式调用 Close内部若开了 FileStream 而且没放掉也一样。解决所有文件流全部用 using 或者 try/finally 包住编码会话结束后统一释放然后再加一段验证代码尝试用 File.Delete 删一次输出文件能删说明没锁。5.3 现象路径带中文或空格就直接失败输入路径是 D:\视频素材\我的视频 (1).mp4 这种常见命名编码器返回“找不到文件”或一串十六进制错误码。原因底层多媒体接口处理路径时把特殊字符截断了尤其是空格和中文不同接口的处理方式还不一样。解决路径统一用 Path.GetFullPath 后传入而不是拼截取过的字符串如果内部封装的是命令行调用路径必须加双引号能传文件句柄的就别传字符串路径后者最容易翻车。5.4 现象Debug 没问题Release 一跑就崩同一个解决方案Debug 下压缩多少视频都正常切换 Release 后要么启动就报错要么压缩一两分钟才异常退出。原因常见的有两类一是 Release 的代码优化让某些未初始化的字段变成默认值该判空的地方没判二是多媒体接口依赖的运行库在你机器上少装了一个Debug 下因为附加调试器所以没暴露。解决先到项目属性里勾掉“优化代码”排查是不是逻辑问题再在程序入口和 Encoder 异常处打好日志Release 崩溃时能看到异常堆栈别让错误被吞在空 try/catch 里。5.5 现象压缩完的体积比原文件还大压缩一个 1 分钟的视频结果输出文件比输入还大三分之一。原因源文件本身已经是高压缩格式比如 H.265 编码的 MP4 又拿来用 H.264 压缩一遍等于二次编码体积不降反升另一种情况是界面给输出设置的比特率太高比如拉到 8000kbps 以上而源视频实际码率只有 4000。解决压缩前先读源视频的码率目标码率设成源码率的 60% 到 70%肉眼画质差异不明显体积能明显降下来源视频已经是高压缩格式的话就不要重复压了这时候工具的意义不大。6. 继续往下走的三个方向编码器替换、批量入口与日志固定这套源码能跑通是一回事能改造成自己趁手的工具是另一回事。我最后说三个实际可操作的延伸方向。6.1 把 Encoder 内部换成 FFmpeg 进程封装如果源码里的系统编码接口满足不了你的格式需求最快的改造方案是把 Encoder.Compress 内部的实现换成调用 ffmpeg.exevar psi new ProcessStartInfo { FileName ffmpeg.exe, Arguments $-i \{input}\ -c:v libx264 -crf 28 -preset veryfast $-c:a aac -b:a 128k \{output}\, UseShellExecute false, CreateNoWindow true, RedirectStandardError true }; using var process Process.Start(psi);逻辑说明这样改之后输出格式和编码参数的选择范围立刻变大FFmpeg 几乎覆盖所有主流格式而且还能用 -progress 参数回传进度。代价是目标机器上必须能访问到 ffmpeg.exe要么放在程序同目录要么加进 PATH。6.2 加一个批量压缩入口单文件压缩跑通以后多半会想要批量处理。做法是窗体加一个“添加文件夹”按钮把选中的文件列表存进一个 Queue 压缩完一个接着取下一个。注意批量时每个文件独立创建 Encoder 实例不要复用同一个编码会话视频格式不同会话参数不通用。压缩前先做格式过滤用扩展名白名单筛掉非视频文件能少踩很多无谓的坑。6.3 我把日志写在配置文件里的习惯从拆这个工程开始我就养成了一个习惯凡是涉及编码器的项目强制把输入路径、输出路径、目标码率、编码耗时写进日志文件不管压缩成功还是失败。碰到 Release 崩了、文件变大了这类问题日志里一行一行读下来五分钟就能定位比自己瞎猜快太多。从那以后我每次改编码参数都先看一眼上一次日志里的码率和体积再决定这次怎么调工具好不好用很大程度取决于你有没有把参数和日志留全。希望这些拆解对你手上的项目有帮助。本文还有配套的精品资源点击获取