long blogs

进一步有进一步惊喜


  • Home
  • Archive
  • Tags
  •  

© 2026 long

Theme Typography by Makito

Proudly published with Hexo

mp4视频获取截图并配置UI管理

Posted at 2026-03-29 ffmpeg qt 

概述

一个目录中有很多MP4文件,需要获取其中10个帧来判断该文件是否需要保留

UI管理实现

使用python+qt来实现

1
2
PyQt5>=5.15.0
send2trash>=2.1.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import sys
import os
import subprocess
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
QWidget, QLineEdit, QPushButton, QFileDialog,
QListWidget, QLabel, QListWidgetItem, QMessageBox)
from PyQt5.QtCore import QByteArray, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtGui import QPixmap
import send2trash

class Mp4SnapTool(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.setWindowTitle('MP4文件预览工具')
self.setGeometry(100, 100, 800, 600)

central_widget = QWidget()
self.setCentralWidget(central_widget)

main_layout = QVBoxLayout(central_widget)

path_layout = QHBoxLayout()

self.path_label = QLabel('目录路径:')
self.path_input = QLineEdit()
self.path_input.setPlaceholderText('请输入目录路径或点击浏览按钮选择...')

self.browse_button = QPushButton('浏览...')
self.browse_button.clicked.connect(self.browse_directory)

self.scan_button = QPushButton('扫描')
self.scan_button.clicked.connect(self.scan_directory)

path_layout.addWidget(self.path_label)
path_layout.addWidget(self.path_input, 1) # 1表示可扩展
path_layout.addWidget(self.browse_button)
path_layout.addWidget(self.scan_button)

file_list_layout = QVBoxLayout()

self.file_count_label = QLabel('文件数量: 0')

self.file_list = QListWidget()
self.file_list.itemClicked.connect(self.on_file_clicked)
self.file_list.itemDoubleClicked.connect(self.on_file_double_clicked)

file_list_layout.addWidget(self.file_count_label)
file_list_layout.addWidget(self.file_list)

main_layout.addLayout(path_layout)
main_layout.addLayout(file_list_layout)

self.statusBar().showMessage('准备就绪')

def browse_directory(self):
directory = QFileDialog.getExistingDirectory(
self,
'选择目录',
self.path_input.text() or os.path.expanduser('~')
)

if directory:
self.path_input.setText(directory)
self.scan_directory()

def scan_directory(self):
directory = self.path_input.text().strip()

if not directory:
QMessageBox.warning(self, '警告', '请输入目录路径')
return

if not os.path.exists(directory):
QMessageBox.warning(self, '警告', f'目录不存在: {directory}')
return

if not os.path.isdir(directory):
QMessageBox.warning(self, '警告', f'路径不是目录: {directory}')
return

try:
self.file_list.clear()
files = []
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if os.path.isfile(item_path) and item.lower().endswith(('.mp4')):
files.append(item)

files.sort()

for file in files:
item = QListWidgetItem(file)
item.setData(Qt.UserRole, os.path.join(directory, file)) # 存储完整路径
self.file_list.addItem(item)

self.file_count_label.setText(f'文件数量: {len(files)}')
self.statusBar().showMessage(f'扫描完成: {len(files)} 个文件')

except PermissionError:
QMessageBox.warning(self, '错误', f'没有权限访问目录: {directory}')
except Exception as e:
QMessageBox.critical(self, '错误', f'扫描目录时发生错误: {str(e)}')

def on_file_clicked(self, item):
file_path = item.data(Qt.UserRole)
file_name = item.text()
file_info = self.get_file_info(file_path)
self.statusBar().showMessage(f'选中: {file_name} - {file_info}')

def get_png_binary_advanced(self, file_path):
try:
process = subprocess.Popen(
['./video_grid', file_path, '3', '3'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)

stdout_data, stderr_data = process.communicate(timeout=30)

if process.returncode != 0:
print(f"错误: 程序返回非零状态码 {process.returncode}", file=sys.stderr)
print(f"错误信息: {stderr_data.decode('utf-8', errors='ignore')}", file=sys.stderr)
return None
print(f'获取PNG图片正常 长度:{len(stdout_data)}')
return stdout_data

except Exception as e:
print(f"错误: {e}", file=sys.stderr)
return None

def delete_file_to_trash(self, file_path):
try:
if os.path.exists(file_path):
send2trash.send2trash(file_path)
print(f"文件已移动到回收站: {file_path}")
else:
print(f"文件不存在: {file_path}")
except Exception as e:
print(f"删除失败: {e}")

def show_image_from_binary(self, file_path, binary_data, title="图像预览"):
pixmap = QPixmap()
if not pixmap.loadFromData(QByteArray(binary_data)):
error_msg = QMessageBox()
error_msg.setIcon(QMessageBox.Icon.Critical)
error_msg.setWindowTitle("错误")
error_msg.setText("无法加载图像数据")
error_msg.setInformativeText("请检查数据是否为有效的PNG格式")
error_msg.exec()
return False

msg_box = QMessageBox()
msg_box.setWindowTitle(title)

scaled_pixmap = pixmap.scaled(800, 800, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
msg_box.setIconPixmap(scaled_pixmap)

msg_box.addButton(QMessageBox.No)
del_btn = msg_box.addButton("删除", QMessageBox.ActionRole)
del_btn.setStyleSheet("""
QPushButton {
min-width: 60px;
min-height: 25px;
color: red;
font-weight: bold;
background-color: white;
border: 1px solid #ccc;
border-radius: 3px;
}
QPushButton:hover {
background-color: #ffe6e6;
border-color: #ff9999;
}
QPushButton:pressed {
background-color: #ffcccc;
}
""")
msg_box.exec()

if msg_box.clickedButton() == del_btn:
print(f'删除文件: {file_path}\n')
self.delete_file_to_trash(file_path=file_path)

return True

def on_file_double_clicked(self, item):
file_path = item.data(Qt.UserRole)
file_name = item.text()
png_data = self.get_png_binary_advanced(file_path)
self.show_image_from_binary(file_path=file_path, binary_data=png_data, title=file_name)
self.scan_directory()

def get_file_info(self, file_path):
try:
size = os.path.getsize(file_path)
if size < 1024:
size_str = f"{size} B"
elif size < 1024 * 1024:
size_str = f"{size/1024:.2f} KB"
else:
size_str = f"{size/(1024*1024):.2f} MB"

return f"大小: {size_str}"
except:
return "大小: 未知"

def main():
app = QApplication(sys.argv)

app.setStyle('Fusion')

window = Mp4SnapTool()
window.show()

sys.exit(app.exec_())

if __name__ == '__main__':
main()

提取MP4中10个帧核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
#include <float.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libavutil/log.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LOGE(format, ...) \
fprintf(stderr, "ERR [%s:%d]:" format "\n", __FUNCTION__, __LINE__, \
##__VA_ARGS__)

// 初始化FFmpeg
void init_ffmpeg() {
av_log_set_level(AV_LOG_QUIET);
avformat_network_init();
}

// 打开视频文件
AVFormatContext* open_video(const char* filename) {
AVFormatContext* format_ctx = NULL;

if (avformat_open_input(&format_ctx, filename, NULL, NULL) != 0) {
LOGE("无法打开视频文件: %s\n", filename);
return NULL;
}

if (avformat_find_stream_info(format_ctx, NULL) < 0) {
LOGE("无法获取流信息\n");
avformat_close_input(&format_ctx);
return NULL;
}

return format_ctx;
}

// 查找视频流
int find_video_stream(AVFormatContext* format_ctx) {
for (unsigned int i = 0; i < format_ctx->nb_streams; i++) {
if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
return i;
}
}
return -1;
}

// 打开解码器
AVCodecContext* open_decoder(AVFormatContext* format_ctx, int video_stream) {
AVCodecParameters* codecpar = format_ctx->streams[video_stream]->codecpar;
AVCodec* codec = avcodec_find_decoder(codecpar->codec_id);

if (!codec) {
LOGE("找不到解码器\n");
return NULL;
}

AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (avcodec_parameters_to_context(codec_ctx, codecpar) < 0) {
LOGE("无法复制编解码器参数到上下文\n");
avcodec_free_context(&codec_ctx);
return NULL;
}

if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("无法打开解码器\n");
avcodec_free_context(&codec_ctx);
return NULL;
}

return codec_ctx;
}

// 跳转到指定时间点
int seek_to_time(AVFormatContext* format_ctx, AVCodecContext* codec_ctx,
double seconds) {
// 使用更精确的跳转方式
int64_t seek_target = (int64_t)(seconds * AV_TIME_BASE);
int ret =
avformat_seek_file(format_ctx, -1, INT64_MIN, seek_target, INT64_MAX, 0);

if (ret < 0) {
LOGE("跳转失败: %.2f秒, 错误码: %d\n", seconds, ret);
return -1;
}

avcodec_flush_buffers(codec_ctx);
return 0;
}

// 读取并解码一帧
AVFrame* decode_frame(AVFormatContext* format_ctx, AVCodecContext* codec_ctx,
int video_stream) {
AVPacket packet;
AVFrame* frame = av_frame_alloc();
int ret;

while (av_read_frame(format_ctx, &packet) >= 0) {
if (packet.stream_index == video_stream) {
ret = avcodec_send_packet(codec_ctx, &packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
av_packet_unref(&packet);
continue;
}

ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == 0) {
av_packet_unref(&packet);
return frame;
} else if (ret != AVERROR(EAGAIN)) {
break;
}
}
av_packet_unref(&packet);
}

av_frame_free(&frame);
return NULL;
}

// 提取指定时间点的帧
AVFrame* extract_frame_at_time(AVFormatContext* format_ctx,
AVCodecContext* codec_ctx, int video_stream,
double target_time) {
if (seek_to_time(format_ctx, codec_ctx, target_time - 0.5) < 0) {
return NULL;
}

AVFrame* best_frame = NULL;
double best_diff = DBL_MAX;
int frames_tried = 0;

// 解码多帧以找到最接近的帧
while (frames_tried < 30) { // 最多尝试30帧
AVFrame* frame = decode_frame(format_ctx, codec_ctx, video_stream);
if (!frame) break;

double frame_time = frame->best_effort_timestamp *
av_q2d(format_ctx->streams[video_stream]->time_base);
double diff = fabs(frame_time - target_time);

if (diff < best_diff) {
if (best_frame) av_frame_free(&best_frame);
best_frame = frame;
best_diff = diff;
} else {
av_frame_free(&frame);
// 如果时间已经超过目标时间,提前退出
if (frame_time > target_time + 0.1) break;
}

frames_tried++;
if (best_diff < 0.05) break; // 如果足够接近,提前退出
}

return best_frame;
}

// 将帧转换为RGB24格式(修复颜色问题)
AVFrame* convert_to_rgb(AVFrame* frame, AVCodecContext* codec_ctx) {
if (!frame || !codec_ctx) return NULL;

struct SwsContext* sws_ctx = sws_getContext(
frame->width, frame->height, codec_ctx->pix_fmt, frame->width,
frame->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

if (!sws_ctx) {
LOGE("无法创建转换上下文\n");
return NULL;
}

AVFrame* rgb_frame = av_frame_alloc();
rgb_frame->format = AV_PIX_FMT_RGB24;
rgb_frame->width = frame->width;
rgb_frame->height = frame->height;

if (av_frame_get_buffer(rgb_frame, 32) < 0) {
LOGE("无法分配RGB帧缓冲区\n");
av_frame_free(&rgb_frame);
sws_freeContext(sws_ctx);
return NULL;
}

// 执行颜色空间转换
sws_scale(sws_ctx, (const uint8_t* const*)frame->data, frame->linesize, 0,
frame->height, rgb_frame->data, rgb_frame->linesize);

sws_freeContext(sws_ctx);
return rgb_frame;
}

// 修复BGR到RGB的转换(如果需要)
void fix_bgr_to_rgb(AVFrame* frame) {
if (!frame || frame->format != AV_PIX_FMT_RGB24) return;

int width = frame->width;
int height = frame->height;
uint8_t* data = frame->data[0];
int line_size = frame->linesize[0];

for (int y = 0; y < height; y++) {
uint8_t* line = data + y * line_size;
for (int x = 0; x < width; x++) {
// 交换R和B通道
uint8_t temp = line[x * 3];
line[x * 3] = line[x * 3 + 2];
line[x * 3 + 2] = temp;
}
}
}

// 创建合并后的图片(修复布局和颜色问题)
AVFrame* create_grid_image(AVFrame** frames, int frame_count, int cols,
int rows) {
if (frame_count == 0 || frames[0] == NULL) return NULL;

int frame_width = frames[0]->width;
int frame_height = frames[0]->height;
int border = 5; // 边框大小

// 创建目标帧
AVFrame* grid_frame = av_frame_alloc();
grid_frame->format = AV_PIX_FMT_RGB24;
grid_frame->width = frame_width * cols + border * (cols + 1);
grid_frame->height = frame_height * rows + border * (rows + 1);

if (av_frame_get_buffer(grid_frame, 32) < 0) {
LOGE("无法分配网格帧缓冲区\n");
av_frame_free(&grid_frame);
return NULL;
}

// 填充白色背景
for (int y = 0; y < grid_frame->height; y++) {
uint8_t* line = grid_frame->data[0] + y * grid_frame->linesize[0];
for (int x = 0; x < grid_frame->width; x++) {
line[x * 3] = 255; // R
line[x * 3 + 1] = 255; // G
line[x * 3 + 2] = 255; // B
}
}

// 将各个帧复制到网格中
for (int i = 0; i < frame_count && i < cols * rows; i++) {
if (!frames[i]) continue;

int row = i / cols;
int col = i % cols;
int dest_x = border + col * (frame_width + border);
int dest_y = border + row * (frame_height + border);

// 复制图像数据
for (int y = 0; y < frame_height; y++) {
if (dest_y + y >= grid_frame->height) continue;

uint8_t* src_line = frames[i]->data[0] + y * frames[i]->linesize[0];
uint8_t* dest_line = grid_frame->data[0] +
(dest_y + y) * grid_frame->linesize[0] + dest_x * 3;

for (int x = 0; x < frame_width; x++) {
if (dest_x + x >= grid_frame->width) continue;

dest_line[x * 3] = src_line[x * 3]; // R
dest_line[x * 3 + 1] = src_line[x * 3 + 1]; // G
dest_line[x * 3 + 2] = src_line[x * 3 + 2]; // B
}
}
}

return grid_frame;
}

// 保存帧为PNG文件(使用FFmpeg编码器)
int save_frame_as_png(AVFrame* frame, FILE* output) {
AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_PNG);
if (!codec) {
LOGE("找不到PNG编码器\n");
return -1;
}

AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGE("无法分配编码器上下文\n");
return -1;
}

codec_ctx->width = frame->width;
codec_ctx->height = frame->height;
codec_ctx->pix_fmt = AV_PIX_FMT_RGB24;
codec_ctx->time_base = (AVRational){1, 25};

if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("无法打开PNG编码器\n");
avcodec_free_context(&codec_ctx);
return -1;
}

