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

举报
天元浪子 发表于 2021/07/26 22:52:50 2021/07/26
【摘要】 文章目录 1. 前言2. 设计思路3. 源码4. 打包4.1 打包成一个目录4.2 打包成一个文件 1. 前言 最近有同学咨询如何用wx写任务托盘程序,也有同学咨询怎样创建wx的异形窗口。恰好,我也正需要一个可以将屏幕显示或者操作录制成gif文件的工具。于是乎,结合同学们的问题,我用wx写了一个屏幕录像机代码,既包含任务托盘的实现,也用到了异形窗口,...

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

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()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369

4. 打包

4.1 打包成一个目录

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

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

4.2 打包成一个文件

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

文章来源: xufive.blog.csdn.net,作者:天元浪子,版权归原作者所有,如需转载,请联系作者。

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

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。