<think>我们之前的主程序代码中,开始转换按钮在初始时被设置为禁用(disabled=True),只有当用户选择了有效的输入视频文件后才会启用。
因此,如果按钮一直是灰色的,可能是因为没有正确选择视频文件,或者选择文件后没有触发启用按钮的逻辑。
回顾代码,在事件循环中,当事件为`-INPUT-`(即输入文件路径发生变化)时,我们会尝试加载视频文件并获取其分辨率,然后启用按钮。
如果视频文件加载失败(例如文件格式不支持、文件损坏等),按钮将保持禁用状态。
可能的原因及解决方案:
1. 文件路径无效:确保选择的文件存在且路径正确。
2. 视频文件格式不支持:虽然我们指定了常见的视频格式,但某些特殊格式可能不被moviepy支持。可以尝试其他格式。
3. 异常处理:在加载视频时发生异常,导致按钮没有启用。我们在异常处理中弹出了错误提示,但按钮状态没有改变(仍然是禁用)。
在代码中,当加载视频失败时,我们更新了状态文本,但按钮的禁用状态没有改变(仍然为初始的禁用状态)。因此,我们需要在加载失败时也保持按钮禁用,这是符合逻辑的。
但是,如果用户重新选择了一个有效的文件,应该会再次触发`-INPUT-`事件,并尝试加载。如果新文件有效,按钮应该被启用。
为了调试,我们可以检查在加载视频文件时是否发生了异常,以及异常信息。
在代码中,当加载视频失败时,我们执行了:
```python
sg.popup_error(f"无法读取视频文件: {str(e)}")
window['-STATUS-'].update('文件加载失败')
```
因此,如果文件加载失败,用户会看到错误弹窗,并且状态栏显示“文件加载失败”。同时,按钮保持禁用(因为只有在加载成功时才启用按钮)。
所以,如果按钮一直是灰色的,请检查:
- 是否选择了正确的视频文件?
- 是否出现了错误弹窗提示?
如果用户没有看到错误弹窗,那么可能是没有触发`-INPUT-`事件,或者事件处理逻辑没有执行到加载视频的部分。
我们可以在事件处理中添加一些调试输出,或者确保文件浏览后输入框确实被更新了。
另外,注意在开发环境中,路径问题也可能导致文件加载失败(例如相对路径不正确)。建议在调试时打印出文件路径,并检查该路径是否存在。
在代码中,我们使用`os.path.exists(input_path)`来检查文件是否存在,如果不存在,就不会尝试加载。因此,确保文件存在是第一步。
如果问题仍然存在,我们可以尝试以下步骤:
1. 在事件处理中打印出`input_path`和`os.path.exists(input_path)`的结果。
2. 检查moviepy是否支持该视频格式(可以尝试用moviepy在独立脚本中打开该文件)。
修改代码,添加调试信息(在开发时):
在`event == '-INPUT-'`的处理块中,添加:
```python
elif event == '-INPUT-':
input_path = values['-INPUT-']
print(f"选择的文件: {input_path}") # 调试信息
print(f"文件存在: {os.path.exists(input_path)}") # 调试信息
... # 后续代码
```
这样可以在控制台看到文件路径和是否存在。
另外,注意在打包成exe后,文件路径可能包含空格或特殊字符,也可能导致问题。确保路径用双引号括起来(但通常Python可以处理)。
如果以上都正常,那么可能是moviepy在读取该视频时出错。可以尝试用moviepy的VideoFileClip直接打开文件,看是否抛出异常。
由于我们无法直接修改用户的环境,因此我们可以在异常处理中提供更详细的错误信息,并允许用户即使加载失败也可以尝试转换(但这样可能不可取,因为不知道原始分辨率,无法保持宽高比)。
另一种方案:即使无法读取视频,也允许用户手动输入分辨率,并启用转换按钮。但这样会失去保持宽高比的功能。
我们可以修改逻辑:当用户选择文件后,即使无法读取视频,也启用转换按钮,但弹出警告,让用户手动输入分辨率。
修改代码:
在`event == '-INPUT-'`的处理块中,当加载失败时,我们仍然启用按钮,但提示用户手动输入分辨率。
```python
elif event == '-INPUT-':
input_path = values['-INPUT-']
if input_path and os.path.exists(input_path):
try:
# 尝试加载视频
... # 成功加载,设置分辨率并启用按钮
except Exception as e:
sg.popup_error(f"无法读取视频文件: {str(e)}\n请手动输入分辨率。")
# 启用按钮,但要求用户手动输入分辨率
window['-CONVERT-'].update(disabled=False)
window['-STATUS-'].update('文件加载失败,请手动输入分辨率')
else:
# 文件不存在,禁用按钮
window['-CONVERT-'].update(disabled=True)
window['-STATUS-'].update('文件不存在')
```
这样,即使加载失败,用户仍然可以手动输入分辨率并尝试转换。
但是,请注意,如果用户没有手动输入分辨率,我们仍然需要在转换时验证输入。
在转换事件中,我们已经对宽度和高度进行了整数转换的验证。
因此,我们可以这样修改。
下面是修改后的事件处理代码(针对`-INPUT-`事件):</think># 解决"开始转换"按钮灰色不可点击问题
"开始转换"按钮灰色不可点击通常是由于程序逻辑中设置的条件未满足导致的。在之前的代码中,按钮在以下情况下会被禁用:
1. 用户未选择有效的输入视频文件
2. 程序未能成功读取视频信息
3. 程序逻辑错误导致按钮状态未正确更新
## 问题分析与解决方案
### 1. 检查文件路径处理逻辑
在主程序代码中,文件路径处理是按钮启用/禁用的关键:
```python
elif event == '-INPUT-':
# 当选择输入文件时,尝试获取原始分辨率
input_path = values['-INPUT-']
if input_path and os.path.exists(input_path):
try:
# ... 成功读取视频信息 ...
window['-CONVERT-'].update(disabled=False)
except Exception as e:
# ... 处理异常 ...
else:
# 文件不存在或路径无效
window['-CONVERT-'].update(disabled=True)
```
**常见问题:**
- 文件路径包含特殊字符或空格
- 相对路径处理不当
- 文件权限问题导致无法访问
### 2. 优化后的文件路径处理代码
以下是修复后的文件路径处理逻辑:
```python
elif event == '-INPUT-':
# 当选择输入文件时,尝试获取原始分辨率
input_path = values['-INPUT-']
if input_path:
try:
# 标准化路径处理
input_path = os.path.normpath(input_path)
if not os.path.exists(input_path):
raise FileNotFoundError(f"文件不存在: {input_path}")
if not os.access(input_path, os.R_OK):
raise PermissionError(f"没有读取权限: {input_path}")
# 检查文件扩展名
valid_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv']
_, ext = os.path.splitext(input_path)
if ext.lower() not in valid_extensions:
raise ValueError(f"不支持的文件格式: {ext}")
# 尝试加载视频
video = VideoFileClip(input_path)
original_width, original_height = video.size
video.close()
# 更新UI
window['-WIDTH-'].update(original_width)
window['-HEIGHT-'].update(original_height)
# 设置默认输出文件名
input_filename = os.path.basename(input_path)
name, ext = os.path.splitext(input_filename)
output_filename = f"{name}_resized{ext}"
window['-OUTPUT_NAME-'].update(output_filename)
# 启用转换按钮
window['-CONVERT-'].update(disabled=False)
window['-STATUS-'].update('文件已加载,准备转换')
except Exception as e:
error_msg = f"无法读取视频文件: {str(e)}"
sg.popup_error(error_msg)
window['-STATUS-'].update(error_msg)
window['-CONVERT-'].update(disabled=True)
else:
window['-CONVERT-'].update(disabled=True)
window['-STATUS-'].update('请选择视频文件')
```
### 3. 完整的修复后主程序代码
```python
import os
import sys
import subprocess
import threading
import psutil
import PySimpleGUI as sg
from moviepy.editor import VideoFileClip
def resource_path(relative_path):
"""获取资源的绝对路径,用于PyInstaller打包后定位资源文件"""
if hasattr(sys, '_MEIPASS'):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def get_cpu_info():
"""获取CPU信息并推荐线程数"""
try:
logical_cores = psutil.cpu_count(logical=True)
physical_cores = psutil.cpu_count(logical=False)
recommended_threads = min((physical_cores or logical_cores) + 1, 16)
return logical_cores, physical_cores, recommended_threads
except:
return 4, 4, 4 # 默认值
def resize_video(input_path, output_path, width, height, threads=4):
"""使用FFmpeg调整视频分辨率并隐藏命令行窗口"""
try:
# 获取FFmpeg路径(优先使用打包后资源路径)
ffmpeg_path = resource_path("bin/ffmpeg.exe")
# 如果不在打包环境中,使用本地路径
if not os.path.exists(ffmpeg_path):
ffmpeg_path = os.path.join(os.path.dirname(__file__), "..", "bin", "ffmpeg.exe")
# 构建FFmpeg命令
cmd = [
ffmpeg_path,
'-i', input_path,
'-vf', f'scale={width}:{height}',
'-c:v', 'libx264',
'-preset', 'medium',
'-crf', '23',
'-c:a', 'aac',
'-threads', str(threads),
'-y',
output_path
]
# 执行命令并隐藏窗口
startupinfo = None
if sys.platform == 'win32':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
creationflags=subprocess.CREATE_NO_WINDOW
)
# 等待进程完成
stdout, stderr = process.communicate()
if process.returncode != 0:
error_msg = stderr.decode('utf-8', errors='ignore') if stderr else f"FFmpeg返回错误代码: {process.returncode}"
return False, f"FFmpeg处理失败: {error_msg}"
return True, "视频处理成功!"
except Exception as e:
return False, f"处理视频时出错: {str(e)}"
def main():
# 获取CPU信息
logical_cores, physical_cores, recommended_threads = get_cpu_info()
# 设置主题和字体
sg.theme('DarkBlue14')
sg.set_options(font=('Helvetica', 10))
# 布局定义
layout = [
[sg.Text('视频分辨率调整工具', font=('Helvetica', 16, 'bold'), justification='center', expand_x=True)],
[sg.HorizontalSeparator()],
[
sg.Column([
[sg.Text('输入视频文件:', size=(15, 1)),
sg.Input(key='-INPUT-', expand_x=True),
sg.FileBrowse(file_types=(("视频文件", "*.mp4 *.avi *.mov *.mkv *.flv *.wmv"),))],
[sg.Text('输出文件夹:', size=(15, 1)),
sg.Input(key='-OUTPUT_DIR-', expand_x=True),
sg.FolderBrowse()],
[sg.Text('输出文件名:', size=(15, 1)),
sg.Input(key='-OUTPUT_NAME-', expand_x=True)],
], expand_x=True)
],
[sg.HorizontalSeparator()],
[
sg.Column([
[sg.Text('目标分辨率:', font=('Helvetica', 11, 'bold'))],
[
sg.Text('宽度:', size=(8, 1)),
sg.Input(key='-WIDTH-', size=(8, 1)),
sg.Text('高度:', size=(8, 1)),
sg.Input(key='-HEIGHT-', size=(8, 1)),
],
[sg.Checkbox('保持原始宽高比', key='-KEEP_ASPECT-', default=True)],
]),
sg.VerticalSeparator(),
sg.Column([
[sg.Text('线程设置:', font=('Helvetica', 11, 'bold'))],
[
sg.Text('线程数:'),
sg.Combo(
values=[str(i) for i in range(1, 17)],
default_value=str(recommended_threads),
key='-THREADS-',
size=(3, 1),
readonly=True
),
sg.Text(f'推荐: {recommended_threads} (物理核心: {physical_cores}, 逻辑核心: {logical_cores})')
],
[sg.Text('性能提示:'),
sg.Text('更多线程可加速处理但增加CPU负载', text_color='yellow')]
])
],
[sg.HorizontalSeparator()],
[
sg.ProgressBar(100, orientation='h', size=(40, 20), key='-PROGRESS-', bar_color=('green', 'gray'), visible=False),
sg.Text('准备就绪', key='-STATUS-', size=(30, 1), justification='right', text_color='lightblue')
],
[
sg.Button('开始转换', key='-CONVERT-', button_color=('white', 'green'), size=(10, 1), disabled=True),
sg.Button('退出', button_color=('white', 'firebrick3'), size=(10, 1)),
sg.Push()
]
]
# 创建窗口
window = sg.Window('视频分辨率调整工具', layout, resizable=True, finalize=True)
window.set_min_size((600, 400))
# 事件循环
original_width, original_height = None, None
conversion_active = False
while True:
event, values = window.read(timeout=100)
if event in (sg.WIN_CLOSED, '退出'):
if conversion_active:
sg.popup_quick_message('请等待当前任务完成', background_color='red')
continue
break
elif event == '-INPUT-':
# 当选择输入文件时,尝试获取原始分辨率
input_path = values['-INPUT-']
if input_path:
try:
# 标准化路径处理
input_path = os.path.normpath(input_path)
if not os.path.exists(input_path):
raise FileNotFoundError(f"文件不存在: {input_path}")
if not os.access(input_path, os.R_OK):
raise PermissionError(f"没有读取权限: {input_path}")
# 检查文件扩展名
valid_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv']
_, ext = os.path.splitext(input_path)
if ext.lower() not in valid_extensions:
raise ValueError(f"不支持的文件格式: {ext}")
# 尝试加载视频
video = VideoFileClip(input_path)
original_width, original_height = video.size
video.close()
# 更新UI
window['-WIDTH-'].update(original_width)
window['-HEIGHT-'].update(original_height)
# 设置默认输出文件名
input_filename = os.path.basename(input_path)
name, ext = os.path.splitext(input_filename)
output_filename = f"{name}_resized{ext}"
window['-OUTPUT_NAME-'].update(output_filename)
# 启用转换按钮
window['-CONVERT-'].update(disabled=False)
window['-STATUS-'].update('文件已加载,准备转换')
except Exception as e:
error_msg = f"无法读取视频文件: {str(e)}"
sg.popup_error(error_msg)
window['-STATUS-'].update(error_msg)
window['-CONVERT-'].update(disabled=True)
else:
window['-CONVERT-'].update(disabled=True)
window['-STATUS-'].update('请选择视频文件')
elif event == '-KEEP_ASPECT-':
# 当切换"保持宽高比"复选框时
if values['-KEEP_ASPECT-'] and original_width and original_height:
try:
new_width = int(values['-WIDTH-'] or original_width)
new_height = int(new_width * original_height / original_width)
window['-HEIGHT-'].update(new_height)
except:
pass
elif event == '-WIDTH-' and values['-KEEP_ASPECT-'] and original_width and original_height:
try:
new_width = int(values['-WIDTH-'] or original_width)
new_height = int(new_width * original_height / original_width)
window['-HEIGHT-'].update(new_height)
except:
pass
elif event == '-CONVERT-':
if conversion_active:
sg.popup_quick_message('请等待当前任务完成', background_color='red')
continue
# 开始转换过程
input_path = values['-INPUT-']
output_dir = values['-OUTPUT_DIR-'] or os.path.dirname(input_path)
output_name = values['-OUTPUT_NAME-'] or f"resized_{os.path.basename(input_path)}"
output_path = os.path.join(output_dir, output_name)
# 验证输入
if not input_path or not os.path.exists(input_path):
sg.popup_error("请输入有效的视频文件!")
continue
try:
width = int(values['-WIDTH-'])
height = int(values['-HEIGHT-'])
threads = int(values['-THREADS-'])
except ValueError:
sg.popup_error("请输入有效的数值!")
continue
# 检查输出目录
if not os.path.exists(output_dir):
try:
os.makedirs(output_dir)
except Exception as e:
sg.popup_error(f"无法创建输出目录: {str(e)}")
continue
# 设置状态
conversion_active = True
window['-CONVERT-'].update(disabled=True, text='处理中...')
window['-STATUS-'].update('正在处理视频...')
window['-PROGRESS-'].update(0, visible=True)
# 在单独的线程中执行转换
def conversion_thread():
try:
# 模拟进度更新
for i in range(1, 101):
window.write_event_value('-PROGRESS_UPDATE-', i)
threading.Event().wait(0.05)
# 实际转换
success, message = resize_video(input_path, output_path, width, height, threads)
if success:
window.write_event_value('-CONVERT_DONE-', (True, message, output_path))
else:
window.write_event_value('-CONVERT_DONE-', (False, message, None))
except Exception as e:
window.write_event_value('-CONVERT_DONE-', (False, f"线程错误: {str(e)}", None))
threading.Thread(target=conversion_thread, daemon=True).start()
elif event == '-PROGRESS_UPDATE-':
# 更新进度条
progress = values[event]
window['-PROGRESS-'].update(progress)
window['-STATUS-'].update(f'处理中... {progress}%')
elif event == '-CONVERT_DONE-':
# 转换完成
success, message, output_path = values[event]
conversion_active = False
window['-CONVERT-'].update(disabled=False, text='开始转换')
window['-PROGRESS-'].update(visible=False)
if success:
window['-STATUS-'].update("转换完成!")
sg.popup("成功", message, f"输出文件: {output_path}")
else:
window['-STATUS-'].update("转换失败!")
sg.popup_error("错误", message)
window.close()
if __name__ == '__main__':
main()
```
## 主要修复点
1. **增强的文件路径验证**:
- 使用 `os.path.normpath()` 标准化路径
- 添加文件存在性检查 `os.path.exists()`
- 添加文件可读性检查 `os.access()`
- 添加文件格式验证
2. **更详细的错误处理**:
- 区分不同类型的错误(文件不存在、无权限、格式不支持等)
- 在状态栏显示具体的错误信息
- 使用弹出窗口显示详细错误
3. **按钮状态管理优化**:
- 当未选择文件时明确禁用按钮
- 在文件加载失败时保持按钮禁用状态
- 添加更详细的状态提示
4. **异常处理增强**:
- 捕获所有可能的异常
- 提供更友好的错误信息
- 防止程序因异常而崩溃
## 调试建议
如果修复后问题仍然存在,可以添加调试代码:
```python
# 在文件加载部分添加调试输出
print(f"尝试加载文件: {input_path}")
print(f"文件存在: {os.path.exists(input_path)}")
print(f"文件可读: {os.access(input_path, os.R_OK)}")
# 在按钮状态变化时添加调试
print(f"按钮状态变化: disabled={window['-CONVERT-'].Disabled}")
```
## 常见问题排查
1. **文件路径问题**:
- 确保路径不包含中文或特殊字符
- 尝试使用绝对路径而非相对路径
- 检查文件权限
2. **依赖问题**:
- 确保安装了所有依赖:`pip install -r requirements.txt`
- 检查FFmpeg是否正确放置
3. **视频格式问题**:
- 尝试使用常见的MP4文件测试
- 确保安装了正确的编解码器
4. **环境问题**:
- 尝试在干净的虚拟环境中运行
- 确保Python版本为3.7+