AVPacket* pkt = av_packet_alloc();
if (!pkt) {
LOGE("无法分配AVPacket\n");
avcodec_free_context(&codec_ctx);
return -1;
}

int ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0) {
LOGE("发送帧到编码器失败\n");
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
return -1;
}

ret = avcodec_receive_packet(codec_ctx, pkt);
if (ret < 0) {
LOGE("从编码器接收包失败\n");
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
return -1;
}

// 写入到标准输出
fwrite(pkt->data, 1, pkt->size, output);
fflush(output);

av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
return 0;
}

// 主函数
int main(int argc, char* argv[]) {
if (argc < 4) {
return 1;
}

const char* input_file = argv[1];
const int row = atoi(argv[2]);
const int col = atoi(argv[3]);
if (row <= 0 || col <= 0) {
LOGE("not correct params grid %dx%d", row, col);
return 1;
}
int frame_count = row * col;

init_ffmpeg();

// 打开视频文件
AVFormatContext* format_ctx = open_video(input_file);
if (!format_ctx) return 1;

// 查找视频流
int video_stream = find_video_stream(format_ctx);
if (video_stream < 0) {
LOGE("找不到视频流\n");
avformat_close_input(&format_ctx);
return 1;
}

// 获取视频时长
double duration = format_ctx->duration / (double)AV_TIME_BASE;
// printf("视频时长: %.2f秒\n", duration);

// 打开解码器
AVCodecContext* codec_ctx = open_decoder(format_ctx, video_stream);
if (!codec_ctx) {
avformat_close_input(&format_ctx);
return 1;
}

// 计算提取时间点(避免开头和结尾)
double start_time = duration * 0.05; // 从5%开始
double end_time = duration * 0.95; // 到95%结束
double effective_duration = end_time - start_time;
double time_interval = effective_duration / frame_count;

AVFrame** extracted_frames = calloc(frame_count, sizeof(AVFrame*));

// printf("开始提取帧...\n");

// 提取10个帧
for (int i = 0; i < frame_count; i++) {
double target_time = start_time + time_interval * i;
// printf("提取 %.2f秒的帧...\n", target_time);

AVFrame* frame =
extract_frame_at_time(format_ctx, codec_ctx, video_stream, target_time);
if (frame) {
extracted_frames[i] = convert_to_rgb(frame, codec_ctx);
if (extracted_frames[i]) {
// 修复颜色顺序(如果需要)
// fix_bgr_to_rgb(extracted_frames[i]);
}
av_frame_free(&frame);
}

if (!extracted_frames[i]) {
LOGE("无法提取第%d帧\n", i + 1);
}
}

// 创建网格图片
// printf("创建网格图片...\n");
AVFrame* grid_image =
create_grid_image(extracted_frames, row * col, col, row);

if (grid_image) {
// 保存结果
if (save_frame_as_png(grid_image, stdout) == 0) {
} else {
LOGE("保存文件失败\n");
return 1;
}
av_frame_free(&grid_image);
} else {
LOGE("无法创建网格图片\n");
return 0;
}

// 清理资源
for (int i = 0; i < frame_count; i++) {
if (extracted_frames[i]) av_frame_free(&extracted_frames[i]);
}

avcodec_free_context(&codec_ctx);
avformat_close_input(&format_ctx);
free(extracted_frames);
return 0;
}

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cmake_minimum_required(VERSION 3.10)
project(video_grid VERSION 1.0.0 LANGUAGES C)

# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# 查找PkgConfig
find_package(PkgConfig REQUIRED)

# 使用pkg-config查找FFmpeg库
pkg_check_modules(FFMPEG REQUIRED
libavcodec
libavformat
libavutil
libswscale
)

# 添加可执行文件
add_executable(video_grid video_grid.c)

# 设置包含目录和链接库
target_include_directories(video_grid PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_directories(video_grid PRIVATE ${FFMPEG_LIBRARY_DIRS})
target_link_libraries(video_grid PRIVATE ${FFMPEG_LIBRARIES} m)

Qt5/Cpp实现的代码

源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// main.cpp
#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QFileDialog>
#include <QListWidget>
#include <QLabel>
#include <QListWidgetItem>
#include <QMessageBox>
#include <QStatusBar>
#include <QByteArray>
#include <QPixmap>
#include <QProcess>
#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QDateTime>
#include <QDesktopServices>
#include <QUrl>
#include <QStyleFactory>
#include <QDebug>
#include <algorithm>

// 用于排序的比较函数
bool fileInfoLessThan(const QFileInfo &info1, const QFileInfo &info2) {
return info1.fileName() < info2.fileName();
}

class Mp4SnapTool : public QMainWindow {
Q_OBJECT

public:
Mp4SnapTool(QWidget *parent = nullptr) : QMainWindow(parent) {
initUI();
}

private slots:
void browseDirectory() {
QString directory = QFileDialog::getExistingDirectory(
this,
"选择目录",
pathInput->text().isEmpty() ? QDir::homePath() : pathInput->text()
);

if (!directory.isEmpty()) {
pathInput->setText(directory);
scanDirectory();
}
}

void scanDirectory() {
QString directory = pathInput->text().trimmed();

if (directory.isEmpty()) {
QMessageBox::warning(this, "警告", "请输入目录路径");
return;
}

QDir dir(directory);
if (!dir.exists()) {
QMessageBox::warning(this, "警告", QString("目录不存在: %1").arg(directory));
return;
}

try {
fileList->clear();

QStringList filters;
filters << "*.mp4" << "*.MP4";
dir.setNameFilters(filters);
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);

QFileInfoList files = dir.entryInfoList();

// 使用std::sort进行排序
std::sort(files.begin(), files.end(), fileInfoLessThan);

for (const QFileInfo &fileInfo : files) {
QListWidgetItem *item = new QListWidgetItem(fileInfo.fileName());
item->setData(Qt::UserRole, fileInfo.absoluteFilePath());
fileList->addItem(item);
}

fileCountLabel->setText(QString("文件数量: %1").arg(files.count()));
statusBar()->showMessage(QString("扫描完成: %1 个文件").arg(files.count()));

} catch (...) {
QMessageBox::critical(this, "错误", "扫描目录时发生错误");
}
}

void onFileClicked(QListWidgetItem *item) {
QString filePath = item->data(Qt::UserRole).toString();
QString fileName = item->text();
QString fileInfo = getFileInfo(filePath);
statusBar()->showMessage(QString("选中: %1 - %2").arg(fileName).arg(fileInfo));
}

void onFileDoubleClicked(QListWidgetItem *item) {
QString filePath = item->data(Qt::UserRole).toString();
QString fileName = item->text();

QByteArray pngData = getPngBinaryAdvanced(filePath);
showImageFromBinary(filePath, pngData, fileName);
scanDirectory();
}

private:
QLineEdit *pathInput;
QPushButton *browseButton;
QPushButton *scanButton;
QLabel *fileCountLabel;
QListWidget *fileList;

void initUI() {
setWindowTitle("MP4文件预览工具");
setGeometry(100, 100, 800, 600);

QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);

QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);

