【Python小工具】PrtSc、Win+Shift+S截图自动保存 & 画板"白嫖"抠图,200行代码搞定桌面截图管家!手把手教会你编写!Python 0.1 基础系列!


声明:本项目为作者原创,采用 MIT 开源协议,欢迎 Star、Fork 和二次开发。
📦 源码仓库https://gitcode.com/lumileth/auto-screenshot-saver-for-windows
✉️ 作者:Gary | 联系邮箱:gary_beijing@126.com


一、为什么要做这个工具?

众所周知,Windows自带一个非常方便的截图工具…

作为一个经常写文档、写博客的人,PrtScWin + Shift + S 截图是我每天用得最多的快捷键之一。

但每次截完图,我都要手动打开文件夹、粘贴、Ctrl + S、取文件名……一套操作下来,思路都断了。

能不能让截图"自己"保存到指定目录? 截完图弹个窗问我要不要存,点"是"就自动归档,截错了就点"否"当做无事发生?

于是,这个 Windows截图自动保存工具 就诞生了。

更妙的是,我发现了一个 “美丽的Bug”——你在Windows自带的 画图板(mspaint) 里随便框选一个区域然后 Ctrl+C,这个工具也会把它当成截图来处理!这意味着你 不用下载PS、GIMP这些重量级软件,用系统自带的画图板就能轻松完成简单的抠图+保存。这个"特性"我直接保留了下来,后面会详细聊 😎。


二、功能概览

先来看看这个小工具都能干什么:

功能 说明
📋 剪贴板监听 实时轮询剪贴板,检测新截图
💬 弹窗确认 检测到截图后弹窗询问,避免误保存
📁 自动归档 日期+小时 自动创建子目录
🔢 自动编号 文件名自动递增编号,不会覆盖
📝 日志记录 控制台+文件双通道日志
🎨 画图板兼容 画图板框选复制也能触发保存(意外之喜!)

整体架构非常清晰,就两个核心类:

ClipboardMonitor(监听剪贴板)
       ↓ 检测到新截图
ScreenshotSaver(弹窗 → 保存)

好,接下来让我们 从零开始,一步步把它写出来!


三、第一步:怎么知道剪贴板有新截图?

🤔 问题来了

要监听剪贴板,第一反应是什么?写个 while True 死循环,不停地去读剪贴板内容呗!

没错,轮询是最朴素的思路。但这里有几个问题值得思考:

  1. 每次都去读剪贴板内容,效率是不是太低了? 如果剪贴板里是一张 4K 截图,每次都要把整张图片读出来比较,CPU不得累死?
  2. 怎么判断"新"截图? 用户截了两次一模一样的图,要不要保存两次?(当你连续按PrtSc的时候)

💡 解决方案:序列号 + 哈希 双重检测

Windows 提供了一个非常贴心的 API:GetClipboardSequenceNumber()。每次剪贴板内容发生变化,这个序列号就会递增。所以我们可以:

  • 第一层过滤:比较序列号,没变就直接跳过(几乎零开销)
  • 第二层过滤:序列号变了,再读取内容,计算图片的 MD5 哈希值 来精确比对

先来看看序列号怎么用:

import ctypes

# 调用 Windows API 获取剪贴板序列号
# ctypes 是 Python 内置库,可以直接调用 C 语言风格的系统函数
last_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()

🧠 小知识ctypes 是 Python 标准库之一,不需要额外安装。windll 是 Windows 专属的动态链接库调用接口,user32 是 Windows 系统中负责用户界面(窗口、消息、对话框等)的核心 DLL。

有了序列号,我们的轮询逻辑就清晰了。先用一个最简单的版本跑起来:

import ctypes
import time
from PIL import Image, ImageGrab  # 需要先 pip install Pillow

# 先获取程序开始运行时的剪切板内容,避免刚运行程序就询问保存截图
last_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()

