有时编写游戏后发博文,为使读者有一个直观的游戏效果,会把游戏运行动画转换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 shelve
def 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,f1
n=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,images
x=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,0
SampleCycle=1/int(fps)
while n==0: #n=1,将退出while循环,线程结束,录屏结束,
start = time.time() #以秒为单位,开始时间。下句是截屏语句
p=ImageGrab.grab((x,y,x1,y1)) #截屏,因PIL的原因,必须将win10显示设置的缩放比例调成100%
m+=1
images.append(p) #将截屏的image类实例保存到列表
end = time.time() #结束时间。在Windows系统中
time.sleep(SampleCycle-(start-end)) #延迟时间取样周期(SampleCycle)-(截屏用去的时间)
saveFile() #保存列表images中所有录屏图像为文件,将覆盖上次录屏数据
reSet() #调用初始化方法
def aTimer(): #在另一线程中的秒表,记录录屏时间
global k,label
k=0
seconds=-1
while 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或2
def __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 images
with 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,scale1
scale1=images[0].height/450 #为了显示大图像,原始图像缩小比例,450是大图允许最大高度
if scale1<1: #比450小,就不用缩小了
scale1=1
scale2=images[0].width/100 #为了显示小图像,原始图像缩小比例,小图允许最大宽度为100
if scale2<1: #比100小,就不用缩小了
scale1=1
canvasM.itemconfig('m1',text=s1+'0') #修改左大图上边显示的帧号
canvasM.itemconfig('m2',text=s2+'1') #修改右大图上边显示的帧号
showBigPic(0,1) #在左大图显示第0帧图像
currenframeN0=0 #当前选中帧号(当前帧)=0
showBigPic(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,scale1
if whichBigPic==1: #whichBigPic=1,左大图
if frameNo>=0:
bigPic1image=reformat(frameNo,scale1) #返回要显示大图,必须保存到全局变量,scale1为缩小倍数
else: #如果帧号小于0,显示空白
bigPic1image=img
canvasM.itemconfig('m3',image=bigPic1image) #在左大图显示指定图像,'m3'为canvas实例tag
currenframeN0=frameNo
elif whichBigPic==2: #whichBigPic=2,右大图
if frameNo>=0:
bigPic2image=reformat(frameNo,scale1)
else: #如果帧号小于0,显示空白
bigPic2image=img
canvasM.itemconfig('m4',image=bigPic2image) #在右大图显示指定图像,'m4'为canvas实例tag
rightBigPicFNo=frameNo
def 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 frameN0
if frameN0<=0:
return
frameN0-=1
showAll(frameN0)
def moveL(event): #12个小图像全部向左移
global shomImage,frameN0,images
if frameN0==len(images)-12:
return
if len(images)<=12:
frameN0=0
return
frameN0+=1
showAll(frameN0)
def showAll(N0): #N0是要显示的起始帧号,从N0开始显示12个小图
global shomImage,img,scale2
shomImage=[] #列表保存12个小图要显示的图像,必须是全局变量
m=12
if 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)) #在指定位置显示帧号,注意通过tag
canvasM.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,frameN0
if currenframeN0>=0:
del images[currenframeN0] #此条语句删除当前选中帧,简称当前帧
if currenframeN0==rightBigPicFNo: #如果右大图也显示此帧,令其显示空,显示帧号为-1
showBigPic(-1,2)
rightBigPicFNo=-1
canvasM.itemconfig('m2',text='')
if currenframeN0<frameN0:#此时frameN0指向的图像帧号将减1,为了使12小图维持不变frameN0-1
if frameN0-1>=0: #帧号不能是负数
frameN0-=1
currenframeN0=-1 #令当前帧为-1,因当前选中帧被删除
showBigPic(-1,1) #令左大图显示空
canvasM.itemconfig('m1',text='')
showAll(frameN0) #刷新所有小图
def delFront(): #该方法删除当前帧之前所有帧
global images,currenframeN0,rightBigPicFNo,frameN0
if currenframeN0>=0: #=-1,没有选则当前帧
images=images[currenframeN0:] #此条语句删除当前帧之前所有帧
if rightBigPicFNo<currenframeN0 and rightBigPicFNo>=0:#第2大图是否被删除?
showBigPic(-1,2) #是,删除第2大图及提示信息
rightBigPicFNo=-1
canvasM.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大图帧号变为0
canvasM.itemconfig('m1',text=s1+'0') #第1大图提示信息中帧号改变
showAll(frameN0) #重新显示12个小图
def delBehind(): #该方法删除当前帧之后所有帧
global images,currenframeN0,rightBigPicFNo,frameN0
if currenframeN0>=0: #=-1,没有选则当前帧
images=images[:currenframeN0+1] #此条语句删除当前帧之后所有帧
if rightBigPicFNo>currenframeN0 and rightBigPicFNo>=0: #第2大图帧号大于当前帧号被删除
showBigPic(-1,2)
rightBigPicFNo=-1
canvasM.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,一定是所有图数<12
frameN0=0
showAll(frameN0)
def change3value(): #该方法修改图像缩放比例、FPS和GIF循环次数
global f2
f2 = 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+', 循环次数='+playNum
f2.destroy()
#s=round(1.234,1) #四舍五入,小数点后保留1位小数
def cancel():
global f2
f2.destroy()
def play():
global f3,images,cv,mainImg
if 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,f3
n9=1
#root.state('normal') #使主窗体正常显示
f3.destroy()
def closef3():
global n9,f3
n9=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 f3
f3.destroy() #线程PlayPic已结束,在关闭f3窗口就不会报错了。
def PlayPic():
global n9,images,fps,img1,cv,playNum
k,n9=0,0
l=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+=1
if k==len(images):
k=0
if int(playNum)!=0: #=0为连续播放
l-=1 #不是连续播放,l是播放次数,播放1次减1
if l==0: #l=0,播放次数完成,退出
return
end = time.time() #结束时间。在Windows系统中
time.sleep(SampleCycle-(start-end)) #延迟时间取样周期(SampleCycle)-(截屏用去的时间)
def saveGIF():
global images,playNum,fps,scaling
fname=tkinter.filedialog.asksaveasfilename(title=u'保存GIF文件',defaultextension='GIF')
if fname=='':
return
img=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 n
ws = 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 n
n=1
def 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()