Python|GIF 解析與構建(6):手搓 tk 錄制工具

目錄

Python|GIF 解析與構建(6):手搓 tk 錄制工具

一、工具功能概覽

二、核心架構設計

1. 幀率控制模塊

2. 屏幕捕獲模塊

3. 主應用模塊

三、關鍵技術解析

1. 屏幕捕獲技術

2. 幀率控制原理

3. 透明窗口實現

四、使用指南

1. 基本操作

2. 高級技巧

五、優化方向

六、總結


Python|GIF 解析與構建(6):手搓 tk 錄制工具

在 GIF 動圖的制作流程中,屏幕錄制是一個非常實用的功能。通過 Python 的 Tkinter 庫,我們可以輕松構建一個輕量級的 GIF 錄制工具,實現自定義區域錄制、幀率控制等功能。

一、工具功能概覽

我們構建的 GIF 錄制工具具備以下核心功能:

  • 自定義錄制區域:可自由設置錄制區域的位置和大小
  • 幀率控制:支持自定義幀率設置,滿足不同場景需求
  • 實時坐標顯示:顯示錄制區域在屏幕上的精確坐標
  • 輕量級界面:基于 Tkinter 構建的簡潔操作界面
  • 窗口拖動:支持拖動窗口調整位置

這個工具適合用于制作教程演示、軟件操作錄制等場景,相比專業錄制軟件更加輕量靈活。

二、核心架構設計

工具采用模塊化設計,主要包含三個核心類:

1. 幀率控制模塊

control_frame類負責管理錄制幀率,確保錄制過程保持穩定的幀速率:

  • 計算每幀的理想間隔時間
  • 監測實際處理時間并進行補償
  • 統計實際幀率和總錄制時間

該模塊通過time.sleep()實現精確的時間控制,確保錄制的 GIF 流暢無卡頓。

2. 屏幕捕獲模塊

ScreenshotData類封裝了屏幕截圖功能,基于 Windows API 實現:

  • 使用ctypes調用 GDI32 和 USER32 動態鏈接庫
  • 支持獲取屏幕 DPI 并計算縮放比例
  • 通過BitBlt函數實現高效屏幕拷貝
  • 提取像素數據用于后續 GIF 生成

這個模塊解決了 Python 中高效屏幕捕獲的問題,為 GIF 錄制提供了基礎數據。

3. 主應用模塊

GIFALL類是主應用類,負責構建 GUI 界面和協調各模塊工作:

  • 構建可視化操作界面,包括參數設置和控制按鈕
  • 處理用戶交互,如窗口拖動、參數修改
  • 協調屏幕捕獲和幀率控制模塊完成錄制流程
  • 實時更新顯示錄制區域坐標

三、關鍵技術解析

1. 屏幕捕獲技術

在 Windows 環境下實現屏幕捕獲,我們采用了 GDI 繪圖接口:

# 通過BitBlt函數拷貝屏幕內容
SRCCOPY = 0x00CC0020
self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)

這種方法相比 Python 的 PIL 庫截圖更加高效,能夠滿足高幀率錄制的需求。通過定義 Windows API 中的結構體,我們可以直接獲取原始像素數據:

# 定義RGB顏色結構體
class RGBQUAD(ctypes.Structure):_fields_ = [("rgbBlue", ctypes.c_ubyte),("rgbGreen", ctypes.c_ubyte),("rgbRed", ctypes.c_ubyte),("rgbReserved", ctypes.c_ubyte)]

2. 幀率控制原理

幀率控制的核心在于計算每幀的理想時間并進行實時補償:

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)

這段代碼會計算實際處理一幀所用的時間,并與理想時間比較,通過time.sleep()進行補償,確保整體幀率穩定。

3. 透明窗口實現

為了讓錄制工具不遮擋屏幕內容,我們實現了透明窗口效果:

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

通過設置窗口的透明顏色屬性,使特定顏色的區域變得透明,提升使用體驗。

四、使用指南

1. 基本操作

  1. 啟動程序后,會看到一個透明的錄制窗口
  2. 通過輸入框設置錄制區域的寬度、高度和坐標
  3. 設置合適的幀率(建議 10-30fps)和總幀數
  4. 點擊 "開始錄制" 按鈕開始錄制
  5. 錄制完成后會顯示總耗時和平均幀率

