Архив рубрики: Python

wxPython in Action. Глава 9. Возможность выбора. Диалоги. (часть 1)

В этой главе рассказывается о том, как:

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

9.1 Работа с модальными диалогами

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

9.1.1 Как создать модальный диалог

Модальный диалог не позволяет другим виджетам получать события от пользователя до тех пор, пока диалог не будет закрыт. Как видно из рисунка 9.1, по внешнему виду не всегда можно отличить, является ли открытое окно диалогом или фреймом.
рис 9.1
Листинг 9.1
import wx
class SubclassDialog(wx.Dialog):
def __init__(self): #инициируем диалог
wx.Dialog.__init__(self, None, -1, 'Dialog Subclass',size=(300, 100))
okButton = wx.Button(self, wx.ID_OK, "OK", pos=(15, 15))
okButton.SetDefault()
cancelButton = wx.Button(self, wx.ID_CANCEL, "Cancel",pos=(115, 15))
if __name__ == '__main__':
app = wx.PySimpleApp()
dialog = SubclassDialog()
result = dialog.ShowModal() #показываем модальный диалог
if result == wx.ID_OK:
print "OK"
else:
print "Cancel"
dialog.Destroy()
В этом листинге надо отметить несколько деталей: при инициализации кнопка добавляется непосредственно к диалогу, а не на панель. Вообще, панели гораздо реже используются в диалогах, чем во фреймах. Отчасти потому, что диалоги должны быть как можно проще, но главная причина в том, что функционал панели (стандартный фон и переключение между элементами с помощью табуляции) уже реализован в диалогах.
Для отображения диалога как модального окна используется метод ShowModal(), имеющийся от метода Show(), который используется для отображения фреймов. Ваше приложение останавливается в точке вызова ShowModal()  до тех пор, пока диалоговое окно не будет закрыто. В этот момент единственным, кто может взаимодействовать с пользователем является диалог (понятно, что другие приложения продолжают работать в обычном режиме).
Программно диалог можно закрыть с помощью метода EndModal(retCode), где retCode — числовое значение, которое возвращается методом ShowModal(). Обычно оно используется для того, чтобы сообщить приложению каким образом было закрыто диалоговое окно и на основании этого принять соответствующее решение. При этом важно помнить, что прекращение отображения диалога ещё не значит его уничтожения — даже после этого он продолжает существовать, благодаря чему после его закрытия Вы всё ещё можете получить данные, введённые пользователем. Пример этого мы увидим в следующем разделе.
Возможно, Вы заметили, что в листинге не было определено ни одного обработчика, что должно было заставить Вас задуматься: «Почему же тогда диалог реагирует на нажатие кнопок?» Никакого секрета тут нет — это поведение уже определено в wx.Dialog. Есть два предопределённых ID, которые имеют особое значение для диалогов:
  1. когда нажимается wx.Button с ID равным wx.ID_OK, диалог закрывается и в точку вызова ShowModal() возвращается значение wx.ID_OK
  2. аналогично действует нажатие на кнопку со значением wx.ID_CANCEL, возвращающая соответствующее значение

после чего приложению остаётся лишь корректно обработать значения кнопок.

Листинг 9.1 показывает стандартный способ работы с модальными диалогами. После активации диалога значение, которое он возвращает, используется в качестве условия в выражении if. В нашем случае мы всего лишь выводим на экран выбор пользователя. Однако обычно в случае результата wx.ID_OK предпринимаются более сложные действия, основанные на выборе пользователя, такие как открытие файла или выбор цвета.
Обычно Вы должны явным образом уничтожить диалог когда Вы заканчиваете его использовать, так как после этого он может быть утилизован сборщиком мусора Python. Если же Вы хотите повторно использовать диалог без того, чтобы тратить время на его создание (например, если это какой-то сложный экземпляр), то Вы можете сохранить на него ссылку и отобразить его позже, вызвав метод ShowModal(). Но при этом важно помнить, что перед завершением программы диалог всё равно придется уничтожить, так как иначе MainLoop() будет рассматривать его как окно верхнего уровня и не сможет корректно завершить работу.

9.1.2 Как вывести пользователю сообщение?

Три наиболее простых диалога для взаимодействия с пользователем — это wx.MessageDialog, выводящий сообщение, wx.TextEntryDialog, предлагающий пользователю ввести короткий текст и wx.SingleChoiceDialog, позволяющий пользователю выбрать варианты из предоставленного списка. В следующих трёх секциях мы обсудим эти три диалога.
Диалог сообщения, как следует из названия, отображает короткое сообщение и закрывается по нажатию кнопки. Обычно он используется для вывода предупреждений или получения ответа на вопрос да / нет. На рисунке 9.2 отображён такой диалог.
рис 9.2
Использование такого диалога не вызовет у Вас затруднений: в листинге 9.2 представлено два способа создания такого диалога.
Листинг 9.2
import wx
if __name__ == "__main__":
app = wx.PySimpleApp()
# с использованием классов
dlg = wx.MessageDialog(None, "Is this explanation OK?",
'A Message Box',wx.YES_NO | wx.ICON_QUESTION)
retCode = dlg.ShowModal()
if (retCode == wx.ID_YES):
print "yes"
else:
print "no"
dlg.Destroy()
# с использованием функции
retCode = wx.MessageBox("Is this way easier?", "Via Function",
wx.YES_NO | wx.ICON_QUESTION)
Мы создаём два диалоговых окна, одно за другим. В первый раз мы создаём экземпляр класса wx.MessageDialog, после чего вызываем его метод ShowModal().
Используем класс wx.MessageDialog

Используя конструктор класса wx.MessageDialog Вы можете задать сообщение в диалоге и выбрать для него необходимые кнопки:
wx.MessageDialog(parent,message,caption=»MessageBox»,style=wx.OK|wx.CANCEL,pos=wx.DefaultPosition)
message — текст, который будет отображаться в диалоге; если Вы хотите разделить его на несколько строк — используйте 'n'. caption отображается в заголовке диалога. pos позволяет Вам определить место на экране, где будет отображён диалог, однако Windows этот аргумент игнорирует.
Флаги стиля можно разделить на две группы. Первая содержит кнопки, которые будут отображаться в диалоге:
СтильОписание
wx.CANCELДобавляет кнопку «отменить» с идентификатором wx.ID_CANCEL
wx.NO_DEFAULTЕсли установлен флаг wx.YES_NO, то кнопкой по умолчанию будет «нет»
wx.OKДобавляет кнопку «да» с идентификатором wx.ID_OK
wx.YES_DEFAULTЕсли установлен флаг wx.YES_NO, то кнопкой по умолчанию будет «да». Такая настройка применяется по умолчанию
wx.YES_NOДобавляет кнопки «да» и «нет» со значениями wx.ID_OK и wx.ID_NO соответственно

Вторая группа флагов отвечает за знак, который будет отображён рядом с текстом:

СтильОписание
wx.ICON_ERRORЗнак ошибки
wx.ICON_EXCLAMATIONВосклицательный знак
wx.ICON_HANDТо же, что и wx.ICON_ERROR
wx.ICON_INFORMATIONЗнак информационного сообщения (буква i)
wx.ICON_QUESTIONВопросительный знак
И наконец, Вы можете использовать флаг стиля wx.STAY_ON_TOP, чтобы отобразить сообщение поверх всех окон в системе.
Как можно видеть из листинга, для того, чтобы отобразить диалог, вызывается метод ShowModal(). В зависимости от отображаемых кнопок Вы получите в ответ wx.ID_OK, wx.ID_CANCEL, wx.ID_YES или wx.ID_NO. Как обычно, результат используется для управления дальнейшей работой программы.

Использование функции wx.MessageBox()

В листинге 9.2 показан и более короткий способ отображения информационного окна: специальная функция wx.MessageBox() сама создаёт диалог, вызывает его метод ShowModall() и возвращает одно из четырёх значений: wx.ID_OK, wx.ID_CANCEL, wx.ID_YES или wx.ID_NO. Сам вызов функции проще конструктора wx.MessageDialog: wx.MessageBox(message,caption=»Message»,style=wx.OK)
В этом примере message,caption и style имеют те же значения, что и в конструкторе класса и Вы можете использовать те же самые флаги стиля. Как Вы увидите дальше в этой главе, многие из предустановленных диалогов имеют схожие функции для их быстрого вызова. Пока Вы планируете использовать диалог всего один раз, можете спокойно выбирать любой из этих двух способов. Но если Вы хотите сохранить диалог в памяти для дальнейшего использования — лучше создать собственный экземпляр. Однако для таких простых диалогов, как наш, экономии времени Вы, конечно, не достигните.
Для того, чтобы отобразить более объемны
й текст (например, лицензию) Вы можете использовать доступный только для Python класс wx.lib.dialogs.ScrolledMessageDialog, использующий такой конструктор:
wx.lib.dialogs.ScrolledMessageDialog(parent, msg, caption, pos=wx.wxDefaultPosition, size=(500,300))
Этот класс не использует встроенные диалоги, а конструирует свой из доступных виджетов. Он отображает только кнопку «ОК» и не принимает флагов стиля.

9.1.3 Как получить короткий текст от пользователя?

Второй простой тип диалога — wx.TextEntryDialog, используемый для получения ввода от пользователя небольшого объема информации. Обычно Вы встречаетесь с ним, когда от Вас требуют имя пользователя или пароль при запуске программы. На рисунке 9.3 отображён типичный представитель этого класса.
рис 9.3
Код, вызвавший его к жизни, приведён в листинге 9.3

Листинг9.3

import wx
if __name__ == "__main__":
app = wx.PySimpleApp()
dialog = wx.TextEntryDialog(None,
"What kind of text would you like to enter?",
"Text Entry", "Default Value", style=wx.OK|wx.CANCEL)
if dialog.ShowModal() == wx.ID_OK:
print "You entered: %s" % dialog.GetValue()
dialog.Destroy()
Как и в предыдущем разделе, мы создаём экземпляр класса диалога, в данном случае wx.TextEntryDialog, однако конструктор этого класса чуть более сложный, чем предыдущий:wx.TextEntryDialog(parent, message, caption=»Please enter text», defaultValue=»», style=wx.OK | wx.CANCEL | wx.CENTRE, pos=wx.DefaultPosition) message — текст приглашения к вводу, caption — текст, отображаемый в заголовке окна. defaultValue, если задано, отображает значение, появляющееся в поле для ввода при отображении диалога. Флаги стиля могут содержать wx.OK и wx.CANCEL для отображения соответствующих кнопок.
В данном диалоге можно использовать и некоторые флаги стиля виджета wx.TextCtrl. Наиболее полезные из них: wx.TE_PASSWORD для замены символов звёздочками, wx.TE_MULTILINE, позволяющий вводить несколько строк текста, и wx.TE_LEFT, wx.TE_CENTRE с wx.TE_RIGHT, определяющие выравнивание вводимого текста.
Последняя строка из этого листинга иллюстрирует ещё одно отличие от информационного диалога: введённая пользователем информация сохраняется в экземпляре диалога и может быть использована после его закрытия. В нашем примере мы получаем её с помощью метода GetValue(). Не стоит забывать, что если пользователь нажал на «отмену», это значит, что он не хочет использовать введённое значение, поэтому имеет смысл проводить проверку, какая кнопка была нажата. Кроме того, Вы можете ввести значение программно с помощью метода SetValue().
Кроме того, у нас есть ещё три удобных функции для этих целей:

  1. wx.GetTextFromUser()
  2. wx.GetPasswordFromUser()
  3. wx.GetNumberFromUser()
