文章目录

  • 1. 前言
  • 2. 设计思路
  • 3. 源码
  • 4. 打包
    • 4.1 打包成一个目录
    • 4.2 打包成一个文件

1. 前言

最近有同学咨询如何用wx写任务托盘程序,也有同学咨询怎样创建wx的异形窗口。恰好,我也正需要一个可以将屏幕显示或者操作录制成gif文件的工具。于是乎,结合同学们的问题,我用wx写了一个屏幕录像机代码,既包含任务托盘的实现,也用到了异形窗口,还使用了DC绘制录像区域边框。这段代码,可以很方便地打包成exe程序。程序启动后,栖身于任务托盘。你需要的时候,可以随时召唤它。录像区域可以调整大小,生成gif的参数也可以调整,此外还提供了启动/停止的热键(Ctr + F2)操作,使用起来非常方便。

2. 设计思路

程序启动后,创建一个全屏的异形窗口,除了10个像素宽的录像区域边框外,其余部分全部透明。全屏窗口位于最顶层,因为录像区域边框外其他区域透明,所以不会影响我们操作其他窗口。当鼠标进入录像区域边框时,可以拖动边框以改变录像区域的大小。启动录像后,使用pillow的ImageGrab定时捕捉录像区域内的内容,保存在一个列表中;停止录像后,使用imageio模块的mimsave()函数,将保存在列表中的PIL图像序列转存为gif文件。

3. 源码

代码比较简单,我在关键位置都有注释,就不再具体分析了,直接贴出源码。运行代码需要一个图标文件,保存在和脚本文件同级的res目录下。请自备图标文件,或者去GitHub上下载,地址在文末。

