PyQt ComboBox widget not signalling when empty - python

I have a comboBox and the contents need to change dynamically. I also need to know when the user clicks on the comboBox. When the comboBox has content, it will fire signals, but when it's empty I don't see any signals fired at all. The following code is a toy example demonstrating that for an empty comboBox, no signal will fire.
from PyQt4 import QtCore, QtGui
import sys
class Ui_Example(QtGui.QDialog):
def setupUi(self, Dialog):
self.dialog = Dialog
Dialog.setObjectName("Dialog")
Dialog.resize(300,143)
self.comboBox = QtGui.QComboBox(Dialog)
self.comboBox.setGeometry(QtCore.QRect(60,20,230,20))
self.comboBox.setObjectName("comboBox")
class Ui_Example_Logic(QtGui.QMainWindow):
def __init__(self):
super(Ui_Example_Logic, self).__init__()
def create_main_window(self):
self.ui = Ui_Example()
self.ui.setupUi(self)
self.ui.comboBox.highlighted.connect(self.my_highlight)
self.ui.comboBox.activated.connect(self.my_activate)
def my_highlight(self):
print "Highlighted"
def my_activate(self):
print "Activated"
if __name__ == '__main__':
APP = QtGui.QApplication([])
WINDOW = Ui_Example_Logic()
WINDOW.create_main_window()
WINDOW.show()
sys.exit(APP.exec_())
So for example, if the line below is added to the create_main_window function, "activated" and "highlighted" will print out on expected events, but as the code is now (with the comboBox empty) nothing will print.
self.ui.comboBox.addItems(['a', 'b'])
How can I detect if the user has interacted with the comboBox when it is empty?

If your combobox is empty, no signal will be emitted. But you can installEventFilter for your combobox and reimplement eventfilter (link). First, create filter:
class MouseDetector(QtCore.QObject):
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.MouseButtonPress and obj.count() == 0:
print 'Clicked'
return super(MouseDetector, self).eventFilter(obj, event)
It will be print Clicked when user press mouse button on empty comboBox, created inside Ui_Example. Then, install event:
class Ui_Example(QtGui.QDialog):
def setupUi(self, Dialog):
self.dialog = Dialog
Dialog.setObjectName("Dialog")
Dialog.resize(300,143)
self.comboBox = QtGui.QComboBox(Dialog)
self.comboBox.setGeometry(QtCore.QRect(60,20,230,20))
self.comboBox.setObjectName("comboBox")
self.mouseFilter = MouseDetector()
self.comboBox.installEventFilter(self.mouseFilter)

Related

Internal C++ object (PySide2.QtGui.QContextMenuEvent) already deleted on Qtableview

I have a very simple program that opens a DB and loads a tableview.
So:
Layout file called TestLayouts.py
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'basic.ui'
##
## Created by: Qt User Interface Compiler version 5.14.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide2.QtCore import (QCoreApplication, QMetaObject,
QRect)
from PySide2.QtWidgets import *
class Ui_MainWindow(object) :
def setupUi(self, MainWindow) :
if not MainWindow.objectName() :
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(1034, 803)
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.pushButton = QPushButton(self.centralwidget)
self.pushButton.setObjectName(u"pushButton")
self.pushButton.setGeometry(QRect(920, 730, 89, 25))
self.tableView = QTableView(self.centralwidget)
self.tableView.setObjectName(u"tableView")
self.tableView.setGeometry(QRect(10, 20, 1001, 711))
MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QStatusBar(MainWindow)
self.statusbar.setObjectName(u"statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow) :
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
self.pushButton.setText(QCoreApplication.translate("MainWindow", u"PushButton", None))
# retranslateUi
My main file called tests.py
import sys
import webbrowser
from PySide2 import QtWidgets, QtGui
from PySide2.QtCore import QCoreApplication, QSortFilterProxyModel
from PySide2.QtCore import Slot
from PySide2.QtSql import QSqlDatabase, QSqlQueryModel
from PySide2.QtWidgets import QMenu, QAction
from TestLayouts import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow) :
def __init__(self, parent=None) :
super(MainWindow, self).__init__()
self.setupUi(self)
self.showMaximized()
self.pushButton.clicked.connect(self._basic)
def _basic(self) :
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("data.sqlite")
db.open()
sourceModel = QSqlQueryModel()
sourceModel.setQuery(
"SELECT id,url FROM database",
db)
proxyModel = QSortFilterProxyModel(self)
proxyModel.setSourceModel(sourceModel)
self.tableView.setModel(proxyModel)
self.tableView.setSortingEnabled(True)
#Slot()
def closeEvent(self, event) :
super(MainWindow, self).closeEvent(event)
QCoreApplication.instance().quit()
def contextMenuEvent(self, event) :
self.menu = QMenu(self)
openlinkAction = QAction('Open Link In A Browser', self)
openlinkAction.triggered.connect(lambda : self.openInANewTab(event))
self.menu.addAction(openlinkAction)
self.menu.popup(QtGui.QCursor.pos())
def openInANewTab(self, event) :
self.row = self.tableView.rowAt(event.pos().y())
self.col = self.tableView.columnAt(event.pos().x())
self.cell = self.tableView.item(self.row, self.col)
cellText = self.cell.text()
webbrowser.open(cellText)
if __name__ == "__main__" :
app = QtWidgets.QApplication([])
w = MainWindow()
w.show()
sys.exit(app.exec_())
Once you run the program and click load, the db loads into view. The right click menu is displayed as expected but on clicking I get the error.
RuntimeError: Internal C++ object (PySide2.QtGui.QContextMenuEvent) already deleted.
I have looked at the few threads like this one here which says
If a QObject falls out of scope in Python, it will get deleted. You have to take care of keeping a reference to the object:
Store it as an attribute of an object you keep around, e.g. self.window = QMainWindow()
Pass a parent QObject to the object’s constructor, so it gets owned by the parent
I am not sure how this can be achieved. Any help is appreciated.
Explanation:
In the contextMenuEvent method you are creating a popup and you are showing it but that consumes very little time so when the user selects an option that method is already finished and therefore Qt removes the "event" object as it is no longer needs to. But you are trying to access an item removed by Qt causing those kinds of errors.
Solution:
There are several solutions:
Use exec_() instead of popup() so that the contextMenuEvent method does not finish executing and thus the "event" object is not removed, in addition the QTableView does not have an item() method so it will throw another exception, instead therefore use the index method:
def contextMenuEvent(self, event):
self.menu = QMenu(self)
openlinkAction = QAction("Open Link In A Browser", self)
openlinkAction.triggered.connect(lambda: self.openInANewTab(event))
self.menu.addAction(openlinkAction)
self.menu.exec_(QtGui.QCursor.pos())
def openInANewTab(self, event):
gp = self.mapToGlobal(event.pos())
vp = self.tableView.viewport().mapFromGlobal(gp)
index = self.tableView.indexAt(vp)
if index.isValid():
cellText = index.data()
if isinstance(cellText, str):
webbrowser.open(cellText)
Get the information from the text and pass it to the lambda before displaying the popup so it is no longer necessary to use the event:
def contextMenuEvent(self, event):
gp = self.mapToGlobal(event.pos())
vp = self.tableView.viewport().mapFromGlobal(gp)
index = self.tableView.indexAt(vp)
if not index.isValid():
return
self.menu = QMenu(self)
cellText = index.data()
openlinkAction = QAction("Open Link In A Browser", self)
openlinkAction.triggered.connect(
lambda *args, text=cellText: self.openInANewTab(text)
)
self.menu.addAction(openlinkAction)
self.menu.popup(QtGui.QCursor.pos())
def openInANewTab(self, text):
if isinstance(text, str):
webbrowser.open(text)

Can't use other keys in QPlainTextEdit with keyPressEvent

