Сайзеры в wxWidgets / wxPython | jenyay.net

Сайзеры в wxWidgets / wxPython

Введение

При построении интерфейсов с помощью библиотеки wxWidgets / wxPython важно понимать как пользоваться так называемыми сайзерами, то есть классов, производными от wx.Sizer (или wxSizer в wxWidgets, в дальнейшем мы будем везде использовать обозначение классов, использующихся в wxPython). Ведь именно сайзеры определяют поведение дочерних элементов управления при изменении размеров главного окна. Именно сайзеры, на мой взгляд, являются наиболее сложной для освоения частью библиотеки wxWidgets / wxPython. И именно сайзерам будет посвящена эта статья.

Общие сведения о сайзерах.

Базовый класс wxSizer

Для того, чтобы лучше понимать то как пользоваться сайзерами, их лучше всего представлять себе в виде книжных полок или шкафчиков, разделенных на секции, внутри каждой секции может располагаться только один элемент управления. Эти элементы управления вы можете придвинуть к левой стенке полки, чтобы справа оставалось место, или наоборот к правой. В отличие от книжек, контролы вы можете приклеивать к потолку "полки" или заставить их висеть по центру, ведь на элементы управления не действует сила притяжения. :) Но, в отличие от настоящей мебели, эти полочки и шкафчики могут также растягивать лежащие в них книжки-контролы, чтобы они занимали все свободное место.

Для начала рассмотрим класс wx.Sizer, базовый для всех сайзеров, кроме того этот класс является абстрактным (в wxWidgets). При создании интерфейса будут использоваться производные от wx.Sizer классы. Сам класс wx.Sizer является производным непосредственно от wx.Object, корня всей иерархии классов библиотеки wxPython/wxWidgets. В библиотеке wxWidgets wx.Sizer также является производным и от wxClientDataContainer, но сейчас для нас это не существенно. Главное запомнить, что сайзеры не являются окнами, то есть элементами управления. Сайзеры - это не панели, на которых располагаются элементы управления, сайзеры только меняют положение и размеры своих "подопечных" контролов.

Внутри себя класс wx.Sizer содержит довольно много методов, но нас в первую очередь будет интересовать только один - метод Add(), с помощью которого в "шкафчик" сайзера добавляется один контрол. Сразу надо сказать, что внутри сайзера вместо элемента управления можно помещать другой сайзер. Благодаря такому приему мы можем каждую "полку" сайзера разбить на несколько ячеек. В следующем разделе, при знакомстве с классом wx.BoxSizer, мы это увидим на примере, а пока рассмотрим параметры, которые передаются в метод Add(). Именно с помощью вызова этого метода в сайзер и добавляются элементы управления или другие сайзеры. Для некоторых классов сайзеров вызов этого метода добавляет виртуальную ячейку или полку, куда помещается контрол. Для других классов сайзера этот метод добавляет элемент управления в уже созданную ячейку (это относится к сайзерам, которые размещают элементы управления в ячейках таблицы).

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

Add(self, item, proportion=0, flag=0, border=0, userData=None)

item. В качестве первого передаваемого параметра (не считая неявно передаваемого параметра self) могут выступать три вида объектов:

  • Элементы управления, а точнее окна, производные от wx.Window.
  • Другие сайзеры, то есть классы, производные от wx.Sizer.
  • Экземпляры класса wx.Size (не путайте с wx.Sizer), которые представляют собой класс-оболочку над двумя числами - шириной и высотой. Если в метод Add() передать экземпляр класса wx.Size, то вместо элемента управления в ячейку сайзера будет добавлено пустое место заданного размера.

proportion. Этот параметр может использоваться классом wx.BoxSizer, о котором мы поговорим в следующем разделе. При использовании параметра proportion можно определить как будут изменяться размеры контролов при изменении размеров их родительского окна. Если proportion = 0 (это значение по умолчанию), то контролы занимают минимальный размер. Если сайзер содержит несколько элементов с одинаковыми значениями proportion, то при изменении размера родительского окна, контролы будут изменять свои размеры таким образом, чтобы иметь одинаковые размеры вдоль одной из осей (какая это ось, горизонтальная или вертикальная, будет зависеть от ориентации сайзера). Если контролы будут иметь разные значения proportion, то их размеры вдоль этой же оси будут пропорциональны значению параметра proportion. В следующем разделе мы увидим это все более наглядно на примерах.

flag. Это, пожалуй, самый интересный и важный параметр, который больше других определяет положение и размеры элементов управления. Параметр flag является целым значением, которое используется как битовая строка, составленную из набора констант. Все эти константы условно можно разделить на три группы:

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

К первой группе флагов относятся флаги:

  • wx.TOP
  • wx.BOTTOM
  • wx.LEFT
  • wx.RIGHT
  • wx.ALL

Они задают с какой стороны от элемента управления будет невидимая рамка или кайма (border): сверху, снизу, слева, справа или со всех сторон соответственно. Рамка представляет собой пустое место около элемента управления, размер которого задается в следующем параметре метода Add(). Если мы, например, хотим, чтобы слева и сверху от элемента было оставлено пустое место, то флаг должен иметь следующий вид:

flag = wx.LEFT | wx.TOP

К флагам, задающим выравнивание элементов управления относятся константы:

  • wx.ALIGN_CENTER
  • wx.ALIGN_LEFT
  • wx.ALIGN_RIGHT
  • wx.ALIGN_TOP
  • wx.ALIGN_BOTTOM
  • wx.ALIGN_CENTER_VERTICAL
  • wx.ALIGN_CENTER_HORIZONTAL

Эти флаги обозначают соответственно, что добавляемый элемент управления будет "приклеен" к центру, к левой или правой боковине, к потолку или полу ячейки. Последние два флага дают возможность указать, что выравнивание по центру нужно делать только по вертикали или только по горизонтали, в то время как wx.ALIGN_CENTER означает, что выравнивание по центру будет производиться по обоим осям.

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

  • wx.EXPAND обозначает, что добавляемый контрол должен занимать всю ячейку сайзера, на сколько он сможет растянуться.
  • wx.SHAPED обозначает, что добавляемый контрол опять же должен занять по возможности всю ячейку, но при этом он должен сохранять исходные пропорции между его длиной и шириной.
  • wx.FIXED_MINSIZE обозначает, что при изменении размеров контрола, не будет учитываться минимально возможный размер, то есть контрол может быть меньше заданного минимального размера.

А теперь перейдем к рассмотрению оставшихся параметров метода Add().

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

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

Установка сайзеров для окон

Допустим, что мы создали сайзер, который нам нужен (о том какие сайзеры есть в библиотеке wxPython мы поговорим позже), заполнили его контролами и (или) другими сайзерами. Теперь главному окну, владельцу контролов, нужно указать, что созданный сайзер нужно использовать для размещения элементов.

Для этого у класса wx.Window есть очень простой метод SetSizer():

SetSizer(self, sizer, deleteOld=True)

Первый параметр sizer представляет собой созданный экземпляр сайзера.

Второй параметр deleteOld булевого типа обозначает, что предыдущий сайзер, установленный для окна должен быть уничтожен (deleteOld=True). Мы не будем использовать этот параметр и будем оставлять его всегда в значении по умолчанию, то есть True.

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

После установки сайзера для окна нужно вызвать метод Layout() экземпляра класса окна wx.Window, чтобы все элементы управления выстроились согласно сайзеру.

Итак, общий алгоритм работы с сайзерами состоит из следующих этапов:

  • Создать экземпляр окна - родителя для элементов управления.
  • После создания окна (например, в конструкторе) создать экземпляр одного из классов, производных от wx.Sizer. Это будет главный для окна сайзер.
  • Заполнить главный сайзер элементами управления или другими, дочерними, сайзерами.
  • Присоединить главный сайзер к окну с помощью метода SetSizer() класса wx.Window.
  • Вызвать метод Layout() экземпляра класса окна wx.Window.

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

# -*- coding: utf-8 -*-

import wx

class MainWindow(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((300, 200))

        self.doLayout()

    def doLayout(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)
        self.Layout()

if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    mainWnd = MainWindow(None, -1, "")
    app.SetTopWindow(mainWnd)
    mainWnd.Show()
    app.MainLoop()

В этом примере объявляется класс окна MainWindow, в конструкторе которого вызывается метод doLayout(). Внутри этого метода в будущих примерах и будет происходить все самое интересное, в том смысле, что в нем мы и будем размещать элементы управления внутри сайзеров. Остальной код меняться не будет, поэтому во всех следующих примерах будет приводиться только этот метод.

В данном примере этот метод содержит создание пустого сайзера класса wx.BoxSizer (без заполнения его элементами управления), и этот сайзер добавляется в окно. Если запустить этот пример, то увидите пустое окно размеров 300x200 пикселей:

А теперь перейдем к самому интересному, к рассмотрению сайзеров, которые существуют в библиотеке wxPython. Заодно на примерах увидим как влияют на расположение элементов все флаги из метода Add() класса wx.Sizer.

wx.BoxSizer

wx.BoxSizer является очень простым сайзером, который позволяет располагать элементы управления вдоль одной линии - по вертикали или по горизонтали. Именно на этом сайзере мы и будем разбираться с тем как пользоваться сайзерами.

__init__(self, orient = wx.HORIZONTAL)

Конструктор класса wx.Boxsizer принимает один параметр, который обозначает как следует располагать контролы: по вертикали или по горизонтали. Соответственно, параметр orient может принимать два значения: wx.HORIZONTAL или wx.VERTICAL.

Если orient = wx.HORIZONTAL, то сайзер можно представить себе в виде книжной полки, разделенной по горизонтали на ячейки, внутри которых располагаются контролы. Причем размер каждой ячейки равен ширине содержащегося контрола с учетом рамки (border). Если при этом сайзер используется как основной для окна (установленный с помощью метода SetSizer()), то по вертикали "полка" будет занимать все родительское окно. Если сайзер будет находиться внутри другого сайзера (и не будет растягиваться с помощью флага wx.EXPAND), то в этом случае по вертикали его размер будет ограничен элементом управления с наибольшей высотой.

Если orient = wx.VERTICAL, то сайзер представляет собой книжный шкафчик, разделенный на горизонтальные полки. Теперь уже высота каждой полки равна точно размеру размещенного в ней контрола (вместе с рамкой). Если сайзер используется как основной для окна, то по горизонтали шкафчик занимает всю ширину окна, иначе его ширина будет равна ширине самого широкого элемента управления.

Рассмотрим примеры.

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1)
    sizer.Add (btn2)

    self.SetSizer(sizer)
    self.Layout()

В этом примере мы создаем вертикальный wx.BoxSizer и добавляем в него две кнопки (btn1 и btn2). Для всех параметров метода Add() кроме первого мы используем значения по умолчанию. Результат расположения кнопок в окне показан на следующем скриншоте:

Как мы и ожидали, кнопки расположились по вертикали друг под другом. Для лучшего понимания работы сайзера на следующей картинке в виде красных прямоугольников нарисованы ячейки сайзера. Еще раз подчеркну, что "физически", если можно применить это слово к виртуальному миру, никаких полочек и ячеек в виде каких-либо панелей не существует, но такая абстракция помогает легче понять работу сайзеров. Итак, то же окно с нарисованным сайзером:

Как видите, полки сайзера заняли все окно по горизонтали, и справа есть еще много места, образовавшегося благодаря тому, что сайзер является основным для окна и он старается занять по возможности как можно больше места. Чтобы убедиться в том, что справа действительно есть место, выровняем первую кнопку по правому краю. Для этого мы при добавлении btn1 будем использовать флаг wx.ALIGN_RIGHT:

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1, flag = wx.ALIGN_RIGHT)
    sizer.Add (btn2)

    self.SetSizer(sizer)
    self.Layout()

В результате выполнения скрипта мы увидим следующее окно:

Или то же самое с нарисованным сайзером.

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

def doLayout(self):
    sizer = wx.BoxSizer(wx.HORIZONTAL)     

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1)
    sizer.Add (btn2)

    self.SetSizer(sizer)
    self.Layout()

В результате получим такое расположение кнопок:

Или то же самое с нарисованным сайзером:

Чтобы опять же убедиться в том, что сайзер по вертикали занимает весь размер окна, выровняем первую кнопку по нижнему краю с помощью флага wx.ALIGN_BOTTOM:

def doLayout(self):
    sizer = wx.BoxSizer(wx.HORIZONTAL)     

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1, flag = wx.ALIGN_BOTTOM)
    sizer.Add (btn2)

    self.SetSizer(sizer)
    self.Layout()

Результат работы такого скрипта (сразу с нарисованным сайзером):

Теперь поиграем с границами вокруг контролов.

Окружим вторую кнопку границей со всех сторон размером 8 пикселей. Для этого значение флага будет равно wx.ALL, а параметр border будет равен 8.

def doLayout(self):
    sizer = wx.BoxSizer(wx.HORIZONTAL)     

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1)
    sizer.Add (btn2, flag = wx.ALL, border = 8)

    self.SetSizer(sizer)
    self.Layout()

Результат работы скрипта будет выглядеть следующим образом:

И с нарисованным сайзером:

Ну и просто из эстетических соображений добавим для первой кнопки границу сверху, ведь окна приятнее смотрятся, если элементы управления у них находятся на одной уровне:

def doLayout(self):
    sizer = wx.BoxSizer(wx.HORIZONTAL)     

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1, flag = wx.TOP, border = 8)
    sizer.Add (btn2, flag = wx.ALL, border = 8)

    self.SetSizer(sizer)
    self.Layout()

Результат работы скрипта с нарисованным сайзером:

Как видите, все довольно просто.

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

def doLayout(self):
    parentsizer = wx.BoxSizer(wx.VERTICAL)
    childsizer = wx.BoxSizer(wx.VERTICAL)

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    childsizer.Add (btn1, flag = wx.ALIGN_RIGHT)
    childsizer.Add (btn2)

    parentsizer.Add (childsizer)

    self.SetSizer(parentsizer)
    self.Layout()

В этом примере для переменной parentsizer мы впервые используем метод Add(), который принимает не элемент управления, а другой сайзер. В результате выполнения скрипта мы увидим, что не смотря на то, что первую кнопку мы выравниваем по правому краю, она остается слева, так как, в отличие от главного сайзера дочерний не растягивается на всю ширину окна.

На следующем рисунке родительский сайзер нарисован красным цветом, а дочерний - синим.

Структура вложений сайзеров и элементов управления показана на следующем рисунке:

Чтобы дочерний сайзер по размерам совпадал с главным, достаточно его растянуть, указав флаг wx.EXPAND:

def doLayout(self):
    parentsizer = wx.BoxSizer(wx.VERTICAL)
    childsizer = wx.BoxSizer(wx.VERTICAL)

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    childsizer.Add (btn1, flag = wx.ALIGN_RIGHT)
    childsizer.Add (btn2)

    parentsizer.Add (childsizer, flag = wx.EXPAND)

    self.SetSizer(parentsizer)
    self.Layout()

И в результате мы увидим уже знакомое нам расположение кнопок в окне:

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

def doLayout(self):
    parentsizer = wx.BoxSizer(wx.VERTICAL)
    childsizer = wx.BoxSizer(wx.VERTICAL)

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    childsizer.Add (btn1)
    childsizer.Add (btn2)

    parentsizer.Add (childsizer, flag = wx.ALIGN_RIGHT)

    self.SetSizer(parentsizer)
    self.Layout()

Скриншот окна:

И скриншот с нарисованными сайзерами:

В следующем примере четыре кнопки располагаются в две строки и два столбца.

def doLayout(self):
    sizer_vert = wx.BoxSizer(wx.VERTICAL)

    sizer_hor_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_hor_2 = wx.BoxSizer(wx.HORIZONTAL)

    sizer_vert.Add (sizer_hor_1)
    sizer_vert.Add (sizer_hor_2)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")
    btn3 = wx.Button (self, wx.NewId(), "Button 3")
    btn4 = wx.Button (self, wx.NewId(), "Button 4")

    sizer_hor_1.Add (btn1)
    sizer_hor_1.Add (btn2)

    sizer_hor_2.Add (btn3)
    sizer_hor_2.Add (btn4)

    self.SetSizer(sizer_vert)
    self.Layout()

Здесь структура несколько сложнее. Во-первых, мы создаем главный вертикальный сайзер, который будет привязан к окну и,соответственно, растянут на всю ширину (потому что сайзер вертикальный). В коде он называется sizer_vert, он у нас будет играть роль горизонтальных полок, на каждой из которых может располагаться только один элемент. Этим элементом у нас будет другой сайзер, уже горизонтальный. На первую полку мы положим сайзер sizer_hor_1, а на вторую - sizer_hor_2. Эти горизонтальные сайзеры-ячейки будут содержать в себе кнопки, каждый сайзер по две кнопки. sizer_hor_1 содержит кнопки btn1 и btn2, а sizer_hor_1 - btn3 и btn4. Таким образом, у нас получается такая структура:

И результат работы скрипта:

Тот же скриншот с нарисованными сайзерами:

Здесь красным цветом нарисован вертикальный сайзер sizer_vert, а синим - горизонтальные вложенные сайзеры sizer_hor_1 и sizer_hor_2.

Как видно из рисунка, вертикальный красный сайзер занимает всю ширину окна, но каждая его ячейка стремится занять как можно меньше места по вертикали (при условии, что параметр proportion = 0 метода Add()). Именно этим размером и ограничивается вертикальный размер вложенных синих сайзеров, которые, наоборот, стараются по возможности уменьшить свои горизонтальные размеры (при том же условии), но занять как можно больше места по вертикали.

А теперь рассмотрим простой пример, который показывает как одни ячейки сайзера могут влиять на размеры других ячеек того же сайзера. Немного исправим предыдущий исходник и добавим вокруг второй кнопки отступы.

def doLayout(self):
    sizer_vert = wx.BoxSizer(wx.VERTICAL)

    sizer_hor_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_hor_2 = wx.BoxSizer(wx.HORIZONTAL)

    sizer_vert.Add (sizer_hor_1)
    sizer_vert.Add (sizer_hor_2)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")
    btn3 = wx.Button (self, wx.NewId(), "Button 3")
    btn4 = wx.Button (self, wx.NewId(), "Button 4")

    sizer_hor_1.Add (btn1)
    sizer_hor_1.Add (btn2, flag = wx.ALL, border = 8)

    sizer_hor_2.Add (btn3)
    sizer_hor_2.Add (btn4)

    self.SetSizer(sizer_vert)
    self.Layout()

Результат работы скрипта выглядит следующим образом:

Или то же самое с нарисованными сайзерами:

Как видно из рисунка, кнопка Button 2 увеличила высоту и первой ячейки горизонтального сайзера sizer_hor_1. То есть, У горизонтального сайзера wx.BoxSizer все ячейки имеют имеют одинаковую высоту. Аналогичный эксперимент можно проделать и с вертикальным сайзером, у него все ячейки будут иметь одинаковую ширину.

Следующий пример будет показывать использование флага wx.EXPAND, который, напомню, указывает, что элемент управления должен занимать все свободное место в ячейке. Заодно этот же пример поможет нам убедиться, что размер первой ячейки sizer_hor_1 действительно имеет такую высоту, как нарисовано на картинке. Итак, немного исправим предыдущий пример:

def doLayout(self):
    sizer_vert = wx.BoxSizer(wx.VERTICAL)

    sizer_hor_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_hor_2 = wx.BoxSizer(wx.HORIZONTAL)

    sizer_vert.Add (sizer_hor_1)
    sizer_vert.Add (sizer_hor_2)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")
    btn3 = wx.Button (self, wx.NewId(), "Button 3")
    btn4 = wx.Button (self, wx.NewId(), "Button 4")

    sizer_hor_1.Add (btn1, flag = wx.EXPAND)
    sizer_hor_1.Add (btn2, flag = wx.ALL, border = 8)

    sizer_hor_2.Add (btn3)
    sizer_hor_2.Add (btn4)

    self.SetSizer(sizer_vert)
    self.Layout()

