# 設計自己的Trait編輯器
在前面的章節中我們知道,每種trait屬性都對應有缺省的trait編輯器,如果在View中不指定編輯器的話,將使用缺省的編輯器構成界面。每個編輯器都可以對應有多個后臺,目前支持的后臺界面庫有pyQt和wxPython。每種編輯器都可以有四種樣式:simple, custom, text, readonly。
traitsUI為我們提供了很豐富的編輯器庫,以至于我們很少有自己設計編輯器的需求,然而如果我們能方便地設計自己的編輯器,將能制作出更加專業的程序界面。
本章節將簡要介紹trait編輯器的工作原理;并且制作一個新的trait編輯器,用以顯示matplotlib提供的繪圖控件;然后以此控件制作一個通用的繪制CSV文件數據圖像的小工具。
## Trait編輯器的工作原理
我們先來看下面這個小程序,它定義了一個TestStrEditor類,其中有一個名為test的trait屬性,其類型為Str,在view中用Item定義要在界面中顯示test屬性,但是沒有指定它所使用的編輯器(通過editor參數)。當執行t.configure_traits()時,traits庫將自動為我們挑選文本編輯框控件作為test屬性的編輯器:
```
from enthought.traits.api import *
from enthought.traits.ui.api import *
class TestStrEditor(HasTraits):
test = Str
view = View(Item("test"))
t = TestStrEditor()
t.configure_traits()
```

使用文本編輯框控件編輯test屬性
Traits庫的路徑
下面的介紹需要查看traits庫的源程序,因此首先你需要知道它們在哪里:
**traits**: site-packages\Traits-3.2.0-py2.6-win32.egg\enthought\traits, 以下簡稱 %traits%
**traitsUI**: site-packages\Traits-3.2.0-py2.6-win32.egg\enthought\traits\UI, 以下簡稱 %ui%
**wx后臺界面庫**: site-packages\TraitsBackendWX-3.2.0-py2.6.egg\enthought\traitsui\wx, 以下簡稱 %wx%
Str對象的缺省編輯器通過其create_editor方法獲得:
```
>>> from enthought.traits.api import *
>>> s = Str()
>>> ed = s.create_editor()
>>> type(ed)
<class 'enthought.traits.ui.editors.text_editor.ToolkitEditorFactory'>
>>> ed.get()
{'auto_set': True,
'custom_editor_class': <class 'enthought.traits.ui.wx.text_editor.CustomEditor'>,
'enabled': True,
'enter_set': False,
'evaluate': <enthought.traits.ui.editors.text_editor._Identity object at 0x0427F1B0>,
'evaluate_name': '',
'format_func': None,
'format_str': '',
'invalid': '',
'is_grid_cell': False,
'mapping': {},
'multi_line': True,
'password': False,
'readonly_editor_class': <class 'enthought.traits.ui.wx.text_editor.ReadonlyEditor'>,
'simple_editor_class': <class 'enthought.traits.ui.wx.text_editor.SimpleEditor'>,
'text_editor_class': <class 'enthought.traits.ui.wx.text_editor.SimpleEditor'>,
'view': None}
```
create_editor方法的源代碼可以在%traits%trait_types.py中的BaseStr類的定義中找到。create_editor方法得到的是一個text_editor.ToolkitEditorFactory類:
```
enthought.traits.ui.editors.text_editor.ToolkitEditorFactory
```
在%ui%editorstext_editor.py中你可以找到它的定義,它繼承于EditorFactory類。EditorFactory類的代碼在%ui%editor_factory.py中。EditorFactory類是Traits編輯器的核心,通過它和后臺界面庫聯系起來。讓我們來詳細看看EditorFactory類中關于控件生成方面的代碼:
```
class EditorFactory ( HasPrivateTraits ):
# 下面四個屬性描述四個類型的編輯器的類
simple_editor_class = Property
custom_editor_class = Property
text_editor_class = Property
readonly_editor_class = Property
# 用simple_editor_class創建實際的控件
def simple_editor ( self, ui, object, name, description, parent ):
return self.simple_editor_class( parent,
factory = self,
ui = ui,
object = object,
name = name,
description = description )
# 這是類的方法,它通過類的以及父類自動找到與其匹配的后臺界面庫中的控件類
@classmethod
def _get_toolkit_editor(cls, class_name):
editor_factory_classes = [factory_class for factory_class in cls.mro()
if issubclass(factory_class, EditorFactory)]
for index in range(len( editor_factory_classes )):
try:
factory_class = editor_factory_classes[index]
editor_file_name = os.path.basename(
sys.modules[factory_class.__module__].__file__)
return toolkit_object(':'.join([editor_file_name.split('.')[0],
class_name]), True)
except Exception, e:
if index == len(editor_factory_classes)-1:
raise e
return None
# simple_editor_class屬性的get方法,獲取屬性值
def _get_simple_editor_class(self):
try:
SimpleEditor = self._get_toolkit_editor('SimpleEditor')
except:
SimpleEditor = toolkit_object('editor_factory:SimpleEditor')
return SimpleEditor
```
EditorFactory的對象有四個屬性保存后臺編輯器控件的類:simple_editor_class, custom_editor_class, text_editor_class, readonly_editor_class。例如前面例子中的ed對象的simple_editor_class為<class 'enthought.traits.ui.wx.text_editor.SimpleEditor'>,我們看到它用的是wx后臺界面庫中的text_editor中的SimpleEditor類,稍后我們將看看其內容。
EditorFactory是通過其類方法_get_toolkit_editor計算出所要用后臺界面庫中的類的。由于_get_toolkit_editor是類方法,它的第一個參數cls就是類本身。當調用text_editor.ToolkitEditorFactory._get_toolkit_editor()時,cls就是text_editor.ToolkitEditorFactory類。通過調用cls.mro獲得cls以及其所有父類,然后一個一個地查找,從后臺界面庫中找到與之匹配的類,這個工作由toolkit_object函數完成。其源代碼可以在%ui%toolkit.py中找到。
因為后臺界面庫中的類的組織結構和traits.ui是一樣的,因此不需要額外的配置文件,只需要幾個字符串替代操作就可以將traits.ui中的EditorFactory類和后臺界面庫中的實際的編輯器類聯系起來。下圖顯示了traits.ui中的EditorFactory和后臺界面庫的關系。

traits.ui中的EditorFactory和后臺界面庫的關系
wx后臺界面庫中定義了所有編輯器控件,在 %wx%text_editor.py 中你可以找到產生文本框控件的類 text_editor.SimpleEditor。類名表示了控件的樣式:simple, custom, text, readonly,而其文件名(模塊名)則表示了控件的類型。下面是 text_editor.SimpleEditor的部分代碼:
```
class SimpleEditor ( Editor ):
# Flag for window styles:
base_style = 0
# Background color when input is OK:
ok_color = OKColor
# Function used to evaluate textual user input:
evaluate = evaluate_trait
def init ( self, parent ):
""" Finishes initializing the editor by creating the underlying toolkit
widget.
"""
factory = self.factory
style = self.base_style
self.evaluate = factory.evaluate
self.sync_value( factory.evaluate_name, 'evaluate', 'from' )
if (not factory.multi_line) or factory.password:
style &= ~wx.TE_MULTILINE
if factory.password:
style |= wx.TE_PASSWORD
multi_line = ((style & wx.TE_MULTILINE) != 0)
if multi_line:
self.scrollable = True
if factory.enter_set and (not multi_line):
control = wx.TextCtrl( parent, -1, self.str_value,
style = style | wx.TE_PROCESS_ENTER )
wx.EVT_TEXT_ENTER( parent, control.GetId(), self.update_object )
else:
control = wx.TextCtrl( parent, -1, self.str_value, style = style )
wx.EVT_KILL_FOCUS( control, self.update_object )
if factory.auto_set:
wx.EVT_TEXT( parent, control.GetId(), self.update_object )
self.control = control
self.set_tooltip()
```
真正產生控件的程序是在init方法中,此方法在產生界面時自動被調用,注意方法名是init,不要和對象初始化方法\_\_init\_\_搞混淆了。
## 制作matplotlib的編輯器
Enthought的官方繪圖庫是采用Chaco,不過如果你對matplotlib庫更加熟悉的話,將matplotlib的繪圖控件嵌入TraitsUI界面中將是非常有用的。下面先來看一下嵌入matplotlib控件的完整源代碼:
```
# -*- coding: utf-8 -*-
# file name: mpl_figure_editor.py
import wx
import matplotlib
# matplotlib采用WXAgg為后臺,這樣才能將繪圖控件嵌入以wx為后臺界面庫的traitsUI窗口中
matplotlib.use("WXAgg")
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from enthought.traits.ui.wx.editor import Editor
from enthought.traits.ui.basic_editor_factory import BasicEditorFactory
class _MPLFigureEditor(Editor):
"""
相當于wx后臺界面庫中的編輯器,它負責創建真正的控件
"""
scrollable = True
def init(self, parent):
self.control = self._create_canvas(parent)
self.set_tooltip()
print dir(self.item)
def update_editor(self):
pass
def _create_canvas(self, parent):
"""
創建一個Panel, 布局采用垂直排列的BoxSizer, panel中中添加
FigureCanvas, NavigationToolbar2Wx, StaticText三個控件
FigureCanvas的鼠標移動事件調用mousemoved函數,在StaticText
顯示鼠標所在的數據坐標
"""
panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN)
def mousemoved(event):
panel.info.SetLabel("%s, %s" % (event.xdata, event.ydata))
panel.mousemoved = mousemoved
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
mpl_control = FigureCanvas(panel, -1, self.value)
mpl_control.mpl_connect("motion_notify_event", mousemoved)
toolbar = NavigationToolbar2Wx(mpl_control)
sizer.Add(mpl_control, 1, wx.LEFT | wx.TOP | wx.GROW)
sizer.Add(toolbar, 0, wx.EXPAND|wx.RIGHT)
panel.info = wx.StaticText(parent, -1)
sizer.Add(panel.info)
self.value.canvas.SetMinSize((10,10))
return panel
class MPLFigureEditor(BasicEditorFactory):
"""
相當于traits.ui中的EditorFactory,它返回真正創建控件的類
"""
klass = _MPLFigureEditor
if __name__ == "__main__":
from matplotlib.figure import Figure
from enthought.traits.api import HasTraits, Instance
from enthought.traits.ui.api import View, Item
from numpy import sin, cos, linspace, pi
class Test(HasTraits):
figure = Instance(Figure, ())
view = View(
Item("figure", editor=MPLFigureEditor(), show_label=False),
width = 400,
height = 300,
resizable = True)
def __init__(self):
super(Test, self).__init__()
axes = self.figure.add_subplot(111)
t = linspace(0, 2*pi, 200)
axes.plot(sin(t))
Test().configure_traits()
```
此程序的運行結果如下:

在TraitsUI界面中嵌入的matplotlib繪圖控件
由于我們的編輯器沒有simple等四種樣式,也不會放到wx后臺界面庫的模塊中,因此不能采用上節所介紹的自動查找編輯器類的辦法。traits.ui為我們提供一個一個方便的類來完成這些操作:BasicEditorFactory。它的源程序可以在 %ui%basic_editor_factory.py中找到。下面是其中的一部分:
```
class BasicEditorFactory ( EditorFactory ):
klass = Any
def _get_simple_editor_class ( self ):
return self.klass
...
```
它通過重載EditorFactory中的simple_editor_class屬性,直接返回創建控件的庫klass。MPLFigureEditor繼承于BasicEditorFactory,指定創建控件的類為_MPLFigureEditor。
和text_editor.SimpleEditor一樣,從Editor類繼承,在_MPLFigureEditor類的init方法中,創建實際的控件。因為Editor類中有一個update_editor方法,在其對應的trait屬性改變是會被調用,而我們的繪圖控件不需要這個功能,所以重載update_editor,讓它不做任何事情。
matplotlib中,在創建FigureCanvas時需要指定與其對應的Figure對象:
```
mpl_control = FigureCanvas(panel, -1, self.value)
```
這里self.value就是這個Figure對象,它在MVC的模型類Test中被定義為:
```
figure = Instance(Figure, ())
```
控件類可以通過self.value獲得與其對應的模型類中的對象。因此_MPLFigureEditor中的self.value和Test類中的self.figure是同一個對象。
_create_canvas方法中的程序編寫和在一個標準的wx窗口中添加控件是一樣的,界面庫相關的細節不是本書的重點,因此不再詳細解釋了。讀者可以參照matplotlib和wxPython的相應文檔。
## CSV數據繪圖工具
下面用前面介紹的matplotlib編輯器制作一個CSV數據繪圖工具。用此工具打開一個CSV數據文檔之后,可以繪制多個X-Y坐標圖。用戶可以自由地添加新的坐標圖,修改坐標圖的標題,選擇坐標圖的X軸和Y軸的數據。
下面是此程序的界面截圖:

CSV數據繪圖工具的界面
圖中以標簽頁的形式顯示多個繪圖,用戶可以從左側的數據選擇欄中選擇X軸和Y軸的數據。標簽頁可以自由的拖動,構成上下左右分欄,并且可以隱藏左側的數據選擇欄:

