Pull to refresh

Ещё немного о компоновке и виджетах

Reading time 9 min
Views 5K
Это продолжение серии статей о PyGTK.

В предыдущей статье мы подготовили Windows и Ubuntu для разработки PyGTK приложений, работали с редактором интерфейсов Glade, рассмотрели горизонтальный и вертикальный типы компоновки, использовали область прокрутки и текстовый редактор, вертикальную группу кнопок, кнопки, и, немного, сигналы. В результате у нас получилось первое настоящее кросплатформенное приложение, которое успешно работало в Ubuntu и Windows. Если вы не читали предыдущую статью, я рекомендую вам начать именно с неё.

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

Идея, сценарий


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

Алгоритм для игры возьмём на википедии: он очень простой и доходчиво описан. Напоминаю, мы осваиваем PyGTK, поэтому сложные алгоритмы увели бы от главной темы. Если вы хотите узнать всё об алгоритмах, почитайте Дональда Кнута.

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

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

Сделаем набросок интерфейса в Inkscape:


Для создания файла интерфейса будем использовать редактор интерфейсов Glade. Пожалуйста, запустите Glade. Если вы не знаете, о чём идёт речь, пожалуйста, прочитайте предыдущую статью.

Табличная компоновка


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

По наброску видно, что потребуется три строчки для крестиков-ноликов, а также по одной строчке для размещения кнопок сверху и снизу.
Создаём новое окно,

сразу делаем его видимым (это будет главное окно нашей программы),

и добавляем табличную компоновку

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

Добавим две кнопки. Размещаем кнопку в самую первую ячейку таблицы:
.
Как видите, кнопка заняла всю ячейку. Мы могли бы использовать эту особенность для отрисовки игрового поля, но не будем этого делать, и здесь нам такое поведение тоже не требуется, поэтому переключаемся в свойствах на вкладку «Упаковка». В «Вертикальные параметры» снимаем галочку с «Заполнение»
.
Таким образом мы указываем, что кнопка не должна заполнять всё доступное ей пространство по высоте
.
Если вы обратили внимание, ячейки таблицы неодинаковы по ширине. Это помешает нам сделать игровое поле с одинаковыми ячейками. Поэтому выбираем table1, в основных свойствах меняем «Гомогенность» на «Да»
,
ячейки стали одинаковы по ширине.

На наброске верхняя и нижняя кнопка занимают всё пространство по горизонтали, а сейчас верхняя кнопка занимает только одну ячейку. Нужно, чтобы она занимала три ячейки по ширине. Выбираем кнопку, свойства «Упаковка», «Прибавление справа»,

увеличиваем значение до трёх. Кнопка занимает три ячейки по ширине
.

Кнопка с иконками


Хочется, чтобы на кнопках «Новая игра» и «Выйти из игры» был не только текст, но и иконки. Сделать это довольно просто. В основных свойствах меняем «Правка типа» на «Контейнер»
.
Теперь мы можем разместить в контейнере на поверхности кнопки всё, что угодно.

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

Внутри выравнивания есть место только для одного элемента, а нам нужно разместить два. Воспользуемся горизонтальной компоновкой с двумя ячейками, разместим её в выравнивании
.
В левую ячейку поместим иконку
,
а в правую — текстовую метку с текстом «Новая игра»
,
.

Чтобы не искать и не рисовать иконку, чтобы не загружать потом её при старте программы, воспользуемся готовыми иконками GTK. Выберите иконку «Создать» в основных свойствах
.

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

Сделайте то же самое для нижней кнопки «Выйти из игры». Должно получиться примерно так
,
я использовал иконку «Закрыть».

Сохраняем интерфейс в файл gui.glade, игровое поле будет создаваться при старте игры программно.

Игровое поле


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

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

Создадим заготовку программы:
#!/usr/bin/env python
# coding: utf-8
import sys
import os
import pygtk
pygtk.require('2.0')
import gtk
import gtk.glade
        
class App:

    def __init__(self):
        # Загружаем файл интерфейса
        self.gladefile = "gui.glade"
        # дерево элементов интерфейса
        self.widgetsTree = gtk.glade.XML(self.gladefile)
        # таблица        
        self.table1 = self.widgetsTree.get_widget("table1")       
        # иициализируем игровое поле   
        self.init_board()
        # Соединяем событие закрытия окна с функцией завершения приложения
        self.window = self.widgetsTree.get_widget("window1")
        if (self.window):
            self.window.connect("destroy", self.close_app)
           
    def close_app(self, widget):    
        gtk.main_quit()        
    
