Use QAction without adding to menu (or toolbar) - python

I'm trying to develop an application with a very modular approach to commands and thought it would be nice, sind I'm using pyqt, to use QAction's to bind shortcuts to the commands.
However, it seems that actions shortcuts only works when the action is visible in a menu or toolbar. Does anyone know a way to get this action to work without it being visible?
Below some example code that shows what I'm trying.
Thanks,
André
from PyQt4 import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class TesteMW(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.create_action()
def create_action(self):
self.na = QAction(self)
self.na.setText('Teste')
self.na.setShortcut('Ctrl+W')
self.connect(self.na, SIGNAL('triggered()'), self.action_callback)
# uncomment the next line for the action to work
# self.menuBar().addMenu("Teste").addAction(self.na)
def action_callback(self):
print 'action called!'
app = QApplication(sys.argv)
mw = TesteMW()
mw.show()
app.exec_()

You need to add your action to a widget before it will be processed. From the QT documentation for QAction:
Actions are added to widgets using
QWidget::addAction() or
QGraphicsWidget::addAction(). Note
that an action must be added to a
widget before it can be used; this is
also true when the shortcut should be
global (i.e., Qt::ApplicationShortcut
as Qt::ShortcutContext).
This does not mean that they will be visible as a menu item or whatever - just that they will be processes as part of the widgets event loop.

Related

PyQt5 buttons not connecting

I am trying to build a simple GUI using PyQT5, with 3 buttons to open file browsers and one more to run processing with the selected files, but I can't get my buttons to connect to the functions needed to carry this out.
In the Ctrl class, the _connect_signals function doesn't seem to be calling _input_select. Can anyone help me figure out why?
import sys
# Import QApplication and the required widgets from PyQt5.QtWidgets
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QFileDialog
# Create a subclass of QMainWindow to setup the calculator's GUI
class UI(QMainWindow):
"""App's View (GUI)."""
def __init__(self):
"""View initializer."""
super().__init__()
# Set some main window's properties
self.setFixedSize(300, 150)
# Set the central widget and the general layout
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Create the buttons
self._create_buttons()
def _create_buttons(self):
"""Create the buttons."""
self.buttons = {}
buttons_layout = QVBoxLayout()
# Button text | position on the QVBoxLayout
buttons = {
"Select input file...": 0,
"Select config file...": 1,
"Select output file...": 2,
"Run": 3,
}
# Create the buttons and add them to the grid layout
for btnText, pos in buttons.items():
self.buttons[btnText] = QPushButton(btnText)
buttons_layout.addWidget(self.buttons[btnText], pos)
# Add buttons_layout to the general layout
self.generalLayout.addLayout(buttons_layout)
# Create a Controller class to connect the GUI and the model
class Ctrl:
"""App's Controller."""
def __init__(self, setup, view):
"""Controller initializer."""
self._view = view
self._setup = setup
# Connect signals and slots
self._connect_signals()
def _input_select(self): # Not being called
print("input selection")
options = QFileDialog.Options()
file_select, _ = QFileDialog.getOpenFileNames(
self,
'Select Input File...',
'',
'CSV Files (*.csv);;All Files (*)',
options=options
)
if file_select:
self._setup["input"] = file_select
def _connect_signals(self):
"""Connect signals and slots."""
self._view.buttons["Select input file..."].clicked.connect(self._input_select) # Not working!
# Client code
def main():
"""Main function."""
# Create an instance of `QApplication`
app = QApplication(sys.argv)
# Show the app's GUI
view = UI()
view.show()
setup = {}
# Create instance of the controller
Ctrl(setup=setup, view=view)
# Execute app's main loop
sys.exit(app.exec_())
if __name__ == "__main__":
main()
In case it helps, I started out by butchering this example code from a Real Python tutorial, but must have broken it along the way.
The problem is that you are not keeping any persistent reference to the Ctrl() instance you are creating. This results in python garbage collecting it as soon as the instance is created.
To solve the issue, just assign it to a variable:
def main():
"""Main function."""
# Create an instance of `QApplication`
app = QApplication(sys.argv)
# Show the app's GUI
view = UI()
view.show()
setup = {}
# Create instance of the controller
ctrl = Ctrl(setup=setup, view=view)
# Execute app's main loop
sys.exit(app.exec_())
Some considerations:
while separating logic from interface is usually good practice, it's a concept that needs to be used with care, as sometimes it only makes things more complex than they should be. Most of the times (especially with simple programs), it only makes a bigger codebase without giving any actual benefit: it's harder to read and to debug, and you'll probably end up continuously switching from the logic parts and the ui parts of your code;
your code shows one of the drawback of that concept: when you create the file dialog, you're using self, but in that case it refers to the Ctrl instance, while the argument should be the UI instance instead (which will result in a crash, as Qt will get an unexpected argument type); you can use self._view instead, but, as said, the whole separation in this case just makes things unnecessarily complex;
using strings for dictionary keys that refer to internal objects is rarely a good idea (especially when using long descriptive strings like you did);
when importing more than one element from a module, it's usually better to group them instead of using single line imports: it makes the code tidier and easier to read and inspect: from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QFileDialog)

UI made in QT Designer shifts behind Title Bar [duplicate]

I'm trying to create an application that contains a web browser within it, but when I add the web browser my menu bar visually disappears but functionally remains in place. The following are two images, one showing the "self.centralWidget(self.web_widget)" commented out, and the other allows that line to run. If you run the example code, you will also see that while visually the entire web page appears as if the menu bar wasn't present, you have to click slightly below each entry field and button in order to activate it, behaving as if the menu bar was in fact present.
Web Widget Commented Out
Web Widget Active
Example Code
import os
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtWebEngineWidgets import *
class WebPage(QWebEngineView):
def __init__(self, parent=None):
QWebEngineView.__init__(self)
self.current_url = ''
self.load(QUrl("https://facebook.com"))
self.loadFinished.connect(self._on_load_finished)
def _on_load_finished(self):
print("Url Loaded")
class MainWindow(QMainWindow):
def __init__(self, parent=None):
# Initialize the Main Window
super(MainWindow, self).__init__(parent)
self.create_menu()
self.add_web_widet()
self.show()
def create_menu(self):
''' Creates the Main Menu '''
self.main_menu = self.menuBar()
self.main_menu_actions = {}
self.file_menu = self.main_menu.addMenu("Example File Menu")
self.file_menu.addAction(QAction("Testing Testing", self))
def add_web_widet(self):
self.web_widget = WebPage(self)
self.setCentralWidget(self.web_widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.showMaximized()
sys.exit(app.exec_()) # only need one app, one running event loop
Development Environment
Windows 10, PyQt5, pyqt5-5.9
EDIT
The problem doesn't seem to be directly related to the menu bar. Even removing the menu bar the issue still occurs. That said, changing from showMaximized() to showFullScreen() does seem to solve the problem.
I no longer believe this is an issue with PyQt5 specifically but rather a problem with the graphics driver. Specifically, if you look at Atlassian's HipChat application it has a similar problem which is documented here:
https://jira.atlassian.com/browse/HCPUB-3177
Some individuals were able to solve the problem by running the application from the command prompt with the addendum "--disable-gpu" but that didn't work for my python application. On the other hand, rolling back the Intel(R) HD Graphics Driver did solve my problem. Version 21.20.16.4627 is the one that seems to be causing problems.

Wizard-like user interface with buttons next and back

I'm developing an application using the Qt Designer and PyQt4, I need to make several screens where each screen I capture user-specific data, for that I need to implement a next button and a back button similar to
where the current screen closes and the following opens when the user clicks next or if he clicks back, the screen closes and opens the previous screen, I made an example with only the next buttons and back to exemplify, if I was not clear:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class Frm(QWidget):
def __init__(self, parent = None):
super(Frm, self).__init__(parent)
next = QPushButton('Next >', self)
back = QPushButton('< Back', self)
hbox = QHBoxLayout()
hbox.addWidget(back)
hbox.addWidget(next)
self.setLayout(hbox)
if __name__ == '__main__':
import sys
root = QApplication(sys.argv)
app = Frm(None)
app.show()
root.exec_()
In short: How do I implement a function that calls another screen and close the current at the click of a button?
First about a misconception: you do usually not create/show one screen (window) and close another, you usually only exchange the content of a wizard-like dialog window upon actions like pressing the buttons. The window is alive the whole time until the multiple page task is finished.
So I take it your question is really about:
How to exchange a widget in a layout?
Since you may still use PyQt4 which does not yet have QLayout.replaceWidget, it's best to just use methods removeWidget and addWidget of QLayout and since addWidget adds a widget to the end of the layout items list, I prefer a dedicated layout just for the interchangeable content of your wizard (see also: How to add an Item to the specific index in the layout).
Example code using PyQt5 but easily transferrable to PyQt4. Only the next button is implemented.
from PyQt5 import QtWidgets
class MyWizard(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# vertical layout, wraps content layout and buttons layout
vertical_layout = QtWidgets.QVBoxLayout()
self.setLayout(vertical_layout)
# content widget and layout
self.content_layout = QtWidgets.QVBoxLayout() # could be almost any layout actually
self.content = QtWidgets.QLabel('Page1') # customize with your content
self.content_layout.addWidget(self.content)
vertical_layout.addLayout(self.content_layout)
# back, forward buttons wraped in horizontal layout
button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch()
back_button = QtWidgets.QPushButton('Back')
back_button.clicked.connect(self.back_button_clicked)
button_layout.addWidget(back_button)
forward_button = QtWidgets.QPushButton('Forward')
forward_button.clicked.connect(self.forward_button_clicked)
button_layout.addWidget(forward_button)
vertical_layout.addLayout(button_layout)
def back_button_clicked(self):
"""
The back button is clicked.
"""
pass
def forward_button_clicked(self):
"""
The forward button is clicked.
"""
# remove old content
self.content_layout.removeWidget(self.content)
self.content.deleteLater()
# create new content
self.content = QtWidgets.QLabel('Another Page')
# add new content
self.content_layout.addWidget(self.content)
app = QtWidgets.QApplication([])
wizard = MyWizard()
wizard.setWindowTitle('MyWizard Example')
wizard.setFixedSize(600, 400)
wizard.show()
app.exec_()
And it looks like:
However, as already written in the comment by Marius, there is quite extensive support for such dialogs in Qt using QWizard. So I strongly recommend to use that instead. The example above is only to demonstrate the ability of inserting and removing widgets in layouts.
You should definitely use QWizard for such problems!
There is a QWizard class that allows you to create wizards in Qt and PyQt. It implements all the functionality you want, and lots more. All you do is design your pages by extending QWizardPage, and add them to the wizard. This is much simpler than doing the whole thing from scratch as you propose.
Old question but missing example with existing API
You don't need to create all structure of an Wizard by yourself. Qt (and PyQt) already provides a class called QWizard.
Basic example:
wizard = QtWidgets.QWizard()
page1 = QtWidgets.QWizardPage()
page1.setTitle('Page 1 is best!')
page1.setSubTitle('1111111111')
lineEdit = QtWidgets.QLineEdit()
hLayout1 = QtWidgets.QHBoxLayout(page1)
hLayout1.addWidget(lineEdit)
wizard.addPage(page1)
Complete example and some explanation:
https://www.youtube.com/watch?v=kTJ1QULxXjg
https://impatientprogrammer.net/2018/07/06/pyside-pyqt-qwizard-in-3-minutes/

Deactivate all buttons during execution - PyQt

I'm building a GUI with PyQt, and I'd like to make it possible to stop all the buttons doing anything while code is running. Lets say the user is copying a lot of data from a table - it would be easy for them to click another button while it is happening even if the cursor has changed to the egg timer. Any ideas or workarounds for this without going through all buttons and greying them out one by one? I'd be happy with a workaround too!
Thanks for any ideas,
Pete
You could use a modal QDialog. From the QDialog pyqt documentation:
A modal dialog is a dialog that blocks input
to other visible windows in the same application.
Also, QProgressDialog is a very convenient tool to handle blocking action in a very simple way. Here is an example :
from PyQt4 import QtGui, QtCore
from time import sleep
class Test(QtGui.QDialog):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
button = QtGui.QPushButton("Button")
hbox = QtGui.QHBoxLayout()
hbox.addWidget(button)
self.setLayout(hbox)
button.clicked.connect(self.slot)
def slot(self):
progress = QtGui.QProgressDialog(self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setLabel(QtGui.QLabel("Doing things..."))
progress.setAutoClose(True)
for i in range(101):
progress.setValue(i);
sleep(0.05)
if progress.wasCanceled():
break
if __name__=="__main__":
import sys
app = QtGui.QApplication(sys.argv)
myapp = Test()
myapp.show()
sys.exit(app.exec_())

PySide how to get QWebInspector same window

I just started dwelling into the realm of Qt (coming from PyGTK) and I'm using PySide. So I found this great example on another answer here on stack exchange.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtWebKit import *
app = QApplication(sys.argv)
web = QWebView()
web.settings().setAttribute(
QWebSettings.WebAttribute.DeveloperExtrasEnabled, True)
# or globally:
# QWebSettings.globalSettings().setAttribute(
# QWebSettings.WebAttribute.DeveloperExtrasEnabled, True)
web.load(QUrl("http://www.google.com"))
web.show()
inspect = QWebInspector()
inspect.setPage(web.page())
inspect.show()
sys.exit(app.exec_())
My question is as follows, how do I make the inspector show up in the same window instead of a new one? I understand I need to add the QWebInspector to another widget inside the main window (a vbox for example), what I want to know is how to connect that event to the signal the context menu "Inspect" triggers. In PyGTK I would need to use .connect() but I can't find the right SIGNAL for this specific action.
Thanks for your time guys/gals
It shouldn't be necessary to do anything special for the context menu to work. Just add an inspector widget to your layout, and hide() it to start with. The default context menu action can then show() the inspector as needed.
A slightly trickier issue is how to hide the inspector again once it's shown, as there doesn't seem to be a corresponding context menu item for that.
The demo script below simply creates a keyboard shortcut to hide/show the inspector:
from PySide import QtGui, QtCore, QtWebKit
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.view = QtWebKit.QWebView(self)
self.view.settings().setAttribute(
QtWebKit.QWebSettings.WebAttribute.DeveloperExtrasEnabled, True)
self.inspector = QtWebKit.QWebInspector(self)
self.inspector.setPage(self.view.page())
self.inspector.hide()
self.splitter = QtGui.QSplitter(self)
self.splitter.addWidget(self.view)
self.splitter.addWidget(self.inspector)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.splitter)
QtGui.QShortcut(QtGui.QKeySequence('F7'), self,
self.handleShowInspector)
def handleShowInspector(self):
self.inspector.setShown(self.inspector.isHidden())
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.view.load(QtCore.QUrl('http://www.google.com'))
window.show()
sys.exit(app.exec_())

Categories