返回介绍

设计自己的 Trait 编辑器

发布于 2025-02-25 22:46:22 字数 16713 浏览 0 评论 0 收藏 0

在前面的章节中我们知道,每种 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 既可以修复。
    &gt;&gt;&gt; from visual import *
    

之后就可以随心所欲的调用 visual 库通过的函数。需要注意的是如果你关闭了 visual 弹出的场景窗口的话,ipython 对话也随之结束。如果你需要关闭场景窗口可以用下面的语句:

>>> scene.visible = False

在 IPython 中交互式地观察 visual 的运行结果

上图是用 IPython 交互式的使用 visual 的一个例子,可以看到通过 IPython 能够控制多个场景窗口。

  • 场景窗口
    • 控制场景窗口
    • 控制照相机

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文