Qt 32位 mingw32 工程集成 FFmpeg 3.4 解码与抽帧

发布时间:2026/10/27 10:37:14
Qt 32位 mingw32 工程集成 FFmpeg 3.4 解码与抽帧

简介:ffmpeg-3.4-mingw32 是面向 32 位 Windows 平台、基于 MinGW32 编译环境构建的 FFmpeg 3.4 多媒体处理套件,适合使用 Qt 开发音视频应用的 C/C++ 开发者,也便于需要轻量解码能力或二次开发的用户快速接入。压缩包共 170 个文件,约 2.62MB,包含 107 个 h 头文件、21 个 c 示例源码、6 个 dll 与配套 lib/a 导入库、6 个 def 模块定义文件及 6 个 pc 配置文件,另有 readme、makefile 和少量 ffpreset 预设,覆盖从接口声明、示例工程到动态链接库的完整开发素材。资源已有 229 人学习下载。借助包内的编解码、封装、滤镜、缩放与重采样模块库,以及转码、封装、解复用解码等示例,读者可对照头文件与源码理解音视频处理流程,并将 DLL 直接用于 Qt/MinGW 工程,减少自行编译与配置依赖的时间。

1. 32 位 Qt 工程里塞 FFmpeg:为什么偏偏要留一份 3.4 的 mingw32 库

上个季度接手一个还在跑的 32 位上位机:Qt 5.9 的 mingw32 Kit,界面里要嵌一路本地视频回放,还得按时间点抽帧存证。第一反应是拿最新版 FFmpeg,头文件一包含就出问题——AVStream::codec被挪走、av_register_all也被标记弃用,编译直接出现大量报错。换 64 位包,链接器又输出架构不匹配的提示。最后落到 ffmpeg-3.4-mingw32 上:FFmpeg 3.4 源码用 i686 mingw 工具链编出来的 32 位动态库、导入库加头文件。

它解决的不是“最新最好”,而是“老工程还得再活几年”:解码、抽帧转 QImage、时间戳换算、音频重采样,3.4 这套 API 都齐;更关键的是,3.4 处在旧接口(avcodec_decode_video2)尚未移除、新接口(avcodec_send_packet/avcodec_receive_frame)已经稳定的重叠区间,老代码抄过来能编过,新写法也能用。适合还在维护 Qt 5 + 32 位 mingw 桌面程序、需要本地解码或取帧、又不想为了编个库去搭一整套 msys 环境的人。

2. 先看清包里有什么:从 include/lib/bin 到 Qt 的 .pro

拿到这种预编译包,最怕的不是不会写代码,是目录结构没搞明白就急着往.pro里塞路径,然后花两小时在链接错误里绕。这个包的结构和所有 FFmpeg 官方 Windows 构建是一套逻辑,先认清楚再动手。

2.1 32 位 mingw 构建的目录分工