Наиболее похожим образом на наш пример действует функция wx.GetTextFromUser():
wx.GetTextFromUser(message, caption=»Input text», default_value=»», parent=None)
Все значения в этом примере идентичны конструктору класса. Если пользователь нажимает «ок», то функция возвращает введёный текст, иначе Вы получите пустую строку.
Для получения пароля надо использовать функцию wx.GetPasswordFromUser():
wx.GetPasswordFromUser(message, caption=»Input text», default_value=»», parent=None)
И тут с аргументами у Вас не должно возникнуть проблем. Вводимый текст, как понятно, заменяется звёздочками. Возвращаемые значения аналогичны предыдущей фу

wxPython in Action. Глава 9. Возможность выбора. Диалоги. (часть 2)

9.2 Использование стандартных диалогов

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

9.2.1 Диалог выбора файла

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

Листинг 9.6

import wx
import os
if __name__ == "__main__":
app = wx.PySimpleApp()
wildcard = "Python source (*.py)|*.py|"
"Compiled Python (*.pyc)|*.pyc|"
"All files (*.*)|*.*"
dialog = wx.FileDialog(None, "Choose a file", os.getcwd(),
"", wildcard, wx.OPEN)
if dialog.ShowModal() == wx.ID_OK:
print dialog.GetPath()
dialog.Destroy()
Диалог выбора файла является одним из наиболее сложных, так как имеет несколько свойств, доступных для чтения / записи. Часть этих свойств Вы можете установить при создании экземпляра этого класса:
wx.FileDialog(parent, message=»Choose a file», defaultDir=»», defaultFile=»», wildcard=»*.*», style=0, pos=wx.DefaultPosition)
message содержит заголовок диалога, defaultDir указывает на каталог, который будет отображён при открытии диалога. Если он не указан или установлен на несуществующий каталог, то будет показано содержимое рабочего каталога. defaultFile содержит название файла и обычно используется при сохранении файлов. wildcart устанавливает фильтры файлов, доступных для выбора, причём в нем можно использовать шаблоны * и ?. В аргументе может содержаться как один шаблон, например *.py, так и набор шаблонов в таком формате: <описание> | <шаблон> | <описание> | <шаблон>, как в листинге выше:
«Python source (*.py)|*.py|Compiled Python (*.pyc)|*.pyc| All files (*.*)|*.*»
Если Вы используете набор шаблонов, то они будут доступны через выпадающий список, как на рисунке 9.6. И ещё, нет никакой гарантии, что аргумент pos будет учитываться конкретной ОС.

Выбор файла

Два наиболее важных флага стиля для данного диалога — это wx.Open и wx.Save, которые определяют поведение диалогового окна.
Диалог, используемый для открытия файла может содержать ещё два флага стиля, а именно wx.HIDE_READONLY, не позволяющий открыть файл в режиме «только для чтения», и wx.MULTIPLE, который позвол
яет выбрать несколько файлов в одной директории.
Для диалога сохранения есть один полезный флаг — wx.OVERWRITE_PROMT, который требует подтверждения от пользователя при его попытке перезаписать уже существующий файл.
Другие типы диалога могут использовать флаг wx.CHANGE_DIR. При его наличии выбор файла изменяет директорию по умолчанию для этого диалога на директорию, содержащую этот файл. Так что в следующий раз открытый диалог уже будет находиться в той директории и для этого Вам не надо сохранять где-либо её название.
В отличие от других диалогов, рассмотренных выше, все эти свойства доступны для чтения и записи при помощи соответствующих методов. Это касается свойств directory, filename, style, message и wildcard, для который существуют соответствующие Get- и Set- методы.
После того, как пользователь закрыл диалог и Вы убедились, что он сделал это кнопкой «ОК», Вы можете получить выбранный им файл с помощью метода GetPath(), возвращающего полное имя и путь к файлу в виде строки. Если Вы использовали флаг wx.MULTIPLE, то Вам потребуется метод GetPaths(), который вернёт Вам список из строк. Если же по какой-то причине Вам надо знать, какой фильтр при этом использовался, то метод GetFilterIndex() вернёт Вам индекс этого фильтра. Для программного изменения выбранного фильтра используйте метод SetFilterIndex().
Кроме того, Вы можете использовать следующую функцию вместо создания экземпляра класса:
wx.FileSelector(message, default_path=»», default_filename=»», default_extension=»», wildcard=»*.*'', flags=0, parent=None, x=-1, y=-1)
Назначение почти всех аргументов понятно из названия, кроме flags — обычно он называется style и default_extension, указывающий на расширение, которое будет добавлено к имени файла при его сохранении. Если пользователь нажимает на «ОК», Вы получите путь к файлу, иначе Вам достанется лишь пустая строка.

Выбор папки

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

Листинг 9.7

import wx
if __name__ == "__main__":
app = wx.PySimpleApp()
dialog = wx.DirDialog(None, "Choose a directory:",
style=wx.DD_DEFAULT_STYLE | wx.DD_NEW_DIR_BUTTON)
if dialog.ShowModal() == wx.ID_OK:
print dialog.GetPath()
dialog.Destroy()
Практически вся функциональность этого диалога заключена в конструкторе:
wx.DirDialog(parent, message=»Choose a directory», defaultPath=»», style=0, pos = wx.DefaultPosition, size = wx.DefaultSize, name=»wxDirCtrl»)
Поскольку message отображается в самом диалоге, Вам нет необходимости (да по всей видимости и возможности — прим. переводчика) в изменении заголовка диалога. defaultPath указывает выбор по умолчанию, пустая строка в данном случае будет означать корень файловой системы. pos и size игнорируются в Windows, а name игнорируется во всех ОС. Флаг стиля wx.DD_NEW_DIR_BUTTON добавляет кнопку создания новой папки, причём он может не работать в старых версиях Winows.
Свойства path, message и style имеют стандартный методы получения и установки. После закрытия диалога можно получит выбранное значение методом GetPath(). Кроме того, вызвать диалог можно и с помощью функции:
wx.DirSelector(message=wx.DirSelectorPromptStr, default_path=»», style=0, pos=wxDefaultPosition, parent=None)
Аргументы аналогичны конструктору класса. Функция возвращает путь к папке в виде строки или пустую строку, если была нажата кнопка отмены.

9.2.2 Диалог выбора шрифта

Диалог выбора шрифта отличается от диалога выбора файла, та

wxPython in Action. Глава 9. Возможность выбора. Диалоги. (часть 3)

9.3 Создание мастеров

Мастер — это набор простых диалогов, соединённых последовательно друг с другом. Их цель — провести пользователя через какую-либо процедуру путём подачи и получения информации небольшими частями. На рисунке 9.11 изображён пример такого простого мастера, содержащего кнопки «вперед» и «назад».
рис 9.11
В wxPython мастер представляет из себя набор страниц, управляемый экземпляром класса wx.wizard.Wizard. Этот экземпляр обрабатывает события, создаваемые пользователем. Сами же страницы представляют из себя экземпляры классов wx.wizard.WizardPageSimple или wx.wizard.WizardPage. В обоих случаях это всего лишь экземпляры класса wx.Panel с дополнительной логикой для обслуживания последовательности страниц. Разница же между этими двумя классами в том, что wx.wizard.WizardPage позволяет динамически определить, к какой странице следует совершить переход, тогда как для wx.wizard.WizardPageSimple последовательность должна быть задана ещё на момент создания мастера. Листинг 9.11 показывает код создания простого мастера:
Листинг 9.11
import wx
import wx.wizard
# создаём простую страницу
class TitledPage(wx.wizard.WizardPageSimple):
def __init__(self, parent, title):
wx.wizard.WizardPageSimple.__init__(self, parent)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.sizer)
titleText = wx.StaticText(self, -1, title)
titleText.SetFont(
wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD))
self.sizer.Add(titleText, 0,
wx.ALIGN_CENTRE | wx.ALL, 5)
self.sizer.Add(wx.StaticLine(self, -1), 0,
wx.EXPAND | wx.ALL, 5)
if __name__ == "__main__":
app = wx.PySimpleApp()
# создаём экземпляр мастера
wizard = wx.wizard.Wizard(None, -1, "Simple Wizard")
# создаём страницы мастера
page1 = TitledPage(wizard, "Page 1")
page2 = TitledPage(wizard, "Page 2")
page3 = TitledPage(wizard, "Page 3")
page4 = TitledPage(wizard, "Page 4")
page1.sizer.Add(wx.StaticText(page1, -1,
"Testing the wizard"))
page4.sizer.Add(wx.StaticText(page4, -1,
"This is the last page."))
# задаём последовательность страниц
wx.wizard.WizardPageSimple_Chain(page1, page2)
wx.wizard.WizardPageSimple_Chain(page2, page3)
wx.wizard.WizardPageSimple_Chain(page3, page4)
# задаём размер мастера
wizard.FitToPage(page1)
# запускаем мастера
if wizard.RunWizard(page1):
print "Success"
  1. Для наполнения мастера мы создаём страницы, которые содержат только фиксированный текст. Вы, скорее всего, захотите добавить сюда ещё какие-то элементы, например, для получения данных от пользователя.
  2. Функция wx.wizard.WizardPageSimple_Chain() является более простым способом построения последовательности, чем вызов методов SetNext() и SetPrev() страниц, переданных ей в качестве аргументов.
  3. Вызов FitToSize() устанавливает размер мастера на основании страницы, переданной ему в качестве аргумента, и всех страниц, доступных от неё по цепочке. Поэтому, вызывайте этот метод лишь после того, как установите последовательность страниц.
  4. Аргументом метода wizard.RunWizard() служит страница, с которой начинается запуск мастера. Причём мастер сам понимает, что надо завершиться, когда он доходит до страницы, для которой не установлена следующая страница. Значение True возвращается только если пользователь прошёл все страницы и нажал в конце «Завершить».

Создание экземпляра wx.wizard.Wizard — первый шаг в использовании мастера. Конструктор класса выглядит так:
wx.wizard.Wizard(parent, id=-1, title=wx.EmptyString, bitmap=wx.NullBitmap, pos=wx.DefaultPosition)
В нашем примере parent, id, title и pos имеют тот же смысл, что и в конструкторе wx.Panel. Если передан аргумент bitmap, то он будет отображаться на каждой странице. Из доступных флагов стиля есть только wx.WIZARD_EX_HELPBUTTON, который отображает кнопку помощи. Это флаг расширенного стиля и его надо устанавливать используя двухступенчатый механизм, описанный в главе 8.
Обычно, для установки размера Вы будете использовать FitToSize(), как в нашем примере, но ничто не мешает Вам задать размер самому с помощью метода SetPageSize(), передав ему в качестве аргумента кортеж или экземпляр wx.Size. Для получения текущего размера используется метод GetPageSize(). В любом случае размер будет использоваться лишь для той части мастера, которая предназначена для отображения страниц, сам мастер будет большего размера (например, из-за кнопок «далее» и «назад» — прим. переводчика).
Контролировать работу мастера можно с помощью его же методов: метод GetCurrentPage() возвращает отображаемую страницу, если же мастер ещё не запущен он вернёт None. Определить, имеет ли текущая стра
ница следующую или предыдущую можно с помощью методов HasNextPage() и HasPrevPage(). Для запуска мастера используется метод RunWizard(), как видно из нашего листинга.
Мастер продуцирует командные события, которые Вы сами можете перехватывать и обрабатывать; они перечислены в таблице ниже. Каждое из этих событий относится к классу wx.wizard.WizardEvent, для которого доступно два метода. GetPage() вернёт нам страницу, на которой было создано событие, а GetDirection() вернёт True, если пользователь движется вперёд по мастеру, и False в другом случае.

