wxPython, в конструктор форм - автоматизация сайзер


Представил для критики есть пара классов, которые автоматизируют создание классификатора и макет на wxPython.

import wx
from wx.lib.combotreebox import ComboTreeBox
from wx.lib.agw.floatspin import FloatSpin

wx.ComboTreeBox = ComboTreeBox
wx.FloatSpin = FloatSpin

class FormDialog(wx.Dialog):
  def __init__(self, parent, id = -1, panel = None, title = "Unnamed Dialog",
               modal = False, sizes = (-1, -1), refid = None):
    wx.Dialog.__init__(self, parent, id, title,
                       style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)

    if panel is not None:
      self._panel = panel(self, refid)

      self._panel.SetSizeHints(*sizes)

      ds = wx.GridBagSizer(self._panel._gap, self._panel._gap)

      ds.Add(self._panel, (0, 0), (1, 1), wx.EXPAND | wx.ALL, self._panel._gap)

      ds.Add(wx.StaticLine(self), (1, 0), (1, 1), wx.EXPAND | wx.RIGHT | wx.LEFT, self._panel._gap)

      self.bs = self.CreateButtonSizer(self._panel._form.get('Buttons', wx.OK | wx.CANCEL))

      ds.Add(self.bs, (2, 0), (1, 1), wx.ALIGN_RIGHT | wx.ALL, self._panel._gap)

      ds.AddGrowableCol(0)
      ds.AddGrowableRow(0)

      self.SetSizerAndFit(ds)

      self.Center()

      self.Bind(wx.EVT_BUTTON, self._panel.onOk, id = wx.ID_OK)
      self.Bind(wx.EVT_BUTTON, self._panel.onClose, id = wx.ID_CANCEL)

      focused = self._panel._form.pop('Focus', None)

      if focused:
        self._panel.itemMap[focused].SetFocus()

      if modal:
        self.ShowModal()
      else:
        self.Show()