После запуска скрипта с таким методом doLayout() мы увидим следующее расположение кнопок в окне:

И то же окно с нарисованными сайзерами:

Как мы и ожидали, кнопка Button 1 "распухла" на всю ячейку.

Вернемся к функции Add() класса wx.Sizer. Как вы помните, она имеет один необязательный параметр proportion, который определяет в каких соотношениях элементы управления одного сайзера должны занимать свободное место.

До этого везде мы неявно использовали значение, принятое по умолчанию, то есть proportion = 0. В этом случае вертикальный сайзер старается как можно больше уменьшить высоту своих ячеек. Если значение параметра proportion отлично от нуля, то поведение сайзера резко меняется. В этом случае вертикальный сайзер будет занимать все свободное по вертикали место, а высота каждой отдельной ячейки будет рассчитываться исходя из соответствующих значений параметра proportion.

Рассмотрим пример с одной кнопкой:

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")

    sizer.Add (btn1, proportion = 1)

    self.SetSizer(sizer)
    self.Layout()

Результат работы такого расположения элементов следующий:

Теперь единственная ячейка сайзера в этом случае занимает все окно.

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

То, что ячейка занимает действительно все окно можно убедиться, выровняв большую кнопку по правому краю:

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)

    btn1 = wx.Button (self, wx.NewId(), "Button 1")

    sizer.Add (btn1, proportion = 1, flag = wx.ALIGN_RIGHT)

    self.SetSizer(sizer)
    self.Layout()

В результате увидим:

А теперь вернем кнопку на свое законное место около левого края окна и добавим в тот же вертикальный сайзер вторую кнопку тоже со значением параметра proportion = 1.

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1, proportion = 1)
    sizer.Add (btn2, proportion = 1)

    self.SetSizer(sizer)
    self.Layout()

Окно с таким расположением кнопок будет выглядеть следующим образом:

Если теперь изменить размеры окна, то высоты кнопок увеличатся на одинаковое количество пикселей:

То есть соотношение между высотами кнопок всегда равно 1:1, в соответствии со значениями параметров proportion.

Изменим предыдущий пример таким образом, чтобы высота верхней кнопки всегда была бы в два раза больше нижней. Для этого установим значение proportion первой кнопки, равное 2:

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1, proportion = 2)
    sizer.Add (btn2, proportion = 1)

    self.SetSizer(sizer)
    self.Layout()

В результате получим:

Или после изменения размеров главного окна:

Как видите, верхняя кнопка по вертикали теперь всегда будет в два раза больше нижней.

Нам осталось рассмотреть еще два флага, которые мы не использовали до этого. Один из них это флаг wx.SHAPED, который обозначает, что при изменении размеров контрол должен сохранять исходные соотношения сторон. Напишем код для окна с двумя кнопками, одна из которых будет добавлена в сайзер с использованием флага wx.SHAPED. Кроме того мы будем использовать значение параметра proportion = 1, чтобы размер кнопок менялся при изменении размера окна.

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1, proportion = 1)
    sizer.Add (btn2, proportion = 1, flag = wx.SHAPED)

    self.SetSizer(sizer)
    self.Layout()

Сразу после запуска скрипта мы увидим:

Если теперь изменить размер окна, то вторая кнопка уменьшит не только свою высоту, но и ширину, чтобы соотношение сторон осталось прежним:

Ну и кратко рассмотрим последний оставшийся флаг wx.FIXED_MINSIZE. Этот флаг имеет немного специфическое назначение. При использовании этого флага сайзер не будет учитывать минимальный размер контрола, который задается с помощью метода SetMinSize(), а будет использовать минимальный размер, предустановленный для некоторых классов.

Пусть у нас есть кнопка, для которой установлен минимальный размер 200x35:

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn1.SetMinSize ( (200, 35) )

    sizer.Add (btn1)

    self.SetSizer(sizer)
    self.Layout()

Тогда мы увидим следующее расположение элементов:

А как только мы установим флаг wx.FIXED_MINSIZE, то кнопка ужмется до привычных значений:

def doLayout(self):
    sizer = wx.BoxSizer(wx.VERTICAL)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn1.SetMinSize ( (200, 35) )

    sizer.Add (btn1, flag = wx.FIXED_MINSIZE)

    self.SetSizer(sizer)
    self.Layout()

Мы рассмотрели как в ячейки сайзера можно добавлять элементы управления и другие сайзеры. Осталось рассмотреть как добавлять пустые ячейки. Для этого достаточно в качестве первого параметра метода Add() передать экземпляр класса wx.Size, который хранит размеры воображаемого невидимого элемента управления, размеры которого и будут определять размеры ячейки. Рассмотрим пример:

def doLayout(self):
    sizer_vert = wx.BoxSizer(wx.VERTICAL)

    sizer_hor_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_hor_2 = wx.BoxSizer(wx.HORIZONTAL)

    sizer_vert.Add (sizer_hor_1)
    sizer_vert.Add (wx.Size (10, 100) )
    sizer_vert.Add (sizer_hor_2)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")
    btn3 = wx.Button (self, wx.NewId(), "Button 3")
    btn4 = wx.Button (self, wx.NewId(), "Button 4")

    sizer_hor_1.Add (btn1)
    sizer_hor_1.Add (wx.Size (100, 10) )
    sizer_hor_1.Add (btn2)

    sizer_hor_2.Add (btn3)
    sizer_hor_2.Add (wx.Size (100, 10) )
    sizer_hor_2.Add (btn4)

    self.SetSizer(sizer_vert)
    self.Layout()

Здесь мы между строками вертикального сайзера, которые содержат кнопки, добавляем пустую строку, размер которой задается в виде wx.Size (10, 100). В нашем случае горизонтальный размер не влияет, так как он меньше ширины окна. А вот вертикальный размерраздвинет полки с кнопками на 100 пикселей. Аналогично в между ячейками горизонтального сайзера добавлен элемента wx.Size (100, 10), который раздвинет по горизонтали кнопки, но в нашем случае не будет влиять на вертикальный размер, так как размер 10 пикселей скорее всего будет меньше, чем вертикальный размер кнопок. Внешний вид окна с таким расположением элементов управления выглядит следующим образом:

Или с нарисованными сайзерами:

Кроме того в классе wx.Sizer есть вспомогательный метод AddSpacer(), который позволяет упростить добавление пустых ячеек, у которых ширина равна высоте.

AddSpacer(int size)