2. 高級技巧

  • 拖動窗口可以調整錄制區域的位置
  • 實時坐標顯示幫助精確定位錄制區域
  • 根據錄制內容特性調整幀率:
    • 靜態內容:10-15fps 即可
    • 動態內容:24-30fps 更流暢
  • 總幀數控制錄制時長:時長 = 總幀數 / 幀率

五、優化方向

當前版本的錄制工具還有很多可以改進的地方:

  1. GIF 生成功能:當前只完成了屏幕捕獲,需要添加像素數據到 GIF 的轉換功能
  2. 文件保存:增加錄制結果保存功能,支持自定義文件名和保存路徑
  3. 區域選擇優化:添加鼠標拖動選擇區域的功能,提升操作便捷性
  4. 跨平臺支持:當前僅支持 Windows 平臺。

六、總結

通過這個基于 Tkinter 的 GIF 錄制工具,我們深入了解了 Python 在圖形界面和系統接口調用方面的能力。從屏幕捕獲到幀率控制,再到用戶界面設計,每個環節都蘊含著豐富的技術細節。

代碼如下:

import time
import ctypes
import tkinter as tk# 控制幀率
class control_frame():def __init__(self):self.start_time = float()  # 每次啟動時間self.fps = int(10)  # fpsself.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_timereturn 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.gdi32self.user32 = ctypes.windll.user32# 定義常量SM_CXSCREEN = 0SM_CYSCREEN = 1# 縮放比例zoom = 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)zoom = dpi / 96.0finally: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 = 0DIB_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 = widthbmi.bmiHeader.biHeight = -height  # 注意:負高度表示自底向上的位圖bmi.bmiHeader.biPlanes = 1bmi.bmiHeader.biBitCount = 24  # 24即3*8   32bmi.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 = rootself.root.title("gif錄制")self.root.geometry("500x250")self.root.attributes('-topmost', True)  # 設置窗口置頂# self.root.overrideredirect(True)# 隱藏標題欄self.width = 100self.height = 100self.x_axis = 0self.y_axis = 0self.fps_choose = 10self.frame_total = 100self.frame_count = 0self.recording = False  # 初始化錄制狀態# 左上右下坐標self.topleft_x = 0self.topleft_y = 0self.bottomright_x = 0self.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("<ButtonPress-1>", self.start_move)self.root.bind("<ButtonRelease-1>", self.stop_move)self.root.bind("<B1-Motion>", self.on_move)# 啟動坐標更新循環self.update_coordinates()# 錄制欄def create_viewfinder(self):# 創建取景框canvas_width = self.width + self.x_axis + 2canvas_height = self.height + self.y_axis + 2self.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 = 500if new_height > 500:new_height = 500# 更新類屬性self.width = new_widthself.height = new_height# 鎖定if new_x_axis > 500:new_x_axis = 500if new_y_axis > 500:new_y_axis = 500if new_x_axis == "":new_x_axis = 0# 更新類屬性self.x_axis = new_x_axisself.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)# 強制刷新Canvasself.canvas.update_idletasks()# 更新坐標def update_coordinates(self):"""更新取景框的坐標顯示"""titlebar_height = 30border_width = 1correction_value = titlebar_height + border_widthcorrection_left_value = 8# 獲取窗口在屏幕上的位置window_x = self.root.winfo_x() + correction_left_valuewindow_y = self.root.winfo_y() + correction_value# 計算取景框在屏幕上的絕對坐標self.topleft_x = window_x + self.x_axis + 1self.topleft_y = window_y + self.y_axis + 1self.bottomright_x = self.topleft_x + self.width - 1self.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 = 1elif new_fps > 100:new_fps = 100self.fps_choose = new_fpsif new_frame < 1:new_frame = 1self.frame_total = new_frameexcept ValueError:# 輸入非數字時忽略pass# 錄制def toggle_recording(self):if not self.recording:# 開始錄制self.recording = Trueself.record_btn.config(text="停止錄制", bg="#2ECC71")Screenshot = ScreenshotData()wait = control_frame()# 幀率設置wait.fps = self.fps_chooseself.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()# print(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)self.frame_count+=1if self.frame_count == self.frame_total:self.frame_count = 0self.recording = Falseself.record_btn.config(text="開始錄制", bg="#E74C3C")print("耗費時間:",time.time()-self.st)print("秒平均幀:",self.frame_total/(time.time()-self.st))return Trueself.root.after(1, work)self.root.after(1,work)else:# 停止錄制self.recording = Falseself.record_btn.config(text="開始錄制", bg="#E74C3C")# 窗口拖動功能def start_move(self, event):self.x = event.xself.y = event.ydef stop_move(self, event):self.x = Noneself.y = Nonedef on_move(self, event):deltax = event.x - self.xdeltay = event.y - self.yx = self.root.winfo_x() + deltaxy = self.root.winfo_y() + deltayself.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# 控制幀率
class control_frame():def __init__(self):self.start_time = float()  # 每次啟動時間self.fps = int(10)  # fpsself.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_timereturn 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.gdi32self.user32 = ctypes.windll.user32# 定義常量SM_CXSCREEN = 0SM_CYSCREEN = 1# 縮放比例zoom = 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)zoom = dpi / 96.0finally: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 = 0DIB_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 = widthbmi.bmiHeader.biHeight = -height  # 注意:負高度表示自底向上的位圖bmi.bmiHeader.biPlanes = 1bmi.bmiHeader.biBitCount = 24  # 24即3*8   32bmi.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 = rootself.root.title("gif錄制")self.root.geometry("500x250")self.root.attributes('-topmost', True)  # 設置窗口置頂# self.root.overrideredirect(True)# 隱藏標題欄self.width = 100self.height = 100self.x_axis = 0self.y_axis = 0self.fps_choose = 10self.frame_total = 100self.frame_count = 0self.recording = False  # 初始化錄制狀態# 左上右下坐標self.topleft_x = 0self.topleft_y = 0self.bottomright_x = 0self.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("<ButtonPress-1>", self.start_move)self.root.bind("<ButtonRelease-1>", self.stop_move)self.root.bind("<B1-Motion>", self.on_move)# 啟動坐標更新循環self.update_coordinates()# 錄制欄def create_viewfinder(self):# 創建取景框canvas_width = self.width + self.x_axis + 2canvas_height = self.height + self.y_axis + 2self.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 = 500if new_height > 500:new_height = 500# 更新類屬性self.width = new_widthself.height = new_height# 鎖定if new_x_axis > 500:new_x_axis = 500if new_y_axis > 500:new_y_axis = 500if new_x_axis == "":new_x_axis = 0# 更新類屬性self.x_axis = new_x_axisself.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)# 強制刷新Canvasself.canvas.update_idletasks()# 更新坐標def update_coordinates(self):"""更新取景框的坐標顯示"""titlebar_height = 30border_width = 1correction_value = titlebar_height + border_widthcorrection_left_value = 8# 獲取窗口在屏幕上的位置window_x = self.root.winfo_x() + correction_left_valuewindow_y = self.root.winfo_y() + correction_value# 計算取景框在屏幕上的絕對坐標self.topleft_x = window_x + self.x_axis + 1self.topleft_y = window_y + self.y_axis + 1self.bottomright_x = self.topleft_x + self.width - 1self.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 = 1elif new_fps > 100:new_fps = 100self.fps_choose = new_fpsif new_frame < 1:new_frame = 1self.frame_total = new_frameexcept ValueError:# 輸入非數字時忽略pass# 錄制def toggle_recording(self):if not self.recording:# 開始錄制self.recording = Trueself.record_btn.config(text="停止錄制", bg="#2ECC71")Screenshot = ScreenshotData()wait = control_frame()# 幀率設置wait.fps = self.fps_chooseself.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()# print(self.topleft_x, self.topleft_y, self.bottomright_x-self.topleft_x+1, self.bottomright_y-self.topleft_y+1)self.frame_count+=1if self.frame_count == self.frame_total:self.frame_count = 0self.recording = Falseself.record_btn.config(text="開始錄制", bg="#E74C3C")print("耗費時間:",time.time()-self.st)print("秒平均幀:",self.frame_total/(time.time()-self.st))return Trueself.root.after(1, work)self.root.after(1,work)else:# 停止錄制self.recording = Falseself.record_btn.config(text="開始錄制", bg="#E74C3C")# 窗口拖動功能def start_move(self, event):self.x = event.xself.y = event.ydef stop_move(self, event):self.x = Noneself.y = Nonedef on_move(self, event):deltax = event.x - self.xdeltay = event.y - self.yx = self.root.winfo_x() + deltaxy = self.root.winfo_y() + deltayself.root.geometry(f"+{x}+{y}")# 窗口移動后更新坐標self.update_coordinates()if __name__ == '__main__':root = tk.Tk()app = GIFALL(root)root.mainloop()

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/84696.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/84696.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/84696.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在VBA中,提取word表格的文本時,通常有什么干擾符號,需要清除

