Pull to refresh

Сказ о wx.Python

Reading time 13 min
Views 24K
Здравствуй хабрхабр!

В данной статье я хотел бы рассказать, сформулировать свои мысли по поводу такой замечательной библиотеки как wxPython. Под катом вы найдете небольшую теорию, описание форм, разбор свойств форм, различных контролов и всё что касается wxPython.
Welcome to wxPython.


the initial data



Некоторая теория-вступление.

1. wxPython in action (Russian) — русская версия книги (о её качестве и происхождении ничего сказать не могу, поскольку предпочитаю оригинал, а вот собственно и он — wxPython in action (English) — тут она стоит 54$ в бумажном виде (электронный вид, надеюсь, найдете сами). Общее мнение — книга отличная. Все подробно описано и написано. Конечно многие моменты не подробно расписаны, а кое-какие вещи лучше всего посмотреть в документации, но в общем и целом все примеры правдивы, рабочие. Книга отлично подойдет новичкам.

2. wxPython 2.8 Application Development Cookbook. Данную книгу в русском варианте даже не пытался найти, ибо скорее всего её нет. Данная книга содержит в себе «рецепты» написания GUI форм, зачастую не тривиальны и не просты для разбора (хотя попытки разобраться каждую строчку в книге имеется. Читать довольно легко.

3. python.su — отличный форум с хорошей базой топиков по различным темам. Не сочтите за рекламу, но он действительно хорош, жаль в последнее время не стабильно работает.

4. habrahabr.ru — а вот тут меня ждало фиаско. На тот момент когда я смотрел, не было ни одной статьи по данной библиотеке. Только не давно промелькала маленькая статья основ (тех что в книжке написаны на первых страницах). Собственно уже тогда у меня зародилась идея по написанию данной статьи.

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

1. wxPythom demos and docs. Это самый лучший пример любых контролов и форм написанных с использованием библиотеки wxpython. Там вы найдете примеры различных форм и контролов, списков, деревьев и различные их комбинации. Исходники этих всех примеров не богаты комментариями, поэтому даже хорошо что я её нашел потом уже, когда понимал что к чему, это очень сильно облегчало понимание того что происходит.

2. wxFormBuilder — приложение-конструктор форм и контролов для wxPython ( и не только ). Данное приложение я обнаружил когда написал руками около 30 форм. Т.е. руками было написано все, от размеров формы, до использования различных типов сайзеров и их свойства. Тогда я смог просто экономить время, по скольку знал что и куда, как и почему. Данное приложение не обладает полным функционалом всей библиотеки, и это понятно, wx сложная и очень объемная библиотека, полное написание конструктора библиотеки займёт огромное количество времени, да и особой необходимости в этом нет. Кидаться и делать формы при помощи данного приложения сразу не стоит, тогда вы не уловите и 30% всего функционала библиотеки и ваш интерфейс ограничится тремя кнопками и двумя событиями.

introduction



Прежде чем начать углубляться в различные виды окошек и их свойства, необходимо усвоить несколько моментов, без которых будет сложно понять что и как должно быть:
1. Все элементы должны находиться в сайзере. Сайзер (wx.Sizer) это некий невидимый элемент формы, который обозначает местоположение всех компонентов формы.
2. Иерархия следующая: Форма (wx.Frame) на ней расположен сайзер, в сайзер добавлен контейнер — панель (wx.Panel), на панели добавлен еще один сайзер, и уже в данный сайзер добавляются все элементы формы.
3. Неправильно добавлять компонент на панель, не добавляя его в сайзер ( а wxFormBuilder и не позволит вам этого).
4. Свойства всех контролов можно не прописывать полностью. Допустим, если вам не важен размер поле для текстового ввода, то автоматически оно построится стандартным.

basic elements



В wx есть формы нескольких типов:
wx.Dialog — диалоговое окно.
wx.Frame — обычная форма представляющая собой окошко со стандартным функционалом (свернуть (wx.ICONIZE), максимизировать (wx.MAXIMIZE), закрыть (wx.Close)).

Обычное представление wx.Frame в коде:
wx.Frame.__init__ ( self, parent=None, id = wx.ID_ANY, title = u"NameWindow", pos = wx.DefaultPosition, size = wx.Size( int,int ), style = wx.CAPTION  )


Данная строчка кода инициализирует форму и рисует её используя введенные свойства. К слову говоря:
wx.Frame.__init__ (self, parent = None)

Тоже нарисует форму, а все свойства форму будут стандартными.

self и parent два обязательных параметра любой формы.

Разберем инициализацию формы:
self обозначает принадлежность к классу, parent — родитель этого окна. id — каждый элемент, окно имеет собственный id, чтобы обращаться или присваивать элементу действие, событие. Можно не заморачиваться с этим и ставить -1 или wx.ID_ANY — что формализует собой любой id (Каждый элемент должен иметь свой уникальный id, который можно использовать для обращения или для присваивания действий элементу; если это не требуется, то в качестве id можно задавать значение wx.ID_ANY, которое, фактически, равно -1.). title — заголовок окна, pos — расположение формы по координатам x и y на экране, size — размер формы в пикселях, style — стиль окна. Всевозможные стили вы можете посмотреть в документации. Отмечу несколько:
wx.CAPTION — отображает верхний бар с заголовком, но без системного меню; wx.STAY_ON_TOP — оставаться поверх окон других приложений, хочется отметить одну деталь: в случае если несколько окон будут иметь данный стиль, на самом «топе» останется то окно, которое будет выше в родительской иерархии в приложении; wx.SYSTEM_MENU — на верхнем баре появляется кнопка «закрыть», но при этом она не активна;

Отличия Dialog от Frame следующие:
1. Dialog модальное окно, т.е. до тех пор, пока не выполнишь действия в этом окне, пользователь не сможет перейти в какое либо еще окно открытое
этим приложением.
2. Dialog окно содержит в себе уже панель, поэтому для диалогового окна необходим только сайзер и все элементы уже располагать на этом сайзере.
3. Диалоговое окно может обойтись без дополнительных событий на кнопки (об этом позже).

Обилие различных контролов, которыми обладает библиотека поражает воображение. Здесь есть всё, от кнопок до ноутбука с вкладками (их, причем, несколько видов).

Рассказывать здесь обо всём подряд бессмысленно, это затянется на несколько месяцев, поэтому вкратце пробежимся по нескольким элементам, объяснив моменты, которые стандартные практически для всех контролов и элементов.

Простейшие элементы показаны в следующем куске кода:

class regexps ( wx.Frame ):
    def __init__( self, main, schema, table, connection ):
		wx.Frame.__init__ ( self, parent=None, id = wx.ID_ANY, title = u"Data Quality -- Выбор и отладка регулярных выражений", 
                            pos = wx.DefaultPosition, size = wx.Size( 510,477 ), style = wx.CAPTION|wx.STAY_ON_TOP|wx.TAB_TRAVERSAL )	
        sizer1 = wx.BoxSizer( wx.HORIZONTAL )
        self.regexps_notebook = wx.Notebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
        self.dq_params_tab = wx.Panel( self.regexps_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )

Инициализация окошка типа wx.Frame, как видите все свойства присутсвуют. Некоторые из них отданы стандартным значениям. Здесь стоит обратить внимание на компоновку стилей. Мы получаем окошко с верхним баром, и системным меню в виде неактивной кнопки «Закрыть». Также, окно будет всегда активно (wx.STAY_ON_TOP). Объявление сайзера. Тип сайзера wx.BoxSizer — это коробка. Все эдементы выстраиваться друг за другом в линию либо по горизонтали, либо по вертикали (за это отвечает аргумент передаваемый сайзеру, в данном случае это wx.HORIZONTAL). Объявление ноутбука(панель с вкладками). Для начала ему дано имя: self.regexps_notebook, чтобы можно было образаться к нему из любого метода класса. Далее идет объявление компонента wx.Notebook со свойствами.Объявление панели, которая помещается на ноутбук. Заметьте, что панель помещаемая на ноутбук будет являться площадкой для всех вкладок. Также, помещение панели в роли вкладки ноутбка не требует отдельного сайзера (это противоречит тому что все элементы должны быть в сайзерах, т.е. это является исключением из этого правила)

        sizer2 = wx.BoxSizer( wx.VERTICAL )
        self.use_param_checkbox = wx.CheckBox( self.dq_params_tab, wx.ID_ANY, u"Использовать параметр", wx.DefaultPosition, 
                                               wx.DefaultSize, 0 )

Объявление второго сайзера. Минимальный размер сайзера. Добавляем новый компонент чекбокс. Добавляем его на панель self.dq_params_tab, т.е. на вкладку ноутбука.

        sizer2.Add( self.use_param_checkbox, 0, wx.ALL, 5 )
        self.params_quality_choices = [1,2,3,4,5,6,7,8,9,10]
        
        self.params_choice_pull = wx.Choice( self.dq_params_tab, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 
                                             self.params_quality_choices, 0 )
		# Обозначение индекс варианта, который будет выбран изначально после инициализации элемента wx.Choice.
        self.params_choice_pull.SetSelection( 0 )
        
		# Добавлени на вкладку статической линии. 
        self.static_line = wx.StaticLine( self.dq_params_tab, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )

Важный момент. Добавление созданного чекбокса на сайзер, этот тот сайзер который лежит на панели (вкладке).Добавление имеют следующую структуру: sizer2 — созданный сайзер, Add — вызываем метод Добавления элемента, и аргументы метода Add: что добавляем(self.use_param_checkbox, wx.ALL — это параметр отображения, в данном случае элменет будет показан в весь размер, 0(это пропорциональность элемента и 5 (это размер бордюров элемента) Далее идет объявление выпадающего списка со значениями. Собственно self.params_quality_choices это список значений. Далее уже сам элемент wx.Choice, который тоже помещается на вкладку.


        sizer2.Add( self.static_line, 0, wx.EXPAND |wx.ALL, 5 )
        self.static_text = wx.StaticText( self.dq_params_tab, wx.ID_ANY, u"Ввод весового коэффициента", wx.DefaultPosition, 
                                          wx.DefaultSize, 0 )
        sizer2.Add( self.static_text, 0, wx.ALL, 5 )
        self.weights_txt = wx.TextCtrl( self.dq_params_tab, validator = WeightsValidator() )
        sizer2.Add( self.weights_txt, 0, wx.ALL, 5 )

Вот тут у нас появилась дополнительная опция. wx.EXPAND — это означает что добавляемый элемент будет растягиваться по ширине, в завимимости это своего размера. Добавление текстового поля для ввода wx.TextCtrl. Здесь есть дополнительная опция validator. Об этом будет расказано в другом коде.

        self.dq_params_tab.SetSizer( sizer2 )
        self.dq_params_tab.Layout()
        sizer2.Fit( self.dq_params_tab )
		
		# Добавление вкладки на панель.
        self.regexps_notebook.AddPage( self.dq_params_tab, u"Параметры оценки", False )
        self.regexps_tab = wx.Panel( self.regexps_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
        sizer3 = wx.BoxSizer( wx.VERTICAL )

Пришли к интересному моменту. После добавления всех элементов на сайзер (sizer2), который на панели (self.dq_params_tab), необходимо «расширить» сайзер на всю площадь панели и закрепить сайзер на панели.

		regexps_listboxChoices = []
        self.regexps_listbox = wx.ListBox( self.regexps_tab, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 
                                           regexps_listboxChoices, wx.LB_HSCROLL|wx.LB_SINGLE )
        
		# Объявление грида (wx.grid.Grid). Это каркас таблицы, как в mysql, oracle, sqlite and etc. В опциях указан размер, и наличие скроллов.
        self.check_grid = wx.grid.Grid( self.check_sql_tab, wx.ID_ANY, wx.DefaultPosition, wx.Size(480,407), wx.HSCROLL|wx.VSCROLL )
        
		# Создание непосредственно грида, количества колонок и строк.
        # Grid
        self.check_grid.CreateGrid( 5, 5 )
		
		# Задание свойств грида.
        self.check_grid.EnableEditing( False )
        self.check_grid.EnableGridLines( True )
        self.check_grid.EnableDragGridSize( False )
        self.check_grid.SetMargins( 0, 0 )
        
		# Задание свойств колонок
        # Columns
        self.check_grid.EnableDragColMove( False )
        self.check_grid.EnableDragColSize( False )
        self.check_grid.SetColLabelSize( 20 )
        self.check_grid.SetColLabelAlignment( wx.ALIGN_CENTRE, wx.ALIGN_CENTRE )
        
		# Свойства строк.
        # Rows
        self.check_grid.AutoSizeRows( True )
        self.check_grid.EnableDragRowSize( True )
        self.check_grid.SetRowLabelSize( 40 )
        self.check_grid.SetRowLabelAlignment( wx.ALIGN_CENTRE, wx.ALIGN_CENTRE )

Добавление листбокса (wx.ListBox). Опции wx.LB_HSCROLL|wx.LB_SINGLE говорят о наличии горизонтального скрола и о том, что выделять можно только один элемент в списке. Создание грида. Далее все в комментариях описано.

Синтаксис отступлений немного не понятен, так как строки имеют большую длинную. Таков уж синтаксис библиотеки.

В коде пропущены похожие конструкции и расположение элементов, но если запустить полный код всего модуля увидим мы следующее (если кого интересует полный код — пишите):


1-ая вкладка

2-ая вкладка

3-тья вкладка.

building list control



Разобрав некоторые простейшие элементы перейдем к более сложным вещам.

Лист контрол универсальный элемент и мы его видим очень часто в различных приложениях.

Сейчас мы разберем код построения лист контрола для случайных данных.

class main_stat(listmix.ColumnSorterMixin): 
    def __init__( self, rows, columns ):     
        data = []
		self.columns = columns
		self.rows = rows
        self.list = wx.ListCtrl(self.main.panelMainStat, 0,
                                 style=wx.LC_REPORT | wx.BORDER_NONE | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)

Объявление класса в качестве аргумента передаем компонент listmix'a для сортировки данных по колонкам. Далее происходит инициализация листконтрола в котором происходит всё самое сокровенное.
Передаем в него наименование колонок (columns) и строки (rows). Длина списка со строками должна равняться длине списка с наименованием колонок. Обзываем листконтрол self.list и прописываем ему свойства.
По поводу свойств: стиль wx.LC_REPORT — листконтрол в виде наименований колонок сверху, все данные под колонками, как таблица, некое подобие. Остальные стили итак понятны исходя из перевода с английского.

        for col, text in enumerate(self.columns):
            self.list.InsertColumn(col, text) 
        for item in rows:   
            info = '%s:(%s)' % (col, item)
            data.append(info)
            index = self.list.InsertStringItem(sys.maxint, item[0]) 
            for col, text in enumerate(item[1:]): 
                self.list.SetStringItem(index, col+1, text) 

Далее чутка хитрый алгоритм формирования данных. К wx это особого отношения не имеет, поэтому подробно раписывать не буду. Но смысл в том,
что данные для лист контрола должны быть следующего вида:
rows = { 1:(«data0», «data0.1», «data0.2»), 2:(«data1», «data1.0», «data1.1», «data1.2»),… }
Приведенный выше алгоритм приводит случайные данные к такому виду.

        self.list.SetSize((900, 200))        
        self.list.SetColumnWidth(0, 120)   
        self.list.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClick, self.list)
        self.list.Bind(wx.EVT_COMMAND_RIGHT_CLICK, self.OnRightClick)
        self.list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
        self.itemDataMap = data
        listmix.ColumnSorterMixin.__init__(self, 3)

Выставляем размер листконтрола, размер колонки и биндим события (об этом подробнее будет дальше). Далее идет не совсем рабочий кусок кода, поскольку
self.itemDataMap должны равняться данным приведенным к виду как показано выше, на данные момент данные сразу заносятся в листконтрол, без сортировки.

    def getColumnText(self, index, col):
        item = self.list.GetItem(index, col)
        return item.GetText()
    
    def OnRightClick(self, event):
        index = self.list.GetFirstSelected()
		print index
        
    def OnColClick(self, event):
        print ("OnColClick: %d\n" % event.GetColumn())
        event.Skip()
    
    def OnItemSelected(self, event):
        self.currentItem = event.m_itemIndex
        self.data = (self.getColumnText(self.currentItem, 1),
                            self.getColumnText(self.currentItem, 2),
                            self.getColumnText(self.currentItem, 3),
                            self.getColumnText(self.currentItem, 4))( True )


Правый клик по строке с данными выведет нам порядковый номер этой строки. Метод OnColClick выдаст нам порядковый номер колонки. А при выделении строки с данными, нам в переменную
self.data будет записан список с данными которые мы выделили ( как раз такие здесь, используется метод getColumnText, при помощи него мы вытаскиваем данные из каждой колонки.


Пример построенного листконтрола.

menu



Теперь, давайте перейдем в неотъемлемой части любого приложения — меню.
        self.menubar = wx.MenuBar( 0 )
        self.BD = wx.Menu()
        self.m_menuItem1 = wx.MenuItem( self.BD, 1, u"Подключиться к базе", u'Выполнить подключение к базе данных', wx.ITEM_NORMAL )
        self.BD.AppendItem(self.m_menuItem1, )
        
        self.m_menuItem2 = wx.MenuItem( self.BD, 2, u"Выбрать таблицу\tCtrl+T", u'Выбрать схему и таблицу для оценки качества данных', wx.ITEM_NORMAL )
        self.BD.AppendItem( self.m_menuItem2 )
        
        self.m_menuItem3 = wx.MenuItem( self.BD, 3, u"Отключиться и выйти\tCtrl+Q", u'Отключиться от базы данных в выйти из приложения', wx.ITEM_NORMAL )
        self.BD.AppendItem( self.m_menuItem3 )	

Объявление menubar. Менюбар это полоса где находится все пункты меню с выпадающими списками. self.BD = wx.Menu() — создание элемента меню, далее добавление
пунктов меню в self.BD. Разберем подробней:
self.m_menuItem1 = wx.MenuItem( self.BD, 1, u"Подключиться к базе", u'Выполнить подключение к базе данных', wx.ITEM_NORMAL )

wx.MenuItem(в какое меню добавляем пункт меню, id, название\hotkey, текст-подсказка высвечиваемый в статусной строке (если она есть), тип пункта меню)
После названия через \t (символ табуляции) идет хоткей, по нажатию на который пункт меню вызовется, если он активный. Обрабатывать дополнительно данное событие не надо.
Типы пункта меню могут быть различными:
wx.ITEM_RADIO — итем меню в видео радио-кнопки, wx.ITEM_CHECK — в виде чекбокса,
Далее мы добавляем несколько пунктов меню по такой же схеме.

        self.statusbar = self.CreateStatusBar( 1, wx.ST_SIZEGRIP, wx.ID_ANY )
		self.BD.Enable(2, False)
		self.statusbar.SetStatusText(u'Это статусная строка, здесь будет информацию о текущем состоянии программы, а также подсказки по ходу работы с программой.')

Создание статусной строки. И представляем один из пунктов меню неактивным. Активным его сделает аргумент True. Также мы добавляем текст в статусную строку сразу при инициализации класса.


А вот и меню.

event-driven environment



Любой элемент, любая форма подвержены воздействию со стороны пользователя. При помощи библиотеки wxPython вы можете описать абсолютно любое действия пользователя, и не только пользователя, но и программы.
Начнем с правил объявления событий. У нас есть кнопка:
self.ok_btn = wx.Button( self.m_panel5, wx.ID_ANY, u"OK", wx.DefaultPosition, wx.DefaultSize, 0 )

И мы хотим чтобы по нажатию на кнопку программа складывала 2+2:
self.ok_btn.Bind( wx.EVT_BUTTON, self.OnOk )
# есть еще альтернативный синтаксис
# self.Bind( wx.EVT_BUTTON, self.OnOk, self.ok_btn )
def OnOk(self, event):
      a = 2+2
      print a

Метод OnOk сработает на нажатие кнопки. Собственно это вся суть событий. Различия лишь в wx.EVT_BUTTON (в обрабатываемом событии) и элементе на который мы вешаем данное событие (в нашем случае это кнопка self.ok_btn.

# 1
self.Bind(wx.EVT_MENU, self.About, id=10)
# 2
self.Bind( wx.EVT_IDLE, self.OnInit )


Событие №1 сработает на нажатие соответствующий пункт меню, а событие №2 при появлении простаивании формы (бездействии пользователя).

validators



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

Вот пример с объяснениями:

class Numbers(wx.PyValidator):
    def __init__(self):
        wx.PyValidator.__init__(self)
        self.Bind(wx.EVT_CHAR, self.OnChar)

    def Clone(self):
        return Numbers()

    def Validate(self, win):
        tc = self.GetWindow()
        val = tc.GetValue()

        for x in val:
            if x not in string.digits:
                return False

        return True


    def OnChar(self, event):
        key = event.GetKeyCode()
        try:
            # 8 это код клавиши backspace
            if chr(key) in string.digits or chr(key) == '.' or key == 8:
                event.Skip()
            else:
                return False
        except ValueError, info:
            print chr(key)
            print info
        return     

Создаем класс аргументом которого мы выдаем wx.PyValidator который говорит, о том что данный класс является валидатором.
Метод OnChar описан событием self.Bind(wx.EVT_CHAR, self.OnChar), и будет исполнятся при вводе с клавиатуры символов. Командной key = event.GetKeyCode() мы ловим код каждого нажатия, а встроенная функция chr() переводит символ кода в обычное представление нажатой клавиши. string.digits содержит в себе список чисел от 0 до 9, также в классе string присутствуют практически все символы которые вводятся с клавиатуры, вплоть до hexdigits (к сожалению там есть даже asciiсимволы, но русский язык, на сколько я понял, туда не входит).

Метод Validate.
tc = self.GetWindow() определяет окно где находится текстовое поле, val введенное значение, далее цикл проверки. Данный валидатор не позволит пользователю нажать ни одну клавишу кроме цифр, точки и backspace. Кстати говоря, о backspace, wx описывает все клавиши, wx.WXK_Numpad0 например, а вот backspace там нет, есть всё, от F1 до Numlock, поэтому пришлось отследить значения клавиши.

После написания валидатора необходимо определить текстовое поле и указать ему на валидатор, который ему соответствует.
self.ip_ctrl = wx.TextCtrl( self.m_panel5, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( 190,-1 ), 0, Numbers() )


На этом собственно всё. Статья получилось очень объемной и немножко сумбурной. Всё потому что если начать описывать ВСЕ свойства и события которые могут быть привязаны, допустим, к wx.ListCtrl то статья получится даже больше чем эта. Если статья понравилась и есть смысл продолжать, то начну написание следующей, раскрывая другие элементы и фичи библиотеки, например wx.aui — это продвинутый notebook с вкладками, там есть где развернутся.

Спасибо за внимание.

ADDITION:
Код
class regexps ( wx.Frame ):

def __init__( self, main, schema, table, connection ):

wx.Frame.__init__ ( self, parent=None, id = wx.ID_ANY, ...)

можно переписать более красиво через super:

class regexps ( wx.Frame ):

def __init__( self, main, schema, table, connection ):

super (regexps, self).__init__ (parent=None, id = wx.ID_ANY, ...)

by Jenyay
Tags:
Hubs:
+31
Comments 24
Comments Comments 24

Articles