СобытиеОписание
EVT_WIZARD_CANCELСоздаётся, когда пользователь нажимает кнопку «отмена». Это событие может быть отменено с помощью метода Veto(). В таком случае диалог не завершит работу.
EVT_WIZARD_FINISHEDСоздаётся, когда пользователь нажимает кнопку «готово» в конце мастера.
EVT_WIZARD_HELPСоздаётся, когда пользователь нажимает кнопку справки
EVT_WIZARD_PAGE_CHANGEDСоздаётся после смены страницы для постобработки
EVT_WIZARD_PAGE_CHANGINGСоздаётся когда пользователь нажал на кнопку изменения страницы, но страница ещё не изменилась. Это событие так же может быть отменено с помощью Veto(), например, если пользователь заполнил не все поля.
Класс wx.wizard.WizardPageSimple обрабатывается так же как панель. Конструктор этого класса позволяет Вам сразу установить предыдущую и следующую страницу:
wx.wizard.WizardPageSimple(parent=None, prev=None, next=None)
Опять же, если Вы не хотите задавать их при создании — используйте методы SetPrev() и SetNext(). Если же и с этим у Вас возникают проблемы — Вам поможет функция wx.wizard.WizardPageSimple_Chain(), которая устанавливает взаимосвязь между двумя страницами.
Более сложная версия страниц мастера, wx.wizard.WizardPage, немногим отличается от рассмотренного нами. Но вместо того, чтобы явно устанавливать последовательность страниц, она предоставляет методы, позволяющие определить более сложную логику следования по мастеру. Конструктор класса выглядит так:
wx.WizardPage(parent, bitmap=wx.NullBitmap, resource=None)
Заданный аргумент bitmap переопределяет свой аналог в конструкторе мастера. resource позволяет загрузить страницу из ресурсов wxPython. Для реализации логики следования переопределите методы GetPrev() и GetNext(), чтобы они возвращали то, что надо делать дальше. Обычно это используется в случаях, когда последовательность страниц определяется ответами пользователя.

9.4 Отображение подсказок при запуске

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

Листинг 9.12

import wx
if __name__ == "__main__":
app = wx.PySimpleApp()
provider = wx.CreateFileTipProvider("tips.txt", 0)
wx.ShowTip(None, provider, True)
У нас есть две функции, управляющие такими советами. Первая создаёт wx.TipProvider:
wx.CreateFileTipProvider(filename, currentTip)
filename — имя файла, содержащего эти советы. currentTip — индекс отображаемого совета (нумерация идёт с 0). Приложение само должно хранить эту информацию между запусками.
Файл с советами — это простой текстовый файл, где каждый совет занимает ровно одну строчку. Пустые линии пропускаются, так же как и линии, начинающиеся с #. Вот пример содержания такого файла:
Создавать советы очень легко.
Feel the force, Luke.

Поставщик советов (tip provider) является экземпляром класса wx.PyTipProvider. Если Вам нужно больше функциональности, Вы можете определить свой подкласс класса wx.TipProvider и переопределить его метод GetTip().
За отображение советов ответственна функция wx.ShowTip(). wx.ShowTip(parent, tipProvider, showAtStartup)
parent — родительское окно, при наличии, а tipProvider — это то, что мы создали на предыдущем шаге. showAtStartup отвечает за то, стоит ли галочка в чекбоксе «показывать при старте», но при этом она не следит, отказался ли пользователь от них — это Вы должны проверить сами. Функция возвращает значение этого чекбокса, так что Вы можете использовать его при следующем запуске вашего приложения, если, конечно, сохраните это значение.

Автор: Ishayahu Lastov

wxPython in Action. Глава 9. Возможность выбора. Диалоги. (часть 4)

9.5 Использование валидаторов

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

  1. Проверяет данные в элементах перед закрытием диалогов
  2. Автоматически передаёт данные из / в диалог
  3. Проверяет данные по мере их ввода пользователем

9.5.1 Как с помощью валидатора убедиться, что я получил верные данные?

Валидатор представляет из себя подкласс класса wx.Validator, который в свою очередь является абстрактным классом в наборе wxWidget для С++, поэтому Вам необходимо определять свои собственные классы на его основе. Как мы увидим позже, Ваши классы создаются не напрямую от wx.Validator, а посредством наследования эксклюзивного класса wx.PyValidator, что даёт Вам возможность переопределять все родительские методы (среди них надо переопределить метод Clone(), возвращающий копию валидатора).
Сам валидатор должен быть прикреплён к какому-нибудь виджету, что можно сделать двумя способами. Во-первых, если виджет это позволяет, валидатор может быть передан его конструктору в качестве аргумента. В противном случае Вы можете создать виджет и прикрепить к нему валидатор с помощью метода этого виджета SetValidator(validator).
Для того, чтобы осуществлять проверку вводимых данных надо сперва переопределить метод Validate(parent) в вашем подклассе валидаторов. Аргумент parent — это родительское окно вашего виджета, соответственно это либо диалог либо панель. Вы можете использовать его для получения информации с других виджетов или просто проигнорировать. Для получения ссылки на связаный с вашим валидатором виджет используйте метод self.GetWindow(). Значение, возвращаемое методом Validate() имеет логический тип: True означает, что данные введены верно, False — что есть какая-то ошибка. Вы можете использовать wx.MessageBox() для отображения предупреждения в методе Validate(), но Вы не должны делать что-либо, что приводит к созданию новых событий.
Значение, возвращаемое методом Validate() играет роль когда пользователь закрывает диалог, нажимая на кнопку «ОК». Как часть обработки этого события wxPthon вызывает функции Validate() всех виджетов диалога, если эти функции определены. И если хоть одна из этих функций вернёт False, диалог не закроется. Листинг 9.13 показывает простой диалог с валидатором, который проверяет наличие данных во всех текстовых полях:

Листинг 9.13
import wx
about_txt = """
Валидатор, используемый в этом примере проверяет наличие текста в
текстовых полях при нажатии на кнопку "ок" и не даёт Вам закрыть диалог,
если это условие не выполняется."""

# создаём подкласс валидатора
class NotEmptyValidator(wx.PyValidator):
def __init__(self):
wx.PyValidator.__init__(self)

def Clone(self):
"""
Обратите внимание, что каждый валидатор должен реализовать метод Clone().
"""
return NotEmptyValidator()

# метод проверки
def Validate(self, win):
textCtrl = self.GetWindow()
text = textCtrl.GetValue()

if len(text) == 0:
wx.MessageBox("This field must contain some text!", "Error")
textCtrl.SetBackgroundColour("pink")
textCtrl.SetFocus()
textCtrl.Refresh()
return False
else:
textCtrl.SetBackgroundColour(
wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
textCtrl.Refresh()
return True

def TransferToWindow(self):
return True

def TransferFromWindow(self):
return True

class MyDialog(wx.Dialog):
def __init__(self):
wx.Dialog.__init__(self, None, -1, "Validators: validating")

# Создаём поля для ввода текста
about = wx.StaticText(self, -1, about_txt)
name_l = wx.StaticText(self, -1, "Name:")
email_l = wx.StaticText(self, -1, "Email:")
phone_l = wx.StaticText(self, -1, "Phone:")

# используем валидаторы
name_t = wx.TextCtrl(self, validator=NotEmptyValidator())
email_t = wx.TextCtrl(self, validator=NotEmptyValidator())
phone_t = wx.TextCtrl(self, validator=NotEmptyValidator())

# Используем стандартные ID кнопок
okay = wx.Button(self, wx.ID_OK)
okay.SetDefault()
cancel = wx.Button(self, wx.ID_CANCEL)

# размещаем виджеты с помощью координаторов
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(about, 0, wx.ALL, 5)
sizer.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5)

fgs = wx.FlexGridSizer(3, 2, 5, 5)
fgs.Add(name_l, 0, wx.ALIGN_RIGHT)
fgs.Add(name_t, 0, wx.EXPAND)
fgs.Add(email_l, 0, wx.ALIGN_RIGHT)
fgs.Add(email_t, 0, wx.EXPAND)
fgs.Add(phone_l, 0, wx.ALIGN_RIGHT)
fgs.Add(phone_t, 0, wx.EXPAND)
fgs.AddGrowableCol(1)
sizer.Add(fgs, 0, wx.EXPAND|wx.ALL, 5)

btns = wx.StdDialogButtonSizer()
btns.AddButton(okay)
btns.AddButton(cancel)
btns.Realize()
sizer.Add(btns, 0, wx.EXPAND|wx.ALL, 5)

self.SetSizer(sizer)
sizer.Fit(self)

app = wx.PySimpleApp()

dlg = MyDialog()
dlg.ShowModal()
dlg.Destroy()

app.MainLoop()
  1. Метод Validate() проверяет, что связанные с ним виджеты содержат какой-нибудь текст. Если же это не так, то цвет их фона меняется на розовый
  2. Для использования валидатора мы прикрепляем его к виджету, передавая в качестве аргумента конструктора виджета экземпляр нашего класса
На рисунке 9.13 показан наш диалог после попытки закрыть его с одним пустым полем.
рис 9.13
Код, который заставляет диалог задействовать валидаторы отсутствует в листинге, так как он является частью системы событий wxPython. В этом заключается ещё одна разница между фреймами и диалогами — диалоги автоматически запускают валидацию, а фреймы нет. Если же Вам понадобиться использовать валидацию во фреймах, то вызовите метод Validate() родительского окна. В таком случае, если у Вас установлен расширеный флаг стиля wx.WS_EX_VALIDATE_RECURSIVELY, будут вызваны методы валидации и всех дочерних виджетов. Как и в случае с диалогом, если хотя бы одна валидация будет провалена, проверка выдаст False. В следующем разделе мы поговорим о том, как с помощью валидаторов передвать данные.

9.5.2 Передача данных с помощью валидаторов

Вторая важная функция валидаторов состоит в том, что они автоматически передают данные в диалог перед его отображением и из диалога при его закрытии. На рисунке 9.14 изображён простейший диалог.
рис 9.14
Для того, чтобы реализовать эту функциональность Вам потребуется переопределить ещё два метода вашего подкласса. Метод TransferToWindow() вызывается автоматически при открытии диалога; его необходимо использовать для передачи данных в виджет, связаный с валидатором. Метод TransferFromWindow() вызывается при закрытии диалога кнопкой «ок» уже после выполненных проверок. Его необходимо использовать для передачи данных из виджета в другое место.
Тот факт, что данные откуда-то и куда-то передаются, означает, что валидатор должен знать об этих откуда-то и куда-то, как показано в листинге 9.14. В этом примере каждый валидатор инициализируется со ссылкой на глобальный словарь данных и с ключом этого словаря. Когда диалог открывается TransferToWindow() читает значение словаря для данного ключа и помещает данные в текстовое поле. При закрытии диалога метод TransferFromWindow() делает то же самое для записи значения в этот словарь. В примере будет отображено окно диалога для того, чтобы показать Вам процесс передачи данных.
Листинг 9.14
import wx
import pprint

