目錄
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. 基本操作
- 啟動程序后,會看到一個透明的錄制窗口
- 通過輸入框設置錄制區域的寬度、高度和坐標
- 設置合適的幀率(建議 10-30fps)和總幀數
- 點擊 "開始錄制" 按鈕開始錄制
- 錄制完成后會顯示總耗時和平均幀率
2. 高級技巧
- 拖動窗口可以調整錄制區域的位置
- 實時坐標顯示幫助精確定位錄制區域
- 根據錄制內容特性調整幀率:
- 靜態內容:10-15fps 即可
- 動態內容:24-30fps 更流暢
- 總幀數控制錄制時長:時長 = 總幀數 / 幀率
五、優化方向
當前版本的錄制工具還有很多可以改進的地方:
- GIF 生成功能:當前只完成了屏幕捕獲,需要添加像素數據到 GIF 的轉換功能
- 文件保存:增加錄制結果保存功能,支持自定義文件名和保存路徑
- 區域選擇優化:添加鼠標拖動選擇區域的功能,提升操作便捷性
- 跨平臺支持:當前僅支持 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()