Qt Software

индекс
199,75

Программирование на PyQt4. Часть 3


   Добро пожаловать на третье занятие нашего курса по изучению PyQt4! Надеюсь, что на прошлых лекциях вы меня внимательно слушали, потому что сегодня мы будем изучать очень важный и интересный вопрос — как создавать сложные интерфейсы с множеством элементов управления. Иными словами, сегодня мы поговорим о менеджерах компоновки виджетов.

   Каждое мало-мальски уважающее себя приложение содержит чуть больше, чем одну кнопку в интерфейсе, поэтому очень важно научиться правильно располагать в окне все элементы управления. Зачастую их может быть не меньше десятка, а то и больше… Представьте, что было бы, если бы все элементы интерфейса были свалены в одну кучу — никакого изящества, красоты и удобства, неговоря уже о вообще возможности работать в такой программе.
   Существует несколько способов группировки виджетов и один из самых старых — создание формы с жестко зафиксированными позициями каждого виджета. Такой подход очень часто практиковался еще сравнительно недавно. Одним из отличительных признаков использования такого подходя является игнорирование действий пользователя на сжатие и растяжение окна, когда виджеты уходят за его пределы, в случае уменьшения размера, или наоборот, не занимают все предоставленное место, когда пользователь увеличил размер окна. Смотрится, надо сказать, ужасно. Да и просчитывать размеры каждого элемента и точные координаты в окна — было кошмаром для любого программиста. Для окон, которые могли изменять свой размер и при этом подстраивать виджеты под новые пропорции, приходилось писать сотни строк кода, только чтобы правильно просчитывались позиции каждого из элементов управления. Но потом был изобретен другой способ — менеджеры компоновки, которые брали эту рутинную задачу на себя.
   Для того, чтобы понять, что же такое менеджер компоновки, представим себе коробку с множеством ячеек. Коробку можно поставить вертикально (как подставку для дисков), а можно положить горизонтально (как книжную полку). В каждую ячейку кладется по одному виджету, и коробка сама подбирает для них лучший размер, исходя из доступного ей пространства и индивидуальных характеристик виджетов. Замечательный способ, не так ли? Разработчики Qt посчитали так же и снабдили свою библиотеку классами QHBoxLayout (компоновка виджетов по горизонтали) и QVBoxLayout (компоновка по вертикали). Итак, давайте подробнее рассмотрим, как же работает менеджер компоновки на примере QHBoxLayout.
   Предположим, что в вашей коробке еще нет виджетов. Вы добавили в него одну кнопку, и таким образом получили один единственный элемент, который будет занимать все доступное коробке пространство:

   +---------+
   |         |
   |   B1    |
   |         |
   +---------+

   Но, как я уже говорил, в эту же коробку можно добавить еще элементов, что мы и делаем дальше, добавив кнопку B2 (виджеты размещаются слева на право для горизонтальных менеджеров и сверху вниз для вертикальных):

   +----+----+
   |    |    |
   | B1 | B2 |
   |    |    |
   +----+----+

   И что же мы получили? Коробка сохранила свой прежний размер, но вместила в себя еще одну кнопку, несколько потеснив первую. Кнопки будут иметь одинаковый размер, равный половине от того, что мы имели в случае с одной кнопкой. Давайте посмотрим, как это выглядит в программном коде на Python. И на этот раз мы будем использовать специальный виджет, в качестве окна — QWidget. Он представляет собой некий абстрактный класс, от которого наследуются все остальные виджеты — QPushButton, QLabel и многие другие. Как уже говорилось, любой виджет может быть окном, чем, собственно, мы и воспользуемся:

00	from PyQt4 import Qt
01	import sys
02	if __name__ == "__main__" :
03		app = Qt.QApplication(sys.argv)
04
05		window = Qt.QWidget()
06
07		layout = Qt.QHBoxLayout()
08		layout.setContentsMargins(3, 3, 3, 3)
09		layout.setSpacing(3)
10		window.setLayout(layout)
11
12		button1 = Qt.QPushButton("B1")
13		layout.addWidget(button1)
14
15		button2 = Qt.QPushButton("B2")
16		layout.addWidget(button2)
17
18		window.show()
19
20		app.exec_()

   Это приложение очень похоже на те, что мы делали ранее, но теперь мы добавили некоторые новые элементы. в строке 5 мы создаем окно приложения, в роли которого выступает QWidget. Далее, в строках 7-9 мы создаем и настраиваем нашу горизонтальную коробку (QHBoxLayout), устанавливаем количество свободного пространство с каждой из сторон коробки (setContentsMargins()) и расстояние между виджетами внутри коробки (setSpacing()). В строке 10 мы устанавливаем для нашего окна менеджер компоновки layout. Затем каждую созданную кнопку мы разместим внутри layout`а (строки 13 и 16). в строке 18 мы показываем пользователю наше окно. Опять же, как я говорил на прошлых занятиях, не нужно выполнять метод show() для каждого виджета, Qt сделает это за вас для тех элементов, которые явным образом находятся внтури какого либо окна (например, с помощью менеджера компоновки). В нашем случае кнопки находятся в layout, который, в свою очередь, в window, для него мы и выполняем метод show().
   Думаю, суть вы уловили — помещая в менеджер компоновки виджеты, мы можем удобно расположить их внутри окна. Кроме того, внутрь одного менеджера компоновки может быть включен другой менеджер компоновки, и таким образом у нас будет формироваться сложная структура компоновки для виджетов.
   Например, создадим три кнопки, две из которых будут находится друг над другом, а третья — правее их обоих. Для этого нам придется использовать два менеджера компоновки — один горизонтальный, и один вертикальный:

   +----+----+
   | B1 |    |
   +----+ B3 |
   | B2 |    |
   +----+----+

   И вот как это будет выглядеть:

00	from PyQt4 import Qt
01	import sys
02
03	if __name__ == "__main__" :
04		app = Qt.QApplication(sys.argv)
05
06		window = Qt.QWidget()
07
08		layout1 = Qt.QHBoxLayout()
09		layout1.setContentsMargins(3, 3, 3, 3)
10		layout1.setSpacing(3)
11		window.setLayout(layout1)
12
13		layout2 = Qt.QVBoxLayout()
14		layout2.setContentsMargins(3, 3, 3, 3)
15		layout2.setSpacing(3)
16		layout1.addLayout(layout2)
17
18		button1 = Qt.QPushButton("B1")
19		layout2.addWidget(button1)
20
21		button2 = Qt.QPushButton("B2")
22		layout2.addWidget(button2)
23
24		button3 = Qt.QPushButton("B3")
25		layout1.addWidget(button3)
26
27		window.show()
28
29		app.exec_()

   Думаю, здесь в пояснении нуждается только одна строка — 16, в которой внутрь layout1 добавляется с помощью addLayout() layout2, как обычный виджет. Такой подход позволяет группировать элементы как угодно и в каком угодно порядке.
   Существует еще третий класс для компоновки элементов, который называется QGridLayout. В общем случае его можно представить себе как некоторую коробку, поделенную на равные ячейки. В каждую из ячеей можно положить по одному виджету, указава координаты этой ячейки. Этот менеджер очень часто используется в диалогах настройки, чтобы все параметры приложения выглядели, как таблица:

   +---------+---------+---------+
   |         |         |         |
   |         |         |         |
   +---------+---------+---------+
   |         |         |         |
   |         |         |         |
   +---------+---------+---------+
   |         |         |         |
   |         |         |         |
   +---------+---------+---------+

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

00	from PyQt4 import Qt
01	import sys
02
03	class Buttons(Qt.QMainWindow) :
04		def __init__(self, parent = None) :
05			Qt.QMainWindow.__init__(self, parent)
06
07			self.main_widget = Qt.QWidget()
08			self.setCentralWidget(self.main_widget)
09
10			self.main_layout = Qt.QVBoxLayout()
11			self.main_widget.setLayout(self.main_layout)
12
13			self.buttons_layout = Qt.QGridLayout()
14			self.main_layout.addLayout(self.buttons_layout)
15
16			#####
17
18			for row in range(4) :
19				for column in range(3) :
20					num = row*3 + column + 1
21					if num == 10 : num = 0
22					button = Qt.QPushButton(str(num))
23					self.buttons_layout.addWidget(button, row, column)
24					if num == 0 : break
25
26
27	if __name__ == "__main__" :
28		app = Qt.QApplication(sys.argv)
29		buttons = Buttons()
30		buttons.show()
31		app.exec_()

   Здесь мы имеем уже большее количество нововведений, чем раньше. Во-первых, мы создали отдальный класс-виджет для окна. Во-вторых, внутри нашего окна есть несколько лейаутов (далее, для краткости, я буду использовать этот термин, вместо «менеджер компоновки»), а так же достаточно интересный прием, который позволил значительно сократить количество кода (да-да, я про вложенные циклы говорю). Итак, давайте разберемся, что к чему.
   В строке 3 мы создаем новый класс Buttons, наследуя его от класса Qt4 QMainWindow. QMainWindow практически всегда используется для реализации главного окна приложения, поскольку имеет множество удобным методов, таких, например, как работу с тулбарами и панелью статуса (как та штучка в вашем браузере, на которой отображается адрес ссылки, если вы наведете на нее курсор :-) ), группировкой виджетов и различных панельей инструментов. В принципе, в качестве окна можно было взять любой из виджетов, но правильнее пользоваться именно этим, поскольку вам, в случае расширения функциональности вашего приложения, не нужно будет перестраивать пол-программы. Короче, учитесь писать сразу правильно :-).
   Функция, начинающаяся со строки 4 и продолжающаяся по строку 24 включительно представляет собой конструктор класса, который вызывается каждый раз при создании экземпляра нашего класса. Для Qt, в начале каждого конструктора необходимо явным образом вызывать метод __init__() наследуемого класса, передавая ему в качестве параметров ссылку на текущий (наш) класс и параметры, переданные конструктору нашего класса (Смотри строку 5).
   В строке 7 мы создаем главный виджет для окна — такова специфика использования QMainWindow и устанавливаем его с помощью метода setCentralWindget() (который, после наследования QMainWindow, уже относится к нашему классу). Далее создается лейаут для главного виджета и устанавливается на него. Заметье, что при создании переменной мы приписываем к ней «префикс» self, обозначающий, что это будет глобальная для всего класса переменная и она сохраниться, что очень важно, даже при выходе из метода (в данном случае конструктора __init__). Если вы будете создавать объекты без этой приписки, то вы не сможете получить к ним доступ из других виджетов и вообще, могут происходит очень странные вещи, вроде исчезания элементов управления из окна.
   И наконец, строки 18-24, самая интересная часть данного примера. Я решил сделать некое подобие телефоной клавиатуры, пронумерованной от 1 до 9 и 0 в конце. Вместо того, чтобы вручную создавать 10 кнопок и указывать их координаты для QGridLayout, я применил небольшую хитрость — сделал два вложенных цикла, которые обходят лейаут, как двумерный массив, и создают в каждом из ячеек кнопку с уникальным номером. В принципе работы этой штуки я предлагаю разобраться вам самим, если что-то не будет понятно, я с удовольствием отвечу на все вопросы в комментарии. Единственное, что здесь стоит пояснение — это то, что я НЕ писал self перед именем переменной button. Как я уже говорил, отсутсвие его черевато глюками, но только не в этот раз. Python уничтожает переменные, если они не используются. При каждом проходе цикла создается переменная button и уничтожается, но кнопка на форме сохраняется, как же так?.. Дело в том, что я сохраняю в явном виде каждый объект, указывая, что кнопки расположены в ячейках лейаута buttons_layout. При этом, не затирается предыдущий объект, и не уничтожаются старые, поскольку они используются классами Qt. Это совершенно банальная теория Python, которую желательно освежить в памяти, сходив, скажем, на python.org :-)

Прочитать часть 2
Прочитать часть 1
+42
23 ноября 2008, 14:54
35

комментарии (16)

+1
tass #
эм, а куда подевался предыдущий одноименный топик с комментами?
+2
Assuri #
НЛО прилетело и удалило :)
0
gigimon #
Автору большое спасибо! хороший материал, пишите еще, будем ждать :)
Повысил бы карму, но не могу :(
0
descentspb #
А как потом обращаться к этим кнопкам? Ведь объект button утерян.
0
Liksys #
Ну, он не утерян, его используют другие объекты. А если хочется обращаться к ним, никто ведь не мешает делать при создании так:
buttons = []
... # тут типа все остальное, включая циклы
button = Qt.QPushButton(str(num))
buttons.append(button)

Мы можем ссылки сохранить в списке. Вообще, я дальше покажу, как делаются такие питоноспецифичные хаки, когда создается N кнопок и N исгналов подключаются к N методом, и к этому примеру с клавиатурой буду возвращаться.
0
descentspb #
Это я и имел ввиду. Т.е. таким способом, как было написано изначалально, все-таки не обратиться к этому объекту.
0
Liksys #
Ну вообще-то это пример на компоновку, просто расставление виджетов.
0
Liksys #
pastie.org/321980
Вот например, что можно сделать :-)
0
sash_ko #
Если так важно получить объект, ему можно присвоить имя (у QObject есть свойство objectName) и затем использовать функции типа findChild или findChildren
НЛО прилетело и опубликовало эту надпись здесь
+2
Liksys #
А можно подробнее, почему нелогично? На самом деле интересно.
0
rapkasta #
Ура… долго ждал, пошёл читать!
+1
dexon #
"… при создании переменной мы приписываем к ней «префикс» self, обозначающий, что это будет глобальная для всего класса переменная и она сохраниться..."

Как-то вы не понятно, на мой взгляд, определили self, как некий магический префикс :).
А ведь посути self — представляет собой экземпляр текущего класса (по принципу this в C++). А присвоение типа:
self.main_widget = Qt.QWidget()
определяет (и инициализирует) переменную-член класса.
0
Liksys #
Русская языка моя неродная есть :-) Не знал, как сказать проще :-)
0
sash_ko #
«Существует еще третий класс для компоновки элементов» — еще и четверный (QBoxLayout), и пятый (QFormLayout), и шестой (QStackedLayout) :)

Вообще статья стала бы более читабельной, если бы короткие пояснения о том, что делается в каждой строке кода были не отдельным текстом, а как комментарии в самом коде. Не очень удобно после каждого предложения перемещаться обратно к кода.
0
sash_ko #
«При каждом проходе цикла создается переменная button и уничтожается...» — важна не переменная, а объект, на который она ссылается. button — это только ссылка на объект класса QPushButton. В строке 23 видно, что buttons_layout тоже хранит ссылку на этот объект. Поэтому совершенно не важно, будет ли уничтожена переменная или нет (вторая ссылка останется и сборщик мусора может спать спокойно). Кстати, не факт, что переменная уничтожается при каждом проходе цикла, это не C++ :)

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