while True:
    # 获取当前序列号
    current_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()
    
    if current_sequence != last_sequence:
        # 序列号变了!说明剪贴板有更新
        last_sequence = current_sequence
        clipboard_content = ImageGrab.grabclipboard()
        
        # 判断是不是图片
        # isinstance() 可以检查一个对象是不是某个类的实例
        if isinstance(clipboard_content, Image.Image):
            print("检测到新截图!")  # 先用 print 看看效果
    
    time.sleep(0.2)  # 每 0.2 秒检查一次

⚠️ 这里 ImageGrab.grabclipboard() 来自第三方库 Pillow(需要使用pip下载),它可以读取剪贴板中的图片数据。ImageGrab.Image.Image 就是 Pillow 中图片对象的类型。
如果发现pip下载速度过慢,可以使用清华源下载: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple Pillow

🔄 优化:引入哈希去重

上面的代码有个问题:如果用户在画图板里对同一张图做了两次复制(比如先复制,再取消选择,再复制),序列号会变,但内容其实一样。这时候就需要 MD5 哈希 来做精确比对:

import hashlib
from PIL import Image

def compute_hash(image):
    """
    计算图片的 MD5 哈希值。
    - image.convert("RGB"):统一转成 RGB 格式,避免不同色彩模式导致哈希不同
    - .tobytes():把图片像素数据转成字节串
    - hashlib.md5(...):计算这段字节的 MD5 摘要
    """
    if image is None:
        return None
    return hashlib.md5(image.convert("RGB").tobytes()).hexdigest()

🧠 什么是 MD5? 简单来说,它能把任意长度的数据"压缩"成一串 32 位的字符。相同的数据一定会得到相同的哈希值,不同的数据(几乎不可能)得到相同的哈希值。所以我们只需要比较这串字符,就知道两张图片是否一模一样。

把哈希去重加进去之后,检测逻辑就更健壮了:

import hashlib
from PIL import Image, ImageGrab

last_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()
last_hash = None

while True:
    current_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()
    
    if current_sequence != last_sequence:
        last_sequence = current_sequence
        
        # 读取剪贴板内容
        clipboard_content = ImageGrab.grabclipboard()
        image = clipboard_content if isinstance(clipboard_content, Image.Image) else None
        
        # 计算哈希,与上次比较
        current_hash = compute_hash(image)
        if current_hash != last_hash:
            if image is not None:
                print("检测到真正的新截图!")
            last_hash = current_hash
    
    time.sleep(0.2)

这样,序列号快速过滤 + MD5精确去重,两层防护让我们的监听既高效又准确 ✅


四、第二步:检测到截图后怎么办?弹窗问用户!

🤔 问题来了

检测到截图就直接保存?那万一是用户不小心截的呢?万一截了个"社死"内容呢?

所以我们需要一个 确认弹窗,让用户自己决定要不要保存。

💡 解决方案:调用 Windows 原生 MessageBox

Python 有很多 GUI 库(Tkinter、PyQt……),但弹个确认框而已,杀鸡焉用牛刀?直接用 Windows 系统自带的 MessageBoxW 就行,还是 ctypes 一行搞定:

import ctypes

# MessageBoxW 的参数:
#   第1个参数 0       → 父窗口句柄,0 表示桌面(即没有父窗口)
#   第2个参数 message → 弹窗正文
#   第3个参数 title   → 弹窗标题
#   第4个参数 flags   → 控制按钮和图标的标志位
result = ctypes.windll.user32.MessageBoxW(
    0, 
    "检测到新的截图,是否保存为文件?", 
    "截图保存", 
    0x4 | 0x20 | 0x40000  # 是/否按钮 + 问号图标 + 窗口置顶
)

# result == 6 表示用户点了"是"
if result == 6:
    print("用户选择保存")
else:
    print("用户选择取消")

🔄 优化:用 IntEnum 让"魔法数字"变得可读

上面的代码里,0x40x206 这些数字是什么意思?过一个月再看,你自己都忘了!

Python 的 enum 模块(标准库,不用安装)可以把这些数字变成有名字的常量:

from enum import IntEnum

