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