// 路径选择布局
QHBoxLayout *pathLayout = new QHBoxLayout();

QLabel *pathLabel = new QLabel("目录路径:");
pathInput = new QLineEdit();
pathInput->setPlaceholderText("请输入目录路径或点击浏览按钮选择...");

browseButton = new QPushButton("浏览...");
scanButton = new QPushButton("扫描");

connect(browseButton, &QPushButton::clicked, this, &Mp4SnapTool::browseDirectory);
connect(scanButton, &QPushButton::clicked, this, &Mp4SnapTool::scanDirectory);

pathLayout->addWidget(pathLabel);
pathLayout->addWidget(pathInput, 1);
pathLayout->addWidget(browseButton);
pathLayout->addWidget(scanButton);

// 文件列表布局
QVBoxLayout *fileListLayout = new QVBoxLayout();

fileCountLabel = new QLabel("文件数量: 0");

fileList = new QListWidget();
connect(fileList, &QListWidget::itemClicked, this, &Mp4SnapTool::onFileClicked);
connect(fileList, &QListWidget::itemDoubleClicked, this, &Mp4SnapTool::onFileDoubleClicked);

fileListLayout->addWidget(fileCountLabel);
fileListLayout->addWidget(fileList);

mainLayout->addLayout(pathLayout);
mainLayout->addLayout(fileListLayout);