if __name__ == "__main__":
    app = App()
    gtk.main()

Игровое поле будет создаваться в self.init_board(), напишем его:
    def init_board(self):
        # словарь-хранилище меток игрового поля
        self.board_widgets = {}
        # будем добавлять метки в строки 1-3
        for row in range(0,3):
            # инициализируем строку хранилища, создаём пустой словарь
            # для хранения содержимого ячеек таблицы
            self.board_widgets[row] = {}
            # проходим столбцы 0-2
            for column in range(0,3):
                # создаём метку, её текст включает номер строки и столбца
                self.board_widgets[row][column] = \
                                gtk.Label("label_%d_%d" % (column, row))
                # присоединяем метку к таблице
                self.table1.attach(self.board_widgets[row][column], 
                                   column, column + 1,
                                   row + 1, row + 2)
                # делаем метку видимой
                self.board_widgets[row][column].show()

Игровое поле хранится в «двумерном массиве», созданном на базе словарей. В конструктор метки, gtk.Label(), передаётся текст, который будет отображать метка. Метод таблицы attach первым параметром принимает виджет, в данном случае нашу метку. Вторым — номер столбца, к которому прикрепится левая сторона метки, третьим — номер столбца, к которому прикрепится правая сторона метки. Т.е., для того, чтобы поместить метку в самый первый столбец, второй и третий параметры должны быть 0 и 1. Строка, в которую следует поместить виджет, указывается в attach точно таким же образом, в третьем и четвёртом параметре. Описание всех параметров вы можете прочитать в документации.

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

Область событий, рамка


Наступил момент оживить программу: сделаем нижнюю кнопку рабочей, а также добавим обработчик нажатия левой кнопки мыши к каждой метке. Откроем Glade, чтобы добавить обработчик нажатия к кнопке. Выбираем кнопку, переходим на вкладку «Сигналы» в свойствах, и назначаем обработчик для сигнала clicked
.
Сохраняем изменения, возвращаемся к редактированию кода. Добавляем в __init__ такие строчки:
        # Словарик, задающий связи событий с функциями-обработчиками
        dic = {                 
                "button2_clicked_cb": self.close_app,
            }
        # Магическая команда, соединяющая сигналы с обработчиками
        self.widgetsTree.signal_autoconnect(dic)

Сохраняем код. Можете проверить, программа закрывается при нажатии на нижнюю кнопку.

Для каждой метки тоже нужен обработчик события нажатия мыши, button-press-event. Но если вы его добавите прямо сейчас, запустите программу, и проверите, работает ли он, то убедитесь, что не работает. Метка не способна обрабатывать такие события, потому что она не имеет своего окна. Все виджеты, которые не имеют своего окна, перечислены в этом списке.

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

Необходимо отразить это в self.init_board(), заодно добавим обработчик событий для областей событий:
    def init_board(self):
        # словарь-хранилище меток игрового поля
        self.board_widgets = {}
        # будем добавлять метки в строки 1-3
        for row in range(0,3):
            # инициализируем строку хранилища, создаём пустой словарь
            # для хранения содержимого ячеек таблицы
            self.board_widgets[row] = {}
            # проходим столбцы 0-2
            for column in range(0,3):
                # создаём область событий
                event_box = gtk.EventBox()
                # создаём метку, её текст включает номер строки и столбца
                label = gtk.Label("label_%d_%d" % (column, row))
                # делаем метку видимой
                label.show()
                # помещаем метку в область событий
                event_box.add(label)
                # определяем обработчик события для области
                event_box.connect("button_press_event", self.label_clicked, row, column)
                # записываем всё это хозяйство в хранилище
                self.board_widgets[row][column] = event_box                
                # присоединяем область событий с меткой к таблице
                self.table1.attach(self.board_widgets[row][column], 
                                   column, column + 1,
                                   row + 1, row + 2)
                # делаем область событий с меткой видимой
                self.board_widgets[row][column].show()    

    def label_clicked(self,widget, event, row, column):
        print "column %s, row %s" % (column, row)

В функции connect помимо сигнала и виджета мы также указываем два дополнительных параметра — строку и столбец. Эти параметры читаются в label_clicked, и затем будут использоваться для уравления игрой. И область событий, и метку нужно сделать видимыми с помощью show(). Если вы запустите программу, и понажимаете на метки, в консоли вы увидите реакцию в виде вывода столбца и строки таблицы, в которой расположена метка.

