Python win32api.keybd_event模拟键盘输入

本文介绍了Python中使用win32api.keybd_event函数模拟键盘输入的方法,包括函数参数解析及示例代码。通过设置不同标志位实现按键按下和释放。此外,还提供了键盘键码对照表的链接。
部署运行你感兴趣的模型镜像

Python  win32api.keybd_event模拟键盘输入


win32api.keybd_event


该函数原型:keybd_event(bVk, bScan, dwFlags, dwExtraInfo)


      第一个参数:虚拟键码(键盘键码对照表见附录);


      第二个参数:硬件扫描码,一般设置为0即可;


      第三个参数:函数操作的一个标志位,如果值为KEYEVENTF_EXTENDEDKEY则该键被按下,也可设置为0即可,如果值为KEYEVENTF_KEYUP则该按键被释放;


      第四个参数:定义与击键相关的附加的32位值,一般设置为0即可。


例子:

import win32api
import win32con
win32api.keybd_event(13,0,0,0)     # enter
win32api.keybd_event(13,0,win32con.KEYEVENTF_KEYUP,0)  #释放按键
 # 按下ctrl+s
    win32api.keybd_event(0x11, 0, 0, 0)
    win32api.keybd_event(0x53, 0, 0, 0)
    win32api.keybd_event(0x53, 0, win32con.KEYEVENTF_KEYUP, 0)
    win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)
    time.sleep(1)
    # 按下回车
    win32api.keybd_event(0x0D, 0, 0, 0)
    win32api.keybd_event(0x0D, 0, win32con.KEYEVENTF_KEYUP, 0)
    time.sleep(1)
    # 按下ctrl+W
    win32api.keybd_event(0x11, 0, 0, 0)
    win32api.keybd_event(0x57, 0, 0, 0)
    win32api.keybd_event(0x57, 0, win32con.KEYEVENTF_KEYUP, 0)
    win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)


