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

К вопросу о влиянии декораторов на возвращаемое значение QObject.sender().

Запись от iamvic размещена 03.07.2022 в 20:23
Показов 2015 Комментарии 0
Метки pyqt5, python, python 3, qt5

Ещё один интересный нюанс выявился для Памятки. Началось-то всё с заметки К вопросу о закрытии окна нажатием на значок в полосе заголовка в попытке найти ответ на вопрос "Что делать?", если в процессе закрытия приложения требуется выполнять некоторые обязательные действия, даже когда пользователь закрывает его нажатием на значок в полосе заголовка. Решение оказалось достаточно простым. Но ведь то же самое может потребоваться не только при закрытии приложения, но и при закрытии каких-то вторичных окон. И для проверки того, что творится там, был написан такой пробник:

sender_probe.py
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''Sender Probe v0.09x
from iamvic on cyberforum.ru
 
Версия без QMetaObject.connectSlotsByName() и декорирования слотов:
- отправитель сигнала, инициировавшего создание диалога, не маскируется.
  Соответствено, именно от его имени прилетают события QCloseEvent
  от внешних источников в полосе заголовка окна.
'''
 
import sys, traceback
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QKeySequence
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QAction, QCheckBox, QPushButton
from PyQt5.QtWidgets import QLabel, QWidget, QShortcut
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QMainWindow, QDialog
 
APP_NAME = 'Sender Probe v0.09x'
 
 
def check_method(client, method_name):
    '''проверяет наличие метода в классе
    '''
    return(method_name in [f for f in dir(client)
           if callable(getattr(client, f)) and not f.startswith('__')])
 
 
def collect_parents(client, s = ''):
    '''собирает и возвращает неименованную строку с цепочкой родителей
    '''
    if type(client).__name__ != 'NoneType':
        s = '{!s}{!s}{!s}'.format(
            type(client).__name__, '.' if (s) else '', s)
        if type(client.parent()).__name__ != 'NoneType':
            s = collect_parents(client.parent(), s)
    return s
 
 
def get_parentage(client):
    '''возвращает именованную строку с цепочкой родителей
    '''
    if type(client).__name__ != 'NoneType':
        s = collect_parents(client, s = '')
        if check_method(client, 'text'):
            s += '({!s})'.format(client.text())
    else:
        s = type(client).__name__
    return s
 
 
def print_outsource_data(client, point):
    '''выводит данные о владельце и родителе текущего объекта,
    а также об отправителе принятого сигнала
    '''
    Var_store.serial_num += 1
 
    print('({!s}) ===> {!s}.{!s}()'.format(
        Var_store.serial_num, get_parentage(client), point))
    print('client =', get_parentage(client))
    print('client.parent() =', get_parentage(client.parent()))
    print('client.sender() =', get_parentage(client.sender()))
    print()
 
 
def print_result_data(client, dialog, point):
    '''выводит данные о результатах закрытия диалога
    '''
    Var_store.serial_num += 1
 
    print('({!s}) <--- {!s}.{!s}()'.format(
        Var_store.serial_num, get_parentage(client), point))
    print('{!s}.result() = {!s}'.format(
        get_parentage(dialog),
        dialog.result() if check_method(dialog, 'result') else '(undefined)'))
    print()
 
 
class Var_store(object):
    '''Сквозной серийный номер
    '''
    serial_num = 0
 
 
class Dialog_window(QDialog):
    '''Диалог
    '''
    def __init__(self, parent=None):
        # строим пользовательский интерфейс
        #
        super(Dialog_window, self).__init__(parent)
 
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        QShortcut(QKeySequence('Alt+F4'), self, self.close)
        QShortcut(QKeySequence('Ctrl+0'), self, self.close)
        # пара комбинаций завязана на один слот для проверки работы
 
        self.outsourcer = self.sender()
 
        info_text = QLabel('Про sender() и parent()', self)
        font = info_text.font()
        font.setWeight(QFont.Bold)
        info_text.setFont(font)
 
        self.check_box = QCheckBox('Отключить внешнее управление', self)
        close_button = QPushButton('Завершить диалог', self)
 
        v_map = QVBoxLayout(None)
        v_map.addStretch(1)
        v_map.addWidget(info_text)
        v_map.addWidget(self.check_box)
        v_map.addWidget(close_button)
        v_map.addStretch(1)
 
        main_map = QHBoxLayout(None)
        main_map.addStretch(1)
        main_map.addLayout(v_map)
        main_map.addStretch(1)
 
        self.setLayout(main_map)
 
        self.check_box.stateChanged.connect(self.on_check_box_stateChanged)
        close_button.pressed.connect(self.accept)
 
    def closeEvent(self, e):
        # повторная реализация для перехвата обработки
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        if (self.sender() == self.outsourcer and
            not self.check_box.checkState() == Qt.Unchecked):
            # если внешнее управление отключено,
            # то игнорируем QCloseEvent от внешних источников:
            # - значок закрытия в заголовке окна,
            # - пункт <Закрыть> в выпадающем меню в заголовке
            # - комбинация клавиш Alt-F4
            e.ignore()
        else:
            e.accept()
 
    def on_check_box_stateChanged(self, state):
        # должен прилетать сигнал от check_box
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
    def accept(self):
        # повторная реализация для протокола
        #
        # должен прилетать сигнал от close_button
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).accept()
 
    def reject(self):
        # повторная реализация для протокола
        #
        # должен прилетать сигнал от Escape
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).reject()
 
    def close(self):
        # повторная реализация для протокола
        #
        # должны прилетать сигналы от Alt-F4 и Ctrl-0
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).close()
 
 
class Main_window(QMainWindow):
    '''Главное окно приложения
    '''
    def __init__(self, parent=None):
        # строим пользовательский интерфейс
        #
        super(Main_window, self).__init__(parent)
 
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        QShortcut(QKeySequence('Alt+F4'), self, self.close)
        QShortcut(QKeySequence('Ctrl+0'), self, self.close)
        # пара комбинаций завязана на один слот для проверки работы
 
        self.outsourcer = self.sender()
 
        dialog1_action = QAction(
            'Диалог с заголовком', self, shortcut='Ctrl+1',
            triggered=self.to_dialog1, enabled=True)
        dialog2_action = QAction(
            'Диалог без заголовка', self, shortcut='Ctrl+2',
            triggered=self.to_dialog2, enabled=True)
        exit_action = QAction(
            'Выход', self, shortcut='Ctrl+Q',
            triggered=self.to_close, enabled=True)
 
        task_menu = self.menuBar().addMenu('Действия')
        task_menu.addAction(dialog1_action)
        task_menu.addAction(dialog2_action)
        task_menu.addAction(exit_action)
 
        info_text = QLabel('Про sender() и parent()', self)
        font = info_text.font()
        font.setWeight(QFont.Bold)
        info_text.setFont(font)
 
        self.check_box = QCheckBox(
            'Отключить внешнее управление', self)
        dialog1_button = QPushButton(
            'Открыть диалог с заголовком', self)
        dialog2_button = QPushButton(
            'Открыть диалог без заголовка', self)
        close_button = QPushButton('Завершить работу', self)
 
        v_map = QVBoxLayout(None)
        v_map.addWidget(info_text)
        v_map.addWidget(self.check_box)
        v_map.addWidget(dialog1_button)
        v_map.addWidget(dialog2_button)
        v_map.addWidget(close_button)
        v_map.addStretch(1)
 
        main_map = QHBoxLayout(None)
        main_map.addStretch(1)
        main_map.addLayout(v_map)
        main_map.addStretch(1)
 
        central_widget = QWidget(self)
        central_widget.setLayout(main_map)
        self.setCentralWidget(central_widget)
 
        self.check_box.stateChanged.connect(self.on_check_box_stateChanged)
        dialog1_button.pressed.connect(self.to_dialog1)
        dialog2_button.pressed.connect(self.to_dialog2)
        close_button.pressed.connect(self.to_close)
 
        self.move(200, 200)
        self.resize(400, 300)
 
    def closeEvent(self, e):
        # повторная реализация для перехвата обработки
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        if (self.sender() == self.outsourcer and
            not self.check_box.checkState() == Qt.Unchecked):
            # если внешнее управление отключено,
            # то игнорируем QCloseEvent от внешних источников:
            # - значок закрытия в заголовке окна,
            # - пункт <Закрыть> в выпадающем меню в заголовке
            # - комбинация клавиш Alt-F4
            e.ignore()
        else:
            e.accept()
 
    def on_check_box_stateChanged(self, state):
        # должен прилетать сигнал от check_box
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
    def to_dialog1(self):
        # должны прилетать сигналы от dialog1_button, dialog1_action и Ctrl-1
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        dialog = Dialog_window(self)
        dialog.setStyleSheet('background-color: rgb(255, 236, 176);')
        dialog.exec_()
 
        print_result_data(self, dialog, traceback.extract_stack()[-1][2])
 
        dialog.deleteLater()
 
    def to_dialog2(self):
        # должны прилетать сигналы от dialog2_button, dialog2_action и Ctrl-2
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        dialog = Dialog_window(self)
        dialog.setStyleSheet('background-color: rgb(255, 236, 176);')
        dialog.setWindowFlags(
            Qt.Window | Qt.FramelessWindowHint | Qt.CustomizeWindowHint)
        dialog.exec_()
 
        print_result_data(self, dialog, traceback.extract_stack()[-1][2])
 
        dialog.deleteLater()
 
    def to_close(self):
        # должны прилетать сигналы от close_button, exit_action и Ctrl-Q
        #
        # по идее, именно здесь должны выполняться все операции,
        # необходимые для корректного закрытия приложения:
        # - сохранение данных
        # - освобождение ресурсов
        # - закрытие соединений и прочнее
        # и только после завершения этих операций может быть вызван close()
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.close()
 
    def close(self):
        # повторная реализация для протокола
        #
        # должны прилетать сигналы от Alt-F4 и Ctrl-0
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Main_window, self).close()
 
 
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setApplicationName(APP_NAME)
 
    mwin = Main_window()
    mwin.show()
    sys.exit(app.exec_())
Пример собранного протокола работы демонстрирует отличия:

1. запускаем приложение
Code
1
2
3
4
5
user@linux:~/qtprobe/sender_probe> python3 sender_probe.py
(1) ===> Main_window.__init__()
client = Main_window
client.parent() = NoneType
client.sender() = NoneType
Конструктор класса Main_window создаёт экземпляр главного окна. Это окно верхнего уровня, поэтому родителя у него нет. А отправителя сигнала нет, потому что инициатором создания этого окна был внешний источник (операционная система) и приложение о нём ничего не знает.

2. нажимаем кнопку "Открыть диалог с заголовком":
Code
1
2
3
4
(2) ===> Main_window.to_dialog1()
client = Main_window
client.parent() = NoneType
client.sender() = Main_window.QWidget.QPushButton(Открыть диалог с заголовком)
Сигнал, полученный слотом to_dialog1(), запускает его на выполнение. Источник этого сигнала определяется вызовом метода sender(). В даннои случае, источником будет соответствующий экземпляр QPushButton из центрального виджета главного окна. Если бы воспользовались меню, то это был бы соответствующий экземпляр QAction.
Code
1
2
3
4
(3) ===> Main_window.Dialog_window.__init__()
client = Main_window.Dialog_window
client.parent() = Main_window
client.sender() = Main_window.QWidget.QPushButton(Открыть диалог с заголовком)
Конструктор класса Dialog_window, инициированный в процессе выполнения to_dialog1(), создаёт экземпляр диалога. Родитель новорожденному назначается при вызове конструктора, а в качестве источника сигнала подхватывается текущий источник из to_dialog1(). Таким образом, данный экземпляр диалога знает не только назначенного ему родителя, но и того, по чьей команде он появился на свет

3. нажимаем значок закрытия в полосе заголовка окна:
Code
1
2
3
4
(4) ===> Main_window.Dialog_window.closeEvent()
client = Main_window.Dialog_window
client.parent() = Main_window
client.sender() = Main_window.QWidget.QPushButton(Открыть диалог с заголовком)
И становится понятно, что для вторичных окон в качестве источника для внешних событий closeEvent, генерируемым значком закрытия в полосе заголовка, комбинацией клавиш Alt-F4 и пунктом "Закрыть (Alt-F4)" в выпадающем меню заголовка, используется именно тот источник, который был подхвачен конструктором класса при создании экземпляра диалога.
Code
1
2
(5) <--- Main_window.to_dialog1()
Main_window.Dialog_window.result() = 0
Диалог закрыт, ждём реакции пользователя.

В принципе, интересная картинка получается. Можно было бы подумать о том, как применить это в созидательных целях, но всё меняется, как только в коде появляются декораторы.

Вот так выглядит трассировка тех же самых действий пользователя в пробнике, использующем декораторы:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(1) ===> Main_window.__init__()
client = Main_window
client.parent() = NoneType
client.sender() = NoneType
 
(2) ===> Main_window.to_dialog1()
client = Main_window
client.parent() = NoneType
client.sender() = Main_window.QWidget.QPushButton(Открыть диалог с заголовком)
 
(3) ===> Main_window.Dialog_window.__init__()
client = Main_window.Dialog_window
client.parent() = Main_window
client.sender() = NoneType
 
(4) ===> Main_window.Dialog_window.closeEvent()
client = Main_window.Dialog_window
client.parent() = Main_window
client.sender() = NoneType
 
(5) <--- Main_window.to_dialog1()
Main_window.Dialog_window.result() = 0
Хорошо видно, что конструктор диалога уже не подхватывает текущий источник из выполняющегося слота. Получается, что источника нет и ситуация становится полностью аналогичной происходящему в секции __main__.

Код пробника с декораторами:
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''Sender Probe v0.09z
from iamvic on cyberforum.ru
 
Версия с QMetaObject.connectSlotsByName() с декорированием слотов:
- отправитель сигнала, инициировавшего создание диалога, маскируется.
  Соответственно, события QCloseEvent от внешних источников
  в полосе заголовка окна всегда анонимны.
'''
 
