转自:http://thisis.yorven.site/blog/index.php/2017/10/11/wxpython-tips/

本节,我们将讲解一些 wxPython 有趣的技巧。

交互按钮

当我们用鼠标进入到按钮区域时, wx.EVT_ENTER_WINDOW 事件将被触发。类似的,当鼠标离开按钮时,wx.EVT_LEAVE_WINDOW 也会被触发。我们对这两个事件进行绑定。

#!/usr/bin/python
# -*- coding: utf-8 -*-

'''
translated by achen @2017/10
===
ZetCode wxPython tutorial

This example shows an interactive button.

author: Jan Bodnar
website: www.zetcode.com
last modified: September 2011
'''

import wx
from wx.lib.buttons import GenButton

class Example(wx.Frame):

def __init__(self, *args, **kwargs):
super(Example, self).__init__(*args, **kwargs)

self.InitUI()

def InitUI(self):

panel = wx.Panel(self)

btn = GenButton(panel, label='Button',
pos=(100, 100))
btn.SetBezelWidth(1)
btn.SetBackgroundColour('DARKGREY')

wx.EVT_ENTER_WINDOW(btn, self.OnEnter)
wx.EVT_LEAVE_WINDOW(btn, self.OnLeave)

self.SetSize((300, 200))
self.SetTitle('Interactive button')
self.Centre()
self.Show(True)

def OnEnter(self, e):

btn = e.GetEventObject()
btn.SetBackgroundColour('GREY79')
btn.Refresh()

def OnLeave(self, e):

btn = e.GetEventObject()
btn.SetBackgroundColour('DARKGREY')
btn.Refresh()

def main():

ex = wx.App()
Example(None)
ex.MainLoop()


if __name__ == '__main__':
main()

我们用了 GenButton 代替了基础的 wx.Button。

from wx.lib.buttons import GenButton

GenButton 位于 wx.lib.buttons 模块。

btn.SetBezelWidth(1)

SetBezelWidth() 方法会创建 3D 效果。

def OnEnter(self, e):

btn = e.GetEventObject()
btn.SetBackgroundColour('GREY79')
btn.Refresh()

当鼠标移至按钮时,我们修改按钮的背景色。

Isabelle

当应用出错时,一般会弹出一个错误对话框,这可能会有些讨厌。SAP 系统中有一种更好的解决办法:当用户输入无效命令时,状态栏会变红,并输出错误信息。红色比较吸引眼球,用户可以很容易的读取错误信息。下面的代码模拟了该场景。

#!/usr/bin/python

# Isabelle

import wx

ID_TIMER = 1
ID_EXIT = 2
ID_ABOUT = 3
ID_BUTTON = 4

class Isabelle(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)

self.timer = wx.Timer(self, ID_TIMER)
self.blick = 0

file = wx.Menu()
file.Append(ID_EXIT, '&Quit\tCtrl+Q', 'Quit Isabelle')

help = wx.Menu()
help.Append(ID_ABOUT, '&About', 'O Programe')


menubar = wx.MenuBar()
menubar.Append(file, '&File')
menubar.Append(help, '&Help')
self.SetMenuBar(menubar)

toolbar = wx.ToolBar(self, -1)
self.tc = wx.TextCtrl(toolbar, -1, size=(100, -1))
btn = wx.Button(toolbar, ID_BUTTON, 'Ok', size=(40, 28))

toolbar.AddControl(self.tc)
toolbar.AddSeparator()
toolbar.AddControl(btn)
toolbar.Realize()
self.SetToolBar(toolbar)

self.Bind(wx.EVT_BUTTON, self.OnLaunchCommandOk, id=ID_BUTTON)
self.Bind(wx.EVT_MENU, self.OnAbout, id=ID_ABOUT)
self.Bind(wx.EVT_MENU, self.OnExit, id=ID_EXIT)
self.Bind(wx.EVT_TIMER, self.OnTimer, id=ID_TIMER)

self.panel = wx.Panel(self, -1, (0, 0), (500 , 300))
self.panel.SetBackgroundColour('GRAY')
self.sizer=wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.panel, 1, wx.EXPAND)
self.SetSizer(self.sizer)
self.statusbar = self.CreateStatusBar()
self.statusbar.SetStatusText('Welcome to Isabelle')
self.Centre()
self.Show(True)

