I am experiencing a rather strange issue with my PyQT QTextEdit.
When I enter a string from my QLineEdit it adds it but say I enter another the first string disappears I assume that's because I am not appending the text.
Any idea how I can do this?
Here is the relevant code:
self.mytext.setText(str(self.user) + ": " + str(self.line.text()) + "\n")
and the important one
self.mySignal.emit(self.decrypt_my_message(str(msg)).strip() + "\n")
Edit
I figured it out I needed to use a QTextCursor
self.cursor = QTextCursor(self.mytext.document())
self.cursor.insertText(str(self.user) + ": " + str(self.line.text()) + "\n")
The setText() method replaces all the current text, so you just need to use the append() method instead. (Note that both these methods automatically add a trailing newline).
import sys
from PyQt4 import QtGui
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QVBoxLayout(self)
self.button = QtGui.QPushButton('Test')
self.edit = QtGui.QTextEdit()
layout.addWidget(self.edit)
layout.addWidget(self.button)
self.button.clicked.connect(self.handleTest)
def handleTest(self):
self.edit.append('spam: spam spam spam spam')
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
Related
How to work with setFormats?
The program displays QTextEdit in QMainWindow.
The task is to find the word "import" and highlight it in red using block.layout.setFormats (I don't want the undo-redo history to include the appearance change, and I don't want to use QSyntaxHighlighter).
I don't understand why when finding the word "import" and then setFormats, the corresponding block becomes invisible.
from PySide6 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.central_widget = QtWidgets.QWidget(self)
self.central_widget.setLayout(QtWidgets.QVBoxLayout(self.central_widget))
self.text_editor = QtWidgets.QTextEdit(self.central_widget)
self.central_widget.layout().addWidget(self.text_editor)
self.setCentralWidget(self.central_widget)
self.text_editor.setFont(QtGui.QFont('Arial', 14))
self.text_editor.textChanged.connect(self.text_changed)
#QtCore.Slot()
def text_changed(self):
word = 'import'
text_cursor_before_find_op = self.text_editor.textCursor()
self.text_editor.moveCursor(QtGui.QTextCursor.MoveOperation.Start)
found = self.text_editor.find(word)
if found:
text_cursor = self.text_editor.textCursor()
text_cursor.setPosition(text_cursor.position())
block = text_cursor.block()
position_in_block = text_cursor.positionInBlock() - len(word)
format_range = QtGui.QTextLayout.FormatRange()
format_range.start = position_in_block
format_range.length = len(word)
format_range.format = self.text_editor.currentCharFormat()
format_range.format.setForeground(QtGui.QColor('#FF0000'))
formats = [format_range]
block.layout().setFormats(formats)
print(position_in_block,
repr(block.text()),
(format_range.start, format_range.length, format_range.format),
block.isValid(), block.isVisible(),
sep='\n', end='\n\n')
self.text_editor.setTextCursor(text_cursor_before_find_op)
app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
I have no idea why block.layout.setFormats() does not work.
But if your intention is to highlight the word "import", then you might want to use something like this.
from PySide6 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.central_widget = QtWidgets.QWidget(self)
self.central_widget.setLayout(QtWidgets.QVBoxLayout(self.central_widget))
self.text_editor = QtWidgets.QTextEdit(self.central_widget)
self.central_widget.layout().addWidget(self.text_editor)
self.setCentralWidget(self.central_widget)
self.text_editor.setFont(QtGui.QFont('Arial', 14))
self.text_editor.textChanged.connect(self.text_changed)
#QtCore.Slot()
def text_changed(self):
word = 'import'
text_cursor = self.text_editor.document().find(word)
if text_cursor:
extraSelection = QtWidgets.QTextEdit.ExtraSelection()
extraSelection.cursor = text_cursor
extraSelection.format = self.text_editor.currentCharFormat()
extraSelection.format.setForeground(QtGui.QColor('#FF0000'))
self.text_editor.setExtraSelections([extraSelection])
else:
self.text_editor.setExtraSelections([])
app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
Of course this has some limitations, e.g. finding only the first occurrence in the whole document, not checking if the word is delimited by whitespace, keeping format after changing the word to something else etc. But you will need to resolve these yourself. Your original coude would have the same limitations.
I have a similar problem to Using lambda expression to connect slots in pyqt. In my case, I want to send two different pieces of information. Which button is being clicked (the port number) and the sensor associated with that port number (stored in a QComboBox).
This is what I want to achieve and this works fine:
self.portNumbers[0].clicked.connect(lambda: self.plot(1, self.sensorNames[0].currentText()))
self.portNumbers[1].clicked.connect(lambda: self.plot(2, self.sensorNames[1].currentText()))
self.portNumbers[2].clicked.connect(lambda: self.plot(3, self.sensorNames[2].currentText()))
...
But when I put this in a loop as this:
for i, (portNumber, sensorName) in enumerate(zip(self.portNumbers, self.sensorNames)):
portNumber.clicked.connect(lambda _, x=i + 1, y=sensorName.currentText(): self.plot(x, y))
I get the correct port numbers but the change in the Combo box is not reflected.
Minimum reproducible code:
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox
portNumbers = [None] * 8
sensorNames = [None] * 8
SENSORS = ["Temperature", "Pressure", "Height"]
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.connectButtonsToGraph()
def init_ui(self):
vbox = QVBoxLayout()
h1box = QHBoxLayout()
h2box = QHBoxLayout()
for i, (portNumber, sensorName) in enumerate(zip(portNumbers, sensorNames)):
# Set portNumber as pushButton and list sensorNames in ComboBox
portNumber = QPushButton()
sensorName = QComboBox()
h1box.addWidget(portNumber)
h2box.addWidget(sensorName)
# Give identifier, text info to individual ports and modify the list
portNumberName = "port_" + str(i + 1)
portNumber.setObjectName(portNumberName)
portNumberText = "Port " + str(i + 1)
portNumber.setText(portNumberText)
portNumbers[i] = portNumber
# Add the textual information in PushButton and add modify the list
sensorNameStringName = "portSensorName_" + str(i + 1)
sensorName.setObjectName(sensorNameStringName)
for counter, s in enumerate(SENSORS):
sensorName.addItem("")
sensorName.setItemText(counter, s)
sensorNames[i] = sensorName
vbox.addLayout(h1box)
vbox.addLayout(h2box)
self.setLayout(vbox)
self.show()
def connectButtonsToGraph(self):
for i, (portNumber, sensorName) in enumerate(zip(portNumbers, sensorNames)):
portNumber.clicked.connect(lambda _, x=i + 1, y=sensorName.currentText(): self.plot(x, y))
def plot(self, portNumber, sensorName):
print(portNumber, sensorName)
def run():
app = QApplication([])
mw = MyWidget()
app.exec_()
if __name__ == '__main__':
run()
Thank you, #musicamante, for the explanation. I am just adding it in the answer to mark it as solved if someone has similar issues.
As they mentioned, the value of the keyword argument of lambda is evaluated when it's created. So, creating a reference to sensorName.currentText() used the value of the current text at that time. But creating a reference to the ComboBox itself allowed for getting the value of the text in run-time.
I have a QLineEdit widget to enter text (a simple search pattern) that can include spaces.
You can't see the space characters, this is especially obvious for trailing spaces. I know you can set option ShowTabsAndSpaces by calling method setDefaultTextOption on the document of a QTextEdit widget, but is there a way to set this option (or something similar) on a QLineEdit widget?
Method setDefaultTextOption is not available for QLineEdit widgets, so I tried:
item = QLineEdit(value)
option = QTextOption()
option.setFlags(QTextOption.ShowTabsAndSpaces)
item.initStyleOption(option)
But that gives me an exception:
<class 'TypeError'>:
'PySide2.QtWidgets.QLineEdit.initStyleOption' called with wrong argument types:
PySide2.QtWidgets.QLineEdit.initStyleOption(PySide2.QtGui.QTextOption)
Supported signatures:
PySide2.QtWidgets.QLineEdit.initStyleOption(PySide2.QtWidgets.QStyleOptionFrame)
How do I set option ShowTabsAndSpaces on a QLineEdit widget or is there another way to make space characters visible in a QLineEdit widget. I would be happy with only space characters that are visible.
Used the suggestion of #ekhumoro to port https://stackoverflow.com/a/56298439/4459346
This resulted in:
#!/usr/bin/env python3
import sys
import time
from PySide2.QtWidgets import (QLineEdit, QPushButton, QApplication,
QVBoxLayout, QDialog, QMessageBox, QPlainTextEdit, QGridLayout)
from PySide2.QtCore import (Qt, QMimeData)
from PySide2.QtGui import (QTextOption, QFontMetrics)
# source: https://stackoverflow.com/a/56298439/4459346
class SpacesLineEdit(QPlainTextEdit):
def __init__(self, text=None):
if text:
super().__init__(text)
else:
super().__init__()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setLineWrapMode(QPlainTextEdit.NoWrap)
self.setTabChangesFocus(True)
option = QTextOption()
option.setFlags(QTextOption.ShowTabsAndSpaces)
self.document().setDefaultTextOption(option)
# limit height to one line
self.setHeight(1)
# Stealing the sizeHint from a plain QLineEdit will do for now :-P
self._sizeHint = QLineEdit().sizeHint()
def minimumSizeHint(self):
return self._sizeHint
def sizeHint(self):
return self._sizeHint
def keyPressEvent(self, event):
if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
event.ignore()
return
super().keyPressEvent(event)
def insertFromMimeData(self, source):
text = source.text()
text = text.replace(str('\r\n'), str(' '))
text = text.replace(str('\n'), str(' '))
text = text.replace(str('\r'), str(' '))
processedSource = QMimeData()
processedSource.setText(text)
super().insertFromMimeData(processedSource)
def setText(self, text):
self.setPlainText(text)
def text(self):
return self.toPlainText()
def setHeight1(self):
# see
doc = self.document()
b = doc.begin()
layout = doc.documentLayout()
h = layout.blockBoundingRect(b).height()
self.setFixedHeight(h + 2 * self.frameWidth() + 1)
def setHeight(self, nRows):
# source: https://stackoverflow.com/a/46997337/4459346
pdoc = self.document()
fm = QFontMetrics(pdoc.defaultFont())
lineSpacing = fm.lineSpacing()
print(lineSpacing) # todo:test
margins = self.contentsMargins()
nHeight = lineSpacing * nRows + \
(pdoc.documentMargin() + self.frameWidth()) * 2 + \
margins.top() + margins.bottom()
self.setFixedHeight(nHeight)
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
demoText = " some text with spaces "
self.lineedit0 = QLineEdit(demoText)
self.lineedit1 = SpacesLineEdit(demoText)
self.lineedit2 = QLineEdit(demoText)
self.lineedit3 = QLineEdit(demoText)
layout = QGridLayout()
layout.addWidget(self.lineedit0, 0, 0)
layout.addWidget(self.lineedit1, 1, 0)
layout.addWidget(self.lineedit2, 0, 1)
layout.addWidget(self.lineedit3, 1, 1)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Form()
print('starting app...')
form.show()
sys.exit(app.exec_())
The spaces are now shown, just as I wanted.
The only thing I had to add was a method to set the height. See method setHeight, which I copied from https://stackoverflow.com/a/46997337/4459346.
The original method tries to calculate the exact line height, but seems it doesn't get the height exactly right:
The one on the bottom-left is the QPlainTextEdit, the other three are just qlineEdit widgets.
I can fix the incorrect height by subtracting a few pixels, but I rather not do that without knowing what I'm doing.
Anyone, any suggestions to improve the code in method setHeight?
I try to extend the contextmenu of a QLineEdit with an additional entry for replacing text. I can extend the contextmenu with .createStandardContextMenu(), which works fine. But when I try to add a shortcut with .setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) it will not react on the key. Same with different keys, which I tried. In addition the shortcut made with QAction('&Replace', self) doesn't work too.
Some examples here in SO and other sources are constructed in the same way, so I'm wondering that nobody else has got the same problem. Seems that I'm missing anything. But what? I can't figure out, checked the docs multiple times.
Working Example:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
print("my_contextMenuEvent")
menu = self.lineEdit.createStandardContextMenu()
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
menu.addAction(action)
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()
Based on musicamante's comment I came to the following solution:
Extract from the docs:
If you want to extend the standard context menu, reimplement this
function, call createStandardContextMenu() and extend the menu
returned.
The default use of the QAction list (as returned by actions()) is to
create a context QMenu.
It's not totally logically to me, not for the 1st time ;-)
Final code:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
self.lineEdit.addAction(action)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
menu = self.lineEdit.createStandardContextMenu()
menu.addActions(self.lineEdit.actions())
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()
I am implement my project using pyqt5. Currently, I have a window including many widget. Now, I want to remove some widgets. The window looks like:
Now, I want to remove the 'name1' widget including the QLabel and QPushButton.
However, after removing all 'name1' widgets, the 'name2' widgets including QLabel and QPushButton can not self-adapte with the window, like:
All my code is:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Window(QDialog):
def __init__(self):
super().__init__()
self.initGUI()
self.show()
def initGUI(self):
layout = QVBoxLayout()
self.setLayout(layout)
removeLayout = QHBoxLayout()
self.__removeText = QLineEdit()
self.__removeBtn = QPushButton('Remove')
self.__removeBtn.clicked.connect(self.remove)
removeLayout.addWidget(self.__removeText)
removeLayout.addWidget(self.__removeBtn)
ROIsLayout = QVBoxLayout()
for name in ['name1', 'name2']:
subLayout = QHBoxLayout()
subText = QLabel(name)
subText.setObjectName(name)
subBtn = QPushButton(name)
subBtn.setObjectName(name)
subLayout.addWidget(subText)
subLayout.addWidget(subBtn)
ROIsLayout.addLayout(subLayout)
layout.addLayout(removeLayout)
layout.addLayout(ROIsLayout)
self.__ROIsLayout = ROIsLayout
def remove(self, checked=False):
name = self.__removeText.text()
while True:
child = self.__ROIsLayout.takeAt(0)
if child == None:
break
while True:
subChild = child.takeAt(0)
if subChild == None:
break
obName = subChild.widget().objectName()
if name == obName:
widget = subChild.widget()
widget.setParent(None)
child.removeWidget(widget)
self.__ROIsLayout.removeWidget(widget)
del widget
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
update:
Actually, the issue may be the takeAt. The following code is workable:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Window(QDialog):
def __init__(self):
super().__init__()
self.initGUI()
self.show()
def initGUI(self):
layout = QVBoxLayout()
self.setLayout(layout)
removeLayout = QHBoxLayout()
self.__removeText = QLineEdit()
self.__removeBtn = QPushButton('Remove')
self.__removeBtn.clicked.connect(self.remove)
removeLayout.addWidget(self.__removeText)
removeLayout.addWidget(self.__removeBtn)
ROIsLayout = QVBoxLayout()
for name in ['name1', 'name2']:
subLayout = QHBoxLayout()
subLayout.setObjectName(name)
subText = QLabel(name, parent=self)
subText.setObjectName(name)
subBtn = QPushButton(name, parent=self)
subBtn.setObjectName(name)
subLayout.addWidget(subText)
subLayout.addWidget(subBtn)
ROIsLayout.addLayout(subLayout)
print(name, subLayout, subText, subBtn)
layout.addLayout(removeLayout)
layout.addLayout(ROIsLayout)
self.__ROIsLayout = ROIsLayout
self.record = [subLayout, subText, subBtn]
def remove(self, checked=False):
layout = self.record[0]
txt = self.record[1]
btn = self.record[2]
layout.removeWidget(txt)
txt.setParent(None)
txt.deleteLater()
layout.removeWidget(btn)
btn.setParent(None)
btn.deleteLater()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
But, I have printed the QLabel/QPushButton in the self.record, and I find it is the same with that from child.takeAt(0).widget().
The main issue in your code is that you're constantly using takeAt(). The result is that all items in the __ROIsLayout layout will be removed from it (but not deleted), which, in your case, are the sub layouts. This is clearly not a good approach: only the widgets with the corresponding object name will be actually deleted, while the others will still be "owned" by their previous parent, will still be visible at their previous position and their geometries won't be updated since they're not managed by the layout anymore.
There are multiple solutions to your question, all depending on your needs.
If you need to remove rows from a layout, I'd consider setting the object name on the layout instead, and look for it using self.findChild().
Also consider that, while Qt allows setting the same object name for more than one object, that's not suggested.
Finally, while using del is normally enough, it's usually better to call deleteLater() for all Qt objects, which ensures that Qt correctly removes all objects (and related parentship/connections).
Another possibility, for this specific case, is to use a QFormLayout.