import sys, traceback
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtCore import QMetaObject
from PyQt5.QtGui import QFont, QKeySequence
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QAction, QCheckBox, QPushButton
from PyQt5.QtWidgets import QLabel, QWidget, QShortcut
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QMainWindow, QDialog
 
APP_NAME = 'Sender Probe v0.09z'
 
 
def check_method(client, method_name):
    '''проверяет наличие метода в классе
    '''
    return(method_name in [f for f in dir(client)
           if callable(getattr(client, f)) and not f.startswith('__')])
 
 
def collect_parents(client, s = ''):
    '''собирает и возвращает неименованную строку с цепочкой родителей
    '''
    if type(client).__name__ != 'NoneType':
        s = '{!s}{!s}{!s}'.format(
            type(client).__name__, '.' if (s) else '', s)
        if type(client.parent()).__name__ != 'NoneType':
            s = collect_parents(client.parent(), s)
    return s
 
 
def get_parentage(client):
    '''возвращает именованную строку с цепочкой родителей
    '''
    if type(client).__name__ != 'NoneType':
        s = collect_parents(client, s = '')
        if check_method(client, 'text'):
            s += '({!s})'.format(client.text())
    else:
        s = type(client).__name__
    return s
 
 
def print_outsource_data(client, point):
    '''выводит данные о владельце и родителе текущего объекта,
    а также об отправителе принятого сигнала
    '''
    Var_store.serial_num += 1
 
    print('({!s}) ===> {!s}.{!s}()'.format(
        Var_store.serial_num, get_parentage(client), point))
    print('client =', get_parentage(client))
    print('client.parent() =', get_parentage(client.parent()))
    print('client.sender() =', get_parentage(client.sender()))
    print()
 
 