使用可調整DOCK的多標簽頁界面方便用戶對比數據
由于繪圖控件是matplotlib所提供的,因此平移、縮放、保存文件等功能也一應俱全。由于所有的界面都是采用TraitsUI設計的,因此主窗口既可以用來單獨顯示,也可以嵌入到一個更大的界面中,運用十分靈活。
下面是完整的源程序,運行時需要和mpl_figure_editor.py放在一個文件夾下。包括注釋程序一共約170行,編寫時間少于一小時。
```
# -*- coding: utf-8 -*-
from matplotlib.figure import Figure
from mpl_figure_editor import MPLFigureEditor
from enthought.traits.ui.api import *
from enthought.traits.api import *
import csv
class DataSource(HasTraits):
"""
數據源,data是一個字典,將字符串映射到列表
names是data中的所有字符串的列表
"""
data = DictStrAny
names = List(Str)
def load_csv(self, filename):
"""
從CSV文件讀入數據,更新data和names屬性
"""
f = file(filename)
reader = csv.DictReader(f)
self.names = reader.fieldnames
for field in reader.fieldnames:
self.data[field] = []
for line in reader:
for k, v in line.iteritems():
self.data[k].append(float(v))
f.close()
class Graph(HasTraits):
"""
繪圖組件,包括左邊的數據選擇控件和右邊的繪圖控件
"""
name = Str # 繪圖名,顯示在標簽頁標題和繪圖標題中
data_source = Instance(DataSource) # 保存數據的數據源
figure = Instance(Figure) # 控制繪圖控件的Figure對象
selected_xaxis = Str # X軸所用的數據名
selected_items = List # Y軸所用的數據列表
clear_button = Button(u"清除") # 快速清除Y軸的所有選擇的數據
view = View(
HSplit( # HSplit分為左右兩個區域,中間有可調節寬度比例的調節手柄
# 左邊為一個組
VGroup(
Item("name"), # 繪圖名編輯框
Item("clear_button"), # 清除按鈕
Heading(u"X軸數據"), # 靜態文本
# X軸選擇器,用EnumEditor編輯器,即ComboBox控件,控件中的候選數據從
# data_source的names屬性得到
Item("selected_xaxis", editor=
EnumEditor(name="object.data_source.names", format_str=u"%s")),
Heading(u"Y軸數據"), # 靜態文本
# Y軸選擇器,由于Y軸可以多選,因此用CheckBox列表編輯,按兩列顯示
Item("selected_items", style="custom",
editor=CheckListEditor(name="object.data_source.names",
cols=2, format_str=u"%s")),
show_border = True, # 顯示組的邊框
scrollable = True, # 組中的控件過多時,采用滾動條
show_labels = False # 組中的所有控件都不顯示標簽
),
# 右邊繪圖控件
Item("figure", editor=MPLFigureEditor(), show_label=False, width=600)
)
)
def _name_changed(self):
"""
當繪圖名發生變化時,更新繪圖的標題
"""
axe = self.figure.axes[0]
axe.set_title(self.name)
self.figure.canvas.draw()
def _clear_button_fired(self):
"""
清除按鈕的事件處理
"""
self.selected_items = []
self.update()
def _figure_default(self):
"""
figure屬性的缺省值,直接創建一個Figure對象
"""
figure = Figure()
figure.add_axes([0.05, 0.1, 0.9, 0.85]) #添加繪圖區域,四周留有邊距
return figure
def _selected_items_changed(self):
"""
Y軸數據選擇更新
"""
self.update()
def _selected_xaxis_changed(self):
"""
X軸數據選擇更新
"""
self.update()
def update(self):
"""
重新繪制所有的曲線
"""
axe = self.figure.axes[0]
axe.clear()
try:
xdata = self.data_source.data[self.selected_xaxis]
except:
return
for field in self.selected_items:
axe.plot(xdata, self.data_source.data[field], label=field)
axe.set_xlabel(self.selected_xaxis)
axe.set_title(self.name)
axe.legend()
self.figure.canvas.draw()
class CSVGrapher(HasTraits):
"""
主界面包括繪圖列表,數據源,文件選擇器和添加繪圖按鈕
"""
graph_list = List(Instance(Graph)) # 繪圖列表
data_source = Instance(DataSource) # 數據源
csv_file_name = File(filter=[u"*.csv"]) # 文件選擇
add_graph_button = Button(u"添加繪圖") # 添加繪圖按鈕
view = View(
# 整個窗口分為上下兩個部分
VGroup(
# 上部分橫向放置控件,因此用HGroup
HGroup(
# 文件選擇控件
Item("csv_file_name", label=u"選擇CSV文件", width=400),
# 添加繪圖按鈕
Item("add_graph_button", show_label=False)
),
# 下部分是繪圖列表,采用ListEditor編輯器顯示
Item("graph_list", style="custom", show_label=False,
editor=ListEditor(
use_notebook=True, # 是用多標簽頁格式顯示
deletable=True, # 可以刪除標簽頁
dock_style="tab", # 標簽dock樣式
page_name=".name") # 標題頁的文本使用Graph對象的name屬性
)
),
resizable = True,
height = 0.8,
width = 0.8,
title = u"CSV數據繪圖器"
)
def _csv_file_name_changed(self):
"""
打開新文件時的處理,根據文件創建一個DataSource
"""
self.data_source = DataSource()
self.data_source.load_csv(self.csv_file_name)
del self.graph_list[:]
def _add_graph_button_changed(self):
"""
添加繪圖按鈕的事件處理
"""
if self.data_source != None:
self.graph_list.append( Graph(data_source = self.data_source) )
if __name__ == "__main__":
csv_grapher = CSVGrapher()
csv_grapher.configure_traits()
```
程序中已經有比較詳細的注釋,這里就不再重復。如果你對traits庫的某項用法還不太了解的話,可以直接查看其源代碼,代碼中都有詳細的注釋。下面是幾個比較重點的部分:
* 整個程序的界面處理都只是組裝View對象,看不到任何關于控件操作的代碼,因此大大地節省了程序的開發時間。
* 通過配置141行的ListEditor,使其用標簽頁的方式顯示graph_list中的每個元素,以此管理多個Graph對象。
* 在43行中,Graph類用HSplit將其數據選擇部分和繪圖控件部分分開,HSplit提供的更改左右部分的比例和隱藏的功能。
* 本書寫作時所采用的traitsUI庫版本為3.2,如果在標簽頁標題中輸入中文,會出現錯誤,這是因為TraitsUI中還有些代碼對unicode的支持不夠,希望日后會有所改善。目前可以通過分析錯誤提示信息,修改TraitsUI庫的源代碼,只需要將下面提示中的770行中的str改為unicode既可以修復。
> ```
> >>> from visual import *
>
> ```
之后就可以隨心所欲的調用visual庫通過的函數。需要注意的是如果你關閉了visual彈出的場景窗口的話,ipython對話也隨之結束。如果你需要關閉場景窗口可以用下面的語句:
```
>>> scene.visible = False
```

