Python|GIF 解析与构建(8):可视化录制 GIF(完结)

目录

Python|GIF 解析与构建(8):可视化录制 GIF(完结)

功能概述

核心模块解析

GIF 文件构建模块

帧率控制模块

屏幕捕获模块

主应用模块

关键技术细节

GIF 文件格式解析

颜色转换与量化

LZW 压缩算法

使用方法

总结与优化方向


录制过程

Python|GIF 解析与构建(8):可视化录制 GIF(完结)

在 GIF 技术探索的最后一篇文章中,我们将实现一个完整的可视化 GIF 录制工具。这个工具能够捕获屏幕指定区域的画面,并将其转换为 GIF 动画,让我们能够轻松记录屏幕操作、生成演示动画或分享有趣的画面。完结撒花!!!!

功能概述

我们的可视化 GIF 录制工具具有以下核心功能:

  • 自定义录制区域:可自由设置录制的宽度、高度和屏幕坐标
  • 帧率控制:支持调整录制帧率和总帧数
  • 实时预览:通过可视化界面实时查看录制区域
  • 屏幕捕获:高效获取屏幕指定区域的像素数据
  • GIF 生成:将捕获的帧数据转换为标准 GIF 格式并保存

核心模块解析

整个录制工具由几个关键模块组成,每个模块负责特定的功能,下面我们来逐一解析。

GIF 文件构建模块

GIFGenerate类是 GIF 文件构建的核心,它实现了 GIF 文件格式的各个部分:

class GIFGenerate():
    def __init__(self):
        # 初始化GIF文件头、颜色表、扩展块等基本数据结构
        self.fps = 1
        self.gif_data = []
        self.screen_width = int(100)
        self.screen_height = int(100)
        # ... 其他初始化代码 ...

这个类处理 GIF 文件的各个组成部分:

  • 文件头部分:包含 "GIF89a" 标识和屏幕尺寸信息
  • 全局颜色表:将 RGB 颜色转换为 GIF 支持的索引颜色
  • 应用程序扩展块:处理 GIF 循环播放设置
  • 图形控制扩展块:控制帧之间的延迟时间
  • 图像数据块:包含每一帧的图像数据和压缩信息

帧率控制模块

control_frame类负责控制录制帧率,确保捕获的帧速率稳定:

class control_frame():
    def __init__(self):
        self.start_time = float()
        self.fps = int(10)
        self.time_one_frame = 1 / self.fps
        self.fps_count = 0
        self.time_all = time.time()
    
    def start(self):
        self.start_time = time.time()
        self.fps_count += 1
    
    def spend(self):
        return time.time() - self.start_time
    
    def wait(self):
        spend = self.spend()
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)

这个模块通过计算和控制每帧的捕获时间间隔,确保录制的 GIF 动画播放流畅,不会出现帧率忽快忽慢的情况。

屏幕捕获模块

ScreenshotData类使用 Windows API 实现屏幕指定区域的捕获:

class ScreenshotData():
    def __init__(self):
        self.gdi32 = ctypes.windll.gdi32
        self.user32 = ctypes.windll.user32
        # 获取屏幕尺寸和缩放比例
        # ... 初始化代码 ...
    
    def capture_screen(self, x, y, width, height):
        # 获取桌面窗口句柄和设备上下文
        hwnd = self.user32.GetDesktopWindow()
        hdc_src = self.user32.GetDC(hwnd)
        
        # 创建兼容的内存设备上下文和位图
        hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)
        bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)
        old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)
        
        # 复制屏幕内容到位图
        SRCCOPY = 0x00CC0020
        self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
        
        # 获取像素数据
        # ... 处理像素数据代码 ...
        
        # 清理资源
        self.gdi32.SelectObject(hdc_dest, old_bmp)
        self.gdi32.DeleteDC(hdc_dest)
        self.user32.ReleaseDC(hwnd, hdc_src)
        self.gdi32.DeleteObject(bmp)
        
        return pixel_data

这个模块使用 Windows 的 GDI API 实现高效的屏幕捕获,能够获取指定区域的像素数据,为 GIF 录制提供原始图像数据。

主应用模块

GIFALL类整合了上述所有模块,提供可视化界面:

class GIFALL():
    def __init__(self, root):
        self.root = root
        self.root.title("gif录制")
        self.root.geometry("500x250")
        self.root.attributes('-topmost', True)
        
        # 初始化录制参数
        self.width = 100
        self.height = 100
        self.x_axis = 0
        self.y_axis = 0
        self.fps_choose = 10
        self.frame_total = 100
        self.recording = False
        self.gif_data = []
        
        # 创建界面元素
        self.create_viewfinder()
        self.create_control_panel()
        
        # 绑定窗口事件
        self.root.bind("", self.start_move)
        self.root.bind("", self.stop_move)
        self.root.bind("", self.on_move)
        
        # 启动坐标更新
        self.update_coordinates()

这个类创建了一个 Tkinter 界面,包含:

  • 左侧取景区域:显示录制范围
  • 右侧控制面板:包含尺寸、帧率设置和控制按钮
  • 窗口拖动功能:方便用户移动录制窗口

关键技术细节

GIF 文件格式解析

GIF 文件采用二进制格式,主要包含以下几个部分:

  1. 文件头:"GIF89a" 标识和屏幕尺寸
  2. 全局颜色表:存储图像中使用的颜色
  3. 扩展块:包含应用程序信息和图形控制信息
  4. 图像数据块:每一帧的图像数据
  5. 结束标识:0x3B

GIFGenerate类中,我们通过字节操作构建了这些部分,特别注意了字节顺序和格式规范。

颜色转换与量化

GIF 使用索引颜色模式,最多支持 256 种颜色。我们的程序实现了颜色量化功能,将 24 位 RGB 颜色转换为 GIF 支持的索引颜色:

def rgb_deal(self, data):
    r = data[2] >> 5  # 3 bits (0-7)
    g = data[1] >> 5  # 3 bits (0-7)
    b = data[0] >> 6  # 2 bits (0-3)
    return (r << 5) | (g << 2) | b

这段代码将 24 位 RGB 颜色转换为 8 位索引颜色,通过位运算减少颜色数量,同时保持视觉上的相似性。

LZW 压缩算法

GIF 使用 LZW (Lempel-Ziv-Welch) 压缩算法压缩图像数据。我们在lzw_compress方法中实现了这一算法:

def lzw_compress(self, data):
    dictionary = {bytes([i]): i for i in range(256)}
    next_code = 258  # 256/257 保留
    w = bytes()
    buffer = 0
    bits = 0
    compressed = bytearray()
    code_size = 9  # 初始位宽 9 位
    
    for byte in data:
        c = bytes([byte])
        wc = w + c
        if wc in dictionary:
            w = wc
        else:
            # 写入当前w的编码
            buffer |= dictionary[w] << bits
            bits += code_size
            
            # 按字节刷新缓冲区
            while bits >= 8:
                compressed.append(buffer & 0xFF)
                buffer >>= 8
                bits -= 8
            
            # 添加新条目到字典
            if next_code < 4096:
                dictionary[wc] = next_code
                next_code += 1
                
                # 动态增加位宽
                if next_code > (1 << code_size) and code_size < 12:
                    code_size += 1
            w = c
    
    # 处理最后残留的w
    if w:
        buffer |= dictionary[w] << bits
        bits += code_size
    
    # 写入结束标记
    buffer |= 257 << bits
    bits += code_size
    
    # 清空缓冲区
    while bits > 0:
        compressed.append(buffer & 0xFF)
        buffer >>= 8
        bits -= 8 if bits >= 8 else bits
    
    return bytes(compressed)

LZW 算法通过构建字典来压缩重复出现的数据模式,特别适合 GIF 这种包含大面积单一颜色的图像格式。

使用方法

