设计自己的 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 能够控制多个场景窗口。
- 场景窗口
- 控制场景窗口
- 控制照相机
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论