class Form(wx.Panel):
  def __init__(self, parent = None, refid = None, id = -1, gap = 3, sizes = (-1, -1)):
    wx.Panel.__init__(self, parent, id)

    self.SetSizeHints(*sizes)

    self._gap = gap

    self.itemMap = {}

    self.refid = refid

    if not hasattr(self, 'q'):
      self.q = getattr(self.GrandParent, 'q', None)

    if hasattr(self, '_form'):
      # Before building verify that several required elements exist in the form
      # definition object.
      self.loadDefaults()

      self._build()

      self._bind()

  def _build(self):
    """
    The Build Method automates sizer creation and element placement by parsing
    a properly constructed object.
    """

    # The Main Sizer for the Panel.
    panelSizer = wx.BoxSizer(wx.VERTICAL)

    # Parts is an Ordered Dictionary of regions for the form.
    for container, blocks in self._form['Parts'].iteritems():
      flags, sep, display = container.rpartition('-') #@UnusedVariable

      if 'NC' in flags:
        for block in blocks:
          element, proportion = self._parseBlock(block)

          panelSizer.Add(element, proportion, flag = wx.EXPAND | wx.ALL, border = self._gap)
      else:
        box = wx.StaticBox(self, -1, display)

        sizer = wx.StaticBoxSizer(box, wx.VERTICAL)

        for block in blocks:
          element, proportion = self._parseBlock(block)

          sizer.Add(element, proportion, flag = wx.EXPAND | wx.ALL)

        if 'G' in flags:
          sizerProportion = 1
        else:
          sizerProportion = 0

        panelSizer.Add(sizer, sizerProportion, flag = wx.EXPAND | wx.ALL, border = self._gap)

      continue

    self.SetSizerAndFit(panelSizer)

  def _bind(self): pass

  def _parseBlock(self, block):
    """
      The form structure is a list of rows (blocks) in the form.  Each row 
      consists of a single element, a row of elements, or a sub-grid of 
      elements.  These are represented by dictionaries, tuples, or lists, 
      respectively and are each processed differently.
    """
    proportion = 0

    if isinstance(block, list):
      item = self.makeGrid(block)

    elif isinstance(block, tuple):
      item = self.makeRow(block)

    elif isinstance(block, dict):
      proportion = block.pop('proportion', 0)

      item = self.makeElement(block)

    return item, proportion

  def makeElement(self, object):
    """
      In the form structure a dictionary signifies a single element.  A single
      element is automatically assumed to expand to fill available horizontal
      space in the form.
    """
    sizer = wx.BoxSizer(wx.HORIZONTAL)

    flags = object.pop('flags', wx.ALL)

    element = self._makeWidget(object)

    sizer.Add(element, 1, border = self._gap,
              flag = wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | flags)

    return sizer

  def makeRow(self, fields):
    """
      In the form structure a tuple signifies a row of elements.  These items
      will be arranged horizontally without dependency on other rows.  Each
      item may provide a proportion property which can cause that element to 
      expand horizontally to fill space.
    """
    sizer = wx.BoxSizer(wx.HORIZONTAL)

    for field in fields:
      proportion = field.pop('proportion', 0)

      sizer.Add(self.makeElement(field), proportion,
                flag = wx.ALIGN_CENTER_VERTICAL | wx.ALL)

    return sizer

  def makeGrid(self, rows):
    """ 
      In the form structure a list signifies a grid of elements (equal width
      columns, rows with similar numbers of elements, etc).
    """

    sizer = wx.GridBagSizer(0, 0)

    for row, fields in enumerate(rows):
      for col, field in enumerate(fields):
        flags = field.pop('flags', wx.ALL)

        # Each item may specify that its row or column 'grow' or expand to fill
        # the available space in the form.
        rowGrowable, colGrowable = (field.pop('rowGrowable', False),
                                    field.pop('colGrowable', False))

        if rowGrowable:
          sizer.AddGrowableRow(row)

        if colGrowable:
          sizer.AddGrowableCol(col)

        span = field.pop('span', (1, 1))

        colpos = field.pop('colpos', col)

        rowpos = field.pop('rowpos', row)

        element = self._makeWidget(field)

        sizer.Add(element, (rowpos, colpos), span, border = self._gap,
                  flag = wx.ALIGN_CENTER_VERTICAL | flags)

    return sizer

  def _makeWidget(self, params):
    """ 
      This function actually creates the widgets that make up the form. In most
      cases these will be items from the wx libraries, though they may be
      'custom' elements which require delayed instantiation by leveraging
      lambdas.
    """

    type = params.pop('type')

    if type == 'Custom':
      lookup = params.pop('lookup')

      element = self._form[lookup](self)

      self.itemMap[lookup] = element
    else:
      # StaticText items may carry a bold attribute - retrieve it for use later.
      if type == 'StaticText':
        bold = params.pop('bold', False)

      # ComboBoxes and ListBoxes need to have choices.
      if type in ('ComboBox', 'ListBox'):
        params['choices'] = self._form['Options'].get(params['name'], [])

      element = getattr(wx, type)(self, -1, **params)

      if type == 'ComboTreeBox':
        choices = self._form['Options'].get(params['name'], [])

        for category, options in choices:
          id = element.Append(category)

          for option in options:
            element.Append(option, parent = id)

          element.GetTree().Expand(id)

      # Require the user to use the browse buttons for File / Folder browsing.
      if type in ('DirPickerCtrl', 'FilePickerCtrl'):
        element.GetTextCtrl().SetEditable(False)

      if params.has_key('name'):
        # Populate the itemMap - facilitates element retrieval / event bindings.
        self.itemMap[params['name']] = element

        # Default value assignment.  Must unfortunately do a dance to check
        # element type - some require ints / floats, while others are ok with
        # strings.  
        value = self._form['Defaults'].get(params['name'], '')

        if hasattr(element, 'SetValue'):
          if type == 'SpinCtrl':
            if value == '':
              value = 0
            element.SetValue(int(value))
          elif type == 'FloatSpin':
            if value == '':
              value = 0
            element.SetValue(float(value))
          elif type in ('CheckBox', 'RadioButton'):
            element.SetValue(bool(value))
          else:
            element.SetValue(unicode(value))

            if type == 'ComboTreeBox':
              element._text.SetInsertionPoint(0)
        elif hasattr(element, 'SetPath'):
          element.SetPath(value)
        elif type != 'Button':
          print element

        # Check for elements we should disable at load time.
        if params['name'] in self._form['Disabled']:
          element.Enable(False)

        # Check for a Validator and add it if required.
        try:
          validator = self._form['Validators'][params['name']]()

          element.SetValidator(validator)
        except KeyError: pass # No Validator Specified.

      # Take the bold attribute into account for StaticText elements.
      if type == 'StaticText' and bold:
        font = element.GetFont()

        font.SetWeight(wx.BOLD)

        element.SetFont(font)

    return element

  def loadDefaults(self):
    if 'Defaults' not in self._form: self._form['Defaults'] = {}

    if 'Disabled' not in self._form: self._form['Disabled'] = []

    if 'Validators' not in self._form: self._form['Validators'] = {}

    self.loadOptions()

  def loadOptions(self):
    if 'Options' not in self._form: self._form['Options'] = {}

  def onOk(self, evt):
    self.onClose(evt)

  def onClose(self, evt):
    self.GetParent().Destroy()