def print_result_data(client, dialog, point):
    '''выводит данные о результатах закрытия диалога
    '''
    Var_store.serial_num += 1
 
    print('({!s}) <--- {!s}.{!s}()'.format(
        Var_store.serial_num, get_parentage(client), point))
    print('{!s}.result() = {!s}'.format(
        get_parentage(dialog),
        dialog.result() if check_method(dialog, 'result') else '(undefined)'))
    print()
 
 
class Var_store(object):
    '''Сквозной серийный номер
    '''
    serial_num = 0
 
 
class Dialog_window(QDialog):
    '''Диалог
    '''
    def __init__(self, parent=None):
        # строим пользовательский интерфейс
        #
        super(Dialog_window, self).__init__(parent)
 
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        QShortcut(QKeySequence('Alt+F4'), self, self.close)
        QShortcut(QKeySequence('Ctrl+0'), self, self.close)
        # пара комбинаций завязана на один слот для проверки работы
 
        self.outsourcer = self.sender()
 
        info_text = QLabel('Про sender() и parent()', self)
        font = info_text.font()
        font.setWeight(QFont.Bold)
        info_text.setFont(font)
 
        self.check_box = QCheckBox('Отключить внешнее управление', self)
        close_button = QPushButton('Завершить диалог', self)
 
        self.check_box.setObjectName('check_box')
        close_button.setObjectName('close_button')
 
        v_map = QVBoxLayout(None)
        v_map.addStretch(1)
        v_map.addWidget(info_text)
        v_map.addWidget(self.check_box)
        v_map.addWidget(close_button)
        v_map.addStretch(1)
 
        main_map = QHBoxLayout(None)
        main_map.addStretch(1)
        main_map.addLayout(v_map)
        main_map.addStretch(1)
 
        self.setLayout(main_map)
 
        QMetaObject.connectSlotsByName(self)
 
    def closeEvent(self, e):
        # повторная реализация для перехвата обработки
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        if (self.sender() == self.outsourcer and
            not self.check_box.checkState() == Qt.Unchecked):
            # если внешнее управление отключено,
            # то игнорируем QCloseEvent от внешних источников:
            # - значок закрытия в заголовке окна,
            # - пункт <Закрыть> в выпадающем меню в заголовке
            # - комбинация клавиш Alt-F4
            e.ignore()
        else:
            e.accept()
 
    def on_check_box_stateChanged(self, state):
        # должен прилетать сигнал от check_box
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
    @pyqtSlot(name = 'on_close_button_pressed')
    def accept(self):
        # повторная реализация для протокола
        #
        # должен прилетать сигнал от close_button
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).accept()
 
    def reject(self):
        # повторная реализация для протокола
        #
        # должен прилетать сигнал от Escape
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).reject()
 
    def close(self):
        # повторная реализация для протокола
        #
        # должны прилетать сигналы от Alt-F4 и Ctrl-0
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).close()
 
 
class Main_window(QMainWindow):
    '''Главное окно приложения
    '''
    def __init__(self, parent=None):
        # строим пользовательский интерфейс
        #
        super(Main_window, self).__init__(parent)
 
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        QShortcut(QKeySequence('Alt+F4'), self, self.close)
        QShortcut(QKeySequence('Ctrl+0'), self, self.close)
        # пара комбинаций завязана на один слот для проверки работы
 
        self.outsourcer = self.sender()
 
        dialog1_action = QAction(
            'Диалог с заголовком', self, shortcut='Ctrl+1',
            triggered=self.to_dialog1, enabled=True)
        dialog2_action = QAction(
            'Диалог без заголовка', self, shortcut='Ctrl+2',
            triggered=self.to_dialog2, enabled=True)
        exit_action = QAction(
            'Выход', self, shortcut='Ctrl+Q',
            triggered=self.to_close, enabled=True)
 
        task_menu = self.menuBar().addMenu('Действия')
        task_menu.addAction(dialog1_action)
        task_menu.addAction(dialog2_action)
        task_menu.addAction(exit_action)
 
        info_text = QLabel('Про sender() и parent()', self)
        font = info_text.font()
        font.setWeight(QFont.Bold)
        info_text.setFont(font)
 
        self.check_box = QCheckBox(
            'Отключить внешнее управление', self)
        dialog1_button = QPushButton(
            'Открыть диалог с заголовком', self)
        dialog2_button = QPushButton(
            'Открыть диалог без заголовка', self)
        close_button = QPushButton('Завершить работу', self)
 
        self.check_box.setObjectName('check_box')
        dialog1_button.setObjectName('dialog1_button')
        dialog2_button.setObjectName('dialog2_button')
        close_button.setObjectName('close_button')
 
        v_map = QVBoxLayout(None)
        v_map.addWidget(info_text)
        v_map.addWidget(self.check_box)
        v_map.addWidget(dialog1_button)
        v_map.addWidget(dialog2_button)
        v_map.addWidget(close_button)
        v_map.addStretch(1)
 
        main_map = QHBoxLayout(None)
        main_map.addStretch(1)
        main_map.addLayout(v_map)
        main_map.addStretch(1)
 
        central_widget = QWidget(self)
        central_widget.setLayout(main_map)
        self.setCentralWidget(central_widget)
 
        QMetaObject.connectSlotsByName(self)
 
        self.move(200, 200)
        self.resize(400, 300)
 
    def closeEvent(self, e):
        # повторная реализация для перехвата обработки
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        if (self.sender() == self.outsourcer and
            not self.check_box.checkState() == Qt.Unchecked):
            # если внешнее управление отключено,
            # то игнорируем QCloseEvent от внешних источников:
            # - значок закрытия в заголовке окна,
            # - пункт <Закрыть> в выпадающем меню в заголовке
            # - комбинация клавиш Alt-F4
            e.ignore()
        else:
            e.accept()
 
    def on_check_box_stateChanged(self, state):
        # должен прилетать сигнал от check_box
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
    @pyqtSlot(name = 'on_dialog1_button_pressed')
    def to_dialog1(self):
        # должны прилетать сигналы от dialog1_button, dialog1_action и Ctrl-1
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        dialog = Dialog_window(self)
        dialog.setStyleSheet('background-color: rgb(255, 236, 176);')
        dialog.exec_()
 
        print_result_data(self, dialog, traceback.extract_stack()[-1][2])
 
        dialog.deleteLater()
 
    @pyqtSlot(name = 'on_dialog2_button_pressed')
    def to_dialog2(self):
        # должны прилетать сигналы от dialog2_button, dialog2_action и Ctrl-2
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        dialog = Dialog_window(self)
        dialog.setStyleSheet('background-color: rgb(255, 236, 176);')
        dialog.setWindowFlags(
            Qt.Window | Qt.FramelessWindowHint | Qt.CustomizeWindowHint)
        dialog.exec_()
 
        print_result_data(self, dialog, traceback.extract_stack()[-1][2])
 
        dialog.deleteLater()
 
    @pyqtSlot(name = 'on_close_button_pressed')
    def to_close(self):
        # должны прилетать сигналы от close_button, exit_action и Ctrl-Q
        #
        # по идее, именно здесь должны выполняться все операции,
        # необходимые для корректного закрытия приложения:
        # - сохранение данных
        # - освобождение ресурсов
        # - закрытие соединений и прочнее
        # и только после завершения этих операций может быть вызван close()
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.close()
 
    def close(self):
        # повторная реализация для протокола
        #
        # должны прилетать сигналы от Alt-F4 и Ctrl-0
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Main_window, self).close()
 
 
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setApplicationName(APP_NAME)
 
    mwin = Main_window()
    mwin.show()
    sys.exit(app.exec_())
