Вылезла всё-таки одна нестыковка в работе последнего варианта приложения из предыдущей записи блога,
которая к теме той записи не относится, но вполне достойна отдельного упоминания в Памятке.
Началось всё с обнаружения того факта, что количество "детей" у главного окна в процессе работы
не растёт. Можно сколько угодно раз открывать и закрывать окно диалога, родителем которого является
главное окно, но общее количество "детей" у главного окна после закрытия диалога не меняется,
несмотря на отсутствующий вызов deleteLater() в функции запуска диалога.
Проверить то, что диалоги действительно удаляются, труда не составляет - цепляем сигнал destroyed
диалога к специально обученному слоту главного окна и начинаем открывать-закрывать диалоги:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView, QTableView
from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout
from PyQt5.QtWidgets import QMessageBox, QWidget
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QMetaObject, QObject
from PyQt5.QtCore import QModelIndex, QTimer
def slot_trace_note(client):
return '{!s} --> {!s} --> {!s}'.format(
client.sender().objectName(),
client.sender().metaObject().method(
client.senderSignalIndex()).name().data().decode('utf-8'),
traceback.extract_stack()[-2][2])
def check_children_amount(client):
'''подcчёт количества детей у client
'''
print('==> {!s} children amount = {!s}'.format(
client.objectName(), len(client.children())))
def check_method(client, method_name):
'''проверка наличия метода в классе средствами Python
'''
return(method_name in [f for f in dir(client)
if callable(getattr(client, f)) and not f.startswith('__')])
def check_signal_connected(client, signal_name):
'''проверка наличия установленного соединения для сигнала
'''
i = client.metaObject().indexOfMethod(signal_name + '()')
if i >= 0:
m = client.metaObject().method(i)
if m:
return client.isSignalConnected(m)
return False
def make_dialog(client, holder):
'''диалог показа данных, полученных из выбранной строки QTableView
client - родитель для диалога
holder - источник данных
'''
try:
print('--> inherits() index =',
holder.metaObject().indexOfMethod('inherits()'))
except:
print('--> holder is not Qt member')
# inherits() отсутствует в метаданных QObject и, соответственно,
# всех его наследников, хотя в самом классе он есть
# поэтому наличие inherits() проверяем средствами Python
if not (check_method(holder, 'inherits') and
holder.inherits('QTableView')):
QMessageBox.warning(
client, 'О Ш И Б К А !!!',
'Из источника данных класса\n{!s}\n'
'получить данные для диалога невозможно!\n\n'
'Допустимы только наследники QTableView.'.format(type(holder)))
return
m_dialog = QDialog(client)
m_dialog.setObjectName('solutiondialog')
m_dialog.destroyed.connect(client.on_anyobjects_destroyed)
vbox = QVBoxLayout()
for i in range(0, len(holder.selectionModel().selectedIndexes())):
x = QLabel(holder.selectionModel().selectedIndexes()[i].data(),
m_dialog)
vbox.addWidget(x)
vbox.addStretch(1)
m_dialog.setLayout(vbox)
m_dialog.resize(300, 200)
QTimer.singleShot(200, lambda: check_children_amount(client))
m_dialog.exec_()
#m_dialog.deleteLater()
class Table_view(QTableView):
enterkeypressed = pyqtSignal()
mousedoubleclicked = pyqtSignal()
def keyPressEvent(self, e):
if ((e.key() == Qt.Key_Enter or e.key() == Qt.Key_Return) and
check_signal_connected(self, 'enterkeypressed')):
QTimer.singleShot(50, lambda: self.enterkeypressed.emit())
super(Table_view, self).keyPressEvent(e)
def mouseDoubleClickEvent(self, e):
if check_signal_connected(self, 'mousedoubleclicked'):
QTimer.singleShot(50, lambda: self.mousedoubleclicked.emit())
super(Table_view, self).mouseDoubleClickEvent(e)
class Main_window(QMainWindow):
def __init__(self, parent=None):
super(Main_window, self).__init__(parent)
self.setObjectName('mainwindow')
self.setGeometry(300, 200, 500, 400)
w_view = Table_view(self)
w_view.setObjectName('tableview')
w_model = QStandardItemModel(self)
w_model.setObjectName('itemmodel')
w_model.setHorizontalHeaderLabels(['Деталь', 'Длина', 'Поставщик'])
w_view.setModel(w_model)
w_view.setSortingEnabled(True)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
w_view.horizontalHeader().setStretchLastSection(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionMode(QAbstractItemView.SingleSelection)
QMetaObject.connectSlotsByName(self)
rows = [['Стойка', 200, 'Фабрика'],
['Штанга', 10, 'Завод'],
['Перекладина', 5, 'Мастерская']
]
for row in rows:
w_model.appendRow([QStandardItem(row[0]),
QStandardItem(str(row[1])),
QStandardItem(row[2])])
w_view.selectRow(0)
self.setCentralWidget(w_view)
QTimer.singleShot(200, lambda: check_children_amount(self))
@pyqtSlot('QModelIndex', name='on_tableview_pressed')
@pyqtSlot('QModelIndex', name='on_tableview_clicked')
@pyqtSlot('QModelIndex', name='on_tableview_entered')
@pyqtSlot(QModelIndex, name='on_tableview_doubleClicked')
@pyqtSlot(QModelIndex, name='on_tableview_activated')
@pyqtSlot(QModelIndex)
def on_tableview_orignal_signals(self, idx):
print('{!s}({!s},{!s})'.format(
slot_trace_note(self), idx.row(), idx.column()))
@pyqtSlot(name='on_tableview_enterkeypressed')
@pyqtSlot(name='on_tableview_mousedoubleclicked')
@pyqtSlot()
def on_tableview_extra_signals(self):
print('{!s}()'.format(slot_trace_note(self)))
sender = self.sender()
QTimer.singleShot(100, lambda: make_dialog(self, sender))
#QTimer.singleShot(100, lambda: make_dialog(self, None))
@pyqtSlot(QObject)
def on_anyobjects_destroyed(self, client):
print('{!s}()'.format(slot_trace_note(self)))
QTimer.singleShot(200, lambda: check_children_amount(self))
if __name__ == '__main__':
app = QApplication(sys.argv)
mwin = Main_window()
mwin.show()
app.exec_()
sys.exit() |
|
Тут и обнаруживается пара мелочей, которые доставляют определённые неудобства.
Слот, обрабатывающий сигнал destroyed:
1. не умеет извлекать имя сигнала из метаданных, если в декораторе указана сигнатура
(что вообще-то характерно для ситуации отсутствия декоратора),
2. не даёт воспользоваться тем "фокусом", который позволяет вести обработку множества однородных
сигналов в одном слоте. Смотри, например, обработку сигналов pressed, clicked, entered, doubleClicked
и activated в приведённом исходнике. Там QMetaObject.connectSlotsByName() успешно цепляет сигналы
к декорированному слоту, работая и с сигнатурами, и с типами (в примере типы с сигнатурами
специально перемешаны).
Впрочем, строго говоря, разработчики PyQt и не предлагают такое использование pyqtSlot
(см. Support for Signals and Slots). Хотя, судя по всему, оно тоже легитимно, но для destroyed не подходит.
Ну, и как результат, это приводит к тому, что для наблюдаемых объектов приходится прописывать
соединения сигнала destroyed вручную на один и тот же слот или, полагаясь на QMetaObject.connectSlotsByName(),
создавать отдельные слоты для каждого именованного объекта.
Протокол работы приложения (с комментариями):
| Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| user@linux:~> python3 destroyed_probe.py
==> mainwindow children amount = 5 <== сколько детей у главного окна в начале
tableview --> activated --> on_tableview_orignal_signals(0,0) <== нажата клавиша ENTER
tableview --> enterkeypressed --> on_tableview_extra_signals()
--> inherits() index = -1 <== стартовал диалог
==> mainwindow children amount = 6 <== сколько теперь детей у главного окна
solutiondialog --> destroyed --> on_anyobjects_destroyed() <== диалог завершён
==> mainwindow children amount = 5 <== сколько теперь детей у главного окна
tableview --> pressed --> on_tableview_orignal_signals(2,2) <== двойной щелчок ЛКМ
tableview --> clicked --> on_tableview_orignal_signals(2,2)
tableview --> activated --> on_tableview_orignal_signals(2,2)
tableview --> doubleClicked --> on_tableview_orignal_signals(2,2)
tableview --> mousedoubleclicked --> on_tableview_extra_signals()
tableview --> clicked --> on_tableview_orignal_signals(2,2)
tableview --> activated --> on_tableview_orignal_signals(2,2)
--> inherits() index = -1 <== стартовал диалог
==> mainwindow children amount = 6 <== сколько теперь детей у главного окна
solutiondialog --> destroyed --> on_anyobjects_destroyed() <== диалог завершён
==> mainwindow children amount = 5 <== сколько теперь детей у главного окна
user@linux:~> |
|
А что касается deleteLater(), то можно сказать, что он оказался действительно не обязателен
в данном конкретном случае (если только не используется какая-нибудь древняя версия, например,
под Qt/PyQt v5.1 deleteLater() нужен обязательно, а под v5.5 уже и без него работает).
Понятно, что это относится только к вызову диалога из внешней функции, как это сделано здесь.
Хотя, лучше всего, конечно, про deleteLater() не забывать и указывать его всегда явно, если требуется.
Дополнение 27.01.2022:
Как это обычно и бывает в процессе детальных разборок - в конце концов всё оказывается не совсем так,
как кажется . Но как бы там ни было, в путевые заметки, как в бортовой журнал, пишем всё...
На самом-то деле, с этой многоэтажной конструкцией:
| Python | 1
2
3
4
5
6
7
8
9
| @pyqtSlot('QModelIndex', name='on_tableview_pressed')
@pyqtSlot('QModelIndex', name='on_tableview_clicked')
@pyqtSlot('QModelIndex', name='on_tableview_entered')
@pyqtSlot(QModelIndex, name='on_tableview_doubleClicked')
@pyqtSlot(QModelIndex, name='on_tableview_activated')
@pyqtSlot(QModelIndex)
def on_tableview_orignal_signals(self, idx):
print('{!s}({!s},{!s})'.format(
slot_trace_note(self), idx.row(), idx.column())) |
|
прекрасно справляется как раз умный QMetaObject.connectSlotsByName().
А вот соединения, создаваемые с помощью connect(), в функциональности ограничены.
Проиллюстрирую это на примере обработки сигнала pressed.
В вышеприведённом приложении результаты обработки всех сигналов после одиночного
щелчка ЛКМ выглядят так (отличия Linux/Windows незначительны):
| Code | 1
2
| tableview --> pressed --> on_tableview_orignal_signals(1,2)
tableview --> clicked --> on_tableview_orignal_signals(1,2) |
|
Добавим после
| Python | 1
| QMetaObject.connectSlotsByName(self) |
|
ещё одну строку, создающую второе соединение для этого сигнала, указав в качестве обработчика
оригинальный слот:
| Python | 1
2
| QMetaObject.connectSlotsByName(self)
w_view.pressed.connect(self.on_tableview_orignal_signals) |
|
Для чистоты эксперимента можно даже удалить или заменить строку
на
| Python | 1
| @pyqtSlot(QModelIndex, name='on_tableview_orignal_signals') |
|
Но можно этого и не делать, оставив всё как есть - результат не изменится.
Теперь результаты обработки сигналов после одиночного щелчка ЛКМ стали выглядеть так:
| Code | 1
2
3
| tableview --> pressed --> on_tableview_orignal_signals(1,2)
tableview --> pressed --> on_tableview_orignal_signals(1,2)
tableview --> clicked --> on_tableview_orignal_signals(1,2) |
|
Ничего не поменялось, только сигнал pressed обрабатывается дважды, но ведь и соединений
у нас теперь два.
Добавим ещё одну строку между QMetaObject.connectSlotsByName(self) и вторым соединением,
но используем для него имя, указанное в декораторе, ориентируясь на которое, QMetaObject.connectSlotsByName(self)
как раз и создавал своё соединение:
| Python | 1
2
3
| QMetaObject.connectSlotsByName(self)
w_view.pressed.connect(self.on_tableview_pressed) # <===
w_view.pressed.connect(self.on_tableview_orignal_signals) |
|
А вот тут уже результат удручающий (пустые строки вставлены для наглядности):
| Code | 1
2
3
4
5
6
| tableview --> pressed --> on_tableview_orignal_signals(1,2)
tableview --> --> on_tableview_orignal_signals(0,2)
tableview --> pressed --> on_tableview_orignal_signals(1,2)
tableview --> clicked --> on_tableview_orignal_signals(1,2) |
|
Очевидно, что-то не то происходит с обработкой сигнала, раз не удаётся извлечь имя сигнала
из метаданных. Хотя древние версии PyQt5 всё-таки дают объяснение происходящего:
| Code | 1
2
3
| Trying to create a QVariant instance of QMetaType::Void type, an invalid QVariant will be constructed instead
tableview --> --> on_tableview_orignal_signals(0,2)
TypeError: unable to convert a QVariant of type 0 to a QMetaType of type 43 |
|
Но ведь QMetaObject.connectSlotsByName() с этим как-то справляется. Значит решение есть.
Но не про нашу честь. Есть у функционала, ориентированного на Qt Designer, неоспоримые
преимущества в этом случае.
По итогам-то получается, что даже такой не задекларированный в документации способ декорирования
слотов всё же имеет право на жизнь. Правда, применять его следует с осторожностью, исключительно
с целью организации обработки нескольких однородных сигналов в одном и том же слоте средствами
QMetaObject.connectSlotsByName().
Для сигналов, подключаемых к такому слоту с помощью connect(), существует ограничение.
Если для обработчика сигнала важно знать имя пришедшего на обработку сигнала, то создавать
такое соединение следует только с оригинальным слотом, использующим безымянный декоратор.
Иначе извлечь имя сигнала из метаданных не удастся.
И всё это в полной мере относится также и к обработчику сигнала destroyed с учетом, выявленных
для него ограничений на использование декоратора.
|