6.3. 定时器和线程
在一个桌面程序中,GUI线程是主线程,其他线程若要更新显示内容,Tkinter使用的是类型对象,PyQt使用的信号和槽机制,wxPython则相对原始:它允许子线程更新GUI,但需要借助于wx.CallAfter函数。
这个例子里面设计了一个数字式钟表,一个秒表,秒表显示精度十分之一毫秒。从代码设计上来说没有任何难度,实现的方法有很多种,可想要达到一个较好的显示效果,却不是一件容易的事情。请注意体会 wx.CallAfter 的使用条件。
import wx
import time
import threading
class MainFrame(wx.Frame):
"""桌面程序主窗口类"""
def __init__(self):
"""构造函数"""
wx.Frame.__init__(self, parent=None, style=wx.CAPTION|wx.SYSTEM_MENU|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SIMPLE_BORDER)
self.SetTitle('定时器和线程')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((224, 224, 224))
self.SetSize((320, 300))
self._init_ui
self.Center
def _init_ui(self):
"""初始化界面"""
font = wx.Font(30, wx.DECORATIVE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, 'Monaco')
self.clock = wx.StaticText(self, -1, '08:00:00', pos=(50,50), size=(200,50), style=wx.TE_CENTER|wx.SUNKEN_BORDER)
self.clock.SetForegroundColour(wx.Colour(0, 224, 32))
self.clock.SetBackgroundColour(wx.Colour(0, 0, 0))
self.clock.SetFont(font)
self.stopwatch = wx.StaticText(self, -1, '0:00:00.00', pos=(50,150), size=(200,50), style=wx.TE_CENTER|wx.SUNKEN_BORDER)
self.stopwatch.SetForegroundColour(wx.Colour(0, 224, 32))
self.stopwatch.SetBackgroundColour(wx.Colour(0, 0, 0))
self.stopwatch.SetFont(font)
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
self.timer.Start(50)
self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
self.sec_last = None
self.is_start = False
self.t_start = None
thread_sw = threading.Thread(target=self.StopWatchThread)
thread_sw.setDaemon(True)
thread_sw.start
def on_timer(self, evt):
"""定时器函数"""
t = time.localtime
if t.tm_sec != self.sec_last:
self.clock.SetLabel('d:d:d'%(t.tm_hour, t.tm_min, t.tm_sec))
self.sec_last = t.tm_sec
def on_key_down(self, evt):
"""键盘事件函数"""
if evt.GetKeyCode == wx.WXK_SPACE:
self.is_start = not self.is_start
self.t_start= time.time
elif evt.GetKeyCode == wx.WXK_ESCAPE:
self.is_start = False
self.stopwatch.SetLabel('0:00:00.00')
def StopWatchThread(self):
"""线程函数"""
while True:
if self.is_start:
t = time.time - self.t_start
ti = int(t)
wx.CallAfter(self.stopwatch.SetLabel, '%d:d:d.%.02d'%(ti//3600, ti//60, ti`, int((t-ti)*100)))
time.sleep(0.02)
if __name__ == '__main__':
app = wx.App
frame = MainFrame
frame.Show
app.MainLoop
代码运行界面如下图所示。界面上方的时钟一直再跑,下方的秒表则是按键启动或停止。
6.4. DC绘图
DC 是 Device Context 的缩写,字面意思是设备上下文——我一直不能正确理解DC这个中文名字,也找不到更合适的说法,所以,我坚持使用DC而不是设备上下文。DC可以在屏幕上绘制点线面,当然也可以绘制文本和图像。事实上,在底层所有控件都是以位图形式绘制在屏幕上的,这意味着,我们一旦掌握了DC这个工具,就可以自己创造我们想要的控件了
DC有很多种,PaintDC,ClientDC,MemoryDC等。通常,我们可以使用 ClientDC 和 MemoryDC,PaintDC 是发生重绘事件(wx.EVT_PAINT)时系统使用的。使用 ClientDC 绘图时,需要记录绘制的每一步工作,不然,系统重绘时会令我们前功尽弃——这是使用DC最容易犯的错误。
import wx
class MainFrame(wx.Frame):
"""桌面程序主窗口类"""
def __init__(self):
"""构造函数"""
wx.Frame.__init__(self, parent=None, style=wx.CAPTION|wx.SYSTEM_MENU|wx.CLOSE_BOX|wx.MINIMIZE_BOX|wx.SIMPLE_BORDER)
self.SetTitle('使用DC绘图')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((224, 224, 224))
self.SetSize((800, 480))
self._init_ui
self.Center
def _init_ui(self):
"""初始化界面"""
self.palette = wx.Panel(self, -1, style=wx.SUNKEN_BORDER)
self.palette.SetBackgroundColour(wx.Colour(0, 0, 0))
btn_base = wx.Button(self, -1, '文字和图片', size=(100, -1))
sizer_max = wx.BoxSizer
sizer_max.Add(self.palette, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.BOTTOM, 5)
sizer_max.Add(btn_base, 0, wx.ALL, 20)
self.SetAutoLayout(True)
self.SetSizer(sizer_max)
self.Layout
btn_base.Bind(wx.EVT_BUTTON, self.on_base)
self.palette.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse)
self.palette.Bind(wx.EVT_PAINT, self.on_paint)
self.xy = None
self.lines = list
self.img = wx.Bitmap('res/forever.png', wx.BITMAP_TYPE_ANY)
self.update_palette
def on_mouse(self, evt):
"""移动鼠标画线"""
if evt.EventType == 10030: #左键按下
self.xy = (evt.x, evt.y)
elif evt.EventType == 10031: #左键弹起
self.xy = None
elif evt.EventType == 10036: #鼠标移动
if self.xy:
dc = wx.ClientDC(self.palette)
dc.SetPen(wx.Pen(wx.Colour(0,224,0), 2))
dc.DrawLine(self.xy[0], self.xy[1], evt.x, evt.y)
self.lines.append((self.xy[0], self.xy[1], evt.x, evt.y))
self.xy = (evt.x, evt.y)
def on_base(self, evt):
"""DC基本方法演示"""
img = wx.Bitmap('res/forever.png', wx.BITMAP_TYPE_ANY)
w, h = self.palette.GetSize
dc = wx.ClientDC(self.palette)
dc.SetPen(wx.Pen(wx.Colour(224,0,0), 1))
dc.SetBrush(wx.Brush(wx.Colour(0,80,80) ))
dc.DrawRectangle(10,10,w-22,h-22)
dc.DrawLine(10,h/2,w-12,h/2)
dc.DrawBitmap(img, 10, 10)
dc.SetTextForeground(wx.Colour(224,224,224))
dc.SetFont(wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, 'Comic Sans MS'))
dc.DrawText('霜重闲愁起', 100, 360)
dc.DrawRotatedText('春深风也疾', 400, 360, 30)
def on_paint(self, evt):
"""响应重绘事件"""
dc = wx.PaintDC(self.palette)
self.paint(dc)
def update_palette(self):
"""刷新画板"""
dc = wx.ClientDC(self.palette)
self.paint(dc)
def paint(self, dc):
"""绘图"""
dc.Clear
dc.SetPen(wx.Pen(wx.Colour(0,224,0), 2))
for line in self.lines:
dc.DrawLine(line[0],line[1],line[2],line[3])
if __name__ == '__main__':
app = wx.App
frame = MainFrame
frame.Show
app.MainLoop
代码运行界面如下图所示。
6.5. 内嵌浏览器
wx.html2是wxPython扩展模块中封装得最干净漂亮的模块之一,它被设计为允许为每个端口创建多个后端,尽管目前只有一个可用。它与wx.html.HtmlWindow的不同之处在于,每个后端实际上都是一个完整的渲染引擎,MSW上是Trident, macOS和GTK上是Webkit。wx.html2渲染web文档,对于HTML、CSS和javascript都可以有很好的支持。
import wx
import wx.html2 as webview
class MainFrame(wx.Frame):
"""桌面程序主窗口类"""
def __init__(self):
"""构造函数"""
wx.Frame.__init__(self, parent=None)
self.SetTitle('内嵌浏览器')
self.SetIcon(wx.Icon('res/wx.ico', wx.BITMAP_TYPE_ICO))
self.SetBackgroundColour((224, 224, 224))
self.SetSize((800, 480))
self.Center
wv = webview.WebView.New(self)
wv.LoadURL('https://cn.bing.com')
if __name__ == '__main__':
app = wx.App
frame = MainFrame
frame.Show
app.MainLoop
代码运行界面如下图所示。