Класс форма является длительным, и я ценю терпение тех, кто успел прочитать далеко. Весь расчет на то, что бланк должен быть разделен на подклассы следующим образом:

class GeneralSettings(Form):
  def __init__(self, parent, refid = None):
    self._form = {
      'Parts': OD([
        ('Log Settings', [
          ({'type': 'StaticText', 'label': 'Remove messages after'},
           {'type': 'FloatSpin', 'name': 'interval', 'min_val': 1, 'max_val': 10, 'digits': 2, 'increment': 0.1, 'size': (55, -1), 'flags': wx.LEFT | wx.RIGHT},
           {'type': 'ComboBox', 'name': 'unit', 'flags': wx.LEFT | wx.RIGHT, 'style': wx.CB_READONLY},
           {'type': 'StaticText', 'label': 'Log Detail:'},
           {'type': 'ComboBox', 'name': 'log-level', 'flags': wx.LEFT | wx.RIGHT, 'style': wx.CB_READONLY, 'proportion': 1})
        ]),
        ('Folder Settings', [
          [({'type': 'StaticText', 'label': 'Spool Folder:'},
            {'type': 'DirPickerCtrl', 'name': 'dir', 'style': wx.DIRP_USE_TEXTCTRL, 'colGrowable': True, 'flags': wx.EXPAND | wx.ALL}),
           ({'type': 'StaticText', 'label': 'Temp Folder:'},
            {'type': 'DirPickerCtrl', 'name': 'temp', 'style': wx.DIRP_USE_TEXTCTRL, 'flags': wx.EXPAND | wx.ALL})]
        ]),
        ('Email Notifications', [
          [({'type': 'StaticText', 'label': 'Alert Email To:'},
            {'type': 'TextCtrl', 'name': 'alert-to', 'colGrowable': True, 'flags': wx.EXPAND | wx.ALL}),
           ({'type': 'StaticText', 'label': 'Alert Email From:'},
            {'type': 'TextCtrl', 'name': 'alert-from', 'flags': wx.EXPAND | wx.ALL}),
           ({'type': 'StaticText', 'label': 'Status Email From:'},
            {'type': 'TextCtrl', 'name': 'status-from', 'flags': wx.EXPAND | wx.ALL}),
           ({'type': 'StaticText', 'label': 'Alert Email Server:'},
            {'type': 'TextCtrl', 'name': 'alert-host', 'flags': wx.EXPAND | wx.ALL}),
           ({'type': 'StaticText', 'label': 'Login:'},
            {'type': 'TextCtrl', 'name': 'alert-login', 'flags': wx.EXPAND | wx.ALL}),
           ({'type': 'StaticText', 'label': 'Password:'},
            {'type': 'TextCtrl', 'name': 'alert-password', 'style': wx.TE_PASSWORD, 'flags': wx.EXPAND | wx.ALL})]
        ]),
        ('Admin User', [
          ({'type': 'CheckBox', 'name': 'req-admin', 'label': 'Require Admin Rights to make changes.'},
           {'type': 'Button', 'name': 'admin-button', 'label': 'Customize Permissions ... '})
        ]),
        ('User Interface Behavior', [
          ({'type': 'StaticText', 'label': 'Job Drag Options'},
           {'type': 'ComboBox', 'name': 'jobdrop', 'flags': wx.LEFT | wx.RIGHT | wx.EXPAND})
        ])
      ]),
      'Options': {
        'unit': ['Hours', 'Days', 'Months'],
        'log-level': ['Minimal', 'Low', 'High', 'Debug'],
        'jobdrop': ['Copy Job to Queue', 'Move Job to Queue']
      },
      'Defaults': {
        'interval': 3,
        'log-level': 'Low',
        'req-admin': False,
        'unit': 'Days',
        'printtasks': 5,
        'jobdrop': 'Copy Job to Queue'
      }
    }

    Form.__init__(self, parent, refid)

Как только форма будет подклассы, а "структура" определяется - в OrderedDict находится в собственной.И _form['детали'], вы можете открыть диалог с элементами такой:

FormDialog(frame, panel = GeneralSettings, title = "General Settings")

Существует ряд начальную критику, что у меня уже есть для него:

  1. Сложная "структура форма" определение. Он глубоко вложенные в разы.
  2. Использует анализ тип - списки представляют собой сетку элементов разбиты на одну или более строк, кортежей и строк элементов, а также словари отдельных элементов. Это традиционно не подходящие для Python.
  3. Комбинированными свойствами - в словари, которые представляют отдельные элементы есть директивы, которые снимаются и используются для создания сайзер. В частности, такие вещи, как 'colGrowable', 'доля' и т. д. Они включаются в определение виджета и выскочил для использования при добавлении элемента в дробилку при строительстве.