about_txt = """
В данном примере валидатор используется для демонстрации
возможности автоматической передачи данных в и из текстового поля
при открытии и закрытии диалога."""

# объявляем валидатор
class DataXferValidator(wx.PyValidator):
def __init__(self, data, key):
wx.PyValidator.__init__(self)
self.data = data
self.key = key

def Clone(self):
"""
Будьте внимательны: каждый валидатор должен реализовать метод Clone()
"""
return DataXferValidator(self.data, self.key)

# не проверяем данные
def Validate(self, win):
return True

# вызывается при открытии диалога
def TransferToWindow(self):
textCtrl = self.GetWindow()
textCtrl.SetValue(self.data.get(self.key, ""))
return True

# вызывается при закрытии диалога
def TransferFromWindow(self):
textCtrl = self.GetWindow()
self.data[self.key] = textCtrl.GetValue()
return True

class MyDialog(wx.Dialog):
def __init__(self, data):
wx.Dialog.__init__(self, None, -1, "Validators: data transfer")

about = wx.StaticText(self, -1, about_txt)
name_l = wx.StaticText(self, -1, "Name:")
email_l = wx.StaticText(self, -1, "Email:")
phone_l = wx.StaticText(self, -1, "Phone:")

name_t = wx.TextCtrl(self,
validator=DataXferValidator(data, "name"))
email_t = wx.TextCtrl(self,
validator=DataXferValidator(data, "email"))
phone_t = wx.TextCtrl(self,
validator=DataXferValidator(data, "phone"))

okay = wx.Button(self, wx.ID_OK)
okay.SetDefault()
cancel = wx.Button(self, wx.ID_CANCEL)

sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(about, 0, wx.ALL, 5)
sizer.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5)

fgs = wx.FlexGridSizer(3, 2, 5, 5)
fgs.Add(name_l, 0, wx.ALIGN_RIGHT)
fgs.Add(name_t, 0, wx.EXPAND)
fgs.Add(email_l, 0, wx.ALIGN_RIGHT)
fgs.Add(email_t, 0, wx.EXPAND)
fgs.Add(phone_l, 0, wx.ALIGN_RIGHT)
fgs.Add(phone_t, 0, wx.EXPAND)
fgs.AddGrowableCol(1)
sizer.Add(fgs, 0, wx.EXPAND|wx.ALL, 5)

btns = wx.StdDialogButtonSizer()
btns.AddButton(okay)
btns.AddButton(cancel)
btns.Realize()
sizer.Add(btns, 0, wx.EXPAND|wx.ALL, 5)

self.SetSizer(sizer)
sizer.Fit(self)

app = wx.PySimpleApp()

data = { "name" : "Jordyn Dunn" }
dlg = MyDialog(data)
dlg.ShowModal()
dlg.Destroy()

wx.MessageBox("You entered these values:nn" +
pprint.pformat(data))
app.MainLoop()
Вызов методов передачи данных валидаторов происходит в диалогах автоматически. Для того же, чтобы использовать их вне пределов диалогов вызывайте методы TransferDataFromWindow() и TransferDataToWindow() родительского окна. В таком случае, если у Вас установлен расширеный флаг стиля wx.WS_EX_VALIDATE_RECURSIVELY, будут вызваны методы передачи данных и всех дочерних виджетов.
В следующем разделе мы поговорим о наиболее востребованном использовании валидаторов — проверке данных в процессе их ввода пользователем.

9.5.3 Как проверять данные в процессе ввода?

Кроме всего прочего Вы можете использовать валидаторы для проверки данных, вводимых пользователем, «на лету», ещё до их передачи в виджет. Это очень важная возможность, так как Вы можете уберечь свою программу от попадания в неё плохих данных. На рисунке 9.15 изображён пример такого диалога с текстом, поясняющим эту идею.
рис 9.15
Данный метод проверки данных менее автоматизирован чем те, что мы рассмотрели выше. Вы должны напрямую связать событие введения символов в виджет валидатора с функцией проверки:
self.Bind(wx.EVT_CHAR, self.OnChar)
Виджет сам передаёт событие валидатору. Пример реализации такой возможности приведён в листинге 9.15
Листинг 9.15
import wx
import string

about_txt = """
Валидатор, используемый в данном примере проверяет "на лету" ввод данных
пользователем и не ждёт нажатия кнопки "ок". Первое поле не допускает
ввода цифр, во второе поле можно ввести всё, что угодно, а в третье -
всё, кроме букв.
"""

class CharValidator(wx.PyValidator):
def __init__(self, flag):
wx.PyValidator.__init__(self)
self.flag = flag
# связываем функцию с событием ввода символа
self.Bind(wx.EVT_CHAR, self.OnChar)

def Clone(self):
"""
Будьте внимательны: каждый валидатор должен реализовать метод Clone()
"""
return CharValidator(self.flag)

def Validate(self, win):
return True

def TransferToWindow(self):
return True

def TransferFromWindow(self):
return True

# обработчик ввода данных в виджет
def OnChar(self, evt):
key = chr(evt.GetKeyCode())
if self.flag == "no-alpha" and key in string.letters:
return
if self.flag == "no-digit" and key in string.digits:
return
evt.Skip()

class MyDialog(wx.Dialog):
def __init__(self):
wx.Dialog.__init__(self, None, -1, "Validators: behavior
modification")

# Создаём поля для ввода текста
about = wx.StaticText(self, -1, about_txt)
name_l = wx.StaticText(self, -1, "Name:")
email_l = wx.StaticText(self, -1, "Email:")
phone_l = wx.StaticText(self, -1, "Phone:")

# добавляем валидатор
name_t = wx.TextCtrl(self, validator=CharValidator("no-digit"))
email_t = wx.TextCtrl(self, validator=CharValidator("any"))
phone_t = wx.TextCtrl(self, validator=CharValidator("no-alpha"))
okay = wx.Button(self, wx.ID_OK)
okay.SetDefault()
cancel = wx.Button(self, wx.ID_CANCEL)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(about, 0, wx.ALL, 5)
sizer.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5)

fgs = wx.FlexGridSizer(3, 2, 5, 5)
fgs.Add(name_l, 0, wx.ALIGN_RIGHT)
fgs.Add(name_t, 0, wx.EXPAND)
fgs.Add(email_l, 0, wx.ALIGN_RIGHT)
fgs.Add(email_t, 0, wx.EXPAND)
fgs.Add(phone_l, 0, wx.ALIGN_RIGHT)
fgs.Add(phone_t, 0, wx.EXPAND)
fgs.AddGrowableCol(1)
sizer.Add(fgs, 0, wx.EXPAND|wx.ALL, 5)

btns = wx.StdDialogButtonSizer()
btns.AddButton(okay)
btns.AddButton(cancel)
btns.Realize()
sizer.Add(btns, 0, wx.EXPAND|wx.ALL, 5)

self.SetSizer(sizer)
sizer.Fit(self)

app = wx.PySimpleApp()

dlg = MyDialog()
dlg.ShowModal()
dlg.Destroy()

app.MainLoop()
Так как метод OnChar() находится в валидаторе, он будет вызван ещё до реакции виджета на ввод символа. Мы так же позволяем виджету обработать этот метод, вызывая метод события Skip() (иначе это повлияло бы на нормальную обработку этого события). Валидатор производит проверку допустимости ввода данных и если ввод содержит недопустимые символы метод Skip() не будет вызван и обработка события на этом завершается. Если у Вас возникнет необходимость, Вы можете связать и другие методы

wxPython in Action. Глава 10. Создаём и используем меню в wxPython.

В этой главе Вы узнаете:

  1. Как создавать меню
  2. Как работать с элементами меню
  3. Как добавлять подменю, всплывающие меню и собственные виды меню
  4. Как создавать удобные меню
Трудно представить себе приложение без знакомой полоски сверху со словами «Файл», «Редактировать» и «Справка». Меню настолько привычная часть интерфейса, что многие не уделяют достаточного внимания его созданию, что тоже плохо. Ведь настолько легко и быстро предоставить пользователю доступ к возможностям вашего приложения без меню практически не возможно!
В wxPython есть три исходных класса, обеспечивающих создание меню. Класс wx.MenuBar управляет самой панелью меню, wx.Menu — каждым конкретным меню в этой панели (очевидно, что экземпляр wx.MenuBar может содержать несколько экземпляров wx.Menu). Класс wx.MenuItem отвечает за каждый пункт в wx.Menu.
Во второй главе мы уже вкратце познакомились с меню, в листинге 5.5 показали простой механизм их создания, а в 7 главе поговорили о специальных возможностях меню. Теперь настало время поговорить о них более подробно.

10.1 Создаём меню

Для начала займемся полосой меню. Для того, чтобы её использовать, нам необходимо сделать следующее:
  • Создать полосу меню (menu bar)
  • Добавить полосу меню к фрейму
  • Создать отдельные меню
  • Добавить эти меню к полосе меню или к родительскому меню
  • Создать отдельные пункты меню
  • Добавить эти пункты к соответствующим меню
  • Создать привязку (binding) для событий каждого пункта меню
На самом деле порядок выполнения этих действий достаточно произволен пока вы создаёте меню до его отображения в методе инициализации формы. Вы можете изменять меню и после его отображения, но в таком случае то, что увидит пользователь зависит от порядка ваших действий. Например, не будет иметь значение прикрепляете ли Вы полосу меню к фрейму сразу после создания или сначала создаёте пункты и подменю. Но всё же, для облегчения сопровождения кода и большей его читабельности, мы рекомендуем размещать в коде логически связанные компоненты поблизости вместе. Более подробно эта тема освещалась в главе 5, посвящённой рефакторингу кода. В следующем разделе мы начнём наш разговор с основных действий при создании меню.

10.1.1 Как создать полосу меню и прикрепить её к фрейму.

Для создания полосы меню необходимо использовать конструктор класса wx.MenuBar, не принимающий аргументов:
wx.MenuBar()
После того, как полоса меню создана, можно прикрепить её к wx.Frame или его подклассу используя его метод SetMenuBar(). Обычно это происходит в методе __init__() или OnInit() фрейма:

menubar = wx.MenuBar()

self.SetMenuBar

Вам не обязательно создавать временную переменную для полосы меню, но её наличие облегчит Вам позже добавление отдельных меню. Если Вы не создаёте для неё отдельной переменной, то можно получить ссылку на полосу меню с помощью метода wx.Frame.GetMenuBar().

10.1.2 Как создать меню и прикрепить его к полосе меню?

Полоса меню wxPython состоит из отдельных меню, каждое из которых необходимо создавать по отдельности. Вот конструктор класса wx.Menu:
wx.Menu(title=»», style=0)

У нас есть только одни доступный стиль для экземпляров wx.Menu. При использовании GTK стиль wx.MENU_TEAROFF даёт возможность отделить меню от полосы меню и использовать его отдельно. На других платформах этот флаг будет просто проигнорирован. Если платформа позволяет, то конструктору меню при создании можно передать заголовок (title), который будет отображаться над всеми элементами меню, добавленными позже. На рисунке 10.1 показано окно с тремя меню, код этого окна приведён в листинге 10.1
рис 10.1

import wx
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Simple Menu Example")
p = wx.Panel(self)
# создаём панель меню
menuBar = wx.MenuBar()
# создаём меню
menu = wx.Menu()
# добавляем меню к панели меню
menuBar.Append(menu, "Left Menu")
menu2 = wx.Menu()
menuBar.Append(menu2, "Middle Menu")
menu3 = wx.Menu()
menuBar.Append(menu3, "Right Menu")
self.SetMenuBar(menuBar)
if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()
В API меню wxPython большинство манипуляций с объектом производятся через класс его контейнера. Позже в этой главе мы обсудим специфичные для wx.Menu методы и Вы увидите, что почти все они предназначены для работы с пунктами меню из самого меню. В завершение этого раздела, раз уж мы говорим о работе с объектом wx.Menu, мы перечислим методы wx.Menubar предназначенные для управления объектами меню:
ФункцияОписание
Append(menu, title)
Добавляет menu в конец полосы меню (то есть в после последнего правого элемента). Параметр title используется для отображения нового меню. При удачном выполнении возвращает True, иначе — False.
Insert(pos, menu, title)
Вставляет переданное menu в заданную в pos позицию (после данного вызова выражение GetMenu(pos)==menu будет истинно). Как и при вставке в список, все позиции всех последующих меню сдвигаются вправо. Отсчёт позиций начинается с 0, так что pos=0 эквивалентно помещению меню в крайнюю левую позицию полосы меню. Если в качестве pos передать результат вызова GetMenuCount(), то результат будет такой же как при вызове метода Append(). Параметр title используется для отображения нового меню. При удачном выполнении возвращает True, иначе — False.
Remove(pos)
Удаляет меню из позиции pos, сдвигая оставшиеся меню влево. Возвращает то меню, которое было удалено.
Replace(pos, menu, title)
Заменяет меню на позиции pos меню переданным в аргументе menu а для отображения использует параметр title. Не затрагивает остальные меню в полосе. Возвращает то меню, которое раньше находилось на этом месте.
Класс wx.MenuBar содержит ещё и методы, позволяющие манипулировать меню таким образом:
МетодОписание
EnableTop(pos, enable)Делает меню на позиции pos доступным или нет, в зависимости от значения параметра enable — True или False.
GetMenu(pos)Возвращает объект меню, находящийся в заданной позиции.
GetMenuCount()Возвращает количество меню в полосе меню.
FindMenu(title)
Возвращает числовой индекс меню в полосе меню при заданном title. Если такого меню нет, метод возвращает wx.NOT_FOUND. Метод игнорирует декораторы акселераторов (то есть значки перед буквой для быстрого вызова) при их наличии.
GetLabelTop(pos)
SetLabelTop(pos, label)
Метод установки и получения метки меню в данной позиции

10.1.3 Как добавить элементы в выпадающее меню?

Есть несколько методов для добавления пунктов меню в меню. Самый простой — использовать метод Apend() экземпляра wx.Menu:
Append(id, string, helpStr=»», kind=wx.ITEM_NORMAL)
Параметр id — это ID wxPython'а. string — стока, которая будет отображаться в меню. helpStr — стока, которая будет отображаться в статусной строке фрейма при подсветке этого пункта меню. Аргумент kind позволяет установить тип элемента меню в переключатель, позже мы поговорим об этом подробнее. Метод Append помещает данный элемент в конец выбранного меню.
Если Вы хотите добавить в меню разделитель — самый простой способ сделать это — вызвать метод wx.Menu.AppendSeparator(), не принимающий аргументов, который помещает разделитель в конец меню.
В листинге 10.2 приведен пример использования метода Append() для построения меню с двумя разделителями.
Листинг 10.2

import wx
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Menu Example with StatusBar")
p = wx.Panel(self)
self.CreateStatusBar()
menu = wx.Menu()
simple = menu.Append(-1, "Simple menu item",
"This is some help text")
menu.AppendSeparator()
exit = menu.Append(-1, "Exit",
"Selecting this item will exit the program")
self.Bind(wx.EVT_MENU, self.OnSimple, simple)
self.Bind(wx.EVT_MENU, self.OnExit, exit)
menuBar = wx.MenuBar()
menuBar.Append(menu, "Menu")
self .SetMenuBar(menuBar)
def OnSimple(self, event):
wx.MessageBox("You selected the simple menu item")
def OnExit(self, event):
self.Close()

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()

На рисунке 10.2 показан получившийся фрейм:
рис 10.2
Кроме метода Append есть ещё два других набора методов для добавления элементов меню. Для того, чтобы поместить элемент меню в начало меню используйте один из этих двух методов:
  • Prepend(id, string, helpStr=»», kind=wx.ITEM_NORMAL
  • PrependSeparator()
Эти два метода принимают те же аргументы, что и их аналоги Append и AppendSeparator. Для того же, чтобы поместить элемент в любую другую позицию у Вас есть
  • Insert(pos, id, string, helpStr=»», kind=wx.ITEM_NORMAL)
  • InsertSeparator(pos)
Аргумент pos, как понятно, содержит номер позиции, куда будет помещён указанный элемент, так что если pos=0, элемент будет помещён в начало списка, а если он равен длине списка — то в его конец. Все элементы списка, находящиеся после указанной позиции, сдвигаются вниз. Все эти методы неявно создают экземпляры класса wx.MenuItem, однако Вы можете создать их и явно при помощи конструктора данного класса для того, чтобы установить дополнительные свойства, кроме названия, этого экземпляра. Вы можете задать, например, свой цвет или шрифт. Конструктор данного класса выглядит таким образом:
wx.MenuItem(parentMenu=None, id=ID_ANY, text=»», helpString=»», kind=wx.ITEM_NORMAL, subMenu=None)
Аргумент parentMenu, если передан, должен быть экземпляром класса wx.Menu. Новый элемент меню, при создании, автоматически не добавляется к родительскому меню для отображения — Вы должны сделать это вручную. В этом меню отличается от других виджетов и контейнеров wxPython. id определяет идентификатор для нового элемента, и с ним можно проделать тот же трюк, передав значение -1 для автоматической генерации значения, как и с окнами. Аргумент text определяет текст, отображаемый на элементе, а helpString — текст, отображаемый в строке статуса. kind определяет тип элемента меню, для стандартного элемента он равен wx.ITEM_NORMAL, о других значениях мы поговорим позже. Если subMenu не пустой аргумент, то элемент на самом деле является субменю. Однако мы не рекомендуем использовать такой механизм — лучше воспользоваться методом, описанным в разделе 10.3
В отличие от большинства виджетов, созданный элемент не добавляется к родительскому меню. Для того, чтобы сделать это необходимо использовать один из трёх методов экземпляра wx.Menu:
  • AppendItem(aMenuItem)
  • InsertItem(pos, aMenuItem)
  • PrependItem(aMenuItem)

Все они ведут себя так, как и ожидается из их названия.

Для того, чтобы удалить элемент из меню, нужно использовать метод Remove(id) или RemoveItem(item), в зависимости от того, что у Вас есть — id или сам элемент. Все нижележащие меню сдвигаются, соответственно, вверх. Метод Remove() возвращает удалённый элемент меню, что позволяет Вам использовать его позже. В отличие от полосы меню, сами меню не имеют метода для непосредственной замены элементов, для этого вам сперва надо удалить элемент, а после — вставить в нужную позицию.
Класс wx.Menu так же содержит два метода для получения информации о своих компонентах. GetMenuItemCount() возвращает количество элементов в меню, а GetMenuItems() возвращает список элементов меню в порядке их расположения в меню. Этот список является копией актуальной последовательности элементов, то есть изменения в нём не повлияют на само меню. Поэтому Вы не можете использовать этот список в качестве замены методов добавления и удаления элементов.
Вы можете добавлять и удалять элементы меню в процессе выполнения программы пока меню активно. Листинг 10.3 демонстрирует примеры кода, добавляющего элементы меню в процессе выполнения программы. Метод OnAddItem(), вызываемый при нажатии на кнопку, добавляет новый элемент в конец меню.

import wx
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Add Menu Items")
p = wx.Panel(self)
self.txt = wx.TextCtrl(p, -1, "new item")
btn = wx.Button(p, -1, "Add Menu Item")
# связываем метод с событием кнопки
self.Bind(wx.EVT_BUTTON, self.OnAddItem, btn)

sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.txt, 0, wx.ALL, 20)
sizer. Add(btn, 0, wx.TOP|wx.RIGHT, 20)
p.SetSizer(sizer)

self.menu = menu = wx.Menu()
simple = menu.Append(-1, "Simple menu item")
menu.AppendSeparator()
exit = menu.Append(-1, "Exit")
self.Bind(wx.EVT_MENU, self.OnSimple, simple)
self.Bind(wx.EVT_MENU, self.OnExit, exit)

menuBar = wx.MenuBar()
menuBar.Append(menu, "Menu")
self.SetMenuBar(menuBar)

def OnSimple(self, event):
wx.MessageBox("You selected the simple menu item")

def OnExit(self, event):
self.Close()

def OnAddItem(self, event):
# добавляем элемент меню
item = self.menu.Append(-1, self.txt.GetValue())
# связываем его с методом
self.Bind(wx.EVT_MENU, self.OnNewItemSelected, item)

def OnNewItemSelected(self, event):
wx.MessageBox("You selected a new item")

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()

В этом примере onAddItem() читает значение, введённое в текстовое поле и использует Append() для добавления нового элемента в меню. Кроме того, он связывает событие выбора элемента меню, так что у него появляется некоторая функциональность. В следующем разделе мы обсудим события элементов меню.

10.1.4 Как отвечать на события меню?

Теперь посмотрим на два примера, которые покажут нам, как реагировать на событие выбора меню. Как и многие другие виджеты, которые мы рассматривали в 8 главе, события элементов меню являются экземплярами специального типа wx.CommandEvent. В нашем случае это будет wx.EVT_MENU.
События элементов меню отличаются от других командных событий двумя деталями. Во-первых, функция Bind(), которая связывает событие меню с определенной функцией, относится не к элементу меню, меню или полосе меню, а к фрейму, содержащему полосу меню. Во-вторых, так как фрейм обычно содержит несколько элементов меню с одним и тем же событием wx.EVT_MENU, методу Bind() нужно передать третий параметр, который и является этим элементом меню. Это позволяет фрейму отличать события от разных элементов меню (c этим же мы встречались при обработке нажатий на кнопки).
Таким образом, типичный вызов Bind() для привязки события меню выглядит так:
self.Bind(wx.EVT_MENU, self.OnExit, exit_menu_item)
где self — это фрейм, self.OnExit — метод обработчик события, а exit_menu_item — элемент меню.
Хотя идея связывания элементов меню с обработчиками через фрейм может казаться немного странной, тем не менее для этого есть своя причина. Связывание на уровне фрейма позволит Вам прозрачно привязать кнопку с панели инструментов к тому же обработчику, что и элемент меню. Если кнопка панели инструментов имеет тот же id, что и элемент меню, то будет достаточно одного вызова Bind() c передачей ему этого id. Это становится возможным благодаря тому, что и события меню и события панели инструментов проходят через фрейм. Если бы события элементов меню перехватывались в полосе меню, этот обработчик никогда бы не узнал о событиях с панели инструментов.
Иногда бывает так, что у Вас есть несколько элементов меню, которые должны быть связаны с одним и тем же обработчиком. Например, набор переключателей, которые фактически делают одно и то же, могут быть связаны с одним обработчиком. Если элементы меню имеют последовательные id, то вместо того, чтобы связывать их по отдельности, Вы можете использовать тип события wx.EVT_MENU_RANGE:
self.Bind(wx.EVT_MENU_RANGE, function, id1=menu1, id2=menu2)
В этом случае все элементы меню с идентификаторами между menu1 и menu2 (включительно) будут связаны с переданной функцией.
И хотя обычно Вас интересуют только командные события меню, тем не менее у меню есть и другие события, которые можно использовать. В wxPython класс wx.MenuEvent управляет событиями отображения и выделения меню. Таблица 10.3 описывает четыре типа событий класса wx.MenuEvent
Таблица 10.3 Типы событий wx.MenuEvent
Тип событияОписание
EVT_MENU_CLOSEВозникает когда меню закрывается
EVT_MENU_HIGHLIGHT
Возникает при подсветке (наведении на) элемента меню. Связано с конкретным id. По умолчанию отображает подсказку в статусной панели фрейма
EVT_MENU_HIGHLIGHT_ALL
Возникает когда меню подсвечивается на при этом не привязано к конкретному id, другими словами может быть только один обработчик этого события для всей полосы меню. Его следует использовать если Вы хотите чтобы происходило какое-то действие при подсвечивании любого элемента меню.
EVT_MENU_OPENВо
зникает когда меню открывается

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

10.2 Работа с элементами меню

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

10.2.1 Как найти определённый элемент меню в меню?

Есть несколько способов найти нужное меню или элемент меню имея его метку или идентификатор. Чаще всего Вы будете использовать эти методы в обработчиках событий, особенно если Вы захотите изменить элемент меню или отобразить его метку (текст на элементе) в другом месте. Листинг 10.4 расширяет динамическое меню из предыдущего примера используя FindItemById() для получения подходящего элемента меню для дальнейшего отображения.

import wx

class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Find Item Example")
p = wx.Panel(self)
self.txt = wx.TextCtrl(p, -1, "new item")
btn = wx.Button(p, -1, "Add Menu Item")
self.Bind(wx.EVT_BUTTON, self.OnAddItem, btn)

sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.txt, 0, wx.ALL, 20)
sizer.Add(btn, 0, wx.TOP|wx.RIGHT, 20)
p.SetSizer(sizer)

self.menu = menu = wx.Menu()
simple = menu.Append(-1, "Simple menu item")
menu.AppendSeparator()
exit = menu.Append(-1, "Exit")
self.Bind(wx.EVT_MENU, self.OnSimple, simple)
self.Bind(wx.EVT_MENU, self.OnExit, exit)

menuBar = wx.MenuBar()
menuBar.Append(menu, "Menu")
self.SetMenuBar(menuBar)

def OnSimple(self, event):
wx.MessageBox("You selected the simple menu item")

def OnExit(self, event):
self.Close()

def OnAddItem(self, event):
item = self.menu.Append(-1, self.txt.GetValue())
self.Bind(wx.EVT_MENU, self.OnNewItemSelected, item)

def OnNewItemSelected(self, event):
# получаем элемент меню
item = self.GetMenuBar().FindItemById(event.GetId())
text = item.GetText()
wx.MessageBox("You selected the '%s' item" % text)

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()


В этом примере метод FindItemById() используется для получения ссылки на элемент меню, чтобы отобразить его метку.
И wx.MenuBar и wx.Menu имеют по сути одни и те же методы для поиска информации о конкретных элементах меню. Главная разница в том, что метод wx.MenuBar будет искать элемент в пределах полосы меню, а wx.Menu будет искать только в пределах этого меню. В большинстве случаев предпочтительнее использовать wx.MenuBar, хотя бы потому, что ссылку на неё легко получить с помощью метода wx.Frame.GetMenuBar().
Для поиска меню верхнего уровня можно использовать метод wx.MenuBar.FindMenu(title). Он возвращает либо индекс соответствующего меню либо константу wx.NOT_FOUND. Для получения ссылки на найденное меню используйте GetMenu():
def FindMenuInMenuBar(menuBar, title):
pos = menuBar.FindMenu(title)
if pos == wx.NOT_FOUND:
return None
return menuBar.GetMenu(pos)

Параметр title метода FindMenu является текстом меню с или без декоратора акселератора, о которых мы будем говорить ниже. Например FindMenu(«File») найдет меню даже если его название задано как &File. Это касается и всех методов класса wx.Menu, которые производят поиск на основании строки названия. В таблице 10.4 перечислены методы wx.MenuBar, которые могут использоваться для поиска и манипуляции определённым элементом меню:

Таблица 10.4 Методы взаимодействия wx.MenuBar

МетодОписание
FindMenuItem(menuString, itemString)
Ищет элемент меню с названием itemString в меню с названием menuString. Либо возвращает найденный элемент меню либо wx.NOT_FOUND
FindItemById(id
Возвращает элемент меню, ассоциированный с переданным идентификатором. Если такого элемента нет — возвращает None.
GetHelpString(id)
SetHelpString(id, helpString)
Методы для получения и установки строки помощи для элемента меню с переданным идентификатором. Если такого элемента меню нет, метод установки вернёт «», а метод установки просто ничего не сделает.
GetLabel(id)
SetLabel(id,label)
Методы получения и установки для отображаемого названия элемента меню с переданным идентификатором. Обрабатывает отсутствующие э
лементы так же как и методы для строки помощи. Эти методы следует использовать только после того, как полоса меню была соединена с фреймом.

Таблица 10.5 содержит API wx.Menu для элементов меню. Эти методы ведут себя так же как и методы wx.MenuBar кроме того, что элемент меню должен присутствовать в данном меню.

Таблица 10.4 Методы взаимодействия wx.Menu

МетодОписание
FindItem(itemString)Возвращает элемент меню с названием либо wx.NOT_FOUND
FindItemById(id)Как и у полосы меню
FindItemByPosition(pos)Возвращает элемент меню, находящийся в данной позиции
GetHelpString(id)
SetHelpString(id, helpString)
Как и у полосы меню
GetLabel(id)
SetLabel(id,label)
Как и у полосы меню

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

10.2.2 Как я могу активировать или деактивировать элемент меню?

Так же как и другие виджеты меню и элементы меню могут быть активированы или деактивированы. Деактивированное меню или элемент меню отображается серым, а не черным цветом; оно не продуцирует события при подсветке или выборе и оно «невидимо» для системы событий.
Листинг 10.5 показывает пример простого кода, который изменяет состояния элементов меню используя методы полосы меню IsEnabled() и Enable() при обработке события кнопки:

Листинг 10.5

import wx

ID_SIMPLE = wx.NewId()

class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Enable/Disable Menu Example")
p = wx.Panel(self)
self.btn = wx.Button(p, -1, "Disable Item", (20,20))
self.Bind(wx.EVT_BUTTON, self.OnToggleItem, self.btn)

menu = wx.Menu()
menu.Append(ID_SIMPLE, "Simple menu item")
self.Bind(wx.EVT_MENU, self.OnSimple, id=ID_SIMPLE)

menu.AppendSeparator()
menu.Append(wx.ID_EXIT, "Exit")
self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)

menuBar = wx.MenuBar()
menuBar.Append(menu, "Menu")
self.SetMenuBar(menuBar)

def OnSimple(self, event):
wx.MessageBox("You selected the simple menu item")
def OnExit(self, event):
self.Close()
def OnToggleItem(self, event):
menubar = self.GetMenuBar()
enabled = menubar.IsEnabled(ID_SIMPLE)
menubar.Enable(ID_SIMPLE, not enabled)
self.btn.SetLabel(
(enabled and "Enable" or "Disable") + " Item")

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()
Чтобы посмотреть или изменить статус элемента меню из полосы меню, меню или самого этого элемента меню используйте wx.MenuBar.IsEnabled(id), wx.Menu.IsEnabled(id) или wx.MenuItem.IsEnabled(). Методы полосы меню и меню принимают в качестве аргумента идентификатор меню. Оба метода возвращают True если элемент активирован и False в противном случае или если элемент не существует. Единственная разница между ними в том, что метод wx.Menu ищет лишь в данном меню, тогда как метод wx.MenuBar ищет среди всех меню. Метод wx.MenuItem не принимает аргументов и возвращает значение для того элемента меню, для которого он вызывается.
Для изменения состояния меню используйте wx.MenuBar.Enable(id, enable), wx.Menu.Enable(id, enable) или wx.MenuItem.Enable(enable). Параметр enable принимает логическое значение True для активации элемента и False для его деактивации. Область действия методов Enable такая же как и для IsEnabled. Кроме того Вы можете деактивировать меню целиком с помощью метода wx.MenuBar.EnableTop(pos, enable). Аргумент pos определяет номер позиции меню в полосе меню.

10.2.3 Как прикрепить к элементу меню акселератор (горячую клавишу)

В wxPython Вы можете настроить управление с помощью клавиатуры и клавиатурные сокращения (акселераторы) для элементов меню. На рисунке 10.3 изображено простое меню с добавленными к нему акселераторами. Обратите внимание, что в названии элементов меню есть подчёркнутые буквы и что рядом с элементом с названием Accelerated есть надпись Ctrl+A
Рисунок 10.3 Элементы меню с горячими клавишами
Практика показывает, что горячие клавиши не всегда увеличивают производительность труда. И всё же они являются стандартными элементами интерфейса и пользователи всегда предполагают их наличие. Кроме того, горячие клавиши полезны для людей с ограниченными возможностями. Листинг 10.6 содержит код для добавления горячих клавиш к элементам меню.
Листинг
10.6 Добавление горячих клавиш к элементам меню
import wx

class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Accelerator Example")
p = wx.Panel(self)
menu = wx.Menu()
# создаём мнемонику
simple = menu.Append(-1, "Simple &menu item")
# создаём акселератор
accel = menu.Append(-1, "&AcceleratedtCtrl-A")

menu.AppendSeparator()
exit = menu.Append(-1, "E&xit")

self.Bind(wx.EVT_MENU, self.OnSimple, simple)
self.Bind(wx.EVT_MENU, self.OnAccelerated, accel)
self.Bind(wx.EVT_MENU, self.OnExit, exit)

menuBar = wx.MenuBar()
menuBar.Append(menu, "&Menu")
self.SetMenuBar(menuBar)

# используем таблицу акселераторов
acceltbl = wx.AcceleratorTable( [
(wx.ACCEL_CTRL, ord('Q'), exit.GetId())
])
self.SetAcceleratorTable(acceltbl)

def OnSimple(self, event):
wx.MessageBox("You selected the simple menu item")

def OnAccelerated(self, event):
wx.MessageBox("You selected the accelerated menu item")

def OnExit(self, event):
self.Close()

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()
У нас есть два типа горячих клавиш в wxPython: мнемоники и акселераторы. Сейчас мы с ними разберёмся.
Использование мнемоник

Мнемоники — это символ, который используется для доступа к элементу меню и выделяется при отображении подчёркиванием. Мнемоники могут быть созданы при определении отображаемого текста для меню или элемента меню если вы поместите знак амперсанда перед нужным символом в метке элемента, например &File, &Edit, Ma&cros. Если амперсанд должен отображаться в метке меню Вы должны его продублировать, например, Font && Spacing. Мнемоники предоставляют альтернативный способ навигации по дереву меню. Для перехода в режим выбора мнемоники необходимо специальное действие; в MS Windows это нажатие на клавишу alt. После того, как режим выбора мнемоники активирован, нажатие на дополнительную клавишу открывает меню высшего уровня соответствующего мнемонике. Выбор следующей мнемоники откроет соответствующее подменю и так до тех пор, пока Вы не выберете элемент меню, вслед за чем активируется событие выбора меню, как если бы оно было выбрано при помощи мышки. Мнемоники должны быть уникальны в пределах меню, но вполне могут повторяться в пределах полосы меню. Обычно мнемоникой делают первую букву отображаемого текста (метки). Если у Вас есть несколько элементов, начинающихся на одну и ту же букву каких-то общепринятых правил для выбора мнемоники не существует (обычно это вторая или последняя буква, кому как больше нравится). Главное не забывать, что важнее продуманные названия меню чем продуманные мнемоники.

