从命令行小白到GUI开发者:我用Trae 2.0开发Robocopy可视化工具的全过程

259 阅读39分钟

从命令行小白到GUI开发者:我用Trae 2.0开发Robocopy可视化工具的全过程

问题背景与工具选择

在日常办公中,文件复制是高频需求,但 Windows 系统自带的 Robocopy 命令行工具却常常让普通用户望而却步。作为每天需要处理大量项目文件备份的职场人,我曾无数次在命令提示符窗口前陷入困境——明明只是想把文件夹从 D 盘复制到移动硬盘,却要先回忆 /E(复制子目录)、/Z(重启模式)这些参数的具体含义,稍有疏漏就可能导致复制不完整。更让人焦虑的是,当复制几十个 G 的设计文件时,屏幕上只有滚动的字符流,完全无法判断进度,只能干等着命令行结束的提示音。

Robocopy 命令行工具的三大核心痛点

  • 参数记忆负担:内置超过 80 个参数,专业用户都需频繁查阅文档,普通用户难以掌握
  • 操作流程繁琐:需手动打开命令提示符、输入完整路径和参数,多步骤操作易出错
  • 进度感知缺失:无图形化进度条,无法直观判断复制速度、剩余时间及异常状态

作为一名编程基础薄弱的"小白",我迫切需要一套能降低技术门槛的解决方案:既能解决 Robocopy 的可视化问题,又不需要深入学习复杂的 GUI 开发知识。这时,两个工具的组合进入了我的视线。

Trae 2.0 成为开发助手的核心原因,在于其 AI 驱动的双引擎支持:代码生成功能能根据自然语言描述直接产出基础框架,比如我输入"创建一个包含源路径选择按钮的窗口",它就能自动生成 PyQt5 的界面代码;而调试指导功能更像实时导师,当我因语法错误卡壳时,它会定位问题行并给出修改建议,甚至解释"信号与槽"机制的底层逻辑。这种"边学边做"的模式,让我这种非科班出身的开发者也能快速上手。

PyQt5 则完美适配了跨平台 GUI 的开发需求。它基于 Python 语言,语法简洁易读,且提供了丰富的预制组件(如文件选择对话框、进度条、表格控件),几乎不需要从零构建界面元素。更重要的是,用 PyQt5 开发的程序能直接运行在 Windows、macOS 和 Linux 系统上,这意味着我开发的工具不仅能解决自己的问题,还能分享给团队中使用不同设备的同事。

从 Robocopy 的使用痛点出发,到明确"低门槛开发跨平台可视化工具"的核心需求,再到选择 Trae 2.0 与 PyQt5 的技术组合,这个"问题-需求-解决方案"的逻辑闭环,让整个开发计划从一开始就具备了清晰的目标和可行性。接下来,我将带你一步步拆解如何用这两个工具将想法变成现实。(界面截图如下)

局部截取_20250830_133713.png

开发全过程:从0到1的Trae协作

需求分析:用自然语言向Trae“提需求”

作为一个只会复制粘贴命令的“命令行小白”,要把Robocopy这个黑框工具变成可视化程序,我首先卡在了“该让它具备什么功能”这个基础问题上。对着空白的记事本发呆时突然想到:为什么不直接用说人话的方式,让Trae帮我梳理需求呢?

于是我打开对话框,像和同事聊天一样敲下了办公场景里最真实的痛点:“每次用Robocopy复制文件都要记一堆参数,能不能让工具自己选文件夹啊?就像平时用资源管理器那样点一点就行。还有复制大文件的时候,黑框里滚一堆字根本不知道到哪了,最好能有个进度条什么的。”这些完全没有技术术语的描述,没想到Trae秒懂了。

我(小白提问):“核心功能就是替代命令行操作Robocopy,所以得有选文件夹的功能,不用手动输路径。然后复制的时候得知道跑到哪了,比如传了多少文件、还剩多少,最好能看到速度。对了,如果复制失败能不能告诉我为啥?”

Trae回复:“根据你的描述,我们可以把需求拆解为3个核心模块和对应功能点:

  1. 可视化文件选择模块
    • 源目录选择:通过图形界面的‘浏览’按钮打开文件夹选择对话框
    • 目标目录选择:支持一键选择或新建目标文件夹,自动校验路径合法性
  2. 实时进度监控模块
    • 进度条显示:直观展示整体复制进度(按文件数/大小双重维度)
    • 动态数据面板:实时更新已复制文件数/总文件数、传输速度(MB/s)、预计剩余时间
  3. 异常处理与反馈模块
    • 错误提示系统:当目标目录无权限、源文件被占用等情况时,弹窗显示通俗解释(如‘目标文件夹需要管理员权限,请右键以管理员身份运行’)
    • 日志记录功能:自动保存复制过程日志,支持导出查看详细错误信息”

看着Trae输出的结构化功能清单,我突然意识到AI最神奇的不是生成代码,而是能把“我想要XXX”这种模糊的感受,精准“翻译”成开发者能看懂的技术语言。原本可能需要翻文档、画原型图才能理清楚的需求,现在通过5分钟对话就形成了可执行的开发蓝图——这种“说人话就能推进项目”的协作方式,大概就是小白也能跨界开发的底气吧。

界面设计:Trae生成PyQt5代码初稿

作为一个连PS都只会裁剪图片的纯后端选手,UI设计曾是我开发路上的“拦路虎”。每次看到那些按钮对齐、颜色搭配的教程,都感觉像在看“火星文”——直到我试着向Trae抛出了一个朴素的需求:“帮我生成一个Robocopy可视化工具的界面,要简洁布局,至少得有源目录选择按钮和进度显示条,别太复杂,我怕看不懂。”

让我意外的是,10秒后Trae返回的不是设计图,而是一段带着详细注释的PyQt5代码。最关键的是,这些代码像“带拼音的课文”一样好懂:窗口初始化部分用QMainWindow创建主界面,控件布局用QVBoxLayout纵向排列元素,连按钮的点击事件都标好了“这里可以绑定目录选择功能”。比如这段窗口设置代码,连我这种小白都能一眼看出每个参数的作用:

# 创建主窗口
self.setWindowTitle("Robocopy 可视化工具")  # 设置窗口标题
self.setGeometry(100, 100, 600, 400)  # 窗口位置(100,100),大小600x400像素
central_widget = QWidget()
self.setCentralWidget(central_widget)

# 创建布局管理器
layout = QVBoxLayout(central_widget)  # 垂直布局,控件上下排列
layout.setContentsMargins(20, 20, 20, 20)  # 控件与窗口边缘的间距

更惊喜的是代码的“可修改性”。Trae似乎预判了我这种新手的需求,特意在注释里标注了“可调整区域”:比如想让按钮更大,就改setMinimumHeight(50)里的数字;想换个主题色,加一句setStyleSheet就能实现。我试着把进度条颜色从默认的灰色改成了醒目的蓝绿色,只花了3分钟——这要是以前查文档,可能半小时都搞不定控件样式表的语法。

