当前位置: 首页 > news >正文

Python截图轻量化工具

一、兼容局限性

这是用Python做的截图工具,不过由于使用了ctypes调用了Windows的API, 同时访问了Windows中"C:/Windows/Cursors/"中的.cur光标样式文件, 这个工具只适用于Windows环境;

如果要提升其跨平台性的话,需要考虑替换ctypes的一些专属于Windows的API以及设置不同的.cur访问方式

二、轻量化

该模块只使用了PIL这一个第三方库,和别的使用pygame\pyautogui等模块不同,该工具着重强调轻量化,无关或没必要使用的库尽可能不使用。

目前截图功能已经高度接近微信截图,不过没有编辑功能(建议保存后再编辑);里面包含了一些必备的快捷功能,不过还没有绑定快捷键;有需要者自行为代码中加入功能。

三、基础功能演示

功能演示

四、功能阐述

①基本的截图后等待,可继续移动边框,拖动调整上下左右对角位置(类似微信)

②打开图片文件,可以直接显示在面板上;

③复制图片文件到剪切板上;

④保存当前视图上的图片。

⑤对截取过的图片进行翻页,快速查看不同截图;

⑥删除视图中的图片,不影响磁盘文件,只是删除内存中的图片引用;

⑦通过调节窗口可动态调整图片在视图中的大小(不改变根引用图片);

⑧窗口置顶,即无论怎么打开其他窗口,该窗口都始终置于顶部不会被遮挡,方便我们对截图后的图片放置到一侧,参照图片进行操作

五、完整代码

代码如下,单文件,请自行安装PIL模块,确保在Windows环境上运行。

