Skip to content

Commit

Permalink
重写截图子窗口,完善等比例缩放窗口和拖拽图片移动窗口功能。
Browse files Browse the repository at this point in the history
  • Loading branch information
hiroi-sora committed Apr 22, 2023
1 parent 07a229a commit 137f8db
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 67 deletions.
260 changes: 193 additions & 67 deletions ui/win_show_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,104 +9,230 @@
from win32clipboard import OpenClipboard, EmptyClipboard, SetClipboardData, CloseClipboard, CF_DIB
from io import BytesIO

minSize = 140 # 最小大小


class ShowImage:
def __init__(self, imgPIL=None, imgData=None):
def __init__(self, imgPIL=None, imgData=None, imgInfo=None):
# imgPIL:PIL对象,imgData:位图数据。传入一个即可
self.img, self.imgData = imgPIL, imgData
if not self.imgData and not self.img:
# imgInfo:图片信息,截图位置等。

# 初始化图片数据
self.imgPIL, self.imgData = imgPIL, imgData
if not self.imgData and not self.imgPIL:
return
if not self.imgData: # 从PIL Image对象创建位图数据
output = BytesIO()
imgPIL.save(output, 'BMP') # 以位图保存
self.imgData = output.getvalue()[14:] # 去除header
output.close()
if not self.img: # 从位图数据创建PIL Image对象
self.img = Image.open(BytesIO(imgData))
if not self.imgPIL: # 从位图数据创建PIL Image对象
self.imgPIL = Image.open(BytesIO(imgData))
self.imgTK = ImageTk.PhotoImage(self.imgPIL) # 保存图片对象
self.ratio = self.imgPIL.width / self.imgPIL.height # 图片比例
self.wh = (self.imgPIL.width, self.imgPIL.height) # 当前图片宽高

# 创建Tkinter窗口
self.win = tk.Toplevel()
self.win.iconphoto(False, Asset.getImgTK('umiocr24')) # 设置窗口图标
# 设置窗口大小
self.ratio = self.img.width / self.img.height
self.w, self.h = self.img.width, self.img.height
hPlus = 20 # 标题栏等所占的高度
if self.w > self.win.winfo_screenwidth(): # 防止超大
self.w = self.win.winfo_screenwidth()
self.h = int(self.w/self.ratio)
if self.h > self.win.winfo_screenheight()-hPlus:
self.h = self.win.winfo_screenheight()-hPlus
self.w = int(self.h*self.ratio)
self.win.geometry(f'{self.w}x{self.h+hPlus}') # 高度+hPlus
self.win.resizable(False, False) # 禁止原生缩放窗口

# 菜单栏
self.menubar = tk.Menu(self.win)
self.win.config(menu=self.menubar)
self.menubar.add_command(label=' 置顶', command=self._gotoTop)
self.menubar.add_command(label='识别', command=self._ocr)
self.menubar.add_command(label='保存', command=self._saveImage)
self.menubar.add_command(label='锁定', command=self.__switchLock)
self.menubar.add_command(label='识别', command=self.__ocr)
self.menubar.add_command(label='保存', command=self.__saveImage)
# self.menubar.add_command(label='复制', command=self.copyImage)
submenu = tk.Menu(self.menubar, tearoff=False)
submenu.add_command(label='窗口置顶:Ctrl+T', command=self._gotoTop)
submenu.add_command(label='文字识别:回车', command=self._ocr)
submenu.add_command(label='保存图片到本地:Ctrl+S', command=self._saveImage)
submenu.add_command(label='复制图片到剪贴板:Ctrl+C', command=self._copyImage)
submenu.add_command(label='关闭窗口:Esc', command=self._onClose)
submenu.add_command(label='锁定窗口+置顶:Ctrl+T 或 Ctrl+L',
command=lambda *e: self.__switchLock(1))
submenu.add_command(label='文字识别:回车', command=self.__ocr)
submenu.add_command(label='保存图片到本地:Ctrl+S', command=self.__saveImage)
submenu.add_command(label='复制图片到剪贴板:Ctrl+C', command=self.__copyImage)
submenu.add_command(label='关闭窗口:Esc', command=self.__onClose)
self.menubar.add_cascade(label='更多', menu=submenu)