I'm trying to install a keyPressEvent on a QPlainTextEdit widget such that when type, i type normally and when I hit enter, it will add text to the QPlainTextEdit. I have to files, QtDes.py created by Qt Designer and another file, QtTextEvent.py. These are my files:
QtDes.py:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'QtDes.ui'
#
# Created by: PyQt5 UI code generator 5.13.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtGui, QtWidgets, QtCore
class Ui_MainWindow(object):
def __init__(self, *args, **kwargs):
super(Ui_MainWindow, self).__init__(*args, **kwargs)
self.exactAns = ""
self.approxAns = 0
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 569)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.plainTextEdit = QtWidgets.QPlainTextEdit(self.centralwidget)
font = QtGui.QFont()
font.setFamily("Courier New")
self.plainTextEdit.setFont(font)
self.plainTextEdit.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.plainTextEdit.setTabChangesFocus(False)
self.plainTextEdit.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
self.plainTextEdit.setOverwriteMode(False)
self.plainTextEdit.setTabStopWidth(40)
self.plainTextEdit.setTabStopDistance(40.0)
self.plainTextEdit.setObjectName("plainTextEdit")
self.plainTextEdit.appendPlainText("First Line: ")
self.plainTextEdit.keyPressEvent = self.keyPressEvent
self.gridLayout.addWidget(self.plainTextEdit, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
and QtTextEvent.py:
from PyQt5 import QtCore, QtGui, QtWidgets
from QtDes import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
QtWidgets.QMainWindow.__init__(self, *args, **kwargs)
self.setupUi(self)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return or event.key() == QtCore.Qt.Key_Enter:
print("Enter pressed")
self.plainTextEdit.appendPlainText("New Line: ")
else:
super(MainWindow, self).keyPressEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Pressing enter does what it should do, but pressing other buttons doesn't work. I took the answer from this question and this question. Is there something wrong with my implementation and how could I fix it?
Explanation:
The keyPressEvent method implements the task of adding text to the QPlaintTextEdit when you press a key but you are assigning it another keyPressEvent (from the QMainWindow) that does not implement this logic, that is, it does not add the text. So it is not correct to assign one method to another since you delete the behavior that the widgets have by default.
Solution:
In your case it is only necessary to listen to the keyboard and if you press enter or return then add a text, then to listen to an event you only need an event filter.
To do this you must delete self.plainTextEdit.keyPressEvent = self.keyPressEvent in the QtDes.py file. Also implement the event filter in QtTextEvent.py file:
from PyQt5 import QtCore, QtGui, QtWidgets
from QtDes import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setupUi(self)
self.plainTextEdit.installEventFilter(self)
def eventFilter(self, obj, event):
if obj is self.plainTextEdit and event.type() == QtCore.QEvent.KeyPress:
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
print("Enter pressed")
self.plainTextEdit.appendPlainText("New Line: ")
return True
return super(MainWindow, self).eventFilter(obj, event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Another possible solution is to inherit from QPlainTextEdit and override the keyPressEvent method.
plaintextedit.py
class PlainTextEdit(QtWidgets.QPlainTextEdit):
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
print("Enter pressed")
self.appendPlainText("New Line: ")
return
super(PlainTextEdit, self).keyPressEvent(event)
Then you change to:
QtDes.py
from plaintextedit import PlainTextEdit
# ...
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
# ...
self.plainTextEdit = PlainTextEdit(self.centralwidget)
# ...
(you could also promote it as I indicated in this answer)
You cannot "install a keyPressEvent", and even if you could, it wouldn't work with your approach.
By doing this:
self.plainTextEdit.keyPressEvent = self.keyPressEvent
you are practically doing something like this:
mainWindowInstance.plainTextEdit.keyPress = mainWindowInstance.keyPressEvent
The result is that the event will not be received by the plainTextEdit, but the main window, and since events are always sent back to the parent if not processed, nothing else will happen.
A theoretical solution would be to call the base class implementation against the QPlainTextEdit widget instead:
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return or event.key() == QtCore.Qt.Key_Enter:
print("Enter pressed")
self.plainTextEdit.appendPlainText("New Line: ")
else:
QtWidgets.QPlainTextEdit.keyPressEvent(self.plainTextEdit, event)
Note that I didn't call self.plainTextEdit.keyPressEvent(event), as it would cause a recursion.
This idea is not good anyway, because in this way you're overwriting the QMainWindow keyPressEvent too (which could be a problem if you need it, but that's not the point).
There are two possible (and more "elegant") solutions:
1. Subclass QPlainTextEdit and promote it in Designer
This method allows you to create your UI in designer and set basic parameters for a custom widget that you can extend with your own code; see this answer for an explanation about the procedure.
With this you can just do something like this:
class MyTextEdit(QtWidget.QPlainTextEdit):
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return or event.key() == QtCore.Qt.Key_Enter:
print("Enter pressed")
self.appendPlainText("New Line: ")
else:
super().keyPressEvent(event)
The extra advantage is that, in this way, the code is also more cleaner and easier to implement.
2. Install an event filter on the widget
An event filter is able to "intercept" any event a widget receives, and possibly react to it. In your case it could be something like this:
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
QtWidgets.QMainWindow.__init__(self, *args, **kwargs)
self.setupUi(self)
self.plainTextEdit.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.KeyPress and
event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)):
# ensure that the widget receives the key event
super().eventFilter(source, event)
print("Enter pressed")
self.plainTextEdit.appendPlainText("New Line: ")
# the event is accepted and not sent to its ancestors
return True
return super().eventFilter(source, event)