import os
import ctypes
import subprocess as sp
import tkinter as tk
from threading import Thread
from tkinter import messagebox, filedialog
from PIL import Image, ImageGrab, ImageTk, UnidentifiedImageErrorScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0)
ctypes.windll.shcore.SetProcessDpiAwareness(1)
TEMP_FILE_PATH = os.path.join(os.getenv("TEMP"), "tempScreenshot")
os.makedirs(TEMP_FILE_PATH, exist_ok=True)class FlatButton(tk.Label):def __init__(self, parent, command=None, enter_fg="#000000", click_color="#25C253", *args, **kwargs):super().__init__(parent, *args, **kwargs)self.__fg = fg = kwargs.get("fg", "#3B3B3B")self.__enter_fg = enter_fgself.__click_fg = click_colorself.command = commandself.config(cursor="hand2", fg=fg)self.enable()if fg == enter_fg:raise ValueError("enter_fg must be different from fg")def enable(self):self.bind("<Enter>", lambda _: self.config(fg=self.__enter_fg))self.bind("<Leave>", lambda _: self.config(fg=self.__fg))self.bind("<Button-1>", lambda _: self.config(fg=self.__click_fg))self.bind("<ButtonRelease-1>", self.__command)def disable(self):for event in ("<Enter>", "<Leave>", "<Button-1>", "<ButtonRelease-1>"):self.unbind(event)def __command(self, event):try:if self.cget("fg") in (self.__enter_fg, self.__click_fg):self.command(event)self.config(fg=self.__fg)except tk.TclError:passexcept TypeError:self.config(fg=self.__fg)class AdjustableRect(object):"""The judgement seq is so important that you must care about:(right, bottom), (left, top), (right, top), (left, bottom),(center_x, top), (center_x, bottom), (left, center_y, ), (right, center_y)"""ANCHOR_SIZE = 3ANCHOR_HOVER_DISTANCE = 20CURSOR_FILES_NAME = ["aero_nwse_l.cur", "aero_nesw_l.cur", "aero_ns_l.cur", "aero_ew_l.cur"]CURSOR_FILES = [f"@C:/Windows/Cursors/{cursor_file}" for cursor_file in CURSOR_FILES_NAME]CURSORS = [CURSOR_FILES[0], CURSOR_FILES[0], CURSOR_FILES[1], CURSOR_FILES[1],CURSOR_FILES[2], CURSOR_FILES[2], CURSOR_FILES[3], CURSOR_FILES[3],"fleur", "arrow"]def __init__(self, parent, screenshot):self.parent: tk.Canvas = parentself.screenshot: ScreenshotUtils = screenshotself.__rect: int = 0self.__anchors: list[int] = []self.anchor_id: int = 0def rect_coords(self) -> tuple[int, int, int, int]:return self.parent.coords(self.__rect)def anchor_coords(self) -> tuple[int, int, int, int]:left, top, right, bottom = self.rect_coords()horizontal_middle = (left + right) // 2vertical_middle = (top + bottom) // 2return ((left, top), (horizontal_middle, top), (right, top), (right, vertical_middle),(right, bottom), (horizontal_middle, bottom), (left, bottom), (left, vertical_middle))def rect_width_height(self) -> tuple[int, int]:left, top, right, bottom = self.rect_coords()return int(right - left), int(bottom - top)def get_anchor(self, event) -> int:cls = self.__class__left, top, right, bottom = self.rect_coords()center_x, center_y = (left + right) // 2, (top + bottom) // 2def near(actual, target):return abs(actual - target) < cls.ANCHOR_HOVER_DISTANCE# 务必注意这个判断顺序,这与后面rect_adjust密切相关judgement_pos = ((right, bottom), (left, top), (right, top), (left, bottom),(center_x, top), (center_x, bottom), (left, center_y, ), (right, center_y))for index, pos in enumerate(judgement_pos):if near(event.x, pos[0]) and near(event.y, pos[1]):return indexif left < event.x < right and top < event.y < bottom:return 8return -1def create_anchors(self):cls = self.__class__for coord in self.anchor_coords():anchor = self.parent.create_rectangle(coord[0]-cls.ANCHOR_SIZE, coord[1]-cls.ANCHOR_SIZE,coord[0]+cls.ANCHOR_SIZE, coord[1]+cls.ANCHOR_SIZE,fill="#1AAE1A", outline="#1AAE1A")self.__anchors.append(anchor)def create_rect(self) -> None:self.__rect= self.parent.create_rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, outline='#1AAE1A', width=2)self.create_anchors()def move_anchors(self):cls = self.__class__for anchor, coord in zip(self.__anchors, self.anchor_coords()):self.parent.coords(anchor, coord[0]-cls.ANCHOR_SIZE, coord[1]-2, coord[0]+cls.ANCHOR_SIZE, coord[1]+cls.ANCHOR_SIZE)def on_press(self, event):self.screenshot.start_x = event.xself.screenshot.start_y = event.ydef on_release(self, _):self.screenshot.start_x, self.screenshot.start_y,\self.screenshot.end_x, self.screenshot.end_y = self.rect_coords()def on_hover(self, event):self.anchor_id = self.get_anchor(event)cursor = self.CURSORS[self.anchor_id]self.parent.config(cursor=cursor)def move_rect(self, event):offset_x = event.x - self.screenshot.move_start_xoffset_y = event.y - self.screenshot.move_start_yif self.screenshot.start_x + offset_x > 0 and self.screenshot.end_x + offset_x < SCREEN_WIDTH:self.screenshot.start_x += offset_xself.screenshot.end_x += offset_xif  self.screenshot.start_y + offset_y > 0 and self.screenshot.end_y + offset_y < SCREEN_HEIGHT:self.screenshot.start_y += offset_yself.screenshot.end_y += offset_yself.screenshot.move_start_x = event.xself.screenshot.move_start_y = event.yself.parent.coords(self.__rect, self.screenshot.start_x, self.screenshot.start_y, self.screenshot.end_x, self.screenshot.end_y)self.move_anchors()def rect_adjust(self, event):if self.anchor_id == 8:return self.move_rect(event)if self.anchor_id == 0:self.screenshot.end_x, self.screenshot.end_y = event.x, event.yelif self.anchor_id == 1:self.screenshot.start_x, self.screenshot.start_y = event.x, event.yelif self.anchor_id == 2:self.screenshot.end_x, self.screenshot.start_y = event.x, event.yelif self.anchor_id == 3:self.screenshot.start_x, self.screenshot.end_y = event.x, event.yelif self.anchor_id == 4:self.screenshot.start_y = event.yelif self.anchor_id == 5:self.screenshot.end_y = event.yelif self.anchor_id == 6:self.screenshot.start_x = event.xelif self.anchor_id == 7:self.screenshot.end_x = event.xelse:returnself.parent.coords(self.__rect, self.screenshot.start_x, self.screenshot.start_y, self.screenshot.end_x, self.screenshot.end_y)self.move_anchors()class ScreenshotUtils(object):"""截图的关键是坐标;这个类管理着图片的引用和截图坐标;"""def TkS(value) -> int:return int(ScaleFactor/100*value)ZOOM: int = 4ZOOM_WIDTH: float = TkS(28.75)ZOOM_SCREEN_SIZE: int = int(ZOOM_WIDTH*ZOOM)MAGNIFIER_OFFSET: int = 36WIDTH_HEIGHT_OFFSET = 40POS_TAG_GAP = 10RGB_TAG_GAP = 47MAGNIFIER_ADJUST = 70AJUST_BAR_WIDTH: int = TkS(100)def __init__(self):self.start_x = self.start_y = self.end_x = self.end_y = 0self.move_start_x = self.move_start_y = self.move_end_x = self.move_end_y = 0self.page_index: int = 0self.current_image: Image.Image = Noneself.pixel_reader = Noneself.final_images: list[Image.Image] = list()# 这种是只移动但不改变大小和内容的控件,只需移动无需重绘self.screenshot_move_widget = list()# 这种是移动和改变大小的控件,需要实时重绘self.screenshot_redraw_widget = list()@staticmethoddef TkS(value) -> int:return int(ScaleFactor/100*value)@classmethoddef move_widget_coords(cls, x, y) -> list[tuple[int, int, int, int]]:# 按照主框架,水平线,垂直线的顺序返回坐标main_frame_coord  = (x, y, x+cls.ZOOM_SCREEN_SIZE, y+cls.ZOOM_SCREEN_SIZE)horrontal_line_coord = (x, y+cls.ZOOM_SCREEN_SIZE // 2, x+cls.ZOOM_SCREEN_SIZE, y+cls.ZOOM_SCREEN_SIZE // 2)vertical_line_coord = (x+cls.ZOOM_SCREEN_SIZE // 2, y, x+cls.ZOOM_SCREEN_SIZE // 2, y+cls.ZOOM_SCREEN_SIZE)return [main_frame_coord, horrontal_line_coord, vertical_line_coord]def redraw_widget_coords(self, x, y) -> list[tuple]:# 按照"放大镜图像"、"长 × 宽"、"POS标签"、"RGB标签"的顺序返回坐标offset = self.__class__.MAGNIFIER_OFFSETzoom_size = self.__class__.ZOOM_SCREEN_SIZEif x + offset + zoom_size < SCREEN_WIDTH:x_offset = x + offsetelse:x_offset = x - offset - zoom_sizeif y + offset + zoom_size + self.__class__.MAGNIFIER_ADJUST < SCREEN_HEIGHT:y_offset = y + offsetelse:y_offset = y - offset - zoom_size - self.__class__.MAGNIFIER_ADJUSTwidth_height_y = max(min(self.start_y, self.end_y) - self.__class__.WIDTH_HEIGHT_OFFSET, 0)width_height_info = (max(min(self.start_x, self.end_x), 0), width_height_y)magnifier_coord = (x_offset, y_offset)pos_info = (x_offset, y_offset + zoom_size + self.__class__.POS_TAG_GAP)rgb_info = (x_offset, y_offset + zoom_size + self.__class__.RGB_TAG_GAP)return [magnifier_coord, width_height_info, pos_info, rgb_info]class MainUI(tk.Tk):def __init__(self):super().__init__()self.screenshot = ScreenshotUtils()self.set_window()self.menu_bar: tk.Frame = self.set_menubar()self.cut_btn: FlatButton = self.set_cut_btn()self.load_image_btn: FlatButton = self.set_load_image_btn()self.copy_btn: FlatButton = self.set_copy_btn()self.save_btn: FlatButton = self.set_save_btn()self.turn_left_btn: FlatButton = self.set_turn_left_btn()self.turn_right_btn: FlatButton = self.set_turn_right_btn()self.delete_btn: FlatButton = self.set_delete_btn()self.show_image_canvas: tk.Canvas = self.set_show_image_canvas()self.capture_win: tk.Toplevel = Noneself.full_screenshot_canvas: tk.Canvas = Noneself.adjust_rect: AdjustableRect = Noneself.adjust_bar: tk.Frame = Nonedef set_window(self):self.title("截图工具")width, height = self.screenshot.TkS(255), self.screenshot.TkS(30)self.minsize(width=width, height=height)  # 这里可以根据需要调整最小宽高self.attributes("-topmost", True)self.geometry(f"{width}x{height}")def set_menubar(self) -> tk.Frame:menubar = tk.Frame(self, bg="#FFFFFF", height=ScreenshotUtils.TkS(30))menubar.pack(fill=tk.X)menubar.pack_propagate(False)  # 阻止内部组件改变框架大小return menubardef set_cut_btn(self) -> FlatButton:btn_cut = FlatButton(self.menu_bar, text="✂", bg="#FFFFFF", font=("Segoe UI Emoji", 18),)btn_cut.pack(side=tk.LEFT, ipadx=0.1)return btn_cutdef set_load_image_btn(self) -> FlatButton:btn_load = FlatButton(self.menu_bar, text="📄", bg="#FFFFFF",font=("Segoe UI Emoji", 18, "bold"))btn_load.pack(side=tk.LEFT, ipadx=0.1)return btn_loaddef set_copy_btn(self) -> FlatButton:btn_copy = FlatButton(self.menu_bar, text="⎘", bg="#FFFFFF", font=("Segoe UI Symbol", 26),)btn_copy.pack(side=tk.LEFT, ipadx=0.1)return btn_copydef set_save_btn(self) -> FlatButton:btn_save = FlatButton(self.menu_bar, text="💾", bg="#FFFFFF", font=("Segoe UI Emoji", 18),)btn_save.pack(side=tk.LEFT, ipadx=0.1)return btn_savedef set_turn_left_btn(self) -> FlatButton:turn_left_btn = FlatButton(self.menu_bar, text="\u25C0", bg="#FFFFFF", font=("Segoe UI Emoji", 18),)turn_left_btn.pack(side=tk.LEFT, ipadx=0.1)return turn_left_btndef set_turn_right_btn(self) -> FlatButton:turn_page_btn = FlatButton(self.menu_bar, text="\u25B6", bg="#FFFFFF", font=("Segoe UI Emoji", 18),)turn_page_btn.pack(side=tk.LEFT, ipadx=0.1)return turn_page_btndef set_delete_btn(self) -> FlatButton:delete_btn = FlatButton(self.menu_bar, text="🗑", bg="#FFFFFF", font=("Segoe UI Symbol", 26, "bold"),)delete_btn.pack(side=tk.LEFT, ipadx=0.1)return delete_btndef set_cancel_btn(self, parent) -> FlatButton:cancel_btn = FlatButton(parent, self.clear_capture_info, text="×", bg="#FFFFFF",enter_fg="#DB1A21",fg="#CC181F", font=("微软雅黑", 20))cancel_btn.pack(side=tk.RIGHT, padx=5)return cancel_btndef set_confirm_btn(self, parent) -> FlatButton:confirm_btn = FlatButton(parent, self.confirm_capture, fg="#23B34C", text="√",enter_fg="#27C956", bg="#FFFFFF", font=("微软雅黑", 20))confirm_btn.pack(side=tk.RIGHT, padx=10)return confirm_btndef set_show_image_canvas(self) -> tk.Canvas:canvas = tk.Canvas(self, bg="white")return canvasdef set_adjust_bar(self) -> tk.Frame:self.adjust_bar = tk.Frame(self.full_screenshot_canvas, bg="#FFFFFF", height=50)cancel_btn = self.set_cancel_btn(self.adjust_bar)confirm_btn = self.set_confirm_btn(self.adjust_bar)cancel_btn.pack(side=tk.RIGHT, padx=5)confirm_btn.pack(side=tk.RIGHT, padx=10)def set_magnifier_frame(self, event) -> None:initial_coord = (0, 0, 0, 0)main_frame_id = self.full_screenshot_canvas.create_rectangle(*initial_coord, outline='#1AAE1A', width=1)horrontal_line = self.full_screenshot_canvas.create_line(*initial_coord, fill="#1AAE1A", width=2)vertical_line = self.full_screenshot_canvas.create_line(*initial_coord, fill="#1AAE1A", width=2)self.screenshot.screenshot_move_widget = [main_frame_id, horrontal_line, vertical_line]event.x = event.x + event.widget.winfo_rootx()event.y = event.y + event.widget.winfo_rooty()def set_full_screenshot_canvas(self, parent) -> tk.Canvas:img = ImageGrab.grab()self.screenshot.current_image = imgself.screenshot.pixel_reader = img.convert("RGB")photo = ImageTk.PhotoImage(img)full_screenshot_canvas = tk.Canvas(parent, bg="white", highlightthickness=0)full_screenshot_canvas.create_image(0, 0, anchor=tk.NW, image=photo)full_screenshot_canvas.image = photofull_screenshot_canvas.pack(fill=tk.BOTH, expand=True)return full_screenshot_canvasdef config_pos_rgb_info(self, parent: tk.Canvas, pos: str, rgb: str, pos_coord: tuple[int, int], rgb_coord: tuple[int, int]) -> tuple[int]:style = {"anchor": tk.NW, "font": ("微软雅黑", 9), "fill": "#FFFFFF"}pos_info = parent.create_text(*pos_coord, text=pos, **style)rgb_info = parent.create_text(*rgb_coord, text=rgb, **style)pos_bg = parent.create_rectangle(*parent.bbox(pos_info), outline="#000000", fill="#000000")rgb_bg = parent.create_rectangle(*parent.bbox(rgb_info), outline="#000000", fill="#000000")parent.tag_raise(pos_info)parent.tag_raise(rgb_info)return pos_info, rgb_info, pos_bg, rgb_bgclass ScreenshotTool(MainUI):def __init__(self):super().__init__()self.add_command()def add_command(self):self.protocol("WM_DELETE_WINDOW", self.on_close)self.show_image_canvas.bind("<Configure>", self.auto_adjust_image_size)self.cut_btn.command = self.start_captureself.load_image_btn.command = self.load_imageself.copy_btn.command = self.copy_imageself.save_btn.command = self.save_imageself.turn_left_btn.command = self.turn_pageself.turn_right_btn.command = self.turn_pageself.delete_btn.command = self.delete_imagedef initialize_screenshot_coords(self):self.is_drag = Falseself.screenshot.start_x = self.screenshot.start_y = 0self.screenshot.end_x = SCREEN_WIDTHself.screenshot.end_y = SCREEN_HEIGHTdef start_capture(self, event):self.attributes('-alpha', 0)self.update()self.capture_win = tk.Toplevel()self.capture_win.geometry(f"{SCREEN_WIDTH}x{SCREEN_HEIGHT}+0+0")self.capture_win.overrideredirect(True)self.full_screenshot_canvas = self.set_full_screenshot_canvas(self.capture_win)self.adjust_rect = AdjustableRect(self.full_screenshot_canvas, self.screenshot)self.initialize_screenshot_coords()self.adjust_rect.create_rect()self.set_magnifier_frame(event)self.update_magnifier(event)self.set_adjust_bar()self.full_screenshot_canvas.bind("<Button-1>", self.on_press)self.full_screenshot_canvas.bind("<Motion>", self.update_magnifier)self.full_screenshot_canvas.bind("<ButtonRelease-1>", self.on_release)def on_press(self, event) -> None:self.adjust_rect.on_press(event)self.full_screenshot_canvas.unbind("<Motion>")self.full_screenshot_canvas.bind("<Motion>", self.on_drag)def on_drag(self, event) -> None:self.adjust_rect.rect_adjust(event)self.update_magnifier(event)def on_release(self, event, resize=True) -> None:self.unbind_all()self.adjust_rect.on_release(event)self.full_screenshot_canvas.bind("<Button-1>", self.enter_adjust_mode)self.full_screenshot_canvas.bind("<Motion>", self.adjust_rect.on_hover)self.adjust_bar.place(x=min(self.screenshot.end_x - 300, SCREEN_WIDTH - 300), width=300,y=max(min(self.screenshot.end_y + 10, SCREEN_HEIGHT - ScreenshotUtils.TkS(40)), 0),)if resize:self.screenshot.end_x, self.screenshot.end_y = event.x, event.yself.conceal_move_widget()self.update_width_height_info(event)def rect_adjust(self, event) -> None:self.adjust_rect.rect_adjust(event)if self.adjust_rect.anchor_id != 8 or self.adjust_rect.anchor_id == -1:self.update_magnifier(event)def unbind_all(self):events = ("<Button-1>", "<Motion>", "<ButtonRelease-1>")for event in events:self.full_screenshot_canvas.unbind(event)def clear_redraw_widget(self) -> None:for redraw_widget in self.screenshot.screenshot_redraw_widget:self.full_screenshot_canvas.delete(redraw_widget)self.screenshot.screenshot_redraw_widget.clear()def conceal_move_widget(self):for widget in self.screenshot.screenshot_move_widget:self.full_screenshot_canvas.tag_lower(widget)def update_width_height_info(self, event) -> None:self.clear_redraw_widget()w, h = self.adjust_rect.rect_width_height()coord = self.screenshot.redraw_widget_coords(event.x, event.y)[1]wh_info_widget = self.full_screenshot_canvas.create_text(*coord, anchor=tk.NW, fill="white", text=f"{w} × {h}")self.screenshot.screenshot_redraw_widget = [wh_info_widget]def update_magnifier(self, event, ) -> None:x, y = event.x, event.ysize = ScreenshotUtils.ZOOM_WIDTHimg = self.screenshot.current_image.crop((x - size//2, y - size//2, x + size//2, y + size//2))img = img.resize((ScreenshotUtils.ZOOM_SCREEN_SIZE, ScreenshotUtils.ZOOM_SCREEN_SIZE))photo = ImageTk.PhotoImage(img)self.full_screenshot_canvas.image2 = photow, h = self.adjust_rect.rect_width_height()self.clear_redraw_widget()redraw_widget_coords = self.screenshot.redraw_widget_coords(x, y)magnifier_coord, width_height_info, pos_coord, rgb_coord = redraw_widget_coordszoom_img = self.full_screenshot_canvas.create_image(*magnifier_coord, anchor=tk.NW, image=photo)wh_info_widget = self.full_screenshot_canvas.create_text(*width_height_info, anchor=tk.NW, fill="white", text=f"{w} × {h}")pos_rgb_info = self.config_pos_rgb_info(self.full_screenshot_canvas, f"POS: ({x}, {y})", f"RGB: {self.screenshot.pixel_reader.getpixel((x, y))}", pos_coord, rgb_coord)self.screenshot.screenshot_redraw_widget = [zoom_img, wh_info_widget, *pos_rgb_info]self.update_magnifier_frame(*self.full_screenshot_canvas.coords(zoom_img))def update_magnifier_frame(self, x, y) -> None:coords = self.screenshot.move_widget_coords(x, y)for widget, coord in zip(self.screenshot.screenshot_move_widget, coords):self.full_screenshot_canvas.coords(widget, *coord)self.full_screenshot_canvas.tag_raise(widget)def enter_adjust_mode(self, event) -> None:self.screenshot.move_start_x = event.xself.screenshot.move_start_y = event.yself.adjust_bar.place_forget()for widget in self.screenshot.screenshot_redraw_widget:self.full_screenshot_canvas.delete(widget)for widget in self.screenshot.screenshot_move_widget:self.full_screenshot_canvas.tag_lower(widget)self.full_screenshot_canvas.bind("<B1-Motion>", self.rect_adjust)self.full_screenshot_canvas.bind("<ButtonRelease-1>", lambda e: self.on_release(e, False))def clear_capture_info(self, _) -> None:self.capture_win.destroy()self.full_screenshot_canvas.destroy()self.attributes('-alpha', 1)self.screenshot.screenshot_move_widget.clear()def check_capture_screenshot(self) -> None:if self.screenshot.start_x == self.screenshot.end_x or \self.screenshot.start_y == self.screenshot.end_y:self.initialize_screenshot_coords()def confirm_capture(self, event) -> None:x1, y1, x2, y2 = self.adjust_rect.rect_coords()self.clear_capture_info(event)image = self.screenshot.current_image.crop((x1, y1, x2, y2))result = self.show_image(image)self.screenshot.final_images.append(result)self.screenshot.page_index = len(self.screenshot.final_images) - 1self.attributes('-topmost', 1)def show_image(self, image: Image.Image, window_resize=True) -> None:if window_resize:self.geometry(f"{image.width}x{image.height+self.menu_bar.winfo_height()}")photo = ImageTk.PhotoImage(image)self.show_image_canvas.delete("all")self.show_image_canvas.create_image(0, 0, anchor=tk.NW, image=photo)self.show_image_canvas.image = photoself.show_image_canvas.pack(fill=tk.BOTH, expand=True)return imagedef current_limiting(self, canvas_w, canvas_h) -> bool:if not self.screenshot.final_images:return Truetry:coords = self.show_image_canvas.bbox("all")img_view_w, img_view_h = coords[2], coords[3]except TypeError:return Trueif (img_view_w == canvas_w and img_view_h < canvas_h) or \(img_view_h == canvas_h and img_view_w < canvas_w):return Truereturn Falsedef auto_adjust_image_size(self, _) -> None:canvas_w = self.show_image_canvas.winfo_width()canvas_h = self.show_image_canvas.winfo_height()if self.current_limiting(canvas_w, canvas_h):returnimage = self.screenshot.final_images[self.screenshot.page_index]width_ratio = canvas_w / image.widthheight_ratio = canvas_h / image.heightratio = min(width_ratio, height_ratio)new_width = int(image.width * ratio)new_height = int(image.height * ratio)resized_image = image.resize((new_width, new_height), Image.LANCZOS)self.show_image(resized_image, False)def load_image(self, _) -> None:file_types = (("Image files", "*.jpg *.png *.jpeg"),)img_path = filedialog.askopenfilename(filetypes=file_types)if not img_path:returntry:image = Image.open(img_path)except UnidentifiedImageError:return messagebox.showerror("错误", "无法识别该图片文件!")self.show_image(image)self.screenshot.final_images.append(image)self.screenshot.page_index = len(self.screenshot.final_images) - 1def copy_image(self, _) -> None:def __copy_image():self.copy_btn.disable()self.copy_btn.config(fg="#25C253")image = self.screenshot.final_images[self.screenshot.page_index]temp_name = f"{int.from_bytes(os.urandom(4), byteorder='big')}.png"temp_file = os.path.join(TEMP_FILE_PATH, temp_name)image.save(temp_file)startupinfo = sp.STARTUPINFO()startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOWargs = ['powershell', f'Get-Item {temp_file} | Set-Clipboard']process = sp.Popen(args=args, startupinfo=startupinfo)process.wait()self.title("截图工具")self.copy_btn.enable()self.copy_btn.config(fg="#3B3B3B")if len(self.screenshot.final_images) == 0:return messagebox.showerror("复制失败", "未检测到截取图像")self.title("复制成功!")Thread(target=__copy_image, daemon=True).start()def save_image(self, _) -> None:try:image = self.screenshot.final_images[self.screenshot.page_index]filename = filedialog.asksaveasfilename(defaultextension=".png",filetypes=[("PNG files", "*.png"), ("JPEG files", "*.jpg")],initialfile=f"{image.width}x{image.height}.png")if not filename:returnimage.save(filename)except IndexError:messagebox.showerror("保存失败", "未检测到截取图像")def turn_page(self, event) -> None:if len(self.screenshot.final_images) == 0:return messagebox.showinfo("提示", "暂无图片可切换!")if event.widget == self.turn_left_btn:if self.screenshot.page_index == 0:return messagebox.showinfo("提示", "已经是第一张图片!")self.screenshot.page_index -= 1else:if self.screenshot.page_index == len(self.screenshot.final_images) - 1:return messagebox.showinfo("提示", "已经是最后一张图片!")self.screenshot.page_index += 1self.show_image(self.screenshot.final_images[self.screenshot.page_index])def delete_image(self, _) -> None:if len(self.screenshot.final_images) == 0:return messagebox.showinfo("提示", "暂无图片可删除!")if not messagebox.askokcancel("提示", "确认删除当前图片?"):returnself.screenshot.final_images.pop(self.screenshot.page_index)if self.screenshot.page_index == len(self.screenshot.final_images):self.screenshot.page_index -= 1if len(self.screenshot.final_images) == 0:self.show_image_canvas.delete("all")self.geometry(f"{self.screenshot.TkS(255)}x{self.screenshot.TkS(30)}")else:self.show_image(self.screenshot.final_images[self.screenshot.page_index])def on_close(self):def delete_tmp_files():for file in os.scandir(TEMP_FILE_PATH):os.remove(file)self.destroy()Thread(target=delete_tmp_files).start()if __name__ == "__main__":app = ScreenshotTool()SCREEN_WIDTH = app.winfo_screenwidth()SCREEN_HEIGHT = app.winfo_screenheight()app.mainloop()

