PyQt Keypress signal to multiple slots in different classes - python

I have a custom table class, where I want to tie the Return key to advancing the edit focus to the next row, as follows:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class App(QWidget):
def __init__(self):
super().__init__()
# self.shortcut = QShortcut(QKeySequence('Return'), self)
# self.shortcut.activated.connect(self.plotter)
table = Table(1, 1)
layout = QHBoxLayout()
layout.addWidget(table)
self.setLayout(layout)
self.show()
def plotter(self):
print('I am plotting')
class Table(QTableWidget):
def __init__(self, rows, cols):
super().__init__(1, 1)
self.shortcut = QShortcut(QKeySequence('Return'), self)
self.shortcut.activated.connect(self.advance)
def advance(self):
print('I am advancing')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = App()
sys.exit(app.exec_())
Which works fine. But I also want to tie that Return key to a plotting routine in the application in which the table resides. The net effect of this would be hitting the Return key to accept the table edit and advance the table cell focus, then updating the plot determined by those table values.
Implementing the above code in the same fashion for the plotting routine in the main application class also works well. But I can't implement them both at the same time. When I tie the Return key to two separate slots in two separate classes, nothing happens when I hit Return. Any ideas? Thanks.

The activated signal does not trigger since the sequence is ambiguous as the docs points out, so you should use the activatedAmbiguously signal
void QShortcut::activatedAmbiguously() When a key sequence is
being typed at the keyboard, it is said to be ambiguous as long as it
matches the start of more than one shortcut.
When a shortcut's key sequence is completed, activatedAmbiguously() is
emitted if the key sequence is still ambiguous (i.e., it is the start
of one or more other shortcuts). The activated() signal is not emitted
in this case.
See also activated().
(emphasis mine)
But unfortunately it will not work since it shoots one at a time, that is to say first the shortcut of the parent(QWidget) and then the shortcut of the children(QTableWidget), who does not meet your requirement.
So in this case a possible solution is to connect the same signal to several slots:
class App(QWidget):
def __init__(self):
super().__init__()
table = Table(1, 1)
layout = QHBoxLayout(self)
layout.addWidget(table)
self.show()
self.shortcut = QShortcut(QKeySequence("Return"), self)
self.shortcut.activated.connect(self.plotter)
self.shortcut.activated.connect(table.advance)
def plotter(self):
print("I am plotting")
class Table(QTableWidget):
def __init__(self, rows, cols):
super().__init__(1, 1)
def advance(self):
print("I am advancing")
Another alternative is to use an eventFilter instead of the QShortcut, since the eventfilter will not consume the event as the QShorcut so all the filters will be notified although it may be inconvenient that the widget does not have the focus and therefore does not receive the event of the keyboard:
class ReturnListener(QObject):
pressed = pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, o, e):
if self.widget is o and e.type() == QEvent.KeyPress:
if e.key() == Qt.Key_Return:
self.pressed.emit()
return super().eventFilter(o, e)
class App(QWidget):
def __init__(self):
super().__init__()
table = Table(1, 1)
layout = QHBoxLayout(self)
layout.addWidget(table)
listener = ReturnListener(self)
listener.pressed.connect(self.plotter)
self.show()
def plotter(self):
print("I am plotting")
class Table(QTableWidget):
def __init__(self, rows, cols):
super().__init__(1, 1)
listener = ReturnListener(self)
listener.pressed.connect(self.advance)
def advance(self):
print("I am advancing")
In conclusion, the best solution is to only have a QShorcut connecting all the slots to the same signal.

Related

How should I differentiate between whether a click happened in a QListWidget or a QWidget that is within it?

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
)

Variables contained in a QToolTip not updating automatically

I have a QToolTip on a QLineEdit and the tooltip contains variables in the text. The tooltip code is contained in the init. The problem is that the variable values in the tooltip do not update automatically when they are changed in the operation of the program. For example, I hover over the line edit and values appear in the tooltip. I change the program, go back to the line edit, and variables in the tooltip have not changed.
I can fix the issue by moving the .setToolTip to a function and calling the function EACH time ANYTHING is changed in the program, but that seems like overkill, especially when 99% of the program changes have nothing to do with this particular tooltip).
Are variables supposed to update automatically? Here is the tooltip setup code contained in the init.
self.ui.YourSSAmount.setToolTip(
'<span>Click Reports/Social Security to see your<br>SS income at each start age'
'<br><br>Your inf adj FRA amt at age {}: ${:,.0f}'
'<br>Age adjustment: {:.0f}%'
'<br>SS Income at age {}: ${:,.0f}</span>'.format(
self.generator.YouSSStartAge, self.generator.your_new_FRA_amt,
self.generator.SS66.get(self.generator.YouSSStartAge, 1.32) * 100, self.generator.YouSSStartAge,
self.generator.YourSSAmount))
The setToolTip method takes the text and stores it, and is not notified if any of the variables used to form the text change.
Given this there are 2 possible solutions:
Update the tooltip every time a variable changes:
from PyQt5 import QtCore, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.le = QtWidgets.QLineEdit()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.le)
self.foo = QtCore.QDateTime.currentDateTime().toString()
self.update_tooltip()
timer = QtCore.QTimer(self, timeout=self.on_timeout)
timer.start()
def on_timeout(self):
self.foo = QtCore.QDateTime.currentDateTime().toString()
# every time any variable used to build the tooltip changes
# then the text of the tooltip must be updated
self.update_tooltip()
def update_tooltip(self):
# update tooltip text
self.setToolTip("dt: {}".format(self.foo))
if __name__ == "__main__":
app = QtWidgets.QApplication([])
w = Widget()
w.show()
app.exec_()
Override the toolTip to take the text using the variables:
from PyQt5 import QtCore, QtWidgets
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self._foo = ""
#property
def foo(self):
return self._foo
#foo.setter
def foo(self, foo):
self._foo = foo
def event(self, e):
if e.type() == QtCore.QEvent.ToolTip:
text = "dt:{}".format(self.foo)
QtWidgets.QToolTip.showText(e.globalPos(), text, self, QtCore.QRect(), -1)
e.accept()
return True
return super().event(e)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.le = LineEdit()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.le)
self.le.foo = QtCore.QDateTime.currentDateTime().toString()
timer = QtCore.QTimer(self, timeout=self.on_timeout)
timer.start()
def on_timeout(self):
self.le.foo = QtCore.QDateTime.currentDateTime().toString()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
w = Widget()
w.show()
app.exec_()

