ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 设计自己的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() ``` ![](https://box.kancloud.cn/2016-03-19_56ed1bae75de9.png) 使用文本编辑框控件编辑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为&lt;class 'enthought.traits.ui.wx.text_editor.SimpleEditor'&gt;,我们看到它用的是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和后台界面库的关系。 ![](https://box.kancloud.cn/2016-03-19_56ed1bae85b53.png) 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() ``` 此程序的运行结果如下: ![](https://box.kancloud.cn/2016-03-19_56ed1baea865a.png) 在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轴的数据。 下面是此程序的界面截图: ![](https://box.kancloud.cn/2016-03-19_56ed1baed4140.png) CSV数据绘图工具的界面 图中以标签页的形式显示多个绘图,用户可以从左侧的数据选择栏中选择X轴和Y轴的数据。标签页可以自由的拖动,构成上下左右分栏,并且可以隐藏左侧的数据选择栏: ![](https://box.kancloud.cn/2016-03-19_56ed1baeebc0d.png) 使用可调整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既可以修复。 > ``` > &gt;&gt;&gt; from visual import * > > ``` 之后就可以随心所欲的调用visual库通过的函数。需要注意的是如果你关闭了visual弹出的场景窗口的话,ipython对话也随之结束。如果你需要关闭场景窗口可以用下面的语句: ``` >>> scene.visible = False ``` ![](https://box.kancloud.cn/2016-03-19_56ed1baf2d568.png) 在IPython中交互式地观察visual的运行结果 上图是用IPython交互式的使用visual的一个例子,可以看到通过IPython能够控制多个场景窗口。 * [场景窗口](visual_manual_display.html) * [控制场景窗口](visual_manual_display.html#id2) * [控制照相机](visual_manual_display.html#id3)