六、打包程序

如无Python环境,可以通过以下链接下载:
链接:https://pan.quark.cn/s/f87bf5f9e825
提取码:nMGP

相关文章:

Python截图轻量化工具

一、兼容局限性 这是用Python做的截图工具&#xff0c;不过由于使用了ctypes调用了Windows的API, 同时访问了Windows中"C:/Windows/Cursors/"中的.cur光标样式文件, 这个工具只适用于Windows环境&#xff1b; 如果要提升其跨平台性的话&#xff0c;需要考虑替换cty…...

Android图片加载框架Coil,Kotlin

Android图片加载框架Coil&#xff0c;Kotlin implementation("io.coil-kt:coil:1.4.0") import android.os.Bundle import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import coil.Coil i…...

重生之我要当云原生大师(十四)分析和存储日志

目录 一、简述常用的日志文件所存储的消息类型。 二、syslog的优先级&#xff1f; 三、维护准确时间的意义&#xff1f; 一、简述常用的日志文件所存储的消息类型。 1. 系统日志文件 /var/log/messages 消息类型&#xff1a;通用的系统日志文件&#xff0c;记录系统启动、…...

C++基础知识学习记录—补充

1、C新增的赋值语法 #include <iostream>using namespace std;int main() {//C新的赋值语法//1、传统的赋值语法int a,b;a100;b99;//C新的赋值语法int c(2);//相当于给c赋值2int d(c);//相当于把c的值赋值给dcout << "c" << c << endl;co…...