標題 在VBA中&#xff0c;提取word表格的文本時&#xff0c;通常有什么干擾符號,需要清除 正文 解決問題提取word表格的文本時&#xff0c;通常有什么干擾符號,需要清除 在VBA中提取Word表格文本時&#xff0c;常見的干擾符號及其清除方法如下&#xff1a; ?? 一、主要干擾符…

C++基礎學習:深入理解類中的構造函數、析構函數、this指針與new關鍵字

前言 在C面向對象編程中&#xff0c;類是構建復雜程序的基本單元。今天&#xff0c;我們將深入探討類中的幾個核心概念&#xff1a;構造函數、析構函數、this指針以及new關鍵字。這些概念對于理解C對象生命周期和內存管理至關重要。 1. 構造函數 構造函數是類的一個特殊成員…

2025 高考游記/總結

坐標GD 新課標一卷選手 前言 思緒有點亂&#xff0c;想想從哪里說起 沒想到這個博客已經三年沒發過東西了&#xff0c;上次發還是初三準備特長生的時候&#xff0c;一瞬間就已經高考結束了&#xff0c;有種不真實感 對于高中的三年&#xff0c;有很多話、很多感悟想說&#xff…

Python基礎之函數(1/3)

函數(基礎) [函數后續還會更新兩次] 一.認識函數的作用 函數就是將一段具有獨立功能的代碼塊整合到一個整體并命名&#xff0c;在需要的位置&#xff0c;調用這個名稱即可完成對應的需求 函數在開發過程中&#xff0c;可以更高效的實現代碼重用 二.函數的使用步驟 1定義函…