新手友好的3个关键设计

  1. 注释比代码还多:每个控件功能、布局逻辑都有中文说明,相当于自带“入门教程”
  2. 最小化依赖:只用到PyQt5核心模块,避免复杂第三方库增加学习负担
  3. 预留修改入口:尺寸、颜色等样式参数单独标出,不用通读代码就能定位修改点

现在回头看,Trae最厉害的不是“帮我写了代码”,而是把“UI开发”这个曾经需要系统学习的技能,拆解成了“填空式修改”。就像搭积木时,AI已经帮你拼好了基础框架,你只需要根据喜好调整细节——这种“降维”的技术普惠,或许就是AI工具对普通开发者最珍贵的价值。

功能实现:从命令行调用到逻辑串联

在开发 Robocopy 可视化工具时,我将核心功能拆解为「命令行调用封装」和「进度实时显示」两大模块。作为命令行小白,这两个模块曾让我望而却步,但在 Trae 2.0 的引导下,原本复杂的技术逻辑被拆解成了一步步可操作的代码实践。

命令行调用封装:让参数拼接不再是「猜谜游戏」

Robocopy 的命令行参数多达数十个(如 /MIR 镜像复制、/XA:H 排除隐藏文件),手动拼接不仅容易出错,还需要熟记各种参数组合规则。Trae 首先帮我理清了参数逻辑,建议通过 Python 的 subprocess 模块实现调用,并设计了参数映射机制。