Linux常用命令——磁盘管理类

文章目录 du 查看文件和目录占用的磁盘空间df 查看磁盘空间使用情况free 显示内存占用情况lsblk 查看设备挂载情况mount/umount 挂载/卸载fdisk 分区Linux分区分区操作按键说明挂载硬盘操作 du 查看文件和目录占用的磁盘空间 ls-lh是查看文件大小du: disk usage 磁盘占用情况d…...

【AI学习】DeepSeek为什么强?

个人的一些思考&#xff0c;请大家批评指正。 这个问题&#xff0c;首先当然是在恰当的时间出现&#xff0c;模型性能跻身世界一流&#xff0c;又开源&#xff0c;戳破了OpenAI和英伟达潜心构造的叙事逻辑。 DeepSeek为什么强&#xff1f;四个方面&#xff1a;模型的智能水平…...

【快应用】原生广告下载状态监听案例

问题背景&#xff1a; 快应用中下载类原生广告监听下载状态变化接口调用没生效&#xff0c;在上报点击接口里触发下载监听后仅第一次返回状态&#xff0c;之后就不返回了&#xff0c;该如何处理&#xff1f; 问题分析&#xff1a; 快应用在1100版本新增了一个ad-button组件&a…...

C++语言的软件工程

C语言的软件工程 引言 在当今快速发展的技术领域&#xff0c;软件工程作为一门综合性的学科&#xff0c;越来越受到重视。而C语言&#xff0c;作为一种功能强大且具有广泛应用的编程语言&#xff0c;在软件工程中占据着重要的地位。本文将探讨C语言在软件工程中的应用与特点&…...