class MessageBoxFlags(IntEnum):
    """MessageBoxW 的标志位,用名字代替数字"""
    YESNO = 0x4          # 显示"是"和"否"两个按钮
    ICONQUESTION = 0x20   # 显示问号图标
    DEFBUTTON1 = 0x0      # 默认选中第一个按钮("是")
    TOPMOST = 0x40000     # 弹窗置顶(不被其他窗口遮挡)

class MessageBoxResult(IntEnum):
    """MessageBoxW 的返回值"""
    IDYES = 6             # 用户点了"是"

这样写出来的代码,一眼就能看懂

def ask_yes_no(title, message):
    """弹出一个置顶的"是/否"确认框,返回 True 表示用户点了"是"。"""
    flags = (
        MessageBoxFlags.YESNO          # 是/否按钮
        | MessageBoxFlags.ICONQUESTION # 问号图标
        | MessageBoxFlags.DEFBUTTON1   # 默认焦点在"是"
        | MessageBoxFlags.TOPMOST      # 窗口置顶
    )
    # "|" 是按位或运算符,用来组合多个标志位,效果和加法类似
    result = ctypes.windll.user32.MessageBoxW(0, message, title, flags)
    return result == MessageBoxResult.IDYES

🧠 什么是"按位或"(|)? 每个标志位其实是一个二进制数中不同的位。| 运算可以把多个标志"合并"在一起,这是 Windows API 中非常常见的参数传递方式。你不需要深入理解位运算,只需要知道:| 把多个标志连起来就行

⚡ 进阶问题:弹窗会"卡住"整个程序!

如果你跑一下上面的代码,会发现:弹窗出来的时候,整个程序都停了,剪贴板监听也暂停了。这是因为 MessageBoxW 是一个 同步阻塞 调用——它会一直等到用户点按钮才返回。

这就引出了我们下一个话题:异步编程


五、第三步:让程序"一心二用"——异步编程入门

🤔 问题来了

监听剪贴板要一直循环检查,弹窗要等用户操作——两件事都是"死等"型任务。如果用传统的同步写法,弹窗的时候监听就停了,监听的时候就没法弹窗。怎么办?开多线程?会不会太复杂了?

多线程当然可以,但 Python 有一个更轻量的方案:asyncio 异步协程

💡 什么是异步协程?(大白话版)

想象你是一个餐厅服务员(事件循环):

  • 同步方式:你给1号桌点完菜,站在厨房等菜做好,端给1号桌,然后才去2号桌。效率极低。
  • 异步方式:你给1号桌点完菜,把单子交给厨房,马上去2号桌点菜。厨房做好了会喊你端菜。

在 Python 里,async def 定义的任务就是"可以暂停和恢复"的函数。遇到 await 就暂停自己,让出控制权给其他任务。

import asyncio

# async def 定义一个"异步函数"
async def my_task():
    print("开始做某事...")
    await asyncio.sleep(1)  # "暂停"1秒,期间可以做别的事
    print("做完了!")

# asyncio.run() 启动事件循环,运行异步任务
asyncio.run(my_task())

⚠️ 注意asyncawait 是 Python 3.5+ 引入的语法,它们需要配合 asyncio 模块使用。await 只能在 async def 函数内部使用。

🔄 把弹窗放进线程池

MessageBoxW 是阻塞的,没法直接 await。但 asyncio 提供了一个神器:asyncio.to_thread()——它可以把一个阻塞的同步函数丢到后台线程去执行,主线程继续干别的事:

async def _ask_user(self):
    """
    asyncio.to_thread() 把阻塞的 ask_yes_no 函数丢到线程池执行,
    await 等待结果期间,事件循环可以继续监听剪贴板。
    """
    return await asyncio.to_thread(
        ask_yes_no,
        "截图保存",
        "检测到新的截图,是否保存为文件?",
    )

这样一来,弹窗和监听互不干扰,完美!🎉


六、第四步:截图保存到哪里?怎么命名?

🤔 问题来了

用户点了"是",截图往哪儿存?文件名叫什么?总不能都叫 screenshot.png 互相覆盖吧?

