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, UnidentifiedImageError


    
ScaleFactor = 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_fg
        self.__click_fg = click_color
        self.command = command
        self.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:
            pass
        except 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 = 3
    ANCHOR_HOVER_DISTANCE = 20
    CURSOR_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 = parent
        self.screenshot: ScreenshotUtils = screenshot
        self.__rect: int = 0
        self.__anchors: list[int] = []
        self.anchor_id: int = 0
 
    def 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) // 2
        vertical_middle = (top + bottom) // 2
        return (
            (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) // 2
        def 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 index
        if left < event.x < right and top < event.y < bottom:
            return 8
        return -1
 
    def 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.x
        self.screenshot.start_y = event.y
 
    def 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_x
        offset_y = event.y - self.screenshot.move_start_y
        if self.screenshot.start_x + offset_x > 0 and self.screenshot.end_x + offset_x < SCREEN_WIDTH:
            self.screenshot.start_x += offset_x
            self.screenshot.end_x += offset_x
        if  self.screenshot.start_y + offset_y > 0 and self.screenshot.end_y + offset_y < SCREEN_HEIGHT:
            self.screenshot.start_y += offset_y
            self.screenshot.end_y += offset_y
        self.screenshot.move_start_x = event.x
        self.screenshot.move_start_y = event.y
        self.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.y
        elif self.anchor_id == 1:
            self.screenshot.start_x, self.screenshot.start_y = event.x, event.y
        elif self.anchor_id == 2:
            self.screenshot.end_x, self.screenshot.start_y = event.x, event.y
        elif self.anchor_id == 3:
            self.screenshot.start_x, self.screenshot.end_y = event.x, event.y
        elif self.anchor_id == 4:
            self.screenshot.start_y = event.y
        elif self.anchor_id == 5:
            self.screenshot.end_y = event.y
        elif self.anchor_id == 6:
            self.screenshot.start_x = event.x
        elif self.anchor_id == 7:
            self.screenshot.end_x = event.x
        else:
            return
        self.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 = 4
    ZOOM_WIDTH: float = TkS(28.75)
    ZOOM_SCREEN_SIZE: int = int(ZOOM_WIDTH*ZOOM)
    MAGNIFIER_OFFSET: int = 36
    WIDTH_HEIGHT_OFFSET = 40
    POS_TAG_GAP = 10
    RGB_TAG_GAP = 47
    MAGNIFIER_ADJUST = 70
    AJUST_BAR_WIDTH: int = TkS(100)

    def __init__(self):
        self.start_x = self.start_y = self.end_x = self.end_y = 0
        self.move_start_x = self.move_start_y = self.move_end_x = self.move_end_y = 0
        self.page_index: int = 0
        self.current_image: Image.Image = None
        self.pixel_reader = None
        self.final_images: list[Image.Image] = list()
        # 这种是只移动但不改变大小和内容的控件,只需移动无需重绘
        self.screenshot_move_widget = list()
        # 这种是移动和改变大小的控件,需要实时重绘
        self.screenshot_redraw_widget = list()
 
    @staticmethod
    def TkS(value) -> int:
        return int(ScaleFactor/100*value)
    
    @classmethod
    def 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_OFFSET
        zoom_size = self.__class__.ZOOM_SCREEN_SIZE
        if x + offset + zoom_size < SCREEN_WIDTH:
            x_offset = x + offset
        else:
            x_offset = x - offset - zoom_size
        if y + offset + zoom_size + self.__class__.MAGNIFIER_ADJUST < SCREEN_HEIGHT:
            y_offset = y + offset
        else:
            y_offset = y - offset - zoom_size - self.__class__.MAGNIFIER_ADJUST
        width_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 = None
        self.full_screenshot_canvas: tk.Canvas = None
        self.adjust_rect: AdjustableRect = None
        self.adjust_bar: tk.Frame = None

    def 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 menubar

    def 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_cut
    
    def 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_load
    
    def 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_copy

    def 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_save

    def 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_btn
 
    def 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_btn
    
    def 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_btn
    
    def 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_btn

    def 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_btn
 
    def set_show_image_canvas(self) -> tk.Canvas:
        canvas = tk.Canvas(self, bg="white")
        return canvas
    
    def 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 = img
        self.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 = photo
        full_screenshot_canvas.pack(fill=tk.BOTH, expand=True)
        return full_screenshot_canvas
    
    def 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_bg


class 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_capture
        self.load_image_btn.command = self.load_image
        self.copy_btn.command = self.copy_image
        self.save_btn.command = self.save_image
        self.turn_left_btn.command = self.turn_page
        self.turn_right_btn.command = self.turn_page
        self.delete_btn.command = self.delete_image
 
    def initialize_screenshot_coords(self):
        self.is_drag = False
        self.screenshot.start_x = self.screenshot.start_y = 0
        self.screenshot.end_x = SCREEN_WIDTH
        self.screenshot.end_y = SCREEN_HEIGHT

    def 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.y
        self.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.y
        size = ScreenshotUtils.ZOOM_WIDTH
        img = 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 = photo
        w, 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_coords
        zoom_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.x
        self.screenshot.move_start_y = event.y
        self.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) - 1
        self.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 = photo
        self.show_image_canvas.pack(fill=tk.BOTH, expand=True)
        return image

    def current_limiting(self, canvas_w, canvas_h) -> bool:
        if not self.screenshot.final_images:
            return True
        try:
            coords = self.show_image_canvas.bbox("all")
            img_view_w, img_view_h = coords[2], coords[3]
        except TypeError:
            return True
        if (img_view_w == canvas_w and img_view_h < canvas_h) or \
        (img_view_h == canvas_h and img_view_w < canvas_w):
            return True
        return False

    def 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):
            return
        image = self.screenshot.final_images[self.screenshot.page_index]
        width_ratio = canvas_w / image.width
        height_ratio = canvas_h / image.height
        ratio = 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:
            return
        try:
            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) - 1

    def 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_USESHOWWINDOW
            args = ['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:
                return
            image.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 -= 1
        else:
            if self.screenshot.page_index == len(self.screenshot.final_images) - 1:
                return messagebox.showinfo("提示", "已经是最后一张图片!")
            self.screenshot.page_index += 1
        self.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("提示", "确认删除当前图片?"):
            return
        self.screenshot.final_images.pop(self.screenshot.page_index)
        if self.screenshot.page_index == len(self.screenshot.final_images):
            self.screenshot.page_index -= 1
        if 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/967658.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

字节跳动后端一面

&#x1f4cd;1. Gzip压缩技术详解 Gzip是一种流行的无损数据压缩格式&#xff0c;它使用DEFLATE算法来减少文件大小&#xff0c;广泛应用于网络传输和文件存储中以提高效率。 &#x1f680; 使用场景&#xff1a; • 网站优化&#xff1a;通过压缩HTML、CSS、JavaScript文件来…

Visual Studio踩过的坑

统计Unity项目代码行数 编辑-查找和替换-在文件中查找 查找内容输入 b*[^:b#/].*$ 勾选“使用正则表达式” 文件类型留空 也有网友做了指定&#xff0c;供参考 !*\bin\*;!*\obj\*;!*\.*\*!*.meta;!*.prefab;!*.unity 打开Unity的项目 注意&#xff1a;只是看&#xff0…

智慧机房解决方案(文末联系,领取整套资料,可做论文)

智慧机房解决方案-软件部分 一、方案概述 本智慧机房解决方案旨在通过硬件设备与软件系统的深度整合&#xff0c;实现机房的智能化管理与服务&#xff0c;提升机房管理人员的工作效率&#xff0c;优化机房运营效率&#xff0c;确保机房设备的安全稳定运行。软件部分包括机房管…

ubuntu中如何在vscode的终端目录后显示(当前的git分支名) 实测有用

效果展示 配置过程&#xff1a; 在 Ubuntu 中&#xff0c;如果你想在 VS Code 的终端提示符后显示当前的 Git 分支名&#xff0c;可以通过修改 Shell 配置文件&#xff08;如 ~/.bashrc 或 ~/.zshrc&#xff09;来实现。以下是具体步骤&#xff1a; 1. 确定使用的 Shell 首…

【机器学习案列】车辆二氧化碳排放量预测

这里是引用 &#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中…

SpringCloud - Sentinel服务保护

前言 该博客为Sentinel学习笔记&#xff0c;主要目的是为了帮助后期快速复习使用 学习视频&#xff1a;7小快速通关SpringCloud 辅助文档&#xff1a;SpringCloud快速通关 源码地址&#xff1a;cloud-demo 一、简介 官网&#xff1a;https://sentinelguard.io/zh-cn/index.h…

基于 GEE 利用插值方法填补缺失影像

目录 1 完整代码 2 运行结果 利用GEE合成NDVI时&#xff0c;如果研究区较大&#xff0c;一个月的影像覆盖不了整个研究区&#xff0c;就会有缺失的地方&#xff0c;还有就是去云之后&#xff0c;有云量的地区变成空值。 所以今天来用一种插值的方法来填补缺失的影像&#xf…

海云安开发者智能助手(D10)全面接入DeepSeek,赋能开发者安全高效编码新范式

海云安正式宣布完成与DeepSeek&#xff08;深度求索&#xff09;的深度技术融合&#xff0c;旗下核心产品D10开发者智能助手全面接入DeepSeek R1模型。此次合作标志着海云安在"AI驱动开发安全"领域实现重要突破。数据显示&#xff0c;通过DeepSeek R1模型的优化与蒸馏…

Docker 1. 基础使用

1. Docker Docker 是一个 基于容器的虚拟化技术&#xff0c;它能够将应用及其依赖打包成 轻量级、可移植 的容器&#xff0c;并在不同的环境中运行。 2. Docker指令 &#xff08;1&#xff09;查看已有镜像 docker images &#xff08;2&#xff09;删除镜像 docker rmi …

【批量获取图片信息】批量获取图片尺寸、海拔、分辨率、GPS经纬度、面积、位深度、等图片属性里的详细信息,提取出来后导出表格,基于WPF的详细解决方案

摄影工作室通常会有大量的图片素材&#xff0c;在进行图片整理和分类时&#xff0c;需要知道每张图片的尺寸、分辨率、GPS 经纬度&#xff08;如果拍摄时记录了&#xff09;等信息&#xff0c;以便更好地管理图片资源&#xff0c;比如根据图片尺寸和分辨率决定哪些图片适合用于…

如何使用C++将处理后的信号保存为PNG和TIFF格式

在信号处理领域&#xff0c;我们常常需要将处理结果以图像的形式保存下来&#xff0c;方便后续分析和展示。C提供了多种库来处理图像数据&#xff0c;本文将介绍如何使用stb_image_write库保存为PNG格式图像以及使用OpenCV库保存为TIFF格式图像。 1. PNG格式保存 使用stb_ima…

【机器学习】超参数的选择,以kNN算法为例

分类准确度 一、摘要二、超参数的概念三、调参的方法四、实验搜索超参数五、扩展搜索范围六、考虑距离权重的kNN算法七、距离的计算方法及代码实现八、明可夫斯基距离的应用九、网格搜索超参数 一、摘要 本博文讲解了机器学习中的超参数问题&#xff0c;以K近邻算法为例&#…

使用PyCharm进行Django项目开发环境搭建

如果在PyCharm中创建Django项目 1. 打开PyCharm&#xff0c;选择新建项目 2.左侧选择Django&#xff0c;并设置项目名称 3.查看项目解释器初始配置 4.新建应用程序 执行以下操作之一&#xff1a; 转到工具| 运行manage.py任务或按CtrlAltR 在打开的manage.pystartapp控制台…

【python】matplotlib(animation)

文章目录 1、matplotlib.animation1.1、FuncAnimation1.2、修改 matplotlib 背景 2、matplotlib imageio2.1、折线图2.2、条形图2.3、散点图 3、参考 1、matplotlib.animation 1.1、FuncAnimation matplotlib.animation.FuncAnimation 是 Matplotlib 库中用于创建动画的一个…

IntelliJ IDEA使用经验(十三):使用Git克隆github的开源项目

文章目录 问题背景办法1、设置git代理&#xff1b;2、再次克隆项目&#xff1b;3、再次按常规方式进行git克隆即可。 问题背景 由于github在国外&#xff0c;很多时候我们在使用idea克隆开源项目的时候&#xff0c;没办法检出&#xff0c;提示 连接重置。 办法 1、设置git代…

人工智能学习(七)之神经网络

目录 一、引言 二、经典神经网络回顾 &#xff08;一&#xff09;结构与计算过程 &#xff08;二&#xff09;局限性 三、循环神经网络&#xff08;RNN&#xff09;原理 &#xff08;一&#xff09;基本结构 &#xff08;二&#xff09;计算过程 &#xff08;三&#xf…

IDEA编写SpringBoot项目时使用Lombok报错“找不到符号”的原因和解决

目录 概述|背景 报错解析 解决方法 IDEA配置解决 Pom配置插件解决 概述|背景 报错发生背景&#xff1a;在SpringBoot项目中引入Lombok依赖并使用后出现"找不到符号"的问题。 本文讨论在上述背景下发生的报错原因和解决办法&#xff0c;如果仅为了解决BUG不论原…

使用golang wails写了一个桌面端小工具:WoWEB, 管理本地多前端项目

WoWEB 本地快速启动 http 服务。 辅助管理本地前端项目。 使用界面配置代理转发。 支持平台 windows 10macOS 功能描述 管理本地前端项目启动本地 HTTP 服务&#xff0c;可本地或者局域网访问快速打开项目文件夹配置 HTTP 代理转发规则&#xff0c;方便开发调试 以下情况…

Unity Dots理论学习-5.与ECS相关的概念

DOTS的面向数据编程方式比你在MonoBehaviour项目中常见的面向对象编程方式更适合硬件开发。可以尝试理解一些与数据导向设计&#xff08;DOD&#xff09;相关的关键概念&#xff0c;以及这些概念如何影响你的代码&#xff0c;对你在MonoBehaviour项目中的C#编程通常是较少涉及的…

【hive】记一次hiveserver内存溢出排查,线程池未正确关闭导致

一、使用 MemoryAnalyzer软件打开hprof文件 很大有30G&#xff0c;win内存24GB&#xff0c;不用担心可以打开&#xff0c;ma软件能够生成索引文件&#xff0c;逐块分析内存&#xff0c;如下图。 大约需要4小时。 overview中开不到具体信息。 二、使用Leak Suspects功能继续…