Форум программистов, компьютерный форум, киберфорум
iamvic
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  
Путевые заметки в процессе познания Python и PyQt/PySide.
Помни - только тег CODE не портит код добавлением пробела в начало пустой строки.

К вопросу об особенностях декорирования слотов, обрабатывающих сигнал destroyed.

Запись от iamvic размещена 26.01.2022 в 12:55
Показов 1742 Комментарии 0
Метки pyqt5, python, python 3, qt5

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

Началось всё с обнаружения того факта, что количество "детей" у главного окна в процессе работы
не растёт. Можно сколько угодно раз открывать и закрывать окно диалога, родителем которого является
главное окно, но общее количество "детей" у главного окна после закрытия диалога не меняется,
несмотря на отсутствующий вызов 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)
на
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 с учетом, выявленных
для него ограничений на использование декоратора.
Метки pyqt5, python, python 3, qt5
Размещено в Памятка
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Запустил конкурс "тем и промптов для текстовых квестов созданных почти чисто ИИ"
Adler 06.10.2026
Всем привет! За последние три-четыре дня я создал более 16 текстовых квестовых игр используя преимущественно по одному запросу к ИИ на игру. Мне так понравилось смотреть все ветки/ сцены во всех. . .
ИИ не может найти нужный язык в списке
Supersumestria 05.10.2026
Я ему даю вот такое изображение и прошу найти и подчеркнуть немецкий язык. Возвращает он вот это: https:/ / i. **********/ vqBWLe2. png Нужную строчку в 3й колонке просто выдумал. . Это. . .
Новая последняя моя музыка в SUNO
zorxor 05.10.2026
Здравствуйте, дорогие мои друзья! С большой радостью я хотел бы представить вам свою новую последнею музыку, которую сгенерировала мне по моей просьбе нейросеть SUNO. С уважением, zorxor. Это. . .
Программный домашний кинотеатр
russiannick 27.09.2026
Сподобился на программный домашний кинотеатр. В качестве ЯВУ по традиции выбрал js. В помощники взял Яндекс-Алису. Было создано три зала на разные интересы. исторические и ретро сериал Хичкок. . .
Беседа с ИИ о программистах, недопускающих к созданию и правке кода генеративные ИИ и причины этого
zorxor 21.09.2026
Раньше я радовался или получал некоторые эмоции, пусть небольшие, но всё же, от самого процесса написания кода, рекомпиляции и запуска, видя постепенное развитие программы и прочее. А теперь лень. . .
Мобильное приложение ColorStep
pavlinmavlin 17.09.2026
Реализовал приложение Красный, Зеленый, Синий в Unity3d + c#. Название изменил на ColorStep. Приложение прошло модерацию и теперь доступно для скачивания. Делал его сам, шаг за шагом — и вот,. . .
Запрет дублирования строк в табличной части
Maks 13.09.2026
Реализация из решения ниже выполнена на нетиповом справочнике "Нормы ТО" с табличной часть "Виды ТО", разработанного в КА2, со следующими реквизитами: - ВидТО (СправочникСсылка. ВидыТО); - ВидГСМ. . .
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru