PyQt: Emit signal when cell entered in QCalendarWidget - python

In my Qt application I'm using the QCalendarWidget and I would like to get notified when the mouse enters a new cell of the calendar. I know that the QCalendarWidget is using a QTableView internally which inherits from QAbstractItemView and this has an entered signal:
This signal is emitted when the mouse cursor enters the item specified
by index. Mouse tracking needs to be enabled for this feature to work.
I tried to receive the signal with following code:
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyCalendar:
def __init__(self):
app = QApplication(sys.argv)
window = QMainWindow()
cal = QCalendarWidget(window)
window.resize(320, 240)
cal.resize(320, 240)
table = cal.findChild(QTableView)
table.setMouseTracking(True)
table.entered.connect(self.onCellEntered)
window.show()
sys.exit(app.exec_())
def onCellEntered(self, index):
print("CellEntered")
if __name__ == "__main__":
window = MyCalendar()
But my callback function is never called. Do you have any ideas why?

The QCalendarWidget class uses a custom table-view which bypasses the normal mouse-event handlers - so the enetered signal never gets emitted. However, it is possible to work-around that by using an event-filter to emit a custom signal that does the same thing.
Here is a demo script that implements that:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
cellEntered = QtCore.pyqtSignal(object)
def __init__(self):
super(Window, self).__init__()
self.calendar = QtGui.QCalendarWidget(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.calendar)
self._table = self.calendar.findChild(QtGui.QTableView)
self._table.setMouseTracking(True)
self._table.installEventFilter(self)
self._index = None
self.cellEntered.connect(self.handleCellEntered)
def eventFilter(self, source, event):
if source is self._table:
if event.type() == QtCore.QEvent.MouseMove:
index = QtCore.QPersistentModelIndex(
source.indexAt(event.pos()))
if index != self._index:
self._index = index
self.cellEntered.emit(QtCore.QModelIndex(index))
elif event.type() == QtCore.QEvent.Leave:
self._index = None
return super(Window, self).eventFilter(source, event)
def handleCellEntered(self, index):
print(index.row(), index.column())
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(600, 100, 300, 200)
window.show()
sys.exit(app.exec_())