使用这个 GIF 录制工具非常简单:

  1. 运行程序后,会看到一个带有取景框的窗口
  2. 在右侧控制面板中可以设置:
    • 录制区域的宽度和高度
    • 录制区域在屏幕上的 x 轴和 y 轴坐标
    • 录制帧率和总帧数
  3. 点击 "开始录制" 按钮开始捕获屏幕
  4. 录制完成后点击 "停止录制" 按钮
  5. 选择保存位置和文件名,点击保存

总结与优化方向

通过这个项目,我们实现了从屏幕捕获到 GIF 生成的完整流程,深入理解了 GIF 文件格式和相关算法。这个工具已经可以满足基本的 GIF 录制需求,但还有一些可以优化的方向:

  1. 支持更多平台:当前版本仅支持 Windows 系统,可以考虑使用跨平台的屏幕捕获方案
  2. 提高性能:对于高分辨率和高帧率的录制,性能可能成为瓶颈,可以考虑使用更高效的图像捕获和处理方法
  3. 添加更多功能:如添加水印、实时标注、音频录制等
  4. 优化用户界面:提供更友好的用户体验和更丰富的可视化反馈

完结撒花!!!!

以下是全部代码:

import time
import ctypes
import tkinter as tk
from tkinter import filedialog
from datetime import datetime

class GIFGenerate():
    def __init__(self):
        # 提供数据
        # 帧率
        self.fps = 1
        self.gif_data = []  # gif数据对象
        self.screen_width = int(100)  # 宽度
        self.screen_height = int(100)  # 长度
        self.data = []

        # 基础数据 顺序安排
        # 开头 宽 长 标志参数(计算) 背景颜色索引 宽高比
        self.header = 0
        self.header_sign = 0
        """
        m 占位 7 → 需左移 7 位(m << 7)全局颜色表;
        cr 占位 6-4 → 需左移 4 位(cr << 4)颜色深度;
        s 占位 3 → 需左移 3 位(s << 3)分类标志;
        pixel 占位 2-0 → 无需位移(直接取 pixel)
        """
        self.header_transparent_color = int(0).to_bytes(1, byteorder="little")
        self.header_ratio = int(0).to_bytes(1, byteorder="little")
        # 全局颜色表 颜色提取(计算)
        self.global_color_table = b""

        # 拓展块 应用程序扩展 块长度 扩展应用标记符 子块长度 循环类型 循环参数
        self.application_sign = b"\x21" + b"\xFF"
        self.application_block_number = int(11).to_bytes(1, byteorder="little")
        self.application_marker = b"NETSCAPE2.0"
        self.application_sub_block_number = int(3).to_bytes(1, byteorder="little")
        self.application_loop_kind = int(1).to_bytes(1, byteorder="little")
        self.application_loop_parameters = int(0).to_bytes(2, byteorder="little")
        """
        0x00 00 表示 无限循环。
        若为 0x01 00,则表示循环 1 次(播放 2 次后停止)。
        若为 0x05 00,则表示循环 5 次(播放 6 次后停止)。
        """
        self.application_sign_end = b"\x00"

        # 拓展块 图形控制扩展 子块长度 标志参数 延迟时间(计算) 透明色索引
        self.control_sign = b"\x21" + b"\xF9"
        self.control_block_number = int(4).to_bytes(1, byteorder="little")
        self.control_parameters = int(0).to_bytes(1, byteorder="little")
        """
        - 第 7 位(最高位):透明度标志(0 = 关闭)
        - 第 6 位:用户输入标志(0 = 无需等待输入)
        - 第 3-5 位: disposal 方法(0 = 不指定,默认保留)
        - 第 0-2 位:保留位(0)
        """
        self.control_time = 0
        self.control_transparent_color = int(0).to_bytes(1, byteorder="little")
        self.control_sign_end = b"\x00"

        # 图像标识符 x偏移 y偏移 宽度 长度 标志参数
        self.image_sign = b"\x2C"
        self.image_offset_x = int(0).to_bytes(2, byteorder="little")
        self.image_offset_y = int(0).to_bytes(2, byteorder="little")
        self.image_width = 0
        self.image_length = 0
        self.image_sign_parameters = int(0).to_bytes(1, byteorder="little")
        # 局部颜色表 局部颜色(不处理)

        # 图像内容 压缩位数 子块长度
        self.image_compress = int(8).to_bytes(1, byteorder="little")
        self.image_sub_block_length = b""
        self.image_content = b""
        # 长度 数据 长度 数据
        self.image_sign_end = b"\x00"

        self.batch_color = b""

        # 结束标识符
        self.end_sign = b"\x3B"

    def start(self):
        self.header = (b"GIF89a" +
                       int(self.screen_width).to_bytes(2, byteorder="little") +
                       int(self.screen_height).to_bytes(2, byteorder="little"))
        self.image_width = int(self.screen_width).to_bytes(2, byteorder="little")
        self.image_length = int(self.screen_height).to_bytes(2, byteorder="little")
        self.control_time = int(100 / self.fps).to_bytes(2, byteorder="little")

    # 处理rgb
    def rgb_deal(self, data):
        r = data[2] >> 5  # 3 bits (0-7)
        g = data[1] >> 5  # 3 bits (0-7)
        b = data[0] >> 6  # 2 bits (0-3)
        return (r << 5) | (g << 2) | b

    # 计算
    def conversion(self):
        for one in self.gif_data:
            list_one = [one[i:i + 3] for i in range(0, len(one), 3)]
            self.data.append(list_one)
        # 展开
        list_color = [k for i in self.data for k in i]  # 二维展开 实际上可以算作一维
        # 处理成1字节颜色
        list_deal = [self.rgb_deal(bytes(i)) for i in list_color]
        # 去重
        list_set = set(list_deal)
        # 生成字典转换
        hash_map = {key: (0, 0, 0) for key in list_set}
        # 全部单个遍历映射字典
        for s in list_set:
            hash_map[s] = list_color[list_deal.index(s)]  # 字典赋值

        key_mapping = {key: i for i, key in enumerate(hash_map.keys())}
        print(key_mapping)
        # 获取全局颜色表
        color_data = list(hash_map.values())
        color_number = len(color_data)
        color_number_change = color_number
        # 强制色彩数量
        if color_number <= 4:
            color_number_change = 4

        binary_number = (color_number_change - 1).bit_length()
        # 填充色数量
        fill_color_number = 2 ** binary_number - color_number
        print(fill_color_number)

        # 计算数量
        self.header_sign = 0
        self.header_sign |= (1 << 7)
        self.header_sign |= (3 << 4)
        self.header_sign |= (0 << 3)
        self.header_sign |= (binary_number - 1 << 0)  # 需处理
        self.header_sign = int(self.header_sign).to_bytes(1, byteorder="little")
        print("全局颜色数量:", 2 ** ((self.header_sign[0] & 0b00000111) + 1))

        def bgr_to_rgb(bgr_list):
            rgb_list = []
            for bgr in bgr_list:
                # 提取BGR值并转换为RGB
                rgb = [bgr[2], bgr[1], bgr[0]]
                rgb_list.append(rgb)
            return rgb_list

        color_data = bgr_to_rgb(color_data)
        # 展平元组并转换为字节
        color_data_list = [value for tup in color_data for value in tup] + fill_color_number * [0, 0, 0]  # 填充色
        print(color_data_list)
        self.global_color_table = bytes(color_data_list)
        print(self.global_color_table)

        # 按照长宽转换回去 这里是序列号
        serial_number = [key_mapping.get(x, x) for x in list_deal]
        print(serial_number)
        reshaped = []
        group_size = int(self.screen_width) * int(self.screen_height)
        for i in range(0, len(serial_number), group_size):
            reshaped.append(serial_number[i:i + group_size])
        self.data = reshaped
        print(self.data)
        pass

    # 批量图像颜色数据
    def batch_color_deal(self):
        self.batch_color = b""
        # 将所有序列进行分批处理 然后拼合在一起
        # 对象数量遍历处理
        for one in range(len(self.gif_data)):
            # 将其转换成字节
            data = bytes([i for i in self.data[one]])
            print(data)
            data_deal = self.lzw_compress(data)
            print("压缩码:", data_deal)

            # 处理好的数据需要分解开来,因为长度是1字节需要处理 长度 数据 长度 数据
            # 因为 0x00 是结束符号 所以最大应该是255
            def deal_Byte(byte):
                byte_deal = b""
                length = len(byte)
                max_length = length // 255
                remaining = length % 255 > 0
                start = 0

                for i in range(max_length):
                    max_length_sign = b"\xFF"
                    byte_deal += max_length_sign + byte[start:start + 255]
                    start += 255
                if remaining:
                    sign = int(length % 255).to_bytes(1, byteorder="little")
                    byte_deal += sign + byte[start:]

                return bytes(byte_deal)

            # 核心数据
            image_core_data = deal_Byte(data_deal)

            self.image_sign = b"\x2C"
            self.image_offset_x = int(0).to_bytes(2, byteorder="little")
            self.image_offset_y = int(0).to_bytes(2, byteorder="little")
            self.image_width = int(self.screen_width).to_bytes(2, byteorder="little")
            self.image_length = int(self.screen_height).to_bytes(2, byteorder="little")
            self.image_sign_parameters = int(0).to_bytes(1, byteorder="little")
            # 局部颜色表 局部颜色(不处理)
            # 图像内容 压缩位数 子块长度
            self.image_compress = int(8).to_bytes(1, byteorder="little")
            self.image_sub_block_length = b""
            self.image_content = b""
            # 长度 数据 长度 数据
            self.image_sign_end = b"\x00"

            # 合成批量
            self.batch_color += (self.image_sign +
                                 self.image_offset_x +
                                 self.image_offset_y +
                                 self.image_width +
                                 self.image_length +
                                 self.image_sign_parameters +
                                 self.image_compress +
                                 image_core_data +
                                 self.image_sign_end)
        pass

    # 保存
    def gif_save(self, path):
        # 开头数据
        gif_data = self.header + self.header_sign + self.header_transparent_color + self.header_ratio + self.global_color_table
        # 应用程序扩展
        gif_data += self.application_sign + self.application_block_number + self.application_marker + self.application_sub_block_number + self.application_loop_kind + self.application_loop_parameters + self.application_sign_end
        # 图形控制扩展
        gif_data += self.control_sign + self.control_block_number + self.control_parameters + self.control_time + self.control_transparent_color + self.control_sign_end
        # 图像数据
        gif_data += self.batch_color
        # 结束
        gif_data += self.end_sign
        print(path)
        with open(path, "wb") as f:
            f.write(gif_data)
        print("保存成功")
        pass

    # 压缩
    def lzw_compress(self, data):
        dictionary = {bytes([i]): i for i in range(256)}
        next_code = 258  # 256/257 保留
        w = bytes()
        buffer = 0
        bits = 0
        compressed = bytearray()
        code_size = 9  # 初始位宽 9 位

        for byte in data:
            c = bytes([byte])
            wc = w + c
            if wc in dictionary:
                w = wc
            else:
                # 写入当前 w 的编码
                buffer |= dictionary[w] << bits
                bits += code_size

                # 按字节刷新缓冲区
                while bits >= 8:
                    compressed.append(buffer & 0xFF)
                    buffer >>= 8
                    bits -= 8

                # 添加新条目
                if next_code < 4096:  # 限制最大字典大小
                    dictionary[wc] = next_code
                    next_code += 1

                    # 动态增加位宽 (9->10->11->12)
                    if next_code > (1 << code_size) and code_size < 12:
                        code_size += 1
                w = c

        # 处理最后残留的 w
        if w:
            buffer |= dictionary[w] << bits
            bits += code_size

        # 写入结束标记 (257)
        buffer |= 257 << bits
        bits += code_size

        # 清空缓冲区
        while bits > 0:
            compressed.append(buffer & 0xFF)
            buffer >>= 8
            bits -= 8 if bits >= 8 else bits

        return bytes(compressed)


