【Python】用Tkinter实现一个简单的任意区域截图软件

本文介绍了一种使用Python的Tkinter库和Pillow库实现全屏与选区截图的方法。通过创建全屏无边框半透明窗口,监听鼠标操作在Canvas上绘制选区,结合屏幕缩放比调整坐标,最终实现精准截图。文章详细解析了Tkinter显示、截图及代码实现过程。

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

1. 思路

基本思路是:

  1. 创建一个 tkinter 窗体,铺满整个屏幕,设置窗体无边框半透明
  2. 在窗体中添加一个canvas
  3. 监控按键,按下鼠标左键并拖动时,自动在canvas中绘制出对应的矩形方块
  4. 放开左键后,当按下 enter 时,检查当前的矩形区域
  5. 调用 Pillow 库进行截图

2. 重难点

2.1 Tkinter 显示

Tkinter 显示的唯一难点在于如何全屏无边框透明,代码如下:

self.win = tk.Tk()
# self.win.tk.call('tk', 'scaling', scaling_factor)
self.width = self.win.winfo_screenwidth()
self.height = self.win.winfo_screenheight()

# 无边框,没有最小化最大化关闭这几个按钮,也无法拖动这个窗体,程序的窗体在Windows系统任务栏上也消失
self.win.overrideredirect(True)
self.win.attributes('-alpha', 0.25)

2.2 截图

截图很简单,调用 PIL.ImageGrab 就行:

ImageGrab(rect_loc)

如果直接使用tkinter所探测到的矩形区域来进行截图的话,那就会存在一个潜在的bug,这个bug只在电脑设置了分辨率缩放的地方才会出现。出现这个 bug 的原因是 tkinter 检测的是基于缩放后的电脑分辨率的屏幕坐标,而 ImageGrab 需要的是基于原始分辨率的坐标。这是个很有趣的问题,具体可以参考我的另一篇博文https://blog.csdn.net/frostime/article/details/104798061

总之,如果想要正常运行,必须把所有坐标乘以一个缩放比例。

2.3 总代码

__author__ = 'Frostime'

from win32 import win32api, win32gui, win32print
from win32.lib import win32con

from win32.win32api import GetSystemMetrics

import tkinter as tk
from PIL import ImageGrab


def get_real_resolution():
    """获取真实的分辨率"""
    hDC = win32gui.GetDC(0)
    # 横向分辨率
    w = win32print.GetDeviceCaps(hDC, win32con.DESKTOPHORZRES)
    # 纵向分辨率
    h = win32print.GetDeviceCaps(hDC, win32con.DESKTOPVERTRES)
    return w, h


def get_screen_size():
    """获取缩放后的分辨率"""
    w = GetSystemMetrics(0)
    h = GetSystemMetrics(1)
    return w, h


real_resolution = get_real_resolution()
screen_size = get_screen_size()

# Windows 设置的屏幕缩放率
# ImageGrab 的参数是基于显示分辨率的坐标,而 tkinter 获取到的是基于缩放后的分辨率的坐标
screen_scale_rate = round(real_resolution[0] / screen_size[0], 2)


class Box:

    def __init__(self):
        self.start_x = None
        self.start_y = None
        self.end_x = None
        self.end_y = None

    def isNone(self):
        return self.start_x is None or self.end_x is None

    def setStart(self, x, y):
        self.start_x = x
        self.start_y = y

    def setEnd(self, x, y):
        self.end_x = x
        self.end_y = y

    def box(self):
        lt_x = min(self.start_x, self.end_x)
        lt_y = min(self.start_y, self.end_y)
        rb_x = max(self.start_x, self.end_x)
        rb_y = max(self.start_y, self.end_y)
        return lt_x, lt_y, rb_x, rb_y

    def center(self):
        center_x = (self.start_x + self.end_x) / 2
        center_y = (self.start_y + self.end_y) / 2
        return center_x, center_y


class SelectionArea:

    def __init__(self, canvas: tk.Canvas):
        self.canvas = canvas
        self.area_box = Box()

    def empty(self):
        return self.area_box.isNone()

    def setStartPoint(self, x, y):
        self.canvas.delete('area', 'lt_txt', 'rb_txt')
        self.area_box.setStart(x, y)
        # 开始坐标文字
        self.canvas.create_text(
            x, y - 10, text=f'({x}, {y})', fill='red', tag='lt_txt')

    def updateEndPoint(self, x, y):
        self.area_box.setEnd(x, y)
        self.canvas.delete('area', 'rb_txt')
        box_area = self.area_box.box()
        # 选择区域
        self.canvas.create_rectangle(
            *box_area, fill='black', outline='red', width=2, tags="area")
        self.canvas.create_text(
            x, y + 10, text=f'({x}, {y})', fill='red', tag='rb_txt')


