有时编写游戏后发博文,为使读者有一个直观的游戏效果,会把游戏运行动画转换GIF格式动图发到博文中。本文介绍如何用python PIL库ImageGrab.grab()函数截屏,编写录屏程序,将视频转换为GIF格式动图文件。
所谓录屏后保存为GIF格式动图文件,就是以固定周期对播放视频连续截屏,保存为多帧图像,再把多帧图像保存为GIF格式动图文件。可使用PIL库ImageGrab.grab()函数截屏,函数的参数是播放视频窗体在显示器屏幕坐标系的左上角和右下角坐标。得到其它软件播放视频的这两个坐标对于一个python程序是很困难的。解决的方法是将播放视频窗体移到录屏窗体中,录屏程序计算自己窗体在显示器屏幕坐标系的左上角和右下角坐标,间接地得到播放软件中的视频窗体在显示器屏幕坐标系的左上角和右下角坐标。为避免被录视频被录屏窗体遮住,这就要求录屏窗体是透明的。实现方法是在窗体画一个和窗体等宽和高的矩形,其填充色和外轮廓色都是透明的。源程序第12行openDialog方法实现这些功能。第15-16行建立一个模式对话框,第28-33行定义了一个透明颜色;然后在canvas中用透明颜色画一个和窗体等宽高的透明的矩形,使窗体变透明。可用移动录屏窗体位置和改变录屏窗体尺寸方法,将播放软件中的视频窗体移到录屏窗体中。由于录屏窗体移动以及窗体尺寸改变,导致系统调用系统函数根据录屏窗体新位置和窗体新尺寸,重画新窗体,同时重画那个矩形,但不会使用透明颜色画矩形。为重画透明矩形,定义自己的on_resize函数(第8句),重画透明矩形,在第19句使自定义on_resize函数替换系统对应函数。在第21-27行,在录屏窗体增加一个’开始录屏’按钮、一个tix组件Control用来设定截屏频率和一个Label组件用显示录屏的时间的秒表。单击主窗体(图3)的’检测fps值’按钮,可检测最大fps,即每秒截屏的最大频率,因此在录屏窗体中设定的fps必须小于所测值。动图本质上是一个图片,但图像会动。一般网站发图片不能超过5M。选择截屏频率要根据将来动图播放频率和动图最终尺寸综合考虑来决定。连续截屏和秒表都是无限循环程序,所有它们代码都不能在主线程运行,必须在子线程运行,否则就无法用单击主线程’停止’按钮结束截屏或秒表。如先关闭主线程,后关闭子线程,有可能产生错误抛出异常。虽然官方文档介绍第48行语句可保证在关闭主线程前关闭相关子线程,但试验后还是抛出了异常,因此将关闭窗体事件绑定自定义函数closef1(第20行),保证先关闭子线程,再关闭主线程。‘开始录屏’和’停止录屏’共用同一按钮,根据标题执行不同代码(第42-57行)。如标题是’开始录屏’将建立两个子线程,一个用来录屏(用第59行的RecordScreen方法),一个用来做秒表计时(用第77行的aTimer方法)。秒表线程while循环条件k==0,如k=1,退出循环,因此退出aTimer()方法,也就退出秒表子线程。另一个录屏子线程同样的道理。要注意第68、72和73行语句,它们保证指定的截屏频率。
单击主窗体(图3)"录屏"按钮,打开录屏窗体(图1),该窗体是模式对话框,其不关闭无法操作主窗口,可以看到其窗体下部是透明的,有个应用程序在其中,还可以看到win10桌面上的图案和图标。点击标题栏左侧图标在下拉菜单中选择移动或大小,可将窗体移动或改变大小。用此法可使视频窗体(图4)移到录屏窗体透明矩形内(图5),请注意,视频窗体的标题栏也被遮挡。

                  图4 将此视频录屏

             图5 已将视频窗体移到录屏窗体透明矩形内
单击开始录屏按钮开始录屏,单击停止录屏按钮停止录屏,关闭录屏窗体。返回主窗体,这时可直接把录屏所得到的多帧图像保存为GIF格式动图文件。但实际上所得图像还是要编辑一下的。例如fps不匹配,截屏频率大于视频播放频率,有重复帧,应删除多余的仅保留一个。录屏前后由于操作鼠标,前后产生很多无用帧,必须删除等等。当然可以保存为动图后,用其它动图软件进行编辑。本程序采用直接编辑所得图像方法,因此关闭录屏窗体后,所录图像出现在主窗体如图2。

