I'm trying to select/unselect Qcheckbox by clicking on a label and moving the mouse on other Checkboxes
What I would like is, for example, to click on the '0' and maintaining the mouse clicked and move it down on the '1', '2'... by moving on those checkboxes they must change their value (True to False).
I don't understand how to use the mouseMoveEvent
.
I made a minimal code to start with
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class CheckBox(QCheckBox):
def __init__(self, *args, **kwargs):
QCheckBox.__init__(self, *args, **kwargs)
def mouseMoveEvent(self,event):
if event.MouseButtonPress == Qt.MouseButton.LeftButton:
self.setChecked(not self.isChecked())
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__()
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.mainHBOX = QVBoxLayout()
self.CB_list = []
for i in range(20):
CB = CheckBox(str(i))
CB.setChecked(True)
self.CB_list.append(CB)
self.mainHBOX.addWidget(CB)
self.centralWidget.setLayout(self.mainHBOX)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
There are two problems in your code.
First of all, when a widget receives a mouse button press, it normally becomes the "mouse grabber", and from that moment on only that widget will receive mouse move events until the button is released.
Then you didn't properly check the buttons: event.MouseButtonPress
is a constant that indicates a mouse press event, Qt.MouseButton.LeftButton
is another constant indicating the left button of the mouse; you practically compared two different constants, which would have the same result as in doing if 2 == 1:
.
A mouseMoveEvent
will always return event.type() == event.MouseMove
, so there's no need to check for it, but for the current buttons: for mouse press and release events, the button that causes the event is returned by event.button()
, while, for move events, the currently pressed buttons are returned by event.buttons()
(plural). Remember this difference, because event.button()
(singular) will always return NoButton
for a mouse move events (which is pretty obvious if you think about it, since the event is not generated by a button).
With that in mind, what you could do is to check the child widgets of the parent corresponding to the mouse position mapped to it.
In order to achieve this, you have to ensure that the mouse button has been actually pressed on the label (using style functions) and set an instance attribute as a flag to know if the movement should be tracked for other siblings. Note that in the following example I also used another flag to check for children that are direct descendants of the parent: if that flag is set, only siblings of the first pressed checkbox will be toggled, if another CheckBox
instance is found but its parent is a child of the first one, no action is performed.
class CheckBox(QCheckBox):
maybeDrag = False
directChildren = True
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
opt = QStyleOptionButton()
self.initStyleOption(opt)
rect = self.style().subElementRect(QStyle.SE_CheckBoxContents, opt, self)
if event.pos() in rect:
self.setChecked(not self.isChecked())
self.maybeDrag = True
# we do *not* want to call the default behavior here
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.maybeDrag:
parent = self.parent()
child = parent.childAt(self.mapToParent(event.pos()))
if isinstance(child, CheckBox) and child != self:
# with the directChildren flag set, we will only toggle CheckBox
# instances that are direct children of this (siblings),
# otherwise we toggle them anyway
if not self.directChildren or child.parent() == parent:
child.setChecked(self.isChecked())
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.maybeDrag:
self.maybeDrag = False
self.setDown(False)
else:
super().mouseReleaseEvent(event)
The only drawback with this implementation is that it won't work if you need to do this for items that are children of other parents (they share a common ancestor, but not the same parent).
In that case, you need to install an event filter for all check boxes for which you need this behavior and implement the above functions accordingly, with the difference that you need to use the top level window to map mouse coordinates (self.mapTo(self.window(), event.pos())
) and use that window's childAt()
.
Finally, consider that SE_CheckBoxContents
returns the full area of the contents, even if the shown text is smaller than the available space; the default behavior with most styles is to react to click events only when done inside the actual shown contents (the icon and/or the bounding rect of the text). If you want to revert to the default behavior, you need to construct another rectangle for SE_CheckBoxClickRect
(which is the whole clickable area including the indicator) and check if the mouse is within the intersected rectangle of both:
contents = self.style().subElementRect(QStyle.SE_CheckBoxContents, opt, self)
click = self.style().subElementRect(QStyle.SE_CheckBoxClickRect, opt, self)
if event.pos() in (contents & click):
# ...
The &
binary operator for QRects works in the same way as bitwise operators, in their logical sense: it only returns a rectangle that is contained by both source rectangles. It's literal function is intersected()
.