def OnExit(self, event):
dlg = wx.MessageDialog(self, 'Are you sure to quit Isabelle?',
'Please Confirm', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_YES:
self.Close(True)


def OnAbout(self, event):
dlg = wx.MessageDialog(self, 'Isabelle\t\n' '2004\t', 'About',
wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()


def OnLaunchCommandOk(self, event):
input = self.tc.GetValue()
if input == '/bye':
self.OnExit(self)
elif input == '/about':
self.OnAbout(self)
elif input == '/bell':
wx.Bell()
else:
self.statusbar.SetBackgroundColour('RED')
self.statusbar.SetStatusText('Unknown Command')
self.statusbar.Refresh()
self.timer.Start(50)

self.tc.Clear()

def OnTimer(self, event):
self.blick = self.blick + 1
if self.blick == 25:
self.statusbar.SetBackgroundColour('#E0E2EB')
self.statusbar.Refresh()
self.timer.Stop()
self.blick = 0

app = wx.App()
Isabelle(None, -1, 'Isabelle')
app.MainLoop()

在状态栏有一个 wx.TextCtrl 控件,可以接受输入命令。我们定义了 3 个命令: /bye, /about, /beep。如果输错了,状态栏会变红,并输入错误。这些是通过 wx.Timeer 类完成的。
Isabelle

Undo/Redo 框架

许多应用允许用户撤销、反撤销他们的操作,下面的例子展示了如何在 wxPython 中完成这样的功能。
Undo/Redo

#!/usr/bin/python

# undoredo.py

from wx.lib.sheet import *
import wx

stockUndo = []
stockRedo = []

ID_QUIT = 10
ID_UNDO = 11
ID_REDO = 12
ID_EXIT = 13

ID_COLSIZE = 80
ID_ROWSIZE = 20


class UndoText:
def __init__(self, sheet, text1, text2, row, column):
self.RedoText = text2
self.row = row
self.col = column
self.UndoText = text1
self.sheet = sheet

def undo(self):
self.RedoText = self.sheet.GetCellValue(self.row, self.col)
if self.UndoText == None:
self.sheetSetCellValue('')
else: self.sheet.SetCellValue(self.row, self.col, self.UndoText)

def redo(self):
if self.RedoText == None:
self.sheet.SetCellValue('')
else: self.sheet.SetCellValue(self.row, self.col, self.RedoText)

class UndoColSize:
def __init__(self, sheet, position, size):
self.sheet = sheet
self.pos = position
self.RedoSize = size
self.UndoSize = ID_COLSIZE

def undo(self):
self.RedoSize = self.sheet.GetColSize(self.pos)
self.sheet.SetColSize(self.pos, self.UndoSize)
self.sheet.ForceRefresh()

def redo(self):
self.UndoSize = ID_COLSIZE
self.sheet.SetColSize(self.pos, self.RedoSize)
self.sheet.ForceRefresh()

class UndoRowSize:
def __init__(self, sheet, position, size):
self.sheet = sheet
self.pos = position
self.RedoSize = size
self.UndoSize = ID_ROWSIZE

def undo(self):
self.RedoSize = self.sheet.GetRowSize(self.pos)
self.sheet.SetRowSize(self.pos, self.UndoSize)
self.sheet.ForceRefresh()

def redo(self):
self.UndoSize = ID_ROWSIZE
self.sheet.SetRowSize(self.pos, self.RedoSize)
self.sheet.ForceRefresh()

class MySheet(CSheet):
instance = 0
def __init__(self, parent):
CSheet.__init__(self, parent)
self.SetRowLabelAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
self.text = ''

def OnCellChange(self, event):
toolbar = self.GetParent().toolbar
if (toolbar.GetToolEnabled(ID_UNDO) == False):
toolbar.EnableTool(ID_UNDO, True)
r = event.GetRow()
c = event.GetCol()
text = self.GetCellValue(r, c)
# self.text - text before change
# text - text after change
undo = UndoText(self, self.text, text, r, c)
stockUndo.append(undo)

if stockRedo:
# this might be surprising, but it is a standard behaviour
# in all spreadsheets
del stockRedo[:]
toolbar.EnableTool(ID_REDO, False)

def OnColSize(self, event):
toolbar = self.GetParent().toolbar

if (toolbar.GetToolEnabled(ID_UNDO) == False):
toolbar.EnableTool(ID_UNDO, True)

pos = event.GetRowOrCol()
size = self.GetColSize(pos)
undo = UndoColSize(self, pos, size)
stockUndo.append(undo)

if stockRedo:
del stockRedo[:]
toolbar.EnableTool(ID_REDO, False)

def OnRowSize(self, event):
toolbar = self.GetParent().toolbar
if (toolbar.GetToolEnabled(ID_UNDO) == False):
toolbar.EnableTool(ID_UNDO, True)

pos = event.GetRowOrCol()
size = self.GetRowSize(pos)
undo = UndoRowSize(self, pos, size)

stockUndo.append(undo)
if stockRedo:
del stockRedo[:]
toolbar.EnableTool(ID_REDO, False)

class Newt(wx.Frame):
def __init__(self,parent,id,title):
wx.Frame.__init__(self, parent, -1, title, size=(550, 500))

box = wx.BoxSizer(wx.VERTICAL)
menuBar = wx.MenuBar()
menu = wx.Menu()
quit = wx.MenuItem(menu, ID_QUIT, '&Quit\tCtrl+Q', 'Quits Newt')
quit.SetBitmap(wx.Bitmap('icons/exit16.png'))
menu.AppendItem(quit)
menuBar.Append(menu, '&File')
self.Bind(wx.EVT_MENU, self.OnQuitNewt, id=ID_QUIT)
self.SetMenuBar(menuBar)


self.toolbar = wx.ToolBar(self, id=-1, style=wx.TB_HORIZONTAL | wx.NO_BORDER |
wx.TB_FLAT | wx.TB_TEXT)
self.toolbar.AddSimpleTool(ID_UNDO, wx.Bitmap('icons/undo.png'),
'Undo', '')
self.toolbar.AddSimpleTool(ID_REDO, wx.Bitmap('icons/redo.png'),
'Redo', '')
self.toolbar.EnableTool(ID_UNDO, False)

self.toolbar.EnableTool(ID_REDO, False)
self.toolbar.AddSeparator()
self.toolbar.AddSimpleTool(ID_EXIT, wx.Bitmap('icons/exit.png'),
'Quit', '')
self.toolbar.Realize()
self.toolbar.Bind(wx.EVT_TOOL, self.OnUndo, id=ID_UNDO)
self.toolbar.Bind(wx.EVT_TOOL, self.OnRedo, id=ID_REDO)
self.toolbar.Bind(wx.EVT_TOOL, self.OnQuitNewt, id=ID_EXIT)

box.Add(self.toolbar, border=5)
box.Add((5,10), 0)

self.SetSizer(box)
self.sheet1 = MySheet(self)
self.sheet1.SetNumberRows(55)
self.sheet1.SetNumberCols(25)

for i in range(self.sheet1.GetNumberRows()):
self.sheet1.SetRowSize(i, ID_ROWSIZE)

self.sheet1.SetFocus()
box.Add(self.sheet1, 1, wx.EXPAND)
self.CreateStatusBar()
self.Centre()
self.Show(True)

def OnUndo(self, event):
if len(stockUndo) == 0:
return

a = stockUndo.pop()
if len(stockUndo) == 0:
self.toolbar.EnableTool(ID_UNDO, False)

a.undo()
stockRedo.append(a)
self.toolbar.EnableTool(ID_REDO, True)

def OnRedo(self, event):
if len(stockRedo) == 0:
return

a = stockRedo.pop()
if len(stockRedo) == 0:
self.toolbar.EnableTool(ID_REDO, False)

a.redo()
stockUndo.append(a)

self.toolbar.EnableTool(ID_UNDO, True)

def OnQuitNewt(self, event):
self.Close(True)

app = wx.App()
Newt(None, -1, 'Newt')
app.MainLoop()

stockUndo = []
stockRedo = []

有两个 list 对象。stockUndo 存储所有我们可以撤销的操作, stockRedo 存储所有可以反撤销的操作。改变的内容实例化至 UndoText 对象,该对象有两个方法: undo 和 redo。

class MySheet(CSheet):
def __init__(self, parent):
CSheet.__init__(self, parent)

我们的例子继承自 CSheet 类,它是一个有一些额外逻辑功能的 grid 控件。

self.SetRowLabelAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)

我们对 label 做了居中对齐。

r = event.GetRow()
c = event.GetCol()
text = self.GetCellValue(r, c)
# self.text - text before change
# text - text after change
undo = UndoText(self, self.text, text, r, c)
stockUndo.append(undo)

每次我们进行修改时,都会创建一个 UndoText 对象,被附加到stockUndo list中。

if stockRedo:
# this might be surprising, but it is a standard behaviour
# in all spreadsheets
del stockRedo[:]
toolbar.EnableTool(ID_REDO, False)

基本上,如果我们撤销了一些修改,然后重新输入,所有的 redo 修改都会丢失,OpenOffice Calc 是这样的,Gnumeric 也一样。

if len(stockUndo) == 0:
self.toolbar.EnableTool(ID_UNDO, False)
...
self.toolbar.EnableTool(ID_REDO, True)

undo 和 redo 按钮各自 enable 或 disable,比如,如果没有什么可以撤销的,那撤销按钮就被禁用。

a = stockUndo.pop()
if len(stockUndo) == 0:
self.toolbar.EnableTool(ID_UNDO, False)

a.undo()
stockRedo.append(a)

如果我们点击 undo,我们会从 stockUndo list 中 pop 一个 UndoText 对象,然后调用 undo(),并将对象附加到 stockRedo list。

应用配置设置

很多应用允许用户配置他们的设置,可以开关工具栏、改变字体、修改默认下载路径等等。多数情况下,他们有一个菜单项叫做 preferences。应用设置被存储在硬盘中,所以用户不需要每次打开应用的时候都修改设置。

在 wxPython 中,我们有 wx.Config 来完成这些事情。

在 Linux 中,设置被存储在隐藏文件中,该文件默认位于主文件夹下,当然路径是可以修改的。文件名在 wx.Config 的构造函数中可以修改。在下面的例子中,我们可以配置窗口的大小。如果没有配置文件,高和宽会被默认配置为 250px。我们可以设置从 200 到 500px,保存后重启程序将看到效果。

#!/usr/bin/python

# myconfig.py

import wx

class MyConfig(wx.Frame):
def __init__(self, parent, id, title):
self.cfg = wx.Config('myconfig')
if self.cfg.Exists('width'):
w, h = self.cfg.ReadInt('width'), self.cfg.ReadInt('height')
else:
(w, h) = (250, 250)
wx.Frame.__init__(self, parent, id, title, size=(w, h))

wx.StaticText(self, -1, 'Width:', (20, 20))
wx.StaticText(self, -1, 'Height:', (20, 70))
self.sc1 = wx.SpinCtrl(self, -1, str(w), (80, 15), (60, -1), min=200, max=500)
self.sc2 = wx.SpinCtrl(self, -1, str(h), (80, 65), (60, -1), min=200, max=500)
wx.Button(self, 1, 'Save', (20, 120))

self.Bind(wx.EVT_BUTTON, self.OnSave, id=1)
self.statusbar = self.CreateStatusBar()
self.Centre()
self.Show(True)

def OnSave(self, event):
self.cfg.WriteInt("width", self.sc1.GetValue())
self.cfg.WriteInt("height", self.sc2.GetValue())
self.statusbar.SetStatusText('Configuration saved, %s ' % wx.Now())


app = wx.App()
MyConfig(None, -1, 'myconfig.py')
app.MainLoop()

我们在代码中获取到配置文件的内容,包括两个键值对。

$ cat .myconfig
height=230
width=350

MyConfig

鼠标手势

鼠标手势结合鼠标动作和点击来完成特定任务。我们可以在 FireFox 或者 Opera 中看到鼠标手势的应用,可以帮我们节省很多时间。在 wxPython 中,可以使用 wx.lib.gestures.MouseGestures 类来完成鼠标手势。

可用的手势:

  • L for left
  • R for right
  • U for up
  • D for down
  • 7 for northwest
  • 9 for northeast
  • 1 for southwest
  • 3 for southeast

如果你好奇为什么会选用这些数字,你可以看一下数字小键盘。当我们按下鼠标指针画一个正方形时,手势 ‘RDLU’ 将被触发。

可用的标识:

  • wx.MOUSE_BTN_LEFT
  • wx.MOUSE_BTN_MIDDLE
  • wx.MOUSE_BTN_RIGHT
#!/usr/bin/python

# mousegestures.py

import wx
import wx.lib.gestures as gest

class MyMouseGestures(wx.Frame):

def __init__ (self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(300, 200))

panel = wx.Panel(self, -1)
mg = gest.MouseGestures(panel, True, wx.MOUSE_BTN_LEFT)
mg.SetGesturePen(wx.Colour(255, 0, 0), 2)
mg.SetGesturesVisible(True)
mg.AddGesture('DR', self.OnDownRight)

self.Centre()
self.Show(True)

def OnDownRight(self):
self.Close()

app = wx.App()
MyMouseGestures(None, -1, 'mousegestures.py')
app.MainLoop()

在上面的例子中,我们注册了一个鼠标手势。当左键点击,同时鼠标向下、右滑动时,这个手势会被触发,然后会关闭应用。

mg = gest.MouseGestures(panel, True, wx.MOUSE_BTN_LEFT)

如果我们要使用鼠标手势,我们需要创建一个 MouseGesture 对象。第一个参数是一个窗口,手势要向该窗口注册。第二个参数定义了注册的方式,True 代表自动注册,False 代表手动注册。最后一个参数定义了鼠标的按键,表明当触发的时候需要按的键。之后可以通过 SetMouseButton() 来修改。

mg.SetGesturePen(wx.Colour(255, 0, 0), 2)

我们的手势将会被绘制成红色的线,宽为 2px。

mg.SetGesturesVisible(True)

使用上面的方法可以让手势可见。

mg.AddGesture('DR', self.OnDownRight)

我们使用 AddGesture() 方法注册一个鼠标手势。第一个参数是手势,第二个参数是需要触发的方法。

在本节,我们讲解了一些 wxPython 的技巧。