Adding tools to Scribble app with PYQT4

I am trying to write an app which works as MS Paint or other simple graphics editor. My goal is to provide such funcionality as: different tools( drawing lines, figures etc.), posibility to change colors, width, size and undo/redo function. I thought that the easiest way to do it was to create separete classes for drawing and viewing objects. Unfortunetly, it is not working properly. Here`s my code:
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.update()
self.view = View(self)
self.action = Action(self.view.scene)
self.UI()
def UI(self):
self.setGeometry(300,300,300,300)
self.setWindowTitle('Pen styles')
self.undo = QtGui.QPushButton("undo", self)
self.undo.clicked.connect(self.action.undoStack.undo)
self.redo = QtGui.QPushButton("redo", self)
self.redo.clicked.connect(self.action.undoStack.redo)
self.redo.move(0,50)
self.tool = QtGui.QPushButton("tool", self)
self.tool.setCheckable(True)
self.tool.clicked[bool].connect(self.new)
self.tool.move(0,100)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.view)
self.setLayout(layout)
self.show()
def new(self):
pass
class Action(QtGui.QGraphicsScene):
def __init__(self, scene):
QtGui.QGraphicsScene.__init__(self)
self.undoStack = QtGui.QUndoStack(self)
self.scene = scene
self.scene.addLine(0,0,150,150)
self.undoStack = QtGui.QUndoStack(self)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.start = event.pos()
self.scene.addLine(0,0,150,250)
def mouseReleaseEvent(self, event):
start = QtCore.QPointF(self.mapToScene(self.start))
end = QtCore.QPointF(self.mapToScene(event.pos()))
self.drawing(start, end)
def drawing(self, start, end):
self.line = QtGui.QGraphicsLineItem(QtCore.QLineF(start, end))
self.line.setPen(QtGui.QPen(QtCore.Qt.blue,3,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
self.scene.addLine(0,0,150,300)
command = Draw(self.line, self.scene)
self.undoStack.push(command)
class Draw(QtGui.QUndoCommand):
def __init__(self, line, scene):
QtGui.QUndoCommand.__init__(self)
self.line = line
self.scene = scene
def redo(self):
self.scene.addItem(self.line)
def undo(self):
self.scene.removeItem(self.line)
class View(QtGui.QGraphicsView):
def __init__(self, parent):
QtGui.QGraphicsView.__init__(self, parent)
self.scene = QtGui.QGraphicsScene()
self.action = Action(self.scene)
self.setScene(self.scene)
self.setSceneRect(QtCore.QRectF(self.viewport().rect()))
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
My problem is that i cannot draw anything using my mouse. If class Action and View are combined everything is working fine, but when I put them away nothing happens(no error as well). The reason I did this separation is that I simple want to add another classes with other functionality (drawing elipses, rects...) and swap them with Action class. I added line to the scene (in Action init) and it`s being painted corectly, but the MouseEvents dont work at all.The button "tool" was made for changing the drawing tool.
This way, in my opinion, is the best way to achive ability to scribble with different tools on the same canvas(scene). My way aint good so I am asking for help. What can I do fix this code, so it works as I want? Where are the mistakes? or maybe the whole approach is wrong and it should be done another way? I use PYQT 4 and Python 3.4 .
Your current code has two instances of a QGraphicsScene created inside View.__init__. One is a standard QGraphicsScene and the other is your subclass Action. But your view can only be attached to one scene, so one of these is redundant and not functioning correctly. You have attached it to the QGraphicsScene, so the Action object is the one not working.
Instead, you should kill off the plain boring QGraphicsScene, and only instantiate the Action class. Use the instantiated Action object in the call to view.setScene(). Similarly, in the Action class, there is no need to pass in another scene. Just use itself (so replace all instances of self.scene with self in the Action class)

How to override isclick() sigal in Pyqt?

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.

PySide signals alternative using events not working

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.

Categories

Resources