В документации достаточно подробно разбираются основные отличия реализаций Qt для различных
платформ, но далеко не все. По крайней мере, о том, что поведение QTableView под Linux отличается
от поведения под Windows, упоминаний найти не удалось.
Представим, что выбрав строку из какой-либо таблицы, требуется нажатием клавиши ENTER или
двойным щелчком любой клавиши мыши запустить некий диалог, который будет что-то делать
с данными, полученными из этой строки. Какие минимальный набор средств потребуется
для реализации такого сценария под Windows и под Linux?
Для того, чтобы понять есть ли разница в процессах, происходящих после соответствующих
действий пользователя на разных платформах, воспользуемся упрощённой моделькой
из предыдущей записи блога:
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
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView
from PyQt5.QtWidgets import QTableView
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtCore import QMetaObject, QModelIndex
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])
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setObjectName('mainwindow')
self.setGeometry(300, 200, 500, 400)
w_view = QTableView(self)
w_view.setObjectName('tableview')
w_model = QStandardItemModel(self)
w_model.setObjectName('itemmodel')
w_model.setHorizontalHeaderLabels(['Деталь', 'Длина', 'Поставщик'])
w_view.setModel(w_model)
w_view.setSortingEnabled(True)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
w_view.horizontalHeader().setStretchLastSection(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionMode(QAbstractItemView.SingleSelection)
QMetaObject.connectSlotsByName(self)
rows = [['Стойка', 200, 'Фабрика'],
['Штанга', 10, 'Завод'],
['Перекладина', 5, 'Мастерская']
]
for row in rows:
w_model.appendRow([QStandardItem(row[0]),
QStandardItem(str(row[1])),
QStandardItem(row[2])])
w_view.selectRow(0)
self.setCentralWidget(w_view)
@pyqtSlot(QModelIndex, name='on_tableview_activated')
@pyqtSlot(QModelIndex, name='on_tableview_clicked')
@pyqtSlot(QModelIndex, name='on_tableview_doubleClicked')
@pyqtSlot(QModelIndex, name='on_tableview_entered')
@pyqtSlot(QModelIndex, name='on_tableview_pressed')
@pyqtSlot(QModelIndex)
def on_tableview_signals(self, idx):
print('{!s}({!s},{!s})'.format(slot_trace_note(self),
idx.row(), idx.column()
))
if __name__ == '__main__':
app = QApplication(sys.argv)
mwin = MainWindow()
mwin.show()
app.exec_()
sys.exit() |
|
Протокол для Windows (с комментариями):
Код:
C:\>py table_view_probe.py
tableview --> activated --> on_tableview_signals(0,0) # нажатие ENTER
tableview --> pressed --> on_tableview_signals(0,1) # одиночный щелчок клавиши мыши
tableview --> clicked --> on_tableview_signals(0,1)
tableview --> pressed --> on_tableview_signals(1,0) # двойной щелчок клавиши мыши
tableview --> clicked --> on_tableview_signals(1,0)
tableview --> doubleClicked --> on_tableview_signals(1,0)
tableview --> activated --> on_tableview_signals(1,0)
Протокол для Linux (с комментариями):
Код:
user@linux:~> python3 table_view_probe.py
tableview --> activated --> on_tableview_signals(0,0) # нажатие ENTER
tableview --> pressed --> on_tableview_signals(0,0) # одиночный щелчок клавиши мыши
tableview --> clicked --> on_tableview_signals(0,0)
tableview --> activated --> on_tableview_signals(0,0)
tableview --> pressed --> on_tableview_signals(0,1) # двойной щелчок клавиши мыши
tableview --> clicked --> on_tableview_signals(0,1)
tableview --> activated --> on_tableview_signals(0,1)
tableview --> doubleClicked --> on_tableview_signals(0,1)
tableview --> clicked --> on_tableview_signals(0,1)
tableview --> activated --> on_tableview_signals(0,1)
Разница, как видим, весьма существенна. Если под Windows, судя по протоколу, достаточно
запустить диалог при получении сигнала QTableView.activated, то под Linux всё гораздо сложнее.
Python | 1
2
3
4
5
6
| if __name__ == '__main__':
app = QApplication(sys.argv)
if 'Windows' in QStyleFactory.keys():
app.setStyle(QStyleFactory.create('Windows'))
. . . |
|
проблему не решает, зато перестают подсвечиваться ячейки таблицы при наведении на них курсора .
Тем не менее, решение, которое удовлетворяло бы обе стороны, найти можно
(первоначальный вариант убрал под спойлер, пусть хранится для истории):
Кликните здесь для просмотра всего текста
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
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView
from PyQt5.QtWidgets import QTableView, QDialog, QLabel
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtCore import QMetaObject, QModelIndex
def make_dialog(client):
set_dialog_permitted(client, False)
m_dialog = QDialog(client)
for i in range(0, len(client.sender().selectionModel().selectedIndexes())):
x = QLabel(client.sender().selectionModel().selectedIndexes()[i].data(),
m_dialog)
x.move(30, i*30)
m_dialog.resize(300, 200)
m_dialog.exec_()
def check_dialog_permitted(client):
return client.dialog_permitted
def set_dialog_permitted(client, value):
client.dialog_permitted = value
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])
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setObjectName('mainwindow')
self.setGeometry(300, 200, 500, 400)
self.dialog_permitted = True
# диалог разрешён
w_view = QTableView(self)
w_view.setObjectName('tableview')
w_model = QStandardItemModel(self)
w_model.setObjectName('itemmodel')
w_model.setHorizontalHeaderLabels(['Деталь', 'Длина', 'Поставщик'])
w_view.setModel(w_model)
w_view.setSortingEnabled(True)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
w_view.horizontalHeader().setStretchLastSection(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionMode(QAbstractItemView.SingleSelection)
QMetaObject.connectSlotsByName(self)
rows = [['Стойка', 200, 'Фабрика'],
['Штанга', 10, 'Завод'],
['Перекладина', 5, 'Мастерская']
]
for row in rows:
w_model.appendRow([QStandardItem(row[0]),
QStandardItem(str(row[1])),
QStandardItem(row[2])])
w_view.selectRow(0)
self.setCentralWidget(w_view)
@pyqtSlot(QModelIndex, name='on_tableview_clicked')
@pyqtSlot(QModelIndex, name='on_tableview_entered')
@pyqtSlot(QModelIndex, name='on_tableview_pressed')
@pyqtSlot(QModelIndex)
def on_tableview_signals(self, idx):
print('{!s}({!s},{!s})'.format(slot_trace_note(self),
idx.row(), idx.column()
))
if check_dialog_permitted(self):
set_dialog_permitted(self, False)
QTimer.singleShot(200, lambda: set_dialog_permitted(self, True))
@pyqtSlot(QModelIndex, name='on_tableview_activated')
def on_tableview_activated(self, idx):
print('{!s}({!s},{!s})'.format(slot_trace_note(self),
idx.row(), idx.column()
))
if check_dialog_permitted(self):
make_dialog(self)
set_dialog_permitted(self, True)
@pyqtSlot(QModelIndex, name='on_tableview_doubleClicked')
def on_tableview_doubleClicked(self, idx):
print('{!s}({!s},{!s})'.format(slot_trace_note(self),
idx.row(), idx.column()
))
make_dialog(self)
set_dialog_permitted(self, False)
# к этому моменту диалог мог быть разрешён запоздавшим QTimer.singleShot,
# но ведь должен ещё прилететь заключительный activated,
# поэтому запрещаем
QTimer.singleShot(200, lambda: set_dialog_permitted(self, True))
if __name__ == '__main__':
app = QApplication(sys.argv)
mwin = MainWindow()
mwin.show()
app.exec_()
sys.exit() |
|
Дополнение от 15.01.2022
Что не так в первоначальном варианте? Смущает то, что под Linux сигналы clicked и activated,
которые должны были бы поступить на обработку после сигнала doubleClicked, так и не приходят.
Под Windows же сигнал activated, формируемый после doubleClicked, ожидаемо приходит уже
после закрытия диалога, что тоже как-то коряво выглядит, на мой взгляд.
А вот вариант, где работа с диалогом осуществляется вне обработчика сигналов, этих неоднозначных
проявлений лишён и выглядит более предпочтительным:
Изменение от 19.01.2022
А если ещё отделить линуксово от виндовсового , то можно получить вариант, работающий
под той и другой системой в полном соответствии с ожиданиями (убрал под спойлер для истории):
Кликните здесь для просмотра всего текста
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
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView, QTableView
from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtCore import QMetaObject, QModelIndex
def make_dialog(client, sender):
if type(sender) != QTableView:
QMessageBox.warning(client, 'О Ш И Б К А !!!',
'С отправителем типа\n{!s}\n'
' диалог невозможен!'.format(type(sender)))
return
m_dialog = QDialog(client)
vbox = QVBoxLayout()
for i in range(0, len(sender.selectionModel().selectedIndexes())):
x = QLabel(sender.selectionModel().selectedIndexes()[i].data(),
m_dialog)
vbox.addWidget(x)
vbox.addStretch(1)
m_dialog.setLayout(vbox)
m_dialog.resize(300, 200)
m_dialog.exec_()
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])
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setObjectName('mainwindow')
self.setGeometry(300, 200, 500, 400)
w_view = QTableView(self)
w_view.setObjectName('tableview')
w_view.is_pressed_for_linux = False
w_view.is_double_clicked_for_linux = False
w_model = QStandardItemModel(self)
w_model.setObjectName('itemmodel')
w_model.setHorizontalHeaderLabels(['Деталь', 'Длина', 'Поставщик'])
w_view.setModel(w_model)
w_view.setSortingEnabled(True)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
w_view.horizontalHeader().setStretchLastSection(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionMode(QAbstractItemView.SingleSelection)
QMetaObject.connectSlotsByName(self)
rows = [['Стойка', 200, 'Фабрика'],
['Штанга', 10, 'Завод'],
['Перекладина', 5, 'Мастерская']
]
for row in rows:
w_model.appendRow([QStandardItem(row[0]),
QStandardItem(str(row[1])),
QStandardItem(row[2])])
w_view.selectRow(0)
self.setCentralWidget(w_view)
@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_signals(self, idx):
print('{!s}({!s},{!s})'.format(
slot_trace_note(self), idx.row(), idx.column()))
sender = self.sender()
signal_name = sender.metaObject().method(
self.senderSignalIndex()).name().data().decode('utf-8')
if sys.platform == 'linux':
if signal_name == 'pressed':
sender.is_pressed_for_linux = True
sender.is_double_clicked_for_linux = False
elif signal_name == 'clicked':
sender.is_pressed_for_linux = True
elif signal_name == 'entered':
sender.is_pressed_for_linux = False
sender.is_double_clicked_for_linux = False
elif signal_name == 'doubleClicked':
sender.is_double_clicked_for_linux = True
elif signal_name == 'activated':
if (sender.is_double_clicked_for_linux or
(not sender.is_pressed_for_linux and
not sender.is_double_clicked_for_linux)):
QTimer.singleShot(50, lambda: make_dialog(self, sender))
sender.is_pressed_for_linux = False
sender.is_double_clicked_for_linux = False
else:
# решение для Windows
if signal_name == 'activated':
QTimer.singleShot(50, lambda: make_dialog(self, sender))
if __name__ == '__main__':
app = QApplication(sys.argv)
mwin = MainWindow()
mwin.show()
app.exec_()
sys.exit() |
|
Изменение от 22.01.2022
Однако, и этот вариант не лишён "сюрпризов", бороться с которыми уже тяжело.
В частности, под Linux диалог вызывается не только двойным щелчком клавиши мыши,
но и после одновременного однократного нажатия и отпускания любой пары клавиш мыши.
И остаётся единственный способ - создать подкласс QTableView и всё делать самому:
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
| #!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys, traceback
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QAbstractItemView, QTableView
from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QMetaObject
from PyQt5.QtCore import QModelIndex, QTimer
def slot_trace_note(client):
return '{!s} --> {!s} --> {!s}'.format(
client.sender().objectName(),
client.sender().metaObject().method(
client.senderSignalIndex()).name().data().decode('utf-8'),
traceback.extract_stack()[-2][2])
def check_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_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 make_dialog(client, holder):
'''диалог показа данных, полученных из выбранной строки QTableView
client - родитель для диалога
holder - источник данных
'''
try:
print('--> inherits() index =',
holder.metaObject().indexOfMethod('inherits()'))
except:
print('--> holder is not Qt member')
# inherits() отсутствует в метаданных QObject и, соответственно,
# всех его наследников, хотя в самом классе он есть
# поэтому наличие inherits() проверяем средствами Python
if not (check_method(holder, 'inherits') and
holder.inherits('QTableView')):
QMessageBox.warning(
client, 'О Ш И Б К А !!!',
'Из источника данных класса\n{!s}\n'
'получить данные для диалога невозможно!\n\n'
'Допустимы только наследники QTableView.'.format(type(holder)))
return
m_dialog = QDialog(client)
vbox = QVBoxLayout()
for i in range(0, len(holder.selectionModel().selectedIndexes())):
x = QLabel(holder.selectionModel().selectedIndexes()[i].data(),
m_dialog)
vbox.addWidget(x)
vbox.addStretch(1)
m_dialog.setLayout(vbox)
m_dialog.resize(300, 200)
m_dialog.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 check_signal_connected(self, 'mousedoubleclicked'):
QTimer.singleShot(50, lambda: self.mousedoubleclicked.emit())
super(Table_view, self).mouseDoubleClickEvent(e)
class Main_window(QMainWindow):
def __init__(self, parent=None):
super(Main_window, self).__init__(parent)
self.setObjectName('mainwindow')
self.setGeometry(300, 200, 500, 400)
w_view = Table_view(self)
w_view.setObjectName('tableview')
w_model = QStandardItemModel(self)
w_model.setObjectName('itemmodel')
w_model.setHorizontalHeaderLabels(['Деталь', 'Длина', 'Поставщик'])
w_view.setModel(w_model)
w_view.setSortingEnabled(True)
w_view.setSelectionBehavior(QAbstractItemView.SelectRows)
w_view.horizontalHeader().setStretchLastSection(True)
w_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
w_view.setSelectionMode(QAbstractItemView.SingleSelection)
QMetaObject.connectSlotsByName(self)
rows = [['Стойка', 200, 'Фабрика'],
['Штанга', 10, 'Завод'],
['Перекладина', 5, 'Мастерская']
]
for row in rows:
w_model.appendRow([QStandardItem(row[0]),
QStandardItem(str(row[1])),
QStandardItem(row[2])])
w_view.selectRow(0)
self.setCentralWidget(w_view)
@pyqtSlot(QModelIndex, name='on_tableview_pressed')
@pyqtSlot(QModelIndex, name='on_tableview_clicked')
@pyqtSlot(QModelIndex, name='on_tableview_entered')
@pyqtSlot(QModelIndex, name='on_tableview_doubleClicked')
@pyqtSlot(QModelIndex, name='on_tableview_activated')
@pyqtSlot(QModelIndex)
def on_tableview_orignal_signals(self, idx):
print('{!s}({!s},{!s})'.format(
slot_trace_note(self), idx.row(), idx.column()))
@pyqtSlot(name='on_tableview_enterkeypressed')
@pyqtSlot(name='on_tableview_mousedoubleclicked')
@pyqtSlot()
def on_tableview_extra_signals(self):
print('{!s}()'.format(slot_trace_note(self)))
sender = self.sender()
QTimer.singleShot(100, lambda: make_dialog(self, sender))
#QTimer.singleShot(100, lambda: make_dialog(self, None))
if __name__ == '__main__':
app = QApplication(sys.argv)
mwin = Main_window()
mwin.show()
app.exec_()
sys.exit() |
|
И вот это уже работает надёжно и предсказуемо в любой системе. |