# 创建Canvas对象并将其填充为整个窗口
self.canvas = tk.Canvas(
self.win, width=self.img.width, height=self.img.height)
self.win, width=self.imgPIL.width, height=self.imgPIL.height)
self.canvas.pack(fill='both', expand=True)
self.photo = ImageTk.PhotoImage(self.img) # 保存图片对象
self.canvas.config(borderwidth=0, highlightthickness=0) # 隐藏Canvas的边框
# 在Canvas上创建图像
self.canvas_image = self.canvas.create_image(
0, 0, anchor='nw', image=self.photo)
# 隐藏Canvas的边框
self.canvas.config(borderwidth=0, highlightthickness=0)
self.imgCanvas = self.canvas.create_image(
0, 0, anchor='nw', image=self.imgTK)

# 缩放和移动相关参数
imgArr = Asset.getImgTK('zoomArrow48')
self.zoomSize = (imgArr.width(), imgArr.height())
self.mouseOriginXY = None # 本次操作的起始鼠标位置
self.zoomOriginWH = None # 本次缩放的起始图片宽高
self.zoomArrow2 = self.canvas.create_image( # 缩放箭头第2层(鼠标进入时显示)
self.wh[0]-self.zoomSize[0], self.wh[1]-self.zoomSize[1], anchor='nw', image=imgArr)
self.zoomArrow1 = self.canvas.create_image( # 缩放箭头第1层(鼠标接近时显示)
self.wh[0]-self.zoomSize[0], self.wh[1]-self.zoomSize[1], anchor='nw', image=imgArr)
self.canvas.itemconfig(self.zoomArrow1, state=tk.HIDDEN) # 默认隐藏1、2层
self.canvas.itemconfig(self.zoomArrow2, state=tk.HIDDEN)
self.moveOriginXY = None # 本次移动的起始窗口位置

# 绑定事件
self.canvas.bind('<Configure>', self._onResize) # 窗口大小改变事件
self.win.bind('<Return>', self._ocr)
self.win.bind('<Control-t>', self._gotoTop)
self.win.bind('<Control-s>', self._saveImage)
self.win.bind('<Control-c>', self._copyImage)
self.win.bind('<Escape>', self._onClose)
self.win.bind('<Enter>', self.__onWinEnter) # 鼠标进入窗口
self.win.bind('<Leave>', self.__onWinLeave) # 鼠标离开窗口
self.canvas.bind('<ButtonPress-1>', self.__onCanvasPress) # 按下画布
self.canvas.bind('<ButtonRelease-1>', self.__onCanvasRelease) # 松开画布
self.canvas.bind('<B1-Motion>', self.__onCanvasMotion) # 拖拽画布
self.canvas.tag_bind( # 鼠标进入缩放按钮
self.zoomArrow1, '<Enter>', self.__onZoomEnter)
self.canvas.tag_bind( # 鼠标离开缩放按钮
self.zoomArrow1, '<Leave>', self.__onZoomLeave)

# 绑定快捷键
self.win.bind('<Return>', self.__ocr) # 回车:OCR
self.win.bind( # Ctrl+T:锁定&置顶
'<Control-t>', lambda *e: self.__switchLock(0))
self.win.bind( # Ctrl+L:还是锁定&置顶
'<Control-l>', lambda *e: self.__switchLock(0))
self.win.bind('<Control-s>', self.__saveImage) # Ctrl+S:保存
self.win.bind('<Control-c>', self.__copyImage) # Ctrl+C:复制图片
self.win.bind('<Escape>', self.__onClose) # Esc:关闭窗口

# 窗口属性
self.isWindowTop = False
if Config.get('isWindowTop'): # 初始置顶
self._gotoTop()
self.win.after(200, lambda: self.win.focus()) # 窗口获得焦点

