SoFunction
Updated on 2025-04-06

Write screenshots in Python lightweight tool

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!