# 按下ctrl+a
win32api.keybd_event(0x11, 0, 0, 0)
win32api.keybd_event(0x41, 0, 0, 0)
win32api.keybd_event(0x41, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(1)
# 按下ctrl+v
win32api.keybd_event(0x11, 0, 0, 0)
win32api.keybd_event(0x56, 0, 0, 0)
win32api.keybd_event(0x56, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(1)


更多可参考:http://timgolden.me.uk/pywin32-docs/PyWin32.html


键盘键码对照表:

按键

键码

按键

键码

按键

键码

按键

键码

A

65

6(数字键盘)

102

;

59

:

58

B

66

7(数字键盘)

103

=

61

+

                   43

C

67

8(数字键盘)

104

,

44

60

D

68

9(数字键盘)

105

-

45

_

95

E

69

*

106

.

46

62

F

70

!

33

/

47

?

63

G

71

Enter

13

`

96

~

126

H

72

@

64

[

91

{

123

I

73

#

35

\

92

|

124

J

74

$

36

}

125

]

93

K

75

F1

112

a

97

b

98

L

76

F2

113

c

99

d

100

M

77

F3

114

e

101

f

102

N

78

F4

115

g

103

h

104

O

79

F5

116

i

105

j

106

P

80

F6

117

k

107

l

108

Q

81

F7

118

m

109

n

110

R

82

F8

119

o

111

p

112

S

83

F9

120

q

113

r

114

T

84

F10

121

s

115

t

116

U

85

F11

122

u

117

v

118

V

86

F12

123

w

119

x

120

W

87

Backspace

8

y

121

z

122

X

88

Tab

9

0(数字键盘)

96

Up Arrow

38

Y

89

Clear

12

1(数字键盘)

97

Right Arrow

39

Z

90

Shift

16

2(数字键盘)

98

Down Arrow

40

0(小键盘)

48

Control

17

3(数字键盘)

99

Insert

45

1(小键盘)

49

Alt

18

4(数字键盘)

100

Delete

46

2(小键盘)

50

Cap Lock

20

5(数字键盘)

101

Num Lock

144

3(小键盘)

51

Esc

27

2(数字键盘)

98

Down Arrow

40

4(小键盘)

52

Spacebar

32

3(数字键盘)

99

Insert

45

5(小键盘)

53

Page Up

33

4(数字键盘)

100

Delete

46

6(小键盘)

54

Page Down

34

5(数字键盘)

101

Num Lock

144

7(小键盘)

55

End

35

8(小键盘)

56

Home

36

9(小键盘)

57

Left Arrow

37

# coding=utf-8
from selenium import webdriver
import win32api
import win32con
import win32clipboard
from ctypes import *
import time# 浏览器打开百度网页
browser = webdriver.Chrome()
browser.maximize_window()
browser.get("https://www.baidu.com/")
time.sleep(2)# 获取页面title作为文件名
title = browser.title
# 设置路径为:当前项目的绝对路径+文件名
path = (os.path.dirname(os.path.realpath(__file__)) + "\\" + title + ".html")
# 将路径复制到剪切板
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardText(path)
win32clipboard.CloseClipboard()
# 按下ctrl+s
win32api.keybd_event(0x11, 0, 0, 0)
win32api.keybd_event(0x53, 0, 0, 0)
win32api.keybd_event(0x53, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(1)
# 鼠标定位输入框并点击
windll.user32.SetCursorPos(700, 510)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(1)
# 按下ctrl+a
win32api.keybd_event(0x11, 0, 0, 0)
win32api.keybd_event(0x41, 0, 0, 0)
win32api.keybd_event(0x41, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(1)
# 按下ctrl+v
win32api.keybd_event(0x11, 0, 0, 0)
win32api.keybd_event(0x56, 0, 0, 0)
win32api.keybd_event(0x56, 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(0x11, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(1)
# 按下回车
win32api.keybd_event(0x0D, 0, 0, 0)
win32api.keybd_event(0x0D, 0, win32con.KEYEVENTF_KEYUP, 0)
browser.close()
有个小问题...鼠标定位
windll.user32.SetCursorPos(700, 510)





About Me

........................................................................................................................

● 本文作者:小麦苗,部分内容整理自网络,若有侵权请联系小麦苗删除

● 本文在itpub( http://blog.itpub.net/26736162 )、博客园( http://www.cnblogs.com/lhrbest )和个人weixin公众号( xiaomaimiaolhr )上有同步更新

● 本文itpub地址: http://blog.itpub.net/26736162

● 本文博客园地址: http://www.cnblogs.com/lhrbest

● 本文pdf版、个人简介及小麦苗云盘地址: http://blog.itpub.net/26736162/viewspace-1624453/

● 数据库笔试面试题库及解答: http://blog.itpub.net/26736162/viewspace-2134706/

● DBA宝典今日头条号地址: http://www.toutiao.com/c/user/6401772890/#mid=1564638659405826

........................................................................................................................

● QQ群号: 230161599 (满) 、618766405

● weixin群:可加我weixin,我拉大家进群,非诚勿扰

● 联系我请加QQ好友 646634621 ,注明添加缘由

● 于 2019-05-01 06:00 ~ 2019-05-30 24:00 在魔都完成

● 最新修改时间:2019-05-01 06:00 ~ 2019-05-30 24:00

● 文章内容来源于小麦苗的学习笔记,部分整理自网络,若有侵权或不当之处还请谅解

● 版权所有,欢迎分享本文,转载请保留出处

........................................................................................................................

小麦苗的微店 https://weidian.com/s/793741433?wfr=c&ifr=shopdetail

小麦苗出版的数据库类丛书 http://blog.itpub.net/26736162/viewspace-2142121/

小麦苗OCP、OCM、高可用网络班 http://blog.itpub.net/26736162/viewspace-2148098/

小麦苗腾讯课堂主页 https://lhr.ke.qq.com/

........................................................................................................................

使用 weixin客户端 扫描下面的二维码来关注小麦苗的weixin公众号( xiaomaimiaolhr )及QQ群(DBA宝典)、添加小麦苗weixin, 学习最实用的数据库技术。

........................................................................................................................

欢迎与我联系

 

 



来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/26736162/viewspace-2644877/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/26736162/viewspace-2644877/

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

#软件著作人:杨晨 2025.07.11 import os import subprocess import shutil import time import tkinter as tk from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage import threading import queue import traceback import webbrowser import datetime import configparser import win32com.client import pythoncom import win32gui import win32con import win32api 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('View.TButton', font=('Segoe UI', 10, 'bold'), borderwidth=1, foreground="#008000", # 绿色文本 background="#4CAF50", # 绿色背景 bordercolor="#388E3C", relief="flat", padding=8, anchor="center") self.style.map('View.TButton', background=[('active', '#388E3C'), ('disabled', '#BDBDBD')], foreground=[('active', '#004D00'), ('disabled', '#808080')]) 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.create_folder_selector(file_frame, "原始文件夹:", "old_folder") self.new_folder_entry, _ = self.create_folder_selector(file_frame, "修改后文件夹:", "new_folder") # 比较选项区域 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) # 输出设置区域 self.excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12") self.excel_frame.pack(fill=tk.X, pady=5) # 目标Excel选择 ttk.Label(self.excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.excel_file_entry = ttk.Entry(self.excel_frame, width=60) self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(self.excel_frame, text="浏览...", command=lambda: self.select_file(self.excel_file_entry, "excel_file", [("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5) # WinMerge路径设置 winmerge_frame = ttk.Frame(self.excel_frame) winmerge_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5) ttk.Label(winmerge_frame, text="WinMerge路径:").grid(row=0, column=0, sticky=tk.W) self.winmerge_entry = ttk.Entry(winmerge_frame, width=60) self.winmerge_entry.grid(row=0, column=1, padx=5) self.winmerge_entry.insert(0, r"E:\App\WinMerge\WinMerge2.16.46.0\WinMergeU.exe") # 更新为您的版本 ttk.Button(winmerge_frame, text="浏览...", command=lambda: self.select_file(self.winmerge_entry, "winmerge_path", [("WinMerge 可执行文件", "*.exe")])).grid(row=0, column=2) # 新增:删除临时文件选项 delete_frame = ttk.Frame(self.excel_frame) delete_frame.grid(row=2, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5) self.delete_temp_files_var = tk.BooleanVar(value=False) # 默认不删除 delete_check = ttk.Checkbutton( delete_frame, text="完成后删除临时文件", variable=self.delete_temp_files_var, command=self.update_view_button_state ) delete_check.grid(row=0, column=0, padx=5, sticky=tk.W) # 状态提示标签 self.delete_status_label = ttk.Label( delete_frame, text="(勾选后将删除报告文件,无法查看)", foreground="#FF0000", font=("Segoe UI", 9) ) self.delete_status_label.grid(row=0, column=1, padx=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.view_folder_report_button = ttk.Button(button_frame, text="查看文件夹报告", command=lambda: self.view_report("folder"), width=15, state=tk.DISABLED, style='View.TButton') self.view_folder_report_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.folder_report_path = None self.files_dir = None self.copied_html_files = [] # 存储复制的HTML文件路径 # 配置文件路径 self.config_file = "folder_compare_config.ini" # 加载保存的路径 self.load_paths() # 启动队列处理 self.root.after(100, self.process_queue) def update_view_button_state(self): """根据删除选项更新查看报告按钮状态""" if self.delete_temp_files_var.get(): # 如果勾选了删除选项,按钮应禁用 self.view_folder_report_button.config(state=tk.DISABLED) # 更新按钮样式为禁用状态 self.style.configure('View.TButton', foreground='#808080', background='#BDBDBD') else: # 如果不删除,但报告文件存在,则启用按钮 if self.folder_report_path and os.path.exists(self.folder_report_path): self.view_folder_report_button.config(state=tk.NORMAL) # 更新按钮样式为启用状态(绿色) self.style.configure('View.TButton', foreground='#008000', background='#4CAF50') else: self.view_folder_report_button.config(state=tk.DISABLED) self.style.configure('View.TButton', foreground='#808080', background='#BDBDBD') # 应用样式 self.view_folder_report_button.configure(style='View.TButton') def create_folder_selector(self, parent, label_text, config_key): """创建文件夹选择器组件并绑定保存功能""" 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, config_key)) button.grid(row=0, column=2, padx=5, pady=5) return entry, button def select_folder(self, entry, config_key): """选择文件夹并保存路径,同时设置文件对话框的初始目录""" # 获取上次保存的路径作为初始目录 initial_dir = self.get_last_path(config_key) if not initial_dir or not os.path.exists(initial_dir): initial_dir = os.getcwd() foldername = filedialog.askdirectory(initialdir=initial_dir) if foldername: entry.delete(0, tk.END) entry.insert(0, foldername) self.populate_folder_tree(foldername) self.save_path(config_key, foldername) def select_file(self, entry, config_key, filetypes=None): """选择文件并保存路径,同时设置文件对话框的初始目录""" if filetypes is None: filetypes = [("所有文件", "*.*")] # 获取上次保存的路径作为初始目录 initial_dir = self.get_last_path(config_key) if not initial_dir or not os.path.exists(initial_dir): initial_dir = os.getcwd() # 如果路径是文件,则使用其父目录 if os.path.isfile(initial_dir): initial_dir = os.path.dirname(initial_dir) filename = filedialog.askopenfilename(filetypes=filetypes, initialdir=initial_dir) if filename: entry.delete(0, tk.END) entry.insert(0, filename) self.save_path(config_key, filename) def get_last_path(self, config_key): """获取上次保存的路径""" config = configparser.ConfigParser() if os.path.exists(self.config_file): config.read(self.config_file) if config.has_option('Paths', config_key): return config.get('Paths', config_key) return None 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 view_report(self, report_type): """查看生成的报告""" if report_type == "folder" and self.folder_report_path and os.path.exists(self.folder_report_path): try: webbrowser.open(self.folder_report_path) except Exception as e: messagebox.showerror("错误", f"无法打开文件夹报告: {str(e)}") else: messagebox.showwarning("警告", f"没有可用的{report_type}报告文件") def process_folders(self, old_path, new_path, excel_file): """处理文件夹比较的线程函数""" try: report_dir = os.path.dirname(excel_file) or os.getcwd() os.makedirs(report_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self.folder_report_path = os.path.join(report_dir, f"folder_diff_report_{timestamp}.html") # 生成比较报告 self.update_status("生成比较报告...") self.update_progress(30) winmerge_path = self.winmerge_entry.get() if not self.run_winmerge(winmerge_path, old_path, new_path): self.update_status("WinMerge执行失败") return if not os.path.exists(self.folder_report_path) or os.path.getsize(self.folder_report_path) == 0: self.log_message("警告: 文件夹报告为空或未生成") else: self.log_message(f"文件夹报告生成成功: {self.folder_report_path} ({os.path.getsize(self.folder_report_path)} bytes)") self.copy_detail_files(report_dir) self.update_status("打开Excel文件...") self.update_progress(80) if not self.open_excel_file(excel_file): self.update_status("打开Excel失败") return # 根据用户选择决定是否删除临时文件 if self.delete_temp_files_var.get(): self.delete_winmerge_reports() self.log_message("已删除所有临时文件") else: # 不删除文件,启用查看报告按钮 self.log_message("保留临时文件,可查看报告") # 更新按钮状态 self.root.after(100, self.update_view_button_state) self.update_progress(100) self.update_status("处理完成!") self.log_message("文件夹比较流程执行完毕") messagebox.showinfo("完成", "已生成比较实施文件") 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() def delete_winmerge_reports(self): """删除WinMerge生成的报告文件、.files目录和复制的HTML文件""" if not self.folder_report_path: return # 1. 删除主报告文件 if os.path.exists(self.folder_report_path): try: os.remove(self.folder_report_path) self.log_message(f"已删除报告文件: {self.folder_report_path}") except Exception as e: self.log_message(f"删除报告文件失败: {str(e)}") # 2. 删除对应的.files目录 base_path = os.path.splitext(self.folder_report_path)[0] files_dir = base_path + ".files" if os.path.exists(files_dir): try: shutil.rmtree(files_dir) self.log_message(f"已删除.files目录: {files_dir}") except Exception as e: self.log_message(f"删除.files目录失败: {str(e)}") # 3. 删除本次运行复制的HTML文件 report_dir = os.path.dirname(self.folder_report_path) if os.path.exists(report_dir) and hasattr(self, 'copied_html_files') and self.copied_html_files: deleted_count = 0 for file_path in self.copied_html_files: if os.path.exists(file_path): try: os.remove(file_path) deleted_count += 1 self.log_message(f"已删除复制的HTML文件: {file_path}") except Exception as e: self.log_message(f"删除HTML文件失败: {file_path} - {str(e)}") self.log_message(f"已删除 {deleted_count}/{len(self.copied_html_files)} 个复制的HTML文件") # 清空列表 self.copied_html_files = [] # 4. 重置状态并禁用查看按钮 self.folder_report_path = None self.view_folder_report_button.config(state=tk.DISABLED) # 更新按钮状态 self.root.after(100, self.update_view_button_state) def copy_detail_files(self, report_dir): """复制.files目录中的HTML文件到报告目录,并记录复制的文件""" base_path = os.path.splitext(self.folder_report_path)[0] files_dir = base_path + ".files" if not os.path.exists(files_dir): self.log_message(f"警告: 详细文件目录不存在 {files_dir}") return html_files = [f for f in os.listdir(files_dir) if f.lower().endswith('.html')] if not html_files: self.log_message(f"警告: 详细文件目录中没有HTML文件 {files_dir}") return # 初始化复制的文件列表 if not hasattr(self, 'copied_html_files') or not self.copied_html_files: self.copied_html_files = [] copied_count = 0 for file_name in html_files: src_path = os.path.join(files_dir, file_name) dst_path = os.path.join(report_dir, file_name) try: shutil.copy2(src_path, dst_path) copied_count += 1 # 记录复制的文件路径 self.copied_html_files.append(dst_path) except Exception as e: self.log_message(f"复制文件失败: {file_name} - {str(e)}") self.log_message(f"已复制 {copied_count}/{len(html_files)} 个详细HTML文件到报告目录") 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格式") winmerge_path = self.winmerge_entry.get() if not winmerge_path or not os.path.exists(winmerge_path): validation_errors.append("WinMerge路径无效或未设置") if validation_errors: self.log_message("错误: " + "; ".join(validation_errors)) messagebox.showerror("输入错误", "\n".join(validation_errors)) return self.run_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.view_folder_report_button.config(state=tk.DISABLED) self.processing = True # 重置复制的文件列表 self.copied_html_files = [] 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, winmerge_path, path1, path2): """针对WinMerge 2.16.46.0优化的报告生成方法""" if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}") return False os.makedirs(os.path.dirname(self.folder_report_path), exist_ok=True) # 更新命令行参数以兼容WinMerge 2.16.46.0 cmd = [ winmerge_path, '/u', # 禁用单实例模式 '/nosplash', # 不显示启动画面 '/dl', 'Base', # 左侧标题 '/dr', 'Modified', # 右侧标题 '/noninteractive', # 非交互模式 '/minimize' # 最小化窗口 ] # 递归选项 - 使用新版本的参数 if self.recursive_var.get(): cmd.extend(['/r', '/s']) # 启用递归比较 else: cmd.extend(['/r-', '/s-']) # 显式禁用递归比较 # 文件过滤 file_filter = self.filter_var.get() if file_filter and file_filter != "*.*": cmd.extend(['-f', file_filter]) # 输出报告路径 cmd.extend(['/or', self.folder_report_path]) # 添加要比较的路径 cmd.extend([path1, path2]) self.update_status("正在生成比较报告...") return self.execute_winmerge_command(cmd, "比较报告") def execute_winmerge_command(self, cmd, report_type): """执行WinMerge命令并处理结果""" try: self.log_message(f"开始生成{report_type}...") self.log_message(f"执行命令: {' '.join(cmd)}") start_time = time.time() # 创建进程标志 creation_flags = 0 if os.name == 'nt': creation_flags = subprocess.CREATE_NO_WINDOW process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags ) timeout = 900 # 15分钟超时 try: stdout, stderr = process.communicate(timeout=timeout) except subprocess.TimeoutExpired: process.kill() stdout, stderr = process.communicate() self.log_message(f"{report_type}生成超时({timeout}秒),已终止进程") return False elapsed = time.time() - start_time self.log_message(f"{report_type}生成完成,耗时: {elapsed:.2f}秒") # 记录输出 if stdout.strip(): self.log_message(f"WinMerge输出:\n{stdout[:2000]}") if stderr.strip(): self.log_message(f"WinMerge错误:\n{stderr[:1000]}") # 处理返回码 if process.returncode == 0: self.log_message(f"{report_type}命令执行成功") return True elif process.returncode == 1: self.log_message(f"{report_type}命令执行完成(发现差异)") return True elif process.returncode == 2: self.log_message(f"{report_type}命令执行完成(发现错误)") return False else: error_msg = f"{report_type}生成失败(退出码{process.returncode})" self.log_message(error_msg) return False except Exception as e: self.log_message(f"{report_type}生成错误: {str(e)}\n{traceback.format_exc()}") return False def send_enter_key(): # 查找活动窗口(通常是弹出的对话框) hwnd = win32gui.GetForegroundWindow() if hwnd: # 发送Enter键 win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0) win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_RETURN, 0) def open_excel_file(self, excel_path): self.log_message("正在后台自动化处理Excel文件...") # 初始化COM库 pythoncom.CoInitialize() try: if not os.path.exists(excel_path): self.log_message(f"错误: Excel文件不存在 {excel_path}") return False excel_app = None try: # 创建Excel应用程序对象 excel_app = win32com.client.Dispatch("Excel.Application") excel_app.Visible = True excel_app.WindowState = -4140 # win32com.client.constants.xlMinimized excel_app.DisplayAlerts = False excel_app.ScreenUpdating = False excel_app.EnableEvents = False # 打开工作簿 wb = excel_app.Workbooks.Open(os.path.abspath(excel_path)) self.log_message(f"Excel文件已打开: {excel_path}") # 自动执行宏并处理后续交互 success = self.execute_macro_with_automation(excel_app, wb) # 保存并关闭工作簿 if success: wb.Close(SaveChanges=True) self.log_message("Excel文件处理完成并保存") else: wb.Close(SaveChanges=False) self.log_message("宏执行失败,已放弃更改") return success except Exception as e: error_msg = f"Excel自动化失败: {str(e)}" self.log_message(error_msg) self.log_message(traceback.format_exc()) return False finally: # 确保释放Excel对象 if excel_app is not None: try: excel_app.Quit() # 确保Excel进程完全退出 time.sleep(1) # 额外的清理操作 del excel_app except: pass finally: # 确保取消初始化COM库 pythoncom.CoUninitialize() def execute_macro_with_automation(self, excel_app, workbook): """异步执行宏并监控对话框 - 增强版""" try: # 尝试多次获取窗口句柄 hwnd = None for i in range(5): # 增加尝试次数 hwnd = self.find_excel_window(excel_app) if hwnd: break self.log_message(f"尝试 {i+1}/5 获取Excel窗口句柄") time.sleep(1) # 等待1秒再试 if not hwnd: self.log_message("错误: 多次尝试后仍无法获取Excel窗口句柄!") return False # 创建事件标志用于线程通信 self.macro_finished = threading.Event() # 启动宏执行监控线程 monitor_thread = threading.Thread( target=self.monitor_macro_execution, args=(excel_app, workbook, hwnd) ) monitor_thread.daemon = True monitor_thread.start() self.log_message("宏监控线程已启动") # 执行宏 try: self.log_message("开始执行宏...") excel_app.Run("Sheet1.ExposedMacro") self.log_message("宏执行完成") except Exception as e: self.log_message(f"宏执行异常: {str(e)}") finally: # 标记宏执行完成 self.macro_finished.set() self.log_message("已设置宏完成标志") # 等待监控线程完成 monitor_thread.join(timeout=60) # 最多等待60秒 if monitor_thread.is_alive(): self.log_message("警告: 宏监控线程超时") return True except Exception as e: self.log_message(f"宏执行框架错误: {str(e)}\n{traceback.format_exc()}") return False def monitor_macro_execution(self, excel_app, workbook, hwnd): """增强的宏执行监控逻辑,确保所有对话框都被处理""" self.log_message("启动宏执行监控线程(增强版)...") # 配置对话框处理参数 max_wait_time = 300 # 5分钟最大等待时间 dialog_cooldown = 0.5 # 对话框处理后的冷却时间 last_dialog_time = 0 # 记录上次处理对话框的时间 # 对话框处理状态跟踪 handled_dialogs = set() dialog_retry_count = {} # 主监控循环 start_time = time.time() while not self.macro_finished.is_set() and time.time() - start_time < max_wait_time: try: current_time = time.time() # 仅在冷却期结束后检查新对话框 if current_time - last_dialog_time < dialog_cooldown: time.sleep(0.1) continue # 查找当前活动对话框 dialog_hwnd = self.find_dialog_window(hwnd) if dialog_hwnd: self.log_message(f"test1") dialog_title = win32gui.GetWindowText(dialog_hwnd) dialog_id = f"{dialog_hwnd}_{dialog_title}" self.log_message(f"检测到对话框: {dialog_title}") # 跳过最近处理过的对话框 if dialog_id in handled_dialogs: self.log_message(f"对话框已处理过,跳过: {dialog_title}") time.sleep(0.5) continue # 处理确认对话框 if "Winmerge差分ファイルまとめるツール" in dialog_title: self.log_message(">>> 处理确认对话框 <<<") if self.handle_confirmation_dialog(dialog_hwnd): handled_dialogs.add(dialog_id) last_dialog_time = current_time # 确认后可能需要等待路径对话框出现 time.sleep(2) else: self.log_message("确认对话框处理失败,将重试") # 处理路径选择对话框 elif "一覧ファイル(*.html)を選択" in dialog_title or "Select" in dialog_title: self.log_message(">>> 处理路径选择对话框 <<<") if self.fill_path_dialog(dialog_hwnd): handled_dialogs.add(dialog_id) last_dialog_time = current_time # 路径选择后可能需要等待后续处理 time.sleep(3) else: self.log_message("路径选择对话框处理失败,将重试") # 处理其他未知对话框 else: self.log_message(">>> 处理未知类型对话框 <<<") if self.handle_unknown_dialog(dialog_hwnd): handled_dialogs.add(dialog_id) last_dialog_time = current_time else: self.log_message("未知对话框处理失败") # 如果没有对话框,检查宏是否已完成 else: if self.is_macro_completed(excel_app): self.log_message("宏执行已完成") break # 短暂休眠避免CPU占用过高 time.sleep(0.3) except Exception as e: error_msg = f"对话框监控错误: {str(e)}\n{traceback.format_exc()}" self.log_message(error_msg) time.sleep(1) # 出错后短暂暂停 if self.macro_finished.is_set(): self.log_message("宏执行监控正常结束") else: self.log_message(f"警告: 宏执行监控超时 ({max_wait_time}秒)") def handle_confirmation_dialog(self, hwnd): """专门处理确认对话框""" try: # 确保对话框在前台 win32gui.SetForegroundWindow(hwnd) time.sleep(0.3) # 查找确认按钮 confirm_button = self.find_dialog_button(hwnd, ["确定", "OK", "Yes", "はい"]) if confirm_button: win32gui.PostMessage(confirm_button, win32con.BM_CLICK, 0, 0) self.log_message(f"已点击确认按钮: {win32gui.GetWindowText(confirm_button)}") return True else: # 备选方案:模拟回车键 win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0) win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0) self.log_message("使用回车键确认对话框") return True except Exception as e: self.log_message(f"确认对话框处理失败: {str(e)}") return False def fill_path_dialog(self, dialog_hwnd): """增强的路径选择对话框处理方法""" try: # 确保对话框处于活动状态 win32gui.SetForegroundWindow(dialog_hwnd) time.sleep(0.5) # 获取完整报告文件路径 report_path = self.folder_report_path # 方法1: 尝试通过控件设置路径 if self.try_set_path_via_controls(dialog_hwnd, report_path): return True # 方法2: 使用键盘导航和输入路径 self.log_message("尝试键盘导航方式输入路径") return self.set_path_via_keyboard(dialog_hwnd, report_path) except Exception as e: self.log_message(f"路径对话框处理失败: {str(e)}") return False def try_set_path_via_controls(self, dialog_hwnd, report_path): """尝试通过查找控件设置路径""" try: # 查找所有编辑框 edit_controls = [] def find_edit_controls(hwnd, results): if win32gui.GetClassName(hwnd) == "Edit": results.append(hwnd) win32gui.EnumChildWindows(dialog_hwnd, find_edit_controls, edit_controls) if not edit_controls: self.log_message("未找到编辑框控件") return False # 尝试在最后一个编辑框中设置路径(通常是文件路径框) file_edit = edit_controls[-1] win32gui.SendMessage(file_edit, win32con.WM_SETTEXT, 0, report_path) self.log_message(f"已设置路径: {report_path}") # 查找并点击"打开"按钮 open_button = self.find_dialog_button(dialog_hwnd, ["打开", "Open", "開く"]) if open_button: win32gui.PostMessage(open_button, win32con.BM_CLICK, 0, 0) self.log_message(f"已点击打开按钮: {win32gui.GetWindowText(open_button)}") return True # 如果找不到按钮,尝试回车键确认 win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0) win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0) self.log_message("使用回车键确认路径选择") return True except Exception as e: self.log_message(f"控件方式设置路径失败: {str(e)}") return False def set_path_via_keyboard(self, dialog_hwnd, report_path): """使用键盘输入路径""" try: # 切换到对话框 win32gui.SetForegroundWindow(dialog_hwnd) time.sleep(0.3) # 导航到文件路径输入框 win32api.keybd_event(win32con.VK_ALT, 0, 0, 0) # 按下Alt time.sleep(0.1) win32api.keybd_event(win32con.VK_N, 0, 0, 0) # 按下N win32api.keybd_event(win32con.VK_N, 0, win32con.KEYEVENTF_KEYUP, 0) time.sleep(0.1) win32api.keybd_event(win32con.VK_ALT, 0, win32con.KEYEVENTF_KEYUP, 0) # 释放Alt time.sleep(0.5) # 全选现有内容并删除 win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0) # Ctrl+A win32api.keybd_event(win32con.VK_A, 0, 0, 0) win32api.keybd_event(win32con.VK_A, 0, win32con.KEYEVENTF_KEYUP, 0) win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0) time.sleep(0.2) win32api.keybd_event(win32con.VK_DELETE, 0, 0, 0) # 删除 win32api.keybd_event(win32极con.VK_DELETE, 0, win32con.KEYEVENTF_KEYUP, 0) time.sleep(0.2) # 输入完整路径 for char in report_path: if char == '\\': win32api.keybd_event(win32con.VK_BACKSLASH, 0, 0, 0) win32api.keybd_event(win32con.VK_BACKSLASH, 0, win32con.KEYEVENTF_KEYUP, 0) else: vk_code = win32api.VkKeyScan(char) win32api.keybd_event(vk_code & 0xFF, 0, 0, 0) win32api.keybd_event(vk_code & 0xFF, 0, win32con.KEYEVENTF_KEYUP, 0) time.sleep(0.02) self.log_message(f"已输入路径: {report_path}") # 确认选择 time.sleep(0.5) win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0) win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0) return True except Exception as e: self.log_message(f"键盘输入失败: {str(e)}") return False def find_dialog_button(self, dialog_hwnd, button_texts): """查找对话框中的特定按钮""" try: buttons = [] def enum_buttons(hwnd, results): if "Button" in win32gui.GetClassName(hwnd): text = win32gui.GetWindowText(hwnd) results.append((hwnd, text)) win32gui.EnumChildWindows(dialog_hwnd, enum_buttons, buttons) # 优先匹配特定文本的按钮 for hwnd, text in buttons: for btn_text in button_texts: if btn_text in text: return hwnd # 如果没有匹配项,返回第一个按钮 if buttons: return buttons[0][0] return None except Exception as e: self.log_message(f"查找按钮失败: {str(e)}") return None def is_macro_completed(self, excel_app): """检查宏是否已完成""" try: # 方法1: 检查Excel是否就绪 if excel_app.Ready: # 方法2: 检查是否有打开的对话框 if excel_app.Dialogs.Count == 0: return True return False except: return False def find_excel_window(self, excel_app): """获取Excel应用程序窗口句柄""" try: # 方法1:通过窗口标题匹配 titles = [] def enum_windows_callback(hwnd, results): if win32gui.IsWindowVisible(hwnd): title = win32gui.GetWindowText(hwnd) # 匹配Excel主窗口标题的模式(通常包含"Excel") if "Excel" in title and "Book" in title: results.append(hwnd) win32gui.EnumWindows(enum_windows_callback, titles) if titles: return titles[0] # 返回第一个匹配的窗口 # 方法2:如果没有找到,尝试通过类名 class_names = [] def enum_class_callback(hwnd, results): if win32gui.IsWindowVisible(hwnd): class_name = win32gui.GetClassName(hwnd) if class_name == "XLMAIN": results.append(hwnd) win32gui.EnumWindows(enum_class_callback, class_names) if class_names: return class_names[0] # 方法3:作为备选,尝试获取活动窗口 hwnd = win32gui.GetForegroundWindow() if hwnd: title = win32gui.GetWindowText(hwnd) if "Excel" in title: return hwnd self.log_message("警告: 未找到Excel窗口") return None except Exception as e: self.log_message(f"查找Excel窗口失败: {str(e)}") return None def find_dialog_window(self, parent_hwnd): """精确查找特定对话框窗口""" try: specific_titles = [ "Winmerge差分ファイルまとめるツール", "一覧ファイル(*.html)を選択" ] dialogs = [] def enum_windows_callback(hwnd, results): if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): title = win32gui.GetWindowText(hwnd) # 只匹配特定标题 for spec_title in specific_titles: if spec_title in title: results.append(hwnd) return # 找到匹配即返回 # 先在父窗口内查找 win32gui.EnumChildWindows(parent_hwnd, enum_windows_callback, dialogs) if dialogs: return dialogs[0] # 如果未找到,在所有顶层窗口中查找 all_dialogs = [] win32gui.EnumWindows(enum_windows_callback, all_dialogs) return all_dialogs[0] if all_dialogs else None except Exception as e: self.log_message(f"查找对话框失败: {str(e)}") return None def stop_processing(self): """停止处理""" self.processing = False self.stop_button.config(state=tk.DISABLED) self.run_button.config(state=tk.NORMAL) # 更新按钮状态 self.root.after(100, self.update_view_button_state) self.update_status("操作已停止") def load_paths(self): """从配置文件加载保存的路径""" config = configparser.ConfigParser() if os.path.exists(self.config_file): try: config.read(self.config_file) # 原始文件夹路径 if config.has_option('Paths', 'old_folder'): old_path = config.get('Paths', 'old_folder') self.old_folder_entry.delete(0, tk.END) self.old_folder_entry.insert(0, old_path) if os.path.isdir(old_path): self.populate_folder_tree(old_path) # 修改后文件夹路径 if config.has_option('Paths', 'new_folder'): new_path = config.get('Paths', 'new_folder') self.new_folder_entry.delete(0, tk.END) self.new_folder_entry.insert(0, new_path) # Excel文件路径 if config.has_option('Paths', 'excel_file'): excel_path = config.get('Paths', 'excel_file') self.excel_file_entry.delete(0, tk.END) self.excel_file_entry.insert(0, excel_path) # WinMerge路径 if config.has_option('Paths', 'winmerge_path'): winmerge_path = config.get('Paths', 'winmerge_path') self.winmerge_entry.delete(0, tk.END) self.winmerge_entry.insert(0, winmerge_path) self.log_message("已加载上次保存的路径") except Exception as e: self.log_message(f"加载配置文件失败: {str(e)}") else: self.log_message("未找到配置文件,将使用默认路径") def save_path(self, key, path): """保存单个路径到配置文件""" config = configparser.ConfigParser() if os.path.exists(self.config_file): config.read(self.config_file) if not config.has_section('Paths'): config.add_section('Paths') config.set('Paths', key, path) try: with open(self.config_file, 'w') as configfile: config.write(configfile) self.log_message(f"已保存路径: {key} = {path}") except Exception as e: self.log_message(f"保存路径失败: {str(e)}") def save_all_paths(self): """保存所有路径到配置文件""" config = configparser.ConfigParser() config.add_section('Paths') config.set('Paths', 'old_folder', self.old_folder_entry.get()) config.set('Paths', 'new_folder', self.new_folder_entry.get()) config.set('Paths', 'excel_file', self.excel_file_entry.get()) config.set('Paths', 'winmerge_path', self.winmerge_entry.get()) try: with open(self.config_file, 'w') as configfile: config.write(configfile) self.log_message("所有路径已保存") except Exception as e: self.log_message(f"保存所有路径失败: {str(e)}") def on_closing(self): """窗口关闭时的处理""" self.save_all_paths() self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = DiffProcessorApp(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop() 弹出的处理窗口好像是wps选择文件的窗口,进行查找时显示找不到窗口
最新发布
09-10
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值