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.
Related
For a project I'm creating a GUI using Python 3 and PyQt5. Because it has to be usable by people outside of my immediate team, I want to disable actions on the menu until they've already filled out some forms in other parts of the program (e.g. disabling the final solution view when they haven't set up the initial data connection). The issue is that when I try to call the QAction's setEnabled function outside of the function that created it (but still inside the overall class), it's causing my script to crash with no error code, so I'm having trouble understanding the issue. In the snipit below, I'm trying to set the "View Solution" menu option as true. There are some more options in that menu, but I deleted them here to make it more easy to read.
The code is structured something like this:
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication, QMessageBox, QStackedLayout
class MediaPlanner(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# Menu bar example from: zetcode.com/gui/pyqt5/
exitAction = QAction('&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(self.close)
newProject = QAction('&New Project', self)
newProject.setShortcut('Ctrl+N')
newProject.setStatusTip('Start A New Project')
newProject.triggered.connect(self.createNewProject)
openProject = QAction('&Open Project',self)
openProject.setShortcut('Ctrl+O')
openProject.setStatusTip('Open A Saved Project')
openProject.setEnabled(False)
viewSolution = QAction('&View Solution',self)
viewSolution.setStatusTip('View the Current Solution (If Built)')
viewSolution.setEnabled(False)
self.statusBar()
menubar = self.menuBar()
filemenu = menubar.addMenu('&File')
filemenu.addAction(newProject)
filemenu.addAction(openProject)
filemenu.addAction(exitAction)
viewmenu = menubar.addMenu('&View')
viewmenu.addAction(viewSolution)
self.setGeometry(300,300,700,300)
self.setWindowTitle('Menubar')
self.show()
def createNewProject(self):
print('Project Created')
self.viewSolution.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = MediaPlanner()
sys.exit(app.exec_())
The problem is that viewSolution is a variable, but it is not a member of the class so you will not be able to access it through the self instance. One possible solution is to make viewSolution member of the class as shown below:
self.viewSolution = QAction('&View Solution',self)
self.viewSolution.setStatusTip('View the Current Solution (If Built)')
self.viewSolution.setEnabled(False)
...
viewmenu.addAction(self.viewSolution)
Another possible solution is to use the sender() function, this function returns the object that emits the signal, using the following:
def createNewProject(self):
print('Project Created')
self.sender().setEnabled(True)
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.
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.
I want when the user press the button a form will appear after MainWindow is blocked pending form filling
You don't need to do anything that the other answers suggest. Using any exec() methods is a surefire way to have bugs, since suddenly your gui code can be reentered. Don't do it.
All you need to do is to set proper window modality before you show it (that's the important part). So:
widget.setWindowModality(Qt.ApplicationModal)
widget.show()
If you wish the window to block only some other window, not the entire application:
widget.setWindowFlags(widget.windowFlags() | Qt.Window)
widget.setParent(otherWindow)
widget.setWindowModality(Qt.WindowModal)
widget.show()
Note that this code is for PyQt4 only, it won't work with Qt 5 as there, the window functionality belongs in a class separate from QWidget.
You need to use a QDialog and show it using exec, which will block the rest of the application until it is closed. The return value of exec also tells you whether the form was closed without committing changes (i.e. cancelled).
Here is a simple demo script that shows how to use a QDialog:
from PyQt4 import QtCore, QtGui
class Dialog(QtGui.QDialog):
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.checkbox1 = QtGui.QCheckBox('Option one', self)
self.checkbox2 = QtGui.QCheckBox('Option two', self)
self.buttonOk = QtGui.QPushButton('Ok', self)
self.buttonOk.clicked.connect(self.accept)
self.buttonCancel = QtGui.QPushButton('Cancel', self)
self.buttonCancel.clicked.connect(self.reject)
layout = QtGui.QGridLayout(self)
layout.addWidget(self.checkbox1, 0, 0, 1, 2)
layout.addWidget(self.checkbox2, 1, 0, 1, 2)
layout.addWidget(self.buttonOk, 2, 0)
layout.addWidget(self.buttonCancel, 2, 1)
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
widget = QtGui.QWidget(self)
layout = QtGui.QVBoxLayout(widget)
self.button = QtGui.QPushButton('Show Dialog', self)
self.button.clicked.connect(self.handleButton)
layout.addWidget(self.button)
self.setCentralWidget(widget)
def handleButton(self):
dialog = Dialog(self)
if dialog.exec_() == QtGui.QDialog.Accepted:
print('Option one: %s' % dialog.checkbox1.isChecked())
print('Option two: %s' % dialog.checkbox2.isChecked())
else:
print('Cancelled')
dialog.deleteLater()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 200, 100)
window.show()
sys.exit(app.exec_())
This is what you need
self.setWindowModality(QtCore.Qt.ApplicationModal)
Ok , so you want to block the parent window until the child window has been closed.
dialog = QInputDialog()
dialog.exec_()
Use the exec_() function , it will block until the child window is not closed
for further info :
launch a PyQT window from a main PyQt window, and get the user input?
Python - make a window appear on top of another, block access to other windows until button clicked
Subclass your QDialog or your QWidget with your form, and then connect it like this in the constructory of your main window. You will want to convert this code from c++ to python:
QObject::connect(myPushButton, SIGNAL(clicked), this, SLOT(on_myPushButton()));
//...
void MainWindow::on_myPushButton()
{
Dialog d;
int retVal = d.exec();// this is a blocking call
// Here the user has finished filling out the form.
// save any data that should be in the form, or respond to the retVal
}
EDIT: Added link to docs on using QDialog::exec()
http://qt-project.org/doc/qt-5/qdialog.html#exec
Hope that helps.
must create Widget inherits from Qdialog
AjoutArBase, AjoutArForm = uic.loadUiType('ajoutArticle.ui')
class AjoutArticle(AjoutArBase,QtGui.QDialog):
I would like to use a QMenu as a permanent widget in the gui. (I like its appearance and layout, and the fact that as soon as I hover over it, the requisite menu pops up, no clicking needed. It would be a pain in the neck to try and emulate it with a custom widget.) I have tried adding it to a parent widget's layout, but after the first time it is used, it disappears. How would I go about keeping it there?
I can't find any option in QMenu that would disable auto-hide, so simplest way would be a subclass that overrides hideEvent. hideEvent is fired just before hide() completes. That means you can't intercept/ignore hide() but you can re-show it:
class PermanentMenu(QtGui.QMenu):
def hideEvent(self, event):
self.show()
Just make your top-level menu from PermanentMenu and it should be fine.
A simple example using it:
import sys
from PyQt4 import QtGui
class PermanentMenu(QtGui.QMenu):
def hideEvent(self, event):
self.show()
class Window(QtGui.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.menu = PermanentMenu()
self.menu.addAction('one')
self.menu.addAction('two')
self.submenu = self.menu.addMenu('submenu')
self.submenu.addAction('sub one')
self.submenu.addAction('sub two')
self.submenu2 = self.menu.addMenu('submenu 2')
self.submenu2.addAction('sub 2 one')
self.submenu2.addAction('sub 2 two')
layout = QtGui.QHBoxLayout()
layout.addWidget(self.menu)
self.setLayout(layout)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())