Использование акселераторов

Акселератор в wxPython собственно и есть горячая клавиша, то есть комбинация клавиш, которую можно нажать в любой момент для активации элемента меню. Акселератор можно создать двумя способами. Самый простой путь — добавить комбинацию клавиш в отображаемый текст меню или элемента меню при его добавлении к родительскому виджету. Для этого добавьте в конец метки вашего меню знак табуляции ('t') и собственно данную комбинацию. Первой частью комбинации клавиш должна быть Alt, Ctrl или Shift, затем + или — и, наконец, сама клавиша акселератора. Например Newtctrl+n, Save Astctrl-shift-s. Регистр клавиш значения не имеет.
Сама клавиша акселератора может быть цифрой, буквой или функциональной клавишей, обозначаемой от F1 до F12, а так же одним из слов, приведённых в таблице 10.6
АкселераторКлавиша
delDelete
deleteDelete
downDown arrow
endEnd
enterEnter
escEscape
escapeEscape
homeHome
insInsert
insertInsert
leftLeft arrow
pgdnPage down
pgupPage up
returnEnter
rightRight arrow
spaceSpace bar
tabTab
upUp arrow
Методы wxPython не обращают внимания ни на акселераторы ни на мнемоники при поиске меню или элемента меню по имени. Другими словами, вызов menubar.FindMenuItem(«File»,»Save As») найдёт нужный нам элемент меню, даже если его имя было задано как Save &Astctrl-shift-s.
Кроме того, акселераторы могут быть созданы напрямую при помощи таблицы акселераторов, которая представляет из себя экземпляр класса wx.AcceleratorTable. Она содержит список объектов wx.AcceleratorEntry. Конструктор wx.AcceleratorTable принимает список акселераторов; кроме того, он может быть вызван вообще без аргументов. В листинге 10.6 мы используем тот факт, что wxPython вызывает конструктор wx.AcceleratorEntry со списком аргументов (wx.ACCEL_CTRL, ord('Q'), exit.GetId()
). Конструктор wx.AcceleretorEntry выглядит следующим образом:
wx.AcceleratorEntry(flags, keyCode, cmd)
Параметр flags это битовая маска c одной или несколькими из следующих констант: wx.ACCEL_ALT, wx.ACCEL_CTRL, wx.ACCEL_NORMAL, wx.ACCEL_SHIFT. Он определяет, какая из клавиш-модификаторов должна быть нажата для вызова акселератора. Аргумент keyCode содержит саму буквенно-цифровую клавишу, определяющую акселератор. Это может быть либо ASCII код соответствующего символа, либо специальный символ из документации wxWidgets из раздела Keycodes. Аргумент cmd является идентификатором элемента меню, который должен создать командное событие при вызове акселератора. Как можно видеть из листинга 10.6, такое определение акселератора никак не будет отображено в получившейся форме, так что информацию о его наличии на форму придётся добавлять вручную.
10.2.4 Как создать меню с переключателями или чекбоксами?
Элементы меню используются не только для предоставления возможности выбора пользователю, они так же могут отображать состояние приложения. Наиболее популярный способ отображения состояния при помощи меню — использовать меню с переключателями чекбоксами (хотя Вы можете просто изменять текст на элементе меню или активировать/деактивировать его). На рисунке 10.4 отображён пример использования обоих вариантов.
Рисунок 10.4
Листинг 10.7

import wx

class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Toggle Items Example")
p = wx.Panel(self)
menuBar = wx.MenuBar()
menu = wx.Menu()
exit = menu.Append(-1, "Exit")
self.Bind(wx.EVT_MENU, self.OnExit, exit)
menuBar.Append(menu, "Menu")

menu = wx.Menu()
menu.AppendCheckItem(-1,"Check Item 1")
menu.AppendCheckItem(-1,"Check Item 2")
menu.AppendCheckItem(-1,"Check Item 3")
menu.AppendSeparator()
menu.AppendRadioItem(-1,"Radio Item 1")
menu.AppendRadioItem(-1,"Radio Item 2")
menu.AppendRadioItem(-1, "Radio Item 3")
menuBar.Append(menu, "Toggle Items")

self.SetMenuBar(menuBar)
def OnExit(self, event):
self.Close()

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()

Как Вы можете увидеть в листинге, чекбоксы добавляются при помощи метода AppendCheckItem(id, item, helpString=»»), который похож на Append(). Аргументами являются идентификатор wxPython, отображаемое имя и строка помощи для отображения в строке статуса. Кроме того, Вы можете использовать PrependCheckItem(id, item, helpString=»») и InsertCheckItem(pos, id, item, helpString=»»), которые ведут себя так, как Вы от них и ожидаете.
Для добавления переключателей используйте AppendRadioItem(id, item, helpString=»»), PrependRadioItem(id, item, helpString=»») и InsertRadioItem(pos, id, item, helpString=»»). Набор последовательных переключателей образуют группу и из этой группы только один переключатель может быть выбран. Для того, чтобы создать новую группу надо добавить любой другой элемент меню или сепаратор. По умолчанию выбранным является первый переключатель в группе.
Кроме того, различные элементы меню можно создать и при помощи обычного метода Append(), если передать в качестве значения параметра kind wx.ITEM_CHECK, wx.ITEM_NORMAL, wx.ITEM_RADIO или wx.ITEM_SEPARATOR. Это полезно, если вы создаёте меню автоматически на основании каких-либо данных, так как Вы можете создавать любые варианты элементов меню (только учтите, что для создания сепаратора необходимо в качестве id передать wx.ID_SEPARATOR).
Более того, Вы можете создать элемент меню при помощи конструктора wx.MenuItem c использованием данных констант, а потом передать полученный элемент одному из этих методов: AppendItem(), PrependItem() или InsertItem().
Для того, чтобы определить тип элемента меню используйте IsCheckable(), который возвращает True, если это чекбокс или переключатель; или используйте IsChecked(), который возвращает True, если элемент может изменять состояние и на данный момент выбран. Вы можете задать состояние переключаемого элемента при помощи метода Check(check), который принимает логический аргумент, отражающий новое состояние элемента. Если же элемент является переключателем, то автоматически поменяются состояния других элементов группы.
Кроме того, Вы можете получить состояние элемента меню из меню или из строки меню используя метод IsChecked(id), которому надо
передать идентификатор элемента, который Вы хотите проверить. Кончено, тут есть ожидаемые ограничения — метод полосы меню работает только если она уже прикреплена к фрейму, а метод меню работает для элементов, которые находятся в этом меню. Для установки состояния можно использовать их методы Check(id, check) c очевидными аргументами.
10.3 Наводим блеск на ваши меню
В следующих разделах мы рассмотрим с помощью чего Вы можете сделать ваши меню более удобными и более содержательными. Сперва мы поговорим о вложенных подменю, затем о выплывающих меню. В заключение же мы обсудим, как изменить внешний вид ваших меню.
10.3.1 Как создать подменю?
Если ваше приложение сложнее, чем те, которые мы рассмотрели, Вам могут пригодиться вложенные меню (подменю). Обычно они используются для группировки набора опций, объединённых логически, особенно в тех случаях, когда таких опций так много, что поместить их в меню верхнего уровня не представляется возможным. На рисунке 10.5 показан пример такого приложения в wxPython. А в листинге 10.5 приведён его код.
Рисунок 10.5 подменю в wxPython
Листинг 10.5
import wx

class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Sub-menu Example")
p = wx.Panel(self)
menu = wx.Menu()

submenu = wx.Menu()
submenu.Append(-1, "Sub-item 1")
submenu.Append(-1, "Sub-item 2")
# добавляем подменю
menu.AppendMenu(-1, "Sub-menu", submenu)

menu.AppendSeparator()
exit = menu.Append(-1, "Exit")
self.Bind(wx.EVT_MENU, self.OnExit, exit)

menuBar = wx.MenuBar()
menuBar.Append(menu, "Menu")
self.SetMenuBar(menuBar)

def OnExit(self, event):
self.Close()

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()

Вы можете заметить из нашего примера, что подменю создаётся так же как и меню верхнего уровня. Сперва Вы создаёте экземпляр класса wx.Menu, затем добавляете в него элементы меню, как обычно. Разница лишь в том, что вместо того, чтобы прикреплять его к полосе меню, Вы добавляете его к обычному меню при помощи метода AppendMenu(id, text, submenu, helpStr). Аргументы идентичны аргументам метода Append(). Ну и кроме того, есть методы PrependMenu(id, text,submenu, helpStr) и InsertMenu(pos, text, submenu, helpStr) c уже понятным действием.
Помните, что порядок действий для создания подменю более важен, чем для обычных элементов меню. Рекомендуется сперва добавить элементы меню и лишь затем прикреплять его к родительскому меню. Это позволит wxPython корректно зарегистрировать горячие клавиши. Вы можете создавать подменю произвольной глубины, добавляя новое подменю к уже существующему, но порядок создания должен оставаться описанным выше.

10.3.2 Как создавать всплывающие меню?

Меню совсем не обязаны выпадать из полосы меню, находящейся в вершине вашего фрейма. Они вполне могут появляться в любом месте в пределах фрейма. По большей части такие меню используются для выбора действий в зависимости от контекста и относятся к тому объекту, на котором вызывается меню. На рисунке 10.6 показан пример такого приложения. Такие меню создаются так же как и обычные меню, но они не присоединяются к полосе меню. Код этого приложения показан в листинге 10.9

Рисунок 10.6
Листинг 10.9

import wx

class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,
"Popup Menu Example")
self.panel = p = wx.Panel(self)
menu = wx.Menu()
exit = menu.Append(-1, "Exit")
self.Bind(wx.EVT_MENU, self.OnExit, exit)

menuBar = wx.MenuBar()
menuBar.Append(menu, "Menu")
self.SetMenuBar(menuBar)

wx.StaticText(p, -1,
"Right-click on the panel to show a popup menu",
(25,25))

# создаём меню
self.popupmenu = wx.Menu()
# наполняем его
for text in "one two three four five".split():
item = self.popupmenu.Append(-1, text)
self.Bind(wx.EVT_MENU, self.OnPopupItemSelected, item)
# связываем всплывающее меню с событием для его вызова
p.Bind(wx.EVT_CONTEXT_MENU, self.OnShowPopup)

# показываем всплывающее меню
def OnShowPopup(self, event):
pos = event.GetPosition()
pos = self.panel.ScreenToClient(pos)
sel f.panel.PopupMenu(self.popupmenu, pos)

def OnPopupItemSelected(self, event):
item = self.popupmenu.FindItemById(event.GetId())
text = item.GetText()
wx.MessageBox("You selected item '%s'" % text)

def OnExit(self, event):
self.Close()

