R语言:expand.grid() 函数解析

本文介绍如何使用R语言中的expand.grid()函数来构造数据框,通过实例展示该函数可以生成参数各水平的完全搭配组合,类似于三层嵌套循环的效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

expand.grid() 构造一个数据框,将各参数的各水平完全搭配。

示例:

     type=c("A", "M")
     trend=c("N","A", "M")
     seasonal=c("N","A","M")
     hw_grid <- expand.grid(type,trend,seasonal)

     print(hw_grid)
                              

那么观察一下,这组输出的规律:
首先,一共18行恰好是 2*3*3

输出的结果跟三层嵌套循环很像:

      行数是括号内数(项)的个数相乘

      第一列:括号内第一项数(字符)依次循环

      第二列:括号内第二项的数(字符)每个重复第一项数的个数之后循环

      第三列:括号内第三项的数(字符)每个重复第一项数(字符)的个数乘以第二项的个数之后再循环
 

import tkinter as tk from tkinter import ttk, filedialog, scrolledtext import asyncio import threading import aiohttp import os import shutil import tempfile import requests from subprocess import run, CalledProcessError, PIPE from urllib.parse import urljoin import time import re # ------------------- 配置(请根据实际情况修改) ------------------- FFMPEG_PATH = r"D:\下载\ffmpeg-2025-08-14-git-cdbb5f1b93-essentials_build\bin\ffmpeg.exe" # FFmpeg路径 MAX_RETRY = 3 # 下载重试次数 TIMEOUT = 30 # 网络超时时间(秒) # 防盗链请求头(从浏览器抓包获取) DEFAULT_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", "Referer": "https://6x.iu6437d.cc:8888/", # 替换为视频所在页面地址 "Cookie": "" # 替换为浏览器中的Cookie(登录后获取) } # ------------------- 工具函数 ------------------- def run_async(coroutine): """在新线程中运行异步函数,解决事件循环冲突""" def wrapper(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: return loop.run_until_complete(coroutine) finally: loop.close() thread = threading.Thread(target=wrapper, daemon=True) thread.start() thread.join() # 等待异步任务完成 return thread.result if hasattr(thread, 'result') else None # ------------------- GUI 主类 ------------------- class VideoDownloaderGUI: def __init__(self, master): self.master = master master.title("M3U8视频下载工具(最终版)") master.geometry("800x650") master.minsize(800, 650) master.configure(bg="#2C3E50") # 设置网格权重,让界面可拉伸 master.grid_rowconfigure(8, weight=1) master.grid_columnconfigure(1, weight=1) # 变量初始化 self.url_var = tk.StringVar() self.title_var = tk.StringVar(value="未解析") self.duration_var = tk.StringVar(value="未知") self.save_path_var = tk.StringVar(value=os.path.join(os.path.expanduser("~"), "Videos")) self.headers = DEFAULT_HEADERS.copy() self.temp_dir = self._create_safe_temp_dir() # 安全的临时目录 self.is_downloading = False self.m3u8_content = None # 存储M3U8内容,避免重复下载 # 创建界面组件 self._create_widgets() def _create_safe_temp_dir(self): """创建确保可读写的临时目录""" try: temp_dir = os.path.join(os.path.expanduser("~"), "m3u8_download_temp") os.makedirs(temp_dir, exist_ok=True) # 验证目录可写 test_file = os.path.join(temp_dir, "test_write.txt") with open(test_file, "w") as f: f.write("test") os.remove(test_file) return temp_dir except Exception as e: temp_dir = tempfile.mkdtemp(prefix="m3u8_safe_") self.log(f"⚠️ 用户目录临时文件夹创建失败,使用系统临时目录:{temp_dir},错误:{str(e)}") return temp_dir def _create_widgets(self): # 1. M3U8链接输入区 frame_url = ttk.Frame(self.master) frame_url.grid(row=0, column=0, columnspan=3, padx=10, pady=10, sticky=tk.W+tk.E) frame_url.grid_columnconfigure(1, weight=1) ttk.Label(frame_url, text="M3U8链接:", background="#2C3E50", foreground="#ECF0F1").pack(side=tk.LEFT, padx=5) self.url_entry = ttk.Entry(frame_url, textvariable=self.url_var) self.url_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) self.parse_btn = ttk.Button(frame_url, text="解析视频", command=self.parse_video) self.parse_btn.pack(side=tk.LEFT, padx=5) # 2. 视频信息区 frame_info = ttk.Frame(self.master) frame_info.grid(row=1, column=0, columnspan=3, padx=10, pady=5, sticky=tk.W) ttk.Label(frame_info, text="视频标题:", background="#2C3E50", foreground="#ECF0F1").grid(row=0, column=0, padx=10, pady=3, sticky=tk.W) self.title_entry = ttk.Entry(frame_info, textvariable=self.title_var, width=50) self.title_entry.grid(row=0, column=1, padx=5, pady=3, sticky=tk.W) ttk.Label(frame_info, text="时长:", background="#2C3E50", foreground="#ECF0F1").grid(row=1, column=0, padx=10, pady=3, sticky=tk.W) ttk.Label(frame_info, textvariable=self.duration_var, background="#2C3E50", foreground="#ECF0F1").grid(row=1, column=1, padx=5, pady=3, sticky=tk.W) # 3. 保存路径区 frame_save = ttk.Frame(self.master) frame_save.grid(row=2, column=0, columnspan=3, padx=10, pady=10, sticky=tk.W+tk.E) frame_save.grid_columnconfigure(1, weight=1) ttk.Label(frame_save, text="保存路径:", background="#2C3E50", foreground="#ECF0F1").pack(side=tk.LEFT, padx=5) self.path_entry = ttk.Entry(frame_save, textvariable=self.save_path_var) self.path_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) self.browse_btn = ttk.Button(frame_save, text="浏览...", command=self.browse_path) self.browse_btn.pack(side=tk.LEFT, padx=5) # 4. 请求头配置区 ttk.Label(self.master, text="请求头配置(防盗链):", background="#2C3E50", foreground="#ECF0F1").grid(row=3, column=0, padx=10, pady=5, sticky=tk.W) self.headers_text = scrolledtext.ScrolledText(self.master, height=4, wrap=tk.WORD) self.headers_text.grid(row=4, column=0, columnspan=3, padx=10, pady=5, sticky=tk.W+tk.E) self._init_headers_text() # 5. 下载控制区 self.start_btn = ttk.Button(self.master, text="开始下载", command=self.start_download, state=tk.DISABLED) self.start_btn.grid(row=5, column=1, pady=10) # 6. 进度条 ttk.Label(self.master, text="下载进度:", background="#2C3E50", foreground="#ECF0F1").grid(row=6, column=0, padx=10, pady=5, sticky=tk.W) self.progress_bar = ttk.Progressbar(self.master, orient=tk.HORIZONTAL, length=0, mode='determinate') self.progress_bar.grid(row=6, column=1, padx=5, pady=5, sticky=tk.W+tk.E) # 7. 日志区 ttk.Label(self.master, text="操作日志:", background="#2C3E50", foreground="#ECF0F1").grid(row=7, column=0, padx=10, pady=5, sticky=tk.W) self.log_text = scrolledtext.ScrolledText(self.master, wrap=tk.WORD, background="#34495E", foreground="#ECF0F1") self.log_text.grid(row=8, column=0, columnspan=3, padx=10, pady=5, sticky=tk.W+tk.E+tk.N+tk.S) def _init_headers_text(self): """初始化请求头文本框内容""" headers_str = "" for key, value in self.headers.items(): headers_str += f"{key}: {value}\n" self.headers_text.insert(tk.END, headers_str.strip()) def browse_path(self): """选择保存目录""" path = filedialog.askdirectory() if path: self.save_path_var.set(path) self.log(f"已选择保存目录: {path}") def parse_video(self): """解析视频信息""" url = self.url_var.get().strip() if not url or not url.endswith(".m3u8"): self.log("❌ 请输入有效的M3U8链接(必须以.m3u8结尾)") return # 更新请求头 self._update_headers() # 重置状态 self.title_var.set("解析...") self.duration_var.set("解析...") self.start_btn.config(state=tk.DISABLED) self.m3u8_content = None # 异步解析 threading.Thread(target=self._async_parse, args=(url,), daemon=True).start() def _update_headers(self): """从文本框更新请求头""" try: headers_text = self.headers_text.get(1.0, tk.END).strip() self.headers = {} for line in headers_text.split('\n'): if ':' in line: key, value = line.split(':', 1) self.headers[key.strip()] = value.strip() self.log("✅ 请求头已更新") except Exception as e: self.log(f"⚠️ 请求头格式错误:{str(e)},将使用默认配置") self.headers = DEFAULT_HEADERS.copy() def _async_parse(self, url): """后台解析M3U8信息""" try: # 使用自定义异步运行函数,避免事件循环冲突 self.m3u8_content = run_async(self._fetch_m3u8(url)) if not self.m3u8_content: raise Exception("未获取到M3U8内容") # 解析标题和时长 title = self._parse_title(self.m3u8_content, url) duration = self._parse_duration(self.m3u8_content) # 更新界面 self.master.after(0, lambda: self.title_var.set(title)) self.master.after(0, lambda: self.duration_var.set(duration)) self.master.after(0, lambda: self.start_btn.config(state=tk.NORMAL)) self.log(f"✅ 解析成功:{title}(时长:{duration})") except Exception as e: self.log(f"❌ 解析失败:{str(e)}") self.master.after(0, lambda: self.title_var.set("解析失败")) self.master.after(0, lambda: self.duration_var.set("未知")) self.master.after(0, lambda: self.start_btn.config(state=tk.DISABLED)) async def _fetch_m3u8(self, url): """异步获取M3U8内容""" async with aiohttp.ClientSession() as session: async with session.get( url, headers=self.headers, timeout=aiohttp.ClientTimeout(total=TIMEOUT) ) as resp: if resp.status != 200: raise Exception(f"服务器拒绝访问(状态码:{resp.status})") return await resp.text() def _parse_title(self, m3u8_content, url): """解析视频标题""" default_title = os.path.basename(url).replace(".m3u8", "") if not default_title: default_title = f"视频_{int(time.time())}" for line in m3u8_content.splitlines(): if line.startswith("#EXTINF") and ',' in line: title = line.split(',', 1)[1].strip() # 过滤非法字符 return re.sub(r'[\\/:\*\?"<>\|]', '_', title) return default_title def _parse_duration(self, m3u8_content): """解析视频时长""" for line in m3u8_content.splitlines(): if line.startswith("#EXT-X-TOTAL-DURATION"): try: duration = float(line.split(':', 1)[1].strip()) return f"{duration:.1f}秒" except: return "计算失败" return "未知" def start_download(self): """开始下载""" if self.is_downloading: self.log("⚠️ 正在下载中,请不要重复点击") return url = self.url_var.get().strip() save_dir = self.save_path_var.get() title = self.title_var.get() # 验证保存目录 if not os.path.exists(save_dir): self.log("❌ 保存目录不存在,请重新选择") return # 生成安全的文件名(解决"Q:/视频\.mp4"这种错误路径) safe_title = re.sub(r'[\\/:\*\?"<>\|]', '_', title) if not safe_title: # 防止标题为空 safe_title = f"视频_{int(time.time())}" save_path = os.path.join(save_dir, f"{safe_title}.mp4") # 检查文件是否已存在 if os.path.exists(save_path): self.log(f"⚠️ 文件已存在,将覆盖:{save_path}") # 更新状态 self.is_downloading = True self.start_btn.config(text="下载中...", state=tk.DISABLED) self.progress_bar.config(value=10) self.log(f"开始下载:{save_path}") # 启动下载线程 threading.Thread( target=self._async_download, args=(url, save_path), daemon=True ).start() def _async_download(self, m3u8_url, save_path): """后台下载并合并视频""" downloader = M3U8Downloader( ffmpeg_path=FFMPEG_PATH, temp_dir=self.temp_dir, log_func=self.log, headers=self.headers ) try: # 使用解析阶段已获取的M3U8内容,避免重复下载 if self.m3u8_content: result = downloader.download_with_content(m3u8_url, save_path, self.m3u8_content) else: result = downloader.download(m3u8_url, save_path) if result: self.master.after(0, lambda: self.progress_bar.config(value=100)) self.log(f"✅ 下载完成!文件保存至:{save_path}") else: raise Exception("下载过程未正常完成") except Exception as e: self.log(f"❌ 下载失败:{str(e)}") self.master.after(0, lambda: self.progress_bar.config(value=0)) finally: # 重置状态 self.is_downloading = False self.master.after(0, lambda: self.start_btn.config(text="开始下载", state=tk.NORMAL)) # 清理临时文件 self._clean_temp_files() def _clean_temp_files(self): """清理临时文件""" try: if os.path.exists(self.temp_dir): shutil.rmtree(self.temp_dir, ignore_errors=True) # 重新创建临时目录,供下次使用 self.temp_dir = self._create_safe_temp_dir() self.log("✅ 临时文件已清理") except Exception as e: self.log(f"⚠️ 临时文件清理失败:{str(e)}") def log(self, message): """添加日志""" self.log_text.insert(tk.END, f"[{time.strftime('%H:%M:%S')}] {message}\n") self.log_text.see(tk.END) # ------------------- 下载核心类 ------------------- class M3U8Downloader: def __init__(self, ffmpeg_path, temp_dir, log_func, headers): self.ffmpeg_path = ffmpeg_path self.temp_dir = temp_dir self.log = log_func self.headers = headers self.base_url = "" async def _fetch_with_headers(self, session, url, is_binary=False, retry=0): """带请求头的网络请求""" try: async with session.get( url, headers=self.headers, timeout=aiohttp.ClientTimeout(total=TIMEOUT) ) as resp: if resp.status != 200: raise Exception(f"状态码:{resp.status}") return await resp.read() if is_binary else await resp.text() except Exception as e: if retry < MAX_RETRY: self.log(f"⚠️ 下载失败({e}),第{retry+1}次重试...") return await self._fetch_with_headers(session, url, is_binary, retry + 1) raise async def _download_key(self, key_url): """异步下载加密密钥(单独的异步函数,避免事件循环冲突)""" async with aiohttp.ClientSession() as session: return await self._fetch_with_headers(session, key_url, is_binary=True) def _parse_encryption(self, m3u8_content): """解析加密信息""" key_url = None iv = None for line in m3u8_content.splitlines(): if line.startswith("#EXT-X-KEY"): parts = line.split(',') for part in parts: if "URI=" in part: key_url = part.split('URI="')[1].split('"')[0] if "IV=" in part: iv = part.split('IV=')[1] break if not key_url: return None, None # 处理相对路径 if not key_url.startswith(("http://", "https://")): key_url = urljoin(self.base_url, key_url) # 下载密钥(使用自定义异步运行函数) key_path = os.path.join(self.temp_dir, "encryption_key.bin") try: key_data = run_async(self._download_key(key_url)) if not key_data: raise Exception("未获取到密钥数据") # 确保临时目录存在 os.makedirs(self.temp_dir, exist_ok=True) with open(key_path, "wb") as f: f.write(key_data) # 验证文件是否写入成功 if not os.path.exists(key_path) or os.path.getsize(key_path) == 0: raise FileNotFoundError("密钥文件写入失败或为空") self.log("✅ 已获取加密密钥") return key_path, iv except Exception as e: self.log(f"❌ 加密密钥获取失败:{e}") # 即使密钥获取失败也继续尝试,可能视频未加密 return None, None def _merge_with_ffmpeg(self, m3u8_path, save_path, key_path, iv): """合并视频,并返回详细错误信息""" # 确保FFmpeg路径正确 if not os.path.exists(self.ffmpeg_path): raise FileNotFoundError(f"FFmpeg未找到:{self.ffmpeg_path}") cmd = [ self.ffmpeg_path, "-hide_banner", "-loglevel", "error", # 只输出错误信息 "-allowed_extensions", "ALL", "-i", m3u8_path, "-c", "copy", "-y", save_path ] # 添加解密参数 if key_path: cmd.insert(4, "-decryption_key") cmd.insert(5, f"file:{key_path}") if iv: cmd.insert(6, "-iv") cmd.insert(7, iv) try: # 捕获FFmpeg的错误输出 result = run(cmd, check=True, stderr=PIPE, text=True) return True except CalledProcessError as e: # 输出FFmpeg的详细错误信息 self.log(f"FFmpeg错误详情:{e.stderr}") raise Exception(f"FFmpeg合并失败(代码:{e.returncode}):{e.stderr[:200]}") def download_with_content(self, m3u8_url, save_path, m3u8_content): """使用已有的M3U8内容进行下载""" try: self.base_url = urljoin(m3u8_url, ".") # 设置基地址 # 保存M3U8到临时文件 os.makedirs(self.temp_dir, exist_ok=True) temp_m3u8 = os.path.join(self.temp_dir, "index.m3u8") with open(temp_m3u8, "w", encoding="utf-8") as f: f.write(m3u8_content) # 验证文件是否存在 if not os.path.exists(temp_m3u8): raise FileNotFoundError(f"M3U8临时文件创建失败:{temp_m3u8}") self.log(f"✅ M3U8索引已保存到临时文件") # 解析加密信息 key_path, iv = self._parse_encryption(m3u8_content) # 合并视频 self.log("开始合并视频...") return self._merge_with_ffmpeg(temp_m3u8, save_path, key_path, iv) except Exception as e: raise Exception(f"核心错误:{str(e)}") def download(self, m3u8_url, save_path): """从网络下载M3U8并处理(备用方法)""" try: # 下载M3U8索引 self.log("正在下载M3U8索引...") m3u8_content = run_async(self._fetch_with_headers( aiohttp.ClientSession(), m3u8_url )) return self.download_with_content(m3u8_url, save_path, m3u8_content) except Exception as e: raise Exception(f"下载M3U8失败:{str(e)}") # ------------------- 启动程序 ------------------- if __name__ == "__main__": root = tk.Tk() app = VideoDownloaderGUI(root) root.mainloop() 帮我优化代码
08-16
在UI界面顶部生成菜单栏选项:一级目录<Excel文件处理>,点击该目录生成二级目录<宏模块加载分析>、二级目录<文件合并统计分析>。点击二级目录<宏模块加载分析>时加载出如下UI界面:import tkinter as tk from tkinter import ttk, filedialog, messagebox import os import win32com.client as win32 import time from datetime import datetime import threading import re class VBAExecutorApp: def __init__(self, root): self.root = root self.root.title("WPS Excel宏执行器") self.root.geometry("600x280") self.root.resizable(True, True) self.style = ttk.Style() # 配置按钮样式 self.style.configure("Run.TButton", foreground="white", background="#4CAF50", font=("Arial", 10, "bold"), padding=6) self.style.map("Run.TButton", foreground=[('disabled', 'gray')], background=[('disabled', '#CCCCCC')]) # 创建网格布局 self.root.columnconfigure((0, 1, 2), weight=1) self.root.rowconfigure((0, 1, 2), weight=1) # 第一行标题 self.create_label("日志文件选择:", 0, 0, "黑体", 12) self.create_label("分析模块选择:", 0, 1, "黑体", 12) self.create_label("宏主程序读取:", 0, 2, "黑体", 12) # 第二行文件选择 self.excel_path = tk.StringVar() self.bas_path = tk.StringVar() self.create_file_entry(1, 0, self.excel_path, [("Excel文件", "*.xls *.xlsx")]) self.create_file_entry(1, 1, self.bas_path, [("BAS文件", "*.bas")]) # 宏函数下拉框 self.macro_var = tk.StringVar() self.macro_combobox = ttk.Combobox( self.root, textvariable=self.macro_var, state="readonly", height=8 ) self.macro_combobox.grid(row=1, column=2, padx=5, pady=2, sticky="ew") # 第三行按钮 self.run_button = ttk.Button( self.root, text="执行宏命令分析", command=self.start_execution, width=23, style="Run.TButton" ) self.run_button.grid(row=2, column=0, padx=5, pady=(7, 8), sticky="ew") # 添加悬停效果绑定 self.run_button.bind("<Enter>", self.on_enter) self.run_button.bind("<Leave>", self.on_leave) self.run_button.bind("<ButtonPress-1>", self.on_press) self.run_button.bind("<ButtonRelease-1>", self.on_release) # 进度条容器 progress_container = ttk.Frame(self.root) progress_container.grid(row=2, column=1, columnspan=2, padx=5, pady=0, sticky="ew") progress_container.columnconfigure(0, weight=1) progress_container.columnconfigure(1, weight=0) # 进度条 self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar( progress_container, variable=self.progress_var, maximum=100, mode="determinate", length=200 ) self.progress_bar.grid(row=0, column=0,ipadx=30,ipady=7,padx=(0, 0), sticky="ew") # 百分比标签 self.percent_label = ttk.Label( progress_container, text="0%", font=("Arial", 12), width=5 ) self.percent_label.grid(row=0, column=1, ipadx=2,ipady=7,padx=(10, 0),sticky="e") # 绑定事件 self.bas_path.trace_add("write", self.parse_bas_file) self.progress_var.trace_add("write", self.update_percent_label) def update_percent_label(self, *args): """更新百分比标签显示""" progress_value = self.progress_var.get() self.percent_label.config(text=f"{int(progress_value)}%") # 根据进度改变标签颜色 if progress_value < 30: self.percent_label.config(foreground="red") elif progress_value < 70: self.percent_label.config(foreground="orange") else: self.percent_label.config(foreground="green") # 关键修改:当进度达到100%时重新启用按钮 if progress_value == 100: self.run_button.config(state=tk.NORMAL) def create_label(self, text, row, column, font, size): label = tk.Label( self.root, text=text, font=(font, size), anchor="w" ) label.grid(row=row, column=column, padx=0, pady=(25, 0), sticky="w") return label def create_file_entry(self, row, column, text_var, filetypes): frame = ttk.Frame(self.root) frame.grid(row=row, column=column, padx=5, pady=2, sticky="ew") frame.columnconfigure(1, weight=1) entry = ttk.Entry(frame, textvariable=text_var) entry.grid(row=0, column=0, sticky="ew", padx=(0, 2)) browse = ttk.Button(frame, text="浏览", command=lambda: self.browse_file(text_var, filetypes)) browse.grid(row=0, column=1, padx=(0, 0)) def browse_file(self, text_var, filetypes): file_path = filedialog.askopenfilename(filetypes=filetypes) if file_path: text_var.set(file_path) def parse_bas_file(self, *args): bas_path = self.bas_path.get() if not bas_path or not os.path.exists(bas_path): return try: # 尝试使用UTF-8和GBK解码 try: with open(bas_path, "r", encoding="utf-8") as f: content = f.read() except UnicodeDecodeError: with open(bas_path, "r", encoding="gbk") as f: content = f.read() # 查找Main_开头的宏函数 macro_pattern = r"Sub\s+(Main_\w+)\s*\(\)" macros = re.findall(macro_pattern, content) if macros: self.macro_combobox["values"] = macros if self.macro_combobox["values"]: self.macro_combobox.current(0) except Exception as e: messagebox.showerror("错误", f"解析BAS文件失败: {str(e)}") def start_execution(self): """关键修改:点击后立即禁用按钮""" if not self.excel_path.get() or not self.bas_path.get(): messagebox.showerror("错误", "请先选择需要分析的文件或宏模块") return # 禁用按钮防止重复点击 self.run_button.config(state=tk.DISABLED) self.progress_var.set(0) # 重置进度条 # 在后台线程中执行宏 threading.Thread(target=self.execute_macro, daemon=True).start() def on_enter(self, event): self.style.configure("Run.TButton", background="#45a049") def on_leave(self, event): self.style.configure("Run.TButton", background="#4CAF50") def on_press(self, event): self.style.configure("Run.TButton", background="#2E7D32") def on_release(self, event): self.style.configure("Run.TButton", background="#45a049") def execute_macro(self): try: # 初始化WPS Excel excel = win32.gencache.EnsureDispatch("Excel.Application") excel.Visible = False excel.DisplayAlerts = False # 打开Excel文件 wb = excel.Workbooks.Open(os.path.abspath(self.excel_path.get())) self.update_progress(20) # 导入BAS文件 vb_project = wb.VBProject vb_project.VBComponents.Import(os.path.abspath(self.bas_path.get())) self.update_progress(40) # 执行宏 macro_name = self.macro_var.get() excel.Application.Run(macro_name) self.update_progress(70) time.sleep(1) # 模拟执行时间 # 另存为新文件 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") save_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx"), ("Excel 97-2003", "*.xls")], initialfile=f"{self.macro_combobox["values"]}{timestamp}" ) if save_path: wb.SaveAs(save_path) self.update_progress(90) messagebox.showinfo("成功", f"文件已保存至: {save_path}") wb.Close(False) excel.Quit() self.update_progress(100) # 关键点:设置进度为100% except Exception as e: messagebox.showerror("执行错误", f"宏执行失败: {str(e)}") self.update_progress(100) # 出错时也设置进度为100% finally: # 确保进度达到100%(update_progress(100)会启用按钮) if self.progress_var.get() < 100: self.update_progress(100) def update_progress(self, value): """更新进度条值(线程安全)""" # 使用after确保在主线程更新UI self.root.after(0, lambda: self.progress_var.set(value)) self.root.update_idletasks() if __name__ == "__main__": root = tk.Tk() app = VBAExecutorApp(root) root.mainloop()
07-24
import tkinter as tk from tkinter import ttk, messagebox, filedialog import json from datetime import datetime, timedelta class Task: """任务数据模型""" def __init__(self, id, title, description, due_date, priority, completed=False): self.id = id self.title = title self.description = description self.due_date = due_date # 格式: "YYYY-MM-DD" self.priority = priority # 1(高), 2(中), 3(低) self.completed = completed def to_dict(self): return { "id": self.id, "title": self.title, "description": self.description, "due_date": self.due_date, "priority": self.priority, "completed": self.completed } @classmethod def from_dict(cls, data): return cls( data["id"], data["title"], data["description"], data["due_date"], data["priority"], data["completed"] ) class TaskManager: """任务管理核心逻辑""" def __init__(self): self.tasks = [] self.next_id = 1 def add_task(self, title, description, due_date, priority): task = Task(self.next_id, title, description, due_date, priority) self.next_id += 1 self.tasks.append(task) return task def delete_task(self, task_id): self.tasks = [t for t in self.tasks if t.id != task_id] def toggle_completion(self, task_id): for task in self.tasks: if task.id == task_id: task.completed = not task.completed return def get_sorted_tasks(self, sort_by="due_date"): if sort_by == "priority": return sorted(self.tasks, key=lambda x: x.priority) return sorted(self.tasks, key=lambda x: x.due_date) def filter_tasks(self, show_completed=False): if show_completed: return self.tasks return [t for t in self.tasks if not t.completed] def calculate_days_left(self, due_date): due = datetime.strptime(due_date, "%Y-%m-%d").date() today = datetime.now().date() return (due - today).days def mark_overdue_tasks(self): today = datetime.now().date() for task in self.tasks: if not task.completed and datetime.strptime(task.due_date, "%Y-%m-%d").date() < today: task.priority = 0 # 用0表示逾期 def export_to_json(self, filename): data = [task.to_dict() for task in self.tasks] with open(filename, 'w') as f: json.dump(data, f, indent=2) def import_from_json(self, filename): with open(filename, 'r') as f: data = json.load(f) self.tasks = [Task.from_dict(item) for item in data] if self.tasks: self.next_id = max(t.id for t in self.tasks) + 1 class TodoApp: """GUI应用程序""" def __init__(self, root): self.root = root self.root.title("任务待办清单管理系统") self.manager = TaskManager() # 创建界面 self.setup_ui() self.load_data() self.refresh_list() def setup_ui(self): # 主框架 main_frame = ttk.Frame(self.root, padding=10) main_frame.pack(fill=tk.BOTH, expand=True) # 任务列表 (Treeview) columns = ("id", "title", "due_date", "priority", "status", "days_left") self.tree = ttk.Treeview(main_frame, columns=columns, show="headings", selectmode="browse") # 配置列 self.tree.heading("id", text="ID", anchor=tk.W) self.tree.heading("title", text="任务标题", anchor=tk.W) self.tree.heading("due_date", text="截止日期", anchor=tk.W) self.tree.heading("priority", text="优先级", anchor=tk.CENTER) self.tree.heading("status", text="状态", anchor=tk.CENTER) self.tree.heading("days_left", text="剩余天数", anchor=tk.CENTER) self.tree.column("id", width=50, minwidth=50) self.tree.column("title", width=200, minwidth=150) self.tree.column("due_date", width=100, minwidth=80) self.tree.column("priority", width=80, minwidth=60, anchor=tk.CENTER) self.tree.column("status", width=80, minwidth=60, anchor=tk.CENTER) self.tree.column("days_left", width=80, minwidth=60, anchor=tk.CENTER) # 滚动条 scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) # 布局 self.tree.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) scrollbar.grid(row=0, column=1, sticky="ns") # 按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.grid(row=1, column=0, columnspan=2, pady=10, sticky="ew") buttons = [ ("添加任务", self.add_task_dialog), ("删除任务", self.delete_task), ("标记完成/未完成", self.toggle_completion), ("按日期排序", lambda: self.sort_tasks("due_date")), ("按优先级排序", lambda: self.sort_tasks("priority")), ("显示未完成", self.show_incomplete), ("导出数据", self.export_data), ("导入数据", self.import_data) ] for i, (text, command) in enumerate(buttons): ttk.Button(btn_frame, text=text, command=command).grid( row=i // 4, column=i % 4, padx=5, pady=5, sticky="ew" ) # 配置网格布局权重 main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) btn_frame.columnconfigure((0, 1, 2, 3), weight=1) def refresh_list(self): """刷新任务列表显示""" # 标记逾期任务 self.manager.mark_overdue_tasks() # 清空当前列表 for item in self.tree.get_children(): self.tree.delete(item) # 添加任务到列表 for task in self.manager.tasks: days_left = self.manager.calculate_days_left(task.due_date) status = "已完成" if task.completed else "未完成" # 逾期状态覆盖 if task.priority == 0 and not task.completed: status = "逾期" # 优先级文本 priority_map = {0: "逾期", 1: "高", 2: "中", 3: "低"} priority_text = priority_map.get(task.priority, "") # 插入列表 item = self.tree.insert("", "end", values=( task.id, task.title, task.due_date, priority_text, status, days_left if days_left >= 0 and not task.completed else "" )) # 设置颜色 if task.completed: self.tree.item(item, tags=("completed",)) elif task.priority == 0: self.tree.item(item, tags=("overdue",)) # 配置标签样式 self.tree.tag_configure("completed", foreground="gray") self.tree.tag_configure("overdue", foreground="red") def add_task_dialog(self): """添加任务弹窗(修复版本)""" dialog = tk.Toplevel(self.root) dialog.title("添加新任务") dialog.transient(self.root) dialog.grab_set() # 表单字段(添加优先级字段) fields = [ ("标题", "title", tk.Entry), ("描述", "description", tk.Text), ("截止日期 (YYYY-MM-DD)", "due_date", tk.Entry), ("优先级 (1:高, 2:中, 3:低)", "priority", ttk.Combobox) # 新增优先级字段 ] entries = {} for i, (label, key, widget_type) in enumerate(fields): ttk.Label(dialog, text=label).grid(row=i, column=0, padx=5, pady=5, sticky="w") if widget_type == tk.Entry: entry = tk.Entry(dialog, width=30) entry.grid(row=i, column=1, padx=5, pady=5) elif widget_type == tk.Text: entry = tk.Text(dialog, width=30, height=4) entry.grid(row=i, column=1, padx=5, pady=5) elif widget_type == ttk.Combobox: # 优先级专用控件 entry = ttk.Combobox(dialog, width=27, values=["1", "2", "3"]) entry.current(1) # 默认选中"中"优先级 entry.grid(row=i, column=1, padx=5, pady=5) entries[key] = entry # 保存任务函数(添加数据验证) def save_task(): data = { "title": entries["title"].get(), "description": entries["description"].get("1.0", tk.END).strip(), "due_date": entries["due_date"].get(), "priority": entries["priority"].get() # 获取优先级值 } # 数据验证(包含优先级验证) if not data["title"]: messagebox.showerror("错误", "标题不能为空") return if not data["due_date"]: messagebox.showerror("错误", "截止日期不能为空") return if data["priority"] not in ["1", "2", "3"]: # 优先级验证 messagebox.showerror("错误", "优先级值无效(必须是1,2或3)") return # 添加任务(包含优先级) self.manager.add_task( data["title"], data["description"], data["due_date"], int(data["priority"]) # 转换为整数 ) self.refresh_list() dialog.destroy() # 保存按钮 ttk.Button(dialog, text="保存", command=save_task).grid( row=len(fields), column=0, columnspan=2, pady=10 ) def delete_task(self): """删除选中任务""" selected = self.tree.selection() if not selected: messagebox.showwarning("警告", "请先选择任务") return item = self.tree.item(selected[0]) task_id = int(item["values"][0]) self.manager.delete_task(task_id) self.refresh_list() def toggle_completion(self): """切换任务完成状态""" selected = self.tree.selection() if not selected: messagebox.showwarning("警告", "请先选择任务") return item = self.tree.item(selected[0]) task_id = int(item["values"][0]) self.manager.toggle_completion(task_id) self.refresh_list() def sort_tasks(self, key): """排序任务""" self.manager.tasks = self.manager.get_sorted_tasks(key) self.refresh_list() def show_incomplete(self): """筛选未完成任务""" self.manager.tasks = self.manager.filter_tasks(show_completed=False) self.refresh_list() def export_data(self): """导出为JSON文件""" filename = filedialog.asksaveasfilename( defaultextension=".json", filetypes=[("JSON files", "*.json")] ) if filename: self.manager.export_to_json(filename) messagebox.showinfo("成功", f"数据已导出到 {filename}") def import_data(self): """从JSON导入""" filename = filedialog.askopenfilename( filetypes=[("JSON files", "*.json")] ) if filename: try: self.manager.import_from_json(filename) self.refresh_list() messagebox.showinfo("成功", "数据导入完成") except Exception as e: messagebox.showerror("错误", f"导入失败: {str(e)}") def load_data(self): """启动时加载数据""" try: with open("tasks.json", "r") as f: data = json.load(f) self.manager.tasks = [Task.from_dict(item) for item in data] if self.manager.tasks: self.manager.next_id = max(t.id for t in self.manager.tasks) + 1 except FileNotFoundError: pass def save_data(self): """退出时保存数据""" with open("tasks.json", "w") as f: data = [task.to_dict() for task in self.manager.tasks] json.dump(data, f, indent=2) def run(self): self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.root.mainloop() def on_close(self): self.save_data() self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = TodoApp(root) app.run() 说明本程序中用到的所有抽象数据类型的定义、主程序的流程以及各程序模块之间的层次(调用)关系。
06-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

满腹的小不甘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值