1. 思路
基本思路是:
- 創建一個 tkinter 窗體,鋪滿整個屏幕,設置窗體無邊框半透明
- 在窗體中添加一個canvas
- 監控按鍵,按下鼠標左鍵並拖動時,自動在canvas中繪製出對應的矩形方塊
- 放開左鍵後,當按下 enter 時,檢查當前的矩形區域
- 調用 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()