PySide how to toggle visibility of QTextBrowser

I want to show/hide QTextBrowser widget by pressing a single button. Is there any mean to toggle it? Now I have two buttons; one for displaying the textbrowser and another for hiding it. Buttons are also hided depending on the visibility of the textbrowser. This implementation works as expected, but I think there should/must be a more sophisticated way to implement it. Any suggestions?
def __init__(self, parent=None):
super(Program, self).__init__(parent)
...code...
self.connect(self.showDetailsButton, SIGNAL("clicked()"), self.showTextBrowser)
self.textBrowser.hide() #hide the textbrowser by default
self.resize(461, 200)
self.connect(self.hideDetailsButton, SIGNAL("clicked()"), self.hideTextBrowser)
self.hideDetailsButton.hide() #hide the hideDetailsButton by default
...code...
def showTextBrowser(self):
self.textBrowser.show()
self.hideDetailsButton.show()
self.showDetailsButton.hide()
self.resize(461, 444)
def hideTextBrowser(self):
self.textBrowser.hide()
self.showDetailsButton.show()
self.hideDetailsButton.hide()
self.resize(461, 200)
...code...
Dialog is resized whenever the QTextBrowser widget is shown or hided.
You only need one button, and one handler. Change the text of the button when it's clicked, and use the current visibility of the browser to toggle between the two states.
Here's a working demo:
from PySide import QtCore, QtGui
class Program(QtGui.QWidget):
def __init__(self):
super(Program, self).__init__()
self.textBrowser = QtGui.QTextBrowser(self)
self.button = QtGui.QPushButton('Hide', self)
self.button.clicked.connect(self.toggleTextBrowser)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.textBrowser)
layout.addWidget(self.button)
def toggleTextBrowser(self):
if self.textBrowser.isHidden():
self.button.setText('Hide')
self.textBrowser.show()
self.resize(461, 444)
else:
self.button.setText('Show')
self.textBrowser.hide()
self.resize(461, 200)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Program()
window.setGeometry(500, 300, 461, 444)
window.show()
sys.exit(app.exec_())
Just save the current state and use it to toggle the visibility:
def toggleVisibleTextBrowser(self):
isVisible= !isVisible # declared somewhere else
self.textBrowser.setVisible(isVisible)
if isVisible:
self.resize(461, 444)
else
self.resize(461, 200)
...

PyQt : Prevent a window to be opened several times