参数拼接三步法

  1. 用户输入转义:将 GUI 中的「复制模式」(如镜像/增量)映射为 Robocopy 参数(/MIR/E
  2. 条件参数过滤:排除文件类型(如 .tmp.log)自动转换为 /XF *.tmp *.log 格式
  3. 安全校验:Trae 生成的代码会检查路径合法性(如目标文件夹是否存在),避免命令执行失败

例如,当用户选择「增量复制」并排除 .txt 文件时,Trae 生成的代码会自动拼接出:
robocopy "C:\源文件夹" "D:\备份" /E /XF *.txt /R:3 /W:5
(其中 /R:3/W:5 是 Trae 建议的重试策略,防止因文件占用导致复制中断)

进度实时显示:用多线程破解「假死」难题

最初直接在主线程调用 Robocopy 时,整个界面会卡住不动——这是 GUI 开发的经典坑点。Trae 给出的解决方案简洁明了:用多线程分离命令执行与进度更新

多线程实现要点

  • 主线程:负责 UI 交互(按钮点击、路径输入)
  • 子线程:单独运行 Robocopy 命令,通过管道捕获输出日志
  • 信号机制:子线程解析日志后,通过 threading.Event() 通知主线程更新进度条

日志解析则用到了正则表达式。Trae 分析 Robocopy 输出格式后,生成了匹配进度的正则表达式:
r'(\d+)%\s+已复制'
(该表达式能从「50% 已复制」这样的日志行中提取进度百分比)。子线程每捕获到新日志,就用这个表达式提取数值,再通过信号传递给主线程的进度条控件。

整个过程中,最让我惊喜的是 Trae 将「多线程同步」「正则表达式调试」这些曾让我头疼的概念,转化为了带注释的代码片段。比如它会特意标注:「这里用 queue 传递日志数据比全局变量更安全」,让我在复制粘贴代码时也能理解背后的逻辑。

从对着命令行参数文档发呆,到能独立调试线程同步问题,这个过程让我真切感受到:AI 不是替你写代码,而是帮你把技术手册翻译成行动指南。下一章我会分享界面设计中如何平衡美观与实用,以及那些「看似简单却卡了三天」的细节优化。

调试优化:Trae陪我解决“卡壳时刻”

开发过程中遇到的几个“卡壳”场景,让我真切体会到AI工具作为“实时导师”的价值。从进度条纹丝不动到中文路径复制失败,这些新手常踩的坑,在Trae的辅助下都高效突破了。

第一个拦路虎是进度条不刷新。首次运行工具时,点击“开始复制”后界面瞬间凝固,进度条卡在0%一动不动。我盯着屏幕慌了神:“明明代码没报错,怎么界面卡住了?难道是程序崩了?”赶紧把代码片段和现象截图发给Trae,它秒回的调试思路让我豁然开朗:“UI操作需在主线程,耗时任务用子线程——你把复制任务直接放主线程了,当然会阻塞界面更新。”按照提示重构代码,用子线程处理复制逻辑,主线程只负责进度条刷新,再次运行时进度条终于开始平滑跳动,那一刻的成就感至今记得。

紧接着又遇到中文路径复制失败。测试含“文档资料”“工作汇报”等中文文件夹时,日志总显示“路径不存在”。我反复核对路径拼写,甚至手动复制路径粘贴,问题依旧。带着“中文路径为什么不识别”的困惑咨询Trae,它直指核心:“Windows系统需用unicode编码处理路径,默认编码可能导致中文乱码。”添加unicode编码转换后,中文文件夹顺利复制成功,原来差的就是这关键一步编码转换。

最棘手的是调用Robocopy后界面持续卡顿。虽然用了子线程,但复制大文件时界面还是会间歇性冻结,进度条卡顿严重。Trae这次给出了更具体的方案:“使用QThread实现后台复制,通过信号槽机制让主线程更新进度条。”它不仅解释了“后台线程负责复制+主线程专注UI”的原理,还附上了信号定义和线程调用的示例代码。照着实现后,界面彻底摆脱卡顿,即使复制几十GB文件,进度条也能实时刷新,操作其他按钮也丝滑流畅。

回想如果纯靠自己解决,每个问题可能都要花1-2小时查文档、试错:进度条问题可能要翻遍Qt文档的线程章节,中文路径报错可能得调试编码转换函数,界面卡顿甚至可能怀疑是电脑配置问题。但在Trae辅助下,每个问题平均20分钟就搞定——它像个有问必答的导师,直接点出症结并给出行得通的方案。这种“即时反馈+精准指导”的模式,让新手也能绕过“盲人摸象”的阶段,把精力集中在创造性开发上。

新手调试小贴士:遇到UI卡顿、进度条不更新等问题,优先检查是否犯了“耗时任务阻塞主线程”的经典错误。记住Trae的黄金法则:UI操作走主线程,复制、计算等耗时任务用子线程/后台线程,两者通过信号槽或事件机制通信,就能兼顾效率与流畅度。

这种“提问-解惑-实践”的闭环,比传统查资料更高效,也让我对“AI提升开发者生产力”有了切身体会——它不是替代思考,而是帮你在卡壳时快速找到突破方向,让学习曲线不再陡峭。

成果展示与功能亮点

每天和文件打交道的办公族,是不是也曾被 Robocopy 的命令行参数搞得头大?“/E”和“/S”到底哪个能复制子目录?“/Z”断点续传要怎么写进命令?现在这些烦恼终于有了可视化解决方案——我们基于 PyQt5 开发的 Robocopy 图形工具,把黑框里的复杂命令变成了“点一点”就能操作的界面,让文件复制从此告别参数手册。

极简线框图1.jpg

界面设计:把命令行装进“可视化抽屉”

工具界面采用四分区设计,每个区域对应办公场景的核心需求:

  • 路径选择区:左侧“源目录”和右侧“目标目录”旁各带一个“浏览文件夹”按钮,点击即可通过系统文件管理器选择路径,再也不用手动输入冗长的绝对路径。
  • 参数配置区:将 Robocopy 核心命令转化为直观的勾选框,比如“复制子目录”对应经典的 /E 参数,“保留文件属性”映射 /COPYALL 参数,“断点续传”则关联 /Z 参数——勾选即生效,无需记忆任何命令字符。
  • 操作控制区:居中的“开始复制”按钮搭配“暂停”“取消”功能键,复制过程中发现选错文件夹?点“暂停”修改路径后继续,避免重复劳动。
  • 进度展示区:底部左侧实时进度条动态显示复制百分比,右侧日志输出框按时间顺序记录每一步操作,成功文件绿色标注,失败文件红色提醒,连“复制速度 2.3MB/s”这样的细节都清晰可见。

极简线框图2.jpg

办公实测:1000 个文件的复制全流程

上周用部门共享盘中的“2024 项目资料”文件夹(含 1000 个文件、23 个子目录)做了实测,整个操作像用普通软件一样简单:

  1. 选路径:点击“浏览文件夹”分别选中“D:\项目资料”(源)和“E:\备份\项目资料”(目标);
  2. 设参数:勾选“复制子目录”“保留文件属性”“断点续传”三个常用选项;
  3. 点开始:按下“开始复制”按钮,进度条从 0% 开始跳动,日志框实时刷新“正在复制:需求文档 v3.docx”“已完成:设计稿/移动端/首页.png”;
  4. 看结果:12 分钟后进度条达 100%,日志显示“成功复制 998 个文件,失败 2 个(权限不足:secret.xlsx、损坏文件:old.jpg)”,直接定位问题文件。

下载1.png

三大核心亮点,完美闭环命令行痛点

  • 无需记忆命令:所有参数可视化勾选,/E、/Z 等参数变成“复制子目录”“断点续传”的中文选项,新手也能秒上手;
  • 可视化操作:从路径选择到进度监控,全流程图形化呈现,告别黑屏白字的命令行界面;
  • 过程可控:支持暂停/取消/断点续传,失败文件单独记录,避免命令行“一旦启动就无法回头”的尴尬。

从对着命令行手册反复试错,到现在“点选-开始-监控”三步完成复制,这个工具真正让技术服务于效率。下次再遇到批量文件复制,打开它,你会发现——原来命令行的痛点,早该用可视化解决了。

循环箭头续传图标.jpg

小白视角:开发中的踩坑与成长

技术认知的突破:从“代码恐惧”到“主动拆解问题”

回想项目启动时,我对编程的认知还停留在“能写几行Python循环语句”的阶段——GUI界面设计像天书,多线程更是只在教程里见过名词。面对“要把Robocopy命令变成可视化工具”这个目标,第一反应是:“这真的是我能做到的吗?”当时连调用系统命令都不知道从何下手,更别提让界面按钮和后台逻辑联动了。

从“不敢碰”到“敢拆解”的技术跃迁

项目像一把钥匙,打开了我对实用编程的认知大门。最开始卡在“如何让Python和Robocopy命令对话”,Trae提示我:“试试subprocess模块,它就像给Python开了个‘系统命令窗口’。”跟着他给的示例代码,我第一次用subprocess.Popen调用了robocopy /?,看着控制台输出的命令说明,突然意识到:原来复杂功能都是由一个个小工具组合而成的

随着开发深入,更多知识点像拼图一样串联起来:用PyQt5设计界面时,Trae花了20分钟画了张“信号与槽”关系图,告诉我“按钮点击是‘信号’,触发文件复制是‘槽’,它们的连接就是界面动起来的秘密”;处理UI卡顿问题时,他推荐我看《Fluent Python》里的多线程章节,让我明白“为什么要把耗时操作丢给子线程”;解析日志时,正则表达式从“一堆乱码符号”变成了“提取进度百分比的手术刀”。

核心知识点清单
subprocess模块:学会用Python调用系统命令,实现Robocopy功能的“底层对接”
PyQt5信号与槽:理解界面交互的逻辑链条,让按钮、进度条“活”起来
多线程处理:解决UI卡顿问题,让文件复制时界面仍能流畅操作
正则表达式:从日志文本中精准提取复制进度、文件数量等关键信息

技术进度图标.jpg

Trae的“脚手架式”指导:让学习有迹可循

最让我感激的是Trae的“授人以渔”。他从不说“你应该懂这个”,而是当我卡壳时:

  • 拆解代码逻辑:比如讲多线程时,他用“厨房分工”比喻——主线程是“前台服务员”(负责界面展示),子线程是“后厨厨师”(处理文件复制),两者互不干扰才能高效运转;
  • 推荐精准资源:针对PyQt5,他没让我啃厚重的官方文档,而是甩来3个B站实战视频:“先跟着做个简易计算器,信号与槽自然就懂了”。

这种“遇到问题→解决问题→总结规律”的过程,让我彻底摆脱了“代码恐惧”。现在再拿到复杂需求,第一反应不是“我不会”,而是拿出纸笔拆分模块:“界面要哪些组件?数据怎么流转?哪里需要异步处理?”这种主动拆解问题的能力,比任何知识点都更珍贵。

就像Trae说的:“编程不是背公式,而是拆解问题的思维游戏。你不需要一开始就会所有知识,做着做着,答案自然会浮现。”这次经历让我真切体会到:最好的学习,永远是在解决真实问题的路上。

心态转变:AI工具不是“作弊”,而是“脚手架”

当我看着屏幕上Trae生成的代码流畅运行时,心里却像压着块石头——这些代码真的属于我吗?我是不是在“作弊”?这个疑问像根刺,扎在我开发日志功能的那周。作为一个刚从命令行摸索到GUI开发的新手,每一行由AI生成的代码都让我既依赖又恐慌:“如果离开Trae,我还能独立写出什么?”这种“工具依赖焦虑”几乎成了那段时间的日常,甚至让我开始怀疑自己做这个可视化工具的意义。

直到一个关于日志显示的小需求,彻底改变了我的看法。当时Trae在分析代码后建议:“当前日志文本堆砌在控制台,用户体验较差,建议优化日志显示格式。”它给出了几个实现方向,但没有直接提供完整代码。那一刻,我没有像往常一样复制粘贴建议,而是盯着“优化显示格式”这几个字琢磨:什么样的格式才算“优化”?Qt里哪个控件适合展示富文本日志?

带着这些问题,我点开了Qt的官方文档,第一次主动研究QTextEdit控件。从基础的文本插入方法,到如何通过HTML设置字体颜色和行距,再到自定义滚动条样式让日志区域更符合整体UI风格,整个过程花了我整整一个下午。当最终实现的彩色日志区域在界面上亮起——成功操作显示绿色文字,错误信息标红并自动换行,甚至支持手动复制日志内容时,我突然意识到:Trae给的不是答案,而是一把钥匙。它没有替我思考“用什么控件”,而是通过“优化显示”这个具体目标,引导我主动去探索“如何实现更好的用户体验”。

真正的成长从来不是从零开始造轮子,而是知道在什么时候该借力,又该在哪个节点深入钻研。 AI工具就像建筑工地上的脚手架,它不会替你砌墙,但能稳稳托住你,让你够到独自难以触及的高度。当你借助它的支撑解决了问题,那些主动学习的知识点、独立做出的决策,才是真正沉淀下来的能力。

现在回头看,当初纠结“代码是谁写的”实在有些本末倒置。工具的价值从来不是替代人的思考,而是放大思考的效率。就像我后来在优化文件拖拽功能时,Trae提示“需要处理QDragEnterEvent事件”,我没有止步于复制事件处理代码,而是顺着这个线索系统学习了Qt的事件机制——这种“AI指路,自己铺路”的过程,比独自闷头查文档高效得多,也扎实得多。

或许你也曾有过类似的挣扎:担心依赖AI会让自己变成“代码搬运工”,害怕那些看似流畅的开发过程只是虚假的繁荣。但当你真正带着问题去使用工具,把它的建议当作思考的起点而非终点时,就会发现:工具是脚手架,而你才是那个决定建筑高度的建筑师。 真正的成长,永远在于借助工具解决问题的过程中,那些主动延伸的认知触角和独立做出的判断。

Robocopy参数图标.jpg

经验总结与未来展望

从命令行小白到独立开发出可视化工具,这个过程让我深刻体会到:技术学习的核心不是背诵知识点,而是建立解决问题的思维框架。在这里,我想把最实用的心得浓缩成3条建议,或许能帮同路的你少走一些弯路;同时也想聊聊这个工具的未来可能性,期待它能成为更多人技术成长的起点。

给同路小白的3条实操建议

开发过程中踩过的坑,最终都沉淀成了可复用的方法论。这3个技巧看似简单,却帮我高效推进了项目:

需求描述公式:场景+目标+细节
刚开始写需求时总像“记流水账”,直到摸索出这个公式:先明确使用场景(比如“团队每天下班前需要备份工作文件”),再锁定核心目标(“确保10分钟内完成500个文件的增量复制”),最后补充关键细节(“仅复制修改时间在24小时内的.docx和.xlsx文件,跳过大于100MB的临时文件”)。这样无论是自己梳理思路,还是向他人请教,都能精准传递信息。

调试三步法:复现→提取→提问
面对报错不用慌,按这三步走:先稳定复现问题(比如“每次选择包含特殊字符的文件夹就崩溃”),再提取关键信息(从命令行日志复制具体报错代码,如“ERROR 5: 拒绝访问”),最后向AI精准提问。给Trae提问时,我会附上“操作步骤+完整报错+已尝试的解决方案”,比如:“用Robocopy复制文件时出现‘ERROR 5’,已确认目标文件夹权限,尝试过以管理员身份运行仍无效,可能的原因是什么?”——这样得到的答案往往能直接命中问题核心。

持续迭代:让工具真正融入办公场景

这个可视化工具的基础功能已经能用,但办公场景的需求远不止于此。结合日常工作痛点,我计划从这两个方向继续优化,也欢迎你基于此开发自己的专属功能:

定时备份任务:很多同事需要每天固定时间备份文件,但手动操作容易遗忘。可以添加一个“定时任务模块”,让用户设置每周一至周五18:00自动执行备份,完成后通过系统通知提醒。实现时只需调用Windows的任务计划程序API,或用Python的schedule库写个轻量定时器。

文件差异对比:备份后总想确认“到底复制了哪些内容”?增加一个差异对比功能,用不同颜色标记新增、修改和删除的文件,甚至可以生成HTML报告。技术上可以通过对比文件的MD5哈希值或修改时间实现,对新手来说也是很好的练手项目。

从命令行的黑白窗口到GUI的交互界面,这个工具的进化本质上是“解决问题”思维的具象化。如果你也想动手试试,不妨从这两个功能开始——毕竟最好的学习,永远是在创造中完成的。期待在评论区看到你的二次开发作品!

资源分享

为了让大家能快速用上这个 Robocopy 可视化工具,我已经打包好了所有资源,无论是想直接使用还是研究源码的朋友,都能找到合适的下载方式~【源代码在文末】

【exe 文件下载】

百度网盘:链接: pan.baidu.com/s/1ssS2Uz2X… 提取码: 6ke9
夸克网盘:链接: pan.quark.cn/s/8afd32e74… 提取码: 5hYT

使用小指南

  • exe 文件:双击即可运行,无需安装 Python 环境,小白友好~
  • 权限提示:如果弹窗提示“权限不足”,右键文件选择“以管理员身份运行”就能解决。
  • 源代码:需要 Python 3.8+ 和 PyQt5 环境,适合想二次开发的技术同学。

如果在使用中遇到 bug 或者有功能建议,欢迎通过文章评论区留言,你的每一条反馈都会帮我把工具打磨得更好~

【源代码】

#!/usr/bin/env python3
# -*- coding: utf-8 -*- 
"""
Robocopy 文件复制工具 1.05

版权所有 (C) 2025 于海峰
微信: 5727717

这是一个基于PyQt5的Robocopy图形界面工具,提供了文件复制、移动、镜像等功能。
"""
import sys
import subprocess
import threading
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                             QGroupBox, QLabel, QLineEdit, QPushButton, QCheckBox, QSpinBox,
                             QTextEdit, QComboBox, QGridLayout, QFileDialog, QProgressBar,
                             QMessageBox, QFrame, QDialog, QTextBrowser)  # 确保包含了 QDialog