其中12个小图是所录图像的缩小图像,键盘左右键可使12个小图显示不同帧号图像。单击小图下边的数字帧号使其变红色,表示该帧被选中,称为’选中帧’,选中帧图像同时在左大图显示。可删除选中帧图像,删除选中帧前边所有帧图像(不包括选中帧),删除选中帧后边所有帧图像(不包括选中帧)。右击数字帧号,所选图像在右大图显示,用来和左图比较。所有这些图都不是原始尺寸。单击’按2参数播放’按钮可查看原始尺寸动图效果。单击’按3参数保存为动图’按钮保存为GIF格式文件。保存的动图根据所选缩小倍数做了缩放。单击’修改3参数’按钮,打开对话框修改每秒播放几帧(fps)、为减小文件字节数保存为gif格式动图时每帧图像缩小倍数(scaling)和完整动图重复播放次数(playNum)。
为了读懂图像编辑程序,必须知道所有这些图像都是在Canvas实例上用create_image方法创建的image实例显示的,所有提示信息都是用create_text方法创建的text实例显示的。为了显示12个小图、2个大图和所有提示信息,在程序运行后,用第418-428行代码创建了14个image实例和14个text实例,如图3。所有image实例没有显示图像,仅显示一个白色小矩形。12个小图下边显示帧号的text实例为空,另2个text实例信息不完整,缺少帧号。当录屏结束或打开图像文件后就会出现界面如图2。使图像在image实例上显示的方法是修改每个image实例的image属性为相应的图像,修改每个text实例的text属性为相应的提示信息。请注意在第421行和第425-428行都设置属性tag值。通过tag使用canvas的itemconfig方法可以很容易修改image实例和text实例的其它属性。例如第144行,是修改tag=‘m3’的image实例的image属性为bigPic1image,让tag=‘m3’的image实例显示指定图像。canvas上创建的多个实例允许有相同tag(例如第91-92行的’allt’),可修改有相同tag的全部实例的同一个属性,例如第191行。12个小图像下边的Text实例显示的数字是帧号,单击数字帧号,选中该帧,该帧图像在左大图显示,右击数字帧号,该帧图像在右大图显示。因此必须为每个显示帧号的Text实例增加鼠标单击和右击事件,为简化程序设计,因此定义了MyText类(第86-106行)。可参见本人博文:数字华容道-将显示数字、单击数字事件绑定及事件处理函数封装在python类中以简化编程。

录屏后的图像保存到Images列表,这些图像是PIL的Image类实例,并不能被Canvas的image实例直接显示,需要转换后才能显示。另外Images列表中保存的是录屏后图像的原始尺寸,可能原始尺寸并不满足在编辑界面的14个图形对于尺寸的要求,可能需要根据实际情况缩小。因此这14个图像都不是原始尺寸图像。只有单击’按2参数播放’按钮播放图像才是原始尺寸图像。方法reformat(第154-159行)完成以上这两个功能。具体缩小倍数是在reSet方法(第120-135行)中获得的,共有两个缩小倍数,scale2是12个小图的缩小倍数,scale1是两个大图的缩小倍数。每当录屏结束或打开图像文件后就会调用reSet方法(第75、116行),出现界面如图2,同时获得缩小倍数,因此这是初始化程序。还有一点需要特别注意,被方法reformat转换后图像必须保存到全局变量中,例如bigPic1image、bigPic2image和shomImage列表,因为Canvas的image实例显示转换后图像后,因某些原因,例如窗体最小化然后最大化,系统重画窗体以及窗体上的图像,为重画图像,系统都会使用这些转换后图像,它们必须是全局变量系统才能找到。
为了更容易读懂有关编辑图像程序,必须明白几个变量的意义,可参见第382-392行的内容。例如frameN0是要显示的12个小图像的第一个图像的帧号,将此帧号传递给方法showAll(),该方法通过循环,修改12个小图的属性image以显示指定图像。方法showAll()在编辑图像程序中经常被使用,例如删除一些帧后就可能调用这个方法。还有如currenframeN0,是被选中的帧号,简称选中帧,由此才能明白按钮’删选中帧’、'删前边帧’和’删后边帧’的意义,选中帧也是左边大图正在显示的图像帧号。右边大图正在显示的图像帧号是:rightBigPicFNo。三者的值为-1,表示还未选定。方法showBigPic使两个大图显示图像。
单击’按2参数播放’按钮,可播放录屏所得多帧图像。这里的两个参数是:视频播放频率fps和完整视频播放几次,可以是有限次数,也可是无限次播放。所谓播放,就是将保存在列表中,用截屏方法得到的多帧图像,以fps速度从列表逐帧取出,以原始尺寸在canvas上image实例显示。该功能是为了模拟gif格式动图效果,以便决定在保存为gif格式动图文件时所设定的参数,包括图像缩小倍数、fps和循环次数3个参数。播放时创建一个独立窗口(第281-282),由于是连续播放,实际的播放在子线程完成,首先创建一个子线程(第290-292行),子线程中运行的程序是方法PlayPic(第312行),它完成实际播放功能。该功能和录屏功能注意事项类似,可参考前边有关内容。但是为避免关闭窗体f3抛出异常,采用录屏中方法仍会抛出异常,因此在关闭窗体函数中,并未关闭窗体,仅是令n9=1,以便使方法PlayPic退出循环后结束线程,然后又延时0.1秒启动新子线程(第303-304行),给播放子线程关闭时间,最后在新子线程中关闭f3窗体(第310行)。
单击’按3参数保存为动图’按钮,可将列表中的图像保存为gif格式的动图(第347行)。在保存前,最好单击’按2参数播放’按钮播放实际尺寸的图像,再决定保存动图图像的缩小倍数,使用不同fps播放,看一下效果,决定保存动图图像的fps。单击’修改3参数’按钮,可修改图像缩小倍数、fps和循环次数3个参数。
完整程序如下。该程序可能有bug,也可能有许多不足之处,希望读者批评指正,非常感谢。再一次提醒,因PIL的问题,显示设置的缩放比例必须调成100%,录屏fsp必须小于所测值,否则截屏所得图像不正确。

