在基于对话框的应用中执行空闲状态处理(比如用ON_UPDATE_COMMAND_UI更新控件)

本文围绕MFC展开,探讨了基于对话框程序中OnIdle无法工作的问题,指出MFC在模态对话框处理上的变化及WM_ENTERIDLE的局限性,介绍了WM_KICKIDLE作为替代方案。还提及使ON_COMMAND_UPDATE_UI处理函数在对话框中工作的方法,需处理WM_KICKIDLE并调用UpdateDialogControls。

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

June 1995,Microsoft System Journal

 

Paul DiLascia 是一个自由软件顾问,专长是训练和软件开发(C++ and Windows).他是Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992)的作者.

问:我的问题是OnIdle在通常的文档/视图程序中可以工作,但是看起来在基于对话框的程序中不行。我的CApp::InitInstance调用dlg.DoModal,调用一个函数:不调用OnIdle的CWnd::RunModalLoop。我想我应该在WM_ENTERIDLE中做一些后台处理,但是这个消息是发送到对话框的父窗口的。在我的这种情况下,父窗口不存在。请帮忙!

Jim Kallimani

如你所见,“模态”对话框在MFC4.0中实际上是非模态的。当你调用CDialog::DoModal的时候,MFC并不调用::DialogBox,像它以前所做的,而是调用CreateDialogIndirect (在三思之后)然后通过禁用父窗口并且进入自己的消息循环模拟模态行为。这是::DialogBox所做的本质工作。这样做的好处是MFC拥有对话框的消息循环,以前它被Windows API函数::DialogBox隐藏起来了。这样MFC通过通常MFC的消息泵CWinThread::PumpMessage取模态对话框消息,像其他窗口一样。特别的,你可以为模态对话框重载CWnd::PreTranslateMessage—例如实现加速键。MFC的早期版本允许你为模态对话框实现你自己的PreTranslateMessage,但是没有被系统调用,因为CDialog::DoModal直接执行::DialogBox,直到你的对话框消息处理函数调用EndDialog才返回。同样,使用::DialogBox, 使用通常的MFC方法进行消息处理是不可能的,因为程序控制消失在::DialogBox中,直到对话框结束才返回。

作为替代方案, Windows有它自己的机制, WM_ENTERIDLE, 在模态对话框中进行消息处理。当处理完一个或多个消息之后,Windows 发送 WM_ ENTERIDLE 到一个模态对话框或菜单的所有者,如果消息队列中没有等待的消息的话。只有模态对话框发送WM_ENTERIDLE,而非模态对话框不发送。因为MFC现在使用非模态对话框,甚至是在使用模态对话框的时候实际上也是使用非模态对话框,MFC不得不自己发送WM_ENTERIDLE以模仿模态对话框—-但是仅当对话框有父窗口的时候才这么干。Jim碰到麻烦,因为没有父窗口来接收WM_ENTERIDLE。你的头快昏了吗?

如果MFC通过标准消息泵取模态对话框消息,为什么不调用CWinApp::OnIdle作为自己消息处理的一部分?为题是CWnd::RunModalLoop 调用了CWinThread::PumpMessage但是OnIdle在CWinThread::Run中出现。当你的应用程序调用了InitInstance函数之后,MFC调用CWinThread::Run运行你的应用程序。CWinThread::Run的浓缩形式看起来像这样:

// (from THRDCORE.CPP)
int CWinThread::Run()
{
      // 为了空闲状态处理

      BOOL bIdle = TRUE;
      LONG lIdleCount = 0;

      for (;;) {
            while (bIdle && !::PeekMessage(...)) {
                  //当在bIdle状态时调用OnIdle

                      if (!OnIdle(lIdleCount++))
                        // 假设"非空闲" 状态
                        bIdle = FALSE;

            }

            // 获取/预处理/分派消息
            // (调用 CWinThread::PumpMessage)

      }
}

我砍掉了很多,以强调空闲处理如何工作。如果没有消息在等待,MFC重复调用CWinThread::OnIdle,传递给它每次增加的一个计数器参数。你可以使用这个参数区分不同种类的空闲处理的优先次序。你可能在空闲计数为1时作格式化,空闲计数为2时更新一个指示当天时间的时钟。当你的OnIdle返回FALSE时,MFC停止调用它并且等待,直到你的线程得到另一个消息,因此空闲循环从头开始。