В качестве параметра метода задается сторона квадрата. Следующие две строки кода равносильны:

sizer.Add (wx.Size (20, 20) )
sizer.AddSpacer (20)

И для наглядности пример кода:

def doLayout(self):
    sizer_vert = wx.BoxSizer(wx.VERTICAL)

    sizer_hor_1 = wx.BoxSizer(wx.HORIZONTAL)
    sizer_hor_2 = wx.BoxSizer(wx.HORIZONTAL)

    sizer_vert.Add (sizer_hor_1)
    sizer_vert.AddSpacer (20)
    sizer_vert.Add (sizer_hor_2)       

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")
    btn3 = wx.Button (self, wx.NewId(), "Button 3")
    btn4 = wx.Button (self, wx.NewId(), "Button 4")

    sizer_hor_1.Add (btn1)
    sizer_hor_1.AddSpacer (10)
    sizer_hor_1.Add (btn2)

    sizer_hor_2.Add (btn3)
    sizer_hor_2.Add (btn4)

    self.SetSizer(sizer_vert)
    self.Layout()

И результат его работы сразу с нарисованными сайзерами:

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

wx.StaticBoxSizer

Класс wx.StaticBoxSizer является производным от wx.BoxSizer и отличается от него только тем, что этот сайзер помещает свои дочерние элементы в рамку с надписью, которая представляет собой экземпляр класса wx.StaticBox.

Использование класса wx.StaticBoxSizer практически ничем не отличается от использования wx.BoxSizer за исключением того, что конструктор этого класса требует еще одного параметра - экземпляра класса wx.StaticBox, который и будет обводить дочерние элементы сайзера.

__init__(self, box, orient=HORIZONTAL)  

В wxWidgets есть еще один конструктор, который принимает ссылку на родительское для StaticBox окно и текстовую строку. Этот конструктор самостоятельно создает экземпляр класса wx.StaticBox, но в wxPython такого конструктора нет и эта работа возложена на программистов.

Рассмотрим простой пример, использующий wx.StaticBoxSizer вместе с двумя кнопками.

def doLayout(self):
    staticbox = wx.StaticBox (self, wx.NewId(), label="StaticBox")
    sizer = wx.StaticBoxSizer(staticbox, wx.VERTICAL)

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")

    sizer.Add (btn1)
    sizer.Add (btn2)

    self.SetSizer(sizer)
    self.Layout()

В этом примере мы сначала создаем экземпляр класса wx.StaticBox, сайзер wx.StaticBoxSizer, а конструктор которого и передали только что созданный StaticBox, и две кнопки.

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

Обратите внимание, что StaticBox занял все окно, так как он является основным сайзером для окна. А вот если в первый wx.StaticBoxSizer вставить дочерние такие же сайзеры, то они будут обводить только элементы управления. Давайте добавим еще пару кнопок:

def doLayout(self):
    staticbox0 = wx.StaticBox (self, wx.NewId(), label="StaticBox 0")
    sizer_vert = wx.StaticBoxSizer(staticbox0, wx.VERTICAL)

    staticbox1 = wx.StaticBox (self, wx.NewId(), label="StaticBox 1")
    staticbox2 = wx.StaticBox (self, wx.NewId(), label="StaticBox 2")

    sizer_1 = wx.StaticBoxSizer(staticbox1, wx.HORIZONTAL)
    sizer_2 = wx.StaticBoxSizer(staticbox2, wx.VERTICAL)

    sizer_vert.Add (sizer_1)
    sizer_vert.Add (sizer_2, flag = wx.ALIGN_RIGHT | wx.ALIGN_BOTTOM)

    btn1 = wx.Button (self, wx.NewId(), "Button 1")
    btn2 = wx.Button (self, wx.NewId(), "Button 2")
    btn3 = wx.Button (self, wx.NewId(), "Button 3")
    btn4 = wx.Button (self, wx.NewId(), "Button 4")

    sizer_1.Add (btn1)
    sizer_1.Add (btn2)

    sizer_2.Add (btn3)
    sizer_2.Add (btn4)

Здесь мы создали три экземпляра класса wx.StaticBoxSizer и передали каждому по своему экземпляру класса wx.StaticBox. В первый вертикальный сайзер мы добавили еще два сайзера, один из них горизонтальный (sizer_1), другой вертикальный (sizer_2). Первый будет привязан по умолчанию по левому верхнему краю, а второй мы перенесли в правый нижний угол, указав флаги wx.ALIGN_RIGHT и wx.ALIGN_BOTTOM. На самом деле флаг wx.ALIGN_BOTTOM не будет играть никакой роли, так как вторая "полка" sizer_vert по высоте будет ограничена именно эти самым вторым сайзером sizer_2. Но зато этот флаг даст понять истинные размеры главного сайзера sizer_vert, потому что размер его рамки может ввести в заблуждение.

В результате выполнения этого примера мы увидим:

Или, чтобы было более понятно, то же окно с нарисованными сайзерами:

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

На этом рассмотрение сайзера wx.StaticBoxSizer закончим и перейдем к другим сайзерам.

wx.GridSizer

Следующий класс сайзеров, которые мы с вами рассмотрим называется wx.GridSizer. Этот сайзер размещает элементы управления (или дочерние сайзеры) в ячейках виртуальной таблицы. Виртуальной, потому что на самом деле никакие таблицы не создаются. Размеры всех ячеек в этой таблице одинаковые как по ширине, так и по высоте. При этом размеры ячеек зависят от того используется ли флаг wx.EXPAND для растяжения сайзера до максимально возможных приделах или нет. В случае если флаг wx.EXPAND не используется, ширина каждой ячейке равна ширине наиболее широкого элемента управления, а высота, соответственно, равна высоте наиболее высокого контрола. Если же при добавлении сайзера был установлен флаг wx.EXPAND (или сайзер является основным для окна), то вся занимаемая сайзером площадь делится поровну на количество строк и столбцов, необходимых для размещения элементов управления.

Для начала рассмотрим конструктор wx.GridSizer:

__init__(self, rows=1, cols=0, vgap=0, hgap=0)  

Он принимает четыре параметра: количество строк виртуальной таблицы (rows), количество столбцов (cols), величину зазора между строками в пикселях (vgap) и величину зазора между столбцами, так же в пикселях (hgap).

Рассмотрим примеры. Пусть сначала wx.GridSizer у нас является основным сайзером для окна. Разместим шесть кнопок в две строки и в три столбца:

def doLayout(self):
    gridsizer = wx.GridSizer(2, 3)

    for n in range (6):
        btn = wx.Button (self, id = wx.NewId(), label = "Button " + str(n) )
        gridsizer.Add (btn)

    self.SetSizer(gridsizer)
    self.Layout()