💡 我的解决方案:按时间归档 + 自动编号

我设计的目录结构是这样的:

~/Pictures/Screenshots/
├── 20250630-14/         ← 2025年6月30日 14点
│   ├── 01_2530.png      ← 第1张,14:25:30截的
│   ├── 02_3015.png      ← 第2张,14:30:15截的
│   └── 03_4500.png
├── 20250630-15/         ← 2025年6月30日 15点
│   └── 01_0230.png
└── ...

好处显而易见:

  • 按小时分目录,不会一个文件夹里塞几千张图
  • 自动编号,不会覆盖
  • 文件名包含分秒信息,方便回溯

💻代码实现

先写保存逻辑,这部分是纯同步的,不需要异步:

from datetime import datetime
from pathlib import Path
 
# pathlib 是 Python 标准库,用来处理文件路径,比 os.path 好用得多
# Path.home() 返回用户主目录,比如 C:\Users\你的用户名
# 这里可以换成你自己喜欢的位置,但是要用 Path 类型!!

save_dir = Path.home() / "Pictures" / "Screenshots"

def save_image(image):
    now = datetime.now()
    
    # strftime() 把时间格式化:%Y年%m月%d日-%H时
    # Path 的方便之处:使用 / 进行直观的路径拼接
    target_dir = save_dir / now.strftime("%Y%m%d-%H") # 这个是Path类型的变量
    
    # mkdir() 创建目录
    # parents=True  → 如果父目录不存在也一并创建(类似 mkdir -p)
    # exist_ok=True → 目录已存在也不报错
    target_dir.mkdir(parents=True, exist_ok=True)
    
    # 生成文件名
    file_name = f"截图_{now:%M%S}.png"
    file_path = target_dir / file_name
    
    image.save(file_path, "PNG")
    print(f"截图已保存: {file_path}")

🧠 如何自定义目录? 更改save_dir = Path.home() / "Pictures" / "Screenshots"save_dir = Path(“your_custom_path”)

🔄 优化:加上自动编号

上面的代码虽然能用,但如果同一分钟截了两张图,文件名就会冲突。我们需要扫描目录里已有的文件,取最大编号 +1:

def get_next_sequence(directory):
    """
    扫描目录中已有的 .png 文件,找出最大编号,返回下一个可用编号。
    
    文件命名格式:01_2530.png
    我们用 "_" 分割,取前面那段数字。
    """
    max_seq = 0
    for file_path in directory.iterdir():  # iterdir() 遍历目录下所有文件
        if file_path.is_file() and file_path.suffix == ".png":
            stem = file_path.stem  # stem 是不含后缀的文件名,如 "01_2530"
            if "_" in stem:
                prefix = stem.split("_", 1)[0]  # 取 "_" 前面的部分
                if prefix.isdigit():             # 检查是不是纯数字
                    max_seq = max(max_seq, int(prefix))
    return max_seq + 1

然后把它整合进保存逻辑:

def save_image(image):
    now = datetime.now()
    target_dir = save_dir / now.strftime("%Y%m%d-%H")
    target_dir.mkdir(parents=True, exist_ok=True)
    
    # 自动编号
    seq = get_next_sequence(target_dir)
    file_name = f"{seq:02d}_{now:%M%S}.png"  # :02d 表示至少2位,不足补零
    file_path = target_dir / file_name
    
    image.save(file_path, "PNG")
    print(f"截图已保存: {file_path}")

🧠 f"{seq:02d}" 是什么? 这是 Python 的格式化字符串语法。:02d 表示把整数格式化为至少 2 位的十进制数,不足补零。比如 1"01"12"12"

💡补充说明

其实这里也可以使用 正则表达式(Regular Expression) 进行提取。在Python里,我们可以使用re库来便捷的使用它。re库在数据处理中扮演了非常重要的角色。因为这里不是重点,也不方便新手理解,这里大概了解一下即可:

import re

#我们的代码...

