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

网友投稿 553 2022-05-28

文章目录

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

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

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

Python

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:皇后问题
下一篇:【Linux】LVM的创建及使用
相关文章