К вопросу о модальности QDialog.
Однако, ещё одна тема для Памятки появилась. Пришлось тут столкнуться с ситуацией, которую учат избегать чуть ли не на уровне рефлексов, но вот находятся же пытливые умы 
Да, в документации QDialog Class | Qt Widgets нет прямого запрета на запуск диалога методом QDialog.exec_() после выполнения QDialog.show(). Возможность применения этих методов таким способом никому даже в голову не приходит, поскольку это просто разные техники, предназначенные для использования в обстоятельствах, кардинально отличающихся друг от друга.
И из того, что там написано
Немодальные диалоги отображаются с помощью функции show(), которая немедленно возвращает управление вызывающей стороне.
| вовсе не следует, что эта функция больше ничего не делает, и в результате последующего за ней вызова QDialog.exec_() родится модальный диалог.
Забегая вперёд, скажу, что если всё-таки это сделать, то диалог-то запустится и даже будет считать сам себя модальным:
1. QDialog.isModal() будет возвращать значение True,
2. QDialog.windowModality() будет возвращать значение Qt.ApplicationModal,
но никакой модальностью там даже пахнуть не будет - остальные окна приложения заблокированы не будут и ни что не помешает выполнять в них любые операции. То есть, фактически в результате получаем немодальное окно с собственным циклом обработки событий.
Если запустить несколько таких диалогов, то возможно их циклы обработки событий главному циклу мешать и не будут, а вот между собой явно будут грызться. Но это надо проверять, делать тестовое приложение, тем более кандидат на это дело уже есть (см. К вопросу об особенностях декорирования слотов, обрабатывающих сигнал destroyed), переделать его недолго.
demodal_001.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
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
"""deModal v0.01
From iamvic on www.cyberforum.ru
Restrictions: Python 3.x, PyQt5 and no more.
"""
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView, QTableView
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout
from PyQt5.QtWidgets import QLineEdit, QPushButton, QFormLayout
from PyQt5.QtWidgets import QMessageBox, QWidget
from PyQt5.QtGui import QCursor, QStandardItem, QStandardItemModel
from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QMetaObject, QObject
from PyQt5.QtCore import QModelIndex, QTimer
def probe_cursor_screen(client):
'''В соответствии с соглашениями о пользовательском интерфейсе
в многоэкранных системах окна вержнего уровня должны открываться
на том экране, на котором в момент открытия окна находится
указатель мыши. Linux, в силу особенностей X11, нарушает данные
соглашения, открывая их на активном экране.
Следуя соглашениям, определяем экран, на котором находится указатель
мыши в момент открытия окна.
'''
selected_screen = QApplication.screens()[0]
if (int(QT_VERSION_STR.split('.')[1]) >= 10 and
int(PYQT_VERSION_STR.split('.')[1]) >= 10):
selected_screen = QApplication.screenAt(QCursor.pos())
else:
screens = QApplication.screens()
if len(screens) > 0:
for i in range(len(screens)):
if screens[i].geometry().contains(QCursor.pos()):
selected_screen = screens[i]
break
return selected_screen
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 print_modality(client, point):
print('--> {!s}: {!s}.isModal() = {!s}'
' {!s}.windowModality() = {!s}'.format(
point,
client.objectName(), client.isModal(),
client.objectName(), client.windowModality()))
def make_dialog(client, holder, signal, count):
'''редактор данных, полученных из выбранной строки QTableView
client - родитель для диалога
holder - источник данных
signal - сигнатура сигнала
count - порядковый номер
'''
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 = Probe_dialog(client)
m_dialog.setObjectName('editor' + str(count))
m_dialog.setWindowTitle(m_dialog.objectName())
m_dialog.destroyed.connect(client.on_anyobjects_destroyed)
idx_count = len(holder.selectionModel().selectedIndexes())
m_form = QFormLayout()
m_clip = []
for i in range(0, idx_count):
x = QLineEdit(holder.selectionModel().selectedIndexes()[i].data(),
m_dialog)
x.setObjectName('editorformlineedit'+str(i))
m_form.addRow(holder.model().headerData(i, Qt.Horizontal), x)
m_clip.append(
[x,
holder.selectionModel().selectedIndexes()[i].row(),
holder.selectionModel().selectedIndexes()[i].column()])
m_ok = QPushButton('Сохранить', m_dialog)
m_ok.setObjectName('editorbuttonok')
m_ok.clicked.connect(m_dialog.accept)
m_cancel = QPushButton('Отмена', m_dialog)
m_cancel.setObjectName('editorbuttoncancel')
m_cancel.clicked.connect(m_dialog.reject)
m_hbox = QHBoxLayout()
m_hbox.addWidget(m_ok)
m_hbox.addStretch(1)
m_hbox.addWidget(m_cancel)
m_vbox = QVBoxLayout()
m_vbox.addLayout(m_form)
m_vbox.addStretch(1)
m_vbox.addLayout(m_hbox)
m_dialog.setLayout(m_vbox)
QTimer.singleShot(200, lambda: check_children_amount(client))
if signal == 'enterkeypressed':
print_modality(m_dialog, 'before show()')
m_dialog.show() # <== провоцирует потерю модальности
print_modality(m_dialog, 'after show()')
print_modality(m_dialog, 'before exec()')
QTimer.singleShot(200, m_dialog.modality_probe)
m_dialog.exec_()
print_modality(m_dialog, 'after exec()')
if m_dialog.result():
for i in range(0, idx_count):
holder.model().item(
m_clip[i][1],
m_clip[i][2]
).setText(m_clip[i][0].text().strip())
m_dialog.deleteLater()
class Probe_dialog(QDialog):
def modality_probe(self):
print_modality(self, 'inside exec()')
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 (e.button() == Qt.LeftButton and
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.w_count = 0 # счётчик запусков диалога
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.horizontalHeader().setStretchLastSection(True)
w_view.setSortingEnabled(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
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)
selected_screen = probe_cursor_screen(self)
# Следуя соглашениям о пользовательском интерфейсе,
# определяем экран, на котором находится указатель мыши,
# и размещаем главное окно в центре этого экрана.
# Размеры главного окна главного окна - половина высоты
# и ширины выбранного экрана.
# (Windows это не портит, а для Linux, особенно
# в многоэкоанных системах, просто необходимо)
current_geo = selected_screen.availableGeometry()
self.move(current_geo.x() + current_geo.width() // 4,
current_geo.y() + current_geo.height() // 4)
self.resize(current_geo.width() // 2,
current_geo.height() // 2)
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()
signal = self.sender().metaObject().method(
self.senderSignalIndex()).name().data().decode('utf-8')
print('signal =', signal)
self.w_count += 1
QTimer.singleShot(
100,
lambda: make_dialog(self, sender, signal, self.w_count))
@pyqtSlot(QObject)
# Судя по документации, разработчики PyQt отдают предпочтение указанию
# сигнатуры класса PyQt в декораторе - так, как это сделано для слота
# on_tableview_orignal_signals. Но именно здесь это не работает.
# Указав сигнатуру 'QObject', теряем определение сигнатуры полученного
# сигнала, а с указанием класса QObject - всё работает.
# https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html
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)
app.setApplicationName('deModal v0.01')
mwin = Main_window()
mwin.show()
app.exec_()
sys.exit() |
|
Суть получившегося приложения проста - есть некая табличка в несколько строк, которые можно редактировать, вызывая окно редактора. Двойной щелчок ЛКМ по выбранной строке вызывает модальное окно редактора общепринятым способом, а нажатие на клавишу ENTER - немодальное, используя способ, описанный выше, т.е. сначала QDialog.show(), а затем QDialog.exec_(). Всё происходящее протоколируется в сессии командной строки, из которой запустили приложение. Вызываемые окна редактора последовательно нумеруются, начиная с 1.

Описание действий оператора, приведших к ситуации, изображённой на картинке 1:
1. оператор запустил приложение в сессии командной строки:
--> активировалось главное окно,
--> сформировалась таблица,
--> активировалась строка 1 таблицы
2. нажал клавишу ENTER:
--> запустилось немодальное окно редактора editor1, содержащее данные строки 1
3. окно editor1 перетащил мышью в левый нижний угол
--> (это действие не протоколируется)
4. выполнил одиночный щелчок ЛКМ по заголовку окна приложения
--> (это действие не протоколируется)
5. нажав на клавишу Down (стрелка вниз) выбрал строку 2 таблицы
--> (это действие не протоколируется)
6. нажал клавишу ENTER:
--> запустилось немодальное окно редактора editor2, содержащее данные строки 2
7. окно editor2 перетащил мышью вниз по центру
--> (это действие не протоколируется)
8. одиночным щелчком ЛКМ выбрал строку 3 таблицы
9. выполнил двойной щелчок ЛКМ по строке 3 таблицы
--> запустилось модальное окно редактора editor3, содержащее данные строки 3
10. окно editor3 перетащил мышью в правый нижний угол
--> (это действие не протоколируется)
Протокол событий, соответствующий действиям оператора:
| 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
29
30
31
32
33
34
35
| user@linux:~> python3 demodal_001.py
==> mainwindow children amount = 5
tableview --> activated --> on_tableview_orignal_signals(0,0)
tableview --> enterkeypressed --> on_tableview_extra_signals()
signal = enterkeypressed
--> inherits() index = -1
--> before show(): editor1.isModal() = False editor1.windowModality() = 0
--> after show(): editor1.isModal() = False editor1.windowModality() = 0
--> before exec(): editor1.isModal() = False editor1.windowModality() = 0
==> mainwindow children amount = 6
--> inside exec(): editor1.isModal() = True editor1.windowModality() = 2
tableview --> activated --> on_tableview_orignal_signals(1,0)
tableview --> enterkeypressed --> on_tableview_extra_signals()
signal = enterkeypressed
--> inherits() index = -1
--> before show(): editor2.isModal() = False editor2.windowModality() = 0
--> after show(): editor2.isModal() = False editor2.windowModality() = 0
--> before exec(): editor2.isModal() = False editor2.windowModality() = 0
==> mainwindow children amount = 7
--> inside exec(): editor2.isModal() = True editor2.windowModality() = 2
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 --> 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()
signal = mousedoubleclicked
tableview --> clicked --> on_tableview_orignal_signals(2,2)
tableview --> activated --> on_tableview_orignal_signals(2,2)
--> inherits() index = -1
--> before exec(): editor3.isModal() = False editor3.windowModality() = 0
==> mainwindow children amount = 8
--> inside exec(): editor3.isModal() = True editor3.windowModality() = 2 |
|
Конечно, лучше бы всё это в динамике наблюдать, но и так видно, что к этому моменту уже целых три диалоговых окна запустили свои собственные циклы обработки событий.
И если с модальным диалоговым окном editor3 всё понятно - поскольку оно последнее в этой "летке-енке", не допустит открытия новых диалоговых окон и не даст никому закрыться раньше себя, то и изменённые в нём данные сразу отобразятся в соответствующей строке таблицы после сохранения.
А с немодальными диалогами editor1 и editor2 возникают проблемы. Тут всё зависит от порядка сохранения данных. Если сначала изменить и сохранить данные в окне editor2 (а оно открывалось позже editor1), то всё пройдёт благополучно. А если начать с окна editor1, то после нажатия на кнопку "Сохранить" окно editor1 закроется, а вот сохранённые данные отобразятся в соответствующей строке таблицы только после закрытия окна editor2. Просто потому, что цикл обработки событий окна editor2 был запущен позже и, видимо, неким образом блокирует обработку событий окна editor1 вплоть до своего завершения. Ну, правило такое - "последним вошёл, первым вышел"...
Независимо от причин появления таких "косяков", можно попытаться как-то предохраняться от последствий. Хотя бы так:

demodal_002.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
319
320
321
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
"""deModal v0.02
From iamvic on www.cyberforum.ru
Restrictions: Python 3.x, PyQt5 and no more.
"""
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView, QTableView
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout
from PyQt5.QtWidgets import QLineEdit, QPushButton, QFormLayout
from PyQt5.QtWidgets import QMessageBox, QWidget
from PyQt5.QtGui import QCursor, QStandardItem, QStandardItemModel
from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QMetaObject, QObject
from PyQt5.QtCore import QModelIndex, QTimer
def probe_cursor_screen(client):
'''В соответствии с соглашениями о пользовательском интерфейсе
в многоэкранных системах окна вержнего уровня должны открываться
на том экране, на котором в момент открытия окна находится
указатель мыши. Linux, в силу особенностей X11, нарушает данные
соглашения, открывая их на активном экране.
Следуя соглашениям, определяем экран, на котором находится указатель
мыши в момент открытия окна.
'''
selected_screen = QApplication.screens()[0]
if (int(QT_VERSION_STR.split('.')[1]) >= 10 and
int(PYQT_VERSION_STR.split('.')[1]) >= 10):
selected_screen = QApplication.screenAt(QCursor.pos())
else:
screens = QApplication.screens()
if len(screens) > 0:
for i in range(len(screens)):
if screens[i].geometry().contains(QCursor.pos()):
selected_screen = screens[i]
break
return selected_screen
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 check_modality(client, point):
print('--> {!s}: {!s}.isModal() = {!s}'
' {!s}.windowModality() = {!s}'.format(
point,
client.objectName(), client.isModal(),
client.objectName(), client.windowModality()))
def make_dialog(client, holder, signal, count):
'''редактор данных, полученных из выбранной строки QTableView
client - родитель для диалога
holder - источник данных
signal - сигнатура сигнала
count - порядковый номер
'''
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 = Probe_dialog(client)
m_dialog.setObjectName('editor' + str(count))
m_dialog.setWindowTitle(m_dialog.objectName())
m_dialog.destroyed.connect(client.on_anyobjects_destroyed)
idx_count = len(holder.selectionModel().selectedIndexes())
m_form = QFormLayout()
m_clip = []
for i in range(0, idx_count):
x = QLineEdit(holder.selectionModel().selectedIndexes()[i].data(),
m_dialog)
x.setObjectName('editorformlineedit'+str(i))
m_form.addRow(holder.model().headerData(i, Qt.Horizontal), x)
m_clip.append(
[x,
holder.selectionModel().selectedIndexes()[i].row(),
holder.selectionModel().selectedIndexes()[i].column()])
m_ok = QPushButton('Сохранить', m_dialog)
m_ok.setObjectName('editorbuttonok')
m_ok.clicked.connect(m_dialog.accept)
m_cancel = QPushButton('Отмена', m_dialog)
m_cancel.setObjectName('editorbuttoncancel')
m_cancel.clicked.connect(m_dialog.reject)
m_hbox = QHBoxLayout()
m_hbox.addWidget(m_ok)
m_hbox.addStretch(1)
m_hbox.addWidget(m_cancel)
m_vbox = QVBoxLayout()
m_vbox.addLayout(m_form)
m_vbox.addStretch(1)
m_vbox.addLayout(m_hbox)
m_dialog.setLayout(m_vbox)
QTimer.singleShot(200, lambda: check_children_amount(client))
if signal == 'enterkeypressed':
check_modality(m_dialog, 'before show()')
m_dialog.show() # <== провоцирует потерю модальности
check_modality(m_dialog, 'after show()')
check_modality(m_dialog, 'before exec()')
QTimer.singleShot(200, m_dialog.modality_probe)
m_dialog.exec_()
check_modality(m_dialog, 'after exec()')
if m_dialog.result():
for i in range(0, idx_count):
holder.model().item(
m_clip[i][1],
m_clip[i][2]
).setText(m_clip[i][0].text().strip())
m_dialog.deleteLater()
class Probe_dialog(QDialog):
def __init__(self, parent=None):
super(Probe_dialog, self).__init__(parent)
self.show_count = 0
def show(self):
self.show_count += 1
super(Probe_dialog, self).show()
def exec_(self):
print('{!s}.show_count = {!s}'.format(
self.objectName(), self.show_count))
if self.show_count > 0:
QTimer.singleShot(200, self.emergency_drop)
self.show_count += 1
return super(Probe_dialog, self).exec_()
def modality_probe(self):
check_modality(self, 'inside exec()')
def emergency_drop(self):
QMessageBox.critical(
self, 'О Ш И Б К А !!!',
'Попытка повторного открытия окна диалога {!s} методом'
' QDialog.exec_(), ранее открывавшегося методами'
' QDialog.show() или QDialog.exec_().\n\n'
'Обратитесь к разработчику программы для устранения'
' этой ошибки.'.format(self.objectName()))
self.reject()
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 (e.button() == Qt.LeftButton and
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.w_count = 0 # счётчик запусков диалога
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.horizontalHeader().setStretchLastSection(True)
w_view.setSortingEnabled(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
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)
selected_screen = probe_cursor_screen(self)
# Следуя соглашениям о пользовательском интерфейсе,
# определяем экран, на котором находится указатель мыши,
# и размещаем главное окно в центре этого экрана.
# Размеры главного окна главного окна - половина высоты
# и ширины выбранного экрана.
# (Windows это не портит, а для Linux, особенно
# в многоэкоанных системах, просто необходимо)
current_geo = selected_screen.availableGeometry()
self.move(current_geo.x() + current_geo.width() // 4,
current_geo.y() + current_geo.height() // 4)
self.resize(current_geo.width() // 2,
current_geo.height() // 2)
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()
signal = self.sender().metaObject().method(
self.senderSignalIndex()).name().data().decode('utf-8')
print('signal =', signal)
self.w_count += 1
QTimer.singleShot(
100,
lambda: make_dialog(self, sender, signal, self.w_count))
@pyqtSlot(QObject)
# Судя по документации, разработчики PyQt отдают предпочтение указанию
# сигнатуры класса PyQt в декораторе - так, как это сделано для слота
# on_tableview_orignal_signals. Но именно здесь это не работает.
# Указав сигнатуру 'QObject', теряем определение сигнатуры полученного
# сигнала, а с указанием класса QObject - всё работает.
# https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html
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)
app.setApplicationName('deModal v0.02')
mwin = Main_window()
mwin.show()
app.exec_()
sys.exit() |
|
Дополнение от 05.09.2022:
И всё-таки, хотя бы для прикола, можно добиться того, чтобы изменённые данные вовремя попадали в редактируемую таблицу даже у обсуждаемого "кадавра":
demodal_003.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
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
346
347
348
349
350
351
352
353
354
355
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
"""deModal v0.03
From iamvic on www.cyberforum.ru
Restrictions: Python 3.x, PyQt5 and no more.
"""
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView, QTableView
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout
from PyQt5.QtWidgets import QLineEdit, QPushButton, QFormLayout
from PyQt5.QtWidgets import QMessageBox, QWidget
from PyQt5.QtGui import QCursor, QStandardItem, QStandardItemModel
from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QMetaObject, QObject
from PyQt5.QtCore import QModelIndex, QTimer
def probe_cursor_screen(client):
'''В соответствии с соглашениями о пользовательском интерфейсе
в многоэкранных системах окна вержнего уровня должны открываться
на том экране, на котором в момент открытия окна находится
указатель мыши. Linux, в силу особенностей X11, нарушает данные
соглашения, открывая их на активном экране.
Следуя соглашениям, определяем экран, на котором находится указатель
мыши в момент открытия окна.
'''
selected_screen = QApplication.screens()[0]
if (int(QT_VERSION_STR.split('.')[1]) >= 10 and
int(PYQT_VERSION_STR.split('.')[1]) >= 10):
selected_screen = QApplication.screenAt(QCursor.pos())
else:
screens = QApplication.screens()
if len(screens) > 0:
for i in range(len(screens)):
if screens[i].geometry().contains(QCursor.pos()):
selected_screen = screens[i]
break
return selected_screen
def slot_trace_note(client):
'''расшифровка сигнала, полученного слотом
учитываем, что QTimer тоже посылает сигналы,
но метода windowTitle не имеет
'''
t = client.sender().windowTitle() if check_method(
client.sender(), 'windowTitle') else ''
n = client.sender().objectName()
return '{!s} --> {!s} --> {!s}'.format(
t if len(t) > 0 else n if len(n) > 0 else (
type(client.sender()).__name__),
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 print_modality(client, point):
'''отчёт о модальности виджета client
'''
t = client.windowTitle()
n = client.objectName()
print('--> {!s}: {!s}.isModal() = {!s}'
' {!s}.windowModality() = {!s}'.format(
point,
t if len(t) > 0 else n if len(n) > 0 else (
type(client).__name__),
client.isModal(),
t if len(t) > 0 else n if len(n) > 0 else (
type(client).__name__),
client.windowModality()
)
)
class Probe_dialog(QDialog):
def __init__(self, parent=None, holder=None, signal='', count=0):
super(Probe_dialog, self).__init__(parent)
self.m_clip = []
self.m_holder = holder # источник данных
self.idx_count = len(self.m_holder.selectionModel().selectedIndexes())
# поскольку окнам диалога присваивается порядковый номер,
# его надо где-то хранить; имя объекта для этих целей не годится,
# т.к. QMetaObject.connectSlotsByName(self), создавая
# соединения ориентируется именно на имена объектов и сигналов,
# подбирая соответствущий слот;
# будем пользовать заголовок окна, здесь это непринципиально
self.setObjectName('editor')
self.setWindowTitle('{!s}{!s}'.format(
self.objectName(), str(count)))
m_form = QFormLayout()
for i in range(0, self.idx_count):
x = QLineEdit(
self.m_holder.selectionModel().selectedIndexes()[i].data(),
self)
x.setObjectName('editorformlineedit')
x.setWindowTitle('{!s}{!s}({!s})'.format(
x.objectName(), str(count), str(i)))
m_form.addRow(self.m_holder.model().headerData(
i, Qt.Horizontal), x)
self.m_clip.append(
[x,
self.m_holder.selectionModel().selectedIndexes()[i].row(),
self.m_holder.selectionModel().selectedIndexes()[i].column()])
m_ok = QPushButton('Сохранить', self)
m_ok.setObjectName('editorbuttonok')
m_ok.setWindowTitle('{!s}{!s}'.format(
m_ok.objectName(), str(count)))
m_cancel = QPushButton('Отмена', self)
m_cancel.setObjectName('editorbuttoncancel')
m_cancel.setWindowTitle('{!s}{!s}'.format(
m_cancel.objectName(), str(count)))
m_hbox = QHBoxLayout()
m_hbox.addWidget(m_ok)
m_hbox.addStretch(1)
m_hbox.addWidget(m_cancel)
m_vbox = QVBoxLayout()
m_vbox.addLayout(m_form)
m_vbox.addStretch(1)
m_vbox.addLayout(m_hbox)
self.setLayout(m_vbox)
QMetaObject.connectSlotsByName(self)
QTimer.singleShot(200, lambda: check_children_amount(parent))
@pyqtSlot()
def on_editorbuttonok_clicked(self):
print('{!s}()'.format(slot_trace_note(self)))
for i in range(0, self.idx_count):
self.m_holder.model().item(
self.m_clip[i][1],
self.m_clip[i][2]
).setText(self.m_clip[i][0].text().strip())
QTimer.singleShot(200, self.accept)
# именно то, что вызов self.accept будет выполнен с задержкой,
# позволяет отработать циклу обработки событий диалога редактора
# и изменные данные попадут в редактируемую строку таблицы вовремя
@pyqtSlot()
def on_editorbuttoncancel_clicked(self):
print('{!s}()'.format(slot_trace_note(self)))
self.reject()
@pyqtSlot()
def modality_probe(self):
print('{!s}()'.format(slot_trace_note(self)))
print_modality(self, 'inside exec()')
def make_dialog(client, holder, signal, count):
'''для наружного употребления, подобно Static Public Members.
Способ употребления:
Probe_dialog.make_dialog(parent, holder, signal, count)
'''
print('--> make_dialog')
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
idx_count = len(holder.selectionModel().selectedIndexes())
m_dialog = Probe_dialog(client, holder, signal, count)
if signal == 'enterkeypressed':
print_modality(m_dialog, 'before show()')
m_dialog.show() # <== провоцирует потерю модальности
print_modality(m_dialog, 'after show()')
print_modality(m_dialog, 'before exec()')
QTimer.singleShot(200, m_dialog.modality_probe)
m_dialog.exec_()
print_modality(m_dialog, 'after exec()')
m_dialog.deleteLater()
return
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 (e.button() == Qt.LeftButton and
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.w_count = 0 # счётчик запусков диалога
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.horizontalHeader().setStretchLastSection(True)
w_view.setSortingEnabled(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
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)
selected_screen = probe_cursor_screen(self)
# Следуя соглашениям о пользовательском интерфейсе,
# определяем экран, на котором находится указатель мыши,
# и размещаем главное окно в центре этого экрана.
# Размеры главного окна главного окна - половина высоты
# и ширины выбранного экрана.
# (Windows это не портит, а для Linux, особенно
# в многоэкоанных системах, просто необходимо)
current_geo = selected_screen.availableGeometry()
self.move(current_geo.x() + current_geo.width() // 4,
current_geo.y() + current_geo.height() // 4)
self.resize(current_geo.width() // 2,
current_geo.height() // 2)
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()
signal = self.sender().metaObject().method(
self.senderSignalIndex()).name().data().decode('utf-8')
print('==> signal =', signal)
self.w_count += 1
QTimer.singleShot(
100,
lambda: Probe_dialog.make_dialog(
self, sender, signal, self.w_count))
@pyqtSlot(QObject)
# Судя по документации, разработчики PyQt отдают предпочтение указанию
# сигнатуры класса PyQt в декораторе - так, как это сделано для слота
# on_tableview_orignal_signals. Но именно здесь это не работает.
# Указав сигнатуру 'QObject', теряем определение сигнатуры полученного
# сигнала, а с указанием класса QObject - всё работает.
# https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html
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)
app.setApplicationName('deModal v0.03')
mwin = Main_window()
mwin.show()
app.exec_()
sys.exit() |
|
Для удобства восприятия кнопки диалога редактора тоже получили номер, соответствующий номеру окна редактора. В качестве примера можно привести вот такой фрагмент протокола:
| Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| editorbuttonok1 --> clicked --> on_editorbuttonok_clicked()
editorbuttoncancel5 --> clicked --> on_editorbuttoncancel_clicked()
editorbuttonok2 --> clicked --> on_editorbuttonok_clicked()
editorbuttoncancel6 --> clicked --> on_editorbuttoncancel_clicked()
editorbuttonok3 --> clicked --> on_editorbuttonok_clicked()
editorbuttoncancel7 --> clicked --> on_editorbuttoncancel_clicked()
editorbuttonok4 --> clicked --> on_editorbuttonok_clicked()
editorbuttoncancel8 --> clicked --> on_editorbuttoncancel_clicked()
--> after exec(): editor8.isModal() = False editor8.windowModality() = 0
--> after exec(): editor7.isModal() = False editor7.windowModality() = 0
--> after exec(): editor6.isModal() = False editor6.windowModality() = 0
--> after exec(): editor5.isModal() = False editor5.windowModality() = 0
--> after exec(): editor4.isModal() = False editor4.windowModality() = 0
--> after exec(): editor3.isModal() = False editor3.windowModality() = 0
--> after exec(): editor2.isModal() = False editor2.windowModality() = 0
--> after exec(): editor1.isModal() = False editor1.windowModality() = 0 |
|
Пользователь выполнял следующие действия:
- выбирал одну из строк таблицы,
- нажимая клавишу ENTER, открывал немодальное окно редактора описанным выше странным способом,
- повторял эти действия до открытия восьми таких окон,
- затем редактировал данные в произвольно выбранном окне и либо сохранял их, либо отменял сохранение (последним в серии этих операций было выбрано то окно, которое было открыто последним).
В результате оказалось, что изменённые в диалогах данные вовремя попадают в редактируемую таблицу после их сохранения и напрашивается вывод, что циклы обработки событий таких диалогов работе друг друга не мешают, т.е. обработка событий в ранее запущенных циклах не блокируется, а вот выход из цикла ранее запущенного диалога как раз блокируется работающим циклом диалога, запущенного позже.
Происходящее стало понятнее, но привлекательности этому корявому решению не добавило.
Да и у пользователя такая интересная нецензурная реакция была, типа "Да, вы одичали!!! Я тут щаз окончательно запутаюсь и так напортачу!!!"
|