def _onResize(self, event): # 缩放图像以适应Canvas的大小
w, h = event.width, event.height
imgW, imgH = w, h
if w/h > self.ratio: # 窗口更宽
imgW = int(h*self.ratio) # 重设图片宽度
self.__resize(self.imgPIL.width, self.imgPIL.height) # 设定初始大小
self.isLock = False # 初始值必为False

# 初始处理
def start(): # 展开
self.win.attributes('-topmost', 1) # 弹到最顶层
if not Config.get('isWindowTop'): # 跟随主窗口设置
self.win.attributes('-topmost', 0) # 取消锁定置顶
self.win.focus() # 窗口获得焦点
self.win.after(200, start)

def __resize(self, w, h): # 缩放图片。应用w或h中按图片比例更大的一个值。
# 适应w或h中比例更大的一个
if w/h > self.ratio:
h = int(w/self.ratio) # w更大,则应用w,改变h
else:
imgH = int(w/self.ratio) # 重设图片高度
resized_img = self.img.resize(
(imgW, imgH), Image.ANTIALIAS)
# 将PIL Image对象转换为Tkinter PhotoImage对象,并更新Canvas上的图像
self.photo = ImageTk.PhotoImage(resized_img)
self.canvas.itemconfigure(self.canvas_image, image=self.photo)
self.w, self.h = w, h

def _gotoTop(self, event=None):
self.isWindowTop = not self.isWindowTop
if self.isWindowTop: # 启用置顶
self.menubar.entryconfigure(1, label='已置顶')
self.win.attributes('-topmost', 1)
w = int(h*self.ratio) # h更大,则应用h,改变w
# 防止窗口大小超出屏幕
if w > self.win.winfo_screenwidth():
w = self.win.winfo_screenwidth()
h = int(w/self.ratio)
if h > self.win.winfo_screenheight():
h = self.win.winfo_screenheight()
w = int(h*self.ratio)
# 最小大小
if w < minSize:
w = minSize
h = int(w/self.ratio)
if h < minSize:
h = minSize
w = int(h*self.ratio)

self.wh = (w, h)
# 生成并设定缩放后的图片
resizedImg = self.imgPIL.resize((w, h), Image.ANTIALIAS)
self.imgTK = ImageTk.PhotoImage(resizedImg)
self.canvas.itemconfigure(self.imgCanvas, image=self.imgTK)
self.win.geometry(f'{w}x{h}') # 缩放窗口
# 移动缩放按钮
ax, ay = w-self.zoomSize[0], h-self.zoomSize[1]
self.canvas.coords(self.zoomArrow1, ax, ay)
self.canvas.coords(self.zoomArrow2, ax, ay)

# ============================== 事件 ==============================

def __onWinEnter(self, e=None): # 鼠标进入窗口
if self.isLock:
return
self.canvas.itemconfig(self.zoomArrow1, state=tk.NORMAL)

def __onWinLeave(self, e=None): # 鼠标离开窗口
self.canvas.itemconfig(self.zoomArrow1, state=tk.HIDDEN)

def __canvasFunc(self, e, zoomFunc, moveFunc): # 根据鼠标处于画布哪个位置,执行相应方法
ids = self.canvas.find_withtag(tk.CURRENT) # 获取当前鼠标位置的元素
if self.zoomArrow1 in ids or self.zoomArrow2 in ids: # 若是缩放按钮
zoomFunc(e)
else: # 若是本体
moveFunc(e)

def __onCanvasPress(self, e=None): # 按下画布
self.__canvasFunc(e, zoomFunc=self.__onZoomPress,
moveFunc=self.__onMovePress)

def __onCanvasRelease(self, e=None): # 松开画布
self.__canvasFunc(e, zoomFunc=self.__onZoomRelease,
moveFunc=self.__onMoveRelease)

def __onCanvasMotion(self, e=None): # 拖拽画布
if self.isLock:
return
self.__canvasFunc(e, zoomFunc=self.__onZoomMotion,
moveFunc=self.__onMoveMotion)