模态对话框从不执行这个代码,因为CWnd::RunModalLoop直接在自己的消息循环中调用CWinThread::PumpMessage。它没有调用CWinThread::Run,因此从不调用CWinThread::OnIdle。 Redmond 的人员告诉我这是由设计上决定的。显然,在模态对话框中调用OnIdle是危险的,因为许多消息处理函数建立临时CWnd对象,它们被期望在对话框生存期中存在。默认空闲处理的一部分就是释放临时句柄映射。(译者注:临时CWnd对象依赖于临时句柄映射而存在。.)

(我不得不告诉你,依我所见,整个MFC用来连接HWND和CWnd的临时/永久句柄映射机制是整个架构中的灾难之一,甚至比它们的消息映射还要坏。临时映射机制的问题不断出现在程序中—特别是在多线程应用中,使得他们很难用MFC编写。)

这样看来,你如何在基于对话框的应用程序中进行消息处理,当对话框没有父窗口的时候?幸运的是,它易如反掌。MFC开发者提供一个钩子: WM_KICKIDLE。 RunModalLoop 不断发送这个MFC私有消息,当消息队列中没有消息的时候—就像CWinThread::Run调用OnIdle一样。 RunModalLoop甚至还为你提供一个计数器并且依次递增。实际上,WM_KICKIDLE是对话框的OnIdle替代品。 (历史信息:早期版本的MFC为属性表作这个模态/非模态切换和提供WM_KICKIDLE。显然它工作的如此之好,以至于他们决定使所有的模态对话框非模态化。)

要警告你的是:你可能在OnKickIdle函数中,想调用你的主应用程序的OnIdle函数

LRESULT CMyDlg::OnKickIdle(WPARAM, LPARAM lCount)
{
      return AfxGetApp()->OnIdle(lCount);
}

MFC人员告诉我这是危险的;因为临时映射问题。在OnKickIdle中执行你的空闲处理会更安全一些。如果有必要,你可以组合共有的空闲处理成为一个辅助函数,在CApp::OnIdle 和 CMyDlg::OnKickIdle中调用。

当我在处理空闲处理的问题的时候,发现不是所有程序员都知道CDocTemplate和CDocument的OnIdle函数! 如果你要在文档或文档模板中执行空闲处理,只需重载这些函数。


Internet:

Paul DiLascia
72400.2702@compuserve.com

From the June 1996 issue of Microsoft Systems Journal.

July 1997,Microsoft System Journal

......

下面我将指出如何使ON_COMMAND_UPDATE_UI处理函数在对话框中工作。在通常的MFC文档/视图应用中,MFC使用内部消息WM_IDLEUPDATECMDUI更新菜单项、工具栏按钮、状态栏格等用户界面对象。作为空闲处理的一部分,IDLEUPDATECMDUI广播到所有你的应用程序的窗口。工具栏、状态栏和对话栏的命令处理依次调用UpdateDialogControls广播另一个命令,CN_UPDATE_COMMAND_UI,到窗口上的所有控件。从你的程序员的角度来看,这些消息是不可见的。你只需实现ON_UPDATE_COMMAND_UI处理你的菜单项和按钮,然后,看? 它们被变魔法似的更新了。(需要更多信息的话,参见我的在1995年6月MSJ.上的文章 "Meandering Through the Maze of MFC Message and Command Routing" )
      不幸的是,这个奇妙的UI更新机制不能用于对话框—至少不是自动的。 你必须自己修补一下。幸好它很简单。你只需处理WM_KICKIDLE,一个MFC私有消息;当对话框空闲时发送出来(类似应用程序的OnIdle处理)给.你自己调用UpdateDialogControls。.


 LRESULT CTabDialog::OnKickIdle(WPARAM wp,
                                LPARAM lCount)
 {
     UpdateDialogControls(this, TRUE);
     return 0;
 }

