pythonqtqgraphicsviewpyside6pyqt6

QLineEdit inside QGraphicsItemGroup does not receive focus


I'm creating a custom PySide6 application with a QGraphicsView containing various custom items, including a QLineEdit inside a QGraphicsProxyWidget, which is part of a QGraphicsItemGroup. However, I cannot type in the QLineEdit, and it doesn't seem to get the focus when clicked.

Here's a minimal code example illustrating the issue:

import sys
from PySide6.QtCore import Qt, QPointF
from PySide6.QtGui import QBrush, QPen, QPainter, QColor
from PySide6.QtWidgets import (
    QApplication, QGraphicsItem, QGraphicsRectItem,
    QGraphicsScene, QGraphicsView, QVBoxLayout, QWidget, QGraphicsTextItem, QGraphicsLineItem, QGraphicsItemGroup,
    QLineEdit, QGraphicsProxyWidget
)

class CompositeItem(QGraphicsItemGroup):
    def __init__(self, x=0, y=0):
        super().__init__()

        self.setPos(x, y)

        rect = QGraphicsRectItem(0, 0, 100, 50, self)
        rect.setBrush(QBrush(QColor(255, 0, 0, 100)))  # Semi-transparent red
        rect.setPen(QPen(Qt.GlobalColor.black, 2))
        self.addToGroup(rect)

        line = QGraphicsLineItem(0, 0, 100, 100, self)
        line.setPen(QPen(Qt.GlobalColor.green, 2, Qt.PenStyle.DashLine))
        self.addToGroup(line)

        text = QGraphicsTextItem("Test Item", self)
        text.setDefaultTextColor(Qt.GlobalColor.darkMagenta)
        text.setPos(10, -30)  # Position above the other components
        self.addToGroup(text)

        self.line_edit = QLineEdit("Edit me")
        self.line_edit.setFixedWidth(100)
        self.line_edit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)

        self.line_edit_proxy = QGraphicsProxyWidget(self)
        self.line_edit_proxy.setWidget(self.line_edit)
        self.line_edit_proxy.setPos(0, 55)
        self.line_edit_proxy.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        self.line_edit_proxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
        self.addToGroup(self.line_edit_proxy)

        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)

    def mousePressEvent(self, event):
        self.line_edit_proxy.setFocus()  # Set focus when the item is clicked
        super().mousePressEvent(event)

class CustomCanvas(QGraphicsView):
    def __init__(self):
        super().__init__()

        self.scene = QGraphicsScene(0, 0, 600, 400)
        self.setScene(self.scene)
        self.setRenderHint(QPainter.RenderHint.Antialiasing)

        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        self.setInteractive(True)

        self.add_items()

    def add_items(self):
        comp = CompositeItem(100, 100)
        self.scene.addItem(comp)

app = QApplication(sys.argv)
window = QWidget()
layout = QVBoxLayout(window)
canvas = CustomCanvas()
layout.addWidget(canvas)
window.setLayout(layout)
window.show()
sys.exit(app.exec())

The code also had panning, zooming and snap to grid functionalities which I removed to keep it minimal. I think this code should also be runnable with PyQt6 or PyQt5.

I'm trying to set up the QLineEdit so that it gains focus and allows text input when clicked. The whole group should still be movable.

Problem:

I've tried setting focus policies on both the QLineEdit and QGraphicsProxyWidget, and I also set the ItemIsFocusable flag on QGraphicsProxyWidget. However, clicking the QLineEdit does not enable typing or trigger focus, although I see a change in cursor when hovering the QLineEdit.

Questions:


Solution

  • Documentation for QGraphicsItemGroup suggests that you might not need QGraphicsItemGroup for your task:

    If all you want is to store items inside other items, you can use any QGraphicsItem directly by passing a suitable parent to setParentItem().

    When you set parent graphics item, child's coordinate system is tied to parent position, meaning when you move parent you move child as well, they act like a group.

    Here's example of movable rect item with focusable and editable lineedit implemented using this suggestion:

    import sys
    from PySide6.QtCore import Qt
    from PySide6.QtGui import QBrush, QPen, QColor
    from PySide6.QtWidgets import (
        QApplication, QGraphicsItem, QGraphicsRectItem,
        QGraphicsScene, QGraphicsView, QVBoxLayout, QWidget,
        QLineEdit, QGraphicsProxyWidget
    )
    
    app = QApplication(sys.argv)
    window = QWidget()
    layout = QVBoxLayout(window)
    canvas = QGraphicsView()
    scene = QGraphicsScene(0, 0, 600, 400)
    canvas.setScene(scene)
    
    rect = QGraphicsRectItem(0, 0, 100, 50)
    rect.setBrush(QBrush(QColor(255, 0, 0, 100)))
    rect.setPen(QPen(Qt.GlobalColor.black, 2))
    rect.setPos(100, 100)
    rect.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
    
    line_edit = QLineEdit("Edit me")
    line_edit.setFixedWidth(100)
    
    line_edit_proxy = QGraphicsProxyWidget()
    line_edit_proxy.setWidget(line_edit)
    line_edit_proxy.setPos(0, 55)
    line_edit_proxy.setParentItem(rect)
    
    scene.addItem(rect)
    
    layout.addWidget(canvas)
    window.setLayout(layout)
    window.show()
    sys.exit(app.exec())
    

    You can incapsulate subitem creation in a class like this:

    import sys
    from PySide6.QtCore import Qt
    from PySide6.QtGui import QBrush, QPen, QColor
    from PySide6.QtWidgets import (
        QApplication, QGraphicsItem, QGraphicsRectItem,
        QGraphicsScene, QGraphicsView,
        QLineEdit, QGraphicsProxyWidget
    )
    
    class CompositeItem(QGraphicsRectItem):
        def __init__(self):
            super().__init__(0, 0, 100, 50)
            self.setBrush(QBrush(QColor(255, 0, 0, 100)))
            self.setPen(QPen(Qt.GlobalColor.black, 2))
            self.setPos(100, 100)
            self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
            line_edit = QLineEdit("Edit me")
            line_edit.setFixedWidth(100)
            line_edit_proxy = QGraphicsProxyWidget()
            line_edit_proxy.setWidget(line_edit)
            line_edit_proxy.setPos(0, 55)
            line_edit_proxy.setParentItem(self)
            self.line_edit = line_edit
            self.line_edit_proxy = line_edit_proxy
    
    app = QApplication(sys.argv)
    canvas = QGraphicsView()
    scene = QGraphicsScene(0, 0, 600, 400)
    canvas.setScene(scene)
    item = CompositeItem()
    scene.addItem(item)
    canvas.show()
    sys.exit(app.exec())
    

    If you insist on using QGraphicsItemGroup, it's also possible, but there's a catch: QGraphicsItemGroup is probably should forward keyboard and mouse events to it's subitems, but it doesn't. Maybe there's a way to enable it, maybe we should do it ourlselves, like this:

    import sys
    from PySide6.QtCore import Qt, QPointF, QTimer
    from PySide6.QtGui import QBrush, QPen, QPainter, QColor
    from PySide6.QtWidgets import (
        QApplication, QGraphicsItem, QGraphicsRectItem,
        QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QVBoxLayout, QWidget, QGraphicsTextItem, QGraphicsLineItem, QGraphicsItemGroup,
        QLineEdit, QGraphicsProxyWidget
    )
    
    class CompositeItem(QGraphicsItemGroup):
        def __init__(self, x=0, y=0):
            super().__init__()
    
            self.setPos(x, y)
    
            self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable)
            self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
            self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
    
            rect = QGraphicsRectItem(0, 0, 100, 50, self)
            rect.setBrush(QBrush(QColor(255, 0, 0, 100)))
            rect.setPen(QPen(Qt.GlobalColor.black, 2))
            self.addToGroup(rect)
    
            self.line_edit = QLineEdit("Edit me")
            self.line_edit.setFixedWidth(100)
            
            self.line_edit_proxy = QGraphicsProxyWidget(self)
            self.line_edit_proxy.setWidget(self.line_edit)
            self.line_edit_proxy.setPos(0, 55)
            self.addToGroup(self.line_edit_proxy)
    
            self._mouse_in_line_edit = False
    
        def mousePressEvent(self, event):
            rect = self.line_edit_proxy.sceneBoundingRect()
            self._mouse_in_line_edit = rect.contains(event.scenePos())
            if self._mouse_in_line_edit:
                self.line_edit_proxy.mousePressEvent(event)
                self.line_edit.setFocus()
            else:
                super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self._mouse_in_line_edit:
                self.line_edit_proxy.mouseMoveEvent(event)
            else:
                super().mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            if self._mouse_in_line_edit:
                self.line_edit_proxy.mouseReleaseEvent(event)
            else:
                super().mouseReleaseEvent(event)
    
        def keyPressEvent(self, event) -> None:
            if self.line_edit.hasFocus():
                self.line_edit_proxy.keyPressEvent(event)
            else:
                super().keyPressEvent(event)
    
    class CustomCanvas(QGraphicsView):
        def __init__(self):
            super().__init__()
            self.scene = QGraphicsScene(0, 0, 600, 400)
            self.setScene(self.scene)
            self.setRenderHint(QPainter.RenderHint.Antialiasing)
            self.add_items()
    
        def add_items(self):
            comp = CompositeItem(100, 100)
            self.scene.addItem(comp)
    
    app = QApplication(sys.argv)
    canvas = CustomCanvas()
    canvas.show()
    sys.exit(app.exec())