import tkinter.tix as tk#导入Tkinter.tix,如开始用import tkinter as tk,后又要使用tix,此改法,前边不用修改
from PIL import ImageGrab,Image,ImageTk
import threading
import time
import tkinter.filedialog,tkinter.messagebox
import shelvedef on_resize(evt):     #窗体大小改变时,调用自定义方法,是为了增加第2条语句,画透明矩形f1.configure(width=evt.width,height=evt.height)canvas.create_rectangle(0,0,canvas.winfo_width(),canvas.winfo_height(),fill=TRANSCOLOUR,outline=TRANSCOLOUR)def openDialog():           #打开对话框准备录屏,移动位置,改变大小,将要被录屏的窗体放到本窗体透明区域global f1,canvas,TRANSCOLOUR,s,label,tixC     #在Toplevel窗口和主窗口可以互相使用对方的变量和方法。root.state('icon')                            #主窗体最小化。f1 = tk.Toplevel(root)                        #用Toplevel类创建独立主窗口的新窗口f1.grab_set()           #将f1设置为模式对话框,f1不关闭无法操作主窗口,将所有事件由f1接受。        f1.geometry('550x400+400+150')f1.title('改变窗体大小和位置使屏幕被录制部分在窗体透明矩形中后开始录制')f1.bind('<Configure>', on_resize)           #窗体大小改变时,调用自定义方法f1.protocol("WM_DELETE_WINDOW", closef1)    #使f1窗口关闭时调用参数2指定函数,关闭其它线程,避免报错    frm = tk.Frame(f1)frm.pack(fill=tk.BOTH)tk.Button(frm,textvariable=s,command=startORstop).pack(side='left')   #开始录屏和停止录屏共用按钮tixC=tk.Control(frm,value=5,max=10,min=1,label='fps(1-10,键盘输入回车确认)',integer=True)#选录屏频率tixC.pack(side='left')label=tk.Label(frm,font=("Arial",15),fg='red',text='0')   #显示录屏的时间label.pack(side='right')TRANSCOLOUR = 'gray'f1.wm_attributes('-transparentcolor', TRANSCOLOUR)  #定义透明颜色canvas = tk.Canvas(f1)              #后3句在canvas中画一个和窗体等宽高的透明的矩形,即使窗体变透明canvas.pack(fill=tk.BOTH, expand=tk.Y)canvas.create_rectangle(0,0,canvas.winfo_width(),canvas.winfo_height(),fill=TRANSCOLOUR,outline=TRANSCOLOUR)def closef1():global n,k,f1n=1                             #关掉录屏线程k=1                             #关掉秒表线程,k=1从aTimer方法while循环退出,线程结束root.state('normal')            #使主窗体正常显示f1.destroy()                    #关闭对话框def startORstop(): global s,n,m,k          if s.get()=='开始录屏':s.set('停止录屏')tixC.configure(state="disabled")        #tixC.state="disabled"不报错,但不能使其无效      t1 = threading.Thread(target=aTimer)    #新线程,计数器t1.setDaemon(True) #如不加此条语句,在截屏停止前,即线程未结束,关闭窗口,会抛出异常t1.start()    #将调用aTimer方法在子线程中运行,退出该函数子线程结束,可令k=1结束子线程t = threading.Thread(target=RecordScreen)   #多线程录屏t.setDaemon(True)t.start()        else:n=1                                 #关掉录屏线程k=1                                 #关掉秒表线程,k=1从aTimer方法while循环退出,线程结束root.state('normal')                #使主窗体正常显示f1.destroy()                        #关闭对话框def RecordScreen():                         #实际的录屏方法,就是按指定时间间隔多次截屏global n,m,fps,imagesx=f1.winfo_rootx()+canvas.winfo_x()     #录屏矩形左上角窗体坐标转换为显示器屏幕坐标(x,y)y=f1.winfo_rooty()+canvas.winfo_y()x1=x+canvas.winfo_width()               #录屏矩形右下角显示器屏幕坐标(x1,y1)y1=y+canvas.winfo_height()    n,m=0,0SampleCycle=1/int(fps)while n==0:                             #n=1,将退出while循环,线程结束,录屏结束,start = time.time()                 #以秒为单位,开始时间。下句是截屏语句p=ImageGrab.grab((x,y,x1,y1))    #截屏,因PIL的原因,必须将win10显示设置的缩放比例调成100%m+=1images.append(p)                    #将截屏的image类实例保存到列表end = time.time()                   #结束时间。在Windows系统中time.sleep(SampleCycle-(start-end)) #延迟时间取样周期(SampleCycle)-(截屏用去的时间)saveFile()                              #保存列表images中所有录屏图像为文件,将覆盖上次录屏数据reSet()                                 #调用初始化方法def aTimer():                               #在另一线程中的秒表,记录录屏时间global k,label    k=0seconds=-1while k==0:seconds+=1                          #每隔一秒+1       label['text']=str(seconds)+' '      #在label组件上显示秒数time.sleep(1)                       #延迟1秒class MyText():      #用canvas的text显示12个小图帧号,需要响应鼠标左击和右击事件,为此定义该类。canvas=0                            #canvas为类变量,所有类实例共用的一个变量functionId=None           #将引用showBigPic方法用来显示大图,参数1是帧号,参数2是大图1或2def __init__(self,n):               #构造函数self.tagN="t"+str(n)            #保存下句在Canvas中创建的text实例的tag值MyText.canvas.create_text(60+n*105,605,activefill='red',#text=' ',tag=(self.tagN,'allt'),font=("Arial",15))MyText.canvas.tag_bind(self.tagN,'<Button-1>',self.leftClick)         #绑定左键单击事件MyText.canvas.tag_bind(self.tagN,'<Button-3>',self.rightClick)        #绑定右键单击事件def leftClick(self,event):                   #类实例方法,是鼠标左击事件函数,选第1大图显示的图s=MyText.canvas.itemcget(self.tagN,'text')         #得到属性'text'的值(字符串)k=int(s)                                           #k为帧号MyText.functionId(k,1)                             #调用函数在1号大图显示左键单击选的图像                                          MyText.canvas.itemconfig('allt',fill="black")      #所有text实例的字体颜色都变黑MyText.canvas.itemconfig(self.tagN,fill="red")     #当前选定图像字体颜色变红MyText.canvas.itemconfig('m1',text='鼠标左键单击小图下帧号选此帧为当前图像,当前帧号为:'+s)def rightClick(self,event):                     #类实例方法,是鼠标左击事件函数,选第2大图显示的图s=MyText.canvas.itemcget(self.tagN,'text')  #开始帧号并不显示,只有显示数字才能响应任何事件        k=int(s)                                    #因此,从字符串转换为整型数一定成功MyText.functionId(k,2)                      #调用函数在2号大图显示右键单击选的图像MyText.canvas.itemconfig('m2',text='鼠标右键单击小图下帧号选此帧为比较图像,当前帧号为:'+s)def saveFile():                                 #保存录屏所得图像列表为文件with shelve.open('myFile') as f:f['myKey'] = images                     #保存列表    def openFile():                                 #取出所保存录屏所得图像列表的文件global imageswith shelve.open('myFile') as f:images=f.get('myKey')reSet()
#tkinter.filedialog.askopenfilename()#选择打开什么文件,返回文件名。defaultextension指定文件后缀,该后缀会自动添加
#filetypes,指定筛选文件类型的下拉菜单选项(如:filetypes=[('PNG’,'png'),('JPG’,'jpg’),('GIF’,'gif’)])def reSet():    #在获得新录屏图像后调用此函数。两次调用该函数:录屏结束(第75行),打开图像文件后(第116行)global currenframeN0,rightBigPicFNo,frameN0,scale2,scale1scale1=images[0].height/450             #为了显示大图像,原始图像缩小比例,450是大图允许最大高度if scale1<1:                            #比450小,就不用缩小了scale1=1scale2=images[0].width/100              #为了显示小图像,原始图像缩小比例,小图允许最大宽度为100if scale2<1:                            #比100小,就不用缩小了scale1=1canvasM.itemconfig('m1',text=s1+'0')                #修改左大图上边显示的帧号canvasM.itemconfig('m2',text=s2+'1')                #修改右大图上边显示的帧号showBigPic(0,1)                                     #在左大图显示第0帧图像    currenframeN0=0                                     #当前选中帧号(当前帧)=0showBigPic(1,2)                                     #在右大图显示第1帧图像rightBigPicFNo=1                                    #右大图显示图像的帧号frameN0=0                                           #12个小图的起始帧号showAll(frameN0)                                    #从第0帧开始显示12个小图def showBigPic(frameNo,whichBigPic):#显示大图,参数1正数是帧号,-1显示空白;whichBigPic=1,左大图,=2,右大图global bigPic1image,bigPic2image,currenframeN0,img,rightBigPicFNo,scale1if whichBigPic==1:                                         #whichBigPic=1,左大图if frameNo>=0:bigPic1image=reformat(frameNo,scale1)   #返回要显示大图,必须保存到全局变量,scale1为缩小倍数else:                                                  #如果帧号小于0,显示空白bigPic1image=imgcanvasM.itemconfig('m3',image=bigPic1image)            #在左大图显示指定图像,'m3'为canvas实例tagcurrenframeN0=frameNoelif whichBigPic==2:                                             #whichBigPic=2,右大图if frameNo>=0:bigPic2image=reformat(frameNo,scale1)else:                                               #如果帧号小于0,显示空白bigPic2image=imgcanvasM.itemconfig('m4',image=bigPic2image)         #在右大图显示指定图像,'m4'为canvas实例tagrightBigPicFNo=frameNodef reformat(No,k):               #将列表images[No]图像转换为canvas能显示的格式和合适尺寸im=images[No]                 #从列表取出帧号为No的图像m=int((im.width/k)//1)        #图像宽缩小k倍n=int((im.height/k)//1)       #图像高缩小k倍im=im.resize((m,n))           #缩小图像图像尺寸return ImageTk.PhotoImage(image=im)         #返回canvas能显示的图像def moveR(event):               #12个小图像全部向右移global frameN0if frameN0<=0:return    frameN0-=1    showAll(frameN0)def moveL(event):               #12个小图像全部向左移global shomImage,frameN0,imagesif frameN0==len(images)-12:returnif len(images)<=12:frameN0=0returnframeN0+=1    showAll(frameN0)def showAll(N0):                    #N0是要显示的起始帧号,从N0开始显示12个小图global shomImage,img,scale2shomImage=[]                    #列表保存12个小图要显示的图像,必须是全局变量m=12if len(images)<12:              #如列表保存图像个数<12,12小图后边有些位置无图像可显示m=len(images)                  for n in range(12):         #先让所有小图显示白矩形框,小图下边不显示帧号,显示为空canvasM.itemconfig('p'+str(n),image=img) #清空显示的所有小图,img是一个白矩形框canvasM.itemconfig('t'+str(n),text=' ')  #清空显示的所有小图下边的帧号for n in range(m):#m=12,12小图都有图像,m<12,例如=11,前11个有图像,第12个无图像保留白矩形框shomImage.append(reformat(N0+n,scale2))   #要显示的小图像添加到全局列表中,否则不能显示canvasM.itemconfig('p'+str(n),image=shomImage[n])   #在指定位置显示小图像canvasM.itemconfig('t'+str(n),text=str(N0+n))       #在指定位置显示帧号,注意通过tagcanvasM.itemconfig('allt',fill="black")                 #所有显示帧号的text实例的字体颜色都变黑if N0<=currenframeN0<=N0+11:                            #如frameN0+n是被选中的帧,帧号变红    canvasM.itemconfig('t'+str(currenframeN0-N0),fill="red")  #字体颜色变红def delAframe():                            #该方法删除当前帧,即左大图显示的帧global currenframeN0,rightBigPicFNo,frameN0if currenframeN0>=0:del images[currenframeN0]           #此条语句删除当前选中帧,简称当前帧if currenframeN0==rightBigPicFNo:   #如果右大图也显示此帧,令其显示空,显示帧号为-1showBigPic(-1,2)rightBigPicFNo=-1canvasM.itemconfig('m2',text='')if currenframeN0<frameN0:#此时frameN0指向的图像帧号将减1,为了使12小图维持不变frameN0-1if frameN0-1>=0:        #帧号不能是负数frameN0-=1currenframeN0=-1                    #令当前帧为-1,因当前选中帧被删除showBigPic(-1,1)                    #令左大图显示空canvasM.itemconfig('m1',text='')showAll(frameN0)                        #刷新所有小图def delFront():                         #该方法删除当前帧之前所有帧global images,currenframeN0,rightBigPicFNo,frameN0if currenframeN0>=0:                #=-1,没有选则当前帧images=images[currenframeN0:]   #此条语句删除当前帧之前所有帧if rightBigPicFNo<currenframeN0 and rightBigPicFNo>=0:#第2大图是否被删除?showBigPic(-1,2)                            #是,删除第2大图及提示信息rightBigPicFNo=-1canvasM.itemconfig('m2',text='')elif rightBigPicFNo>=currenframeN0 and rightBigPicFNo>=0:#如不是rightBigPicFNo=rightBigPicFNo-currenframeN0          #第2大图帧号改变canvasM.itemconfig('m2',text=s2+str(rightBigPicFNo)) #提示信息中帧号改变frameN0=frameN0-currenframeN0       #12个小图的起始帧号也要改变,>=0,显示的12小图不变if frameN0<0:                       #如<0,正在显示的12小图将被删除frameN0=0                       #因此从删除后的第0帧开始显示currenframeN0=0                             #第1大图帧号变为0canvasM.itemconfig('m1',text=s1+'0')        #第1大图提示信息中帧号改变showAll(frameN0)                            #重新显示12个小图def delBehind():                            #该方法删除当前帧之后所有帧global images,currenframeN0,rightBigPicFNo,frameN0if currenframeN0>=0:                    #=-1,没有选则当前帧images=images[:currenframeN0+1]                          #此条语句删除当前帧之后所有帧if rightBigPicFNo>currenframeN0 and rightBigPicFNo>=0:   #第2大图帧号大于当前帧号被删除showBigPic(-1,2)rightBigPicFNo=-1canvasM.itemconfig('m2',text='')fn=currenframeN0-frameN0 #选定的当前帧减12小图起始帧if fn<0:                 #删除当前帧之后所有帧,包括原来显示的12小图都被删除frameN0=0            #则12小图从0帧开始显示elif fn<11:              #如fn>=11,原来显示的12小图未被删除,显示的12小图不变frameN0-=(11-fn)     #如fn<11显示的12小图后边有被删除图像,12小图起始帧前移重新显示12图if frameN0<0:        #帧号不能为0,一定是所有图数<12frameN0=0        showAll(frameN0)def change3value():                  #该方法修改图像缩放比例、FPS和GIF循环次数global f2f2 = tk.Toplevel(root)           #用Toplevel类创建独立主窗口的新窗口f2.grab_set()           #将f1设置为模式对话框,f1不关闭无法操作主窗口,将此应用程序的所有事件路由f1。        f2.geometry('420x200+400+150')f2.resizable(width=False,height=False)f2.title('修改图像缩放比例、FPS和GIF循环次数') #f1.destroy()tk.Label(f2,text='建议用箭头键输入,如用键盘输入回车确认。循环次数为0无限循环',fg='red').place(x=5,y=10)tk.Label(f2,text='播放用原尺寸,不用缩放倍数,但用另2个参数。保存动图(GIF)使用3个参数',fg='red').place(x=5,y=30)c1=tk.Control(f2,value=1,max=10,min=1,label='缩放倍数(1-10)',integer=True)c1.place(x=10,y=60)c2=tk.Control(f2,value=4,max=10,min=1,label='  fps(1-10)',integer=True)c2.place(x=145,y=60)c3=tk.Control(f2,value=0,max=10,min=0,label='循环次数(1-10)',integer=True)c3.place(x=265,y=60)tk.Button(f2,text="确定",command=lambda C1=c1,C2=c2,C3=c3:OK(C1,C2,C3)).place(x=100,y=110)tk.Button(f2,text="放弃",command=cancel).place(x=200,y=110)def OK(C1,C2,C3):global f2,label,fps,scaling,playNum    scaling=C1.cget('value')            #得到'图像缩放比例'组件输入的字符串fps=C2.cget('value')playNum=C3.cget('value')label['text']='图像缩小倍数:'+scaling+', fps:'+playNum+', 循环次数='+playNumf2.destroy()#s=round(1.234,1)                        #四舍五入,小数点后保留1位小数
def cancel():global f2f2.destroy()def play():global f3,images,cv,mainImgif len(images)==0:return#root.state('icon')                       #主窗体最小化。               f3 = tk.Toplevel(root)           #用Toplevel类创建独立主窗口的新窗口f3.grab_set()           #将f1设置为模式对话框,f1不关闭无法操作主窗口,将此应用程序的所有事件路由f1。        f3.geometry('380x200+400+150')    f3.title('播放录屏实际大小所有帧图像,Esc键退出')f3.protocol("WM_DELETE_WINDOW", closef3)    #使f1窗口关闭时调用参数2指定函数,否则将报错    f3.bind('<Escape>', stop)                   #绑定键盘Escape键的事件函数,窗口必须在激活状态才接收事件cv = tk.Canvas(f3)cv.pack(fill=tk.BOTH, expand=tk.Y)cv.create_image(0,0,tag='f3im',anchor='nw')         #(0,0)是左上角坐标,即'nw't = threading.Thread(target=PlayPic)                #多线程播放录屏t.setDaemon(True)t.start()def stop(event):global n9,f3n9=1#root.state('normal')            #使主窗体正常显示f3.destroy()def closef3():global n9,f3n9=1#如去掉下边两条语句,使用后边被注释的两条语句,能关闭窗体后正常运行,但Shell窗口报错t = threading.Timer(0.1,close0)#原因应是虽令n9=1,在执行f3.destroy()后,很多变量被销毁,t.start() #另一线程PlayPic方法还未结束,使用被销毁变量,肯定出错。#time.sleep(2)#估计可能原因是n9=1要退出本函数才能生效,延时也不起作用,使线程无法结束。解#f3.destroy()#决方法是,0.1秒后用Timer启动新线程close0,退出本方法,n9=1生效,线程PlayPic结束def close0():global f3f3.destroy()    #线程PlayPic已结束,在关闭f3窗口就不会报错了。def PlayPic():global n9,images,fps,img1,cv,playNum    k,n9=0,0l=int(playNum)                                  #播放次数SampleCycle=1/int(fps)                          #播放周期while n9==0:                                    #n9=1,退出播放,也就退出子线程start = time.time()                         #以秒为单位,开始时间im=images[k]               img1=ImageTk.PhotoImage(image=im)            #返回canvas能显示的图像cv.itemconfig('f3im',image=img1)k+=1if k==len(images):k=0if int(playNum)!=0:                     #=0为连续播放l-=1                                #不是连续播放,l是播放次数,播放1次减1if l==0:                            #l=0,播放次数完成,退出returnend = time.time()                           #结束时间。在Windows系统中time.sleep(SampleCycle-(start-end)) #延迟时间取样周期(SampleCycle)-(截屏用去的时间)def saveGIF():global images,playNum,fps,scalingfname=tkinter.filedialog.asksaveasfilename(title=u'保存GIF文件',defaultextension='GIF')if fname=='':returnimg=images[:]#列表images拷贝到img,两者是独立列表,img列表中图像尺寸可能改变,images图像不被改变if scaling!='1':              #=1保持原尺寸不缩小k=int(scaling)            #=2到10,要缩小2到10倍,for i in range(len(img)):   #对列表中每个图像进行缩放im=img[i]               #取出列表第n项m=int((im.width/k)//1)n=int((im.height/k)//1)img[i]=im.resize((m,n))l=int(playNum)                  #l为完整动图播放几次,=0,无限循环播放d=int(round(1000/int(fps),0))   #d为播放周期,1000/int(fps)后保留整数img[0].save(str(fname),save_all=True,append_images=img,duration=d,loop=l)#img为保存为动图的列表def test_fps():                               #检测每秒截全屏最大次数global nws = root.winfo_screenwidth()              #屏幕长和宽 hs = root.winfo_screenheight()t = threading.Timer(1,dojob)               #1秒后,在另一线程调用dojob方法停止截屏m,n=0,0    t.start()                                   #启动定时while n==0:                                 #n==0,循环,1秒后调用dojob方法,n=1,退出循环   p=ImageGrab.grab((0,0,ws,hs))           #因PIL原因,必须将显示设置的缩放比例调成100%m+=1                                    #调用grab方法次数label1['text']='最大fps='+str(m)            #退出循环,显示调用grab方法次数
def dojob():global nn=1def Help():             #帮助按钮事件处理函数s='因PIL的问题,显示设置的缩放比例必须调成100%,录屏fsp必须小于所测值。'+\'点击标题栏左侧图标在下拉菜单中选择移动或大小,可将窗体移动或改变大小。'+\'单击"录屏"按钮,打开录屏窗体,使被录屏窗体移到录屏窗体透明矩形内后,'+\'单击开始录屏按钮开始录屏,单击停止录屏按钮停止录屏,关闭录屏窗体。'+\'所录图像出现在主窗体。小图是12个缩小图像,左右键可显示不同帧号图像。'+\'单击数字帧号变红色表示选中,被选帧图像在左大图显示。'+\'根据选中帧号可删除指定帧图像。右击数字帧号,'+\'所选图像在右大图显示,用来和左图比较。所有这些图都不是原始尺寸。'+\'单击按2参数播放按钮可查看原始尺寸动图效果。单击按3参数保存为动图按钮保存'+\'为GIF格式文件。保存的动图根据所选缩小比例做了缩放。\n\n保留所有版权'tkinter.messagebox.showinfo(title="帮助",message=s)root = tk.Tk()
root.geometry("1280x650+0+0")#使用计算机分辨率为1280x720,结合下句,在高分辨率下能保持尺寸不变
root.resizable(width=False,height=False) #设置窗口是否可变,这里宽不可变,高不可变,默认为True
#root.state("zoomed")                         #如窗口最大化,分辨率不同,窗体尺寸可能不同
root.title('编辑录屏图像后保存为动图')        #窗口标题
bigPic1image=None       #鼠标左键单击帧号所得选中帧的左边大图像,两大图显示的图像必须是全局变量
bigPic2image=None       #鼠标右键单击选的图像,用来和当前选中图像做比较的图像
images=[]#记录录屏图像,录屏结束保存为文件'myKey',编辑时修改列表图像,放弃修改可从文件重得未修改图像
frameN0=0               #12个小图像的第一个图像的帧号,初始值为第0帧
currenframeN0=-1        #当前被选中帧号,简称'选中帧',也是左边大图显示的帧
rightBigPicFNo=-1       #右边大图显示的帧号
fps=4                   #每秒播放帧数
scaling=1               #存为gif文件时,所有帧图像的缩放比例,=1不缩放,=2缩小2倍...
scale1=1                #编辑大图像为了显示缩小的比例,列表images中的图像是录屏原始图像,没有缩小
scale2=3                #编辑小图像为了显示缩小的比例
playNum=0               #gif图重复播放的次数,=0,循环播放,=1,播放1次
frm = tk.Frame(root)
frm.pack(fill=tk.BOTH)
tk.Button(frm, text="录屏(上次录屏数据将丢失)",command=openDialog).pack(side='left')
tk.Button(frm,text="删选中帧",command=delAframe).pack(side='left')
tk.Button(frm,text="删前边帧",command=delFront).pack(side='left')
tk.Button(frm,text="删后边帧",command=delBehind).pack(side='left')
tk.Button(frm,text="按2参数播放",command=play).pack(side='left')
tk.Button(frm,text="修改3参数",command=change3value).pack(side='left')
label=tk.Label(frm,text='图像缩小倍数:1, fps:4, 循环次数=0为无限循环')
label.pack(side='left')
tk.Button(frm,text="按3参数保存为动图",command=saveGIF).pack(side='left')
tk.Button(frm,text="检测fps值",command=test_fps).pack(side='left')
tk.Button(frm,text="保存编辑后图像",command=saveFile).pack(side='left')
tk.Button(frm,text="读图像或恢复到保存前",command=openFile).pack(side='left')
tk.Button(frm,text="帮助",command=Help).pack(side='left')
label1=tk.Label(frm,text='',fg='red')
label1.pack(side='left')
s=tk.StringVar()
s.set('开始录屏')
canvasM = tk.Canvas(root)
canvasM.pack(fill=tk.BOTH, expand=tk.Y)
MyText.functionId=showBigPic
MyText.canvas=canvasM
root.bind('<Right>', moveR)
root.bind('<Left>', moveL)
im=Image.new("RGB", (100, 100), 'white')
img = ImageTk.PhotoImage(image=im)      #将image1转换为canvas能显示的格式
for n in range(12):canvasM.create_image(60+n*105, 550,image=img,tag='p'+str(n))          #tag=('G','oval')MyText(n)
s1='鼠标左键单击小图下帧号选此帧为当前图像,当前帧号为:'
s2='鼠标右键单击小图下帧号选此帧为比较图像,当前帧号为:'
canvasM.create_text(200,10,text=s1,tag='m1')
canvasM.create_text(840,10,text=s2,tag='m2')
canvasM.create_image(320,250,image=img,tag='m3')
canvasM.create_image(960,250,image=img,tag='m4')
root.mainloop()

