So Basically I Am newbie In PyQt5, I found that the PyQt5 support stylesheets!
So I just thought Of The implementing that drop a list when i user hover on QCombobox.
Any Answer Or Suggestion Is Accepted!
Can it be?
import sys
from PyQt5.QtWidgets import (QWidget, QHBoxLayout,
QComboBox, QApplication)
from PyQt5.QtGui import QIcon
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
combo = QComboBox(self)
combo.addItem('Disk')
combo.setItemIcon(0, QIcon('disk.png'))
combo.addItem('Web')
combo.setItemIcon(1, QIcon('web.png'))
combo.addItem('Computer')
combo.setItemIcon(2, QIcon('computer.png'))
hbox.addWidget(combo)
hbox.setSpacing(20)
self.setContentsMargins(20, 20, 20, 20)
self.setLayout(hbox)
self.setGeometry(300, 300, 250, 180)
self.setWindowTitle('QComboBox')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Stylesheets in Qt do not allow programmatical access to widget functions (with some exceptions for widget properties, which must be used with care, though).
So, if you thought you could use stylesheets to show the popup on hover, you can't.
In order to do that, you must subclass the QComboBox and reimplement its enterEvent(), otherwise install an event filter. In both cases, you need to call showPopup():
class Example(QWidget):
def initUI(self):
# ...
combo.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QEvent.Enter:
source.showPopup()
return super().eventFilter(source, event)
Note that if you want to filter other widgets and different event types, it's better to create a reference for the object(s) and check that the source of the event is actually what you need:
class Example(QWidget):
def initUI(self):
# ...
self.combo = QComboBox()
self.combo.installEventFilter(self)
# ...
def eventFilter(self, source, event):
if source == self.combo and event.type() == QEvent.Enter:
source.showPopup()
return super().eventFilter(source, event)
About the customization with stylesheets, see the examples on customizing QComboBox, but I also warmly suggest you to study both the stylesheet syntax and reference documentation.
Also note that you should always ask one question per post, if you have more different questions, create a post for each of them.
Related
I am trying to make multiple widgets with different styles from the built in styles provided by QStyleFactory, but when I run my code they all look the same. How can I fix this?
from PyQt5 import QtWidgets, QtCore, QtGui
import sys
class Demo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QtWidgets.QWidget()
self.setCentralWidget(self.container)
self.layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.layout)
self.btn = QtWidgets.QPushButton("button")
self.lw = QtWidgets.QListWidget()
self.lw.addItems(["one", "two", "three"])
self.layout.addWidget(self.btn)
self.layout.addWidget(self.lw)
self.resize(400, 150)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widgets = []
for style_name in QtWidgets.QStyleFactory.keys():
demo = Demo()
demo.setWindowTitle(style_name)
style = QtWidgets.QStyleFactory.create(style_name)
demo.setStyle(style)
widgets.append(demo)
sys.exit(app.exec_())
An important aspect of setting a QStyle on widgets (instead of setting it on the whole application) is reported in the QWidget.setStyle() documentation:
Setting a widget's style has no effect on existing or future child widgets.
So, what's happening is that you're setting the style on the QMainWindow only, while the children will always use the QApplication style.
What you could try to do is to manually set the style for the existing children:
for style_name in QtWidgets.QStyleFactory.keys():
demo = Demo()
demo.setWindowTitle(style_name)
style = QtWidgets.QStyleFactory.create(style_name)
demo.setStyle(style)
for child in demo.findChildren(QtWidgets.QWidget):
child.setStyle(style)
widgets.append(demo)
In any case, the above approach has a drawback: any new children created after setting the style will still inherit the QApplication style. The only way to avoid this is to watch for childEvent() by (recursively) installing an event filter on the parent, and set the styles accordingly; note that you need to watch for StyleChange events too.
class ChildEventWatcher(QtCore.QObject):
def __init__(self, parentWidget):
super().__init__()
self.parentWidget = parentWidget
self.parentWidget.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded and isinstance(event.child(), QtWidgets.QWidget):
event.child().installEventFilter(self)
event.child().setStyle(self.parentWidget.style())
for child in event.child().findChildren(QtWidgets.QWidget):
child.installEventFilter(self)
child.setStyle(self.parentWidget.style())
elif event.type() == QtCore.QEvent.StyleChange and source == self.parentWidget:
for child in self.parentWidget.findChildren(QtWidgets.QWidget):
child.setStyle(self.parentWidget.style())
return super().eventFilter(source, event)
class Demo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# this *must* be created before adding *any* child
self.childEventWatcher = ChildEventWatcher(self)
# ...
Also remember another important aspect the documentation warns about:
Warning: This function is particularly useful for demonstration purposes, where you want to show Qt's styling capabilities. Real applications should avoid it and use one consistent GUI style instead.
While the above code will do what you're expecting, installing an event filter on all child QWidgets is not a good thing to do, especially if you only need to do the style change (which is something that should normally be done just once, possibly at the start of the program). Considering the warning about using different styles, I highly suggest you to do this exactly as suggested: for demonstration purposes only.
You can set it on the application.
from PyQt5 import QtWidgets, QtCore, QtGui
import sys
class Demo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QtWidgets.QWidget()
self.setCentralWidget(self.container)
self.layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.layout)
self.btn = QtWidgets.QPushButton("button")
self.lw = QtWidgets.QListWidget()
self.lw.addItems(["one", "two", "three"])
self.layout.addWidget(self.btn)
self.layout.addWidget(self.lw)
self.resize(400, 150)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
win = Demo()
app.setStyle(QtWidgets.QStyleFactory.create("Windows")) # Set style theme on app
sys.exit(app.exec_())
I am reading through some documentation on PyQt5 to come up with a simple signal-slot mechanism. I have come to a halt due to a design consideration.
Consider the following code:
import sys
from PyQt5.QtCore import (Qt, pyqtSignal)
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def printLabel(self, str):
print(str)
def logLabel(self, str):
'''log to a file'''
pass
def initUI(self):
lcd = QLCDNumber(self)
sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(lcd)
vbox.addWidget(sld)
self.setLayout(vbox)
#redundant connections
sld.valueChanged.connect(lcd.display)
sld.valueChanged.connect(self.printLabel)
sld.valueChanged.connect(self.logLabel)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
To track the changes made to the slider, I simply print and log the changes made. What I do not like about the code is that I am required to call the sld.valueChanged slot thrice to send the same information to 3 different slots.
Is it possible to create my own pyqtSignal that sends an integer to a single slot function. And in turn have the slot function emit the changes that need to be made?
Maybe I don't fully understand the purpose of emit() because there are no good examples of it's purpose in the PyQt Signal-Slot docs. All we're given is an example of how to implement an emit with no parameters.
What I would like to do is create a function that handles the emit function. Consider the following:
import sys
from PyQt5.QtCore import (Qt, pyqtSignal)
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
#create signal
self.val_Changed = pyqtSignal(int, name='valChanged')
self.initUI()
def initUI(self):
lcd = QLCDNumber(self)
sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(lcd)
vbox.addWidget(sld)
self.setLayout(vbox)
sld.val_Changed.connect(self.handle_LCD)
self.val_Changed.emit()
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
self.show()
def handle_LCD(self, text):
'''log'''
print(text)
'''connect val_Changed to lcd.display'''
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
There are obviously some serious design flaws here. I cannot wrap my head around the order of function calls. And I am not implementing pyqtSignal correctly. I do however believe that correctly stating the following 3 points will help me produce a proper app:
For a predefined signal: send the signal to the slot function. Slot can be reimplemented to use the signal values.
Produce pyqtSignal object with some parameters. It is not yet clear what the purpose of these parameters are and how they differ from 'emit' parameters.
emit can be reimplemented to send specific signal values to the slot function. It is also not yet clear why I would need to send different values from previously existing signal methods.
Feel free to completely alter the code for what I am trying to do because I have not yet figured out if its in the realm of good style.
You can define your own slot (any python callable) and connect that to the signal, then call the other slots from that one slot.
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def printLabel(self, str):
print(str)
def logLabel(self, str):
'''log to a file'''
pass
#QtCore.pyqtSlot(int)
def on_sld_valueChanged(self, value):
self.lcd.display(value)
self.printLabel(value)
self.logLabel(value)
def initUI(self):
self.lcd = QLCDNumber(self)
self.sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(self.lcd)
vbox.addWidget(self.sld)
self.setLayout(vbox)
self.sld.valueChanged.connect(self.on_sld_valueChanged)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
Also, if you want to define your own signals, they have to be defined as class variables
class Example(QWidget):
my_signal = pyqtSignal(int)
The arguments to pyqtSignal define the types of objects that will be emit'd on that signal, so in this case, you could do
self.my_signal.emit(1)
emit can be reimplemented to send specific signal values to the slot
function. It is also not yet clear why I would need to send different
values from previously existing signal methods.
You generally shouldn't be emitting the built in signals. You should only need to emit signals that you define. When defining a signal, you can define different signatures with different types, and slots can choose which signature they want to connect to. For instance, you could do this
my_signal = pyqtSignal([int], [str])
This will define a signal with two different signatures, and a slot could connect to either one
#pyqtSlot(int)
def on_my_signal_int(self, value):
assert isinstance(value, int)
#pyqtSlot(str)
def on_my_signal_str(self, value):
assert isinstance(value, str)
In practice, I rarely overload signal signatures. I would normally just create two separate signals with different signatures rather than overloading the same signal. But it exists and is supported in PyQt because Qt has signals that are overloaded this way (eg. QComboBox.currentIndexChanged)
the accepted answer, was tricky to me to undestand because of the use of the QSlider built in signal valueChanged() I didn't know about https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSlider.html, so I added my own signal that is emitted when self.sld.valueChanged.connect(self.on_sld_valueChanged) is called, to have an example of how to create my own pyqtSignal. the Post title: "PyQt proper use of emit() and pyqtSignal()" was misleading to me , I am trying to understand signals and slots too, so I wanted to add emit() to the code. I know its not the way to go forward but just to figure out how it works:
import sys
from PyQt5 import QtCore
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
#create signal
val_Changed = pyqtSignal(int, str, name='valChanged')
def __init__(self):
super().__init__()
self.initUI()
self.valChanged.connect(self.mine)
self.show()
def printLabel(self, str):
print(str)
def logLabel(self, str):
'''log to a file'''
pass
#QtCore.pyqtSlot(int, str)
def mine(self, value, string):
self.lcd.display(value)
self.printLabel((str(value)+' '+ string))
self.logLabel(value)
def on_sld_valueChanged(self, value):
self.val_Changed.emit(value, 'using-slider')
def initUI(self):
self.lcd = QLCDNumber(self)
self.sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(self.lcd)
vbox.addWidget(self.sld)
self.setLayout(vbox)
self.sld.valueChanged.connect(self.on_sld_valueChanged)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
if __name__ == '__main__' :
app = QApplication( sys.argv )
ex = Example()
sys.exit(app.exec_( ))
I'll wait to somebody that could show me how to reimplement QSlider valueChanged() signal
I'd like to be able to load a widget (QMainWindow in this case) from a .ui file, but also be able to extend the class so that I can do things like catch keyboard events.
For example, I have the following that doesn't work but shows what I'm trying to accomplish:
class MyMainWindow(QMainWindow):
def __init__(self):
super(MyMainWindow, self).__init__()
self.window = QUiLoader().load("mainwindow.ui", self)
self.setCentralWidget(self.window)
self.window.keyPressEvent = self.key_pressed
self.window.setEnabled(True)
def show(self):
self.window.show()
def key_pressed(self, event):
print(event)
Because I cannot extend the object loaded from QUiLoader, I attempted to hijack the keyPressEvent method in that object, and assign it to my own key_pressed method. This doesn't work, but I'm unsure how else to go about capturing keyboard events.
I know I could create MyMainWindow and set it's base class to QMainWindow, and then override the keyPressEvent method, but then I have to do all the layout in code, and I'd much rather leverage the .ui file. How do you go about doing this?
To listen to events from other widgets you must use an eventFilter. Also the initial window is never displayed so it is better to replace it with a QObject.
from PySide2 import QtCore, QtWidgets, QtUiTools
class MyApp(QtCore.QObject):
def __init__(self):
super(MyApp, self).__init__()
self.window = QtUiTools.QUiLoader().load("mainwindow.ui")
self.window.installEventFilter(self)
def show(self):
self.window.show()
def eventFilter(self, obj, event):
if obj is self.window:
if event.type() == QtCore.QEvent.KeyPress:
print(event.key())
return super(MyApp, self).eventFilter(obj, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyApp()
w.show()
sys.exit(app.exec_())
I have communications working between similar widgets from imported child widgets and the parent main window and widgets. However, I am stumped when it comes to a QGraphicsScene widget imported as a module and sub-widget. I have put some simplified files below. So, QGraphicsView (from the QGraphicsScene) will be the actual widget I need to emit and signal events to other QWidgets inside the main window.
If I have all classes in one file, it works but if I have the classes as separate modules, I get "does not have attribute" errors, specifically in the simple version here for QGraphicsScene .viewport
Attribute Error "self.graphicsView.viewport().installEventFilter(self)"
I guess that the composite graphics widget is actually now a QWidget and I am not initialising the imported module functions/attributes for the QGraphicsView element. Thing is, I want it to exist that way so I can separate the GUI elements and functions of different modules. The others I have used so far have been straightforward QWidget to QWidget signals derived from QObjects, so work fine, but I haven't been able to achieve the same with the imported QGraphicsScene to QWidgets since it errors when it tries to reach QGraphicsView within the main window. Again, all fine if all classes exist in one large file.
Any kind person can point out my error here? How can I separate the module scripts to behave the same way as the single script?
Working single script:
# QWidgetAll.py
from PySide import QtGui, QtCore
class GraphicsView(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.graphicsView = QtGui.QGraphicsView(self)
self.graphicsLabel = QtGui.QLabel("Graphics View within QWidget")
self.graphicsView.setMouseTracking(True)
self.graphicsView.viewport().installEventFilter(self)
self.edit = QtGui.QLineEdit(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.graphicsLabel)
layout.addWidget(self.edit)
layout.addWidget(self.graphicsView)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.MouseMove and
source is self.graphicsView.viewport()):
pos = event.pos()
self.edit.setText('x: %d, y: %d' % (pos.x(), pos.y()))
return QtGui.QWidget.eventFilter(self, source, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = GraphicsView()
window.show()
window.resize(200, 100)
sys.exit(app.exec_())
The same file as separate modules. qWidgetView.py errors with attribute error:
# qWidgetView.py
from PySide import QtGui, QtCore
from qGraphicView import GraphicsView
class WidgetView(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.graphicsView = GraphicsView()
self.graphicsView.setMouseTracking(True)
self.graphicsView.viewport().installEventFilter(self)
self.edit = QtGui.QLineEdit(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.edit)
layout.addWidget(self.graphicsView)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.MouseMove and
source is self.graphicsView.viewport()):
pos = event.pos()
self.edit.setText('x: %d, y: %d' % (pos.x(), pos.y()))
return QtGui.QWidget.eventFilter(self, source, event)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = WidgetView()
window.show()
window.resize(200, 100)
sys.exit(app.exec_())
with imported qGraphicView.py module:
# qGraphicView.py
from PySide import QtGui, QtCore
class GraphicsView(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.graphicsView = QtGui.QGraphicsView(self)
self.graphicsLabel = QtGui.QLabel("Graphics View within QWidget")
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.graphicsLabel)
layout.addWidget(self.graphicsView)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = GraphicsView()
window.show()
window.resize(200, 100)
sys.exit(app.exec_())
You need to filter events for the QGraphicsView that is a child widget of the GraphicsView class, because you only want mouse-moves on the graphics-view itself, not the whole container widget. So I would suggest something like this:
The qGraphicView.py module:
class GraphicsView(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.graphicsView = QtGui.QGraphicsView(self)
self.graphicsView.setMouseTracking(True)
self.graphicsLabel = QtGui.QLabel("Graphics View within QWidget")
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.graphicsLabel)
layout.addWidget(self.graphicsView)
def viewport(self):
return self.graphicsView.viewport()
The qWidgetView.py module:
class WidgetView(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.graphicsView = GraphicsView()
self.graphicsView.viewport().installEventFilter(self)
self.edit = QtGui.QLineEdit(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.edit)
layout.addWidget(self.graphicsView)
I have a working script that uses PyQt-5.5.1, which I now want to port to a new PyQt version (5.7). Adapting most of the things was fine, but I faced two major problems: (1) to perform a (simulated) mouseclick, (2) to access (let's say: print) the html source-code of a webpage which is currently displayed in the QWebView or QWebEngineView, respectively.
For example, I could do the following using QWebView in PyQt-5.5.1:
QTest.mouseClick(self.wvTest, Qt.LeftButton, QPoint(x, y))
and
frame = self.wvTest.page().mainFrame()
print(frame.toHtml().encode('utf-8'))
I am aware of the docs as well as this page about porting to QWebEngineView but unable to convert C++ notation to a working Python code.
How can I adapt this to QWebEngineView in PyQt-5.7? Below is a fully working snippet for PyQt-5.5.1, which fails for the new PyQt-version:
for Button1: no mouse click reaction at all.
for Button2: AttributeError: 'QWebEnginePage' object has no attribute 'mainFrame', and when I delete the mainframe(): TypeError: toHtml(self, Callable[..., None]): not enough arguments.
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication
from PyQt5.QtCore import QRect, Qt, QUrl, QPoint, QEvent
from PyQt5.QtTest import QTest
from PyQt5.Qt import PYQT_VERSION_STR
if PYQT_VERSION_STR=='5.5.1': from PyQt5 import QtWebKitWidgets
else: from PyQt5 import QtWebEngineWidgets
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.button1 = QPushButton('Button1', self)
self.button1.clicked.connect(self.buttonOne)
self.button1.setGeometry(QRect(10, 10, 90, 20))
self.button2 = QPushButton('Button2', self)
self.button2.clicked.connect(self.buttonTwo)
self.button2.setGeometry(QRect(110, 10, 90, 20))
if PYQT_VERSION_STR=='5.5.1': self.wvTest = QtWebKitWidgets.QWebView(self)
else: self.wvTest = QtWebEngineWidgets.QWebEngineView(self)
self.wvTest.setGeometry(QRect(10, 40, 430, 550))
self.wvTest.setUrl(QUrl('http://www.startpage.com'))
self.wvTest.setObjectName('wvTest')
self.setGeometry(300, 300, 450, 600)
self.setWindowTitle('WebView minimalistic')
self.show()
def buttonOne(self):
qp = QPoint(38, 314)
QTest.mouseClick(self.wvTest, Qt.LeftButton, pos=qp) # or: QTest.mouseMove(self.wvTest, pos=self.qp)
print('Button1 pressed.')
def buttonTwo(self):
frame = self.wvTest.page().mainFrame()
print(frame.toHtml().encode('utf-8'))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
The QWebEngineView class is not a drop-in replacement for QWebView. As the porting guide makes clear, many of the APIs have fundamentally changed, and some major features are completely missing. This will probably make it impossible to write a compatibility layer unless your browser implementation is very, very simple. It will most likely be easier to write a separate test-suite (unless you don't mind writing a huge amount of conditional code).
To start with, you will need to implement a work-around for QTBUG-43602. The current design of QWebEngineView means that an internal QOpenGLWidget handles mouse-events, so your code will need to get a new reference to that whenever the page is loaded:
class Example(QWidget):
...
def initUI(self):
...
self._glwidget = None
if PYQT_VERSION_STR=='5.5.1':
self.wvTest = QtWebKitWidgets.QWebView(self)
else:
self.wvTest = QtWebEngineWidgets.QWebEngineView(self)
self.wvTest.installEventFilter(self)
...
def eventFilter(self, source, event):
if (event.type() == QEvent.ChildAdded and
source is self.wvTest and
event.child().isWidgetType()):
self._glwidget = event.child()
self._glwidget.installEventFilter(self)
elif (event.type() == QEvent.MouseButtonPress and
source is self._glwidget):
print('web-view mouse-press:', event.pos())
return super().eventFilter(source, event)
def buttonOne(self):
qp = QPoint(38, 314)
widget = self._glwidget or self.wvTest
QTest.mouseClick(widget, Qt.LeftButton, pos=qp)
For accessing the html of the page, you will need some conditional code, because the web-engine API works asynchronously, and requires a callback. Also there are no built-in APIs for handling frames in web-engine (you need to use javascript for that), so everything needs to go through the web-page:
def buttonTwo(self):
if PYQT_VERSION_STR=='5.5.1':
print(self.wvTest.page().toHtml())
else:
self.wvTest.page().toHtml(self.processHtml)
def processHtml(self, html):
print(html)