QTextEdit: Overwrite Ctrl-Z with undo() in PyQt4 - python

I subclassed QTextEdit
class Q_My_TextEdit(QtGui.QTextEdit):
def __init__(self, *args):
QtGui.QTextEdit.__init__(self, *args)
def undo(self):
print("undo")
#my own undo code
In my other class:
self.textedit=Q_My_TextEdit()
def keyPressEvent(self,event):
if event.key()==(Qt.Key_Control and Qt.Key_Z):
self.textedit.undo()
If you type some text into your QTextEdit, and hit CTRL-Z, it gets undo-ed, but without calling the function "undo". So how does it work in detail?
The background is, in a second step I want to set new Text (setText()), so the undo stack is deleted. I already have running code that makes the undo itself, but I can't trigger it on CTRL-Z, because with the "Z" the keyshortcut is somehow reserved. For example, if I call my own undo with event.key()==(Qt.Key_Control and Qt.Key_Y), it works.

Ah, So additionally to the keyPressEvent which I have in my second class, you have to put it also into the Subclass!
class Q_My_TextEdit(QtGui.QTextEdit):
def __init__(self, *args):
QtGui.QTextEdit.__init__(self, *args)
def keyPressEvent(self,event):
if event.key()==(Qt.Key_Control and Qt.Key_Z):
self.undo()
#To get the remaining functionality back (e.g. without this, typing would not work):
QTextEdit.keyPressEvent(self,event)
def undo(self):
print("undo")
#my own undo code
But now I can't type into my textedit any more! How can I solve this?
--> Solved. See Properly handling a keyPressEvent in a Subclassed PyQT LineEdit

In C++ you would have to install an event filter, probably with PyQt this is similar (overwrite virtual bool eventFilter(QObject* pObject, QEvent* pEvent); in your editor class). CTRL-Z gets probably filtered by the QTextEdit event filter thus it never gets to keyPressEvent.

Related

Handling QDialog's events safely in PyQT

