0

I am trying to move my mouse to the File "button" on the menubar. In my program, pytestqt.mouseMove is moving the mouse to the wrong place (it's currently clicking near the window title).

Mouse Wrong Position

Setup

OS: Windows 10 Professional x64-bit, Build 1909
Python: 3.8.10 x64-bit
PyQt: 5.15.4
pytest-qt: 4.0.2
IDE: VSCode 1.59.0

Project Directory

gui/
├───gui/
│   │   main.py
│   │   __init__.py
│   │   
│   ├───controller/
│   │       controller.py
│   │       __init__.py
│   │
│   ├───model/
│   │      model.py
│   │       __init__.py
│   │
│   └───view/
│           view.py
│            __init__.py
├───resources/
│   │    __init__.py
│   │   
│   └───icons
│       │   main.ico
│       │   __init__.py
│       │   
│       └───toolbar
│               new.png
│               __init__.py
└───tests/
    │   conftest.py
    │   __init__.py
    │
    └───unit_tests
            test_view.py
            __init__.py

Code

gui/main.py:

from PyQt5.QtWidgets import QApplication

from gui.controller.controller import Controller
from gui.model.model import Model
from gui.view.view import View


class MainApp:
    def __init__(self) -> None:
        self.controller = Controller()
        self.model = self.controller.model
        self.view = self.controller.view

    def show(self) -> None:
        self.view.showMaximized()


if __name__ == "__main__":
    app = QApplication([])
    root = MainApp()
    root.show()
    app.exec_()

gui/view.py:

from typing import Any

from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QAction, QFrame, QGridLayout, QStatusBar, QToolBar, QWidget
from pyvistaqt import MainWindow

from resources.icons import toolbar


class View(MainWindow):
    def __init__(
        self, controller, parent: QWidget = None, *args: Any, **kwargs: Any
    ) -> None:
        super().__init__(parent, *args, **kwargs)
        self.controller = controller

        # Set the window name
        self.setWindowTitle("GUI Demo")

        # Create the container frame
        self.container = QFrame()

        # Create the layout
        self.layout = QGridLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)

        # Set the layout
        self.container.setLayout(self.layout)
        self.setCentralWidget(self.container)

        # Create and position widgets
        self._create_actions()
        self._create_menubar()
        self._create_toolbar()
        self._create_statusbar()

    def _create_actions(self):
        self.new_icon = QIcon(toolbar.NEW_ICO)

        self.new_action = QAction(self.new_icon, "&New Project...", self)
        self.new_action.setStatusTip("Create a new project...")

    def _create_menubar(self):
        self.menubar = self.menuBar()

        self.file_menu = self.menubar.addMenu("&File")

        self.file_menu.addAction(self.new_action)

    def _create_toolbar(self):
        self.toolbar = QToolBar("Main Toolbar")
        self.toolbar.setIconSize(QSize(16, 16))

        self.addToolBar(self.toolbar)

        self.toolbar.addAction(self.new_action)

    def _create_statusbar(self):
        self.statusbar = QStatusBar(self)
        self.setStatusBar(self.statusbar)

gui/model.py:

from typing import Any


class Model(object):
    def __init__(self, controller, *args: Any, **kwargs: Any):
        self.controller = controller

gui/controller.py:

from typing import Any

from gui.model.model import Model
from gui.view.view import View


class Controller(object):
    def __init__(self, *args: Any, **kwargs: Any):
        self.model = Model(controller=self, *args, **kwargs)
        self.view = View(controller=self, *args, **kwargs)

resources/icons/toolbar/__init__.py:

import importlib.resources as rsrc

from resources.icons import toolbar

with rsrc.path(toolbar, "__init__.py") as path:
    NEW_ICO = str((path.parent / "new.png").resolve())

test/conftest.py:

from typing import Any, Callable, Generator, List, Sequence, Union

import pytest
import pytestqt
from pytestqt.qtbot import QtBot
from gui.main import MainApp
from PyQt5 import QtCore

pytest_plugins: Union[str, Sequence[str]] = ["pytestqt.qtbot",]
"""A ``pytest`` global variable that registers plugins for use in testing."""


@pytest.fixture(autouse=True)
def clear_settings() -> Generator[None, None, None]:
    yield
    QtCore.QSettings().clear()


@pytest.fixture
def app(qtbot: QtBot) -> Generator[MainApp, None, None]:
    # Setup
    root = MainApp()
    root.show()
    qtbot.addWidget(root.view)

    # Run
    yield root

    # Teardown - None

test/unit_tests/test_view.py:

import time