# 控制帧率
class control_frame():
    def __init__(self):
        self.start_time = float()  # 每次启动时间
        self.fps = int(10)  # fps
        self.time_one_frame = 1 / self.fps  # 补正时间
        self.fps_count = 0  # 总帧率
        self.time_all = time.time()  # 启动时间

    # 启动
    def start(self):
        self.start_time = time.time()
        self.fps_count += 1

    # 花销
    def spend(self):
        spend = time.time() - self.start_time
        return spend

    # 等待
    def wait(self):
        spend = self.spend()
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)


# 获取屏幕数据
class ScreenshotData():
    def __init__(self):
        self.gdi32 = ctypes.windll.gdi32
        self.user32 = ctypes.windll.user32

        # 定义常量
        SM_CXSCREEN = 0
        SM_CYSCREEN = 1
        # 缩放比例
        zoom = 1
        hdc = self.user32.GetDC(None)
        try:
            dpi = self.gdi32.GetDeviceCaps(hdc, 88)
            zoom = dpi / 96.0
        finally:
            self.user32.ReleaseDC(None, hdc)
        self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
        self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)

    # 屏幕截取
    def capture_screen(self, x, y, width, height):
        # 获取桌面窗口句柄
        hwnd = self.user32.GetDesktopWindow()
        # 获取桌面窗口的设备上下文
        hdc_src = self.user32.GetDC(hwnd)

        if len(str(hdc_src)) > 16:
            return 0

        # 创建一个与屏幕兼容的内存设备上下文
        hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)

        # 创建一个位图
        bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)

        # 将位图选入内存设备上下文
        old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)

        # 定义SRCCOPY常量
        SRCCOPY = 0x00CC0020
        # 捕获屏幕
        self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
        """
        gdi32.BitBlt(hdc_src,  # 目标设备上下文  
                 x_dest,   # 目标矩形左上角的x坐标  
                 y_dest,   # 目标矩形左上角的y坐标  
                 width,    # 宽度  
                 height,   # 高度  
                 hdc_dest, # 源设备上下文  
                 x_src,    # 源矩形左上角的x坐标(通常是0)  
                 y_src,    # 源矩形左上角的y坐标(通常是0)  
                 SRCCOPY)  # 复制选项
        """

        # 定义 RGBQUAD 结构体
        class RGBQUAD(ctypes.Structure):
            _fields_ = [("rgbBlue", ctypes.c_ubyte),
                        ("rgbGreen", ctypes.c_ubyte),
                        ("rgbRed", ctypes.c_ubyte),
                        ("rgbReserved", ctypes.c_ubyte)]

        # 定义 BITMAPINFOHEADER 结构体
        class BITMAPINFOHEADER(ctypes.Structure):
            _fields_ = [("biSize", ctypes.c_uint),
                        ("biWidth", ctypes.c_int),
                        ("biHeight", ctypes.c_int),
                        ("biPlanes", ctypes.c_ushort),
                        ("biBitCount", ctypes.c_ushort),
                        ("biCompression", ctypes.c_uint),
                        ("biSizeImage", ctypes.c_uint),
                        ("biXPelsPerMeter", ctypes.c_int),
                        ("biYPelsPerMeter", ctypes.c_int),
                        ("biClrUsed", ctypes.c_uint),
                        ("biClrImportant", ctypes.c_uint)]

        # 定义 BITMAPINFO 结构体
        class BITMAPINFO(ctypes.Structure):
            _fields_ = [("bmiHeader", BITMAPINFOHEADER),
                        ("bmiColors", RGBQUAD * 3)]  # 只分配了3个RGBQUAD的空间

        BI_RGB = 0
        DIB_RGB_COLORS = 0

        # 分配像素数据缓冲区(这里以24位位图为例,每个像素3字节)
        pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 4

        # 填充 BITMAPINFO 结构体
        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biWidth = width
        bmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图
        bmi.bmiHeader.biPlanes = 1
        bmi.bmiHeader.biBitCount = 24  # 24即3*8   32
        bmi.bmiHeader.biCompression = BI_RGB  # 无压缩

        # 调用 GetDIBits 获取像素数据
        ret = self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), DIB_RGB_COLORS)
        if ret == 0:
            print("GetDIBits failed:", ctypes.WinError())

        # 恢复设备上下文
        self.gdi32.SelectObject(hdc_dest, old_bmp)
        # 删除内存设备上下文
        self.gdi32.DeleteDC(hdc_dest)
        # 释放桌面窗口的设备上下文
        self.user32.ReleaseDC(hwnd, hdc_src)
        # bmp已经被处理,现在删除它
        self.gdi32.DeleteObject(bmp)
        return pixel_data