# -*- coding:utf-8 -*-import os
import wx
import wx.adv
import wx.lib.filebrowsebutton as filebrowse
from win32con import MOD_CONTROL, VK_F2
from threading import Thread
from datetime import datetime
from configparser import ConfigParser
from PIL import ImageGrab
from imageio import mimsaveclass MainFrame(wx.Frame):"""屏幕录像机主窗口"""MENU_REC  = wx.NewIdRef()        # 开始/停止录制MENU_SHOW   = wx.NewIdRef()      # 显示窗口MENU_HIDE   = wx.NewIdRef()      # 窗口最小化MENU_STOP   = wx.NewIdRef()      # 停止录制MENU_CONFIG = wx.NewIdRef()      # 设置MENU_FOLFER = wx.NewIdRef()      # 打开输出目录MENU_EXIT   = wx.NewIdRef()      # 退出def __init__(self, parent):wx.Frame.__init__(self, parent, -1, "", style=wx.FRAME_SHAPED|wx.FRAME_NO_TASKBAR|wx.STAY_ON_TOP)x, y, w, h = wx.ClientDisplayRect() # 屏幕显示区域x0, y0 = (w-820)//2, (h-620)//2 # 录像窗口位置(默认大小820x620,边框10像素)self.SetPosition((0, 0)) # 无标题窗口最大化:设置位置self.SetSize((w, h)) # 无标题窗口最大化:设置大小self.SetDoubleBuffered(True) # 启用双缓冲self.taskBar = wx.adv.TaskBarIcon()  # 添加系统托盘self.taskBar.SetIcon(wx.Icon(os.path.join("res", "recorder.ico"), wx.BITMAP_TYPE_ICO), "屏幕录像机")self.box = [x0, y0, 820, 620]       # 屏幕录像窗口大小self.xy = None                      # 鼠标左键按下的位置self.recording = False              # 正在录制标志self.saveing = False                # 正在生成GIF标志self.imgs = list()                  # 每帧的图片列表self.timer = wx.Timer(self)         # 创建录屏定时器self.cfg = self.ReadConfig()        # 读取配置项self.SetWindowShape()               # 设置不规则窗口self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)                                # 鼠标事件self.Bind(wx.EVT_PAINT, self.OnPaint)                                       # 窗口重绘self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBG)                          # 擦除背景self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)                           # 定时器self.taskBar.Bind(wx.adv.EVT_TASKBAR_RIGHT_UP, self.OnTaskBar)              # 右键单击托盘图标self.taskBar.Bind(wx.adv.EVT_TASKBAR_LEFT_UP, self.OnTaskBar)               # 左键单击托盘图标self.taskBar.Bind(wx.adv.EVT_TASKBAR_LEFT_DCLICK, self.OnTaskBar)           # 左键双击托盘图标self.taskBar.Bind(wx.EVT_MENU, self.OnRec, id=self.MENU_REC)                # 开始/停止录制self.taskBar.Bind(wx.EVT_MENU, self.OnShow, id=self.MENU_SHOW)              # 显示窗口self.taskBar.Bind(wx.EVT_MENU, self.OnHide, id=self.MENU_HIDE)              # 隐藏窗口self.taskBar.Bind(wx.EVT_MENU, self.OnOpenFolder, id=self.MENU_FOLFER)      # 打开输出目录self.taskBar.Bind(wx.EVT_MENU, self.OnConfig, id=self.MENU_CONFIG)          # 设置self.taskBar.Bind(wx.EVT_MENU, self.OnExit, id=self.MENU_EXIT)              # 退出self.RegisterHotKey(self.MENU_REC, MOD_CONTROL,  VK_F2)                     # 注册热键self.Bind(wx.EVT_HOTKEY, self.OnRec, id=self.MENU_REC)                      # 开始/停止录制热键def ReadConfig(self):"""读取配置文件"""config = ConfigParser()if os.path.isfile("recorder.ini"):config.read("recorder.ini")else:out_path = os.path.join(os.path.split(os.path.realpath(__file__))[0], 'out')if not os.path.exists(out_path):os.mkdir(out_path)config.read_dict({"recoder":{"fps":10, "frames":100, "loop":0, "outdir":out_path}})config.write(open("recorder.ini", "w"))return configdef SetWindowShape(self):"""设置窗口形状"""path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath()path.AddRectangle(self.box[0], self.box[1], self.box[2], 10)path.AddRectangle(self.box[0], self.box[1]+self.box[3]-10, self.box[2], 10)path.AddRectangle(self.box[0], self.box[1]+10, 10, self.box[3]-2*10)path.AddRectangle(self.box[0]+self.box[2]-10, self.box[1]+10, 10, self.box[3]-2*10)self.SetShape(path) # 设置异形窗口形状def OnMouse(self, evt):"""鼠标事件"""if evt.EventType == wx.EVT_LEFT_DOWN.evtType[0]: # 左键按下if self.box[0]+10 <= evt.x <= self.box[0]+self.box[2]-10 and self.box[1]+10 <= evt.y <= self.box[1]+self.box[3]-10:self.xy = Noneelse:self.xy = (evt.x, evt.y)elif evt.EventType == wx.EVT_LEFT_UP.evtType[0]: # 左键弹起self.xy = Noneelif evt.EventType == wx.EVT_MOTION.evtType[0]:  # 鼠标移动if self.box[0] < evt.x < self.box[0]+10:if evt.LeftIsDown() and self.xy:dx, dy = evt.x-self.xy[0], evt.y-self.xy[1]self.box[0] += dxself.box[2] -= dxif self.box[1] < evt.y < self.box[1]+10: # 左上角self.SetCursor(wx.Cursor(wx.CURSOR_SIZENWSE))if evt.LeftIsDown() and self.xy:self.box[1] += dyself.box[3] -= dyelif evt.y > self.box[1]+self.box[3]-10: # 左下角self.SetCursor(wx.Cursor(wx.CURSOR_SIZENESW))if evt.LeftIsDown() and self.xy:self.box[3] += dyelse: # 左边self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE))elif self.box[0]+self.box[2]-10 < evt.x < self.box[0]+self.box[2]:if evt.LeftIsDown() and self.xy:dx, dy = evt.x-self.xy[0], evt.y-self.xy[1]self.box[2] += dxif self.box[1] < evt.y < self.box[1]+10: # 右上角self.SetCursor(wx.Cursor(wx.CURSOR_SIZENESW))if evt.LeftIsDown() and self.xy:self.box[1] += dyself.box[3] -= dyelif evt.y > self.box[1]+self.box[3]-10: # 右下角self.SetCursor(wx.Cursor(wx.CURSOR_SIZENWSE))if evt.LeftIsDown() and self.xy:self.box[3] += dyelse: # 右边self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE))elif self.box[1] < evt.y < self.box[1]+10: # 上边self.SetCursor(wx.Cursor(wx.CURSOR_SIZENS))if evt.LeftIsDown() and self.xy:dx, dy = evt.x-self.xy[0], evt.y-self.xy[1]self.box[1] += dyself.box[3] -= dyelif self.box[1]+self.box[3]-10 < evt.y < self.box[1]+self.box[3]: #下边self.SetCursor(wx.Cursor(wx.CURSOR_SIZENS))if evt.LeftIsDown() and self.xy:dx, dy = evt.x-self.xy[0], evt.y-self.xy[1]self.box[3] += dyif self.box[0] < 0:self.box[2] += self.box[0]self.box[0] = 0if self.box[1] < 0:self.box[3] += self.box[1]self.box[1] = 0w, h = self.GetSize()if self.box[2] > w:self.box[2] = wif self.box[3] > h:self.box[3] = hself.xy = (evt.x, evt.y)self.isFullScreen = self.GetSize() == (self.box[2],self.box[3])self.SetWindowShape()self.Refresh()def OnPaint(self, evt):"""窗口重绘事件处理"""dc = wx.PaintDC(self)dc.SetBrush(wx.RED_BRUSH if self.recording else wx.GREEN_BRUSH)w, h = self.GetSize()dc.DrawRectangle(*self.box,)def OnEraseBG(self, evt):"""擦除背景事件处理"""passdef OnTaskBar(self, evt):"""托盘图标操作事件处理"""menu = wx.Menu()menu.Append(self.MENU_REC, "开始/停止(Ctrl+F2)")menu.AppendSeparator()if self.IsIconized():menu.Append(self.MENU_SHOW, "显示屏幕录像窗口")else:menu.Append(self.MENU_HIDE, "最小化至任务托盘")menu.AppendSeparator()menu.Append(self.MENU_FOLFER, "打开输出目录")menu.Append(self.MENU_CONFIG, "设置录像参数")menu.AppendSeparator()menu.Append(self.MENU_EXIT, "退出")if self.recording:menu.Enable(self.MENU_CONFIG, False)menu.Enable(self.MENU_EXIT, False)else:menu.Enable(self.MENU_CONFIG, True)menu.Enable(self.MENU_EXIT, True)self.taskBar.PopupMenu(menu)menu.Destroy()def OnShow(self, evt):"""显示窗口"""self.Iconize(False)def OnHide(self, evt):"""隐藏窗口"""self.Iconize(True)def OnRec(self, evt):"""开始/停止录制菜单事件处理"""if self.recording: # 停止录制self.StopRec()else: # 开始录制self.StartRec()def StartRec(self):"""开始录制"""self.OnShow(None)self.recording = Trueself.timer.Start(1000/self.cfg.getint("recoder", "fps")) # 启动定时器self.Refresh() # 刷新窗口def StopRec(self):"""停止录制"""self.timer.Stop() # 停止定时器self.recording = Falseself.OnHide(None)# 启动生成GIF线程t = Thread(target=self.CreateGif)t.setDaemon(True)t.start()# 弹出模态的等待对话窗count, count_max = 0, 100style = wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDEdlg = wx.ProgressDialog("生成GIF", "共计%d帧,正在渲染,请稍候..."%len(self.imgs), parent=self, style=style)while self.saveing and count < count_max:dlg.Pulse()wx.MilliSleep(100)dlg.Destroy() # 关闭等待生成GIF结束的对话窗self.OnOpenFolder(None) # 打开动画文件保存路径def OnOpenFolder(self, evt):"""打开输出目录"""outdir = self.cfg.get("recoder", "outdir")os.system("explorer %s" % outdir)def OnConfig(self, evt):"""设置菜单事件处理"""dlg = ConfigDlg(self,self.cfg.getint("recoder", "fps"),self.cfg.getint("recoder", "frames"),self.cfg.getint("recoder", "loop"),self.cfg.get("recoder", "outdir"))if dlg.ShowModal() == wx.ID_OK:self.cfg.set("recoder", "fps", str(dlg.fps.GetValue()))self.cfg.set("recoder", "frames", str(dlg.frames.GetValue()))self.cfg.set("recoder", "loop", str(dlg.loop.GetValue()))self.cfg.set("recoder", "outdir", dlg.GetOutDir())self.cfg.write(open("recorder.ini", "w"))dlg.Destroy() # 销毁设置对话框def OnExit(self, evt):"""退出菜单事件处理"""self.taskBar.RemoveIcon() # 从托盘删除图标self.Destroy()wx.Exit()def OnTimer(self, evt):"""定时器事件处理:截图"""img = ImageGrab.grab((self.box[0]+10, self.box[1]+10, self.box[0]+self.box[2]-10, self.box[1]+self.box[3]-10))self.imgs.append(img)if len(self.imgs) >= self.cfg.getint("recoder", "frames"):self.StopRec()def CreateGif(self):"""生成gif动画线程"""self.saveing = True # 生成gif动画开始dt = datetime.now().strftime("%Y%m%d%H%M%S")filePath = os.path.join(self.cfg.get("recoder", "outdir"), "%s.gif"%dt)fps = self.cfg.getint("recoder", "fps")loop = self.cfg.getint("recoder", "loop")mimsave(filePath, self.imgs, format='GIF', fps=fps, loop=loop)self.imgs = list() # 清空截屏记录self.saveing = False # 生成gif动画结束class ConfigDlg(wx.Dialog):"""录像参数设置窗口"""def __init__(self, parent, fps, frames, loop, outdir):"""ConfigDlg的构造函数"""wx.Dialog.__init__(self, parent, -1, "设置录像参数", size=(400, 270))sizer = wx.BoxSizer() # 创建布局管理器grid = wx.GridBagSizer(10, 10)subgrid = wx.GridBagSizer(10, 10)text = wx.StaticText(self, -1, "帧率:")grid.Add(text, (0, 0), flag=wx.ALIGN_RIGHT|wx.TOP, border=3)self.fps = wx.SpinCtrl(self, -1, size=(80,-1))self.fps.SetValue(fps)grid.Add(self.fps, (0, 1), flag=wx.LEFT, border=8)text = wx.StaticText(self, -1, "最大帧数")grid.Add(text, (1, 0), flag=wx.ALIGN_RIGHT|wx.TOP, border=3)self.frames = wx.SpinCtrl(self, -1, size=(80,-1))self.frames.SetValue(frames)grid.Add(self.frames, (1, 1), flag=wx.LEFT, border=8)text = wx.StaticText(self, -1, "循环次数")grid.Add(text, (2, 0), flag=wx.ALIGN_RIGHT|wx.TOP, border=3)self.loop = wx.SpinCtrl(self, -1, size=(80,-1))self.loop.SetValue(loop)grid.Add(self.loop, (2, 1), flag=wx.LEFT, border=8)text = wx.StaticText(self, -1, "输出目录")grid.Add(text, (3, 0), flag=wx.TOP, border=8)self.outdir = filebrowse.DirBrowseButton(self, -1, labelText="", startDirectory=outdir, buttonText="浏览", toolTip="请选择输出路径")self.outdir.SetValue(outdir)grid.Add(self.outdir, (3, 1), flag=wx.EXPAND, border=0)okBtn = wx.Button(self, wx.ID_OK, "确定")subgrid.Add(okBtn, (0, 0), flag=wx.ALIGN_RIGHT)canelBtn = wx.Button(self, wx.ID_CANCEL, "取消")subgrid.Add(canelBtn, (0, 1))grid.Add(subgrid, (4, 0), (1, 2), flag=wx.ALIGN_CENTER|wx.TOP, border=10)grid.AddGrowableCol(1)sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 20)self.SetSizer(sizer)self.Layout()self.CenterOnScreen()class MainApp(wx.App):def OnInit(self):self.SetAppName("Hello World")self.frame = MainFrame(None)self.frame.Show()return Trueif __name__ == '__main__':app = MainApp()app.MainLoop()