AWS CloudFormation實戰:構建可復用的ECS服務部署模板

一、前言 在云原生時代,基礎設施即代碼(IaC)已成為DevOps實踐的核心組件。AWS CloudFormation作為AWS原生的IaC服務,允許開發人員和系統管理員以聲明式方式定義和部署AWS資源。本文將深入探討如何構建一個通用的CloudFormation模板,用于在AWS ECS(Elastic Container Servic…

GRUB2 啟動配置的工作原理與優先級規則詳解

一、核心組件概述 /boot/loader/entries/ 類型:目錄,存儲 BLS (Boot Loader Specification) 格式的啟動項配置文件(如 20-custom-kernel-5.14.0.conf)。管理工具:由 grubby、kernel-install 等工具自動生成或修改。配置內容:每個文件定義一個啟動項的詳細參數(內核路徑、…

網頁版便簽應用開發:HTML5本地存儲與拖拽交互實踐

文章目錄 摘要成品顯示核心功能與實現語法1. 本地存儲管理2. 拖拽功能實現3. 自動保存機制4. 時間格式化處理 完整代碼 摘要 本文詳細介紹了一個基于HTML5的便簽應用開發過程&#xff0c;重點講解了如何利用localStorage實現數據持久化存儲&#xff0c;以及如何實現流暢的拖拽…

docker compose安裝Prometheus、Grafana

1、創建目錄結構 mkdir -p /opt/monitoring/{prometheus,grafana} mkdir -p /opt/monitoring/prometheus/{config,data} chmod -R 777 /opt/monitoring # 確保容器有寫入權限 2、準備 Prometheus 配置文件 vi /opt/monitoring/prometheus/config/prometheus.yml global:sc…

稀土化合物在生態環境的應用

稀土化合物憑借強吸附性、催化活性及環境兼容性&#xff0c;已成為生態治理的關鍵材料。氧化物、氯化物、磷酸鹽等基礎產品&#xff0c;通過靈活復配與工藝適配&#xff0c;可高效解決水體凈化、土壤修復、廢氣處理三大核心問題&#xff0c;推動環境治理向低耗高效轉型。那么&a…

搭建網站應該怎樣選擇服務器?

互聯網技術已經全面在各個地區進行發展&#xff0c;越來越多的企業選擇線上業務&#xff0c;搭建屬于自己的網站運營&#xff0c;以此來增加品牌的知名度并進行詳細介紹&#xff0c;但是企業在進行搭建網站的前提&#xff0c;要選擇一種合適的服務器&#xff0c;確保后續網站能…

每日算法刷題Day30 6.13:leetcode二分答案2道題,用時1h10min

5. 1201.丑數III(中等) 1201. 丑數 III - 力扣&#xff08;LeetCode&#xff09; 思想 1.丑數是可以被 a 或 b 或 c 整除的 正整數 。 給你四個整數&#xff1a;n 、a 、b 、c &#xff0c;請你設計一個算法來找出第 n 個丑數。 2.此題是4. 878.第N個神奇數字的進階版&#…

Appium+python自動化(二十一)- Monkey指令操作手機

第一式 - 隱藏命令 monkey隱藏的兩個命令&#xff1a; –pck-blacklist-file<黑名單文件><br><br>–pck-whitelist-file<白名單文件> monkey還有一個隱藏的命令那就是&#xff1a; –f<腳本文件>:可以指定monkey的自定義腳本 一般monkey測試…

微信小程序動態效果實戰指南:從懸浮云朵到絲滑列表加載

小紅書爆款交互設計解析&#xff0c;附完整代碼&#xff01; &#x1f525; 一、為什么動態效果是小程序的關鍵競爭力&#xff1f; 用戶留存提升&#xff1a;數據顯示&#xff0c;86.3%的微商從業者依賴微信小程序&#xff0c;而動態效果能顯著降低跳出率。技術賦能體驗&#…

【機器學習】SAE(Sparse Autoencoders)稀疏自編碼器

SAE(Sparse Autoencoders)稀疏自編碼器 0.引言 大模型一直被視為一個“黑箱”&#xff0c;研究人員對其內部神經元如何相互作用以實現功能的機制尚不清楚。因此研究機理可解釋性&#xff08;Mechanistic Interpretability&#xff09;就成為了一個熱門研究方向。大模型的復雜…

抖音授權登錄-獲取用戶授權調用憑證

實現微信小程序獲取抖音授權,使用Java實現抖音授權登錄,您需要使用抖音開放平臺提供的API 第一步 :抖音獲取授權碼 前提條件 ?需要去官網為應用申請 scope 的使用權限。?需要在本接口的 scope 傳參中填上需要用戶授權的 scope,多個 scope 以逗號分割。?用戶授權通過后…

普通人怎樣用好Deepseek?

今年4月份左右&#xff08;2025年&#xff09;&#xff0c;我在上班路上開車&#xff0c;一邊聽著「黑客與畫家」的播客&#xff0c;一邊想著字節的Trae為啥能夠遠程編程&#xff0c;而我的poclogsender[1] [2]卻只能在本地打日志&#xff0c;3天之后&#xff0c;借助deepseek我…

Python ROS2【機器人中間件框架】 簡介

銷量過萬TEEIS德國護膝夏天用薄款 優惠券冠生園 百花蜂蜜428g 擠壓瓶純蜂蜜巨奇嚴選 鞋子除臭劑360ml 多芬身體磨砂膏280g健70%-75%酒精消毒棉片濕巾1418cm 80片/袋3袋大包清潔食品用消毒 優惠券AIMORNY52朵紅玫瑰永生香皂花同城配送非鮮花七夕情人節生日禮物送女友 熱賣妙潔棉…

織夢dedecms {dede:sql} LIKE模糊查詢問題 多出‘號

我們在用到dede:sql這個標簽時候&#xff0c;查詢語句中 LIKE %~title~%&#xff0c;~title~這個like后會出現單引號&#xff0c;造成查詢出錯或者沒有結果&#xff0c;下面就需要修改一下sql.lib.php這個文件&#xff0c;我們需要把自動為語句添加單引號去掉。 找到/include/…

Cursor-1.0安裝Jupyter-Notebook,可視化運行.ipynb文件中Python分片代碼

Cursor 1.0是AI代碼編輯器的里程碑的最新版本。 Cursor - AI 代碼編輯器 Cursor - The AI Code Editor 下載 Cursor 我使用的Cursor版本信息 Version: 1.0.0 (Universal) VSCode Version: 1.96.2 Commit: 53b99ce608cba35127ae3a050c1738a959750860 Date: 2025-06-04T19:21:39.…

SQL進階之旅 Day 28:跨庫操作與ETL技術

【SQL進階之旅 Day 28】跨庫操作與ETL技術 文章簡述 在現代數據驅動的業務場景中&#xff0c;數據往往分布在多個數據庫系統中&#xff0c;如MySQL、PostgreSQL、Oracle等。如何高效地進行跨庫操作和**數據集成&#xff08;ETL&#xff09;**成為數據工程師和數據庫開發人員必…