Промежуточный вариант, чтобы убедиться, что только декораторы имеют значение, а QMetaObject.connectSlotsByName() тут не причём:
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''Sender Probe v0.09y
from iamvic on cyberforum.ru
 
Версия с QMetaObject.connectSlotsByName() без декорирования слотов:
- отправитель сигнала, инициировавшего создание диалога, не маскируется.
  Соответствено, именно от его имени прилетают события QCloseEvent
  от внешних источников в полосе заголовка окна.
'''
 
import sys, traceback
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QMetaObject
from PyQt5.QtGui import QFont, QKeySequence
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QAction, QCheckBox, QPushButton
from PyQt5.QtWidgets import QLabel, QWidget, QShortcut
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QMainWindow, QDialog
 
APP_NAME = 'Sender Probe v0.09y'
 
 
def check_method(client, method_name):
    '''проверяет наличие метода в классе
    '''
    return(method_name in [f for f in dir(client)
           if callable(getattr(client, f)) and not f.startswith('__')])
 
 
def collect_parents(client, s = ''):
    '''собирает и возвращает неименованную строку с цепочкой родителей
    '''
    if type(client).__name__ != 'NoneType':
        s = '{!s}{!s}{!s}'.format(
            type(client).__name__, '.' if (s) else '', s)
        if type(client.parent()).__name__ != 'NoneType':
            s = collect_parents(client.parent(), s)
    return s
 
 
def get_parentage(client):
    '''возвращает именованную строку с цепочкой родителей
    '''
    if type(client).__name__ != 'NoneType':
        s = collect_parents(client, s = '')
        if check_method(client, 'text'):
            s += '({!s})'.format(client.text())
    else:
        s = type(client).__name__
    return s
 
 
def print_outsource_data(client, point):
    '''выводит данные о владельце и родителе текущего объекта,
    а также об отправителе принятого сигнала
    '''
    Var_store.serial_num += 1
 
    print('({!s}) ===> {!s}.{!s}()'.format(
        Var_store.serial_num, get_parentage(client), point))
    print('client =', get_parentage(client))
    print('client.parent() =', get_parentage(client.parent()))
    print('client.sender() =', get_parentage(client.sender()))
    print()
 
 
def print_result_data(client, dialog, point):
    '''выводит данные о результатах закрытия диалога
    '''
    Var_store.serial_num += 1
 
    print('({!s}) <--- {!s}.{!s}()'.format(
        Var_store.serial_num, get_parentage(client), point))
    print('{!s}.result() = {!s}'.format(
        get_parentage(dialog),
        dialog.result() if check_method(dialog, 'result') else '(undefined)'))
    print()
 
 
class Var_store(object):
    '''Сквозной серийный номер
    '''
    serial_num = 0
 
 
class Dialog_window(QDialog):
    '''Диалог
    '''
    def __init__(self, parent=None):
        # строим пользовательский интерфейс
        #
        super(Dialog_window, self).__init__(parent)
 
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        QShortcut(QKeySequence('Alt+F4'), self, self.close)
        QShortcut(QKeySequence('Ctrl+0'), self, self.close)
        # пара комбинаций завязана на один слот для проверки работы
 
        self.outsourcer = self.sender()
 
        info_text = QLabel('Про sender() и parent()', self)
        font = info_text.font()
        font.setWeight(QFont.Bold)
        info_text.setFont(font)
 
        self.check_box = QCheckBox('Отключить внешнее управление', self)
        close_button = QPushButton('Завершить диалог', self)
 
        self.check_box.setObjectName('check_box')
        close_button.setObjectName('close_button')
 
        v_map = QVBoxLayout(None)
        v_map.addStretch(1)
        v_map.addWidget(info_text)
        v_map.addWidget(self.check_box)
        v_map.addWidget(close_button)
        v_map.addStretch(1)
 
        main_map = QHBoxLayout(None)
        main_map.addStretch(1)
        main_map.addLayout(v_map)
        main_map.addStretch(1)
 
        self.setLayout(main_map)
 
        QMetaObject.connectSlotsByName(self)
 
    def closeEvent(self, e):
        # повторная реализация для перехвата обработки
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        if (self.sender() == self.outsourcer and
            not self.check_box.checkState() == Qt.Unchecked):
            # если внешнее управление отключено,
            # то игнорируем QCloseEvent от внешних источников:
            # - значок закрытия в заголовке окна,
            # - пункт <Закрыть> в выпадающем меню в заголовке
            # - комбинация клавиш Alt-F4
            e.ignore()
        else:
            e.accept()
 
    def on_check_box_stateChanged(self, state):
        # должен прилетать сигнал от check_box
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
    def on_close_button_pressed(self):
        # должен прилетать сигнал от close_button
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.accept()
 
    def accept(self):
        # повторная реализация для протокола
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).accept()
 
    def reject(self):
        # повторная реализация для протокола
        #
        # должен прилетать сигнал от Escape
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).reject()
 
    def close(self):
        # повторная реализация для протокола
        #
        # должны прилетать сигналы от Alt-F4 и Ctrl-0
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Dialog_window, self).close()
 
 
class Main_window(QMainWindow):
    '''Главное окно приложения
    '''
    def __init__(self, parent=None):
        # строим пользовательский интерфейс
        #
        super(Main_window, self).__init__(parent)
 
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        QShortcut(QKeySequence('Alt+F4'), self, self.close)
        QShortcut(QKeySequence('Ctrl+0'), self, self.close)
        # пара комбинаций завязана на один слот для проверки работы
 
        self.outsourcer = self.sender()
 
        dialog1_action = QAction(
            'Диалог с заголовком', self, shortcut='Ctrl+1',
            triggered=self.to_dialog1, enabled=True)
        dialog2_action = QAction(
            'Диалог без заголовка', self, shortcut='Ctrl+2',
            triggered=self.to_dialog2, enabled=True)
        exit_action = QAction(
            'Выход', self, shortcut='Ctrl+Q',
            triggered=self.to_close, enabled=True)
 
        task_menu = self.menuBar().addMenu('Действия')
        task_menu.addAction(dialog1_action)
        task_menu.addAction(dialog2_action)
        task_menu.addAction(exit_action)
 
        info_text = QLabel('Про sender() и parent()', self)
        font = info_text.font()
        font.setWeight(QFont.Bold)
        info_text.setFont(font)
 
        self.check_box = QCheckBox(
            'Отключить внешнее управление', self)
        dialog1_button = QPushButton(
            'Открыть диалог с заголовком', self)
        dialog2_button = QPushButton(
            'Открыть диалог без заголовка', self)
        close_button = QPushButton('Завершить работу', self)
 
        self.check_box.setObjectName('check_box')
        dialog1_button.setObjectName('dialog1_button')
        dialog2_button.setObjectName('dialog2_button')
        close_button.setObjectName('close_button')
 
        v_map = QVBoxLayout(None)
        v_map.addWidget(info_text)
        v_map.addWidget(self.check_box)
        v_map.addWidget(dialog1_button)
        v_map.addWidget(dialog2_button)
        v_map.addWidget(close_button)
        v_map.addStretch(1)
 
        main_map = QHBoxLayout(None)
        main_map.addStretch(1)
        main_map.addLayout(v_map)
        main_map.addStretch(1)
 
        central_widget = QWidget(self)
        central_widget.setLayout(main_map)
        self.setCentralWidget(central_widget)
 
        QMetaObject.connectSlotsByName(self)
 
        self.move(200, 200)
        self.resize(400, 300)
 
    def closeEvent(self, e):
        # повторная реализация для перехвата обработки
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        if (self.sender() == self.outsourcer and
            not self.check_box.checkState() == Qt.Unchecked):
            # если внешнее управление отключено,
            # то игнорируем QCloseEvent от внешних источников:
            # - значок закрытия в заголовке окна,
            # - пункт <Закрыть> в выпадающем меню в заголовке
            # - комбинация клавиш Alt-F4
            e.ignore()
        else:
            e.accept()
 
    def on_check_box_stateChanged(self, state):
        # должен прилетать сигнал от check_box
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
    def on_dialog1_button_pressed(self):
        # должны прилетать сигналы от dialog1_button
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.to_dialog1()
 
    def to_dialog1(self):
        # должны прилетать сигналы от dialog1_action и Ctrl-1
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        dialog = Dialog_window(self)
        dialog.setStyleSheet('background-color: rgb(255, 236, 176);')
        dialog.exec_()
 
        print_result_data(self, dialog, traceback.extract_stack()[-1][2])
 
        dialog.deleteLater()
 
    def on_dialog2_button_pressed(self):
        # должны прилетать сигналы от dialog2_button
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.to_dialog2()
 
    def to_dialog2(self):
        # должны прилетать сигналы от dialog2_action и Ctrl-2
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
 
        dialog = Dialog_window(self)
        dialog.setStyleSheet('background-color: rgb(255, 236, 176);')
        dialog.setWindowFlags(
            Qt.Window | Qt.FramelessWindowHint | Qt.CustomizeWindowHint)
        dialog.exec_()
 
        print_result_data(self, dialog, traceback.extract_stack()[-1][2])
 
        dialog.deleteLater()
 
    def on_close_button_pressed(self):
        # должны прилетать сигналы от close_button
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.to_close()
 
    def to_close(self):
        # должны прилетать сигналы от exit_action и Ctrl-Q
        #
        # по идее, именно здесь должны выполняться все операции,
        # необходимые для корректного закрытия приложения:
        # - сохранение данных
        # - освобождение ресурсов
        # - закрытие соединений и прочнее
        # и только после завершения этих операций может быть вызван close()
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        self.close()
 
    def close(self):
        # повторная реализация для протокола
        #
        # должны прилетать сигналы от Alt-F4 и Ctrl-0
        #
        print_outsource_data(self, traceback.extract_stack()[-1][2])
        super(Main_window, self).close()
 
 
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setApplicationName(APP_NAME)
 
    mwin = Main_window()
    mwin.show()
    sys.exit(app.exec_())
Метки pyqt5, python, python 3, qt5
Размещено в Памятка
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
[golang] Алгоритм «Хак Госпера»
alhaos 17.05.2026
Алгоритм «Хак Госпера» Хак Госпера (Gosper's Hack) — алгоритм нахождения следующего по величине числа с тем же количеством установленных бит. Придуман Биллом Госпером в 1970-х, опубликован в. . .
Рисование бинарного древа до 6-го колена на js, svg.
russiannick 17.05.2026
<svg width="335" height="240" viewBox="0 0 335 240" fill="#e5e1bb"> <style> <!]> </ style> <g id="bush"> </ g> </ svg> function fn(){ let rost;/ / высота древа let xx=165,yy=210,w=256;
FSharp: interface of module
DevAlt 16.05.2026
Интерфейс модуля F# позволяет управлять доступностью членов, содержащихся в реализации модуля. По-умолчанию все члены модуля доступны: module Foo let x = 10 let boo () = printfn "boo" . . .
Хитросплетение родственных связей пантеона греческих богов.
russiannick 14.05.2026
Однооконник, позволяющий узреть и изучить отдельных героев древней Греции. <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible". . .
[golang] Угол между стрелками часов
alhaos 12.05.2026
По заданным значениям часа и минуты необходимо определить значение меньшего угла между стрелками аналогового циферблата часов. import "math" func angleClock(hour int, minutes int) float64 { . . .
Debian 13: Установка Lazarus QT5
ВитГо 09.05.2026
Эта инструкция моя компиляция инструкций volvo https:/ / www. cyberforum. ru/ blogs/ 203668/ 10753. html и его же старой инструкции по установке Lazarus с gtk2. . .
Нейросеть на алгоритме "эстафета хвоста" как перспектива.
Hrethgir 06.05.2026
На десерт, когда запущу сервер. Статья тут https:/ / habr. com/ ru/ articles/ 1030914/ . Автор я сам, нейросеть только помогает в вопросах которые мне не известны - не знаю людей которые знали-бы. . .
Асинхронный приём данных из COM-порта
Argus19 01.05.2026
Асинхронный приём данных из COM-порта Купил на aliexpress термопринтер QR701. Он оказался странным. Поключил к Arduino Nano. Был очень удивлён. Наотрез отказывается печатать русские буквы. Чтобы. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru