This is a screenshot tool using Python. However, since ctypes is used to call the Windows API and access the .cur cursor style file in "C:/Windows/Cursors/" in Windows, this tool is only suitable for Windows environments;
If you want to improve its cross-platformity, you need to consider replacing some Windows-specific APIs of ctypes and setting different .cur access methods;
This module only uses PIL, a third-party library. Unlike other modules such as pygame\pyautogui, this tool emphasizes lightweight and does not use any irrelevant or unnecessary libraries.
The module has not been completed yet, and only includes simple functions such as resizing, saving, and turning pages to view different screenshots when taking basic screenshots; it will continue to be improved in the future, but the availability of the current module is sufficient, and the code has been refactored, with a certain amount of readability and scalability.
The complete code is as follows: a single file, install the PIL module by yourself, and run it on a Windows environment.
import ctypes import tkinter as tk from tkinter import messagebox, filedialog from PIL import ImageGrab, ImageTk ScaleFactor = (0) (1) class FlatButton(): def __init__( self, parent, command, enter_fg="#000000", click_color="#25C253", *args, **kwargs ): super().__init__(parent, *args, **kwargs) self.__fg = fg = ("fg", "#474747") self.__enter_fg = enter_fg self.__click_fg = click_color = command (cursor="hand2") ("<Enter>", lambda _: (fg=enter_fg)) ("<Leave>", lambda _: (fg=fg)) ("<Button-1>", lambda _: (fg=click_color)) ("<ButtonRelease-1>", self.__command) if fg == enter_fg: raise ValueError("enter_fg must be different from fg") def __command(self, event): try: if ("fg") in (self.__enter_fg, self.__click_fg): (event) (fg=self.__fg) except : # Button have been destroy. pass 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): : = parent : ScreenshotUtils = screenshot self.__rect: int = 0 self.__anchors: list[int] = [] self.anchor_id: int = 0 def rect_coords(self) -> tuple[int, int, int, int]: try: return (self.__rect) except : return [ min(.start_x, .end_x), min(.start_y, .end_y), max(.start_x, .end_x), max(.start_y, .end_y) ] 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 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 # Be sure to pay attention to this judgment order, which is closely related to the subsequent 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(, pos[0]) and near(, pos[1]): return index if left < < right and top < < bottom: return 8 return -1 def create_anchors(self): cls = self.__class__ for coord in self.anchor_coords(): anchor = .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 move_anchors(self): cls = self.__class__ for anchor, coord in zip(self.__anchors, self.anchor_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): .start_x = .start_y = self.__rect= .create_rectangle(.start_x, .start_y, .start_x, .start_y, outline='#1AAE1A', width=2) self.create_anchors() def on_release(self, _): .start_x, .start_y,\ .end_x, .end_y = self.rect_coords() def on_hover(self, event): self.anchor_id = self.get_anchor(event) cursor = [self.anchor_id] (cursor=cursor) def move_rect(self, event): offset_x = - .move_start_x offset_y = - .move_start_y .start_x += offset_x .start_y += offset_y .end_x += offset_x .end_y += offset_y .move_start_x = .move_start_y = (self.__rect, .start_x, .start_y, .end_x, .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: .end_x, .end_y = , elif self.anchor_id == 1: .start_x, .start_y = , elif self.anchor_id == 2: .end_x, .start_y = , elif self.anchor_id == 3: .start_x, .end_y = , elif self.anchor_id == 4: .start_y = elif self.anchor_id == 5: .end_y = elif self.anchor_id == 6: .start_x = elif self.anchor_id == 7: .end_x = else: return (self.__rect, .start_x, .start_y, .end_x, .end_y) self.move_anchors() class ScreenshotUtils(object): """ The key to screenshots is coordinates; this class manages the references and screenshot coordinates of images; """ 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 AJUST_BAR_WIDTH: int = TkS(100) # Screenshot related variables 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.current_image = None self.pixel_reader = None self.final_images = list() # This is a control that only moves but does not change the size and content, just moves without repainting self.screenshot_move_widget = list() # This is a control that moves and changes the size and needs to be repainted in real time 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]]: # Return coordinates in the order of main frame, horizontal lines, and vertical lines magnifier_x = x+cls.MAGNIFIER_OFFSET magnifier_y = y+cls.MAGNIFIER_OFFSET main_frame_coord = ( magnifier_x, magnifier_y, magnifier_x+cls.ZOOM_SCREEN_SIZE, magnifier_y+cls.ZOOM_SCREEN_SIZE ) horrontal_line_coord = ( magnifier_x, magnifier_y+cls.ZOOM_SCREEN_SIZE // 2, magnifier_x+cls.ZOOM_SCREEN_SIZE, magnifier_y+cls.ZOOM_SCREEN_SIZE // 2 ) vertical_line_coord = ( magnifier_x+cls.ZOOM_SCREEN_SIZE // 2, magnifier_y, magnifier_x+cls.ZOOM_SCREEN_SIZE // 2, magnifier_y+cls.ZOOM_SCREEN_SIZE ) coords = [main_frame_coord, horrontal_line_coord, vertical_line_coord] return coords def redraw_widget_coords(self, x, y) -> list[tuple]: # Return coordinates in the order of "Length × Width", "Magnifying Glass Image", "POS Tag", and "RGB Tag" offset = self.__class__.MAGNIFIER_OFFSET width_height_info = (min(self.start_x, self.end_x), min(self.start_y, self.end_y) - 40) magnifier_coord = (x + offset, y + offset) pos_info = (x + offset, y + offset+self.__class__.ZOOM_SCREEN_SIZE + 10) rgb_info = (x + offset, y + offset+self.__class__.ZOOM_SCREEN_SIZE + 47) coords = [width_height_info, magnifier_coord, pos_info, rgb_info] return coords class MainUI(): def __init__(self): super().__init__() = ScreenshotUtils() self.set_window() self.menu_bar: = self.set_menubar() self.cut_btn: = self.set_cut_btn() self.save_btn: = self.set_save_btn() self.turn_left_btn: = self.set_turn_left_btn() self.turn_right_btn: = self.set_turn_right_btn() self.show_image_canvas: = self.set_show_image_canvas() self.adjust_rect: AdjustableRect = None self.pos_label: = None self.rgb_label: = None self.adjust_bar: = None def set_window(self): ("Screenshot Tool") (f"{(240)}x{(30)}") def set_menubar(self) -> : menubar = (self, bg="#FFFFFF", height=30) (fill=) return menubar def set_cut_btn(self) -> : btn_cut = FlatButton( self.menu_bar, self.start_capture, text="✂", bg="#FFFFFF", font=("Segoe UI Emoji", 18), ) btn_cut.pack(side=, padx=5) return btn_cut def set_save_btn(self) -> : btn_save = FlatButton( self.menu_bar, self.save_image, text="💾", bg="#FFFFFF", font=("Segoe UI Emoji", 18), ) btn_save.pack(side=, padx=5) return btn_save def set_turn_left_btn(self) -> : turn_left_btn = FlatButton( self.menu_bar, self.turn_page, text="\u25C0", bg="#FFFFFF", font=("Segoe UI Emoji", 18), ) turn_left_btn.pack(side=, padx=5) return turn_left_btn def set_turn_right_btn(self) -> : turn_page_btn = FlatButton( self.menu_bar, self.turn_page, text="\u25B6", bg="#FFFFFF", font=("Segoe UI Emoji", 18), ) turn_page_btn.pack(side=, padx=5) return turn_page_btn def set_cancel_btn(self, parent) -> : cancel_btn = FlatButton( parent, self.clear_capture_info, text="×", bg="#FFFFFF", enter_fg="#DB1A21",fg="#CC181F", font=("Microsoft Yahei", 18) ) cancel_btn.pack(side=, padx=5) return cancel_btn def set_confirm_btn(self, parent) -> : confirm_btn = FlatButton( parent, self.confirm_capture, fg="#23B34C", text="√", enter_fg="#27C956", bg="#FFFFFF", font=("Microsoft Yahei", 18) ) return confirm_btn def set_show_image_canvas(self) -> : canvas = (self, bg="white") return canvas def set_adjust_bar(self) -> : self.adjust_bar = (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=, padx=5) confirm_btn.pack(side=, 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) .screenshot_move_widget = [main_frame_id, horrontal_line, vertical_line] = + .winfo_rootx() = + .winfo_rooty() self.update_magnifier(event) def set_full_screenshot_canvas(self, parent) -> : img = () .current_image = img .pixel_reader = ("RGB") photo = (img) full_screenshot_canvas = (parent, bg="white") full_screenshot_canvas.create_image(0, 0, anchor=, image=photo) full_screenshot_canvas.image = photo full_screenshot_canvas.pack(fill=, expand=True) return full_screenshot_canvas def set_pos_rgb_label(self, parent) -> : label = (parent, bg="#000000", font=("Microsoft Yahei", 8), fg="#ffffff") return label class ScreenshotTool(MainUI): def __init__(self): super().__init__() self.page_index = 0 def start_capture(self, event): ('-alpha', 0) () self.capture_win = () self.capture_win.geometry(f"{self.winfo_screenwidth()}x{self.winfo_screenheight()}+0+0") self.capture_win.overrideredirect(True) self.full_screenshot_canvas = self.set_full_screenshot_canvas(self.capture_win) self.pos_label = self.set_pos_rgb_label(self.full_screenshot_canvas) self.rgb_label = self.set_pos_rgb_label(self.full_screenshot_canvas) self.adjust_rect = AdjustableRect(self.full_screenshot_canvas, ) self.set_magnifier_frame(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): 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): self.adjust_rect.rect_adjust(event) self.update_magnifier(event) def on_release(self, event, resize=True): self.unbind_all() self.adjust_rect.on_release(event) self.full_screenshot_canvas.config(cursor=self.adjust_rect.CURSOR_FILES[0]) self.full_screenshot_canvas.bind("<Button-1>", self.start_move) self.full_screenshot_canvas.bind("<Motion>", self.adjust_rect.on_hover) self.adjust_bar.place(x=.end_x - 300, y=.end_y + 10, width=300) if resize: .end_x, .end_y = , def unbind_all(self): events = ("<Button-1>", "<Motion>", "<ButtonRelease-1>") for event in events: self.full_screenshot_canvas.unbind(event) def update_magnifier(self, event): x, y = , size = ScreenshotUtils.ZOOM_WIDTH img = .current_image.crop((x - size//2, y - size//2, x + size//2, y + size//2)) img = ((ScreenshotUtils.ZOOM_SCREEN_SIZE, ScreenshotUtils.ZOOM_SCREEN_SIZE)) photo = (img) self.full_screenshot_canvas.image2 = photo w, h = abs(.end_x - .start_x), abs(.end_y - .start_y) for redraw_widget in .screenshot_redraw_widget: self.full_screenshot_canvas.delete(redraw_widget) .screenshot_redraw_widget.clear() redraw_widget_coords = .redraw_widget_coords(x, y) width_height_info, magnifier_coord, pos_info, rgb_info = redraw_widget_coords wh_info_widget = self.full_screenshot_canvas.create_text(*width_height_info, anchor=, fill="white", text=f"{w} × {h}") zoom_img = self.full_screenshot_canvas.create_image(*magnifier_coord, anchor=, image=photo) self.pos_label.config(text=f"POS: ({x}, {y})") self.rgb_label.config(text=f"RGB: {.pixel_reader.getpixel((x, y))}") self.pos_label.place(x=pos_info[0], y=pos_info[1]) self.rgb_label.place(x=rgb_info[0], y=rgb_info[1]) .screenshot_redraw_widget = [wh_info_widget, zoom_img] self.update_magnifier_frame(x, y) def update_magnifier_frame(self, x: int, y: int) -> None: coords = .move_widget_coords(x, y) for widget, coord in zip(.screenshot_move_widget, coords): self.full_screenshot_canvas.coords(widget, *coord) self.full_screenshot_canvas.tag_raise(widget) def start_move(self, event): .move_start_x = .move_start_y = self.adjust_bar.place_forget() self.pos_label.place_forget() self.rgb_label.place_forget() for widget in .screenshot_redraw_widget: self.full_screenshot_canvas.delete(widget) for widget in .screenshot_move_widget: self.full_screenshot_canvas.tag_lower(widget) self.full_screenshot_canvas.bind("<B1-Motion>", self.adjust_rect.rect_adjust) self.full_screenshot_canvas.bind("<ButtonRelease-1>", lambda e: self.on_release(e, False)) def clear_capture_info(self, _): self.capture_win.destroy() self.full_screenshot_canvas.destroy() ('-alpha', 1) .screenshot_move_widget.clear() def confirm_capture(self, event): self.clear_capture_info(event) x1, y1, x2, y2 = self.adjust_rect.rect_coords() image = .current_image.crop((x1, y1, x2, y2)) result = self.show_image(image) .final_images.append(result) self.page_index = len(.final_images) - 1 ('-topmost', 1) def show_image(self, image): (f"{max(, (240))}x{+self.menu_bar.winfo_height()}") photo = (image) self.show_image_canvas.config(width=, height=) self.show_image_canvas.delete("all") self.show_image_canvas.create_image(0, 0, anchor=, image=photo) self.show_image_canvas.image = photo self.show_image_canvas.pack() return image def save_image(self, _): try: image = .final_images[self.page_index] filename = ( defaultextension=".png", filetypes=[("PNG files", "*.png"), ("JPEG files", "*.jpg")], initialfile=f"{}x{}.png" ) if not filename: return (filename) except IndexError: ("Save failed", "No screened image detected") def turn_page(self, event): if len(.final_images) == 0: return ("hint", "There is no picture to switch!") if == self.turn_left_btn: if self.page_index == 0: return ("hint", "It's already the first picture!") self.page_index -= 1 else: if self.page_index == len(.final_images) - 1: return ("hint", "It's the last picture!") self.page_index += 1 self.show_image(.final_images[self.page_index]) if __name__ == "__main__": app = ScreenshotTool() ()
This is the end of this article about using Python to write screenshot lightweight tools. For more related Python screenshot content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!