Форум программистов, компьютерный форум, киберфорум
iamvic
Войти
Регистрация
Восстановить пароль
Путевые заметки в процессе познания Python и PyQt/PySide.
Оценить эту запись

К вопросу об отличиях поведения виджетов на различных платформах на примере QTableView.

Запись от iamvic размещена 14.01.2022 в 23:01
Обновил(-а) iamvic Вчера в 10:06 (изменение кода)
Метки linux, pyqt5, python, python 3, qt5, windows

В документации достаточно подробно разбираются основные отличия реализаций 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()
Размещено в Памятка
Показов 371 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2022, CyberForum.ru