I've investigated a bit and I think I know why this happens.
QCalendarWidget creates a private subclass of QTableView called QCalendarView and instantiates it as a child of itself. (This instance is called qt_calendar_calendarview.)
If you look at QCalendarView's code (Qt 5), you'll see this:
void QCalendarView::mouseMoveEvent(QMouseEvent *event)
{
[...]
if (!calendarModel) {
QTableView::mouseMoveEvent(event);
return;
}
[...]
}
This means only if there is no calendarModel, the superclasses mouseMoveEventis called which is responsible for emitting the entered signal. All of this is an implementation detail of `QCalendarWidget, so it's probably best not to rely on any of this anyway.
So for a way around this, I'm not sure what the best approach is. You'll have to catch the event before it gets to the table. This can be done using QObject.installEventFilter() or re-implementing QWidget.mouseMoveEvent(), but then you don't get the model index directly. You could probably use QAbstractItemView.indexAt() for this purpose.

Related

PySide6 setStatusTip() shows QAction name

I'm learning PySide6 and I stumbled upon a weird thing.
When creating a QAction and setting the status tip, the name of the QAction is shown as a status tip instead of the actual status tip.
What am I missing here?
Here is my short example code:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Test App")
label = QLabel("Hello!")
label.setAlignment(Qt.AlignCenter)
self.setCentralWidget(label)
toolbar = QToolBar("My main toolbar")
self.addToolBar(toolbar)
button_action = QAction("Test", self)
button_action.setShortcut('Ctrl+T')
button_action.setStatusTip('Test application')
button_action.triggered.connect(self.onMyToolBarButtonClick)
toolbar.addAction(button_action)
def onMyToolBarButtonClick(self, s):
print("click", s)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Here is the weird result:
Thank you!
What is shown in the image is a tool tip, and by default a tool bar shows the action's text() as a tooltip, unless a tool tip is explicitly set using setToolTip().
The status tip, instead, is shown in the status bar (a QStatusBar).
On a QMainWindow the status bar can be accessed using statusBar() (if none exists, which is the default for new empty main windows, a new one is created and returned).
Just add the following anywhere in the __init__() and you'll see that the "Test application" string is actually shown there when hovering the action:
self.statusBar()
A status bar can also be installed on any QWidget or inherited subclass, and can be used to capture any status event received from itself or its children:
class MainWindow(QWidget): # note: a basic QWidget
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
button = QPushButton('Test')
layout.addWidget(button)
button.setStatusTip('Test application')
self.statusBar = QStatusBar()
layout.addWidget(self.statusBar)
def event(self, event):
if event.type() == event.Type.StatusTip:
self.statusBar.showMessage(event.tip())
# returning True means that the event has been
# successfully handled and will not be propagated to
# the possible parent(s)
return True
return super().event(event)
The above is what actually QMainWindow does under the hood.

Disable mouse wheel scroll on QscrollArea

I would like to simply disable mouse wheel scroll on QScrollArea, in order to scroll down only by clicking on right scrollbar, but I can't find any solution on the Internet.
app = QtGui.QApplication([])
sa = pg.QtGui.QScrollArea()
win = pg.GraphicsWindow()
sa.setWidget(win)
The problem is that I have a lot of graphs in my scroll area, and when I try to mousewheel on one of them, the page will scroll up or down together with the graph.
I can't find a method to call on "sa" to disable mouse wheel scroll.
I found some posts discussing about install event filter but I can't understand how to use them in this case. For example, I tried to use this:
sa.viewport().installEventFilter(???)
but I really didn't understand what arguments to pass and how to check the event.
Thank you in advance if you can help me with this problem.
You have the right idea. Event-filtering requires an object that inherits QObject to watch for the relevant events. Such objects have an eventFilter method which can be overriden to provide custom handing of all events for the watched object. If this method returns True for a given event, it will not be propagated any further. Usually the main-window is used to provide the event-filtering, like this:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.scroll = QtGui.QScrollArea()
self.widget = QtGui.QGraphicsView()
self.widget.setFixedSize(600, 600)
self.scroll.setWidget(self.widget)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.scroll)
self.scroll.viewport().installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.Wheel and
source is self.scroll.viewport()):
return True
return super(Window, self).eventFilter(source, event)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(600, 100, 400, 300)
window.show()
sys.exit(app.exec_())
I finally managed to resolve this, using this solution:
class Scroller(pg.QtGui.QScrollArea):
def __init__(self):
pg.QtGui.QScrollArea.__init__(self)
def wheelEvent(self, ev):
if ev.type() == QtCore.QEvent.Wheel:
ev.ignore()
app = QtGui.QApplication([])
sa = Scroller() # <======
win = pg.GraphicsWindow()
sa.setWidget(win)

PyQt - Add right click to a widget

I am using PyQt and I want to add a right click to a widget, but I can't find any code on this subject online.
How can you do it ?
You just have to override the methods that take care of it.
In this case you will override the mousePressEvent, have a look on this and see if it makes sense and works for what you need.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QWidget
class MyWidget(QWidget):
def __init__(self):
super(MyWidget, self).__init__()
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
print("Left Button Clicked")
elif QMouseEvent.button() == Qt.RightButton:
#do what you want here
print("Right Button Clicked")
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MyWidget()
mw.show()
sys.exit(app.exec_())
Another good way to do that would be installing a event filter in your object and overriding its eventFilter. Inside that method you would make what you want. Remember you can always make use of pyqtSignal for good practices and call another object to make the job, not overloading the method with a lot of logic.
Here is another small example:
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QWidget
class MyWidget(QWidget):
def __init__(self):
super(MyWidget, self).__init__()
self.installEventFilter(self)
def eventFilter(self, QObject, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.RightButton:
print("Right button clicked")
return False
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MyWidget()
mw.show()
sys.exit(app.exec_())
Note: Remember that this last example will receive ALL KIND OF EVENTS, so you will have to be careful and make sure it's the one you want and not runtime breaking your app calling methods of your event that doesn't exist because it's not of that kind. For example if you call event.button() without making sure before that it is a QEvent.MouseButtonPress your app would break of course.
There are other ways to do that, these are the most known ones.
I have come up with a pretty simple way of doing this and works perfectly. In the ControlMainWindow class add the following to initialise the Context menu policy as CustomeContextMenu where listWidget_extractedmeters will be the name of your QListWidget:
self.listWidget_extractedmeters.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.listWidget_extractedmeters.connect(self.listWidget_extractedmeters,QtCore.SIGNAL("customContextMenuRequested(QPoint)" ), self.listItemRightClicked)
Then in the ControlMainwindow class the following functions allow you to add context menu items and to call a funtion that performs some functionality:
def listItemRightClicked(self, QPos):
self.listMenu= QtGui.QMenu()
menu_item = self.listMenu.addAction("Remove Item")
self.connect(menu_item, QtCore.SIGNAL("triggered()"), self.menuItemClicked)
parentPosition = self.listWidget_extractedmeters.mapToGlobal(QtCore.QPoint(0, 0))
self.listMenu.move(parentPosition + QPos)
self.listMenu.show()
def menuItemClicked(self):
currentItemName=str(self.listWidget_extractedmeters.currentItem().text() )
print(currentItemName)

Generating a QCloseEvent won't close QMainWindow

I'm trying to do something quite simple: add a menu bar with an Exit action that will close a QMainWindow when it is selected. However, when I actually click Exit, it doesn't close the application. A SSCCE:
from PyQt4 import QtGui, QtCore
import sys
class Window(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
widget = QtGui.QWidget()
self.setCentralWidget(widget)
self.menu_bar = QtGui.QMenuBar(self)
menu = self.menu_bar.addMenu('File')
exit_action = QtGui.QAction('Exit', self)
exit_action.triggered.connect(lambda:
self.closeEvent(QtGui.QCloseEvent()))
menu.addAction(exit_action)
self.setMenuBar(self.menu_bar)
def closeEvent(self, event):
print('Calling')
print('event: {0}'.format(event))
event.accept()
app = QtGui.QApplication(sys.argv)
form = Window()
form.show()
sys.exit(app.exec_())
What is really confusing me is that when I click Exit from the File menu, I get the following output:
Calling
event: <PyQt4.QtGui.QCloseEvent object at 0x024B7348>
and the application does not exit.
If I click the top-right corner X, I get the same thing (down to the same memory address for the event object):
Calling
event: <PyQt4.QtGui.QCloseEvent object at 0x024B7348>
and the application does exit.
This is on Windows 7 64-bit, Python 2.7.2, PyQt 4.8.6.
Document says,
The QCloseEvent class contains parameters that describe a close event.
Close events are sent to widgets that the user wants to close, usually
by choosing "Close" from the window menu, or by clicking the X title
bar button. They are also sent when you call QWidget.close() to close
a widget programmatically.
Your can call directly with signal close not by QCloseEvent, please call self.close().
from PyQt4 import QtGui, QtCore
import sys
class Window(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
widget = QtGui.QWidget()
self.setCentralWidget(widget)
self.menu_bar = QtGui.QMenuBar(self)
menu = self.menu_bar.addMenu('File')
exit_action = QtGui.QAction('Exit', self)
exit_action.triggered.connect(self.close)
menu.addAction(exit_action)
self.setMenuBar(self.menu_bar)
def closeEvent(self, event):
print('Calling')
print('event: {0}'.format(event))
event.accept()
app = QtGui.QApplication(sys.argv)
form = Window()
form.show()
sys.exit(app.exec_())
The close event doesn't actually make the window close, it's just triggered when the window is already closing. To actually make the window close, you need to call self.close(), which will have the side effect of triggering a QCloseEvent. So simply use this:
exit_action.triggered.connect(self.close)
The documentation of close describes the interaction between close and closeEvent:
bool QWidget.close (self)
This method is also a Qt slot with the C++ signature bool close().
Closes this widget. Returns true if the widget was closed; otherwise
returns false.
First it sends the widget a QCloseEvent. The widget is hidden if it
accepts the close event. If it ignores the event, nothing happens. The
default implementation of QWidget.closeEvent() accepts the close
event.

PyQt monkey patching QLineEdit.paste?

I am trying to intercept paste() for a specific edit box. After much reading and head scratching I decided to try the big hammer and monkey patch. This didn't work for me either. Anyone know why?
import sys
from PyQt4 import QtGui
def myPaste():
print("paste") # Never gets here
if __name__ == "__main__":
# QtGui.QLineEdit.paste = myPaste # Try #1
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setWindowTitle("monkey")
centralWidget = QtGui.QWidget(window)
edit = QtGui.QLineEdit(centralWidget)
# QtGui.QLineEdit.paste = myPaste # Try #2
edit.paste = myPaste # Try #3
window.setCentralWidget(centralWidget)
window.show()
app.exec_()
Based on feedback..i was able to use the event filter suggestion to solve my problem. Updated example code follows...
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setWindowTitle("monkey")
centralWidget = QtGui.QWidget(window)
edit = QtGui.QLineEdit(centralWidget)
window.setCentralWidget(centralWidget)
def eventFilter(obj, e):
if isinstance(obj, QtGui.QLineEdit):
if (e.type() == QtCore.QEvent.KeyPress):
if (e.matches(QtGui.QKeySequence.Paste)):
obj.paste()
t=str(obj.text()).title() # Special handling here...uppercase each word for example
obj.setText(t)
return True
return False
else:
return QtGui.QMainWindow.eventFilter(obj, e)
window.eventFilter = eventFilter
edit.installEventFilter(window)
window.show()
app.exec_()
The reason you can't "monkey-patch" QLineEdit.paste() is because it's not a virtual function. The important thing about virtual functions is that when they are overridden, the reimplemented function will get called internally by Qt; whereas non-virtual overrides will only get called by Python code. So, since QLinedit.paste() isn't virtual, you will instead have to intercept all the events that would normally result in it being called internally by Qt.
That will mean reimplementing QLineEdit.keyPressEvent, so that you can trap the shortcuts for the default key bindings; and also QLineEdit.contextMenuEvent, so that you can modify the default context menu. And, depending on what you're trying to do, you might also need to override the default drag-and-drop handling. (If you prefer not to use a subclass, an event-filter can be used to monitor all the relevant events).
The QClipboard class provides access to the system clipboard, which will allow you to intercept the text before it is pasted. Every application has a single clipboard object, which can be accessed via QApplication.clipboard() or qApp.clipboard().
In order to do what you want you can subclass QLineEdit and create a method that provides the custom paste functionality that you want (paste method isn't virtual so if it is overriden it won't be called from Qt code). In addition you will need an event filter to intercept the shortcut for CTRL+V. Probably you will have to filter the middle mouse button too which is also used to paste the clipboard content. From the event filter you can call your replacement of paste method.
You can use the following code as starting point:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class myEditor(QLineEdit):
def __init__(self, parent=None):
super(myEditor, self).__init__(parent)
def myPaste(self):
self.insert("custom text pasted! ")
class myWindow(QMainWindow):
def __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.customEditor = myEditor(self)
self.setCentralWidget(self.customEditor)
self.customEditor.installEventFilter(self)
def eventFilter(self, obj, e):
if (obj == self.customEditor):
if (e.type() == QEvent.KeyPress):
if (e.matches(QKeySequence.Paste)):
self.customEditor.myPaste()
return True
return False
else:
return QMainWindow.eventFilter(obj, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = myWindow()
window.show()
app.exec_()
The event filter here only takes care of the keyboard shortcut for pasting. As I said you need to consider also other sources of the paste operation.

Categories

Resources