Issue
Im trying to create a simple Pyqt app that has a single button and a QVBoxLayout. When a user clicks on the button, I want it to add a row (QHBoxlayout) that has multiple widgets in it like QLineedit, QLabel, and a QButton. This way users can add multiple rows. Once the user clicks on any of those rows, i want the whole row to change its border color so it will let the user know on which row he stands.
I trying the following code:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QHBoxLayout, QLineEdit
from PyQt5 import QtCore
class MyHBoxLayout(QWidget):
selected = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.setStyleSheet("border: 1px solid black;")
self.line_edit = QLineEdit()
self.button = QPushButton('Remove')
self.layout.addWidget(self.line_edit)
self.layout.addWidget(self.button)
self.button.clicked.connect(self.removeMe)
self.line_edit.installEventFilter(self)
def removeMe(self):
self.setParent(None)
self.deleteLater()
def mousePressEvent(self, event):
self.selected.emit()
def eventFilter(self, source, event):
if source == self.line_edit and event.type() == QtCore.QEvent.MouseButtonPress:
self.selected.emit()
return super().eventFilter(source, event)
class MyApp(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Highlight Selected QHBoxLayout')
self.setGeometry(100, 100, 400, 300)
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
central_layout = QVBoxLayout()
central_widget.setLayout(central_layout)
add_button = QPushButton('Add QHBoxLayout', self)
central_layout.addWidget(add_button)
add_button.clicked.connect(self.addHBoxLayout)
self.container_widget = QWidget()
container_layout = QVBoxLayout()
self.container_widget.setLayout(container_layout)
central_layout.addWidget(self.container_widget)
self.selected_hbox = None
def addHBoxLayout(self):
hbox = MyHBoxLayout()
container_layout = self.container_widget.layout()
container_layout.addWidget(hbox)
hbox.selected.connect(self.onHBoxLayoutSelected)
def onHBoxLayoutSelected(self):
sender = self.sender()
if self.selected_hbox:
self.selected_hbox.setStyleSheet("border: 2px solid black;")
sender.setStyleSheet("border: 2px solid blue;")
self.selected_hbox = sender
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
But it changes the border of all of the widgets separately and not the whole row.
How can I fix it?
(I have another bug that crashes the app once I click on a QHBoxlayout after one row was removed but I focus on the border color at this moment).
Thank you
Solution
First of all, you are not drawing the border of a QHBoxLayout, since layout managers are not widgets and don't paint anything. What you're attempting to do is to draw the border of the container, but your QSS syntax is wrong, since you're setting a generic property, which will apply the border for both the target widget and its children.
It is also wrong to set generic properties on containers or complex widgets, because some widgets need specific subcontrol properties set, and if you don't do that you'll end up with the basic primitive style (based on the "windows" style) which is quite ugly and often doesn't properly draw the widget as expected when there are missing properties.
Furthermore, you don't see the border of the container because, by default, QWidget subclasses do not draw anything set by stylesheets, unless paintEvent()
is properly implemented or the WA_StyledBackground
attribute is set.
The first thing to do is to use proper selector types in the QSS, and set the attribute as explained above:
self.setStyleSheet("MyHBoxLayout { border: 1px solid black; }")
self.setAttribute(Qt.WA_StyledBackground)
The above will show the border for MyHBoxLayout
instances alone, not its children.
Then, if you want to switch color, you need to use the same syntax, but a better approach is to do that in a dedicated function of MyHBoxLayout
, not externally:
class MyHBoxLayout(QWidget):
...
def setSelected(self, selected):
color = 'blue' if selected else 'black'
self.setStyleSheet(
"MyHBoxLayout {{ border: 1px solid {}; }}".format(color))
class MyApp(QMainWindow):
...
def onHBoxLayoutSelected(self):
sender = self.sender()
if sender == self.selected_hbox:
# already selected, no need to go further
return
if self.selected_hbox:
self.selected_hbox.setSelected(False)
sender.setSelected(True)
self.selected_hbox = sender
Note that the deletion of the widget itself is not enough. You must remember that PyQt is a Python binding around the C++ Qt library, so the objects used in Python are just wrappers around Qt objects, and deleting a Qt object does not delete its python reference.
Your program crashes because the Qt object doesn't exist anymore (run your program in a terminal or prompt and you'll see the full error), so any attempt to access it results in a memory access error.
In order to avoid such problems, you could use a simple try/except
block:
if self.selected_hbox:
try:
self.selected_hbox.setSelected(False)
except RuntimeError:
pass
A better approach, though, is to use a further signal before deleting the widget:
class MyHBoxLayout(QWidget):
selected = pyqtSignal()
aboutToBeRemoved = pyqtSignal()
...
def removeMe(self):
self.aboutToBeRemoved.emit()
# no need to call `setParent(None)` if you're going to delete the widget
self.deleteLater()
class MyApp(QMainWindow):
...
def addHBoxLayout(self):
...
hbox.aboutToBeRemoved.connect(self.onHBoxRemove)
def onHBoxRemove(self):
if self.sender() is self.selected_hbox:
self.selected_hbox = None
Note that the code above will show a border for a larger area than required, and that's because the default addWidget()
function always tries to make the added widget occupy all available space in the layout item.
To prevent that, you have two options:
- add the
alignment
argument:
container_layout.addWidget(hbox, alignment=Qt.AlignVCenter)
- set a proper size policy for the container:
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
Finally, you're only considering mouse clicks to show a "focused" row, but that doesn't consider keyboard navigation.
The correct way to do what you want is to check for FocusIn
event types.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.