计算机网络结课设计:通过思科Cisco进行中小型校园网搭建

上学期计算机网络课程的结课设计是使用思科模拟器搭建一个中小型校园网&#xff0c;当时花了几天时间查阅相关博客总算是做出来了&#xff0c;在验收后一直没管&#xff0c;在寒假想起来了简单分享一下&#xff0c;希望可以给有需求的小伙伴一些帮助 目录 一、设计要求 二、…...

无人机避障——基于ESDF地图的JPS算法前端路径规划

原来是用栅格地图的方式&#xff0c;0表示可通行区域&#xff0c;1表示不可通行区域&#xff0c;然后采用JPS算法做路径规划&#xff0c;从起点到终点规划出一条路径。但是目前我需要做的是将栅格地图更换为ESDF地图&#xff0c;那么JPS算法计算代价的部分是否需要进行变化。 …...

【MQ】RabbitMQ 高可用延时功能的探究

延迟消息如果使用延时交换机来实现&#xff0c;如果数据量过大&#xff0c;就会很占 CPU 资源&#xff0c;轻则时间误差大&#xff0c;重则 RabbitMQ 宕机 一、针对一个 RabbitMQ 节点 &#xff08;1&#xff09;利用队列 ttl&#xff0c;将延迟消息根据 delay 的时间进行分级…...