I'm having troubles with a Qt Application (managed through PyQt5) which used to be reliable, until a bunch of updates (where I ended up with PyQt6). PyQt6 is not the culprit here, as I began to have those problems with later PyQt5 versions.
I'm suspecting my problems are instead linked to an abuse of exec() methods call. The full traceback is not available (I've only a RuntimeError with a cryptic message telling me the Dialog is not available anymore); what (I think) I'm seeing are the parent windows randomly disappearing. This behaviour is indeed mentionned in the Qt documentation:
Note: Avoid using this function; instead, use open(). Unlike exec(), open() is asynchronous, and does not spin an additional event loop. This prevents a series of dangerous bugs from happening (e.g. deleting the dialog's parent while the dialog is open via exec()). When using open() you can connect to the finished() signal of QDialog to be notified when the dialog is closed.
Initially, my app was constructed this way:
QMainWindow A > QDialog B > QDialog C > ...
Each QDialog was launched using .exec() method.
The pseudo code could be summarized this way [edit] :
class A(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ui = Some_Ui_From_QtDesigner()
self.ui.setupUi(self)
self.ui.some_menu.triggered.connect(self.load_popup)
def load_popup():
B = QDialog(A)
if B.exec() == 1:
# Do some things (extract data from B and update A)
if C.exec() == 1:
# Do some things (extract data from C and update B)
qApp = QtWidgets.QApplication.instance()
a = A()
a.show()
qApp.setQuitOnLastWindowClosed(True)
qApp.exec()
I've tried to use .open() methods instead of .exec() and came up with something like this (using QLoopEvent's and events handling):
def process_C_qdialog(parent):
C = QDialog(parent)
# Construct your QDialog some more ...
loop = QtCore.QEventLoop()
my_result = None
def _accept():
my_result = "blah"
loop.exit(0)
def _reject():
loop.exit(-1)
f.accepted.connect(_accept)
f.rejected.connect(_reject)
f.open()
loop.exec()
return my_result
My troubles are far from over as yet. I suspect that recreating manually a QEventLoop causes the same problems than using .exec() in the first place.
So some questions:
Am I right about the QEventLoop?
Would it be safer/enough to leave the parent out of the QDialog generator?
Any advice on how to handle this the right way (keeping a modal dialog if possible)?
More generally, are they any safe ways to reproduce the static methods of Qt through a custom python function? (ie, a function which would load a customized popup, wait for the user's input and returning it)
Note: as this behaviour happens erratically (and without the full traceback available), I'm not 100% sure of this; but I suspect that even a simple QMessageBox().exec() has the same effects, even if this is not stated in the doc.

How to connect QChekBox state to QComboBox state?

I want to enable a combobox - which is disabled from the properties editor in Qt Designer - but, only if the user checks the checkbox. I wrote the following, but it is not working. It is put inside the __init__ method of my mainclass. Could you please help me to understand why?
if self.dlg.checkBox.isChecked():
self.dlg.cmbvectorLayer6.setEnabled(True)
EDIT:
I now have the following in the __init__ method of my main class:
self.dlg.checkBox.stateChanged[int].connect(self.enablecombo)
with enablecombo being:
def enablecombo(self):
self.dlg.cmbvectorLayer6.setEnabled(True)
and it works fine in order to activate the comboboxes. But I am not sure how to do the equivalent in order to disactivate the comboboxes when the checkbox is unchecked...
The QCheckBox class inherits QAbstractButton, so you can use the toggled signal to do what you want:
self.dlg.checkBox.toggled.connect(self.enablecombo)
...
def enablecombo(self, checked):
self.dlg.cmbvectorLayer6.setEnabled(checked)
Or connect to the combo-box directly:
self.dlg.checkBox.toggled.connect(self.dlg.cmbvectorLayer6.setEnabled)
(You can also set up these kinds of direct connections in Qt Designer, by using the Signals and Slots Editing Mode)
if self.dlg.checkBox.isEnabled():
self.dlg.cmbvectorLayer6.setEnabled(True)
You checking the state is checked but you need to check isEnabled
self.dlg.checkBox.stateChanged[int].connect(self.checkcombo)
whatewer is the current state , just call a function which checks it and then based on its output enable/disable it
def checkcombo():
if self.dlg.checkBox.isChecked():
self.dlg.cmbvectorLayer6.setEnabled(True)
else:
self.dlg.cmbvectorLayer6.setEnabled(False)

Overriding/Reimplementing Slots in PySide

I have almost the exact same question as the one found here:
Override shouldInterruptJavaScript in QWebPage with PySide
In my case though I want to override the copy and paste slots on QLineEdit
import sys
from PySide import QtGui, QtCore
class myLineEdit(QtGui.QLineEdit):
# FIXME: This is not working, the slot is not overriden!
#QtCore.Slot()
def copy(self):
print 'overridden copy event'
App.clipboard().setText('customized text')
return False
#QtCore.Slot()
def paste(self):
print 'overridden paste event'
self.setText('customized text')
return False
if __name__ == "__main__":
App = QtGui.QApplication(sys.argv)
Widget = myLineEdit()
Widget.show()
cmenu = Widget.createStandardContextMenu()
sys.exit(App.exec_())
I'm using Python 2.7.3, with PySide 1.2.2
I don't know if these methods are even supposed to be override-able, but I can't find any documentation that says they shouldn't be.
I also found this page
http://qt-project.org/faq/answer/is_it_possible_to_reimplement_non-virtual_slots
The page explains how certain kinds of slots get pointers set to them by functions that get called when the object is initialized (or something along those lines, I'm not as familiar with the C++).
Following this logic I added the createStandardContextMenu() call above in the hopes that it would reinitialize the slots for at least the context menu, but no luck.
Am I doing something wrong? Or should I try filing a bug report?
You cannot override QLineEdit.copy or QLineEdit.paste in such a way that they will be called internally by Qt.
In general, you can only usefully override or reimplement Qt functions that are defined as being virtual. The Qt Docs will always specify whether this is the case, and for QLineEdit, there are no public slots that are defined in that way.
There is also no easy workaround. There are a lot of different ways in which copy and paste operations (or their equivalents) can be invoked, such as keyboard shortcuts, context menu, drag and drop, etc. It can be done: but it's a lot of work to get complete control over all these sorts of operations. So you need to think carefully about what you're trying to achieve before deciding how to proceed.

How to Autoresize QLabel pixmap keeping ratio without using classes?

We are making a GUI using PyQt and Qt Designer. Now we need that an image(pixmap) placed in a QLabel rescales nicely keeping ratio when the window is resized.
I've been reading other questions/answers but all of them use extended classes. As we are making constant changes in our UI, and it's created with Qt Creator, the .ui and (corresponding).py files are automatically generated so, if I'm not wrong, using a class-solution is not a good option for us because we should manually change the name of the class each time we update the ui.
Is there any option to autoresize the pixmap in a QLAbel keeping the ratio and avoiding using extended clases?
Thanks.
There are a couple of ways to do this.
Firstly, you can promote your QLabel in Qt Designer to a custom subclass that is written in python. Right-click the QLabel and select "Promote to...", then give the class a name (e.g. "ScaledLabel") and set the header file to the python module that the custom subclass class will be imported from (e.g. 'mylib.classes').
The custom subclass would then re-implement the resizeEvent like this:
class ScaledLabel(QtGui.QLabel):
def __init__(self, *args, **kwargs):
QtGui.QLabel.__init__(self)
self._pixmap = QtGui.QPixmap(self.pixmap())
def resizeEvent(self, event):
self.setPixmap(self._pixmap.scaled(
self.width(), self.height(),
QtCore.Qt.KeepAspectRatio))
For this to work properly, the QLabel should have its size policy set to expanding or minimumExpanding, and the minimum size should be set to a small, non-zero value (so the image can be scaled down).
The second method avoids using a subclass and uses an event-filter to handle the resize events:
class MainWindow(QtGui.QMainWindow):
def __init__(self):
...
self._pixmap = QtGui.QPixmap(self.label.pixmap())
self.label.installEventFilter(self)
def eventFilter(self, widget, event):
if (event.type() == QtCore.QEvent.Resize and
widget is self.label):
self.label.setPixmap(self._pixmap.scaled(
self.label.width(), self.label.height(),
QtCore.Qt.KeepAspectRatio))
return True
return QtGui.QMainWindow.eventFilter(self, widget, event)
Set background-image:, background-repeat: and background-position QSS properties for your label. You may do it via Forms editor or in code QWidget::setStyleSheet.
A good starting point for QSS (with examples) - http://doc.qt.io/qt-5/stylesheet-reference.html
One way is to create a QWidget/QLabel subclass and reimplement the resizeEvent.
void QWidget::resizeEvent(QResizeEvent * event) [virtual protected]
This event handler can be reimplemented in a subclass to receive widget resize events which are passed in the event parameter. When resizeEvent() is called, the widget already has its new geometry. The old size is accessible through QResizeEvent::oldSize().
The widget will be erased and receive a paint event immediately after processing the resize event. No drawing need be (or should be) done inside this handler.
This would need to be done in C++ though, not PyQt.
Having that done, you could add your custom widget to the QtDesigner as follows:
Using Custom Widgets with Qt Designer
Incredibly, after seven years #ekhumoro's excellent answer is still pretty much the only working Python implementation that can be found around; everyone else tells what to do, but nobody gives the actual code.
In spite of this, it did not work at first for me, because I happened to have the pixmap generation somewhere else in the code - specifically, my pixmap was generated inside a function which was only activated when clicking on a button, so not during the window intialization.
After figuring out how #ekhumoro's second method worked, I edited it in order to accomodate this difference. In pratice I generalised the original code, also because I did not like (for efficiency reasons) how it added a new _pixmap attribute to the label, which seemed to be nothing more than a copy of the original pixmap.
The following his is my version; mind that I have not fully tested it, but since it is a shorter version of my original working code, it too should work just fine (corrections are welcome, though):
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Initialize stuff here; mind that no pixmap is added to the label at this point
def eventFilter(self, widget, event):
if event.type() == QEvent.Resize and widget is self.label:
self.label.setPixmap(self.label.pixmap.scaled(self.label.width(), self.label.height(), aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
return True
return QMainWindow.eventFilter(self, widget, event)
def apply_pixelmap(self, image): # This is where the pixmap is added. For simplicity, suppose that you pass a QImage as an argument to this function; however, you can obtain this in any way you like
pixmap = QPixmap.fromImage(image).scaled(new_w, new_h, aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
self.label.setPixmap(pixmap)
self.label.pixmap = QPixmap(pixmap) # I am aware that this line looks like a redundancy, but without it the program does not work; I could not figure out why, so I will gladly listen to anyone who knows it
self.label.installEventFilter(self)
return
This works by setting the ScaledContents property to False and the SizePolicy to either Expanding or Ignored. Note that it might not work if the label containing the image is not set as the central widget (self.setCentralWidget(self.label), where self refers to MainWindow).

PyQt: updating GUI from a callback

Using Python3 and PyQt4 I have a function (run) that takes as an input a callable to provide status updates.
class Windows(QtGui.QWidget):
# Creates a widget containing:
# - a QLineEdit (status_widget)
# - a button, connected to on_run_clicked
def on_run_clicked(self):
def update(text):
self.widget.setText(text)
threading.Thread(target=run, args=(update, )).start()
This works ok (i.e. the text updates are displayed properly in the widget). However, when I replace QLineEdit by QTextEdit and use the append method to add text, I get:
QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)
It still works but point to the fact that I am doing something wrong, and I am not sure that I will keep working when more threads are active. Usually, I do this type of updates using signals and slots, but the run function is not PyQt specific. The questions are:
Why does it work without a warning for QLineEdit and not for
QTextEdit?
What is the right way to deal with a situation like this?
I don't know the specific reason why one class works and the other doesn't - nor do I really know the difference between using Python threading vs. Qt's threading...however, I can tell you that it is very tempremental if you don't set it up properly. Namely, you cannot (or at the very least, should not) modify GUI objects from a thread. Again, not sure the difference of a python vs. a Qt thread on that. But, the safe way to modify your interface from a GUI is by sending signals to your window...easiest way I know to do this is via the Qt threading.
class MyThread(QtCore.QThread):
updated = QtCore.pyqtSignal(str)
def run( self ):
# do some functionality
for i in range(10000):
self.updated.emit(str(i))
class Windows(QtGui.QWidget):
def __init__( self, parent = None ):
super(Windows, self).__init__(parent)
self._thread = MyThread(self)
self._thread.updated.connect(self.updateText)
# create a line edit and a button
self._button.clicked.connect(self._thread.start)
def updateText( self, text ):
self.widget.setText(text)

Categories