statusBar()->showMessage("准备就绪");
}

QByteArray getPngBinaryAdvanced(const QString &filePath) {
QProcess process;
process.start("./video_grid", QStringList() << filePath << "3" << "3");

if (!process.waitForStarted()) {
qDebug() << "错误: 无法启动程序";
return QByteArray();
}

if (!process.waitForFinished(30000)) { // 30秒超时
qDebug() << "错误: 程序执行超时";
process.kill();
return QByteArray();
}

if (process.exitCode() != 0) {
qDebug() << "错误: 程序返回非零状态码" << process.exitCode();
qDebug() << "错误信息:" << process.readAllStandardError();
return QByteArray();
}

QByteArray output = process.readAllStandardOutput();
qDebug() << "获取PNG图片正常 长度:" << output.length();
return output;
}

void deleteFileToTrash(const QString &filePath) {
QFile file(filePath);
if (file.exists()) {
// 更安全的删除方式:使用系统回收站功能
#ifdef Q_OS_WIN
// Windows系统使用移动文件到回收站的方式
QString trashPath = QDir::homePath() + "/.local/share/Trash/files/" + QFileInfo(filePath).fileName();
if (QFile::copy(filePath, trashPath)) {
file.remove();
qDebug() << "文件已移动到回收站:" << filePath;
} else {
qDebug() << "删除失败:" << filePath;
}
#elif defined(Q_OS_LINUX)
if (!file.moveToTrash()) {
qDebug() << "移动失败:" << filePath << "-" << file.errorString();
} else {
qDebug() << "移动成功:" << filePath;
}
#else
// 其他系统:提示用户手动删除
QMessageBox::warning(this, "删除文件",
QString("出于安全考虑,请手动删除文件:\n%1").arg(filePath));
#endif
} else {
qDebug() << "文件不存在:" << filePath;
}
}