在IPython中交互式地觀察visual的運行結果
上圖是用IPython交互式的使用visual的一個例子,可以看到通過IPython能夠控制多個場景窗口。
* [場景窗口](visual_manual_display.html)
* [控制場景窗口](visual_manual_display.html#id2)
* [控制照相機](visual_manual_display.html#id3)
- 用Python做科學計算
- 軟件包的安裝和介紹
- NumPy-快速處理數據
- SciPy-數值計算庫
- matplotlib-繪制精美的圖表
- Traits-為Python添加類型定義
- TraitsUI-輕松制作用戶界面
- Chaco-交互式圖表
- TVTK-三維可視化數據
- Mayavi-更方便的可視化
- Visual-制作3D演示動畫
- OpenCV-圖像處理和計算機視覺
- Traits使用手冊
- 定義Traits
- Trait事件處理
- 設計自己的Trait編輯器
- Visual使用手冊
- 場景窗口
- 聲音的輸入輸出
- 數字信號系統
- FFT演示程序
- 頻域信號處理
- Ctypes和NumPy
- 自適應濾波器和NLMS模擬
- 單擺和雙擺模擬
- 分形與混沌
- 關于本書的編寫
- 最近更新
- 源程序集
- 三角波的FFT演示
- 在traitsUI中使用的matplotlib控件
- CSV文件數據圖形化工具
- NLMS算法的模擬測試
- 三維標量場觀察器
- 頻譜泄漏和hann窗
- FFT卷積的速度比較
- 二次均衡器設計
- 單擺擺動周期的計算
- 雙擺系統的動畫模擬
- 繪制Mandelbrot集合
- 迭代函數系統的分形
- 繪制L-System的分形圖