Maven入门核心知识点总结

Maven 1. POM&#xff08;Project Object Model&#xff09;2. 坐标&#xff08;Coordinates&#xff09;3. 依赖管理&#xff08;Dependency Management&#xff09;4. 常用五个生命周期&#xff08;Life Circle&#xff09;5. Maven 仓库&#xff08;Maven Repository&#x…...

【力扣】138.随机链表的复制

AC截图 题目 代码 使用哈希存储<旧节点&#xff0c;新结点> /* // Definition for a Node. class Node { public:int val;Node* next;Node* random;Node(int _val) {val _val;next NULL;random NULL;} }; */class Solution { public:Node* copyRandomList(Node* hea…...

编程语言的深度剖析:从语法到性能优化

引言 随着软件开发的不断进化&#xff0c;编程语言的选择对项目的成功与否具有关键影响。今天的开发者面临着丰富多样的编程语言选择&#xff1a;每一种语言都有独特的优势、特性和适用场景。然而&#xff0c;语言的设计理念、运行机制和优化技巧背后的技术细节却常常被忽视。本…...

台湾精锐APEX减速机在半导体制造设备中的应用案例

半导体制造设备对传动系统的精度、可靠性和稳定性要求极高&#xff0c;台湾精锐APEX减速机凭借其低背隙、高精度和高刚性等优势&#xff0c;在半导体制造设备中得到了广泛应用。 案例一&#xff1a;晶圆切割设备 1.应用场景 在晶圆切割过程中&#xff0c;设备需要高精度的运…...

