如何使用 pytest-qt 鼠标单击在 QTableWidget 中选择一个项目?

Posted

技术标签:

【中文标题】如何使用 pytest-qt 鼠标单击在 QTableWidget 中选择一个项目?【英文标题】:How to select an item in a QTableWidget using pytest-qt mouse click? 【发布时间】:2021-08-20 02:50:37 【问题描述】:

我的主 GUI 中有一个表格。我想测试我使用右键单击项目时出现的菜单删除表中项目的能力。我正在使用 pytest-qt 进行测试。在单击小部件(例如按钮)时,使用 qtbot.mouseClick 似乎效果很好,但是当我尝试向它传递一个表格项时,它会给我一个类型错误(由于表格项不是小部件)。给我错误的代码行如下:

qtbot.mouseClick(maingui.tablename.item(row, col), Qt.RightButton)

出现错误:

TypeError: arguments did not match any overloaded call:
mouseClick(QWidget, Qt.MouseButton, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay: int = -1): argument 1 has unexpected type 'QTableWidgetItem'

鉴于文档,这个错误对我来说是有意义的。我的问题是,有没有办法做到这一点?

我认为它不应该与问题相关,但是通过右键单击表项调用的函数使用 QPoint 装饰器。我的代码对右键单击的反应如下:

@pyqtSlot(QPoint)
def on_tablename_customContextMenuRequested(self, point):
    current_cell = self.tablename.itemAt(point)
    if current_cell:
        row = current_cell.row()
        deleteAction = QAction('Delete item', self)
        editAction = QAction('Edit item', self)
        menu.addAction(deleteAction)
        menu.addAction(editAction)
        action = menu.exec_(self.tablename.mapToGlobal(point))
        if action == deleteAction:
            # <do delete stuff>
        elif action == editAction:
            # <do edit stuff>

编辑:我可以使用 eyllanesc 的建议在表格中选择一个项目,但是右键单击该项目不会显示自定义上下文菜单。这是我的问题的最小可重现示例,使用带有自定义上下文菜单的两列表。我需要能够在测试期间自动选择“删除项目”选项:

from time import sleep

import pytest
from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSlot
from PyQt5.QtWidgets import QMainWindow, QTableWidgetItem, QMenu, QAction, QAbstractItemView
from tests.test_ui_generated import ui_minimum_main

pytest.main(['-s'])

class TestTable(ui_minimum_main.Ui_minimum_table, QMainWindow):
    def __init__(self, args):
        QMainWindow.__init__(self)
        self.setupUi(self)

        self.table_minimum.setContextMenuPolicy(Qt.CustomContextMenu)
        self.table_minimum.setColumnCount(2)
        self.detectorHorizontalHeaderLabels = ['Col A', 'Col B']
        self.table_minimum.setHorizontalHeaderLabels(self.detectorHorizontalHeaderLabels)
        self.table_minimum.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.table_minimum.setSelectionBehavior(QAbstractItemView.SelectRows)

        self.table_minimum.setRowCount(1)
        self.table_minimum.setRowHeight(0, 22)
        item = QTableWidgetItem('test_col_a')
        item.setData(Qt.UserRole, 'test_col_a')
        self.table_minimum.setItem(0, 0, item)
        item = QTableWidgetItem('test_col_b')
        item.setData(Qt.UserRole, 'test_col_b')
        self.table_minimum.setItem(0, 1, item)
        self.table_minimum.resizeRowsToContents()


    @pyqtSlot(QPoint)
    def on_table_minimum_customContextMenuRequested(self, point):
        print('context_menu_requested')
        current_cell = self.table_minimum.itemAt(point)

        if current_cell:
            deleteAction = QAction('Option A- Delete Row', self)
            nothingAction = QAction('Option B- Nothing', self)
            menu = QMenu(self.table_minimum)
            menu.addAction(deleteAction)
            menu.addAction(nothingAction)
            action = self.menu.exec_(self.table_minimum.mapToGlobal(point))
            if action == deleteAction:
                self.table_minimum.setRowCount(0)
                return


def test_detector_create_delete_gui(qtbot):
    w = TestTable([])
    qtbot.addWidget(w)
    w.show()
    qtbot.waitForWindowShown(w)
    sleep(.5)

    item = w.table_minimum.item(0, 0)

    assert item is not None

    def interact_with_menu():
        # ???????
        pass

    rect = w.table_minimum.visualItemRect(item)
    QTimer.singleShot(100, interact_with_menu)
    qtbot.mouseClick(w.table_minimum.viewport(), Qt.RightButton, pos=rect.center())