用Python编写录屏程序将播放的视频用截屏方法转换为多帧图像编辑后保存为GIF格式动图文件相关推荐

  1. chatgpt赋能python:Python编写录屏软件:方便、高效的自制工具

    Python编写录屏软件:方便.高效的自制工具 录屏软件是一种能够将电脑屏幕上的活动记录下来的工具,通常用于制作教学视频.游戏攻略视频等.市面上有很多录屏软件可供选择,但是有时候这些软件不够灵活,无法 ...

  2. 教你用Python 编写 Hadoop MapReduce 程序

    摘要:Hadoop Streaming 使用 MapReduce 框架,该框架可用于编写应用程序来处理海量数据. 本文分享自华为云社区<Hadoop Streaming:用 Python 编写 ...

  3. 为了偷懒,我开始用Python编写Android应用程序

    为了偷懒,我开始用Python编写Android应用程序 说明 环境准备 开发环境准备(windows) 编译环境准备 自行配置制作编译环境 偷懒:直接下载配置好的虚拟机 编译 验证APK 总结 说明 ...

  4. 使用Python编写网络扫描程序

    使用Python编写网络扫描程序 ​ 网络扫描程序通过向成百上千台计算机发送请求并分析其响应,扫描第2层和第3层网络中指定范围内的网络ID.利用某些扩展技术,网络扫描程序还可以获得通过Samba和Ne ...

  5. Python 编写的图形程序打包为安卓 APP、IOS

    如果想使用 Python 语言编写图形界面程序,那么有不少的框架可以提供支持,比如 Kivy.Tkinter.PyQt.WxPython.pyui4win等等. 这些框架都是只能创建桌面图形界面程序, ...

  6. python编写一计票程序,键盘输入候选人姓名(输入“#”结束),使用字典存储并统计出候选人得票数。python实现分段函数。

    一.编程题目         编程题目1:python编写一计票程序,键盘输入候选人姓名(输入"#"结束),使用字典存储并统计出候选人得票数.        编程题目2:pytho ...

  7. 简易的视频随机截屏程序

    由于笔者在b站投稿MMD视频时,经常会遇到不知道用什么封面的情况,而b站的随机截屏有时候比我人工截的还好,但有时候也并没好看的.所以我便想自己写一个视频随机截屏的程序.截的多了,总归会有好看的. 然后 ...

  8. 威纶通触摸屏一机多屏程序 威纶通触摸屏一机多屏程序

    威纶通触摸屏一机多屏程序 威纶通触摸屏一机多屏程序,一个FX3U系列PLC,四个MT6051ip触摸屏 功能完善的威纶通系列触摸屏模板,很好的一机多屏案例程序,PLC还跟上位机进行MODBUS通讯, ...

  9. 华为抓截屏_华为手机有6种截屏方法,你都知道几种?

    如今手机成为了我们的伴侣,无论何时何地,手机不离身成为了我们的习惯,大家对手机也是了如指掌.华为手机成为了国产手机的老大哥,受到了很多花粉们的青睐,使用华为手机时,我们会发现手机上有很多种截屏方法,我 ...

最新文章

  1. QB:基于深度学习的病毒序列识别
  2. 史上最烂的项目:苦撑 12 年,600 多万行代码!
  3. lay和lied_lie和lay的区别
  4. 关于css的一些特别用法
  5. js字符串slice_JavaScript子字符串示例-JS中的Slice,Substr和Substring方法
  6. 最短路径和距离及可视化——matlab
  7. 二元函数求最小值 c语言,遗传算法C语言源代码(一元函数和二元函数)
  8. KPI总结模板:What
  9. iOS设置圆角的四种方法
  10. java myqq ui_GitHub - ANDRYHU2020/myqq: Java版SWing“高”仿QQ即时通聊天系统
  11. 计算机桌面文档全丢,电脑重启后桌面文件全部丢失怎么办
  12. FRM P1B3笔记:Introduction to Financial Markets and Products
  13. echarts地图数据过旧,通过geojson自定义经纬度地图
  14. 计算机网络中man是,计算机网络分类为LAN、MAN和()。
  15. html 九宫格头像,JAVA-仿微信九宫格头像
  16. 机器视觉 · 工业光源
  17. 尚硅谷JVM下篇:性能监控与调优篇_03_JVM监控及诊断工具-GUI篇
  18. 汇编语言中xor指令_这个汇编代码有什么作用? (TEST,XOR,JNZ)
  19. 【Android】自定义View和控件时出现Binary XML file line #报错行数: Binary XML file line #9: Error inflating class 类路径
  20. vue : 无法加载文件 C:\Users\xxx\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本

热门文章

  1. android am发送广播,adb shell am broadcast 手动发送广播及adb shell am/pm其他命令
  2. 执行文件的减肥工具strip
  3. Python常用模块—— Colorama模块
  4. avue参数个人总结
  5. S3C2440的架构及启动方式
  6. 专家:雾霾天PM2.5会渗进室内 里外差别不大
  7. 【JS】中遍历数组的几种方法
  8. docker使用buildx构建不同架构镜像
  9. centos minimal mysql_安装centos minimal 版本后安装mysql详细过程(linux)
  10. 与TI的lvds芯片兼容-GM8284DD,GM8285C,GM8913,GM8914,GM8905C,GM8906C,国腾振芯LVDS类芯片,