| 站长开始收学徒,辅导python啦! | 站长答疑 | 本站每日ip已超7000,现出租广告位,位置价格可谈,需要合作请联系站长
+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2019-08(62)

2019-09(107)

2019-10(19)

2019-11(7)

2019-12(18)

2020-01(14)

2020-02(27)

2020-03(82)

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

发布于2020-03-26 11:33     阅读(374)     评论(0)     点赞(1)     收藏(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 mimsave
from icon import get_fp

class 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 config

    def 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 = None
            else:
                self.xy = (evt.x, evt.y)
        elif evt.EventType == wx.EVT_LEFT_UP.evtType[0]: # 左键弹起
            self.xy = None
        elif 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] += dx
                    self.box[2] -= dx
                if 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] += dy
                        self.box[3] -= dy
                elif 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] += dy
                else: # 左边
                    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] += dx
                if 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] += dy
                        self.box[3] -= dy
                elif 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] += dy
                else: # 右边
                    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] += dy
                    self.box[3] -= dy
            elif 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] += dy
            
            if self.box[0] < 0:
                self.box[2] += self.box[0]
                self.box[0] = 0
            if self.box[1] < 0:
                self.box[3] += self.box[1]
                self.box[1] = 0
            
            w, h = self.GetSize()
            if self.box[2] > w:
                self.box[2] = w
            if self.box[3] > h:
                self.box[3] = h
            
            self.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):
        """擦除背景事件处理"""

        pass

    def 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 = True
        self.timer.Start(1000/self.cfg.getint("recoder", "fps")) # 启动定时器
        self.Refresh() # 刷新窗口

    def StopRec(self):
        """停止录制"""
        
        self.timer.Stop() # 停止定时器
        self.recording = False
        self.OnHide(None)

        # 启动生成GIF线程
        t = Thread(target=self.CreateGif)
        t.setDaemon(True)
        t.start()
        
        # 弹出模态的等待对话窗
        count, count_max = 0, 100
        style = wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDE
        dlg = 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 True

if __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,感兴趣的同学,请自行下载。不过,打包成一个文件,启动的时候会非常慢,你得有足够的耐心才能接受。

原文链接:https://blog.csdn.net/xufive/article/details/105089996



所属网站分类: 技术文章 > python文章

作者:坚持才能胜利

链接: https://www.pythonheidong.com/blog/article/285064/

来源:python黑洞网 www.pythonheidong.com

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

1 0

赞一赞 or 踩一踩

收藏该文
已收藏

评论内容:(最多支持255个字符)

最新文章

  Python-Socketserver实现FTP,文件上传、下载

  Python3标准库:xml.etree.ElementTree XML操纵API

  django时区问题

  Python编程 | 新手必会的 9 个 Python 技巧

  PAT (Basic)1004 成绩排名 (Python实现)

  Python小技巧:用类写装饰器的正确方法,一般新手都不知道

  教你用Python 每日定时推送睡前小故事给你__的人

  后端开发使用pycharm的技巧

  Django 对多对多字段进行批量add

  史上最全的Python面向对象知识点疏理

优质资源排行榜

 python经典电子书大合集下载 下载次数 8148

 零基础java开发工程师视频教程全套,基础+进阶+项目实战(152G) 下载次数 7556

 零基础前端开发工程师视频教程全套,基础+进阶+项目实战(共120G) 下载次数 7446

 零基础大数据全套视频400G 下载次数 7009

 零基础php开发工程师视频教程全套,基础+进阶+项目实战(80G) 下载次数 6895

 零基础软件测试全套系统教程 下载次数 6507

 这个项目是人人网的爬虫程序 下载次数 6506

 全套人工智能视频+pdf 下载次数 6446

 IOS全套视频教程 基础班+就业班 下载次数 4681

10  python视频各种视频很多 下载次数 3877

11  编程小白的第一本python入门书(高清版)PDF下载 下载次数 3761

12  effective python编写高质量Python代码的59个有效方法 pdf下载 下载次数 3427

13  Python深度学习 pdf下载 下载次数 3197

14  笨办法学python pdf下载 下载次数 3106

15  Python Cookbook第三版中文PDF下载高清完整扫描原版 下载次数 3044

16  树莓派Python编程指南 pdf下载 下载次数 3028

17  python从入门到精通视频(全60集)python视频教程下载 下载次数 3009

18  Python基础教程 pdf下载 下载次数 3002

19  python项目开发视频 下载次数 3002

20  使用python+pygame开发的小游戏《嗷大喵快跑》源码下载 下载次数 3001

21  黑马2017年java就业班全套视频教程 下载次数 2993

22  Python算法教程_中文版 pdf下载 下载次数 2988

23  python实战项目 平铺图像板系统源码下载,适用于想要保存,标记和共享图像,视频和网页的用户 下载次数 2987

24  利用python实现程序内存监控脚本 下载次数 2987

25  老男孩python自动化视频 下载次数 2983

26  老王python基础+进阶+项目视频教程 下载次数 2975

27  尚硅谷Go学科全套视频 下载次数 2973

28  某硅谷Python项目+AI课程+核心基础视频教程 下载次数 2968

29  Web前端实战精品课程 下载次数 2967

30  tron python小游戏 下载次数 2963

31  [小甲鱼]零基础入门学习Python 下载次数 2962

32  老男孩python全栈开发15期 下载次数 2959

33  2017最新web前端开发完整视频教程附源码 下载次数 2948

34  流畅的Python PDF下载高清完整扫描原版 下载次数 2937

35  最新全套完整JAVAWEB2018开发视频 下载次数 2926

36  Python高性能编程 pdf下载 下载次数 2924

37  Spring boot实战视频6套下载 下载次数 2910

38  python全套视频十五期(116G) 下载次数 2910

39  简明python教程 (A Byte of Python)pdf下载 下载次数 2893

40  利用Python进行数据分析 pdf下载 下载次数 2892

41  Python项目实战 下载次数 2888

42  30个小时搞定Python网络爬虫 含源码 下载次数 2887

43  python全自动抢火车票教程-python视频教程下载 下载次数 2883

44  尚硅谷大数据之Hadoop视频 下载次数 2877

45  python接口测试视频 下载次数 2874

46  Python A~B~C~ python视频教程下载 下载次数 2868

47  全套python量化交易视频教程 下载次数 2866

48  数据结构与算法视频(小甲鱼讲解-全) 下载次数 2864

49  web小程序表白天数倒计时源码下载 下载次数 2863

50  python基础视频教程 下载次数 2862