from PyQt5.QtCore import Qt, QTimer, QProcess
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QPixmap  # 确保包含了 QPixmap


class MacStyleRobocopyUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.process = None
        self.initUI()
        self.setupMacStyle()
        
    def initUI(self):
        # 主窗口设置
        self.setWindowTitle("Robocopy 文件复制工具 1.05")
        self.setGeometry(100, 100, 1000, 800)  # 将高度从750修改为800
        
        # 中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 主布局
        main_layout = QVBoxLayout(central_widget)
        main_layout.setSpacing(15)
        main_layout.setContentsMargins(20, 20, 20, 20)
        
        # 标题
        title_label = QLabel("Robocopy 文件复制工具")
        title_label.setAlignment(Qt.AlignCenter)
        title_label.setObjectName("title")
        title_label.setFixedHeight(40)  # 设置标签固定高度为40像素
        main_layout.addWidget(title_label)
        
        # 添加一个小间距(5像素),缩小标题和路径设置组之间的间隔
        main_layout.addSpacing(5)
        
        # 路径设置组
        path_group = QGroupBox("路径设置")
        path_layout = QGridLayout()
        path_layout.setSpacing(10)
        path_layout.addWidget(QLabel("源目录:"), 0, 0)
        self.source_dir = QLineEdit()
        self.source_dir.setPlaceholderText("请选择源目录")
        path_layout.addWidget(self.source_dir, 0, 1)
        self.source_browse = QPushButton("浏览")
        path_layout.addWidget(self.source_browse, 0, 2)
        
        path_layout.addWidget(QLabel("目标目录:"), 1, 0)
        self.dest_dir = QLineEdit()
        self.dest_dir.setPlaceholderText("请选择目标目录")
        path_layout.addWidget(self.dest_dir, 1, 1)
        self.dest_browse = QPushButton("浏览")
        path_layout.addWidget(self.dest_browse, 1, 2)
        
        path_layout.addWidget(QLabel("文件筛选:"), 2, 0)
        self.file_filter = QLineEdit("例如: *.txt 或 *.doc")
        path_layout.addWidget(self.file_filter, 2, 1)
        
        path_group.setLayout(path_layout)
        main_layout.addWidget(path_group)
        
        # 创建水平布局用于并排显示基本复制选项和高级选项组
        options_layout = QHBoxLayout()  # 修复:定义options_layout
        options_layout.setSpacing(15)
        
        # 基本复制选项组
        basic_group = QGroupBox("基本复制选项")
        basic_layout = QGridLayout()
        basic_layout.setSpacing(10)
        
        self.s_option = QCheckBox("/S - 复制子目录(排除空目录)")
        basic_layout.addWidget(self.s_option, 0, 0)
        
        self.sec_option = QCheckBox("/SEC - 复制安全信息")
        basic_layout.addWidget(self.sec_option, 0, 1)
        
        self.mov_option = QCheckBox("/MOV - 移动文件")
        basic_layout.addWidget(self.mov_option, 0, 2)
        
        self.e_option = QCheckBox("/E - 复制子目录(包括空目录)")
        basic_layout.addWidget(self.e_option, 1, 0)
        
        self.copyall_option = QCheckBox("/COPYALL - 复制所有文件信息")
        basic_layout.addWidget(self.copyall_option, 1, 1)
        
        self.move_option = QCheckBox("/MOVE - 移动文件和目录")
        basic_layout.addWidget(self.move_option, 1, 2)
        
        self.mir_option = QCheckBox("/MIR - 镜像模式")
        basic_layout.addWidget(self.mir_option, 2, 0)
        
        self.z_option = QCheckBox("/Z - 可重启模式")
        basic_layout.addWidget(self.z_option, 2, 1)
        
        self.create_option = QCheckBox("/CREATE - 仅创建目录结构")
        basic_layout.addWidget(self.create_option, 2, 2)
        
        self.purge_option = QCheckBox("/PURGE - 删除目标中不存在的文件")
        basic_layout.addWidget(self.purge_option, 3, 0)
        
        self.b_option = QCheckBox("/B - 备份模式")
        basic_layout.addWidget(self.b_option, 3, 1)
        
        basic_layout.addWidget(QLabel("/MT - 多线程:"), 3, 2)
        self.mt_option = QSpinBox()
        self.mt_option.setRange(1, 128)
        self.mt_option.setValue(16)
        basic_layout.addWidget(self.mt_option, 3, 3)
        
        basic_group.setLayout(basic_layout)
        
        # 高级选项组
        advanced_group = QGroupBox("高级选项")
        advanced_layout = QGridLayout()
        advanced_layout.setSpacing(10)
        
        self.fat_option = QCheckBox("/FAT - 使用8.3 FAT文件名")
        advanced_layout.addWidget(self.fat_option, 0, 0)
        
        self.v_option = QCheckBox("/V - 详细输出")
        advanced_layout.addWidget(self.v_option, 0, 1)
        
        # 添加垂直分隔线
        line = QFrame()
        line.setFrameShape(QFrame.VLine)
        line.setFrameShadow(QFrame.Sunken)
        advanced_layout.addWidget(line, 0, 2, 3, 1)  # 跨越0-2行,列2
        
        advanced_layout.addWidget(QLabel("重试设置"), 0, 3)
        
        self.j_option = QCheckBox("/J - 使用无缓冲I/O")
        advanced_layout.addWidget(self.j_option, 1, 0)
        
        self.l_option = QCheckBox("/L - 仅列出不复制")
        advanced_layout.addWidget(self.l_option, 1, 1)
        
        advanced_layout.addWidget(QLabel("重试次数:"), 1, 3)
        self.retry_count = QSpinBox()
        self.retry_count.setRange(0, 100)
        self.retry_count.setValue(1)
        advanced_layout.addWidget(self.retry_count, 1, 4)
        
        self.nocopy_option = QCheckBox("/NOCOPY - 不复制文件信息")
        advanced_layout.addWidget(self.nocopy_option, 2, 0)
        
        self.nfl_option = QCheckBox("/NFL - 不列文件")
        advanced_layout.addWidget(self.nfl_option, 2, 1)
        
        advanced_layout.addWidget(QLabel("等待时间:"), 2, 3)
        self.wait_time = QLineEdit("30")
        advanced_layout.addWidget(self.wait_time, 2, 4)
        
        advanced_group.setLayout(advanced_layout)
        
        # 将两个选项组添加到水平布局中
        options_layout.addWidget(basic_group)
        options_layout.addWidget(advanced_group)
        
        # 将水平布局添加到主布局
        main_layout.addLayout(options_layout)
        
        # 创建另一个水平布局用于并排显示筛选和日志选项组
        bottom_layout = QHBoxLayout()
        bottom_layout.setSpacing(15)
        
        # 软件筛选选项组
        filter_group = QGroupBox("软件筛选选项")
        filter_layout = QGridLayout()
        filter_layout.setSpacing(10)
        
        filter_layout.addWidget(QLabel("文件大小: 最小"), 0, 0)
        self.min_size = QLineEdit()
        filter_layout.addWidget(self.min_size, 0, 1)
        
        filter_layout.addWidget(QLabel("最大"), 0, 2)
        self.max_size = QLineEdit()
        filter_layout.addWidget(self.max_size, 0, 3)
        
        filter_layout.addWidget(QLabel("(单位: KB)"), 0, 4)
        
        filter_layout.addWidget(QLabel("排除文件:"), 1, 0)
        self.exclude_files = QLineEdit()
        self.exclude_files.setPlaceholderText("例如: *.tmp *.bak")
        filter_layout.addWidget(self.exclude_files, 1, 1, 1, 4)
        
        filter_layout.addWidget(QLabel("排除目录:"), 2, 0)
        self.exclude_dirs = QLineEdit()
        filter_layout.addWidget(self.exclude_dirs, 2, 1, 1, 4)
        
        filter_group.setLayout(filter_layout)
        
        # 日志设置组
        log_group = QGroupBox("日志设置")
        log_layout = QGridLayout()
        log_layout.setSpacing(10)
        
        self.enable_log = QCheckBox("启用日志记录")
        log_layout.addWidget(self.enable_log, 0, 0)
        
        self.unicode_log = QCheckBox("Unicode日志")
        log_layout.addWidget(self.unicode_log, 0, 1)
        
        self.append_log = QCheckBox("追加日志")
        log_layout.addWidget(self.append_log, 0, 2)
        
        log_layout.addWidget(QLabel("日志文件:"), 1, 0)
        self.log_file = QLineEdit()
        log_layout.addWidget(self.log_file, 1, 1)
        self.log_browse = QPushButton("浏览")
        log_layout.addWidget(self.log_browse, 1, 2)
        
        log_group.setLayout(log_layout)
        
        # 创建绿色分隔线
        separator = QFrame()
        separator.setFrameShape(QFrame.VLine)
        separator.setFrameShadow(QFrame.Sunken)
        separator.setStyleSheet("color: green; background-color: green;")
        
        # 将筛选和日志组添加到水平布局中,中间加入分隔线
        bottom_layout.addWidget(filter_group)
        bottom_layout.addWidget(separator)
        bottom_layout.addWidget(log_group)
        
        # 将水平布局添加到主布局
        main_layout.addLayout(bottom_layout)
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        self.progress_bar.setObjectName("progress")
        main_layout.addWidget(self.progress_bar)
        
        # 执行按钮布局
        button_layout = QHBoxLayout()
        button_layout.setSpacing(10)
        
        self.execute_button = QPushButton("开始复制")
        self.execute_button.setObjectName("execute")
        self.help_button = QPushButton("使用说明")
        self.help_button.setObjectName("help")
        self.donate_button = QPushButton("赞赏支持")  # 新增:添加赞赏按钮
        self.donate_button.setObjectName("donate")  # 新增:设置赞赏按钮对象名称
        self.exit_button = QPushButton("退出")
        self.exit_button.setObjectName("exit")
        self.exit_button.setEnabled(True)
        
        button_layout.addWidget(self.execute_button)
        button_layout.addWidget(self.help_button)
        button_layout.addWidget(self.donate_button)  # 新增:添加赞赏按钮到布局
        button_layout.addWidget(self.exit_button)
        
        main_layout.addLayout(button_layout)
        
        # 输出区域
        output_label = QLabel("输出日志:")
        main_layout.addWidget(output_label)
        
        self.output_area = QTextEdit()
        self.output_area.setReadOnly(True)
        self.output_area.setMaximumHeight(150)
        main_layout.addWidget(self.output_area)
        
        # 连接信号和槽
        self.source_browse.clicked.connect(self.browse_source)
        self.dest_browse.clicked.connect(self.browse_dest)
        self.log_browse.clicked.connect(self.browse_log)
        self.execute_button.clicked.connect(self.execute_copy)
        self.help_button.clicked.connect(self.show_help)
        self.donate_button.clicked.connect(self.show_donation)  # 新增:连接赞赏按钮信号
        self.exit_button.clicked.connect(self.exit_app)
        
    def setupMacStyle(self):
        # 设置苹果风格样式表
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f5f5f7;
                font-family: '.SF NS Text', 'San Francisco', 'Helvetica Neue', sans-serif;
            }
            
            #title {
                font-size: 24px;
                font-weight: 500;
                color: #333333;
                margin: 10px;
            }
            
            QGroupBox {
                font-weight: 500;
                border: 1px solid #d1d1d6;
                border-radius: 8px;
                margin-top: 25px;
                padding-top: 15px;
                background-color: white;
                font-size: 14px;
            }
            
            QGroupBox::title {
                subcontrol-offset: -20px;  /* 修改:将subline-offset改为subcontrol-offset */
                padding: 0 10px;
                color: #333333;
            }
            
            QPushButton {
                background-color: #007AFF;
                border: none;
                color: white;
                padding: 8px 16px;
                text-align: center;
                font-size: 14px;
                border-radius: 6px;
                font-weight: 500;
                min-height: 30px;
            }
            
            QPushButton:hover {
                background-color: #0062CC;
            }
            
            QPushButton:pressed {
                background-color: #004999;
            }
            
            QPushButton#help {  /* 新增:帮助按钮样式 */
                background-color: #4CAF50;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-size: 13px;
                font-weight: 500;
                subline-offset: -1px;  /* 修正拼写错误:subcontrol-offset -> subline-offset */
            }
            
            QPushButton#help:hover {  /* 新增:帮助按钮悬停样式 */
                background-color: #2DA448;
            }
            
            QPushButton#help:pressed {  /* 新增:帮助按钮按下样式 */
                background-color: #218838;
            }
            
            QPushButton#donate {  /* 新增:赞赏按钮样式 */
                background-color: #FF9500;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
                font-size: 13px;
                font-weight: 500;
            }
            
            QPushButton#donate:hover {  /* 新增:赞赏按钮悬停样式 */
                background-color: #E88B00;
            }
            
            QPushButton#donate:pressed {  /* 新增:赞赏按钮按下样式 */
                background-color: #CC7700;
            }
            
            QPushButton#exit {  /* 修改:更新CSS选择器 */
                background-color: #FF3B30;
            }
            
            QPushButton#exit:hover {  /* 修改:更新CSS选择器 */
                background-color: #D4261F;
            }
            
            QPushButton#exit:pressed {  /* 修改:更新CSS选择器 */
                background-color: #B2120F;
            }
            
            QPushButton:disabled {
                background-color: #C7C7CC;
                color: #FFFFFF;
            }
            
            QLineEdit {
                padding: 8px;
                border: 1px solid #d1d1d6;
                border-radius: 6px;
                font-size: 14px;
                background-color: white;
            }
            
            QLineEdit:focus {
                border: 1px solid #007AFF;
                outline: none;
            }
            
            QTextEdit {
                border: 1px solid #d1d1d6;
                border-radius: 6px;
                background-color: white;
                font-family: 'Monaco', 'Consolas', monospace;
                font-size: 12px;
            }
            
            QCheckBox {
                spacing: 5px;
                font-size: 14px;
            }
            
            QCheckBox::indicator {
                width: 18px;
                height: 18px;
                border-radius: 4px;
                border: 1px solid #d1d1d6;
                background-color: white;
            }
            
            QCheckBox::indicator:checked {
                background-color: #007AFF;
                border: 1px solid #007AFF;
                image: url();
            }
            
            QSpinBox {
                padding: 6px;
                border: 1px solid #d1d1d6;
                border-radius: 6px;
                font-size: 14px;
                background-color: white;
            }
            
            QSpinBox::up-button, QSpinBox::down-button {
                width: 20px;
                border: none;
                background-color: transparent;
            }
            
            QLabel {
                font-size: 14px;
                color: #333333;
            }
            
            #progress {
                border: 1px solid #d1d1d6;
                border-radius: 6px;
                background-color: #f0f0f0;
                text-align: center;
            }
            
            #progress::chunk {
                background-color: #007AFF;
                border-radius: 5px;
            }
        """)
        
    def browse_source(self):
        directory = QFileDialog.getExistingDirectory(self, "选择源目录")
        if directory:
            self.source_dir.setText(directory)
    
    def browse_dest(self):
        directory = QFileDialog.getExistingDirectory(self, "选择目标目录")
        if directory:
            self.dest_dir.setText(directory)
    
    def browse_log(self):
        file_path, _ = QFileDialog.getSaveFileName(self, "选择日志文件", "", "日志文件 (*.log);;所有文件 (*)")
        if file_path:
            self.log_file.setText(file_path)
    
    def execute_copy(self):
        # 检查必要参数
        if not self.source_dir.text() or not self.dest_dir.text():
            QMessageBox.warning(self, "警告", "请设置源目录和目标目录")
            return
        
        # 构建robocopy命令
        command = ["robocopy", self.source_dir.text(), self.dest_dir.text()]
        
        # 添加文件筛选
        if self.file_filter.text():
            command.append(self.file_filter.text())
        
        # 添加选项
        if self.s_option.isChecked():
            command.append("/S")
        if self.e_option.isChecked():
            command.append("/E")
        if self.sec_option.isChecked():
            command.append("/SEC")
        if self.mov_option.isChecked():
            command.append("/MOV")
        if self.copyall_option.isChecked():
            command.append("/COPYALL")
        if self.move_option.isChecked():
            command.append("/MOVE")
        if self.mir_option.isChecked():
            command.append("/MIR")
        if self.z_option.isChecked():
            command.append("/Z")
        if self.create_option.isChecked():
            command.append("/CREATE")
        if self.purge_option.isChecked():
            command.append("/PURGE")
        if self.b_option.isChecked():
            command.append("/B")
        if self.mt_option.value() > 1:
            command.append(f"/MT:{self.mt_option.value()}")
        if self.fat_option.isChecked():
            command.append("/FAT")
        if self.v_option.isChecked():
            command.append("/V")
        if self.j_option.isChecked():
            command.append("/J")
        if self.l_option.isChecked():
            command.append("/L")
        if self.retry_count.value() > 0:
            command.append(f"/R:{self.retry_count.value()}")
        if self.nocopy_option.isChecked():
            command.append("/NOCOPY")
        if self.nfl_option.isChecked():
            command.append("/NFL")
        if self.wait_time.text():
            command.append(f"/W:{self.wait_time.text()}")
        if self.min_size.text():
            command.append(f"/MIN:{self.min_size.text()}")
        if self.max_size.text():
            command.append(f"/MAX:{self.max_size.text()}")
        if self.exclude_files.text():
            command.append(f"/XF {self.exclude_files.text()}")
        if self.exclude_dirs.text():
            command.append(f"/XD {self.exclude_dirs.text()}")
        if self.enable_log.isChecked():
            command.append(f"/LOG:{self.log_file.text()}")
        if self.unicode_log.isChecked():
            command.append("/UNILOG")
        if self.append_log.isChecked():
            command.append("/APP")
        
        # 更新UI状态
        self.execute_button.setEnabled(False)
        # self.stop_button.setEnabled(True)  # 删除:不再需要
        self.output_area.clear()
        self.progress_bar.setValue(0)
        
        # 在线程中执行命令
        self.process = QProcess(self)
        self.process.readyReadStandardOutput.connect(self.handle_stdout)
        self.process.readyReadStandardError.connect(self.handle_stderr)
        self.process.finished.connect(self.process_finished)
        self.process.start("cmd", ["/c"] + command)
        
    def handle_stdout(self):
        data = self.process.readAllStandardOutput()
        stdout = bytes(data).decode("gbk", errors="ignore")
        self.output_area.append(stdout)
        
        # 简单的进度模拟(实际应用中可以根据输出内容更新进度)
        current_value = self.progress_bar.value()
        if current_value < 90:  # 留一些空间给最后的完成步骤
            self.progress_bar.setValue(current_value + 1)
    
    def handle_stderr(self):
        data = self.process.readAllStandardError()
        stderr = bytes(data).decode("gbk", errors="ignore")
        self.output_area.append(stderr)
    
    def process_finished(self):
        self.execute_button.setEnabled(True)
        # self.stop_button.setEnabled(False)  # 删除:不再需要
        self.progress_bar.setValue(100)
        self.output_area.append("\n复制操作已完成")
        
    def exit_app(self):  # 修改:添加退出应用程序的函数
        """退出应用程序"""
        # 如果有正在运行的进程,先终止它
        if self.process and self.process.state() == QProcess.Running:
            self.process.kill()
        # 关闭应用程序
        QApplication.instance().quit()
        
    def show_help(self):  # 新增:显示帮助信息的函数
        """显示使用说明"""
        help_text = """