# GIF录制系统
class GIFALL():
    def __init__(self, root):
        self.root = root
        self.root.title("gif录制")
        self.root.geometry("500x250")
        self.root.attributes('-topmost', True)  # 设置窗口置顶
        # self.root.overrideredirect(True)# 隐藏标题栏

        self.width = 100
        self.height = 100
        self.x_axis = 0
        self.y_axis = 0
        self.fps_choose = 10
        self.frame_total = 100
        self.frame_count = 0
        self.recording = False  # 初始化录制状态

        # 录制数据
        self.gif_data = []

        # 左上右下坐标
        self.topleft_x = 0
        self.topleft_y = 0
        self.bottomright_x = 0
        self.bottomright_y = 0

        # 设置透明背景色
        self.bg_color = '#FFFFF1'
        self.root.config(bg=self.bg_color)
        self.root.wm_attributes('-transparentcolor', self.bg_color)

        # 创建主框架
        self.main_frame = tk.Frame(root, bg='#FFFFF1', bd=0)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)

        # 左侧透明取景区域
        self.left_frame = tk.Frame(self.main_frame, bg='#FFFFF1')
        self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH)

        # 右侧控制面板
        self.right_frame = tk.Frame(self.main_frame, bg='#FFFFF1', width=250)
        self.right_frame.pack(side=tk.RIGHT, fill=tk.Y)
        self.right_frame.pack_propagate(False)

        # 在左侧区域添加取景框
        self.create_viewfinder()

        # 添加右侧控制面板内容
        self.create_control_panel()

        # 添加窗口拖动功能
        self.root.bind("", self.start_move)
        self.root.bind("", self.stop_move)
        self.root.bind("", self.on_move)

        # 启动坐标更新循环
        self.update_coordinates()

    # 录制栏
    def create_viewfinder(self):
        # 创建取景框
        canvas_width = self.width + self.x_axis + 2
        canvas_height = self.height + self.y_axis + 2
        self.canvas = tk.Canvas(
            self.left_frame,
            bg="#FFFFF1",
            width=canvas_width,
            height=canvas_height,
            highlightthickness=0
        )
        self.canvas.pack(padx=0, pady=0)

        # 绘制取景框
        self.viewfinder = self.canvas.create_rectangle(
            self.x_axis, self.y_axis, self.x_axis + self.width + 2, self.y_axis + self.height + 2,
            outline="#00BFFF",
            width=2,
            dash=(5, 20)
        )

    # 操作栏
    def create_control_panel(self):
        # 尺寸信息
        size_frame = tk.Frame(self.right_frame, bg=self.bg_color)
        size_frame.pack(pady=0, padx=5, fill=tk.X)
        self.width_vr = tk.StringVar(value=str(self.width))
        self.height_vr = tk.StringVar(value=str(self.height))
        self.x_axis_vr = tk.StringVar(value=str(self.x_axis))
        self.y_axis_vr = tk.StringVar(value=str(self.y_axis))
        self.fps_vr = tk.StringVar(value=str(self.fps_choose))
        self.frame_vr = tk.StringVar(value=str(self.frame_total))

        # 绑定变量变化事件
        self.width_vr.trace_add("write", self.on_dimension_change)
        self.height_vr.trace_add("write", self.on_dimension_change)
        self.x_axis_vr.trace_add("write", self.on_dimension_change)
        self.y_axis_vr.trace_add("write", self.on_dimension_change)
        self.fps_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件
        self.frame_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件

        # 创建宽度输入框
        tk.Label(size_frame, text="宽度:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.width_vr, width=5).grid(row=0, column=1, padx=5, pady=5)

        # 创建高度输入框
        tk.Label(size_frame, text="高度:").grid(row=0, column=3, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.height_vr, width=5).grid(row=0, column=4, padx=5, pady=5)

        # 创建s轴输入框
        tk.Label(size_frame, text="x轴:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.x_axis_vr, width=5).grid(row=1, column=1, padx=5, pady=5)

        # 创建y轴输入框
        tk.Label(size_frame, text="y轴:").grid(row=1, column=3, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.y_axis_vr, width=5).grid(row=1, column=4, padx=5, pady=5)

        # 创建帧率输入框
        tk.Label(size_frame, text="帧率:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.fps_vr, width=5).grid(row=2, column=1, padx=5, pady=5)

        # 创建总帧率输入框
        tk.Label(size_frame, text="总帧率:").grid(row=2, column=3, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.frame_vr, width=5).grid(row=2, column=4, padx=5, pady=5)

        # 添加坐标显示标签
        self.coord_frame = tk.Frame(self.right_frame, bg=self.bg_color)
        self.coord_frame.pack(pady=5)
        self.topleft_label = tk.Label(
            self.coord_frame,
            text="(0, 0)",
            bg=self.bg_color,
            fg="#FFFFFF",
            font=("Arial", 10)
        )
        self.topleft_label.grid(row=0, column=0)

        self.bottomright_label = tk.Label(
            self.coord_frame,
            text="(0, 0)",
            bg=self.bg_color,
            fg="#FFFFFF",
            font=("Arial", 10)
        )
        self.bottomright_label.grid(row=0, column=1)

        # 控制按钮
        button_frame = tk.Frame(self.right_frame, bg=self.bg_color)
        button_frame.pack(pady=10, padx=5, fill=tk.X)

        self.record_btn = tk.Button(
            button_frame,
            text="开始录制",
            command=self.toggle_recording,
            bg="#E74C3C",
            fg="white",
            font=("Arial", 12, "bold"),
            relief="flat",
            padx=20,
            pady=10,
            width=15
        )
        self.record_btn.pack(pady=10)

        tk.Button(
            button_frame,
            text="退出应用",
            command=self.root.destroy,
            bg="#000011",
            fg="white",
            font=("Arial", 12),
            relief="flat",
            padx=0,
            pady=0
        ).pack(pady=5)

    # 更新尺寸
    def on_dimension_change(self, *args):
        """当尺寸输入框内容变化时更新取景框尺寸"""
        try:
            # 获取新的尺寸值
            new_width = int(self.width_vr.get())
            new_height = int(self.height_vr.get())
            new_x_axis = int(self.x_axis_vr.get())
            new_y_axis = int(self.y_axis_vr.get())

            # 验证尺寸有效性
            if new_width > 0 and new_height > 0:
                # 锁定
                if new_width > 500:
                    new_width = 500
                if new_height > 500:
                    new_height = 500
                # 更新类属性
                self.width = new_width
                self.height = new_height
                # 锁定
                if new_x_axis > 500:
                    new_x_axis = 500
                if new_y_axis > 500:
                    new_y_axis = 500
                if new_x_axis == "":
                    new_x_axis = 0
                # 更新类属性
                self.x_axis = new_x_axis
                self.y_axis = new_y_axis
                # 更新取景框
                self.update_viewfinder()
                # 更新坐标显示
                self.update_coordinates()

        except ValueError:
            # 输入非数字时忽略
            pass

    # 更新重新绘制
    def update_viewfinder(self):
        """更新取景框尺寸"""
        # 重新配置Canvas大小
        self.canvas.config(width=self.width + self.x_axis + 2, height=self.height + self.y_axis + 2)

        # 更新取景框矩形
        self.canvas.coords(self.viewfinder, self.x_axis, self.y_axis, self.width + self.x_axis + 2,
                           self.height + self.y_axis + 2)

        # 强制刷新Canvas
        self.canvas.update_idletasks()

    # 更新坐标
    def update_coordinates(self):
        """更新取景框的坐标显示"""
        titlebar_height = 30
        border_width = 1
        correction_value = titlebar_height + border_width
        correction_left_value = 8

        # 获取窗口在屏幕上的位置
        window_x = self.root.winfo_x() + correction_left_value
        window_y = self.root.winfo_y() + correction_value

        # 计算取景框在屏幕上的绝对坐标
        self.topleft_x = window_x + self.x_axis + 1
        self.topleft_y = window_y + self.y_axis + 1
        self.bottomright_x = self.topleft_x + self.width - 1
        self.bottomright_y = self.topleft_y + self.height - 1

        # 更新坐标标签
        self.topleft_label.config(text=f"({self.topleft_x},{self.topleft_y})")
        self.bottomright_label.config(text=f"({self.bottomright_x},{self.bottomright_y})")

        # 每秒更新一次坐标
        self.root.after(1000, self.update_coordinates)

    # 更新显示
    def on_fps_change(self, *args):
        """当帧率输入框内容变化时更新显示"""
        try:
            new_fps = int(self.fps_vr.get())
            new_frame = int(self.frame_vr.get())
            # 锁定
            if new_fps < 1:
                new_fps = 1
            elif new_fps > 100:
                new_fps = 100
            self.fps_choose = new_fps
            if new_frame < 1:
                new_frame = 1
            self.frame_total = new_frame
        except ValueError:
            # 输入非数字时忽略
            pass

    # 录制
    def toggle_recording(self):
        if not self.recording:
            # 数据重置
            self.gif_data = []
            # 开始录制
            self.recording = True
            self.record_btn.config(text="停止录制", bg="#2ECC71")
            Screenshot = ScreenshotData()
            wait = control_frame()
            # 帧率设置
            wait.fps = self.fps_choose
            self.st = time.time()

            def work():
                wait.start()
                data = Screenshot.capture_screen(self.topleft_x, self.topleft_y,
                                                 self.bottomright_x - self.topleft_x + 1,
                                                 self.bottomright_y - self.topleft_y + 1)
                wait.wait()
                self.gif_data.append(data)
                # print(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)
                self.frame_count += 1
                if self.frame_count == self.frame_total:
                    self.frame_count = 0
                    self.recording = True
                    self.record_btn.config(text="保存", bg="#FFA500")
                    print("耗费时间:", time.time() - self.st)
                    print("秒平均帧:", self.frame_total / (time.time() - self.st))
                    return True
                self.root.after(1, work)

            self.root.after(1, work)
        else:
            # 停止录制
            self.recording = False
            self.record_btn.config(text="开始录制", bg="#E74C3C")

            file_path = filedialog.asksaveasfilename(
                initialfile=datetime.now().strftime("%Y-%m-%d-%H-%M-%S"),
                defaultextension=".gif",
                filetypes=[("GIF files", "*.gif"), ("All files", "*.*")])

            if file_path:
                # 处理数据
                g = GIFGenerate()
                g.fps = self.fps_choose
                g.screen_width = self.width
                g.screen_height = self.height
                g.gif_data = self.gif_data
                g.start()
                g.conversion()  # 计算
                g.batch_color_deal()  # 处理图像
                g.gif_save(file_path)


    # 窗口拖动功能
    def start_move(self, event):
        self.x = event.x
        self.y = event.y

    def stop_move(self, event):
        self.x = None
        self.y = None

    def on_move(self, event):
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.root.winfo_x() + deltax
        y = self.root.winfo_y() + deltay
        self.root.geometry(f"+{x}+{y}")
        # 窗口移动后更新坐标
        self.update_coordinates()


if __name__ == '__main__':
    root = tk.Tk()
    app = GIFALL(root)
    root.mainloop()

import time
import ctypes
import tkinter as tk
from tkinter import filedialog
from datetime import datetime

class GIFGenerate():
    def __init__(self):
        # 提供数据
        # 帧率
        self.fps = 1
        self.gif_data = []  # gif数据对象
        self.screen_width = int(100)  # 宽度
        self.screen_height = int(100)  # 长度
        self.data = []

        # 基础数据 顺序安排
        # 开头 宽 长 标志参数(计算) 背景颜色索引 宽高比
        self.header = 0
        self.header_sign = 0
        """
        m 占位 7 → 需左移 7 位(m << 7)全局颜色表;
        cr 占位 6-4 → 需左移 4 位(cr << 4)颜色深度;
        s 占位 3 → 需左移 3 位(s << 3)分类标志;
        pixel 占位 2-0 → 无需位移(直接取 pixel)
        """
        self.header_transparent_color = int(0).to_bytes(1, byteorder="little")
        self.header_ratio = int(0).to_bytes(1, byteorder="little")
        # 全局颜色表 颜色提取(计算)
        self.global_color_table = b""

        # 拓展块 应用程序扩展 块长度 扩展应用标记符 子块长度 循环类型 循环参数
        self.application_sign = b"\x21" + b"\xFF"
        self.application_block_number = int(11).to_bytes(1, byteorder="little")
        self.application_marker = b"NETSCAPE2.0"
        self.application_sub_block_number = int(3).to_bytes(1, byteorder="little")
        self.application_loop_kind = int(1).to_bytes(1, byteorder="little")
        self.application_loop_parameters = int(0).to_bytes(2, byteorder="little")
        """
        0x00 00 表示 无限循环。
        若为 0x01 00,则表示循环 1 次(播放 2 次后停止)。
        若为 0x05 00,则表示循环 5 次(播放 6 次后停止)。
        """
        self.application_sign_end = b"\x00"

        # 拓展块 图形控制扩展 子块长度 标志参数 延迟时间(计算) 透明色索引
        self.control_sign = b"\x21" + b"\xF9"
        self.control_block_number = int(4).to_bytes(1, byteorder="little")
        self.control_parameters = int(0).to_bytes(1, byteorder="little")
        """
        - 第 7 位(最高位):透明度标志(0 = 关闭)
        - 第 6 位:用户输入标志(0 = 无需等待输入)
        - 第 3-5 位: disposal 方法(0 = 不指定,默认保留)
        - 第 0-2 位:保留位(0)
        """
        self.control_time = 0
        self.control_transparent_color = int(0).to_bytes(1, byteorder="little")
        self.control_sign_end = b"\x00"

        # 图像标识符 x偏移 y偏移 宽度 长度 标志参数
        self.image_sign = b"\x2C"
        self.image_offset_x = int(0).to_bytes(2, byteorder="little")
        self.image_offset_y = int(0).to_bytes(2, byteorder="little")
        self.image_width = 0
        self.image_length = 0
        self.image_sign_parameters = int(0).to_bytes(1, byteorder="little")
        # 局部颜色表 局部颜色(不处理)

        # 图像内容 压缩位数 子块长度
        self.image_compress = int(8).to_bytes(1, byteorder="little")
        self.image_sub_block_length = b""
        self.image_content = b""
        # 长度 数据 长度 数据
        self.image_sign_end = b"\x00"

        self.batch_color = b""

        # 结束标识符
        self.end_sign = b"\x3B"

    def start(self):
        self.header = (b"GIF89a" +
                       int(self.screen_width).to_bytes(2, byteorder="little") +
                       int(self.screen_height).to_bytes(2, byteorder="little"))
        self.image_width = int(self.screen_width).to_bytes(2, byteorder="little")
        self.image_length = int(self.screen_height).to_bytes(2, byteorder="little")
        self.control_time = int(100 / self.fps).to_bytes(2, byteorder="little")

    # 处理rgb
    def rgb_deal(self, data):
        r = data[2] >> 5  # 3 bits (0-7)
        g = data[1] >> 5  # 3 bits (0-7)
        b = data[0] >> 6  # 2 bits (0-3)
        return (r << 5) | (g << 2) | b

    # 计算
    def conversion(self):
        for one in self.gif_data:
            list_one = [one[i:i + 3] for i in range(0, len(one), 3)]
            self.data.append(list_one)
        # 展开
        list_color = [k for i in self.data for k in i]  # 二维展开 实际上可以算作一维
        # 处理成1字节颜色
        list_deal = [self.rgb_deal(bytes(i)) for i in list_color]
        # 去重
        list_set = set(list_deal)
        # 生成字典转换
        hash_map = {key: (0, 0, 0) for key in list_set}
        # 全部单个遍历映射字典
        for s in list_set:
            hash_map[s] = list_color[list_deal.index(s)]  # 字典赋值

        key_mapping = {key: i for i, key in enumerate(hash_map.keys())}
        print(key_mapping)
        # 获取全局颜色表
        color_data = list(hash_map.values())
        color_number = len(color_data)
        color_number_change = color_number
        # 强制色彩数量
        if color_number <= 4:
            color_number_change = 4

        binary_number = (color_number_change - 1).bit_length()
        # 填充色数量
        fill_color_number = 2 ** binary_number - color_number
        print(fill_color_number)

        # 计算数量
        self.header_sign = 0
        self.header_sign |= (1 << 7)
        self.header_sign |= (3 << 4)
        self.header_sign |= (0 << 3)
        self.header_sign |= (binary_number - 1 << 0)  # 需处理
        self.header_sign = int(self.header_sign).to_bytes(1, byteorder="little")
        print("全局颜色数量:", 2 ** ((self.header_sign[0] & 0b00000111) + 1))

        def bgr_to_rgb(bgr_list):
            rgb_list = []
            for bgr in bgr_list:
                # 提取BGR值并转换为RGB
                rgb = [bgr[2], bgr[1], bgr[0]]
                rgb_list.append(rgb)
            return rgb_list

        color_data = bgr_to_rgb(color_data)
        # 展平元组并转换为字节
        color_data_list = [value for tup in color_data for value in tup] + fill_color_number * [0, 0, 0]  # 填充色
        print(color_data_list)
        self.global_color_table = bytes(color_data_list)
        print(self.global_color_table)

        # 按照长宽转换回去 这里是序列号
        serial_number = [key_mapping.get(x, x) for x in list_deal]
        print(serial_number)
        reshaped = []
        group_size = int(self.screen_width) * int(self.screen_height)
        for i in range(0, len(serial_number), group_size):
            reshaped.append(serial_number[i:i + group_size])
        self.data = reshaped
        print(self.data)
        pass

    # 批量图像颜色数据
    def batch_color_deal(self):
        self.batch_color = b""
        # 将所有序列进行分批处理 然后拼合在一起
        # 对象数量遍历处理
        for one in range(len(self.gif_data)):
            # 将其转换成字节
            data = bytes([i for i in self.data[one]])
            print(data)
            data_deal = self.lzw_compress(data)
            print("压缩码:", data_deal)

            # 处理好的数据需要分解开来,因为长度是1字节需要处理 长度 数据 长度 数据
            # 因为 0x00 是结束符号 所以最大应该是255
            def deal_Byte(byte):
                byte_deal = b""
                length = len(byte)
                max_length = length // 255
                remaining = length % 255 > 0
                start = 0

                for i in range(max_length):
                    max_length_sign = b"\xFF"
                    byte_deal += max_length_sign + byte[start:start + 255]
                    start += 255
                if remaining:
                    sign = int(length % 255).to_bytes(1, byteorder="little")
                    byte_deal += sign + byte[start:]

                return bytes(byte_deal)

            # 核心数据
            image_core_data = deal_Byte(data_deal)

            self.image_sign = b"\x2C"
            self.image_offset_x = int(0).to_bytes(2, byteorder="little")
            self.image_offset_y = int(0).to_bytes(2, byteorder="little")
            self.image_width = int(self.screen_width).to_bytes(2, byteorder="little")
            self.image_length = int(self.screen_height).to_bytes(2, byteorder="little")
            self.image_sign_parameters = int(0).to_bytes(1, byteorder="little")
            # 局部颜色表 局部颜色(不处理)
            # 图像内容 压缩位数 子块长度
            self.image_compress = int(8).to_bytes(1, byteorder="little")
            self.image_sub_block_length = b""
            self.image_content = b""
            # 长度 数据 长度 数据
            self.image_sign_end = b"\x00"

            # 合成批量
            self.batch_color += (self.image_sign +
                                 self.image_offset_x +
                                 self.image_offset_y +
                                 self.image_width +
                                 self.image_length +
                                 self.image_sign_parameters +
                                 self.image_compress +
                                 image_core_data +
                                 self.image_sign_end)
        pass

    # 保存
    def gif_save(self, path):
        # 开头数据
        gif_data = self.header + self.header_sign + self.header_transparent_color + self.header_ratio + self.global_color_table
        # 应用程序扩展
        gif_data += self.application_sign + self.application_block_number + self.application_marker + self.application_sub_block_number + self.application_loop_kind + self.application_loop_parameters + self.application_sign_end
        # 图形控制扩展
        gif_data += self.control_sign + self.control_block_number + self.control_parameters + self.control_time + self.control_transparent_color + self.control_sign_end
        # 图像数据
        gif_data += self.batch_color
        # 结束
        gif_data += self.end_sign
        print(path)
        with open(path, "wb") as f:
            f.write(gif_data)
        print("保存成功")
        pass

    # 压缩
    def lzw_compress(self, data):
        dictionary = {bytes([i]): i for i in range(256)}
        next_code = 258  # 256/257 保留
        w = bytes()
        buffer = 0
        bits = 0
        compressed = bytearray()
        code_size = 9  # 初始位宽 9 位

        for byte in data:
            c = bytes([byte])
            wc = w + c
            if wc in dictionary:
                w = wc
            else:
                # 写入当前 w 的编码
                buffer |= dictionary[w] << bits
                bits += code_size

                # 按字节刷新缓冲区
                while bits >= 8:
                    compressed.append(buffer & 0xFF)
                    buffer >>= 8
                    bits -= 8

                # 添加新条目
                if next_code < 4096:  # 限制最大字典大小
                    dictionary[wc] = next_code
                    next_code += 1

                    # 动态增加位宽 (9->10->11->12)
                    if next_code > (1 << code_size) and code_size < 12:
                        code_size += 1
                w = c

        # 处理最后残留的 w
        if w:
            buffer |= dictionary[w] << bits
            bits += code_size

        # 写入结束标记 (257)
        buffer |= 257 << bits
        bits += code_size

        # 清空缓冲区
        while bits > 0:
            compressed.append(buffer & 0xFF)
            buffer >>= 8
            bits -= 8 if bits >= 8 else bits

        return bytes(compressed)


# 控制帧率
class control_frame():
    def __init__(self):
        self.start_time = float()  # 每次启动时间
        self.fps = int(10)  # fps
        self.time_one_frame = 1 / self.fps  # 补正时间
        self.fps_count = 0  # 总帧率
        self.time_all = time.time()  # 启动时间

    # 启动
    def start(self):
        self.start_time = time.time()
        self.fps_count += 1

    # 花销
    def spend(self):
        spend = time.time() - self.start_time
        return spend

    # 等待
    def wait(self):
        spend = self.spend()
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)


# 获取屏幕数据
class ScreenshotData():
    def __init__(self):
        self.gdi32 = ctypes.windll.gdi32
        self.user32 = ctypes.windll.user32

        # 定义常量
        SM_CXSCREEN = 0
        SM_CYSCREEN = 1
        # 缩放比例
        zoom = 1
        hdc = self.user32.GetDC(None)
        try:
            dpi = self.gdi32.GetDeviceCaps(hdc, 88)
            zoom = dpi / 96.0
        finally:
            self.user32.ReleaseDC(None, hdc)
        self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
        self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)

    # 屏幕截取
    def capture_screen(self, x, y, width, height):
        # 获取桌面窗口句柄
        hwnd = self.user32.GetDesktopWindow()
        # 获取桌面窗口的设备上下文
        hdc_src = self.user32.GetDC(hwnd)

        if len(str(hdc_src)) > 16:
            return 0

        # 创建一个与屏幕兼容的内存设备上下文
        hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)

        # 创建一个位图
        bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)

        # 将位图选入内存设备上下文
        old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)

        # 定义SRCCOPY常量
        SRCCOPY = 0x00CC0020
        # 捕获屏幕
        self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
        """
        gdi32.BitBlt(hdc_src,  # 目标设备上下文  
                 x_dest,   # 目标矩形左上角的x坐标  
                 y_dest,   # 目标矩形左上角的y坐标  
                 width,    # 宽度  
                 height,   # 高度  
                 hdc_dest, # 源设备上下文  
                 x_src,    # 源矩形左上角的x坐标(通常是0)  
                 y_src,    # 源矩形左上角的y坐标(通常是0)  
                 SRCCOPY)  # 复制选项
        """

        # 定义 RGBQUAD 结构体
        class RGBQUAD(ctypes.Structure):
            _fields_ = [("rgbBlue", ctypes.c_ubyte),
                        ("rgbGreen", ctypes.c_ubyte),
                        ("rgbRed", ctypes.c_ubyte),
                        ("rgbReserved", ctypes.c_ubyte)]

        # 定义 BITMAPINFOHEADER 结构体
        class BITMAPINFOHEADER(ctypes.Structure):
            _fields_ = [("biSize", ctypes.c_uint),
                        ("biWidth", ctypes.c_int),
                        ("biHeight", ctypes.c_int),
                        ("biPlanes", ctypes.c_ushort),
                        ("biBitCount", ctypes.c_ushort),
                        ("biCompression", ctypes.c_uint),
                        ("biSizeImage", ctypes.c_uint),
                        ("biXPelsPerMeter", ctypes.c_int),
                        ("biYPelsPerMeter", ctypes.c_int),
                        ("biClrUsed", ctypes.c_uint),
                        ("biClrImportant", ctypes.c_uint)]

        # 定义 BITMAPINFO 结构体
        class BITMAPINFO(ctypes.Structure):
            _fields_ = [("bmiHeader", BITMAPINFOHEADER),
                        ("bmiColors", RGBQUAD * 3)]  # 只分配了3个RGBQUAD的空间

        BI_RGB = 0
        DIB_RGB_COLORS = 0

        # 分配像素数据缓冲区(这里以24位位图为例,每个像素3字节)
        pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 4

        # 填充 BITMAPINFO 结构体
        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biWidth = width
        bmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图
        bmi.bmiHeader.biPlanes = 1
        bmi.bmiHeader.biBitCount = 24  # 24即3*8   32
        bmi.bmiHeader.biCompression = BI_RGB  # 无压缩

        # 调用 GetDIBits 获取像素数据
        ret = self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), DIB_RGB_COLORS)
        if ret == 0:
            print("GetDIBits failed:", ctypes.WinError())

        # 恢复设备上下文
        self.gdi32.SelectObject(hdc_dest, old_bmp)
        # 删除内存设备上下文
        self.gdi32.DeleteDC(hdc_dest)
        # 释放桌面窗口的设备上下文
        self.user32.ReleaseDC(hwnd, hdc_src)
        # bmp已经被处理,现在删除它
        self.gdi32.DeleteObject(bmp)
        return pixel_data