CWnd::UpdateDialogControls发送魔术般的CN_ UPDATE_COMMAND_UI 消息给所有对话框控件,结果是现在ON_COMMAND_ UPDATE_UI处理突然在对话框中可以工作了。
import os import subprocess import shutil import time import tkinter as tk from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage import win32com.client as win32 import threading import tempfile import queue import traceback class DiffProcessorApp: def __init__(self, root): self.root = root root.title("高级文件夹比较工具") root.geometry("1000x700") root.configure(bg="#f5f5f5") # 创建现代风格主题 self.style = ttk.Style() self.style.theme_use('clam') # 自定义主题颜色 self.style.configure('TButton', font=('Segoe UI', 10, 'bold'), borderwidth=1, foreground="#333", background="#4CAF50", bordercolor="#388E3C", relief="flat", padding=8, anchor="center") self.style.map('TButton', background=[('active', '#388E3C'), ('disabled', '#BDBDBD')], foreground=[('disabled', '#9E9E9E')]) self.style.configure('TLabel', font=('Segoe UI', 9), background="#f5f5f5") self.style.configure('TLabelframe', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", relief="flat", borderwidth=2) self.style.configure('TLabelframe.Label', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", foreground="#2E7D32") self.style.configure('Treeview', font=('Segoe UI', 9), rowheight=25) self.style.configure('Treeview.Heading', font=('Segoe UI', 9, 'bold')) # 创建主框架 main_frame = ttk.Frame(root, padding="15") main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 标题区域 header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 15)) # 添加标题图标 try: icon = PhotoImage(file="folder_icon.png") self.icon_label = ttk.Label(header_frame, image=icon) self.icon_label.image = icon self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) except: self.icon_label = ttk.Label(header_frame, text="📁", font=("Arial", 24)) self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) title_label = ttk.Label(header_frame, text="高级文件夹比较工具", font=("Segoe UI", 18, "bold"), foreground="#2E7D32") title_label.pack(side=tk.LEFT) # 文件选择区域 file_frame = ttk.LabelFrame(main_frame, text="文件夹选择", padding="12") file_frame.pack(fill=tk.X, pady=5) # 文件夹选择 self.old_folder_entry, self.new_folder_entry = self.create_folder_selector(file_frame, "原始文件夹:") self.new_folder_entry = self.create_folder_selector(file_frame, "修改后文件夹:")[0] # 比较选项区域 options_frame = ttk.LabelFrame(main_frame, text="比较选项", padding="12") options_frame.pack(fill=tk.X, pady=5) # 递归比较选项 self.recursive_var = tk.BooleanVar(value=True) recursive_check = ttk.Checkbutton(options_frame, text="递归比较子文件夹", variable=self.recursive_var) recursive_check.grid(row=0, column=0, padx=10, pady=5, sticky=tk.W) # 文件过滤 filter_frame = ttk.Frame(options_frame) filter_frame.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="文件过滤:").pack(side=tk.LEFT, padx=(0, 5)) self.filter_var = tk.StringVar(value="*.*") filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_var, width=15) filter_entry.pack(side=tk.LEFT) # 目标Excel选择 excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12") excel_frame.pack(fill=tk.X, pady=5) ttk.Label(excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.excel_file_entry = ttk.Entry(excel_frame, width=60) self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(excel_frame, text="浏览...", command=lambda: self.select_file(self.excel_file_entry, [("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5) # 执行按钮区域 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=10) self.run_button = ttk.Button(button_frame, text="执行比较", command=self.start_processing, width=20, style='TButton') self.run_button.pack(side=tk.LEFT) # 停止按钮 self.stop_button = ttk.Button(button_frame, text="停止", command=self.stop_processing, width=10, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=10) # 进度条 self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=700, mode='determinate') self.progress.pack(fill=tk.X, pady=5) # 状态信息 status_frame = ttk.Frame(main_frame) status_frame.pack(fill=tk.X, pady=5) self.status_var = tk.StringVar(value="准备就绪") status_label = ttk.Label(status_frame, textvariable=self.status_var, font=("Segoe UI", 9), foreground="#2E7D32") status_label.pack(side=tk.LEFT) # 日志和预览区域 notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True, pady=5) # 文件夹结构标签 tree_frame = ttk.Frame(notebook, padding="5") notebook.add(tree_frame, text="文件夹结构") # 创建树形视图 self.tree = ttk.Treeview(tree_frame, columns=("Status"), show="tree") self.tree.heading("#0", text="文件夹结构", anchor=tk.W) self.tree.heading("Status", text="状态", anchor=tk.W) self.tree.column("#0", width=400) self.tree.column("Status", width=100) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") # 日志标签 log_frame = ttk.Frame(notebook, padding="5") notebook.add(log_frame, text="执行日志") self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD, font=("Consolas", 9)) self.log_text.pack(fill=tk.BOTH, expand=True) self.log_text.config(state=tk.DISABLED) # 设置网格权重 tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) # 线程控制 self.processing = False self.queue = queue.Queue() # 启动队列处理 self.root.after(100, self.process_queue) def create_folder_selector(self, parent, label_text): """创建文件夹选择器组件""" frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=5) ttk.Label(frame, text=label_text).grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) entry = ttk.Entry(frame, width=70) entry.grid(row=0, column=1, padx=5, pady=5) button = ttk.Button(frame, text="浏览文件夹...", command=lambda: self.select_folder(entry)) button.grid(row=0, column=2, padx=5, pady=5) return entry, button def select_folder(self, entry): """选择文件夹""" foldername = filedialog.askdirectory() if foldername: entry.delete(0, tk.END) entry.insert(0, foldername) # 自动填充文件夹结构 self.populate_folder_tree(foldername) def select_file(self, entry, filetypes=None): """选择文件""" if filetypes is None: filetypes = [("所有文件", "*.*")] filename = filedialog.askopenfilename(filetypes=filetypes) if filename: entry.delete(0, tk.END) entry.insert(0, filename) def populate_folder_tree(self, path): """填充文件夹结构树""" self.tree.delete(*self.tree.get_children()) if not os.path.isdir(path): return # 添加根节点 root_node = self.tree.insert("", "end", text=os.path.basename(path), values=("文件夹",), open=True) self.add_tree_nodes(root_node, path) def add_tree_nodes(self, parent, path): """递归添加树节点""" try: for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isdir(item_path): node = self.tree.insert(parent, "end", text=item, values=("文件夹",)) self.add_tree_nodes(node, item_path) else: self.tree.insert(parent, "end", text=item, values=("文件",)) except PermissionError: self.log_message(f"权限错误: 无法访问 {path}") def log_message(self, message): """记录日志消息""" self.queue.put(("log", message)) def update_progress(self, value): """更新进度条""" self.queue.put(("progress", value)) def update_status(self, message): """更新状态信息""" self.queue.put(("status", message)) def process_queue(self): """处理线程队列中的消息""" try: while not self.queue.empty(): msg_type, data = self.queue.get_nowait() if msg_type == "log": self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, data + "\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) elif msg_type == "progress": self.progress['value'] = data elif msg_type == "status": self.status_var.set(data) except queue.Empty: pass self.root.after(100, self.process_queue) def process_folders(self, old_path, new_path, excel_file): """处理文件夹比较的线程函数 - 简化流程:生成HTML后直接触发按钮""" output_html = None try: # 步骤1: 生成HTML差异文件 self.update_status("生成HTML差异文件...") self.update_progress(30) # 使用临时文件存储HTML报告 with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as temp_file: output_html = temp_file.name if not self.run_winmerge(old_path, new_path, output_html): self.update_status("WinMerge执行失败") return # 步骤2: 将HTML文件与Excel放在同一目录 self.update_status("复制HTML报告到目标目录...") self.update_progress(60) excel_dir = os.path.dirname(excel_file) if excel_dir: target_html = os.path.join(excel_dir, "diff_report.html") try: shutil.copy(output_html, target_html) self.log_message(f"已将HTML文件复制到: {target_html}") except Exception as e: self.log_message(f"文件复制失败: {str(e)}") # 即使复制失败,我们仍然尝试触发按钮 # 步骤3: 直接触发目标Excel中的"作成"按钮 self.update_status("触发Excel按钮...") self.update_progress(80) if not self.trigger_excel_button(excel_file): self.update_status("触发按钮失败") return # 完成 self.update_progress(100) self.update_status("处理完成!") self.log_message("文件夹比较流程执行完毕") messagebox.showinfo("完成", "已生成HTML报告并触发Excel处理") except Exception as e: error_msg = f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}" self.log_message(error_msg) self.update_status("执行失败") messagebox.showerror("错误", f"处理失败: {str(e)}") finally: # 重新启用执行按钮 if self.processing: self.stop_processing() # 清理临时文件 if output_html and os.path.exists(output_html): try: os.remove(output_html) except: pass def start_processing(self): """启动处理线程 - 修复无响应问题""" if self.processing: self.log_message("警告: 处理正在进行中") return # 获取路径 old_path = self.old_folder_entry.get() new_path = self.new_folder_entry.get() excel_file = self.excel_file_entry.get() # 详细路径验证 validation_errors = [] if not old_path: validation_errors.append("原始文件夹路径为空") elif not os.path.isdir(old_path): validation_errors.append(f"原始文件夹路径无效: {old_path}") if not new_path: validation_errors.append("新文件夹路径为空") elif not os.path.isdir(new_path): validation_errors.append(f"新文件夹路径无效: {new_path}") if not excel_file: validation_errors.append("Excel文件路径为空") elif not excel_file.lower().endswith(('.xlsx', '.xlsm')): validation_errors.append("Excel文件必须是.xlsx或.xlsm格式") if validation_errors: self.log_message("错误: " + "; ".join(validation_errors)) messagebox.showerror("输入错误", "\n".join(validation_errors)) return # 检查WinMerge安装 winmerge_path = r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe" if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge未安装在默认位置 {winmerge_path}") messagebox.showwarning("WinMerge未安装", "请确保WinMerge已安装或更新路径配置") return # 禁用执行按钮,启用停止按钮 self.run_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.processing = True # 启动处理线程 thread = threading.Thread(target=self.process_folders, args=(old_path, new_path, excel_file)) thread.daemon = True thread.start() self.log_message("处理线程已启动") def run_winmerge(self, path1, path2, output_html): """调用WinMerge生成HTML差异文件""" winmerge_path = r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe" # 验证WinMerge可执行文件 if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}") return False winmerge_cmd = [ winmerge_path, '/u', '/dl', 'Base', '/dr', 'Modified', '/or', output_html, path1, path2 ] # 添加递归选项 if self.recursive_var.get(): winmerge_cmd.insert(1, '/r') # 添加文件过滤 file_filter = self.filter_var.get() if file_filter and file_filter != "*.*": winmerge_cmd.extend(['-f', file_filter]) self.log_message(f"执行WinMerge命令: {' '.join(winmerge_cmd)}") try: result = subprocess.run( winmerge_cmd, capture_output=True, text=True, timeout=120, creationflags=subprocess.CREATE_NO_WINDOW ) if result.returncode == 0: self.log_message(f"HTML差异报告生成完成: {output_html}") return True else: error_msg = f"WinMerge执行失败(退出码{result.returncode}): {result.stderr}" self.log_message(error_msg) return False except subprocess.TimeoutExpired: self.log_message("WinMerge执行超时(120秒),请检查输入文件大小") return False except Exception as e: self.log_message(f"WinMerge执行错误: {str(e)}") return False def trigger_excel_button(self, excel_path): """触发目标Excel文件中的按钮""" self.log_message("正在打开Excel并触发按钮...") excel = None workbook = None try: # 验证Excel文件存在 if not os.path.exists(excel_path): self.log_message(f"错误: Excel文件不存在 {excel_path}") return False # 使用win32com打开Excel excel = win32.gencache.EnsureDispatch('Excel.Application') excel.Visible = True excel.DisplayAlerts = False # 打开工作簿 try: workbook = excel.Workbooks.Open(os.path.abspath(excel_path)) except Exception as e: self.log_message(f"打开Excel文件失败: {str(e)}") return False # 检查工作表是否存在 sheet_names = [sheet.Name for sheet in workbook.Sheets] if "一覧" not in sheet_names: self.log_message("错误: Excel文件中缺少'一覧'工作表") return False sheet = workbook.Sheets("一覧") # 触发"作成"按钮 self.log_message("正在触发'作成'按钮...") try: # 查找按钮并点击 button = sheet.Buttons("作成") button.OnAction = "作成按钮的处理" button.Click() self.log_message("已触发'作成'按钮") # 等待处理完成 self.update_status("处理中...请等待") wait_time = 0 max_wait = 60 # 最大等待60秒 while self.processing and wait_time < max_wait: if excel.CalculationState == 0: # 0 = xlDone break time.sleep(1) wait_time += 1 self.log_message(f"处理中...({wait_time}秒)") if wait_time >= max_wait: self.log_message("警告: 处理超时") else: self.log_message("处理完成") return True except Exception as e: self.log_message(f"按钮操作失败: {str(e)}. 请手动点击'作成'按钮") return False except Exception as e: self.log_message(f"Excel操作失败: {str(e)}\n{traceback.format_exc()}") return False finally: # 确保正确关闭Excel try: if workbook: workbook.Close(SaveChanges=True) # 保存更改 if excel: excel.Quit() except Exception as e: self.log_message(f"关闭Excel时出错: {str(e)}") def stop_processing(self): """停止处理""" self.processing = False self.stop_button.config(state=tk.DISABLED) self.run_button.config(state=tk.NORMAL) self.update_status("操作已停止") if __name__ == "__main__": root = tk.Tk() app = DiffProcessorApp(root) root.mainloop() 这是我的代码,我希望完成的操作是 1、用WinMerge生成文件夹比较报告及文件比较报告 2、将生成的报告与目标Excel文件放在同一目录下 3、点击目标Excel文件“一覧”工作表上的“作成”按钮 4、等待处理完成
最新发布
07-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值