I made the simple code below as example. It justs open a new window clicking on a button. I don't find a way to prevent this widget to be re-opened if it is already on the screen. I would like to open a QDialog warning if the window already exists and mainly to have the closeEvent method sending a signal to Mainwidget saying that the new window has been closed. This would allow to open the newWidget again.
import sys
from PyQt4 import QtCore, QtGui
class NewWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(NewWidget,self).__init__(parent)
self.lineEdit = QtGui.QLineEdit('new window',self)
self.resize(200,50)
self.show()
def closeEvent(self,ev):
self.Exit = QtGui.QMessageBox.question(self,
"Confirm Exit...",
"Are you sure you want to exit ?",
QtGui.QMessageBox.Yes| QtGui.QMessageBox.No)
ev.ignore()
if self.Exit == QtGui.QMessageBox.Yes:
ev.accept()
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(MainWidget,self).__init__(parent)
self.button = QtGui.QPushButton("button", self)
self.button.clicked.connect(self.open_new)
def open_new(self):
self.new = NewWidget()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
main = MainWidget()
main.resize(200,50)
main.move(app.desktop().screen().rect().center() - main.rect().center())
main.show()
sys.exit(app.exec_())
I think a better solution is to avoid creating a new window every time you click the button.
One way to do this would be to change the subwindow to a QDialog:
class NewWidget(QtGui.QDialog):
...
and move the resize/show lines into the open_new method:
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None):
...
self._subwindow = None
def open_new(self):
if self._subwindow is None:
self._subwindow = NewWidget(self)
self._subwindow.resize(200, 50)
# move it next to the main window
pos = self.frameGeometry().topLeft()
self._subwindow.move(pos.x() - 250, pos.y())
self._subwindow.show()
self._subwindow.activateWindow()
So now there is only ever one subwindow, which just gets re-activated whenever the button is clicked.
Great. The final solution of my problem looks like this :
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None):
...
self._subwindow = QtGui.Qdialog()
def open_new(self):
if self.subwindow.isVisible() is False:
self._subwindow = NewWidget(self)
self._subwindow.resize(200, 50)
# move it next to the main window
pos = self.frameGeometry().topLeft()
self._subwindow.move(pos.x() - 250, pos.y())
self._subwindow.show()
self._subwindow.activateWindow()

Pyside keypress event for textedit

I have 2 QTextEdit widgets and I need to put whatever is entered in the first to the second on a press of the enter key (Return).... I am not able to implement it please help?
I know I need to use KeyPressEvent but I don't understand how to use it only for the QTextEdit??
self.textEdit = QtGui.QTextEdit(self.widget)
self.textEdit.setMinimumSize(QtCore.QSize(201, 291))
self.textEdit.setMaximumSize(QtCore.QSize(201, 291))
self.textEdit.setObjectName("textEdit")
self.textEdit.setReadOnly(True)
self.verticalLayout.addWidget(self.textEdit)
self.textEdit_2 = QtGui.QTextEdit(self.widget)
self.textEdit_2.setMinimumSize(QtCore.QSize(201, 41))
self.textEdit_2.setMaximumSize(QtCore.QSize(201, 41))
self.textEdit_2.setObjectName("textEdit_2")
self.textEdit_2.setFocusPolicy(Qt.StrongFocus)
self.verticalLayout.addWidget(self.textEdit_2)
Any help is appreciated I am stuck.....
Use the viewportEvent (inherited from QAbstractScrollArea)
self.textEdit.viewportEvent.connect(self.copy_the_text)
def copy_the_text(self, event):
if isinstance(event, QtGui.QKeyEvent): # as viewportEvent gets *all* events
if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
# copy the text from textEdit to textEdit_2
You can use Qt.Key_Enter but I think you probably want Qt.Key_Return
EDIT
If you are using an older version of PySide without new style signals and slots you'll need to use
self.connect(self.textEdit, SIGNAL("viewportEvent(event)"), self.copy_the_text)
Here is a small example that shows QLineEdit and its returnPressed signal. Upon pressing return the text in the QLineEdit will be appended to the QTextEdit:
import sys
from PySide import QtGui
class Window(QtGui.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.output = QtGui.QTextEdit()
self.output.setReadOnly(True)
self.input = QtGui.QLineEdit()
self.input.returnPressed.connect(self.addInput)
self.input.setPlaceholderText('input here')
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.output)
layout.addWidget(self.input)
def addInput(self):
# skip empty text
if self.input.text():
self.output.append(self.input.text())
# clear the QLineEdit for new input
self.input.clear()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())

Categories

Resources