-------------------------------------------------------------------------------
   ROBOCOPY     ::     Windows 的可靠文件复制                              
-------------------------------------------------------------------------------

               Copyright @2025 By 于海峰 Wx:5727717
               用法 :: ROBOCOPY source destination [file [file]...] [options]

                 源 :: 源目录(驱动器:\路径或\\服务器\共享\路径)。
               目标 :: 目标目录(驱动器:\路径或\\服务器\共享\路径)。
               文件 :: 要复制的文件(名称/通配符: 默认为 "*.*")。

::
:: 复制选项:
::
                 /S :: 复制子目录,但不复制空的子目录。
                 /E :: 复制子目录,包括空的子目录。
             /LEV:n :: 仅复制源目录树的前 n 层。

                 /Z :: 在可重新启动模式下复制文件。
                 /B :: 在备份模式下复制文件。
                /ZB :: 使用可重新启动模式;如果拒绝访问,请使用备份模式。
                 /J :: 复制时使用无缓冲的 I/O (推荐在复制大文件时使用)。
            /EFSRAW :: 在 EFS RAW 模式下复制所有加密的文件。

      /COPY:复制标记:: 要复制的文件内容(默认为 /COPY:DAT)。
                       (复制标记: D=数据,A=属性,T=时间戳)。
                       (S=安全=NTFS ACL,O=所有者信息,U=审核信息)。

 
               /SEC :: 复制具有安全性的文件(等同于 /COPY:DATS)。
           /COPYALL :: 复制所有文件信息(等同于 /COPY:DATSOU)。
            /NOCOPY :: 不复制任何文件信息(与 /PURGE 一起使用)。
            /SECFIX :: 修复所有文件的文件安全性,即使是跳过的文件。
            /TIMFIX :: 修复所有文件的文件时间,即使是跳过的文件。

             /PURGE :: 删除源中不再存在的目标文件/目录。
               /MIR :: 镜像目录树(等同于 /E 加 /PURGE)。

               /MOV :: 移动文件(复制后从源中删除)。
              /MOVE :: 移动文件和目录(复制后从源中删除)。

     /A+:[RASHCNET] :: 将给定的属性添加到复制的文件。
     /A-:[RASHCNET] :: 从复制的文件中删除给定的属性。

            /CREATE :: 仅创建目录树和长度为零的文件。
               /FAT :: 仅使用 8.3 FAT 文件名创建目标文件。
               /256 :: 关闭超长路径(> 256 个字符)支持。

             /MON:n :: 监视源;发现多于 n 个更改时再次运行。
             /MOT:m :: 监视源;如果更改,在 m 分钟时间后再次运行。

      /RH:hhmm-hhmm :: 可以启动新的复制时运行的小时数 - 时间。
                /PF :: 基于每个文件(而不是每个步骤)来检查运行小时数。

             /IPG:n :: 程序包间的间距(ms),以释放低速线路上的带宽。

                /SL :: 对照目标复制符号链接。

            /MT[:n] :: 使用 n 个线程进行多线程复制(默认值为 8)。
                       n 必须至少为 1,但不得大于 128。
                       该选项与 /IPG 和 /EFSRAW 选项不兼容。
                       使用 /LOG 选项重定向输出以便获得最佳性能。

 /DCOPY:复制标记:: 要复制的目录内容(默认为 /DCOPY:DA)。
                       (复制标记: D=数据,A=属性,T=时间戳)。

           /NODCOPY :: 不复制任何目录信息(默认情况下,执行 /DCOPY:DA)。

         /NOOFFLOAD :: 在不使用 Windows 复制卸载机制的情况下复制文件。