def __onZoomEnter(self, e=None): # 鼠标进入缩放按钮
if self.isLock:
return
self.canvas.itemconfig(self.zoomArrow2, state=tk.NORMAL) # 显示2层图标
self.canvas.config(cursor='sizing') # 改变光标为缩放箭头

def __onZoomLeave(self, e=None): # 鼠标离开缩放按钮
self.canvas.itemconfig(self.zoomArrow2, state=tk.HIDDEN)
self.canvas.config(cursor='') # 改变光标为正常

def __onZoomPress(self, e=None): # 按下缩放按钮
self.mouseOriginXY = (e.x_root, e.y_root)
self.zoomOriginWH = self.wh # 读取宽高起点

def __onZoomRelease(self, e=None): # 松开缩放按钮
self.mouseOriginXY = None

def __onZoomMotion(self, e=None): # 拖拽缩放按钮
if self.isLock:
return
dx = e.x_root-self.mouseOriginXY[0] # 离原点的移动量
dy = e.y_root-self.mouseOriginXY[1]
nw, nh = self.zoomOriginWH[0]+dx, self.zoomOriginWH[1]+dy # 计算大小设定
self.__resize(nw, nh) # 重置图片大小

def __onMovePress(self, e=None): # 按下移动区域
self.mouseOriginXY = (e.x_root, e.y_root) # 必须用_root,排除窗口相对移动的干扰
self.moveOriginXY = (self.win.winfo_x(), self.win.winfo_y())

def __onMoveRelease(self, e=None): # 松开移动区域
self.mouseOriginXY = None
self.moveOriginXY = None

def __onMoveMotion(self, e=None): # 拖拽移动区域
if self.isLock:
return
dx = e.x_root-self.mouseOriginXY[0] # 离原点的移动量
dy = e.y_root-self.mouseOriginXY[1]
nx, ny = self.moveOriginXY[0]+dx, self.moveOriginXY[1]+dy # 计算位置设定
self.win.geometry(f'+{nx}+{ny}') # 移动窗口

# ============================== 功能 ==============================

def __switchLock(self, flag=0): # 切换:锁定/解锁。
# flag=0:切换。>0:锁定。<0:解锁。
if flag == 0:
self.isLock = not self.isLock
elif flag > 0:
self.isLock = True
else:
self.menubar.entryconfigure(1, label=' 置顶')
self.isLock = False

if self.isLock: # 启用锁定
self.win.attributes('-topmost', 1)
else: # 解锁
self.win.attributes('-topmost', 0)

def _ocr(self, event=None):
self._copyImage()
def __ocr(self, e=None):
self.__copyImage()
Config.main.runClipboard()

def _saveImage(self, event=None):
def __saveImage(self, e=None):
# 打开文件选择对话框
now = time.strftime("%Y-%m-%d %H%M%S", time.localtime())
defaultFileName = f'屏幕截图 {now}.png'
Expand All @@ -119,9 +245,9 @@ def _saveImage(self, event=None):

if filePath:
# 将 PIL.Image 对象保存为 PNG 文件
self.img.save(filePath, format='PNG')
self.imgPIL.save(filePath, format='PNG')

def _copyImage(self, event=None):
def __copyImage(self, e=None):
try:
OpenClipboard() # 打开剪贴板
EmptyClipboard() # 清空剪贴板
Expand All @@ -134,6 +260,6 @@ def _copyImage(self, event=None):
except Exception as err:
Notify('无法关闭剪贴板', f'{err}')