Так как основной сайзер старается занять как можно большую область окна, то в нашем случае gridsizer займет все окно, а высота строк (и, соответственно, всех ячеек) будет равна высоте клиентской области окна, деленной на количество строк. Аналогично с шириной ячеек, она будет равна ширине клиентской области окна, деленной на количество столбцов, необходимых для размещения дочерних элементов.

Сайзер из этого примера можно изобразить следующим образом:

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

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

А чтобы убедиться в том, что ячейки сайзера действительно занимают тот размер, который мы только что видели на рисунке, растянем каждую кнопку я всю ячейку с помощью флага wx.EXPAND.

def doLayout(self):
    gridsizer = wx.GridSizer(2, 3)

    for n in range (6):
        btn = wx.Button (self, id = wx.NewId(), label = "Button " + str(n) )
        gridsizer.Add (btn, flag = wx.EXPAND)

    self.SetSizer(gridsizer)
    self.Layout()

И увидим ожидаемый результат:

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

def doLayout(self):
    gridsizer = wx.GridSizer(2, 3)
    boxsizer = wx.BoxSizer (wx.VERTICAL)

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (6)]

    for button in buttons_list:
        gridsizer.Add (button)

    boxsizer.Add (gridsizer)

    self.SetSizer(boxsizer)
    self.Layout()

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

Как и следовало ожидать, размеры ячеек одинаковые и совпадают с размерами кнопок (тоже одинаковых размеров).

А теперь изменим минимально возможный размер одной кнопки и посмотрим как изменятся размеры ячеек таблицы:

def doLayout(self):
    gridsizer = wx.GridSizer(2, 3)
    boxsizer = wx.BoxSizer (wx.VERTICAL)

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (6)]
    buttons_list[0].SetMinSize ( (100, 35) )

    for button in buttons_list:
        gridsizer.Add (button)

    boxsizer.Add (gridsizer)

    self.SetSizer(boxsizer)
    self.Layout()

Все размеры ячеек опять равны, но на этот раз совпадают с размерами самой большой, кнопки "Button 0". Таблицы ячеек сайзера в этом случае можно нарисовать следующим образом:

И в следующем примере мы растянем все кнопки с помощью флага wx.EXPAND, чтобы убедиться, что размеры ячеек именно такие, как мы их себе представили:

def doLayout(self):
    gridsizer = wx.GridSizer(2, 3)
    boxsizer = wx.BoxSizer (wx.VERTICAL)

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (6)]
    buttons_list[0].SetMinSize ( (100, 35) )

    for button in buttons_list:
        gridsizer.Add (button, flag = wx.EXPAND)

    boxsizer.Add (gridsizer)

    self.SetSizer(boxsizer)
    self.Layout()

Теперь рассмотрим параметры vgap и hgap, которые задают расстояние между элементами управления, содержащимися в сайзере. Проще всего эти параметры представить в виде толщины стенок, разделяющие контролы, при этом внешние стенки сайзера-таблицы по-прежнему остаются нулевой толщины.

В следующем примере мы добавляем 10 пикселей расстояния между строками и 20 пикселей между столбцами:

def doLayout(self):
    gridsizer = wx.GridSizer(2, 3, vgap = 10, hgap = 20)
    boxsizer = wx.BoxSizer (wx.VERTICAL)

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (6)]

    for button in buttons_list:
        gridsizer.Add (button)

    boxsizer.Add (gridsizer)

    self.SetSizer(boxsizer)
    self.Layout()

В результате получим:

И тот же скриншот с нарисованным "толстостенным" сайзером:

Подобного эффекта можно было бы достигнуть, используя флаги, добавляющие рамку (border) вокруг элементов: wx.TOP, wx.BOTTOM, wx.LEFT, wx.RIGHT и wx.ALL, но в данном случае использовать параметры vgap и hgap проще, иначе пришлось бы для каждой кнопки индивидуально устанавливать с какой стороны должна быть рамка. Кроме того, рамка ведь изменяла бы и размеры ячеек.

В предыдущих примерах мы всегда явно задавали количество строк и столбцов таблицы, но ведь, зная количество элементов и количество желаемых строк, wx.GridSizer может сам рассчитывать количество необходимых столбцов. Давайте посмотрим как он будет это делать.

def doLayout(self):
    gridsizer = wx.GridSizer(rows = 2)
    boxsizer = wx.BoxSizer (wx.VERTICAL)

    count = 1

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    for button in buttons_list:
        gridsizer.Add (button)

    boxsizer.Add (gridsizer)

    self.SetSizer(boxsizer)
    self.Layout()

В этом примере мы будем менять количество кнопок от 1 до 4 с помощью изменения значения переменной count. Итак,

count = 1:

Ничего интересного, одна кнопка и есть одна кнопка.

count = 2:

Теперь уже сайзер может занять две строки, что он и делает. У нас получается один столбец.

count = 3:

У нас появится второй столбец, но обратите внимание на то, что сначала заполняется вся первая строка, а затем вторая.

count = 4:

Ну и, наконец, мы снова заполнили две строки, у нас получилось два столбца. Если хотите, можете и дальше увеличивать значение переменной count - количество столбцов будет увеличиваться, но всегда в первую очередь будет полностью заполняться сначала первая строка, а затем только вторая.

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

wx.FlexGridSizer

wx.FlexGridSizer по поведению похож на wx.GridSizer, он тоже размещает элементы управления в ячейках таблицы, но, в отличие от wx.GridSizer, у wx.FlexGridSizer строки и столбцы могут растягиваться так, чтобы занимать все оставшееся свободное место по горизонтали или по вертикали.

Конструктор wx.FlexGridSizer ни чем не отличается от конструктора wx.GridSizer:

__init__(self, rows=1, cols=0, vgap=0, hgap=0)  

Первые два параметра задают количество строк (rows) и столбцов (cols) в виртуальной таблице, а последние два параметра определяют величину зазора между соседними ячейками по вертикали (vgap) и горизонтали (hgap).

Для того, чтобы воспользоваться особенностями этого сайзера и установить какие строки и столбцы должны растягиваться предусмотрено соответственно два метода:

AddGrowableRow(self, idx, proportion = 0)
AddGrowableCol(self, idx, proportion = 0)

Эти методы ожидают два параметра:

  • idx - номер строки или столбца, начиная с 0-ого, который должен быть растягивающимся.
  • proportion - пропорции, которые определяют как нужно распределить свободное место между растягивающимися строками и столбцами. Этот параметр играет ту же роль, что и одноименный параметр в метода Add().

Рассмотрим это поведение на примерах.

Для начала создадим wx.FlexGridSizer, который не имеет растягивающихся строк или столбцов. В этом случае все ячейки имеют минимально возможные размеры, даже не смотря на то, что сайзер установлен основным для окна.