路径内容用途
include/libavcodec、libavformat、libavutil、libswscale、libswresample等头文件全是 C 头,C++ 里必须extern "C"包住
lib/*.dll.amingw 格式导入库-lavcodec实际找的是它
lib/*.libMSVC 格式导入库mingw 用不上,留着不碍事
bin/*.dll运行时动态库部署时必须跟 exe 放一起
lib/pkgconfig/.pc文件用 qmake 时基本用不到,写 CMake 才有意义

判断这份库是不是真的 32 位,别看目录名叫不叫 mingw32,直接看 DLL 的文件头。用 mingw 自带的objdump一行就能确认,看到pei-i386才算对,pei-x86-64就是拿错了包:

# 确认导入库背后那份 DLL 的架构 objdump -f bin/avcodec-57.dll | grep architecture # 期望输出:file format pei-i386

还有一个容易被忽略的点:FFmpeg 3.4 这几个库的主版本号是固定的,libavcodec是 57、libavformat是 57、libavutil是 55、libswscale是 4、libswresample是 2、libavfilter是 6。这个号后面选 DLL、写部署脚本都要用,记牢比记版本字符串有用——因为不同小版本之间这些主版本号是不变的,DLL 名字也就不变,但 ABI 细节可能变,所以“同名不同版本混用”是后面要重点防的事。

2.2 .pro 里怎么写:INCLUDEPATH、LIBS 与链接顺序

qmake 的写法本身不复杂,复杂的是条件判断和链接顺序。我一般把三方库统一放工程目录下的third_party/,用$$PWD起相对路径,这样整个工程拷给同事也能直接编。同时在.pro里显式判断架构,别指望开发者每次都记得切 Kit:

# 只有 32 位 Kit 才走这条分支,避免同一个 .pro 在 64 位机器上误链 contains(QT_ARCH, i386) { FFMPEG_ROOT = $$PWD/third_party/ffmpeg-3.4-mingw32 INCLUDEPATH += $$FFMPEG_ROOT/include LIBS += -L$$FFMPEG_ROOT/lib # 顺序有讲究:调用方在前,被调用方在后 LIBS += -lavformat -lavcodec -lswscale -lswresample -lavutil # 3.4 的头文件里用了 UINT64_C 这类 stdint 宏,C++ 下不加这个会编不过 DEFINES += __STDC_CONSTANT_MACROS } else { error("本工程只提供 32 位 mingw 版 FFmpeg,请切换到 x86 Kit") }

逐行说清楚,这几行每一行都是踩过坑才补上的。

contains(QT_ARCH, i386)是 qmake 提供的架构变量判断,32 位 mingw Kit 下它等于i386。加这个分支的意义在于:一个团队里总有人用 64 位 Kit 打开工程,如果不拦,他会得到一堆“架构不匹配”的链接错误,然后跑来问你库是不是坏的。直接error()报一句人话,省掉一轮沟通。

LIBS里的顺序在动态链接场景下影响其实不大,因为导入库只记符号不打包代码;但一旦以后换成静态库(.a),顺序错了就是满屏undefined reference。养成“高层在前、底层在后”的习惯:avformat依赖avcodec,avcodec依赖avutil,swscale/swresample挂在外围。按这个顺序写,静态库也能过。

__STDC_CONSTANT_MACROS这一行是老生常谈但每次换环境都会忘。FFmpeg 的common.h里会间接包含<stdint.h>,在 C++ 编译单元里,如果这个宏没定义,UINT64_C(0)这类宏展开会失败,报错长得像'UINT64_C' was not declared in this scope。C 文件里没这个问题,偏偏 Qt 工程全是.cpp。

-L后面如果路径带空格,一定要整体加引号,Windows 下 qmake 对空格的容忍度不高:LIBS += "-L$$FFMPEG_ROOT/lib"。这个坑在“把工程放在D:/My Projects/下”的时候必现。

2.3 用一段最小代码验证链接是否真的通过

不要一上来就写解码器。先用二十行代码把“能不能链接、DLL 能不能加载、版本号对不对”这三件事一次性验完,出问题时排查范围小得多。

// main.cpp —— 只验证链接和运行时加载,不碰任何解码逻辑 #include <QCoreApplication> #include <QDebug> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // 3.4 里 av_register_all 还没被移除;本地文件解码其实可以不调, // 但老代码里常和对网络协议注册写在一起,保留以对齐习惯 av_register_all(); qDebug() << "av_version_info:" << av_version_info(); // 形如 "3.4.x" qDebug() << "libavcodec:" << avcodec_version(); // 57000 以上 qDebug() << "libavformat major:" << LIBAVFORMAT_VERSION_MAJOR; // 57 qDebug() << "libavcodec major:" << LIBAVCODEC_VERSION_MAJOR; // 57 return 0; }

extern "C"这个包裹必须加。FFmpeg 是纯 C 库,头文件里没有__cplusplus保护,直接用 C++ 编译器包含会做名字修饰,链接时符号名对不上,报的错是undefined reference to 'avcodec_version()'——注意末尾那个空括号,就是这个错误的典型标志。

av_version_info()返回的是字符串,能直接看出小版本;avcodec_version()返回整数,3.4 下大概是 57 万多。两个都打出来,可以快速判断“头文件版本”和“运行时 DLL 版本”是不是同一份——这两者不一致是最阴的一类 bug,编译期完全不报,跑起来在avcodec_open2里崩,栈还看不出所以然。

运行时如果在这一步就崩在av_register_all或av_version_info上,那基本可以断定是 DLL 加载到了别的版本。把bin/下的 DLL 全拷到 exe 同级目录,再跑一次。Windows 的 DLL 搜索顺序里,exe 所在目录优先级很高,但 PATH 里的目录也会被搜,开发机上装过别的东西就很容易撞车。

3. 解码链路:3.4 的 API 边界与 QImage 落地

验证链接通过之后,才是真正写解码。3.4 这个版本的微妙之处在于新旧两套接口并存,网上抄来的代码可能两种都有,混着用不出错,但职责不清会埋雷。我一般统一走 send/receive 这套,因为它对 flush、EAGAIN 的处理语义更清楚。

3.1 打开输入到找到解码器

这一段是所有播放器都一样的骨架:打开容器、探测流信息、挑视频流、建解码器上下文。3.4 里AVStream::codec还能用,但已经是弃用状态,正确做法是从codecpar拷参数到自己的AVCodecContext。

bool Decoder::open(const QString &path) { AVDictionary *opts = nullptr; // 本地文件用不上,但对 rtsp/http 源是保命的;单位微秒 // 注意 rtsp 用 stimeout,http 用 timeout,名字不一样 av_dict_set(&opts, "stimeout", "5000000", 0); // Windows 下 FFmpeg 内部会把 UTF-8 转成宽字符再 fopen, // 所以这里必须传 UTF-8,传 GBK 反而打不开中文路径 QByteArray utf8Path = path.toUtf8(); int ret = avformat_open_input(&fmtCtx_, utf8Path.constData(), nullptr, &opts); av_dict_free(&opts); if (ret < 0) return false; // 探测流信息:这一步会读若干帧,网络源上耗时明显,本地文件几乎无感 if (avformat_find_stream_info(fmtCtx_, nullptr) < 0) return false; int idx = av_find_best_stream(fmtCtx_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (idx < 0) return false; videoIndex_ = idx; AVStream *st = fmtCtx_->streams[idx]; AVCodec *dec = avcodec_find_decoder(st->codecpar->codec_id); if (!dec) return false; codecCtx_ = avcodec_alloc_context3(dec); if (avcodec_parameters_to_context(codecCtx_, st->codecpar) < 0) return false; // 32 位进程地址空间紧张,线程给 2 个就够,开多了帧缓存会把内存吃满 codecCtx_->thread_count = 2; if (avcodec_open2(codecCtx_, dec, nullptr) < 0) return false; // 时间戳换算用的是流的时基,不是 codecCtx 的时基,这两个别搞混 timeBase_ = st->time_base; return true; }

av_find_best_stream比手写“遍历所有流找第一个视频流”靠谱,它会参考容器里的默认流标记,也能顺手把解码器返回给你(最后一个参数传nullptr就不返回)。有些 MP4 里带封面图,封面也是一条视频流,手写循环很容易抽到封面而抽不到正片。

codecCtx_->thread_count = 2这行在 64 位下我一般写 0(让 FFmpeg 自己决定),32 位下反而要手动压住。原因很实际:帧级多线程会同时持有多帧缓冲,1080p 的 YUV420P 一帧大概 3MB,解码器内部排队的帧加上应用层的帧,很容易把 2GB 的用户态地址空间切碎。后面第 4 章会有一条专门讲这个。

timeBase_一定要存下来。AVStream::time_base和AVCodecContext::time_base在 3.4 里都可能不是 1/1000,很多视频是 1/90000 或 1/15360,直接拿 pts 当毫秒用,进度条就会走成各种奇怪的速度。

3.2 收帧:send_packet / receive_frame 的正确循环

3.4 里avcodec_send_packet和avcodec_receive_frame已经完全可用,avcodec_decode_video2虽然还能编过但已被标记弃用。新接口的核心是“一次喂包不一定出一帧,一次收帧不一定需要新包”,循环结构写对了,后面很多玄学问题自然消失。

// 取一帧:成功返回 true,文件结束或出错返回 false bool Decoder::readFrame(AVFrame *frame) { for (;;) { int ret = avcodec_receive_frame(codecCtx_, frame); if (ret == 0) return true; // 拿到一帧,直接返回 if (ret == AVERROR_EOF) return false; // 解码器已吐完 if (ret != AVERROR(EAGAIN)) return false; // 真错误 // EAGAIN:解码器饿了,需要新包 av_packet_unref(pkt_); if (av_read_frame(fmtCtx_, pkt_) < 0) { // 文件读完 != 帧读完。解码器里可能还压着几帧, // 塞一个空包进去把它冲出来,这是新接口的固定套路 avcodec_send_packet(codecCtx_, nullptr); continue; } if (pkt_->stream_index != videoIndex_) continue; // 音频/字幕包直接丢 ret = avcodec_send_packet(codecCtx_, pkt_); if (ret < 0 && ret != AVERROR(EAGAIN)) return false; } }

av_packet_unref(pkt_)在每次av_read_frame之前调,这是引用计数包的标准用法。少了它,包内部的AVBufferRef不会释放,内存会随着播放时长线性上涨——但涨得很慢,播十分钟看不出来,跑一晚上才爆,属于最难查的那类。

avcodec_send_packet(codecCtx_, nullptr)这行是收尾的关键。很多移植代码直接return false就结束了,结果视频最后几帧永远出不来,表现为“播到结尾卡住”或者“抽帧少了几帧”。空包是约定俗成的 flush 信号,用完不需要额外清理。

还有一个细节:av_read_frame返回负值之后,代码会一直continue,下一轮receive_frame如果还是 EAGAIN,会再次send_packet(nullptr)。第二次之后 send 会返回AVERROR_EOF,此时 receive 也必然返回 EOF,循环就退出了。所以不会死循环,但保险起见可以在上面加个计数器兜底。

3.3 sws_scale 转 RGB 与 linesize 的对齐坑

FFmpeg 解出来的帧是 YUV420P 居多,Qt 显示要 RGB。中间的转换全靠libswscale,而这里有一个几乎人人都会踩一次的坑:目标缓冲的行宽不能假设等于width * 4。

bool Decoder::toImage(AVFrame *frame, QImage &out) { // 分辨率变了必须重建上下文,否则会读到越界内存直接崩 if (sws_ && (srcW_ != frame->width || srcH_ != frame->height)) { sws_freeContext(sws_); sws_ = nullptr; } if (!sws_) { sws_ = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format, frame->width, frame->height, AV_PIX_FMT_BGRA, SWS_BILINEAR, nullptr, nullptr, nullptr); if (!sws_) return false; srcW_ = frame->width; srcH_ = frame->height; } // 目标缓冲按 32 字节对齐分配,av_image_alloc 会把实际行宽写回 rgbLinesize_ if (!rgbBuf_) { if (av_image_alloc(&rgbBuf_, &rgbLinesize_, frame->width, frame->height, AV_PIX_FMT_BGRA, 32) < 0) return false; } uint8_t *dstData[4] = { rgbBuf_, nullptr, nullptr, nullptr }; int dstLinesize[4] = { rgbLinesize_, 0, 0, 0 }; sws_scale(sws_, frame->data, frame->linesize, 0, frame->height, dstData, dstLinesize); // 关键:构造 QImage 时把 rgbLinesize_ 传进去,不能传 width * 4 out = QImage(rgbBuf_, frame->width, frame->height, rgbLinesize_, QImage::Format_ARGB32); return true; }

av_image_alloc的最后一个参数是 alignment,传 32 之后rgbLinesize_通常会大于width * 4(比如 1920×4=7680,本来就对齐,但 1366 宽就不一定了)。如果构造 QImage 时用width * 4当 bytesPerLine,Qt 和 FFmpeg 对“每行多少字节”的理解就会错位,画面表现为经典的斜切——每一行都比上一行偏移一点,像被推歪的百叶窗。这个现象非常容易被误判成“解码坏了”,其实是 stride 没对上。

像素格式选AV_PIX_FMT_BGRA而不是AV_PIX_FMT_RGB32,是因为前者在小端机器上的内存布局正好是 B、G、R、A 四字节,对应QImage::Format_ARGB32的 0xAARRGGBB。选AV_PIX_FMT_RGB24配Format_RGB888也能用,但每像素少一个字节意味着sws_scale要做非对齐写,32 位下性能反而略低,而且 QImage 内部还是会按 4 字节对齐补空。除非内存特别紧,我一般直接用 BGRA。

最后一个隐患:QImage这个构造函数不拷贝数据,它直接引用rgbBuf_。所以返回出去的 QImage 生命周期不能超过下一次sws_scale。需要在别处长期持有的话,老老实实调.copy(),一帧 1080p 大概 8MB,拷一次不算贵,但别在 60fps 的循环里每帧拷两遍。

3.4 释放顺序:反着建、顺着毁

FFmpeg 的资源释放顺序错了会崩,而且崩的位置往往在无关的地方。原则是“后创建的先销毁”,因为后面的对象可能持有前面的指针。

Decoder::~Decoder() { if (sws_) { sws_freeContext(sws_); sws_ = nullptr; } if (rgbBuf_) { av_freep(&rgbBuf_); } // av_image_alloc 必须配 av_freep if (pkt_) { av_packet_free(&pkt_); } if (frame_) { av_frame_free(&frame_); } if (codecCtx_) { avcodec_free_context(&codecCtx_); } if (fmtCtx_) { avformat_close_input(&fmtCtx_); } }

注意av_freep传的是指针的地址,它会顺手把指针置空;av_packet_free和av_frame_free也是同理。这几个_free系列的参数都是二级指针,写成av_packet_free(pkt_)会编不过,写成free(pkt_)则是另一种错误——AVPacket内部还有AVBufferRef,直接free会漏掉它引用的那块内存。

fmtCtx_最后关。它持有所有AVStream,而codecCtx_在avcodec_parameters_to_context的时候已经把参数拷走了,所以先关哪个理论上都行,但按“后建先毁”来永远不会错。

4. 避坑与排查:五个在 32 位 mingw 上翻过的车

下面这五条基本覆盖了我在这套组合上遇到的全部高频问题。按编译期、链接期、运行期分组,每条都按“现象 → 原因 → 解决”写,方便对号入座。

4.1 编译期:那个永远会忘的宏

现象:包含libavcodec/avcodec.h之后,报'UINT64_C' was not declared in this scope,或者'INT64_C'、'UINT32_MAX'一串类似错误,位置在common.h里,跟自己的代码毫无关系。

原因:<stdint.h>在 C++ 模式下默认不暴露UINT64_C这类宏,需要__STDC_CONSTANT_MACROS显式打开。FFmpeg 的头文件依赖这些宏来拼常量,C 文件里因为是 C 模式没这个问题,所以官方构建完全正常,只有 C++ 调用方会撞上。

解决:.pro里加DEFINES += __STDC_CONSTANT_MACROS。如果用的是 CMake,对应add_definitions(-D__STDC_CONSTANT_MACROS)。这条不解决,后面所有问题都无从谈起。

4.2 链接期:架构、格式与路径的三重误判

现象:链接报undefined reference to 'avformat_open_input',或者更奇怪的file format not recognized、cannot find -lavcodec。

原因:三种可能,按出现频率排:一是-L路径写错或者路径里带空格没有引号;二是拿 64 位包配 32 位 Kit,或者反过来,导入库里的符号机器码不匹配;三是 mingw 去链了lib/下的 MSVC 格式.lib——有些打包者会把两种导入库放同一个目录,文件名前缀一样,qmake 找的时候先命中哪个看运气。

解决:先跑objdump -f确认 DLL 架构;再确认-L指向的目录里.dll.a确实存在;最后检查.pro里有没有别的地方重复加了LIBS,把顺序搞乱。我一般会在.pro里加一行message($$LIBS),编译时输出实际链接参数,比猜快得多。

4.3 运行期:DLL 版本错配导致 avcodec_open2 崩溃

现象:编译链接都通过,启动也能跑,但一调avcodec_open2就崩,或者avformat_open_input返回一个莫名其妙的负值,错误信息是“Invalid data found when processing input”,可文件明明是好的。

原因:进程加载到的avcodec-57.dll不是这份包里的。开发机上只要装过任意一个带 FFmpeg 的软件,PATH 里就可能有一份同名但版本不同的 DLL。主版本号相同意味着 ABI 大体兼容,但内部结构体字段顺序、枚举值可能已经变了,指针一解引用就飞。

解决:把bin/下所有 DLL 拷到 exe 同级目录,让 exe 目录的搜索优先级生效;发布前用objdump -p把 exe 的依赖列表打出来,逐项确认来源。开发阶段更狠一点,直接改 DLL 名字前缀(比如加个my_),谁被加载一目了然——这招只在排查时用,别带进发布。

4.4 运行期:花屏、斜切与颜色通道错位

现象:画面能出来,但整体斜着推、每行错开几个像素;或者颜色发蓝/发红,人脸变阿凡达。

原因:斜切是QImage的 bytesPerLine 和sws_scale实际写入的行宽不一致,百分之百是width * 4硬编码导致的。颜色错位则是像素格式对错了:AV_PIX_FMT_RGB32在小端上是 BGRA 布局,配Format_RGB32才对;配成Format_RGB888每像素按 3 字节读,整张图会越读越偏。

解决:永远用av_image_alloc返回的 linesize,构造 QImage 时显式传进去;像素格式固定用AV_PIX_FMT_BGRA+QImage::Format_ARGB32这一对,别在几个格式之间来回试,试出来的正确只是巧合,换个分辨率又坏。

4.5 运行期:内存缓慢上涨与 2GB 天花板

现象:循环抽帧跑几十分钟后进程被系统杀掉,或者申请内存直接失败;任务管理器里看到内存一路涨到 1.8GB 左右然后归零。

原因:32 位进程的用户态地址空间只有 2GB,还得留出碎片。三个常见泄漏点:av_read_frame之后没av_packet_unref;AVFrame每次av_frame_alloc新开一个而不复用;sws_getContext在分辨率变化时重建但没释放旧的。另外解码线程开太多,帧缓冲本身就会吃掉几百 MB。

解决:packet 和 frame 都只分配一次,循环里复用;sws_重建前先sws_freeContext;codecCtx_->thread_count压到 2;如果业务允许,把解码和显示解耦,显示端只保留最新一帧,中间帧直接丢。真要处理 4K,32 位就该考虑换了,这不是代码能绕过去的物理限制。

5. 分发:windeployqt 不认识的 DLL 和 32 位依赖检查

开发机上跑通只完成了一半,真正让人难受的是打包发到客户那台机器上。Qt 自带的windeployqt只认 Qt 自己的库和编译器运行时,第三方 DLL 一个都不会带,这是分发环节最基础也最容易漏的一环。

5.1 依赖清单与放置策略

先明确这份包里到底哪些 DLL 是你的程序真正需要的,不要一股脑全拷——多拷几 MB 在客户端更新时就是几 MB 的流量,而且带上一堆用不到的库只会增加版本冲突的概率。

DLL对应库是否必须
avutil-55.dlllibavutil必须,所有库的基础
avcodec-57.dlllibavcodec必须,解码主体
avformat-57.dlllibavformat必须,容器解析
swscale-4.dlllibswscale必须,YUV 转 RGB
swresample-2.dlllibswresample只做视频可不带
avfilter-6.dlllibavfilter只做纯解码可不带
avdevice-57.dlllibavdevice只有采集摄像头/麦克风才需要

放置策略上,我一般把 exe 和所有 DLL 放在同一层,不建bin/子目录。原因是 Windows 的 DLL 搜索顺序里 exe 所在目录排在最前,放一起最稳;建子目录的话要么改 PATH,要么在代码里SetDllDirectory,多一层配置就多一个失败点。确实不想让目录太乱的话,Qt 提供了QCoreApplication::addLibraryPath,但那只管 Qt 插件,第三方 DLL 还是得靠系统搜索路径。

5.2 依赖检查:确认没有混入 64 位或别的版本

发布前必做的一步是把 exe 的依赖列表打出来,逐项对。用 mingw 自带的objdump就够,不需要额外装工具:

# 列出 exe 依赖的所有 DLL objdump -p release/Player.exe | grep "DLL Name" # 抽查其中几个的架构,必须全是 pei-i386 objdump -f release/avcodec-57.dll | grep architecture objdump -f release/Qt5Core.dll | grep architecture

objdump -p的输出里除了 DLL 名字,还会列出导入的符号和序号。如果某个 FFmpeg 的 DLL 在列表里但版本不对,符号表那一栏会明显缺项——比如 3.4 的avcodec-57.dll里应该能找到avcodec_send_packet,找不到就说明拿到的是更早的构建。

另一个容易忽略的点是编译器运行时。mingw 编译出来的 exe 依赖libgcc_s_dw2-1.dll、libstdc++-6.dll、libwinpthread-1.dll这几个,Qt 的 Kit 目录下都有,但windeployqt只会拷它认为需要的部分。最保险的做法是跑完windeployqt之后,把 Qt Kit 的bin/目录下这几个运行时也手动拷一份过来,然后在一台干净机器上双击验证。

5.3 干净机器验证与路径相关的老问题

“我这能跑”是分发环节最大的谎言。开发机上有完整的 Qt 环境、有 PATH 里的一堆目录、有装过的各种运行时,任何一个都能把你漏拷的 DLL 补上。验证必须在一次都没装过开发环境的机器上做,虚拟机快照最省事:拷贝整个发布目录进去,双击,能起来、能解码、能拖进度条,才算过。

路径问题在 32 位老项目上格外突出,因为客户现场经常是映射网络驱动器或者放到很深的目录里。三个具体场景要提前测:带中文的路径(前面说过

本文还有配套的精品资源,点击获取