::
:: 文件选择选项:
::
                 /A :: 仅复制具有存档属性集的文件。
                 /M :: 仅复制具有存档属性的文件并重置存档属性。
    /IA:[RASHCNETO] :: 仅包含具有任意给定属性集的文件。
    /XA:[RASHCNETO] :: 排除具有任意给定属性集的文件。

  /XF 文件[文件]... :: 排除与给定名称/路径/通配符匹配的文件。
  /XD 目录[目录]... :: 排除与给定名称/路径匹配的目录。

                /XC :: 排除已更改的文件。
                /XN :: 排除较新的文件。
                /XO :: 排除较旧的文件。
                /XX :: 排除多余的文件和目录。
                /XL :: 排除孤立的文件和目录。
                /IS :: 包含相同文件。
                /IT :: 包含已调整的文件。

             /MAX:n :: 最大的文件大小 - 排除大于 n 字节的文件。
             /MIN:n :: 最小的文件大小 - 排除小于 n 字节的文件。

          /MAXAGE:n :: 最长的文件存在时间 - 排除早于 n 天/日期的文件。
          /MINAGE:n :: 最短的文件存在时间 - 排除晚于 n 天/日期的文件。
          /MAXLAD:n :: 最大的最后访问日期 - 排除自 n 以来未使用的文件。
          /MINLAD:n :: 最小的最后访问日期 - 排除自 n 以来使用的文件。
                       (If n < 1900 then n = n days, else n = YYYYMMDD date)。

                /XJ :: 排除接合点和符号链接。(默认情况下通常包括)。

               /FFT :: 假设 FAT 文件时间(2 秒粒度)。
               /DST :: 弥补 1 小时的 DST 时间差。

               /XJD :: 排除目录的接合点和符号链接。
               /XJF :: 排除文件的符号链接。

::
:: 重试选项:
::
               /R:n :: 失败副本的重试次数: 默认为 1 百万。
               /W:n :: 两次重试间的等待时间: 默认为 30 秒。

               /REG :: 将注册表中的 /R:n 和 /W:n 保存为默认设置。

               /TBD :: 等待定义共享名称(重试错误 67)。

::
:: 日志记录选项:
::
                 /L :: 仅列出 - 不复制、添加时间戳或删除任何文件。
                 /X :: 报告所有多余的文件,而不只是选中的文件。
                 /V :: 生成详细输出,同时显示跳过的文件。
                /TS :: 在输出中包含源文件的时间戳。
                /FP :: 在输出中包含文件的完整路径名称。
             /BYTES :: 以字节打印大小。

                /NS :: 无大小 - 不记录文件大小。
                /NC :: 无类别 - 不记录文件类别。
               /NFL :: 无文件列表 - 不记录文件名。
               /NDL :: 无目录列表 - 不记录目录名称。

                /NP :: 无进度 - 不显示已复制的百分比。
               /ETA :: 显示复制文件的预期到达时间。

          /LOG:文件 :: 将状态输出到日志文件(覆盖现有日志)。
         /LOG+:文件 :: 将状态输出到日志文件(附加到现有日志中)。

       /UNILOG:文件 :: 以 UNICODE 方式将状态输出到日志文件(覆盖现有日志)。
      /UNILOG+:文件 :: 以 UNICODE 方式将状态输出到日志文件(附加到现有日志中)。

               /TEE :: 输出到控制台窗口和日志文件。

               /NJH :: 没有作业标头。
               /NJS :: 没有作业摘要。

           /UNICODE :: 以 UNICODE 方式输出状态。