【问题讨论】:

【参考方案1】:

QTableWidgetItem 不是小部件,因此您不能直接使用它,而是必须获取与 QTableWidgetItem 关联的单元格的位置并将该信息用于鼠标单击。

item = maingui.tablename.item(row, col)
assert item is not None
rect = maingui.tablename.visualItemRect(item)
qtbot.mouseClick(maingui.tablename.viewport(), Qt.RightButton, pos=rect.center())

应该注意,可能有一些单元格没有与 QTableWidgetItem 关联,所以如果你想测试这种情况,那么你必须使用 QModelIndex:

index = maingui.tablename.model().index(row, col)
assert index.isValid()
rect = maingui.tablename.visualRect(index)
qtbot.mouseClick(maingui.tablename.viewport(), Qt.RightButton, pos=rect.center())

更新:

位置是相对于 QTableWidget 的视口,所以您必须将其更改为:

@pyqtSlot(QPoint)
def on_table_minimum_customContextMenuRequested(self, point):
    print("context_menu_requested")
    current_cell = self.table_minimum.itemAt(point)
    if current_cell:
        deleteAction = QAction("Option A- Delete Row", self)
        nothingAction = QAction("Option B- Nothing", self)
        menu = QMenu(self.table_minimum)
        menu.addAction(deleteAction)
        menu.addAction(nothingAction)
        action = menu.exec_(self.table_minimum.viewport().mapToGlobal(point))
        if action is deleteAction:
            self.table_minimum.setRowCount(0)
            return

另一方面,打开上下文菜单的事件不是单击,而是操作系统检测到您要打开上下文菜单,因此在 Qt 中,您必须通过 QContextMenuEvent 模拟该事件,如下所示:

class Helper(QObject):
    finished = pyqtSignal()


def test_detector_create_delete_gui(qtbot):
    helper = Helper()

    w = TestTable([])
    qtbot.addWidget(w)
    w.show()
    qtbot.waitForWindowShown(w)

    helper = Helper()

    def assert_row_count():
        assert w.table_minimum.rowCount() == 0
        helper.finished.emit()

    def handle_timeout():
        menu = None
        for tl in QApplication.topLevelWidgets():
            if isinstance(tl, QMenu):
                menu = tl
                break
        assert menu is not None
        delete_action = None
        for action in menu.actions():
            if action.text() == "Option A- Delete Row":
                delete_action = action
                break
        assert delete_action is not None
        rect = menu.actionGeometry(delete_action)
        QTimer.singleShot(100, assert_row_count)
        qtbot.mouseClick(menu, Qt.LeftButton, pos=rect.center())

    with qtbot.waitSignal(helper.finished, timeout=10 * 1000):
        QTimer.singleShot(1000, handle_timeout)
        item = w.table_minimum.item(0, 0)
        assert item is not None
        rect = w.table_minimum.visualItemRect(item)
        event = QContextMenuEvent(QContextMenuEvent.Mouse, rect.center())
        QApplication.postEvent(w.table_minimum.viewport(), event)

【讨论】:

这可以点击我表中的对象,谢谢!但是,它似乎没有打开上下文菜单。出于某种原因,该函数似乎没有采用 QPoint 信号。作为参考,我原始问题末尾的函数是主小部件类的函数。我将右键单击发送到表格小部件而不是主 gui 是否有问题,从而永远不会触发 on_tablename_customContextMenuRequested() 函数? @StevenCzyz 您如何检查上下文菜单是否未打开?您可以提供minimal reproducible example 来分析错误原因。 我在自定义上下文菜单函数中使用打印语句来查看是否在测试过程中调用了自定义上下文菜单。当我手动右键单击表中的一个选项时,会按预期调用打印语句。当我使用测试功能时,它没有。我已经编辑了我的原始帖子以添加一个最小可重复的示例。

以上是关于如何使用 pytest-qt 鼠标单击在 QTableWidget 中选择一个项目?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 pytest-qt 自动拖动鼠标?

如何在pytest-qt中单击按钮后检查正确打开窗口

`pytest-qt`函数`mouseMove()`不工作

如何使用 pytest-qt 点击 QMessageBox?

为啥在使用 pytest-qt 进行测试时出现致命的 Python 错误?

在 CircleCI 上运行 pytest-qt