if __name__ == "__main__":
app = wx.PySimpleApp()
frame = MyFrame()
frame.Show()
app.MainLoop()
Всплывающие меню создаются так же как и любые другие меню (обратите внимание на использование цикла для быстрого создания элементов меню). Но вместо того, чтобы прикреплять его к полосе меню мы сохраняем его в self.popupmenu. После этого мы связываем метод OnShowPopup() c событием wx.EVT_CONTEXT_MENU, которое вызывается тогда, когда в данной ОС должно вызываться контекстное меню. В Windows и GTK это нажатие правой кнопкой мыши, в Mac OS это нажатие на control.
В результате, когда пользователь вызывает контекстное меню срабатывает метод-обработчик OnShowPopup(). Во-первых, он определяет координаты для отображения меню. Координаты, передаваемые в экземпляре wx.ContextMenuEvent отражают абсолютные значения для экрана, так что нам надо перевести их в относительные значения для нашей панели. Для этого мы используем метод ScreenToClient().
После того, отображается контекстное меню с помощью метода PopupMenu(menu, pos) или PopupMenuXY(menu, x, y). Данная функция возвращает значение только после того, как будет выбран элемент меню или меню будет закрыто нажатием кнопки escape или кликом вне пределов меню. Если был выбран какой-либо элемент меню, его обработчик вызывается стандартным образом (то есть, Вы должны связать обработчик этого элемента с событием EVT_MENU), и лишь после завершения обработчика управление перейдёт к следующему оператору после вызова PopupMenu(). Этот метод возвращает не интересующее нас логическое значение, так что единственный способ обработать выбор элемента меню — использовать стандартный механизм обработки событий.
Всплывающее меню может иметь заголовок, который отображается вверху меню, когда оно появляется на экране. Заголовок управляется при помощи свойств wx.Menu.SetTitle(title) и wx.Menu.GetTitle().
10.3.3 Как создавать забавные меню
Если обычные элементы меню выглядят не очень интересно, то Вы можете добавить к ним свой рисунок для отображения рядом с этим элементом меню (или использовать какой-то значок). Кроме того, в Windows Вы можете изменить шрифт или цвет элемента меню. На рисунке 10.7 приведён пример такого меню:
Рисунок 10.7
Листинг 10.10 содержит код для получения такого меню. Для определения платформы, на которой запускается ваша программа можно проверить наличие wxMSW в кортеже wx.PlatformInfo.
Листинг 10.10

import wx


class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1,
            «Fancier Menu Example»)
        p = wx.Panel(self)
        menu = wx.Menu()


        bmp = wx.Bitmap(«open.png», wx.BITMAP_TYPE_PNG)
        item = wx.MenuItem(menu, -1, «Has Open Bitmap»)
        # добавляем собственную картинку
        item.SetBitmap(bmp)
        menu.AppendItem(item)


        if 'wxMSW' in wx.PlatformInfo:
            font = wx.SystemSettings.GetFont(
                wx.SYS_DEFAULT_GUI_FONT)
            font.SetWeight(wx.BOLD)
            item = wx.MenuItem(menu, -1, «Has Bold Font»)
            # изменяем шрифт
            item.SetFont(font)
            menu.AppendItem(item)


            item = wx.MenuItem(menu, -1, «Has Red Text»)
            # изменяем цвет
            item.SetTextColour(«red»)
            menu.AppendItem(item)


        menu.AppendSeparator()
        exit = menu.Append(-1, «Exit»)
        self.Bind(wx.EVT_MENU, self.OnExit, exit)

        menuBar = wx.MenuBar()
        menuBar.Append(menu, «Menu»)
        self.SetMenuBar(menuBar)

    def OnExit(self, event):
        self.Close()

if __name__ == «__main__»:
    app = wx.PySimpleApp()
    frame = MyFrame()
    frame.Show()
    app.MainLoop()
Изменение цвета или стиля элемента меню достигается манипуляциями со свойствами этого элемента. Единственным кроссплатформенным свойством является картинка, которая управляется методом GetBitmap(), возвращающим элемент типа wx.Bitmap. Для этого свойства есть два установщика. Первый, работающий на всех платформах, это SetBitmap(bmp). Он устанавливает картинку, отображаемую рядом с элементом меню. Если Вы работаете на Windows и хотите установить картинку для переключаемых меню, Вы можете использовать SetBitmap(checked, unchecked=wx.NullBitmap), который устанавливает одну картинку для отображения рядом с отмеченным элементом и другую для отображения рядом с не отмеченными.
В Windows есть ещё три свойства, которые Вы можете изменять для управления внешним видом элементов меню; они перечислены в таблице 10.7. Мы рекомендуем использовать их осторожно и только в тех случаях, когда они действительно облегчают использование вашего приложения.
МетодыОписание
GetBackgroundColour()
SetBackgroundColour(colour)
Тип свойства — wx.Colour. Установщику так же можно передать строку с именем цвета wxPython. Управляет фоновым цветом элемента меню.
GetFont()
SetFont(font)
Управляет шрифтом элемента. Тип — wx.Font
GetTextColour()
SetTextColour(colour)
Управляет цветом шрифта элемента. Тип как и у методов для цвета фона
Теперь, когда мы поговорили о функциональных аспектах использования меню, мы поговорим о том, как правильно делать меню и как облегчить пользователю работу с ними.
10.4 Как создавать «правильные» меню
Д
ля сложных приложений полоса меню — главная точка взаимодействия пользователя с вашим приложением. Правильно составленное меню может значительно облегчить работу с вашей программой; неверная же компоновка может сделать её использование невозможным. Принимая во внимание сказанное выше, приступим к обсуждению советов по созданию меню.
10.4.1 Поддержка одинаковой длины меню
Если Вы посмотрите на те приложения, которые Вы постоянно используете, то заметите, что меню длиной 10-15 элементов можно охватить взглядом, тогда как более длинные меню просто замыливают взгляд пользователя. Если ваше меню превышает эти пределы — настоятельно рекомендуется его разделить на несколько частей. В идеале все меню должны обладать одинаковым количеством элементов, хотя надо понимать, что это не всегда возможно и не всегда желательно.

10.4.2 Логичная группировка элементов

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

Твёрдо придерживайтесь стандартов при упорядочивании меню

Вы всегда должны придерживаться стандарта при определении порядка расположения меню. Самое левое меню — FILE, содержащее такие элементы, как new, open, save, print и exit, обычно именно в таком порядке. Если Вам нужно добавить сюда ещё какие-то возможности — лучше всего поместить их между print и exit. Такое расположение Вы встретите почти в каждом приложении. Следующим меню идёт EDIT, содержащий такие пункты как undeo, cut, copy, paaste и обычно find, в зависимости от вашего приложения. Меню HELP всегда является самым правым меню, а рядом с ним обычно находится меню WINODWS. Между этими крайними точками Вы можете располагать те меню, которые сочтёте нужным.
Предоставьте простой доступ для часто используемых элементов
Пользователю всегда проще использовать те элементы меню, которые расположены выше, чем те, что расположены ниже, поэтому наиболее часто используемые элементы должны находиться вверху меню. Единственное исключение в том, что эксперимент показывает, что второй сверху элемент доступнее первого.
Используйте понятные названия для меню
Помните, что ширина меню в полосе меню пропорциональна длине имени меню, а ширина меню определяется по элементу меню с самым длинным именем. Постарайтесь не давать меню верхнего уровня имена короче четырёх букв. Мы рекомендуем, за исключением общепринятых обозначений, давать настолько длинные имена, чтобы суть их была однозначна ясна. Не бойтесь длинных имен для меню, но и не забывайте, что читать 30-40 символом может быть утомительно.
Помните про многоточия, когда элемент ведёт к диалоговому окну
Все элементы меню, которые вызывают диалоговые окна, должны заканчиваться на многоточие (…)
Придерживайтесь стандартных горячих клавиш
Для горячих клавиш используйте общепринятые стандарты соответствия комбинаций клавиш действиям, приведённые в таблице 10.8
Для возврата действия нет общепринятой комбинации; Вы можете встретить как ctrl+y так и alt+z, или другие наборы клавиш. Если Вы предоставляете большое количество дополнительных горячих клавиш, то рекомендуется дать пользователю возможность их переназначать. Горячие клавиши особенно полезны в приложениях, гд пользователю приходится много печатать, например в текстовых редакторах, и они могут быть бесполезны там, где пользователь много работает мышкой.

Таблица 10.8
Комбинация клавишФункция
Crtl+aВыделить всё
Crtl+cКопировать
Crtl+fИскать
Crtl+gИскать далее
Crtl+nНовое
Crtl+oОткрыть
Crtl+pПечатать
Crtl+qВыйти
Crtl+sСохранить
Crtl+vВставить
Crtl+wЗакрыть
Crtl+xВырезать
Crtl+zОтменить

Показывайте текущий статус переключателей

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

Реализация импорта в Питоне переписана на чистый Питон

Python 3.3 продолжает радовать. Сегодня в него вошли изменения от Bratt Cannon. Теперь import реализован почти весь на питоне, в библиотеке importlib. Она и раньше присутствовала — но теперь __import__ ведёт напрямую в importlib.

Казалось бы, какая разница? На самом деле всё просто:

  • Проще поддерживать и развивать. Существующий код в import.c нелегко читать и ещё сложнее понимать во всех деталях (включая в том числе и кроссплатформенность). Теперь ситуация значительно улучшится.

  • Легче изучать. Повторюсь, понимать реализацию по чтению Python/import.c (а именно там расположен почти весь код, относящийся к импорту модулей и пакетов) — нелегко. Отчасти потому, что написана на C, в то время как большинства питонщиков этот язык, к сожалению, практически не знаком. Главная же причина в том, что этот механизм эволюционировал потихоньку, счастливо избегая больших переделок — но уверенно вбирая в себя сотни мелких и средних улучшений. Проще и понятней от этого не становилось.

  • Унификация. Сложилась на самом деле парадоксальная ситуация: в то время как PEP 302, исчерпывающе описывает то, как должен работать механизм импорта — Питон этим не пользуется. То есть да, PEP 302 полностью реализован и поддерживается — но стандартный импорт из файловой системы идёт в обход этих правил. Не подумайте плохого — ничего не ломается. Но одновременное существование двух дублирующих систем — одной для импорта из файловой системы, а второй для поддержки PEP 302, — уже не очень хорошая вещь.

  • Снова вспоминаем про поддержку, развитие и изучение. Почему, например, по умолчанию sys.meta_path пустой? Отчего в sys.path_hooks есть только zipimporter? Где вообще import hooks для загрузки модулей из файловой системы? Почему Python не прописывает в модулях __loader__, хотя PEP 302 однозначно предписывает это делать?

  • Как, в конце концов, всё работает? Читатель, вы уверены что понимаете механизм во всех деталях?

Документация хороша, но она описывает далеко не все нюансы. Когда я попытался разобраться — мне пришлось потратить немало времени. Потом это вылилось в серию статей, посвящённых импорту: часть 1, часть 2, часть 3, импорт и юникод. На этом терпение закончилось и заключительные статьи серии так и не были опубликованы.

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

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

Немного о скорости. Питоновская реализация примерно на 5% медленней — что никого не беспокоит. Быть может, к версии 3.4 маятник качнётся назад, и наиболее критические части кода будут реализованы на С (как это произошло, например, с модулем io). Сейчас гораздо важнее корректность, — а скорость, и без того вполне приемлемая, вполне может подождать своего часа.

Запасайтесь поп-корном, Python 3.3 будет выпущен в конце августа. Это будет по настоящему выдающаяся версия!

Автор: Andrew Svetlov