::
:: 作业选项 :
::
      /JOB:作业名称 :: 从命名的作业文件中提取参数。
     /SAVE:作业名称 :: 将参数保存到命名的作业文件
              /QUIT :: 处理命令行后退出(以查看参数)。 
              /NOSD :: 未指定源目录。
              /NODD :: 未指定目标目录。
                /IF :: 包含以下文件。

::
:: 备注:
::
       以前在卷的根目录上使用 /PURGE 或 /MIR 导致 
       robocopy 也对"系统卷信息"目录内的 
       文件应用所请求的操作。现在不再是这种情形;如果 
       指定了任何一项,则 robocopy 将跳过
       复制会话简要源目录和目标目录中具有该名称的任何文件或目录。
        """
        
        # 创建帮助对话框
        help_dialog = QDialog(self)
        help_dialog.setWindowTitle("Robocopy 使用说明")
        help_dialog.resize(800, 600)  # 设置对话框大小
        
        # 创建垂直布局
        layout = QVBoxLayout(help_dialog)
        
        # 创建 QTextBrowser 并设置帮助文本
        text_browser = QTextBrowser()
        text_browser.setPlainText(help_text)
        
        # 创建关闭按钮
        close_button = QPushButton("关闭")
        close_button.clicked.connect(help_dialog.accept)
        
        # 将 QTextBrowser 和按钮添加到布局中
        layout.addWidget(text_browser)
        layout.addWidget(close_button)
        
        # 显示对话框
        help_dialog.exec_()
    
    def show_donation(self):  # 新增:显示赞赏码的函数
        """显示赞赏码"""
        # 创建赞赏对话框
        donate_dialog = QDialog(self)
        donate_dialog.setWindowTitle("赞赏支持")
        donate_dialog.resize(400, 500)  # 设置对话框大小
        
        # 创建垂直布局
        layout = QVBoxLayout(donate_dialog)
        
        # 创建标签显示赞赏信息
        info_label = QLabel("如果该作品对你有帮助,可打赏,谢谢你的支持")
        info_label.setAlignment(Qt.AlignCenter)
        info_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
        layout.addWidget(info_label)
        
        # 创建标签显示二维码
        qr_label = QLabel()
        qr_label.setAlignment(Qt.AlignCenter)
        
        # 加载二维码图片
        try:
            pixmap = QPixmap("e:/Codes/Robocopy界面/于海峰的赞赏码.png")
            if pixmap.isNull():
                qr_label.setText("无法加载赞赏码图片")
            else:
                # 调整图片大小以适应对话框
                scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)
                qr_label.setPixmap(scaled_pixmap)
        except Exception as e:
            qr_label.setText(f"加载图片时出错: {str(e)}")
        
        layout.addWidget(qr_label)
        
        # 创建标签显示作者信息
        author_label = QLabel("于海峰 的赞赏码")
        author_label.setAlignment(Qt.AlignCenter)
        author_label.setStyleSheet("font-size: 14px; margin: 10px;")
        layout.addWidget(author_label)
        
        # 创建关闭按钮
        close_button = QPushButton("关闭")
        close_button.clicked.connect(donate_dialog.accept)
        layout.addWidget(close_button)
        
        # 显示对话框
        donate_dialog.exec_()

def main():
    app = QApplication(sys.argv)
    # 设置应用程序全局字体
    font = QFont(".SF NS Text", 10)
    app.setFont(font)
    window = MacStyleRobocopyUI()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()