from PyQt5 import QtCore, QtWidgets
import pytest
from pytestqt import qt_compat
from pytestqt.qt_compat import qt_api
from pytestqt.qtbot import QtBot

from gui.main import MainApp


def test_menubar_click(app: MainApp, qtbot: QtBot) -> None:
    # Arrange
    file_menu = app.view.file_menu
    file_menu.setMouseTracking(True)

    qtbot.addWidget(file_menu)

    # Act
    qtbot.wait(1000)
    qtbot.mouseMove(file_menu)
    qtbot.wait(5000)
    
    # Assert
    assert False

Problem:

The mouse moves to the wrong place:

Mouse Wrong Position

I need the mouse to move to the File button so I can click it. How can I achieve this?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
adam.hendry
  • 4,458
  • 5
  • 24
  • 51

1 Answers1

3

The following must be taken into account:

  • The QMenu is not a "File button" but it is the popup that is shown when pressing that element. For this reason, since it is not visible, the topleft of the screen is taken as a reference (since it does not have a parent) and the suggested size of the QMenu .

  • That "File button" is not a QWidget either, but is part of the QMenuBar where the QAction associated with the QMenu(menuAction() method) is used to paint it, so the mouseMove must use the QMenuBar and the actionGeometry() method to obtain the coordinates of the item.

def test_menubar_click(app: MainApp, qtbot: QtBot) -> None:
    # Arrange
    file_menu = app.view.file_menu
    menubar = app.view.menubar
    qtbot.add_widget(menubar)
    # Act
    action_rect = menubar.actionGeometry(file_menu.menuAction())
    qtbot.wait(3000)
    qtbot.mouseMove(menubar, action_rect.center())
    qtbot.wait(3000)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Brilliant once again! Do you have a book or resource you can recommend to better learn Qt in python? I've read "Create GUI Applications with Python & Qt 5" by Martin Fitzpatrick, "Modern PyQt" by Joshua Williams, and "Beginning PyQt" by Joshua M. Willman, but none of these have much detailed information. You seem to have a very good grasp of Qt. Have you written a book by chance? – adam.hendry Aug 08 '21 at 23:48
  • 1
    @A.Hendry You just have to read the Qt docs, there it clearly indicates what each element is and how they interact. – eyllanesc Aug 09 '21 at 00:13
  • Follow-up question. I'm trying to read the docs to figure out how to simulate a mouse click that opens the menu. `qtbot.mouseClick(menubar, qt_api.QtCore.Qt.LeftButton)` doesn't work. How do I do this? – adam.hendry Aug 09 '21 at 00:50
  • 1
    @A.Hendry I think you know that if you pass the position then the center of the widget will be used (in this case the menubar), on the other hand the QMenu does not show instantly so give it a delay: `qtbot.mouseClick(menubar, QtCore.Qt.LeftButton, pos=rect.center())` `qtbot.wait(3000)` – eyllanesc Aug 09 '21 at 01:17
  • @eyllansc *facepalm*! I forgot the position argument! Thank you!! You're super helpful! – adam.hendry Aug 09 '21 at 01:19
  • So so sorry, one last question. I know I'm taking up a lot of your time. How do I now (1) move to the file menu entry that appears under the "File button", and (2) click it? I'll buy you two more cups of coffee :) – adam.hendry Aug 09 '21 at 01:24
  • @A.Hendry Do you want to click on "New Project..."? – eyllanesc Aug 09 '21 at 01:36
  • Yes sir. I want to click on "New Project..." and make sure it does what it's supposed to. – adam.hendry Aug 09 '21 at 01:43
  • 1
    @A.Hendry In the following gist is the code: https://gist.github.com/eyllanesc/ded349044bf43dd79f8c43acb049b263, I will delete it in 1 or 2 days – eyllanesc Aug 09 '21 at 01:45
  • @eyllansc Ah, makes perfect sense. Just rinse and repeat the same code as for the menubar. Thanks! – adam.hendry Aug 09 '21 at 01:48
  • Would you be comfortable with adding the gist to the answer so others can benefit from it as well? – adam.hendry Aug 09 '21 at 01:53
  • @A.Hendry No, since that is not the question of your post. – eyllanesc Aug 09 '21 at 01:56
  • Understood and no worries. Thank you again for your help! – adam.hendry Aug 09 '21 at 01:58
  • @eyllansc Since you've already gone through the trouble, would you like me to put the questions in these comments as a separate SO question so I can give you points for the answer? – adam.hendry Aug 09 '21 at 02:00
  • @A.Hendry No, if you have time you post the question and you post the answer. The points are trivial and with my reputation less. – eyllanesc Aug 09 '21 at 02:02