class ScreenShot():

    def __init__(self, scaling_factor=2):
        self.win = tk.Tk()
        # self.win.tk.call('tk', 'scaling', scaling_factor)
        self.width = self.win.winfo_screenwidth()
        self.height = self.win.winfo_screenheight()

        # 无边框,没有最小化最大化关闭这几个按钮,也无法拖动这个窗体,程序的窗体在Windows系统任务栏上也消失
        self.win.overrideredirect(True)
        self.win.attributes('-alpha', 0.25)

        self.is_selecting = False

        # 绑定按 Enter 确认, Esc 退出
        self.win.bind('<KeyPress-Escape>', self.exit)
        self.win.bind('<KeyPress-Return>', self.confirmScreenShot)
        self.win.bind('<Button-1>', self.selectStart)
        self.win.bind('<ButtonRelease-1>', self.selectDone)
        self.win.bind('<Motion>', self.changeSelectionArea)

        self.canvas = tk.Canvas(self.win, width=self.width,
                                height=self.height)
        self.canvas.pack()
        self.area = SelectionArea(self.canvas)
        self.win.mainloop()

    def exit(self, event):
        self.win.destroy()

    def clear(self):
        self.canvas.delete('area', 'lt_txt', 'rb_txt')
        self.win.attributes('-alpha', 0)

    def captureImage(self):
        if self.area.empty():
            return None
        else:
            box_area = [x * screen_scale_rate for x in self.area.area_box.box()]
            self.clear()
            print(f'Grab: {box_area}')
            img = ImageGrab.grab(box_area)
            return img

    def confirmScreenShot(self, event):
        img = self.captureImage()
        if img is not None:
            img.show()
        self.win.destroy()

    def selectStart(self, event):
        self.is_selecting = True
        self.area.setStartPoint(event.x, event.y)
        # print('Select', event)

    def changeSelectionArea(self, event):
        if self.is_selecting:
            self.area.updateEndPoint(event.x, event.y)
            # print(event)

    def selectDone(self, event):
        # self.area.updateEndPoint(event.x, event.y)
        self.is_selecting = False


def main():
    ScreenShot()


if __name__ == '__main__':
    main()

3. 效果

在这里插入图片描述

在这里插入图片描述

### 使用Python Tkinter创建扫雷游戏 #### 创建基本窗口和布局 为了启动扫雷游戏,首先需要导入`Tkinter`模块并设置基础的游戏窗口。 ```python import tkinter as tk from tkinter import messagebox import random class Minesweeper: def __init__(self, master): self.master = master self.frame = tk.Frame(self.master) self.grid_size = 10 # 设置网格大小为10x10 self.mine_count = 10 # 地雷数量设为10 self.create_widgets() def create_widgets(self): for row in range(self.grid_size): for col in range(self.grid_size): button = tk.Button( self.master, width=2, height=1, command=lambda r=row, c=col: self.reveal(r, c), ) button.grid(row=row, column=col) def main(): root = tk.Tk() app = Minesweeper(root) root.mainloop() if __name__ == '__main__': main() ``` 这段代码初始化了一个带有按钮矩阵的窗口[^1]。每个按钮代表游戏中的一格方块,点击这些按钮可以触发相应的动作。 #### 添加地雷逻辑 接下来,在上述基础上增加放置地雷的功能以及计算周围地雷数目的机制: ```python def place_mines(self): mines = set() while len(mines) < self.mine_count: mine_x = random.randint(0, self.grid_size - 1) mine_y = random.randint(0, self.grid_size - 1) mines.add((mine_x, mine_y)) self.board = [[0]*self.grid_size for _ in range(self.grid_size)] for (x,y) in mines: self.board[x][y]=-1 def count_adjacent_mines(self,x,y): adjacent_offsets=[(-1,-1),(0,-1),(1,-1),(-1,0),(1,0),(-1,1),(0,1),(1,1)] count=sum([self.is_mine(x+offset[0], y+offset[1])for offset in adjacent_offsets]) return count if not self.is_mine(x,y)else -1 def is_mine(self,x,y): try:return self.board[x][y]==-1 and x>=0 and y>=0 except IndexError:return False ``` 此部分实现了随机分布的地雷位置,并能够统计任意坐标周围的地雷数目[^2]。 #### 实现揭示功能与胜利条件判断 最后一步是完善当玩家点击某个单元格后的响应行为——即展示该处是否有地雷或是空白区域;同时还需要加入检测是否赢得比赛的逻辑。 ```python revealed_cells=set() def reveal(self,row,col): global revealed_cells cell=self.count_adjacent_mines(row,col) btns=[[child for child in self.master.children.values()][row*self.grid_size+col]] if cell==-1:# Hit a mine messagebox.showinfo('Game Over','You hit a mine!') self.reset_game() elif str(btns[0]['text'])=='': revealed_cells|={(row,col)} btns[0].config(text=str(cell)if cell>0 else '') if len(revealed_cells)==self.grid_size**2-self.mine_count: messagebox.showinfo('Congratulations!', 'You win the game.') self.reset_game() def reset_game(self): self.place_mines() buttons=[widget for widget in self.master.winfo_children()] for b in buttons:b.config(text='') global revealed_cells;revealed_cells.clear() ``` 通过以上步骤,已经完成了一个简易版本的扫雷小游戏开发过程。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值