def _onClose(self, event=None):
self.photo = None # 删除图片对象,释放内存
def __onClose(self, e=None):
self.imgTK = None # 删除图片对象,释放内存
self.win.destroy() # 关闭窗口
6 changes: 6 additions & 0 deletions utils/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
'isSave': False,
'base64': r'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAATVJREFUSEvFlD1SwzAUhPe5cRlKzmBLlQfSQpWcgStQ5AiBC9BRchAqKJNhXEn2OUjpwiPGmVgjyZajcaLBtd9+2n0/hMgfRdbH/wDyPM+I6A3Aao5DItq3bbup63o/cJBl2TJJkt0cYbdGKZUPAJzznVJqCeBRSvk9B8QYewDwBeBzAGCMqU5USnlRf3qdo8g1M+8da8C1M6+qqu4gGhCSOef8uSsSQryP9cTMXEq5tgDnMi+KYtE0zW9XlKbpTVmWBw/E6p124AMwxj4A3AL4AbA9ib4AuCeigxDiyQS5OpMA89W+MXXdmABrTCccaMtO8egY9/+YDzouWgyAdSrOAXwRuYvo05lyMNbkVwB3IU3uH+YFTE1IyJhqQKxF04BYp0IDYh07CzDn5ofWXHTzQyDRAX96A+75hKkLHAAAAABJRU5ErkJggg=='
},
'zoomArrow48': { # 缩放箭头
'path': 'asset/icon/zoomArrow48.png',
'isTK': True,
'isSave': False,
'base64': r'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFyGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDMgNzkuMTY0NTI3LCAyMDIwLzEwLzE1LTE3OjQ4OjMyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIzLTA0LTIyVDA5OjUwOjE3KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0wNC0yMlQxMDoxNzo0MyswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0wNC0yMlQxMDoxNzo0MyswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MmQyMjdhMWEtMzNhZS0xODQ3LWE4NWItYTMwNTg0NWYwMjRiIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZTlmMzQ0N2EtYjRjMS1hNDQ0LTgwZDUtMmNhMDcyYmExZWY3IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NWI5NDBjMTgtZDNkYi1iZTQ3LWI0Y2UtYzU5NmVhOGE3N2M1Ij4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1Yjk0MGMxOC1kM2RiLWJlNDctYjRjZS1jNTk2ZWE4YTc3YzUiIHN0RXZ0OndoZW49IjIwMjMtMDQtMjJUMDk6NTA6MTcrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMi4xIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MmQyMjdhMWEtMzNhZS0xODQ3LWE4NWItYTMwNTg0NWYwMjRiIiBzdEV2dDp3aGVuPSIyMDIzLTA0LTIyVDEwOjE3OjQzKzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+XnXObQAAAcVJREFUaN7t2r9qg0AABvBA50CHhq5CVwPSwaEuTgWHghSEZhM6BIRAiC9wD5A1L5DBwQdw9REyBR8jkBdI7wqGQ877kzb4XcjBt1xOvJ9e9I5zdDqdRjZndDOATnmheaP5okkHzgfNK80D30EZ4JOGAOab5lEFeAftPJ+nPsCUbziZTEiWZWTo4vu+6E4IAUnbyPM80jQNofUQqeu6i3gWAVZtgzzPYTrfJkkSHuCJAOcGTNweyAo7eLFYaF+tawBY4foYagOiKDrXM4jsJGma/raL4xgHwK48/5sMwbeDAQjGXy8CFqCLgAboIOABKoQVABnCGkAfwiqACGEdQIaAAux2OxKGoTTQAMdxjOftUIB2jqObIAjw/gOsTjdQs1GU3AH/tTS8dAgOCjgej8ZPscPhgAPonFwrm80GGyB7Ka7Xa6wh1HcHVGtsSEAXo4uAAlyCgAKIhpUKAQcwRUACTBCwAF0ENEAHMSigKIreN6wMsd1ucSZzy+VS+IaVIaAAprPWqqqMh9BKNU6HzGw2U25wJNdc0/4l+/2ejMdj5RbTtPsUKMvSaC18jcznc+K6rtYmn/XbrDex0W3vpwb3r1UGyg+Pf4rB9XHtOgAAAABJRU5ErkJggg=='
},
'umiocrico': { # 主图标 256-16像素 ico格式
'path': 'asset/icon/umiocr.ico',
'isTK': False,
Expand Down

0 comments on commit 137f8db

Please sign in to comment.