# GIF录制系统
class GIFALL():
    def __init__(self, root):
        self.root = root
        self.root.title("gif录制")
        self.root.geometry("500x250")
        self.root.attributes('-topmost', True)  # 设置窗口置顶
        # self.root.overrideredirect(True)# 隐藏标题栏

        self.width = 100
        self.height = 100
        self.x_axis = 0
        self.y_axis = 0
        self.fps_choose = 10
        self.frame_total = 100
        self.frame_count = 0
        self.recording = False  # 初始化录制状态

        # 录制数据
        self.gif_data = []

        # 左上右下坐标
        self.topleft_x = 0
        self.topleft_y = 0
        self.bottomright_x = 0
        self.bottomright_y = 0

        # 设置透明背景色
        self.bg_color = '#FFFFF1'
        self.root.config(bg=self.bg_color)
        self.root.wm_attributes('-transparentcolor', self.bg_color)

        # 创建主框架
        self.main_frame = tk.Frame(root, bg='#FFFFF1', bd=0)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)

        # 左侧透明取景区域
        self.left_frame = tk.Frame(self.main_frame, bg='#FFFFF1')
        self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH)

        # 右侧控制面板
        self.right_frame = tk.Frame(self.main_frame, bg='#FFFFF1', width=250)
        self.right_frame.pack(side=tk.RIGHT, fill=tk.Y)
        self.right_frame.pack_propagate(False)

        # 在左侧区域添加取景框
        self.create_viewfinder()

        # 添加右侧控制面板内容
        self.create_control_panel()

        # 添加窗口拖动功能
        self.root.bind("", self.start_move)
        self.root.bind("", self.stop_move)
        self.root.bind("", self.on_move)

        # 启动坐标更新循环
        self.update_coordinates()

    # 录制栏
    def create_viewfinder(self):
        # 创建取景框
        canvas_width = self.width + self.x_axis + 2
        canvas_height = self.height + self.y_axis + 2
        self.canvas = tk.Canvas(
            self.left_frame,
            bg="#FFFFF1",
            width=canvas_width,
            height=canvas_height,
            highlightthickness=0
        )
        self.canvas.pack(padx=0, pady=0)

        # 绘制取景框
        self.viewfinder = self.canvas.create_rectangle(
            self.x_axis, self.y_axis, self.x_axis + self.width + 2, self.y_axis + self.height + 2,
            outline="#00BFFF",
            width=2,
            dash=(5, 20)
        )

    # 操作栏
    def create_control_panel(self):
        # 尺寸信息
        size_frame = tk.Frame(self.right_frame, bg=self.bg_color)
        size_frame.pack(pady=0, padx=5, fill=tk.X)
        self.width_vr = tk.StringVar(value=str(self.width))
        self.height_vr = tk.StringVar(value=str(self.height))
        self.x_axis_vr = tk.StringVar(value=str(self.x_axis))
        self.y_axis_vr = tk.StringVar(value=str(self.y_axis))
        self.fps_vr = tk.StringVar(value=str(self.fps_choose))
        self.frame_vr = tk.StringVar(value=str(self.frame_total))

        # 绑定变量变化事件
        self.width_vr.trace_add("write", self.on_dimension_change)
        self.height_vr.trace_add("write", self.on_dimension_change)
        self.x_axis_vr.trace_add("write", self.on_dimension_change)
        self.y_axis_vr.trace_add("write", self.on_dimension_change)
        self.fps_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件
        self.frame_vr.trace_add("write", self.on_fps_change)  # 绑定帧率变化事件

        # 创建宽度输入框
        tk.Label(size_frame, text="宽度:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.width_vr, width=5).grid(row=0, column=1, padx=5, pady=5)

        # 创建高度输入框
        tk.Label(size_frame, text="高度:").grid(row=0, column=3, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.height_vr, width=5).grid(row=0, column=4, padx=5, pady=5)

        # 创建s轴输入框
        tk.Label(size_frame, text="x轴:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.x_axis_vr, width=5).grid(row=1, column=1, padx=5, pady=5)

        # 创建y轴输入框
        tk.Label(size_frame, text="y轴:").grid(row=1, column=3, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.y_axis_vr, width=5).grid(row=1, column=4, padx=5, pady=5)

        # 创建帧率输入框
        tk.Label(size_frame, text="帧率:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.fps_vr, width=5).grid(row=2, column=1, padx=5, pady=5)

        # 创建总帧率输入框
        tk.Label(size_frame, text="总帧率:").grid(row=2, column=3, padx=5, pady=5, sticky="w")
        tk.Entry(size_frame, textvariable=self.frame_vr, width=5).grid(row=2, column=4, padx=5, pady=5)

        # 添加坐标显示标签
        self.coord_frame = tk.Frame(self.right_frame, bg=self.bg_color)
        self.coord_frame.pack(pady=5)
        self.topleft_label = tk.Label(
            self.coord_frame,
            text="(0, 0)",
            bg=self.bg_color,
            fg="#FFFFFF",
            font=("Arial", 10)
        )
        self.topleft_label.grid(row=0, column=0)

        self.bottomright_label = tk.Label(
            self.coord_frame,
            text="(0, 0)",
            bg=self.bg_color,
            fg="#FFFFFF",
            font=("Arial", 10)
        )
        self.bottomright_label.grid(row=0, column=1)

        # 控制按钮
        button_frame = tk.Frame(self.right_frame, bg=self.bg_color)
        button_frame.pack(pady=10, padx=5, fill=tk.X)

        self.record_btn = tk.Button(
            button_frame,
            text="开始录制",
            command=self.toggle_recording,
            bg="#E74C3C",
            fg="white",
            font=("Arial", 12, "bold"),
            relief="flat",
            padx=20,
            pady=10,
            width=15
        )
        self.record_btn.pack(pady=10)

        tk.Button(
            button_frame,
            text="退出应用",
            command=self.root.destroy,
            bg="#000011",
            fg="white",
            font=("Arial", 12),
            relief="flat",
            padx=0,
            pady=0
        ).pack(pady=5)

    # 更新尺寸
    def on_dimension_change(self, *args):
        """当尺寸输入框内容变化时更新取景框尺寸"""
        try:
            # 获取新的尺寸值
            new_width = int(self.width_vr.get())
            new_height = int(self.height_vr.get())
            new_x_axis = int(self.x_axis_vr.get())
            new_y_axis = int(self.y_axis_vr.get())

            # 验证尺寸有效性
            if new_width > 0 and new_height > 0:
                # 锁定
                if new_width > 500:
                    new_width = 500
                if new_height > 500:
                    new_height = 500
                # 更新类属性
                self.width = new_width
                self.height = new_height
                # 锁定
                if new_x_axis > 500:
                    new_x_axis = 500
                if new_y_axis > 500:
                    new_y_axis = 500
                if new_x_axis == "":
                    new_x_axis = 0
                # 更新类属性
                self.x_axis = new_x_axis
                self.y_axis = new_y_axis
                # 更新取景框
                self.update_viewfinder()
                # 更新坐标显示
                self.update_coordinates()

        except ValueError:
            # 输入非数字时忽略
            pass

    # 更新重新绘制
    def update_viewfinder(self):
        """更新取景框尺寸"""
        # 重新配置Canvas大小
        self.canvas.config(width=self.width + self.x_axis + 2, height=self.height + self.y_axis + 2)

        # 更新取景框矩形
        self.canvas.coords(self.viewfinder, self.x_axis, self.y_axis, self.width + self.x_axis + 2,
                           self.height + self.y_axis + 2)

        # 强制刷新Canvas
        self.canvas.update_idletasks()

    # 更新坐标
    def update_coordinates(self):
        """更新取景框的坐标显示"""
        titlebar_height = 30
        border_width = 1
        correction_value = titlebar_height + border_width
        correction_left_value = 8

        # 获取窗口在屏幕上的位置
        window_x = self.root.winfo_x() + correction_left_value
        window_y = self.root.winfo_y() + correction_value

        # 计算取景框在屏幕上的绝对坐标
        self.topleft_x = window_x + self.x_axis + 1
        self.topleft_y = window_y + self.y_axis + 1
        self.bottomright_x = self.topleft_x + self.width - 1
        self.bottomright_y = self.topleft_y + self.height - 1

        # 更新坐标标签
        self.topleft_label.config(text=f"({self.topleft_x},{self.topleft_y})")
        self.bottomright_label.config(text=f"({self.bottomright_x},{self.bottomright_y})")

        # 每秒更新一次坐标
        self.root.after(1000, self.update_coordinates)

    # 更新显示
    def on_fps_change(self, *args):
        """当帧率输入框内容变化时更新显示"""
        try:
            new_fps = int(self.fps_vr.get())
            new_frame = int(self.frame_vr.get())
            # 锁定
            if new_fps < 1:
                new_fps = 1
            elif new_fps > 100:
                new_fps = 100
            self.fps_choose = new_fps
            if new_frame < 1:
                new_frame = 1
            self.frame_total = new_frame
        except ValueError:
            # 输入非数字时忽略
            pass

    # 录制
    def toggle_recording(self):
        if not self.recording:
            # 数据重置
            self.gif_data = []
            # 开始录制
            self.recording = True
            self.record_btn.config(text="停止录制", bg="#2ECC71")
            Screenshot = ScreenshotData()
            wait = control_frame()
            # 帧率设置
            wait.fps = self.fps_choose
            self.st = time.time()

            def work():
                wait.start()
                data = Screenshot.capture_screen(self.topleft_x, self.topleft_y,
                                                 self.bottomright_x - self.topleft_x + 1,
                                                 self.bottomright_y - self.topleft_y + 1)
                wait.wait()
                self.gif_data.append(data)
                # print(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)
                self.frame_count += 1
                if self.frame_count == self.frame_total:
                    self.frame_count = 0
                    self.recording = True
                    self.record_btn.config(text="保存", bg="#FFA500")
                    print("耗费时间:", time.time() - self.st)
                    print("秒平均帧:", self.frame_total / (time.time() - self.st))
                    return True
                self.root.after(1, work)

            self.root.after(1, work)
        else:
            # 停止录制
            self.recording = False
            self.record_btn.config(text="开始录制", bg="#E74C3C")

            file_path = filedialog.asksaveasfilename(
                initialfile=datetime.now().strftime("%Y-%m-%d-%H-%M-%S"),
                defaultextension=".gif",
                filetypes=[("GIF files", "*.gif"), ("All files", "*.*")])

            if file_path:
                # 处理数据
                g = GIFGenerate()
                g.fps = self.fps_choose
                g.screen_width = self.width
                g.screen_height = self.height
                g.gif_data = self.gif_data
                g.start()
                g.conversion()  # 计算
                g.batch_color_deal()  # 处理图像
                g.gif_save(file_path)


    # 窗口拖动功能
    def start_move(self, event):
        self.x = event.x
        self.y = event.y

    def stop_move(self, event):
        self.x = None
        self.y = None

    def on_move(self, event):
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.root.winfo_x() + deltax
        y = self.root.winfo_y() + deltay
        self.root.geometry(f"+{x}+{y}")
        # 窗口移动后更新坐标
        self.update_coordinates()


if __name__ == '__main__':
    root = tk.Tk()
    app = GIFALL(root)
    root.mainloop()

你可能感兴趣的:(python,gif,图像处理,学习,算法优化,tkinter)