def get_next_sequence(directory):
    """新版函数:使用正则表达式,同时匹配开头数字和 .png 后缀。"""
    max_seq = 0
    for file_path in directory.iterdir():
        if file_path.is_file():
            # 注意:这里用 .name 获取完整文件名(如 "01_2530.png"),而不是 .stem
            full_name = file_path.name  
            
            # re.match 从字符串开头开始匹配我们制定的完整规则
            match_obj = re.match(r"^(\d+).*\.png$", full_name)
            
            # 如果匹配成功,说明它既是以数字开头,又是以 .png 结尾
            if match_obj:
                # .group(1) 提取第1个括号(分组)里匹配到的内容,也就是开头的数字
                seq_num = int(match_obj.group(1))
                max_seq = max(max_seq, seq_num)
                
    return max_seq + 1

🧠 正则小课堂(包教包会):

  • r"...":前面的 r 表示“原始字符串”,告诉 Python 不要处理里面的反斜杠,原封不动传给正则引擎。
  • ^:表示“必须从字符串的最开头开始匹配”。
  • (\d+)分组(Group)\d 代表数字,+ 代表1次或多次。括号把它圈起来,方便稍后“提取”。
  • .*:. 代表任意单个字符,* 代表0次或多次。连起来就是“匹配中间的任意内容”(比如_2530)。
  • \.:在正则里 . 是个特殊符号(代表任意字符),所以要在前面加 \ 进行转义,表示“我只想要真正的英文句号”。
  • png$$ 表示“必须到字符串的最结尾”。确保文件是以 .png 真正结束的,而不是 .png.bak 这种伪装者。

七、第五步:把 print 换成专业日志

🤔 问题来了

前面我们到处用 print() 来输出调试信息。但 print 有几个硬伤:

  • 分不清"普通信息"和"调试信息"和"错误信息"
  • 程序关了就什么都没了,没法事后排查问题
  • 想关掉调试输出?得满代码找 print 一个个删?

💡 解决方案:Python 内置 logging 模块

logging 是 Python 标准库,专门用来替代 print 做日志记录。它支持 日志级别多输出通道格式化 等功能。

先看最简单的用法:

import logging

# 获取一个日志记录器,给它起个名字
logger = logging.getLogger("ScreenshotSaver")

# 设置最低记录级别:DEBUG(最详细)
logger.setLevel(logging.DEBUG)

# 创建一个"控制台输出通道"
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)  # 控制台只显示 INFO 及以上级别

# 设置输出格式
formatter = logging.Formatter(
    "%(asctime)s [%(levelname)s] %(message)s",  # 时间 [级别] 内容
    datefmt="%H:%M:%S"                            # 时间只显示时分秒
)
console_handler.setFormatter(formatter)

# 把通道加到记录器上
logger.addHandler(console_handler)

# 使用!
logger.debug("这是调试信息,控制台不会显示(因为控制台级别是INFO)")
logger.info("这是普通信息,控制台会显示")
logger.error("这是错误信息!")

🔄 再加上文件日志

控制台日志方便实时看,但程序关了就没了。我们再加一个 文件输出通道,把所有细节都记到文件里:

# 创建一个"文件输出通道"
# 追加模式(不会覆盖之前的日志),指定 UTF-8 编码(防止中文乱码)
file_handler = logging.FileHandler("./running_log.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)  # 文件记录所有级别的日志

file_formatter = logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"  # 文件中记录完整时间
)
file_handler.setFormatter(file_formatter)

logger.addHandler(file_handler)

这样,我们就实现了 双通道日志

通道 级别 用途
控制台 INFO 实时查看关键事件
文件 DEBUG 事后排查所有细节

🧠 以后你写任何 Python 项目,都可以把这段日志配置直接拷过去用,堪称万能模板!但是别忘了定制哦!


八、第六步:把一切组装起来

🏗️ 面向对象:两个核心类

现在所有零件都有了,我们用两个类把它们组织起来:

ScreenshotSaver —— 负责"保存"的业务逻辑:

class ScreenshotSaver:
    """负责:询问用户、创建目录、生成文件名、保存截图。"""
    
    def __init__(self, save_dir):
        self.save_dir = save_dir  # 保存根目录
    
    async def process(self, image):
        """异步处理入口:先问用户,用户同意就保存。"""
        should_save = await self._ask_user()
        if should_save:
            self._save_image(image)
    
    async def _ask_user(self):
        """在线程池中弹窗,不阻塞事件循环。"""
        return await asyncio.to_thread(
            ask_yes_no,
            "截图保存",
            "检测到新的截图,是否保存为文件?",
        )
    
    def _save_image(self, image):
        """保存图片到按时间组织的目录中。"""
        now = datetime.now()
        target_dir = self.save_dir / now.strftime("%Y%m%d-%H")
        target_dir.mkdir(parents=True, exist_ok=True)
        
        seq = self._get_next_sequence(target_dir)
        file_name = f"{seq:02d}_{now:%M%S}.png"
        file_path = target_dir / file_name
        
        image.save(file_path, "PNG")
        logger.info("截图已保存: %s", file_path)
    
    @staticmethod
    def _get_next_sequence(directory):
        """扫描目录,返回下一个可用编号。"""
        max_seq = 0
        for fp in directory.iterdir():
            if fp.is_file() and fp.suffix == ".png":
                stem = fp.stem
                if "_" in stem:
                    prefix = stem.split("_", 1)[0]
                    if prefix.isdigit():
                        max_seq = max(max_seq, int(prefix))
        return max_seq + 1

ClipboardMonitor —— 负责"监听"的核心循环:

class ClipboardMonitor:
    """
    异步监听剪贴板:
    1. 序列号快速过滤
    2. MD5哈希精确去重
    3. 新截图交给 ScreenshotSaver 处理
    """
    
    def __init__(self, saver, interval=0.2):
        self.saver = saver                      # 注入业务对象
        self.interval = interval                # 轮询间隔(秒)
        self._last_hash = None                  # 上次图片的哈希
        self._last_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()
    
    async def run(self):
        """启动监听循环。"""
        logger.info("开始监听剪贴板截图(Win+Shift+S 等),按 Ctrl+C 退出")
        try:
            while True:
                await self._check_clipboard()
                await asyncio.sleep(self.interval)  # 异步等待,不阻塞
        except KeyboardInterrupt:
            logger.info("监听已停止")
    
    async def _check_clipboard(self):
        """一次完整的剪贴板检查流程。"""
        # ① 序列号没变?跳过!
        current_sequence = ctypes.windll.user32.GetClipboardSequenceNumber()
        if current_sequence == self._last_sequence:
            return
        self._last_sequence = current_sequence
        logger.debug("剪贴板序列号变化: %d", current_sequence)
        
        # ② 读取剪贴板内容
        clipboard_content = ImageGrab.grabclipboard()
        image = (
            clipboard_content 
            if isinstance(clipboard_content, Image.Image) 
            else None
        )
        
        # ③ 哈希比对
        current_hash = self._compute_hash(image)
        if current_hash == self._last_hash:
            logger.debug("剪贴板内容与上次相同,忽略")
            return
        
        # ④ 新图片 → 交给业务处理
        if image is not None:
            logger.info("检测到新的截图,正在询问用户...")
            await self.saver.process(image)
        
        # ⑤ 更新哈希
        self._last_hash = current_hash
    
    @staticmethod
    def _compute_hash(image):
        """计算图片的 MD5 哈希值。"""
        if image is None:
            return None
        return hashlib.md5(image.convert("RGB").tobytes()).hexdigest()

🚀 启动入口

最后,写一个异步主函数把两个类串起来:

async def main():
    # 1. 创建保存器,指定截图存储目录
    saver = ScreenshotSaver(SAVE_DIR)
    
    # 2. 创建监听器,把保存器"注入"进去
    monitor = ClipboardMonitor(saver)
    
    # 3. 启动!
    await monitor.run()

if __name__ == "__main__":
    asyncio.run(main())  # asyncio.run() 是启动异步程序的入口

🧠 if __name__ == "__main__" 是什么意思?这是 Python 的经典写法,表示"只有直接运行这个文件时才执行下面的代码",如果这个文件被别的文件 import,则不会执行。这是一个好习惯。


九、美丽的Bug:画图板秒变"抠图神器" 🎨

在测试过程中,我发现了一个有趣的现象:

在画图板(mspaint)中框选任意区域,按 Ctrl+C 复制,这个工具也会弹窗询问是否保存!

为什么呢?因为画图板的"复制"操作,本质上就是把框选区域的 位图数据 放进了系统剪贴板——这和 Win+Shift+S 截图在剪贴板中存储的数据格式是一模一样的。我们的工具监听的是剪贴板中的 所有图片,并不区分图片来源。

这其实是一个超级实用的"特性"! 想想看:

  1. 打开系统自带的 画图板Win + R → 输入 mspaint → 回车)
  2. 粘贴一张图片(或者直接打开图片文件)
  3. 选择工具 框选你想要的区域
  4. Ctrl + C 复制
  5. 🎉 弹窗自动出现!点"是"就保存了

不需要安装 Photoshop,不需要安装 GIMP,不需要任何第三方截图软件,系统自带工具就能完成"打开图片 → 框选 → 抠图 → 保存"的完整流程!

方案 安装成本 操作步骤
Photoshop 几百元/月 打开 → 选区 → 裁剪 → 导出
GIMP 免费但需下载安装 打开 → 选区 → 裁剪 → 导出
画图板 + 本工具 零成本,系统自带 框选 → Ctrl+C → 弹窗点"是"

💡 使用技巧:如果你只想用画图板抠图而不想触发弹窗,可以先 停止本工具Ctrl+C),再在画图板中操作。反之,如果你正好需要快速抠图保存,这个"Bug"简直就是量身定做的功能!


十、完整代码 & 项目结构

完整源码已开源,欢迎 Star ⭐:

📦 GitCode 仓库https://gitcode.com/lumileth/auto-screenshot-saver-for-windows

运行方式:

# 1. 安装依赖
pip install Pillow
# 使用清华源
#pip install -i https://pypi.tuna.tsinghua.edu.cn/simple Pillow

# 2. 运行程序
python main.py

⚠️ 本工具仅支持 Windows 系统(因为使用了 ctypes.windlluser32.dll)。


十一、知识点回顾

写完这个小工具,我们来盘点一下用到的知识点:

知识点 模块/库 是否标准库 用途
异步编程 asyncio ✅ 标准库 监听循环 + 弹窗不互相阻塞
调用系统API ctypes ✅ 标准库 获取剪贴板序列号、弹出 MessageBox
日志系统 logging ✅ 标准库 替代 print,双通道日志
哈希计算 hashlib ✅ 标准库 MD5 哈希去重
日期时间 datetime ✅ 标准库 按时间归档文件名
文件路径 pathlib ✅ 标准库 跨平台路径处理
枚举类型 enum ✅ 标准库 消除魔法数字
图片处理 Pillow ❌ 第三方库 读取剪贴板图片、保存图片

你看,除了 Pillow 之外,其他全部是 Python 标准库,不需要安装任何额外的包!


十二、写在最后

这个项目的初衷很简单:让截图保存这件事变得无感

但在编写过程中,我们涉及了异步编程、Windows API 调用、日志系统设计、面向对象设计模式等多个知识点。希望这篇博客不仅分享了一个实用的小工具,更能帮你理解 如何从零开始构建一个完整的 Python 项目

如果你觉得这篇文章有帮助,别忘了 点赞 👍 + 收藏 ⭐ + 关注 🔔 三连支持!

有任何问题或建议,欢迎在评论区留言,也欢迎到 GitCode 仓库提 Issue 或 PR!


声明:本文为原创文章,遵循 MIT 协议开源。代码仅供学习交流,请勿用于商业用途。
📦 源码地址https://gitcode.com/your-username/screenshot-saver
✉️ 作者:Gary| gary_beijing@126.com

更多推荐