4. 打包

4.1 打包成一个目录

假定当前路径为脚本文件所在路径,图标文件已经保存当前路径下的res文件夹中。在当前路径下运行下面这个命令,即可生成一个dist文件夹,里面的ScreenGIF文件夹就是可以用来分发的屏幕录像机项目。

pyinstaller -D ScreenGIF.py -i res\recorder.ico -w --add-data “res;res”

4.2 打包成一个文件

要将代码打包成一个可执行文件,需要将图标等资源文件写到代码中。我已将将代码传至GitHub,感兴趣的同学,请自行下载。不过,打包成一个文件,启动的时候会非常慢,你得有足够的耐心才能接受。

我用370行代码写了一个wxPython的任务托盘程序:实用的屏幕录像机相关推荐

  1. windows bat脚本编写_怎样在 txt 中用 2 行代码写出一个锁屏休息提醒脚本?

    锁屏 面对电脑,沉浸在工作中,往往忘记休息.长此以往,脊椎肯定受不了,造成驼背,对体态不好. 何不写一个小脚本,40 分钟锁一次屏,暗示你应该休息放松一下了? 新建 .txt 文档,命名随意,但后缀改 ...

  2. 50行代码写的一个插件,破解一个H5小游戏

    小游戏链接:测测你的眼睛对色差的辨识度http://www.webhek.com/post/color-test.html?from=timeline 废话不多说,先放代码: window.onloa ...

  3. 真香!如何用300行代码写完一个Spring基本框架?

    Spring是一个非侵入性(non-invasive)框架,其目标是使应用程序代码对框架的依赖最小化,应用代码可以在没有Spring或者其他容器的情况下运行. 看spring的源码,学习spring, ...

  4. [教你做小游戏] 用86行代码写一个联机五子棋WebSocket后端

    我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者HullQin授权.我独立开发了<联机桌游合集>,是个网页,可以很方便的跟朋友联机 ...

  5. 直击面试现场:神级程序员仅100秒,60行代码写出俄罗斯方块,成为全公司焦点!...

    小编我今天逛论坛看到一位HR帖子直播公司面试情况,该公司是做棋牌APP的,现在正在招聘前端工程师和运营人员,HR直播的是前端这块的,有写游戏的也有做网站项目的,写特效的都有很多,但是这位HR却看上了一 ...

  6. Python将一行代码写成多行和把多行代码写成一行的方法

    一行代码写成多行 1.使用反斜杠连接 a, b ,c = 1, \2, 3 2.直接换行使用小括号连接 a, b ,c = (1,2, 3) 3.对于字符串使用三个单引号 a = '''1 + 2 + ...

  7. 20行代码来生成一个自己的素描图片

    20行代码来生成一个自己的素描图片 你知道用代码生成自己的一个图片素描吗?今天我们就用20行代码来生成一个自己的素描图片. 现在让我们开始吧 第一步: 导入python模块 from PIL impo ...

  8. sand java_我在sand用java写了一个读取wifi密码的程序,该方法由主方法调用,运行没问题,效果也正常,...

    我在sand用java写了一个读取wifi密码的程序,该方法由主方法调用,运行没问题,效果也正常,就是无法连续调用,就是如果调用一次就退出然后再运行就可以,如果用完了,再主方法里选择... 我在san ...

  9. 50行代码实现的艺术签名设计微信小程序,轻松对接公众号,涨粉神器,学习赚钱两不误.微信公众号引流工具.html,python学习小项目.艺术签名设计微信小程序,前端学习小项目有趣的项目

    50行代码实现的艺术签名设计微信小程序,轻松对接公众号,涨粉神器,学习赚钱两不误 先看效果 这个小程序实现艺术签名设计的功能 对接到公众号之后,相当于给你的公众号添加了一个功能,别人关注公众号后,可以 ...

最新文章

  1. 数据库路由中间件MyCat - 源代码篇(7)
  2. 调用链路_全链路日志分析解决方案介绍
  3. js基础练习---图片无缝左右滚动效果(主要以复制删除为主)
  4. ECstore报表不显示解决
  5. 遇到一个把.o文件strip后出现的奇怪问题
  6. MyBatis中的一级缓存和二级缓存介绍
  7. JSK-369 字符逆序【入门】
  8. qt制作刻度条(可用作时间及其他刻度)实现缩放,以及平移
  9. 泰凌TLSR8266 BLE灯控 智能家居模块
  10. win版跳过id锁工具_实测:一键跳过苹果锁,免费
  11. HTML页面背景图片平铺
  12. 读书感受 之 《写给年轻人的 经济学故事书》
  13. 曼哈顿距离最小生成树(树状数组)
  14. hbuilder运行uniapp,微信开发者工具打开但没有运行项目
  15. Datawhale 数据挖掘新手入门笔记 -Task5 模型融合
  16. 带省略号的比喻句_带有省略号的句子
  17. 免费电子面单Api_快递鸟接口JAVA对接调用案例
  18. 联想高管加盟乐视,手机市场风云突变
  19. XJOI 数组-炸弹
  20. MySQL 自增 ID 超大问题查询

热门文章

  1. 计算机知识竞赛口号,关于知识竞赛的口号大全
  2. 让Wi-Fi变快的更换路由器教程
  3. 运用java--模拟实现保皇发牌功能
  4. AMD CPU Ryzen R7 2700X 开启虚拟化 VT
  5. Matlab系列之运算符和标点符号的功能介绍
  6. c++:内联函数详解和普通函数的区别
  7. 华为服务器的开关位置,手机服务器开关在哪
  8. java保留小数点后两位(小数点保留两位方法)
  9. Windows 远程桌面剪切板失效
  10. Java编程常用英语汇总,让你写程序不再困难