Однако, это дает следующие преимущества, а также:

  1. Автоматизированная дробилок - нет необходимости когда-либо непосредственно создать классификатор, установить одно расширение, определить, где они были порождены, и т. д. Это все определяется на вышеупомянутый "структура формы".
  2. Простой условный включением элементов или удаления, основанный на критерии. В общих настройках выше примеру, можно добавить к условной удалить область из формы: если featureDisabled: дель самостоятельно.И _form['детали']['администратора']. Поскольку растяжки являются автоматически сгенерированными нет необходимости условно исключить создание содержащих дробилок, сами элементы и т. д. (Как правило, несколько методов в других помощников макет). Все это происходит в методе _build.
  3. Легкий, прямой доступ к элементам формы. Все виджеты добавляются к переменным личности.itemMap, ввел имя поля для виджетов. Что-нибудь с именем, могут быть доступны из любого другого метода формы.
  4. Комбинированными свойствами - централизованное декларации всеми соответствующими атрибутами для каждого виджета. Вы объявляете виджет, используя свои ценности сайта, но также предоставляем директивы, классификатор, таких как пропорции или установки. Я считаю, что это удобно и логично для новых разработчиков, именно поэтому он включен как в плюсы и в минусы.

Есть несколько дополнительных демки доступны.



1948
3
задан 9 августа 2011 в 04:08 Источник Поделиться
Комментарии
1 ответ

wx.ComboTreeBox = ComboTreeBox
wx.FloatSpin = FloatSpin

Изменяя содержание другого модуля является подозрительным и может вызвать затруднение. Ваши изменения могут не быть там, когда другого модуля является импорт и может стать причиной неприятностей.

  ds = wx.GridBagSizer(self._panel._gap, self._panel._gap)

ds.Add(self._panel, (0, 0), (1, 1), wx.EXPAND | wx.ALL, self._panel._gap)

ds.Add(wx.StaticLine(self), (1, 0), (1, 1), wx.EXPAND | wx.RIGHT | wx.LEFT, self._panel._gap)

self.bs = self.CreateButtonSizer(self._panel._form.get('Buttons', wx.OK | wx.CANCEL))

Пустые строки могут быть использованы, чтобы собрать соответствующие заявления. Преимущество теряется, когда вы делаете это для каждого оператора.

  focused = self._panel._form.pop('Focus', None)

В _ В и _form указывает, что его для внутреннего использования панели. Так зачем вы заходите сюда?

if not hasattr(self, 'q'):
self.q = getattr(self.GrandParent, 'q', None)

Что жареная обезьяна-это вопрос? Это, наверное, нужно имя получше.

class GeneralSettings(Form):
def __init__(self, parent, refid = None):
self._form = {

Возможно, форма должна быть атрибутом класса, а не атрибут. Тогда вы не определите конструктор в этом классе.

      'Parts': OD([

ОД?

        ('Log Settings', [
({'type': 'StaticText', 'label': 'Remove messages after'},
{'type': 'FloatSpin', 'name': 'interval', 'min_val': 1, 'max_val': 10, 'digits': 2, 'increment': 0.1, 'size': (55, -1), 'flags': wx.LEFT | wx.RIGHT},

Вместо словарей, как об объектах?

     widgets.StaticText(label='Remove messages after'),
widgets.ComboBox(name='unit', flags = wx.LEFT | wx.RIGHT, style = wx.CBREADONLY)

Затем вы можете вызывать методы этих объектов, чтобы сделать всю работу, которую вы делаете выше. Таким образом, вы можете переместить все конкретные данные виджет из него. Следует также сделать его проще расширить вашу систему с новых виджетов. Система также может быть расширена для поддержки таких вещей, как OptionalSection и т. д.

      'Defaults': {
'interval': 3,
'log-level': 'Low',
'req-admin': False,
'unit': 'Days',
'printtasks': 5,
'jobdrop': 'Copy Job to Queue'
}

Этим определяются далеко от остальной части виджета. Почему?

Вы, кажется, сравнивая удобство использования этого против вручную писать код. Однако, если я сравниваю его с использованием графического построителя не видно, что его полезно. т. е. весь код, который вы сохраняете уже написал для меня инструмент строителя и поэтому он просто не помогает в таких делах.

2
ответ дан 9 августа 2011 в 09:08 Источник Поделиться