def doLayout(self):
    gridsizer = wx.FlexGridSizer(rows = 2)

    count = 4

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0])
    gridsizer.Add (buttons_list[1])
    gridsizer.Add (buttons_list[2])
    gridsizer.Add (buttons_list[3])

    self.SetSizer(gridsizer)
    self.Layout()

А теперь изменим пример так, чтобы второй столбец был "растягивающимся".

def doLayout(self):
    gridsizer = wx.FlexGridSizer(rows = 2)
    gridsizer.AddGrowableCol (1)

    count = 4

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0])
    gridsizer.Add (buttons_list[1], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[2])
    gridsizer.Add (buttons_list[3], flag = wx.ALIGN_RIGHT | wx.ALIGN_BOTTOM)

    self.SetSizer(gridsizer)
    self.Layout()

В этом примере мы с помощью вызова метода gridsizer.AddGrowableCol (1) указываем, что второй столбец (первый, считая с нулевого) должен растянуться так, чтобы занять оставшееся свободное место. Чтобы убедиться, что столбец действительно растянулся, мы вторую кнопку (buttons_list[1]) растягиваем на всю ячейку с помощью флага wx.EXPAND, а последнюю кнопку (buttons_list[3]) выравниваем по правому (флаг wx.ALIGN_RIGHT) и нижнему краю (wx.ALIGN_BOTTOM). В данном случае выравнивание по нижнему краю ничего не даст, так как растягивается только второй столбец, а не строка, но благодаря этому мы еще раз убедимся, что по вертикали размер строк не изменился.

Теперь можно изменить размер главного окна и убедиться, что ширина второго столбца при этом тоже увеличится на то же количество пикселей:

Теперь поиграем с растягиванием строк.

def doLayout(self):
    gridsizer = wx.FlexGridSizer(rows = 2)
    gridsizer.AddGrowableRow (1)

    count = 4

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0])
    gridsizer.Add (buttons_list[1])
    gridsizer.Add (buttons_list[2], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[3], flag = wx.ALIGN_RIGHT | wx.ALIGN_BOTTOM)

    self.SetSizer(gridsizer)
    self.Layout()

В этом примере с помощью вызова gridsizer.AddGrowableRow (1) мы указываем, что вторая строка (первая, считая с нулевой) должна быть растягивающейся. На этот раз растягиваться на всю ячейку будет кнопка buttons_list[2], а флаги для buttons_list[3] останутся те же самые. И в результате получим:

Опять же можем изменить размер окна и убедиться, что и высота второй строки изменится:

Для наглядности на следующем рисунке приведено то же самое окно с нарисованными ячейками сайзера:

Кроме того для одного сайзера можно указывать, что растягивающихся строк и (или) столбцов может быть несколько. В следующем примере мы создаем шесть кнопок, расположенные в две строки и три столбца и указываем, что последние два столбцы должны быть растягивающимися. Для кнопок, которые должны попасть в эти два столбца мы устанавливаем флаг wx.EXPAND, чтобы растянуть их на всю ячейку.

def doLayout(self):
    gridsizer = wx.FlexGridSizer(rows = 2)
    gridsizer.AddGrowableCol (1)
    gridsizer.AddGrowableCol (2)

    count = 6

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0])
    gridsizer.Add (buttons_list[1], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[2], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[3])
    gridsizer.Add (buttons_list[4], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[5], flag = wx.EXPAND)

    self.SetSizer(gridsizer)
    self.Layout()

В результате получаем окно:

Как видите, в данном случае растянувшиеся столбцы поделили поровну доставшееся им свободное место. Чтобы изменить соотношение доставшегося места между столбцами изменим значение параметров proportion. С следующем примере соотношение ширин последних двух столбцов будет равно 1:3:

def doLayout(self):
    gridsizer = wx.FlexGridSizer(rows = 2)
    gridsizer.AddGrowableCol (1, proportion = 1)
    gridsizer.AddGrowableCol (2, proportion = 3)

    count = 6

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0])
    gridsizer.Add (buttons_list[1], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[2], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[3])
    gridsizer.Add (buttons_list[4], flag = wx.EXPAND)
    gridsizer.Add (buttons_list[5], flag = wx.EXPAND)

    self.SetSizer(gridsizer)
    self.Layout()

Результат довольно ожидаем:

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

def doLayout(self):
    gridsizer = wx.FlexGridSizer(rows = 2)
    gridsizer.AddGrowableRow (1)
    gridsizer.AddGrowableCol (1)

    count = 4

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0])
    gridsizer.Add (buttons_list[1])
    gridsizer.Add (buttons_list[2], flag = wx.ALIGN_BOTTOM)
    gridsizer.Add (buttons_list[3], flag = wx.EXPAND)

    self.SetSizer(gridsizer)
    self.Layout()

В результате последняя кнопка, которой досталась последняя строка и последний столбец заметно "прибавляет в весе":

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

wx.GridBagSizer

Нам осталось рассмотреть последний из сайзеров - wx.GridBagSizer. Этот сайзер, как и два предыдущих расставляет элементы управления в ячейках таблицы. Отличие состоит в том, что, в отличие от предыдущих сайзеров, этот сайзер не рассчитывает количество столбцов исходя из количества строк, а при добавлении элемента программист должен явно указать в какую ячейку он хочет поместить добавляемый элемент. Кроме того один элемент управления может занимать сразу несколько ячеек. Все ячейки при этом имеют одинаковый размер, а сам сайзер растянуть на все окно мне так и не удалось, его ячейки всегда имеют минимально возможный размер.

Конструктор wx.GridBagSizer даже проще, чем двух предыдущих сайзеров:

__init__(self, vgap=0, hgap=0)  

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

Метод Add() этого сайзера тоже изменился:

Add(self, item, pos, span=DefaultSpan, flag=0, border=0, userData=None)

В нем добавились два параметра:

  • pos определяет в какую ячейку должен будет добавиться элемент. В общем случае этот параметр должен быть экземпляром класса wx.GBPosition, но в wxPython в качестве этого параметра может выступать двухэлементная последовательность, первый элемент которой должен обозначать номер строки, а второй - номер столбца.
  • span определяет сколько ячеек по горизонтали и вертикали должен занимать добавляемый элемент. В общем случае это должен быть экземпляр класса wx.GBSpan, но в версии wxPython в качестве этого параметра опять же может выступать двухэлементная последовательность, первый элемент которой должен обозначать количество занимаемых строк, а второй - количество столбцов. Значением по умолчанию для этого параметра является (1, 1).

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

В качестве примера для начала получим следующее расположение кнопок в окне:

Мы могли бы получить такое же расположение используя сайзер wx.GridSizer, если бы некоторые клетки заполними пустыми местами (добавили бы экземпляры класса wx.Size), но с wx.GridBagSizer это сделать проще:

def doLayout(self):
    gridsizer = wx.GridBagSizer()

    count = 4

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0], pos = (0, 1) )
    gridsizer.Add (buttons_list[1], pos = (1, 0) )
    gridsizer.Add (buttons_list[2], pos = (1, 2) )
    gridsizer.Add (buttons_list[3], pos = (2, 1) )

    self.SetSizer(gridsizer)
    self.Layout()

При добавлении кнопки в сайзера мы просто указываем куда именно ее необходимо поместить. На следующем рисунке показано то же окно с нарисованной таблицей сайзера:

Теперь воспользуемся особенностями этого сайзера и сделаем так, чтобы кнопки заняли бы не одну, а две клетки виртуальной таблицы:

Для этого при добавлении нулевой и второй кнопки указать сколько клеток они должны занимать в обоих напрвлениях с помощью параметра span:

def doLayout(self):
    gridsizer = wx.GridBagSizer()

    count = 4

    buttons_list = [wx.Button (self, id = wx.NewId(), label = "Button " + str(n) ) for n in range (count)]

    gridsizer.Add (buttons_list[0], pos = (0, 1), span = (1, 2), flag = wx.EXPAND )
    gridsizer.Add (buttons_list[1], pos = (1, 0), span = (1, 1), flag = wx.EXPAND )
    gridsizer.Add (buttons_list[2], pos = (1, 2), span = (2, 1), flag = wx.EXPAND )
    gridsizer.Add (buttons_list[3], pos = (2, 1), span = (1, 1), flag = wx.EXPAND )

    self.SetSizer(gridsizer)
    self.Layout()

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

Для наглядности на следующем рисунке то же самое окно, но с нарисованным сайзером. Границы между ячейками, которые пересекают кнопки, нарисованы более тонкой линией:

Заключение

Вот мы и рассмотрели все существующие в wxPython / wxWidgets сайзеры, а заодно научились ими пользоваться. Статья получилась довольно большой, но, надеюсь, что понятной благодаря большому числу примеров. А еще надеюсь, что вам эта статья понравилась и помогла разобраться с тем, как создавать интерфейсы с помощью сайзеров. Все исходники, приведенные в статье вы можете скачать отсюда.

Думаю, что вам стоит почитать еще серию небольших статей Бесплатные дизайнеры интерфейсов для wxPython, в которой приводятся обзоры различных визуальных редакторов интерфейсов для wxPython.

Всего хорошего и удачи.

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 5.0/5. Всего 95 голос(а, ов)



MiDNiT3 12.09.2009 - 17:44

отличная статья

Jenyay 12.09.2009 - 21:34

MiDNiT3, спасибо :)

 06.11.2009 - 16:34

Прекрасная статья. Читая ее я не раз мысленно сравнивал с лайоутами в Qt и надо отметить, что в последнем все сделано немного более логично. Например, совершенно непонятно, зачем использовать какое-то выравнивание элементов, когда этим должен заведовать spacer, просто вставляем растяжку туда, где должно быть свободное место - и пожалуйста, вот вам выравнивание. И еще совсем непонятно, каким образом то, является ли сайзер главным для формы или нет, должно влиять на на логику его работы confused smileyconfused smileyconfused smiley

Так что вывод: если хотите быстро создавать приложения с красивым интерфейсом для повседневных целей, лучше начать с Qt. Однако, если вас вдруг начнет душить жаба, что вашему приложеньицу для работы надо тащить за собой 20 метров (это базовый вариант - QtCore и QtGui, а если захотите встроить еще и скрипты...), то стоит глянуть в сторону wxWidgets

axe 27.02.2010 - 11:35

границы сайзеров

как отобразить границы сайзеров?

Jenyay 27.02.2010 - 19:29

Боюсь, что простым способом никак. Можно сделать так, что в сайзере будет лежать панель с рамкой, а уже в ней нужные элементы, но это не очень удобно.

Eugene 19.08.2010 - 01:48

Очень полезно для начала. Большое спасибо!

rez 31.01.2011 - 19:11

СПАСИБО

MoDen 18.08.2011 - 14:57

Все понятно. Четко. Молодец!

Yoschi 22.07.2012 - 14:21

Спасибо за хорошую вводную!

Подсказочка насчёт GridBagSizer и "сам сайзер растянуть на все окно мне так и не удалось, его ячейки всегда имеют минимально возможный размер."

Согласно документации, этот класс наследуется от FlexGridSizer и соответственно, обладает его возможностями. AddGrowableCol и AddGrowableRow работают - я проверил winking smiley

Александр 08.04.2020 - 20:57

Сайзеры в wxWidgets / wxPython

Как редко можно встретить столь замечательно раскрытую тему.
Я в восторге!!!
Всё разложено по полочкам в определённом порядке и объяснено.
Спасибо.

Сяв 20.04.2020 - 19:50

Автору ОГРОМНАЯ благодарность!
Мне действительно не удалось разобраться с интерфейсом, и эта статься очень помогла!!!

MihaPog 06.03.2021 - 18:11

Спасибо!

happy smiley Спасибо!

MihaPog 06.03.2021 - 18:24

Спасибо!

happy smiley Спасибо!

Антон 10.02.2023 - 01:39

как задать размеры кнопок?

Статья замечательная, почти научная! =).
Но вопрос. А как задавать для UI-элементов, например кнопок, размеры в %? Только помещая их в ячейки сайзеров и задавая proportion? Для самих элементов я увидел только задание размера в пикселях, что как правило не очень хорошо.

Антон 10.02.2023 - 03:12

как задать размеры кнопок?

Статья замечательная, почти научная! =).
Но вопрос. А как задавать для UI-элементов, например кнопок, размеры в %? Только помещая их в ячейки сайзеров и задавая proportion? Для самих элементов я увидел только задание размера в пикселях, что как правило не очень хорошо.

Антон 10.02.2023 - 03:12

как задать размеры кнопок?

Статья замечательная, почти научная! =).
Но вопрос. А как задавать для UI-элементов, например кнопок, размеры в %? Только помещая их в ячейки сайзеров и задавая proportion? Для самих элементов я увидел только задание размера в пикселях, что как правило не очень хорошо.

Jenyay 10.02.2023 - 09:15

> А как задавать для UI-элементов, например кнопок, размеры в %? Только помещая их в ячейки сайзеров и задавая proportion?

Да, других, более прямых способов я не знаю.

unreal666 20.06.2024 - 12:44

Исправь во многих местах GradBagSizer на GridBagSizer

Jenyay 20.06.2024 - 14:07

Спасибо, поправил.


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 633