Is there a signal/event I can make use of for the QMenu tear-off?
I have a QMenu subclass in which it has .setTearOffEnabled(True), but I would like to set this tear-off to always be on the top if the user clicks on the tear-off 'bar'.
I am unable to utilise QtCore.Qt.WindowStaysOnTopHint, as this will result in my menu already being in the tear-off state.
For example: if my main tool area is bigger than the tear-off, and I click on my main tool, the tear-off window will be behind it.
In the following code the clicked signal is emitted when the tear off (dotted lines) is pressed:
import sys
from PyQt5 import QtCore, QtWidgets
class Menu(QtWidgets.QMenu):
clicked = QtCore.pyqtSignal()
def mouseReleaseEvent(self, event):
if self.isTearOffEnabled():
tearRect = QtCore.QRect(
0,
0,
self.width(),
self.style().pixelMetric(
QtWidgets.QStyle.PM_MenuTearoffHeight, None, self
),
)
if tearRect.contains(event.pos()):
self.clicked.emit()
QtCore.QTimer.singleShot(0, self.after_clicked)
super(Menu, self).mouseReleaseEvent(event)
#QtCore.pyqtSlot()
def after_clicked(self):
tornPopup = None
for tl in QtWidgets.QApplication.topLevelWidgets():
if tl.metaObject().className() == "QTornOffMenu":
tornPopup = tl
break
if tornPopup is not None:
print("This is the tornPopup: ", tornPopup)
tornPopup.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow(parent=None)
menu = Menu("Menu", tearOffEnabled=True)
menu.clicked.connect(lambda: print("clicked"))
w.menuBar().addMenu(menu)
for i in range(5):
action = QtWidgets.QAction("action{}".format(i), w)
menu.addAction(action)
w.show()
sys.exit(app.exec_())
When a menu is torn off, it is hidden and Qt replaces it with a copy created from an internal subclass of QMenu. So to set the WindowStaysOnTopHint on a torn off menu, you would first need to find a way to get a reference to it. One way to do this would be to set an event-filter on the application object and watch for child events of the right type:
class MenuWatcher(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ChildAdded and
event.child().metaObject().className() == 'QTornOffMenu'):
event.child().setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
This class will operate on all torn-off menus created in the application.
However, if the event-filtering was done by the source menu class, its own torn-off menu could be identified by comparing menu-items:
class Menu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTearOffEnabled(True)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded:
child = event.child()
if (child.metaObject().className() == 'QTornOffMenu' and
all(a is b for a, b in zip(child.actions(), self.actions()))):
child.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
Related
Grееtings аll. I am new to this site, so go easy on me.
I am building a program in python using PyQt5 for the interface. I currently have, among other things, a QListWidget (list window) into which I insert a number of QWidgets (list items) through a function during the running of the program. I have implemented an eventFilter by subclassing QObject, and I use it to differentiate between left and right click, and from that I send to the controller class to handle one or the other click accordingly.
I have made it so that when right-clicked on a list item, a context menu appears. However, I also need a context menu to appear when the list window is clicked (not on a list item). The problem which occurs is that when right-clicked on a list item, both the list item context menu and list window context menu appear. This must be because the event filter recognises the click as occurring within the list window, because it is occurring on a list item, which is within the list window. What I need is that when right-clicked on a list item, only its context menu appears, and similarly for the list window, when right-clicked outside the list items.
I have tried checking if source equals the widget where the event appeared, but it seems to recognise both widgets' events independently and if I gate the call to the handler with an if condition, one of the handlers never receives a call. I have searched around the web and this site, but I have found nothing of use. Perhaps it is due to me not being a native speaker and so not being able to phrase things correctly (you can see how clunky my phrasing is).
Below follows some code extracted from my program. Note that anything irrelevant has been cut away to make for a minimal example. For your convenience, I have also merged the GUI files into one and did the same for the control files. I have tested this minimal example and it reproduces the problem. It could not get smaller, so if you deem the code listed below too long, notify me and I can reupload it to GitHub if it is allowed to show the minimal example that way instead of putting code into the question directly.
custom_classes.py:
from PyQt5.QtCore import Qt, QEvent, QObject
class MyFilter(QObject):
def __init__(self, parent, ctrl):
super().__init__(parent)
self._parent = parent
self.ctrl = ctrl
self.parent.installEventFilter(self)
#property
def parent(self):
return self._parent
def eventFilter(self, source, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton:
self.ctrl.handle_left_click()
elif event.button() == Qt.RightButton:
self.ctrl.handle_right_click(event)
return super().eventFilter(source, event)
gui.py:
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QListWidget
from PyQt5.QtWidgets import QScrollArea
from PyQt5.QtCore import Qt
class MainFrame(QWidget):
def __init__(self):
super().__init__()
self.main_layout = QHBoxLayout()
self.setLayout(self.main_layout)
class ListItemFrame(QWidget):
def __init__(self):
super().__init__()
self.main = QHBoxLayout()
self.main.setContentsMargins(0,0,0,0)
self.name_layout = QHBoxLayout()
self.name_layout.setContentsMargins(0,0,0,0)
self.main.addLayout(self.name_layout)
self.name = QLabel("")
self.name.setMaximumHeight(20)
self.name_layout.addWidget(self.name)
self.setLayout(self.main)
class ListFrame(QListWidget):
def __init__(self):
super().__init__()
self.main = QVBoxLayout()
self.scroll_widget = QScrollArea()
self.scroll_widget.setWidgetResizable(True)
self.scroll_layout = QVBoxLayout()
self.scroll_layout.setAlignment(Qt.AlignTop)
self.scroll_layout_widget = QWidget()
self.scroll_layout_widget.setLayout(self.scroll_layout)
self.scroll_widget.setWidget(self.scroll_layout_widget)
self.main.addWidget(self.scroll_widget)
self.setLayout(self.main)
ctrl.py:
from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
class Controller:
def __init__(self, ui, app):
self.ui = ui
self.app = app
self.list_ = ListControl(self)
class ListControl:
def __init__(self, ctrl):
self.ctrl = ctrl
self.ui = ListFrame()
self.the_list = self.get_list() #list of stuff
self.item_list = [] #list of list items
self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
self.index = self.ctrl.ui.main_page.main_layout.count() - 1
self.filter = MyFilter(self.ui, self)
self.show_list()
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
one_action = menu.addAction("Something!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.ctrl.ui.close()
elif action == one_action:
self.something()
def something(self):
print("Something!")
def show_list(self):
for info in self.the_list:
item = ListItem(self, info)
self.item_list.append(item)
def get_list(self):
return [x for x in "qwertzuiopasdfghjklyxcvbnm"]
class ListItem:
def __init__(self, main, info):
self.main = main
self.info = info*10
self.ui = ListItemFrame()
self.filter = MyFilter(self.ui, self)
self.set_ui()
self.add_to_ui()
self.main.ui.scroll_layout.addWidget(self.ui)
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
item_action = menu.addAction("Hello!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.main.ctrl.ui.close()
elif action == item_action:
self.hello()
def hello(self):
print(f"Hello! I am {self.info}")
def set_ui(self):
self.ui.name.setText(self.info)
def add_to_ui(self):
self.main.ui.scroll_layout.insertWidget(
self.main.ui.scroll_layout.count() - 1, self.ui
)
main.py:
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QStackedLayout
from PyQt5.QtWidgets import QWidget
from gui import MainFrame
from ctrl import Controller
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("minimal example")
self.stacked = QStackedLayout()
self.main_page = MainFrame()
self.stacked.addWidget(self.main_page)
self.setLayout(self.stacked)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = Window()
window.show()
c = Controller(window, app)
sys.exit(app.exec())
To reiterate, the context menu appears for both the list item and the list window when a list item is right-clicked. What I need is for it to appear only for the list item if a list item is right-clicked.
Edit: seems the site bit off a part of my introduction. Readded it!
this is probably not the best way to do it but it works. You just create a global variable, for example list_element_clicked and when you click "hello" (of course not quit because you are going to exit the window and there is no point) you set that variable to True. If that variable is False, you show the ListControl context menu, and if not, you set that variable to True, so next time if you click on your ListControl it will appear, and if you click on ListItem it will not.
Finally there is an extra case, if you don't click anywhere after clicking on ListItem, nothing will happen (the ListControl is not shown and the variable is not changed) so everything will work perfectly next time.
So here is the code:
ctrl.py:
from PyQt5.QtWidgets import QMenu
from gui import ListFrame, ListItemFrame
from custom_classes import MyFilter
list_element_clicked = False
class Controller:
def __init__(self, ui, app):
self.ui = ui
self.app = app
self.list_ = ListControl(self)
class ListControl:
def __init__(self, ctrl):
self.ctrl = ctrl
self.ui = ListFrame()
self.the_list = self.get_list() #list of stuff
self.item_list = [] #list of list items
self.ctrl.ui.main_page.main_layout.addWidget(self.ui)
self.index = self.ctrl.ui.main_page.main_layout.count() - 1
self.filter = MyFilter(self.ui, self)
self.show_list()
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
global list_element_clicked
if(list_element_clicked == False):
self.show_options(event)
else:
list_element_clicked = False
def show_options(self, event):
menu = QMenu()
one_action = menu.addAction("Something!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.ctrl.ui.close()
elif action == one_action:
self.something()
def something(self):
print("Something!")
def show_list(self):
for info in self.the_list:
item = ListItem(self, info)
self.item_list.append(item)
def get_list(self):
return [x for x in "qwertzuiopasdfghjklyxcvbnm"]
class ListItem:
def __init__(self, main, info):
self.main = main
self.info = info*10
self.ui = ListItemFrame()
self.filter = MyFilter(self.ui, self)
self.set_ui()
self.add_to_ui()
self.main.ui.scroll_layout.addWidget(self.ui)
def handle_left_click(self):
pass #other irrelevant function
def handle_right_click(self, event):
self.show_options(event)
def show_options(self, event):
menu = QMenu()
item_action = menu.addAction("Hello!")
quit_action = menu.addAction("Quit")
action = menu.exec_(self.ui.mapToGlobal(event.pos()))
if action == quit_action:
self.main.ctrl.ui.close()
elif action == item_action:
global list_element_clicked
list_element_clicked = True
self.hello()
def hello(self):
print(f"Hello! I am {self.info}")
def set_ui(self):
self.ui.name.setText(self.info)
def add_to_ui(self):
self.main.ui.scroll_layout.insertWidget(
self.main.ui.scroll_layout.count() - 1, self.ui
)
I would like to make a spin-box which only is editable after double-clicking in the digit display area.
My attempt below disables the focus in all cases except when the increment/decrement buttons are pressed.
I want increment/decrement to perform the actions without stealing the focus.
I do want the the normal blinking cursor and edit functionality when the text area is double-clicked.
After editing, the widget should release focus when another widget is clicked, or enter is pressed.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
event_dict = {v: k for k, v in QtCore.QEvent.__dict__.items() if isinstance(v, int)}
noisy_events = [
'Paint',
'Show',
'Move',
'Resize',
'DynamicPropertyChange',
'PolishRequest',
'Polish',
'ChildPolished',
'HoverMove',
'HoverEnter',
'HoverLeave',
'ChildAdded',
'ChildRemoved',
]
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
if a1.type() == QtCore.QEvent.FocusAboutToChange:
print("intercepted focus about to change")
return True
if a1.type() == QtCore.QEvent.FocusIn:
print("intercepted focus in")
return True
if a1.type() == QtCore.QEvent.MouseButtonPress:
print("intercepted mouse press")
#return True
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
print("intercepted double click")
self.setFocus()
else:
if a1.type() in event_dict:
evt_name = event_dict[a1.type()]
if evt_name not in noisy_events:
print(evt_name)
else:
pass
#print(f"Unknown event type {a1.type()}")
return super().eventFilter(a0, a1)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QtWidgets.QWidget()
l = QtWidgets.QHBoxLayout()
l.addWidget(ClickableSpinBox())
l.addWidget(ClickableSpinBox())
l.addWidget(QtWidgets.QDoubleSpinBox())
w.setLayout(l)
w.show()
app.exec_()
Edit:
To let the mouse-scroll function and the increase/decrease buttons working
I make the QLineEdit inside of the QDoubleSpinBox to be enabled/disabled when you double click inside it or in the borders of the SpinBox. With this, you can still change the value inside it with the mouse-scroll or with the buttons. Here is your code modified:
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.lineEdit().setEnabled(False)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
## When double clicked inside the Disabled QLineEdit of
## the SpinBox, this will enable it and set the focus on it
self.lineEdit().setEnabled(True)
self.setFocus()
elif a1.type() == QtCore.QEvent.FocusOut:
## When you lose the focus, e.g. you click on other object
## this will diable the QLineEdit
self.lineEdit().setEnabled(False)
elif a1.type() == QtCore.QEvent.KeyPress:
## When you press the Enter Button (Return) or the
## Key Pad Enter (Enter) you will disable the QLineEdit
if a1.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.lineEdit().setEnabled(False)
return super().eventFilter(a0, a1)
def stepBy(self, steps):
## The reason of this is because if you click two consecutive times
## in any of the two buttons, the object will trigger the DoubleClick
## event.
self.lineEdit().setEnabled(False)
super().stepBy(steps)
self.lineEdit().deselect()
The result with the QLineEdit disabled and the buttons enabled:
To let ONLY the mouse-scroll function
You just have to remove the buttons from the code above, using setButtonSymbols().
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.lineEdit().setEnabled(False)
self.setFocusPolicy(Qt.NoFocus)
## Changing button's symbol to 2 means to "delete" the buttons
self.setButtonSymbols(2)
The result with the buttons "disabled":
Previous Answer (before the Edit)
I have a tricky solution, and it consists of Enable/Disable the customs spin boxes you created. With this, the spinboxes will be enabled (and editable) only when you double-clicked on them, and when you lose focus on them they will be disabled automatically, passing the focus to the enabled SpinBox.
The reason I did that is that when the SpinBox is enabled, the DoubleClick event will only be triggered when you double click on the borders or on the increment/decrement buttons. Disabling them will do the trick because the double click event will be triggered wherever you press inside the SpinBox.
Here is your code with my modifications: (there are comments inside te code to help you understand what I did)
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
## When you double click inside the Disabled SpinBox
## this will enable it and set the focus on it
self.setEnabled(True)
self.setFocus()
elif a1.type() == QtCore.QEvent.FocusOut:
## When you lose the focus, e.g. you click on other object
## this will disable the SpinBox
self.setEnabled(False)
elif a1.type() == QtCore.QEvent.KeyPress:
## When you press the Enter Button (Return) or the
## Key Pad Enter (Enter) you will disable the SpinBox
if a1.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.setEnabled(False)
return super().eventFilter(a0, a1)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QtWidgets.QWidget()
l = QtWidgets.QHBoxLayout()
## I store the SpinBoxes to give the disable property after
## generating its instance
sp1 = ClickableSpinBox()
sp1.setEnabled(False)
sp2 = ClickableSpinBox()
sp2.setEnabled(False)
sp3 = QtWidgets.QDoubleSpinBox()
l.addWidget(sp1)
l.addWidget(sp2)
l.addWidget(sp3)
w.setLayout(l)
w.show()
app.exec_()
And some screenshots of that code running:
The demo script below should do everything you want. I have added two extra features: (1) disabling of text selection, and (2) disabling of mouse-wheel increments on the text-box (but not the buttons). If these aren't to your taste, they can easily be adapted or removed (see the comments in the code). The implementation is otherwise very simple, since it does not rely on controlling the focus.
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ClickableSpinBox(QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setEditingDisabled(True)
self.lineEdit().installEventFilter(self)
self.editingFinished.connect(self.setEditingDisabled)
def editingDisabled(self):
return self.lineEdit().isReadOnly()
def setEditingDisabled(self, disable=True):
self.lineEdit().setReadOnly(disable)
self.setFocusPolicy(Qt.TabFocus if disable else Qt.WheelFocus)
# optional: control selection in text-box
if disable:
self.clearSelection()
self.lineEdit().selectionChanged.connect(self.clearSelection)
else:
self.lineEdit().selectionChanged.disconnect(self.clearSelection)
self.lineEdit().selectAll()
def clearSelection(self):
self.lineEdit().setSelection(0, 0)
def eventFilter(self, source, event):
if (event.type() == QEvent.MouseButtonDblClick and
source is self.lineEdit() and self.editingDisabled()):
self.setEditingDisabled(False)
self.setFocus()
return True
return super().eventFilter(source, event)
# optional: control mouse-wheel events in text-box
def wheelEvent(self, event):
if self.editingDisabled():
self.ensurePolished()
options = QStyleOptionSpinBox()
self.initStyleOption(options)
rect = self.style().subControlRect(
QStyle.CC_SpinBox, options,
QStyle.SC_SpinBoxUp, self)
if event.pos().x() <= rect.left():
return
super().wheelEvent(event)
def keyPressEvent(self, event):
if not self.editingDisabled():
super().keyPressEvent(event)
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
self.spinboxA = ClickableSpinBox()
self.spinboxB = ClickableSpinBox()
self.spinboxC = QDoubleSpinBox()
layout.addWidget(self.spinboxA)
layout.addWidget(self.spinboxB)
layout.addWidget(self.spinboxC)
self.setFocusPolicy(Qt.ClickFocus)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.setGeometry(900, 100, 200, 100)
window.show()
sys.exit(app.exec_())
My actual application is much more complicated than this, but the example below sums up the majority of my problem. I have multiple QLabels that I've subclassed to make them clickable. The labels display 16x16 images which requires a process of loading the images via Pillow, converting them to ImageQt objects, and then setting the pixmap of the label. In the example, I have 3 clickable QLabels that run the print_something function each time I click on them. My goal is to be able to hold the mouse down, and for each label I hover over, the function gets called. Any pointers would be great.
from PyQt5 import QtCore, QtWidgets, QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
import sys
class ClickableLabel(QtWidgets.QLabel):
def __init__(self):
super().__init__()
clicked = QtCore.pyqtSignal()
def mousePressEvent(self, ev):
if app.mouseButtons() & QtCore.Qt.LeftButton:
self.clicked.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central_widget = QtWidgets.QWidget()
self.setFixedSize(300, 300)
image = Image.open("16x16image.png")
image_imageqt = ImageQt(image)
hbox = QtWidgets.QHBoxLayout()
hbox.setSpacing(0)
hbox.addStretch()
label01 = ClickableLabel()
label01.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label01.clicked.connect(self.print_something)
hbox.addWidget(label01)
label02 = ClickableLabel()
label02.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label02.clicked.connect(self.print_something)
hbox.addWidget(label02)
label03 = ClickableLabel()
label03.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label03.clicked.connect(self.print_something)
hbox.addWidget(label03)
hbox.addStretch()
central_widget.setLayout(hbox)
self.setCentralWidget(central_widget)
def print_something(self):
print("Printing something..")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
The cause of the problem is stated in the docs for QMouseEvent:
Qt automatically grabs the mouse when a mouse button is pressed inside
a widget; the widget will continue to receive mouse events until the
last mouse button is released.
It does not look like there is a simple way around this, so something hackish will be required. One idea is to initiate a fake drag and then use dragEnterEvent instead of enterEvent. Something like this should probably work:
class ClickableLabel(QtWidgets.QLabel):
clicked = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.dragstart = None
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.dragstart = event.pos()
self.clicked.emit()
def mouseReleaseEvent(self, event):
self.dragstart = None
def mouseMoveEvent(self, event):
if (self.dragstart is not None and
event.buttons() & QtCore.Qt.LeftButton and
(event.pos() - self.dragstart).manhattanLength() >
QtWidgets.qApp.startDragDistance()):
self.dragstart = None
drag = QtGui.QDrag(self)
drag.setMimeData(QtCore.QMimeData())
drag.exec_(QtCore.Qt.LinkAction)
def dragEnterEvent(self, event):
event.acceptProposedAction()
if event.source() is not self:
self.clicked.emit()
I have a class BooleanButton contains extra boolean flag to toggle every click. After each click, I want it to emit a signal and the slot will receive the boolean flag. I just write the following, but of course, it won't work.
class BooleanButton(QPushButton):
def __init__(self, name):
QPushButton.__init__(self, name)
self.bool = False
def clicked(self, bool):
self.bool = not self.bool
self.emit(self.bool)
After creating the object, it connects to a slot. When I click this button, a swapping true-false signal will send to the slot.
bool_btn.isclicked[bool].connect(widget.func)
Thanks.
First, don't call a method clicked, that will hide the buttons clicked signal.
If you want to define a new signal, you need to do so using QtCore.pyqtSignal, then you can connect the clicked singal to a slot that will in turn emit your custom signal. Example:
class BooleanButton(QPushButton):
isclicked = pyqtSignal(bool)
def __init__(self, name):
QPushButton.__init__(self, name)
self.bool = False
self.clicked.connect(self.on_clicked)
def on_clicked(self, bool):
self.bool = not self.bool
self.isclicked.emit(self.bool)
As three_pineapples said, QPushButton comes with this feature built-in. Here's a simple example illustrating this behaviour.
from PyQt4 import QtGui, QtCore
class MyWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
self.button = QtGui.QPushButton("Click me", self)
self.button.setCheckable(True)
self.lineEdit = QtGui.QLineEdit(self)
self.button.clicked.connect(self.onClicked)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button)
layout.addWidget(self.lineEdit)
def onClicked(self, checked):
if checked:
self.lineEdit.setText("Button checked")
else:
self.lineEdit.setText("Button unchecked")
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
So your BooleanButton is actually just a QPushButton.
I did something similar to the answer here.
The GUI is simple, you click a button which starts a thread that instead of emitting signals it sends events. The events cause a label to change text.
Here's the code:
from PySide.QtGui import *
from PySide.QtCore import *
import sys, time
class MyEvent(QEvent):
def __init__(self, message):
super().__init__(QEvent.User)
self.message = message
class MyThread(QThread):
def __init__(self, widget):
super().__init__()
self._widget = widget
def run(self):
for i in range(10):
app.sendEvent(self._widget, MyEvent("Hello, %s!" % i))
time.sleep(.1)
class MyReceiver(QWidget):
def __init__(self, parent=None):
super().__init__()
layout = QHBoxLayout()
self.label = QLabel('Test!')
start_button = QPushButton('Start thread')
start_button.clicked.connect(self.startThread)
layout.addWidget(self.label)
layout.addWidget(start_button)
self.setLayout(layout)
def event(self, event):
if event.type() == QEvent.User:
self.label.setText(event.message)
return True
return False
def startThread(self):
self.thread = MyThread(self)
self.thread.start()
app = QApplication(sys.argv)
main = MyReceiver()
main.show()
sys.exit(app.exec_())
The problem is that, only the first event get processed by MyReceiver, then the widget freezes!.
Any clues?. Thanks
The behaviour of the event method of QWidget is altered in your code: you should
let the base class decide on what to do with events, not returning False if it is
not a custom event. Do it like this:
def event(self, event):
if event.type() == QEvent.User:
self.label.setText(event.message)
return True
return QWidget.event(self, event)
This fixes your problem.
Also, you may prefer to emit a Qt signal from the thread, and have it connected in
your widget to some method to change the label - signals and slots are thread-safe
in Qt 4 (contrary to Qt 3). It will achieve the same result.