void showImageFromBinary(const QString &filePath, const QByteArray &binaryData, const QString &title) {
if (binaryData.isEmpty()) {
QMessageBox::warning(this, "错误", "无法生成预览图像");
return;
}

QPixmap pixmap;
if (!pixmap.loadFromData(binaryData)) {
QMessageBox errorMsg;
errorMsg.setIcon(QMessageBox::Critical);
errorMsg.setWindowTitle("错误");
errorMsg.setText("无法加载图像数据");
errorMsg.setInformativeText("请检查数据是否为有效的PNG格式");
errorMsg.exec();
return;
}

QMessageBox msgBox;
msgBox.setWindowTitle(title);

QPixmap scaledPixmap = pixmap.scaled(800, 800, Qt::KeepAspectRatio, Qt::SmoothTransformation);
msgBox.setIconPixmap(scaledPixmap);

msgBox.addButton(QMessageBox::No);
QPushButton *delBtn = msgBox.addButton("删除", QMessageBox::ActionRole);

// 设置删除按钮样式
delBtn->setStyleSheet(
"QPushButton {"
" min-width: 60px;"
" min-height: 25px;"
" color: red;"
" font-weight: bold;"
" background-color: white;"
" border: 1px solid #ccc;"
" border-radius: 3px;"
"}"
"QPushButton:hover {"
" background-color: #ffe6e6;"
" border-color: #ff9999;"
"}"
"QPushButton:pressed {"
" background-color: #ffcccc;"
"}"
);

msgBox.exec();

if (msgBox.clickedButton() == delBtn) {
qDebug() << "删除文件:" << filePath;
deleteFileToTrash(filePath);
}
}

QString getFileInfo(const QString &filePath) {
QFileInfo fileInfo(filePath);
if (fileInfo.exists()) {
qint64 size = fileInfo.size();
QString sizeStr;

if (size < 1024) {
sizeStr = QString("%1 B").arg(size);
} else if (size < 1024 * 1024) {
sizeStr = QString("%1 KB").arg(size / 1024.0, 0, 'f', 2);
} else {
sizeStr = QString("%1 MB").arg(size / (1024.0 * 1024.0), 0, 'f', 2);
}

return QString("大小: %1").arg(sizeStr);
}
return "大小: 未知";
}
};

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

app.setStyle(QStyleFactory::create("Fusion"));

Mp4SnapTool window;
window.show();

return app.exec();
}

#include "main.moc"

对应的pro文件

1
2
3
4
5
6
7
8
9
# Mp4SnapTool.pro
QT += core gui widgets

CONFIG += c++17

TARGET = Mp4SnapTool
TEMPLATE = app

SOURCES += main.cpp

编译链接指令

1
2
3
qmake Mp4SnapTool.pro
make
./Mp4SnapTool

Share 

 Previous post: 大模型部署学习 Next post: 软件窗口贴边 

© 2026 long

Theme Typography by Makito

Proudly published with Hexo