wxPython 教程 (十四): 提示和技巧

总目录:wxPython 教程目录 
本节内容:wxPython 提示和技巧
本节译自:zetcode
上一篇:wxPython 教程 (十三): 自定义控件
下一篇:wxPython 教程 (十五): wxPython Gripts

本节,我们将讲解一些 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 类完成的。
tutorial wxpython-jiaocheng

图:Isabelle

Undo/Redo 框架

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

tutorial wxpython-jiaocheng

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

tutorial wxpython-jiaocheng

图: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 的技巧。


上一节:wxPython 教程 (十三): 自定义控件           下一节:wxPython 教程 (十五): wxPython Gripts

发表评论

电子邮件地址不会被公开。 必填项已用*标注