【Python】用Tkinter實現一個簡單的任意區域截圖軟件

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. 效果

在這裏插入圖片描述

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章