项目地址

Github: competition_tool

主要功能

对整个电脑屏幕进行截图,然后上传截取到的屏幕图片到局域网下的的FTP服务器(ftp://192.168.1.1/),供其他用户下载使用

使用方法

以下两种方法都可触发截图事件:

  • Caps Lock 长按 + 鼠标单击:先长按Caps Lock ,然后单击一次鼠标左键完成截图。截图完成后松开Caps Lock键,需要继续截图时重复上述过程。
  • 2秒内连续四次单次鼠标滚轮:在两秒内连续四次鼠标滚轮完成截图,滚轮上下滚动不影响截图。

注意事项

  • 使用程序时最好以管理员身份运行,确保程序拥有较高权限。
  • 如果使用双屏,无法截取副屏的内容,即使把鼠标焦点移动到副屏上,满足截图条件也只会截取主屏的画面。

用途

可以在某些比赛中将赛题截图然后上传到FTP服务器上,请其他人帮忙搜题解题(有摄像头或屏幕受到监控的情况下)

也可以在招聘的笔试、面试中使用

程序参数说明

本地保存路径:本地PC保存截图的路径,先将截取到的屏幕图片保存到本地PC,然后才上传到FTP服务器

FTP服务器文件夹名称:FTP存放截图的文件夹,如果在FTP服务器上已有个人文件夹直接填入文件夹名称即可,如果没有个人文件夹,填入名称后会自动在FTP服务器上创建下面填入的文件夹,例如lhf

程序运行界面

FTP服务器界面

项目打包

要将 Python 代码打包成一个独立的可执行文件(.exe),可以使用 PyInstaller 模块。以下是详细步骤:

项目结构

1
2
3
4
5
6
项目文件夹/

├── screenshot_gui.py # 主文件(GUI代码)
├── upload.py # FTP上传代码
├── screenshot.py # 监听器逻辑
└── assets/ # 可选的资源文件夹(如图标等)

执行打包命令

1
pyinstaller --onefile --windowed --add-data "upload.py;." --add-data "screenshot.py;." screenshot_gui.py

参数解释:

  • **--onefile**:将所有内容打包为一个独立的可执行文件。
  • **--windowed**:在 Windows 系统上运行时,不显示控制台窗口(适用于 GUI 程序)。
  • **--add-data**:将其他依赖文件一起打包。格式为 "文件路径;目标路径",在 Windows 下用 ; 分隔。
  • **screenshot_gui.py**:主程序入口文件。

检查打包结果

打包完成后,会生成一个 dist/ 文件夹,其中包含一个可执行文件(.exe)可以双击该 .exe 文件来运行程序

1
2
dist/
└── screenshot_gui.exe # 这是生成的可执行文件

发布可执行文件到github

dist/ 文件夹中的 .exe 文件发布到github仓库上

点击仓库右边的release,添加标签,这里新建标签v1.0,简单写一下这版的主要功能,然后把competition_tool_v1.0.exe从本地PC文件夹拖动到web页面的指定位置,就能上传到github仓库上了。

发布github release

程序打包补充说明

在使用 PyInstaller 打包 Python 程序时,会生成两个主要的文件夹:**build/** 和 dist/

build/ 文件夹是打包过程中的缓存文件目录。生成它是正常现象,并且可以在打包完成后手动删除。最终需要的是 dist/ 文件夹中的可执行文件。

build/ 文件夹:

  • 用途build/ 文件夹是 PyInstaller 在打包过程中生成的临时构建文件的目录。

  • 内容:其中包含了与打包相关的中间文件,如 .spec 文件、分析的模块依赖数据等。这些文件是用于构建过程中的缓存数据。

  • 作用:帮助 PyInstaller 分析你的代码和依赖关系。下次运行相同打包命令时,它会利用这些缓存文件加快打包速度。

  • build/ 文件夹在打包完成后可以安全删除,因为它只包含临时数据,不影响生成的 .exe 文件的运行。

dist/ 文件夹:

  • 用途dist/ 文件夹中存放的是最终的打包结果,即生成的 .exe 文件和必要的依赖项。
  • 内容:该文件夹中会包含你的可执行文件(如 screenshot_gui.exe)以及任何其他资源(在某些情况下,如果未使用 --onefile,也可能包括依赖库)。

成功打包代码

项目源码

如果没有打包成exe可执行文件,也可以下载相应的包直接在python的IDE中使用,如果要跑源代码,直接运行screenshot_gui.py即可

screenshot_gui.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import threading
from tkinter import Tk, Label, Entry, Button, filedialog, StringVar, messagebox
from upload import check_ftp_connection # 导入 FTP 检测函数
from screenshot import start_listeners # 从 screenshot.py 导入逻辑
import pystray
from PIL import Image, ImageDraw

# 初始化窗口
window = Tk()
window.title("Assistant")

# 创建变量来存储用户输入的信息
save_directory = StringVar()
ftp_folder = StringVar()

# 选择文件夹函数
def select_folder():
"""
选择截图本地保存路径

调用文件对话框让用户选择截图保存的目录,并将选择的目录设置到 save_directory 变量中。

参数:


返回:

"""
folder = filedialog.askdirectory()
if folder:
save_directory.set(folder)

# 启动按钮点击事件
def on_submit():
"""
启动按钮点击事件处理函数

当用户点击启动按钮时,检查用户是否输入了所有必要的信息,然后检测 FTP 连接是否成功。
如果连接成功,禁用启动按钮并更改其文本,然后启动一个新线程来运行监听器。

参数:


返回:

"""
if not save_directory.get() or not ftp_folder.get():
messagebox.showerror("error", "请填写所有信息!")
return

# 检测 FTP 连接
folder_name = ftp_folder.get()
if not check_ftp_connection(folder_name):
messagebox.showerror("FTP Error", "无法连接到 FTP 服务器,请检查服务器信息。")
return

# 成功连接后禁用按钮并更改文本
submit_button.config(text="程序运行中", state="disabled")

# 启动监听器线程(保持 GUI 可见)
local_save_path = save_directory.get()
threading.Thread(
target=start_listeners, args=(local_save_path, folder_name), daemon=True
).start()

# 显示窗口
def show_window(icon, item):
"""
显示窗口

当用户从托盘图标菜单中选择“显示窗口”时,调用此函数以显示主窗口。

参数:
icon (pystray.Icon): 托盘图标对象
item (pystray.MenuItem): 被点击的菜单项

返回:

"""
window.deiconify()

# 用户关闭窗口时隐藏到托盘
def on_close():
"""
用户关闭窗口时隐藏到托盘

当用户点击窗口的关闭按钮时,调用此函数以隐藏主窗口到托盘。

参数:


返回:

"""
window.withdraw()

# 退出程序
def on_exit(icon, item):
"""
退出程序

当用户从托盘图标菜单中选择“退出”时,调用此函数以退出程序。

参数:
icon (pystray.Icon): 托盘图标对象
item (pystray.MenuItem): 被点击的菜单项

返回:

"""
icon.stop()
window.quit()

# 创建托盘图标
def create_tray_icon():
"""
创建托盘图标

创建一个托盘图标,并设置其菜单选项为“显示窗口”和“退出”。

参数:


返回:

"""
image = Image.new('RGB', (64, 64), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
draw.text((10, 20), "Assistant", fill="black")

icon = pystray.Icon("Screenshot App", image, "Assistant", menu=pystray.Menu(
pystray.MenuItem("显示窗口", show_window),
pystray.MenuItem("退出", on_exit)
))
icon.run()

# 启动托盘图标线程
def start_tray_icon():
"""
启动托盘图标线程

启动一个新线程来运行托盘图标,以避免阻塞主 GUI 线程。

参数:


返回:

"""
threading.Thread(target=create_tray_icon, daemon=True).start()

# 界面布局
Label(window, text="截图本地保存路径:").grid(row=0, column=0, padx=10, pady=5)
Entry(window, textvariable=save_directory).grid(row=0, column=1, padx=10, pady=5)
Button(window, text="选择", command=select_folder).grid(row=0, column=2, padx=10, pady=5)

Label(window, text="FTP服务器文件夹名称:").grid(row=1, column=0, padx=15, pady=5)
Entry(window, textvariable=ftp_folder).grid(row=1, column=1, padx=10, pady=5)

submit_button = Button(window, text="启动", command=on_submit)
submit_button.grid(row=2, column=1, pady=10)

# 用户点击 X 按钮时最小化到托盘
window.protocol("WM_DELETE_WINDOW", on_close)

if __name__ == "__main__":
start_tray_icon() # 启动托盘图标线程
window.mainloop()

uploa.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from ftplib import FTP
import os

# FTP 服务器配置
FTP_SERVER = "192.168.1.1" # FTP 服务器地址
FTP_USER = "anonymous" # 匿名用户模式
FTP_PASSWORD = "" # 匿名无需密码

def upload_to_ftp(local_file, folder):
"""
将截图上传到指定 FTP 文件夹

参数:
local_file (str): 本地文件路径,用来保存屏幕截图
folder (str): FTP 服务器上的目标文件夹路径,用于指定截图在FTP服务器上的位置
返回:
None
"""
try:
# 连接 FTP 服务器
ftp = FTP(FTP_SERVER)
ftp.login(user=FTP_USER, passwd=FTP_PASSWORD)

# 切换或创建目标文件夹
try:
ftp.cwd(folder)
except:
print(f"文件夹 {folder} 不存在,正在创建...")
ftp.mkd(folder)
ftp.cwd(folder)

# 上传文件
with open(local_file, "rb") as file:
ftp.storbinary(f"STOR {os.path.basename(local_file)}", file)
print(f"成功上传 {local_file}{FTP_SERVER}/{folder}/")

# 关闭 FTP 连接
ftp.quit()

except Exception as e:
print(f"FTP 上传失败:{e}")



def check_ftp_connection(folder):
"""
检测是否能连接到 FTP 服务器并切换到指定文件夹

参数:
folder (str): FTP 服务器上的目标文件夹路径

返回:
bool: 是否成功连接并切换到指定文件夹
"""
try:
ftp = FTP(FTP_SERVER)
ftp.login(user=FTP_USER, passwd=FTP_PASSWORD)

# 尝试切换到目标文件夹
ftp.cwd(folder)
ftp.quit()
return True
except Exception as e:
print(f"FTP 连接失败:{e}")
return False

screenshot.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import pyautogui
import os
import time
from pynput import mouse, keyboard
from upload import upload_to_ftp

# 初始化状态和事件记录
capslock_pressed = False # Caps Lock 是否按压状态
screenshot_count = 1 # 截图计数
TIME_WINDOW = 2 # 时间窗口(秒)
wheel_click_times = [] # 存储鼠标滚轮单击事件时间

def take_screenshot_and_upload(i, save_directory, ftp_folder):
"""
截取屏幕并上传

参数:
i (int): 截图的序号
save_directory (str): 保存截图的目录
ftp_folder (str): FTP 服务器上的文件夹

返回:
None
"""
screenshot_path = os.path.join(save_directory, f"screenshot_{i}.png")
screenshot = pyautogui.screenshot()
screenshot.save(screenshot_path)
print(f"截图保存为: {screenshot_path}")

# 上传截图到 FTP 服务器
upload_to_ftp(screenshot_path, ftp_folder)

def prune_old_events(event_times):
"""
移除时间窗口外的事件

参数:
event_times (list): 事件发生的时间列表

返回:
None
"""
current_time = time.time()
event_times[:] = [t for t in event_times if current_time - t <= TIME_WINDOW]

def detect_trigger(event_times, required_events):
"""
检测时间窗口内是否满足触发条件

参数:
event_times (list): 事件发生的时间列表
required_events (int): 触发所需的事件数量

返回:
bool: 是否满足触发条件
"""
prune_old_events(event_times)
return len(event_times) >= required_events

def on_mouse_click(x, y, button, pressed, save_directory, ftp_folder):
"""
鼠标点击事件处理

参数:
x (int): 鼠标点击的 x 坐标
y (int): 鼠标点击的 y 坐标
button (mouse.Button): 点击的鼠标按钮
pressed (bool): 是否按下鼠标按钮
save_directory (str): 保存截图的目录
ftp_folder (str): FTP 服务器上的文件夹

返回:
None
"""
global screenshot_count

if pressed:
current_time = time.time()

# 检测 Caps Lock 长按 + 鼠标单击
if capslock_pressed and button == mouse.Button.left:
take_screenshot_and_upload(screenshot_count, save_directory, ftp_folder)
screenshot_count += 1

# 检测 2 秒内单击 4 次滚轮
elif button == mouse.Button.middle:
wheel_click_times.append(current_time)
if detect_trigger(wheel_click_times, 4):
take_screenshot_and_upload(screenshot_count, save_directory, ftp_folder)
screenshot_count += 1
wheel_click_times.clear() # 清空滚轮单击记录

def on_capslock_press(key):
"""
Caps Lock 按键按下事件

参数:
key (keyboard.Key): 按下的按键

返回:
None
"""
global capslock_pressed
if key == keyboard.Key.caps_lock:
capslock_pressed = True

def on_capslock_release(key):
"""
Caps Lock 按键释放事件

参数:
key (keyboard.Key): 释放的按键

返回:
None
"""
global capslock_pressed
if key == keyboard.Key.caps_lock:
capslock_pressed = False

def start_listeners(save_directory, ftp_folder):
"""
启动鼠标和键盘监听器

参数:
save_directory (str): 保存截图的目录
ftp_folder (str): FTP 服务器上的文件夹

返回:
None
"""
print("监听鼠标和键盘中...")

# 启动鼠标和键盘监听器
mouse_listener = mouse.Listener(
on_click=lambda x, y, button, pressed: on_mouse_click(
x, y, button, pressed, save_directory, ftp_folder
)
)
keyboard_listener = keyboard.Listener(
on_press=on_capslock_press,
on_release=on_capslock_release
)

mouse_listener.start()
keyboard_listener.start()

mouse_listener.join()
keyboard_listener.join()