Интерфейс почти готов, но без линий разметки на клеточки играть неудобно. Нужно добавить рамки к меткам. В GTK есть специальный виджет-контейнер, который выводит рамку. В Glade он так и называется, «Рамка»
.

Нужно изменить код, чтобы область событий располагалась внутри рамки. Заодно уберём вывод текста на метках, нам это больше не нужно.
    def init_board(self):
        # словарь-хранилище меток игрового поля
        self.board_widgets = {}
        # будем добавлять метки в строки 1-3
        for row in range(0,3):
            # инициализируем строку хранилища, создаём пустой словарь
            # для хранения содержимого ячеек таблицы
            self.board_widgets[row] = {}
            # проходим столбцы 0-2
            for column in range(0,3):
                # создаём рамку
                frame = gtk.Frame()
                frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
                # создаём область событий
                event_box = gtk.EventBox()
                # создаём метку
                label = gtk.Label()
                # делаем метку видимой
                label.show()
                # помещаем метку в область событий
                event_box.add(label)
                # определяем обработчик события для области
                event_box.connect("button_press_event", self.label_clicked, row, column)
                event_box.show()
                frame.add(event_box)
                # записываем всё это хозяйство в хранилище
                self.board_widgets[row][column] = frame                
                # присоединяем область событий с меткой к таблице
                self.table1.attach(self.board_widgets[row][column], 
                                   column, column + 1,
                                   row + 1, row + 2)
                # делаем область событий с меткой видимой
                self.board_widgets[row][column].show()

получается такое окошко


Форматированный вывод текста на метках


Метки могут отображать форматированный текст. Можно вывести текст определённым шрифтом, размером, стилем, цветом. Добавим в init_board пару строчек
                # после label = gtk.Label()
                label.set_use_markup(True)
                label.set_markup("<span font_desc='Arial 64'>X</span>")

Запускаем программу:
.

Диалоги



В Glade есть диалоги практически на каждый случай. Я вам покажу, как сделать свой модальный диалог, на основе любого окна.

Создаём новое окно, переходим к его свойствам.


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

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

дерево виджетов:


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


Сохраним интерфейс, и вернёмся в редактор кода.

Первое, что нужно сделать — задать обработчик для события окна диалога delete-event. Если этого не сделать, после закрытия диалога кнопкой X в заголовке окна (или любым другим способом через оконный менеджер) все виджеты этого окна «аннигилируются». Когда вы в следующий раз откроете это окно, оно будет микроскопическим, и совершенно пустым. Итак, в __init__ добавляем
        self.winner_dialog = self.widgetsTree.get_widget("window2")        
        self.winner_dialog.connect("delete-event", self.on_delete_event)

метод on_delete_event обязан возвращать True, чтобы предотвратить дальнейшую обработку события.

В общем-то, почти всё готово, сейчас интересен код метода, который выводит победителя игры:
    def show_winner(self):
        if self.game.and_the_winner_is() == -1:
            self.widgetsTree.get_widget("image3").set_from_file('1.png')
            self.widgetsTree.get_widget("label3").set_markup( 
                        "<span font_desc='Arial 18' >Ну что, ты понял, что я умнее тебя ?</span>")
            self.widgetsTree.get_widget("label4").set_text("Да, я червяк и признаю это.")
            self.widgetsTree.get_widget("label5").set_text(
                         "Ты всего лишь\nконсервная банка, Бендер,\nи я тебе докажу это сейчас !")        
        else:
            self.widgetsTree.get_widget("image3").set_from_file('2.png')
            self.widgetsTree.get_widget("label3").set_markup( 
                        "<span font_desc='Arial 18' >Человек обязан проиграть роботу !</span>")
            self.widgetsTree.get_widget("label4").set_text("Ага, просто повезло.")
            self.widgetsTree.get_widget("label5").set_text("Тебе мало ? Ну, держись !")
        self.winner_dialog.show()

Метод set_from_file используется для указания файла картинки, set_markup для метки указывает разметку, self.winner_dialog.show() открывает диалог.

Осталось запустить игру под Windows:

и Linux

Всё работает под обеими ОС, как и задумано!

Алгоритм в случае, если игрок слабый (например, ребёнок), ставит ход случайно, так что у вас будет шанс выиграть, а у Бендера — проиграть.
Теперь самое время скачать игру, и поиграть с Бендером.
Tags:
Hubs:
+36
Comments 13
Comments Comments 13

Articles