Rocketmq 和 Rabbitmq ,在多消费者的情况下,可以实现顺序消费吗

在多消费者的情况下&#xff0c;RocketMQ 和 RabbitMQ 都可以实现顺序消费&#xff0c;但它们的实现机制和适用场景有所不同。以下是对两者的详细分析和对比&#xff1a; 1. RocketMQ 的顺序消费 1.1 实现机制 顺序消息&#xff1a;RocketMQ 支持顺序消息&#xff08;Orderly …...

Springboot原理(面试高频)

目录 一、 配置优先级 ​编辑 二、Bean管理 ​​​​​​​2.1&#xff1a;获取Bean ​编辑 ​​​​​​​2.2&#xff1a;Bean作用域 ​​​​​​​​​​​​​​2.3&#xff1a;第三方Bean 三、Springboot底层原理 3.1&#xff1a;起步依赖 3.1.1&#xff1a;ma…...

2024 Rust现代实用教程:1.1Rust简介与安装更新

文章目录 一、Rust安装二、更新Rust三、Rust的Stable与Nightly版本四、卸载ubuntu安装的cargo和rustup五、rust源设置六、rust交叉编译工具链说明 rustup稳定版交叉编译步骤 步骤 1&#xff1a;安装目标组件步骤 2&#xff1a;安装交叉编译工具链步骤 3&#xff1a;配置环境变…...

