Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
Рис. 13.5. Схема расположения виджетов в главном окне программы Bookmarks Программы с главным окном 567 Обратите внимание, что не все действия, предусматриваемые системой меню, мы сделали доступными в виде кнопок панели инструментов – это распространенная практика. scrollbar = tkinter.Scrollbar(frame, orient=tkinter.VERTICAL) self.listBox = tkinter.Listbox(frame, yscrollcommand=scrollbar.set) self.listBox.grid(row=1, column=0, sticky=tkinter.NSEW) self.listBox.focus_set() scrollbar["command"] = self.listBox.yview scrollbar.grid(row=1, column=1, sticky=tkinter.NS) self.statusbar = tkinter.Label(frame, text="Ready...", anchor=tkinter.W) self.statusbar.after(5000, self.clearStatusBar) self.statusbar.grid(row=2, column=0, columnspan=2, sticky=tkinter.EW) frame.grid(row=0, column=0, sticky=tkinter.NSEW) Центральную область окна (область между панелью инструментов и строкой состояния) занимает виджет списка и ассоциированная с ним полоса прокрутки. Виджет списка размещается с выравниванием по всем направлениям, а полоса прокрутки – только с выравниванием по северу и югу (northsouth, то есть вертикально). Оба виджета добавля ются в сетку центральной области окна бок о бок. Нам необходимо гарантировать, что при перемещении по списку с по мощью клавиш управления курсором или при изменении позиции движка в полосе прокрутки, оба виджета будут синхронизироваться между собой. Для этого при создании виджета списка в аргументе yscrollcommand ему передается метод set() полосы прокрутки (чтобы при перемещении по списку полоса прокрутки перемещала бы движок соответственно текущей позиции в списке), а в атрибут command полосы прокрутки записывается метод yview() виджета списка (чтобы при пе ремещении движка полосы прокрутки соответственным образом про исходила и прокрутка списка). Полоса состояния – это просто метка. Метод after() запускает таймер однократного срабатывания (таймер, который срабатывает всего один раз через указанный интервал времени). В первом аргументе методу пе редается интервал времени в миллисекундах, а во втором аргументе – функция или метод, который должен быть вызван по истечении ука занного времени. Это означает, что сразу после запуска программы, в течение пяти секунд, в строке состояния будет отображаться текст «Ready…», после чего строка состояния будет очищена. Строка состоя ния размещается в самой последней строке, с выравниванием с запада на восток (westeast, горизонтально). В самом конце в окно добавляется сама рабочая область. На этом мы завершили создание главного окна и размещение виджетов в нем, но при такой реализации все виджеты по умолчанию будут иметь фикси 568 Глава 13. Введение в программирование графического интерфейса рованный размер и изменение размеров окна не будет приводить к из менениям размеров виджетов. Следующий фрагмент программного кода решает эту проблему и завершает метод инициализации. frame.columnconfigure(0, weight=999) frame.columnconfigure(1, weight=1) frame.rowconfigure(0, weight=1) frame.rowconfigure(1, weight=999) frame.rowconfigure(2, weight=1) window = self.parent.winfo_toplevel() window.columnconfigure(0, weight=1) window.rowconfigure(0, weight=1) self.parent.geometry("{0}x{1}+{2}+{3}".format(400, 500, 0, 50)) self.parent.title("Bookmarks Unnamed") Методы columnconfigure() и rowconfigure() позволяют определять вес столбцов и строк сетки. Сначала определяется вес для сетки рабочей области окна, первому столбцу и второй строке (где находится виджет списка) придается максимальный вес, поэтому при изменении разме ров рабочей области весь избыток пространства будет отдан виджету списка. Только этого еще недостаточно – нам необходимо разрешить изменение размеров окна верхнего уровня, содержащего рабочую об ласть, что мы и сделаем, получив ссылку на объект окна вызовом ме тода wininfo_toplevel() и установив вес строки и столбца равным 1. В конце метода устанавливаются начальные размеры окна и его поло жение с помощью строки в формате ширинаxвысота+x+y. (Если бы нам по требовалось определить только размеры окна, мы могли бы сделать это с помощью строки в формате ширинаxвысота.) Наконец, установкой заго ловка окна мы завершаем создание пользовательского интерфейса. Если пользователь щелкнет на кнопке в панели инструментов или вы берет пункт меню, будет вызван метод, ответственный за выполнение данной операции. Некоторые методы опираются на использование других, вспомогательных методов. Мы поочередно рассмотрим все ме тоды, начав с того, который вызывается через пять секунд после за пуска программы. def clearStatusBar(self): self.statusbar["text"] = "" Строка состояния – это простой виджет tkinter.Label. Для ее очистки мы могли бы в методе after() использовать лямбдавыражение, но, так как нам потребуется очищать строку состояния из разных точек про граммы, мы создали для этого специальный метод. def fileNew(self, *ignore): if not self.okayToContinue(): return self.listBox.delete(0, tkinter.END) Программы с главным окном 569 self.dirty = False self.filename = None self.data = {} self.parent.title("Bookmarks Unnamed") Если пользователю потребуется создать новый файл с закладками, мы сначала должны предоставить ему возможность сохранить имеющие ся изменения в текущем файле. Эта возможность реализована в виде отдельного метода MainWindow.okayToContinue(), потому что она будет ис пользоваться в нескольких местах в программе. Метод возвращает True , если можно продолжать создание файла, и False – в противном случае. Если можно продолжать, мы очищаем виджет списка, удаляя все записи – от первой до последней, где tkinter.END – это константа, которая используется для определения последнего элемента в случа ях, когда виджеты могут содержать несколько элементов. Затем сбра сывается флаг наличия изменений, имя файла и словарь с данными, поскольку новый файл еще пустой и никаких изменений еще не было сделано, а затем мы устанавливаем текст заголовка, отражающий тот факт, что был создан новый и еще не сохранявшийся файл. Переменная ignore хранит последовательность из нуля или более пози ционных аргументов, которые нам не нужны. В случае когда метод вызывается в результате выбора пункта меню или щелчка на инстру ментальной кнопке, ему не передается никаких дополнительных аргу ментов, но в случае вызова по нажатию горячей комбинации клавиш (например, Ctrl+N) ему будет передаваться объект события, а поскольку мы не различаем, как именно пользователь вызывает данное действие, мы просто игнорируем объект события. def okayToContinue(self): if not self.dirty: return True reply = tkinter.messagebox.askyesnocancel( "Bookmarks Unsaved Changes", "Save unsaved changes?", parent=self.parent) if reply is None: return False if reply: return self.fileSave() return True Если пользователь желает выполнить действие, которое приведет к очистке виджета списка (например, когда создается новый или от крывается существующий файл), нам необходимо предоставить ему возможность сохранить любые несохраненные изменения. Если содер жимое файла не изменялось, следовательно, отпадает необходимость выполнять сохранение и можно сразу же вернуть значение True. В про тивном случае выводится стандартный диалог с сообщением, содержа щий кнопки Yes (да), No (нет) и Cancel (отмена). Если пользователь отме няет операцию, в переменную reply записывается значение None, мы 570 Глава 13. Введение в программирование графического интерфейса трактуем это как то, что пользователь не хочет продолжать начатую операцию и не хочет сохранять изменения, и просто возвращаем значе ние False. Если пользователь отвечает согласием, в переменную replay записывается значение True, поэтому мы даем пользователю возмож ность сохранить изменения и возвращаем True, если изменения были сохранены и False – в противном случае. Если пользователь ответил от казом, в переменную replay записывается значение False, что для нас означает отказ от сохранения изменений, но мы все равно возвращаем True , потому что пользователь выразил желание продолжить операцию без сохранения изменений. Стандартные диалоги библиотеки Tk не импортируются инструкцией import tkinter , поэтому для данного метода необходимо добавить инст рукцию import tkinter.messagebox и инструкцию import tkinter.filedia log – для следующего метода. В Windows и Mac OS X используются стандартные диалоги этих операционных систем, а для других плат форм используются диалоги, реализованные в самой библиотеке Tk. Мы всегда передаем диалогам ссылку на родительское окно, чтобы при вызове они автоматически располагались в центре родительского окна. Все стандартные диалоги являются модальными, то есть при появле нии на экране они становятся единственным окном приложения, с ко торым пользователь может взаимодействовать, поэтому, чтобы полу чить возможность продолжить работу с приложением, пользователь должен закрыть их (щелчком на кнопке OK (Готово), Open (Открыть), Can cel (Отменить) или подобной им). Модальные диалоги являются для про граммиста самой удобной разновидностью диалогов, так как пользова тель лишен возможности изменить состояние программы, пока откры то окно диалога, поскольку модальный диалог блокирует приложе ние, пока не будет закрыт. Слово «блокирует» здесь означает, что инструкция, следующая за вызовом модального диалога, выполнится, только когда диалог будет закрыт. def fileSave(self, *ignore): if self.filename is None: filename = tkinter.filedialog.asksaveasfilename( title="Bookmarks Save File", initialdir=".", filetypes=[("Bookmarks files", "*.bmf")], defaultextension=".bmf", parent=self.parent) if not filename: return False self.filename = filename if not self.filename.endswith(".bmf"): self.filename += ".bmf" try: with open(self.filename, "wb") as fh: pickle.dump(self.data, fh, pickle.HIGHEST_PROTOCOL) self.dirty = False Программы с главным окном 571 self.setStatusBar("Saved {0} items to {1}".format( len(self.data), self.filename)) self.parent.title("Bookmarks {0}".format( os.path.basename(self.filename))) except (EnvironmentError, pickle.PickleError) as err: tkinter.messagebox.showwarning("Bookmarks Error", "Failed to save {0}:\n{1}".format( self.filename, err), parent=self.parent) return True Если имя текущего файла не задано, мы должны потребовать от поль зователя выбрать имя файла. Если пользователь отменяет операцию, мы возвращаем False, чтобы показать, что операцию следует отме нить. В противном случае мы, если это необходимо, устанавливаем корректное расширение имени файла. Используя существующий или создавая новый файл, мы сохраняем в файле словарь self.data в закон сервированном виде. После сохранения закладок мы сбрасываем при знак наличия изменений, выводим сообщение в строку состояния (ко торое будет очищено через определенное время, в чем мы вскоре убе димся) и помещаем имя файла в заголовок окна (без пути к нему). Ес ли попытка сохранить файл потерпела неудачу, мы выводим диалог с текстом предупреждения (в котором уже присутствует кнопка OK), чтобы проинформировать пользователя о возникшей проблеме. def setStatusBar(self, text, timeout=5000): self.statusbar["text"] = text if timeout: self.statusbar.after(timeout, self.clearStatusBar) Этот метод выводит указанный текст в строке состояния и, если задано предельное время отображения (по умолчанию – пять секунд), метод запускает таймер однократного срабатывания, который очистит стро ку состояния по прошествии указанного времени. def fileOpen(self, *ignore): if not self.okayToContinue(): return dir = (os.path.dirname(self.filename) if self.filename is not None else ".") filename = tkinter.filedialog.askopenfilename( title="Bookmarks Open File", initialdir=dir, filetypes=[("Bookmarks files", "*.bmf")], defaultextension=".bmf", parent=self.parent) if filename: self.loadFile(filename) Этот метод начинается точно так же, как и метод MainWindow.fileNew(), предоставляя пользователю возможность сохранить имеющиеся изме нения или отменить операцию открытия файла. Если пользователь подтверждает свое желание продолжить, мы стараемся предложить 572 Глава 13. Введение в программирование графического интерфейса пользователю рационально выбранный каталог, поэтому мы использу ем каталог, где находится текущий файл, если таковой имеется; в про тивном случае – текущий рабочий каталог. Аргумент filetypes – это список (описание и маска) кортежей из двух элементов, которые ото бражаются диалогом выбора файла. Если пользователь выберет имя файла, мы запоминаем его выбор и вызываем метод loadFile(), кото рый прочитает содержимое файла. Создание отдельного метода loadFile(), позволяющего загружать фай лы без привлечения внимания пользователя, – это обычная практика. Например, некоторые программы на запуске автоматически загружа ют последний использовавшийся файл, а некоторые программы даже сохраняют список последних использовавшихся файлов в виде пунк тов меню, чтобы при выборе любого из таких пунктов напрямую вызы вался бы метод loadFile() с именем файла, ассоциированным с выбран ным пунктом меню. def loadFile(self, filename): self.filename = filename self.listBox.delete(0, tkinter.END) self.dirty = False try: with open(self.filename, "rb") as fh: self.data = pickle.load(fh) for name in sorted(self.data, key=str.lower): self.listBox.insert(tkinter.END, name) self.setStatusBar("Loaded {0} bookmarks from {1}".format( self.listBox.size(), self.filename)) self.parent.title("Bookmarks {0}".format( os.path.basename(self.filename))) except (EnvironmentError, pickle.PickleError) as err: tkinter.messagebox.showwarning("Bookmarks Error", "Failed to load {0}:\n{1}".format( self.filename, err), parent=self.parent) Когда этот метод вызывается, то точно известно, что любые изменения уже были сохранены или отвергнуты, поэтому можно очистить вид жет списка. Мы сохраняем полученное имя файла, очищаем виджет списка и признак наличия изменений и затем производим попытку от крыть файл и распаковать его содержимое в словарь self.data. После того как данные будут прочитаны, выполняется обход всех имен за кладок и добавление их по очереди в виджет списка. В заключение в строку состояния выводится информационное сообщение и обновля ется текст заголовка окна. def fileQuit(self, event=None): if self.okayToContinue(): self.parent.destroy() Это метод последнего пункта в меню File. Мы даем пользователю воз можность сохранить имеющиеся изменения. Если при этом пользова Программы с главным окном 573 тель отменяет операцию, то ничего не происходит и программа продол жит свою работу. В противном случае мы предписываем родительскому виджету уничтожить себя, что приводит к благополучному заверше нию программы. Если бы нам потребовалось сохранить пользователь ские настройки, это можно было бы сделать здесь, непосредственно пе ред вызовом метода destroy(). def editAdd(self, *ignore): form = AddEditForm(self.parent) if form.accepted and form.name: self.data[form.name] = form.url self.listBox.delete(0, tkinter.END) for name in sorted(self.data, key=str.lower): self.listBox.insert(tkinter.END, name) self.dirty = True Этот метод вызывается, если пользователь запрашивает операцию до бавления новой закладки (выбором пункта меню Edit →Add (Правка→ До бавить), или щелчком на кнопке с изображением в панели инстру ментов, или нажатием комбинации клавиш Ctrl+A на клавиатуре). Класс AddEditForm – это наш собственный диалог, который будет описываться в следующем подразделе, а пока нам достаточно знать, что он имеет флаг accepted, который устанавливается в значение True, если пользова тель щелкнет на кнопке OK, и False, если пользователь щелкнет на кноп ке Cancel, а также два атрибута, name и url, в которых хранятся имя и ад рес URL закладки, добавляемой или редактируемой пользователем. Мы создаем новый экземпляр диалога AddEditForm, который тут же по является на экране как модальный диалог – вследствие чего дальней шее выполнение приложения блокируется и инструкция if form.ac cepted ... не будет выполнена, пока диалог не будет закрыт. Если в диалоге AddEditForm пользователь щелкнет на кнопке OK и при этом укажет имя закладки, мы добавляем новую закладку с указан ным именем и адресом URL в словарь self.data. Затем мы очищаем виджет списка и снова вставляем в него все закладки в отсортирован ном порядке. Возможно, более эффективно было бы просто вставлять новую закладку сразу в нужное место, но, даже когда имеется не сколько сотен закладок, на современной машине различия будут едва заметны. В конце мы устанавливаем флаг наличия несохраненных из менений, поскольку теперь у нас появилось изменение, которое еще не было сохранено. def editEdit(self, *ignore): indexes = self.listBox.curselection() if not indexes or len(indexes) > 1: return index = indexes[0] name = self.listBox.get(index) form = AddEditForm(self.parent, name, self.data[name]) if form.accepted and form.name: 574 Глава 13. Введение в программирование графического интерфейса self.data[form.name] = form.url if form.name != name: del self.data[name] self.listBox.delete(0, tkinter.END) for name in sorted(self.data, key=str.lower): self.listBox.insert(tkinter.END, name) self.dirty = True Редактирование представляет собой немного более сложную опера цию, чем добавление новой закладки, потому что сначала нам необхо димо отыскать закладку, которую пользователь желает отредактиро вать. Метод curselection() возвращает список (возможно пустой) пози ций всех выделенных элементов в виджете списка. Если выделен толь ко один элемент, мы запоминаем его текст, то есть имя закладки (а также ключ в словаре self.data), которую пользователь собрался от редактировать. Затем создается новый экземпляр диалога AddEditForm, которому передается имя и адрес URL закладки. После того как диалог будет закрыт, если пользователь указал непус тое имя закладки и щелкнул на кнопке OK, выполняется обновление словаря self.data. Если имя закладки осталось прежним, можно про сто установить флаг наличия несохраненных изменений и выйти (в данном случае предполагается, что пользователь изменил только адрес URL), но если имя закладки изменилось, мы удаляем элемент словаря, ключом которого является прежнее имя, очищаем виджет списка и вновь заполняем его информацией о закладках, как мы дела ли это в методе добавления новой закладки. def editDelete(self, *ignore): indexes = self.listBox.curselection() if not indexes or len(indexes) > 1: return index = indexes[0] name = self.listBox.get(index) if tkinter.messagebox.askyesno("Bookmarks Delete", "Delete '{0}'?".format(name)): self.listBox.delete(index) self.listBox.focus_set() del self.data[name] self.dirty = True Чтобы удалить закладку, мы сначала должны отыскать закладку, вы бранную пользователем, поэтому данный метод начинается точно так же, как и метод MainWindow.editEdit(). Если была выбрана всего одна закладка, мы выводим диалог, спрашивая пользователя, действитель но ли он желает удалить закладку. Если пользователь отвечает утвер дительно, функция вызова диалога возвращает значение True, мы уда ляем закладку из виджета списка и из словаря self.data и устанавли ваем флаг наличия несохраненных изменений. Кроме того, мы возвра щаем фокус ввода в виджет списка. |