yolov11模型在Android设备上运行【踩坑记录】

0) 参考资料: https://github.com/Tencent/ncnn?tabreadme-ov-file https://github.com/pnnx/pnnx https://github.com/nihui/ncnn-android-yolov5 https://github.com/Tencent/ncnn?tabreadme-ov-file 1) &#xff1a;将xxx.pt模型转化成 xxx.onnx ONNX&#xff08;Ope…...

提示工程:少样本提示(Few-shot Prompting)

少样本提示&#xff08;Few-shot Prompting&#xff09;是一种利用大语言模型从少量示例样本中学习并处理任务的方法。它的核心思想是利用大语言模型的上下文学习能力&#xff0c;通过在提示中增加“示例样本”来启发大语言模型达到举一反三的效果。这种方法避免了重新训练或者…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)

骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术&#xff0c;它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton)&#xff1a;由层级结构的骨头组成&#xff0c;类似于人体骨骼蒙皮 (Mesh Skinning)&#xff1a;将模型网格顶点绑定到骨骼上&#xff0c;使骨骼移动…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

docker 部署发现spring.profiles.active 问题

报错&#xff1a; org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

AI,如何重构理解、匹配与决策?

AI 时代&#xff0c;我们如何理解消费&#xff1f; 作者&#xff5c;王彬 封面&#xff5c;Unplash 人们通过信息理解世界。 曾几何时&#xff0c;PC 与移动互联网重塑了人们的购物路径&#xff1a;信息变得唾手可得&#xff0c;商品决策变得高度依赖内容。 但 AI 时代的来…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析

Linux 内存管理实战精讲&#xff1a;核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用&#xff0c;还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...