How to Auto-scroll QT Text Field Widget - python

A code below creates a window with QTextBrowser widget.
For five seconds the new lines of text added to QTextBrowser using
its .insertPlainText() method.
As soon as text fills an entire field QTextBrowser automatically adds a scroll bar. But it does nothing to scroll down so the last line of text would be always visible (or readable) by the user.
I would like to know how to make QTestBrowser scroll down automatically so the last line of the text is always within its text field or visible. How to achieve it?
Screenshot:
Code:
import sys, time
from PyQt4 import QtCore, QtGui
class MyDialog(QtGui.QDialog):
def __init__( self, parent = None ):
super(MyDialog, self).__init__(parent)
self._console = QtGui.QTextBrowser(self)
layout = QtGui.QVBoxLayout()
layout.addWidget(self._console)
self.setLayout(layout)
def updateField(self):
for m in range(5):
for n in range(100):
self._console.insertPlainText('%s : '%n)
QtGui.qApp.processEvents()
time.sleep(1)
if ( __name__ == '__main__' ):
app = None
if ( not QtGui.QApplication.instance() ):
app = QtGui.QApplication([])
dlg = MyDialog()
dlg.show()
dlg.updateField()
if ( app ): app.exec_()

Just use .append instead of .insertPlainText to ensure auto scrolling:
self._console.append('%s : '%n)

Since QTextBrowser is sub-classed from QTextEdit widget be we can use its .ensureCursorVisible() method.
To make QTextBrowser scroll down automatically as it is being filled with text all we need to do is to add an extra line to enable its property responsible for this feature:
self._console.ensureCursorVisible()
Here is the working code solution:
import sys, time
from PyQt4 import QtCore, QtGui
class MyDialog(QtGui.QDialog):
def __init__( self, parent = None ):
super(MyDialog, self).__init__(parent)
self._console = QtGui.QTextBrowser(self)
self._console.ensureCursorVisible()
layout = QtGui.QVBoxLayout()
layout.addWidget(self._console)
self.setLayout(layout)
def updateField(self):
for m in range(5):
for n in range(100):
self._console.insertPlainText('%s : '%n)
QtGui.qApp.processEvents()
time.sleep(1)
if ( __name__ == '__main__' ):
app = None
if ( not QtGui.QApplication.instance() ):
app = QtGui.QApplication([])
dlg = MyDialog()
dlg.show()
dlg.updateField()
if ( app ): app.exec_()

Related

Pyqt5 - how to go back to hided Main Window from Secondary Window?

If I click Back from the second window, the program will just exit. How do I go back to mainwindow in this case? I assume I will need some more code in that clickMethodBack function.
import os
import PyQt5
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QPushButton
import time
from PyQt5.QtCore import QSize
class GUI_Window():
def __init__(self):
self.main_window()
return
def main_window(self):
app = PyQt5.QtWidgets.QApplication(sys.argv)
self.MainWindow = MainWindow_()
self.MainWindow.show()
app.exec_()
return
class MainWindow_(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.TestAButton = QPushButton("TestA", self)
self.TestAButton.clicked.connect(self.TestA_clickMethod)
self.TestAButton.move(20, 0)
self.CloseButton = QPushButton("Close", self)
self.CloseButton.clicked.connect(self.Close_clickMethod)
self.CloseButton.move(20, 40)
self.TestingA = TestA_MainWindow()
def TestA_clickMethod(self):
self.TestAButton.setEnabled(False)
time.sleep(0.2)
self.TestingA.show()
self.hide()
try:
if self.TestingA.back == True:
self.show()
except:
None
def Close_clickMethod(self):
self.Test_Choice = 'Exit'
self.close()
class TestA_MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setMinimumSize(QSize(980,700))
self.setWindowTitle("TestA")
self.Back_Button = False
self.closeButton = QPushButton("Close", self)
self.closeButton.clicked.connect(self.clickMethodClose)
self.returnButton = QPushButton("Back", self)
self.returnButton.clicked.connect(self.clickMethodBack)
self.returnButton.move(0,30)
def clickMethodClose(self):
self.Back_Button = False
self.close()
def clickMethodBack(self):
self.returnButton.setEnabled(False)
time.sleep(0.5)
self.back = True
self.close()
# Run if Script
if __name__ == "__main__":
main = GUI_Window() # Initialize GUI
Your code has two very important issues.
you're using a blocking function, time.sleep; Qt, as almost any UI toolkit, is event driven, which means that it has to be able to constantly receive and handle events (coming from the system or after user interaction): when something blocks the event queue, it completely freezes the whole program until that block releases control;
you're checking for the variable too soon: even assuming the sleep would work, you cannot know if the window is closed after that sleep timer has ended;
The solution is to use signals and slots. Since you need to know when the second window has been closed using the "back" button, create a custom signal for the second window that will be emitted whenever the function that is called by the button is closed.
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(central)
self.testButton = QtWidgets.QPushButton('Test A')
self.closeButton = QtWidgets.QPushButton('Close')
layout.addWidget(self.testButton)
layout.addWidget(self.closeButton)
self.setCentralWidget(central)
self.testButton.clicked.connect(self.launchWindow)
self.closeButton.clicked.connect(self.close)
def launchWindow(self):
self.test = TestA_MainWindow()
self.test.backSignal.connect(self.show)
self.hide()
self.test.show()
class TestA_MainWindow(QtWidgets.QWidget):
backSignal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
self.closeButton = QtWidgets.QPushButton('Close')
self.backButton = QtWidgets.QPushButton('Back')
layout.addWidget(self.closeButton)
layout.addWidget(self.backButton)
self.closeButton.clicked.connect(self.close)
self.backButton.clicked.connect(self.goBack)
def goBack(self):
self.close()
self.backSignal.emit()
def GUI_Window():
import sys
app = QtWidgets.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
GUI_Window()
Notes:
I removed the GUI_Window class and made a function, as using a class for that is not really useful;
you should always prefer layout managers instead of setting manual geometries;
widgets should not be added to a QMainWindow as direct children, and a central widget should always be used (see the creation and use of central in the example); read more about it in the documentation;
only classes and constants should be capitalized, while variables, attributes and functions should always have names starting with a lowercase letter;

QFileDialog.getOpenFileName change button text from 'Open' to 'Remove'

I am using QFileDialog.getOpenFileName(self,'Remove File', "path", '*.pdf') to select a file and get the path in order to remove it from my application. The issue is the QFileDialog.getOpenFileName window button says 'Open' when selecting a file which will be confusing to the user.
Is there any way to change the button text from 'Open' to 'Remove'/'Delete'
When using the static method QFileDialog::getOpenFileName() the first thing is to obtain the QFileDialog object and for that we use a QTimer and the findChild() method:
# ...
QtCore.QTimer.singleShot(0, self.on_timeout)
filename, _ = QtWidgets.QFileDialog.getOpenFileName(...,
options=QtWidgets.QFileDialog.DontUseNativeDialog)
def on_timeout(self):
dialog = self.findChild(QtWidgets.QFileDialog)
# ...
Then you can get the text iterating over the buttons until you get the button with the searched text and change it:
for btn in dialog.findChildren(QtWidgets.QPushButton):
if btn.text() == "&Open":
btn.setText("Remove")
That will work at the beginning but every time you interact with the QTreeView they show, update the text to the default value, so the same logic will have to be applied using the currentChanged signal from the selectionModel() of the QTreeView but for synchronization reasons it is necessary Update the text later using another QTimer.singleShot(), the following code is a workable example:
import sys
from PyQt5 import QtCore, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton("Press me")
button.clicked.connect(self.on_clicked)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(button)
#QtCore.pyqtSlot()
def on_clicked(self):
QtCore.QTimer.singleShot(0, self.on_timeout)
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
"Remove File",
"path",
"*.pdf",
options=QtWidgets.QFileDialog.DontUseNativeDialog,
)
def on_timeout(self):
dialog = self.findChild(QtWidgets.QFileDialog)
dialog.findChild(QtWidgets.QTreeView).selectionModel().currentChanged.connect(
lambda: self.change_button_name(dialog)
)
self.change_button_name(dialog)
def change_button_name(self, dialog):
for btn in dialog.findChildren(QtWidgets.QPushButton):
if btn.text() == self.tr("&Open"):
QtCore.QTimer.singleShot(0, lambda btn=btn: btn.setText("Remove"))
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
The first step can be avoided if the static method is not used and create the dialog using an instance of QFileDialog:
import sys
from PyQt5 import QtCore, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton("Press me")
button.clicked.connect(self.on_clicked)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(button)
#QtCore.pyqtSlot()
def on_clicked(self):
dialog = QtWidgets.QFileDialog(
self,
"Remove File",
"path",
"*.pdf",
supportedSchemes=["file"],
options=QtWidgets.QFileDialog.DontUseNativeDialog,
)
self.change_button_name(dialog)
dialog.findChild(QtWidgets.QTreeView).selectionModel().currentChanged.connect(
lambda: self.change_button_name(dialog)
)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
filename = dialog.selectedUrls()[0]
print(filename)
def change_button_name(self, dialog):
for btn in dialog.findChildren(QtWidgets.QPushButton):
if btn.text() == self.tr("&Open"):
QtCore.QTimer.singleShot(0, lambda btn=btn: btn.setText("Remove"))
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
While I appreciate the solution provided by #eyllanesc, I'd like to propose a variation.
Under certain circumstances, the code for that answer might fail, specifically:
the delay that X11 suffers from mapping windows;
the checking of localized button strings;
the selection using the file name edit box;
Considering the above, I propose an alternate solution, based on the points above.
For obvious reasons, the main point remains: the dialog must be non-native.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class FileDialogTest(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
self.fileEdit = QLineEdit()
layout.addWidget(self.fileEdit)
self.selectBtn = QToolButton(icon=QIcon.fromTheme('folder'), text='…')
layout.addWidget(self.selectBtn)
self.selectBtn.clicked.connect(self.showSelectDialog)
def checkSelectDialog(self):
dialog = self.findChild(QFileDialog)
if not dialog.isVisible():
# wait for dialog finalization, as testOption might fail
return
# dialog found, stop the timer and delete it
self.sender().stop()
self.sender().deleteLater()
if not dialog.testOption(dialog.DontUseNativeDialog):
# native dialog, we cannot do anything!
return
def updateOpenButton():
selection = tree.selectionModel().selectedIndexes()
if selection and not tree.model().isDir(selection[0]):
# it's a file, change the button text
button.setText('Select my precious file')
tree = dialog.findChild(QTreeView)
button = dialog.findChild(QDialogButtonBox).button(
QDialogButtonBox.Open)
# check for selected files on open
updateOpenButton()
# connect the selection update signal
tree.selectionModel().selectionChanged.connect(
lambda: QTimer.singleShot(0, updateOpenButton))
def showSelectDialog(self):
QTimer(self, interval=10, timeout=self.checkSelectDialog).start()
path, filter = QFileDialog.getOpenFileName(self,
'Select file', '<path_to_file>',
"All Files (*);;Python Files (*.py);; PNG Files (*.png)",
options=QFileDialog.DontUseNativeDialog)
if path:
self.fileEdit.setText(path)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = FileDialogTest()
ex.show()
sys.exit(app.exec())
Obviously, for PyQt6 you'll need to use the proper Enum namespaces (i.e. QFileDialog.Option.DontUseNativeDialog, etc.).

How can I make the menubar in PyQt5 have initial focus?

I have created a GUI with a menubar. However, when I run the app and the window opens I want to be able to iterate through the buttons using only the keyboard. This works for the buttons and lineedit fields, but does not work for the MenuBar.
How can I be able to get focus on the MenuBar such that I can iterate through the actions in the menubar using only the keyboard?
from PyQt5 import QtCore, QtWidgets, QtGui
class ConfigNumbers(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.setupUi ( self)
self.setWindowTitle ( 'Nr configs' )
def setupUi(self, ConfigNumbers):
ConfigNumbers.setEnabled ( True )
ConfigNumbers.setFocusPolicy ( QtCore.Qt.TabFocus )
layout = QtWidgets.QGridLayout()
#ELEMENTS
#ACTIONS TO MENUBAR
self.menuBar = QtWidgets.QMenuBar()
delete = self.menuBar.addMenu("Delete")
add = self.menuBar.addMenu("Add")
deleteNr = []
addNr = QtWidgets.QAction("Add New Master", self)
addNr.setShortcut("Ctrl+A")
# addNr.triggered.connect(lambda: self.addNewNr())
add.addAction(addNr)
for nr in range (1,5):
deleteNr.append(QtWidgets.QAction("Nr %d" % nr, self))
delete.addAction(deleteNr[nr-1])
QtCore.QTimer.singleShot(0, self.menuBar.setFocus)
layout.addWidget(self.menuBar)
layout.addWidget(QtWidgets.QPushButton("push"))
self.setLayout( layout )
def main():
app = QtWidgets.QApplication(sys.argv)
window = ConfigNumbers()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You don't need to focus the menu to be able to select it with the keyboard, you can select it by using the key Alt which is the standard key to achieve this, furthermore, you can add the ampersand character before a letter to your menu action titles and then you can use the combination of Alt+letter to access a menu, for example:
import sys
from PyQt5 import QtCore, QtWidgets, QtGui
class ConfigNumbers(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.setupUi ( self)
self.setWindowTitle ( 'Nr configs' )
def setupUi(self, ConfigNumbers):
ConfigNumbers.setEnabled ( True )
ConfigNumbers.setFocusPolicy ( QtCore.Qt.TabFocus )
layout = QtWidgets.QGridLayout()
#ELEMENTS
#ACTIONS TO MENUBAR
self.menuBar = QtWidgets.QMenuBar()
delete = self.menuBar.addMenu("Dele&te")
add = self.menuBar.addMenu("&Add")
deleteNr = []
addNr = QtWidgets.QAction("Add New &Master", self)
addNr.setShortcut("Ctrl+A")
# addNr.triggered.connect(lambda: self.addNewNr())
add.addAction(addNr)
for nr in range (1,5):
deleteNr.append(QtWidgets.QAction("Nr %d" % nr, self))
delete.addAction(deleteNr[nr-1])
layout.addWidget(self.menuBar)
layout.addWidget(QtWidgets.QPushButton("push"))
self.setLayout( layout )
def main():
app = QtWidgets.QApplication(sys.argv)
window = ConfigNumbers()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Now when I hit and release the key Alt I can see the following:
Now you can use the arrow keys to move through the menus; If I hit Alt+A it opens the "Add" menu and so on...
You should also avoid focusing on items using a timer as it may interact with user actions in the interface.

PyQt5 - resize (limit maximum size) input dialog

I read this: How to resize QInputDialog, PyQt but it didnt work for me, as it seems to be about PyQt4
This is my code snipplet:
def ImportURL(self): #URL dialog aufrufen
InputDialog = QtWidgets.QInputDialog(self)
i, okPressed = InputDialog.getText(self, "Import website", "Site to import:", QtWidgets.QLineEdit.Normal, "https://de.wikipedia.org/wiki/Wikipedia:Hauptseite")
if okPressed:
self.getWebsite(i)
And i tried adding .setFixedSize in the 2nd line. I tried adding InputDialog.setFixedSite(self) between line 2 and 3. Nothing worked, it either crashes or it creates a second, empty window. Am i overlooking something here?
In the answers to the other question do not explain the cause of the problem so in my answer will try to cover as much as possible
Explanation:
The getText() method is a static method, which means that an object is not used, in the method internally if it is used but it is not accessible. So the InputDialog that you create is not the one you show and this you can check using the following code since you will see 2 windows:
def ImportURL(self):
InputDialog = QtWidgets.QInputDialog(self)
InputDialog.show()
i, okPressed = InputDialog.getText(self, "Import website", "Site to import:", QtWidgets.QLineEdit.Normal, "https://de.wikipedia.org/wiki/Wikipedia:Hauptseite")
if okPressed:
self.getWebsite(i)
Solutions:
So there are the following solutions:
Taking advantage of what you have passed as a parent to self, you can obtain the object using findChildren:
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
button = QtWidgets.QPushButton(
"Open QInputDialog", clicked=self.ImportURL
)
vlay = QtWidgets.QVBoxLayout(self)
vlay.addWidget(button)
#QtCore.pyqtSlot()
def ImportURL(self):
QtCore.QTimer.singleShot(0, self.after_show)
i, okPressed = QtWidgets.QInputDialog.getText(
self,
"Import website",
"Site to import:",
QtWidgets.QLineEdit.Normal,
"https://de.wikipedia.org/wiki/Wikipedia:Hauptseite",
)
if okPressed:
# self.getWebsite(i)
print(i)
#QtCore.pyqtSlot()
def after_show(self):
size = QtCore.QSize(500, 100)
for d in self.findChildren(QtWidgets.QInputDialog):
if d.isVisible():
d.resize(size)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Do not use the getText() method but create an object that implements the same logic:
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
button = QtWidgets.QPushButton(
"Open QInputDialog", clicked=self.ImportURL
)
vlay = QtWidgets.QVBoxLayout(self)
vlay.addWidget(button)
#QtCore.pyqtSlot()
def ImportURL(self):
dialog = QtWidgets.QInputDialog(self)
dialog.resize(QtCore.QSize(500, 100))
dialog.setWindowTitle("Import website")
dialog.setLabelText("Site to Import")
dialog.setTextValue(
"https://de.wikipedia.org/wiki/Wikipedia:Hauptseite"
)
dialog.setTextEchoMode(QtWidgets.QLineEdit.Normal)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
i = dialog.textValue()
print(i)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Update:
The layout of the QInputDialog has QLayout::SetMinAndMaxSize set as sizeConstraint, so the fixed size will not work, the trick is to change it to QLayout::SetDefaultConstraint:
from functools import partial
# ...
#QtCore.pyqtSlot()
def ImportURL(self):
dialog = QtWidgets.QInputDialog(self)
dialog.setWindowTitle("Import website")
dialog.setLabelText("Site to Import")
dialog.setTextValue(
"https://de.wikipedia.org/wiki/Wikipedia:Hauptseite"
)
dialog.setTextEchoMode(QtWidgets.QLineEdit.Normal)
wrapper = partial(self.on_timeout, dialog)
QtCore.QTimer.singleShot(0, wrapper)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
i = dialog.textValue()
print(i)
def on_timeout(self, dialog):
lay = dialog.layout()
lay.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
dialog.setFixedSize(QtCore.QSize(500, 100))

Text from QLineEdit not displaying

I am grabbing the user input from the line edit and displaying it on the QMessageBox but it won't show for some reason. I thought maybe I wasn't grabbing the input from QLineEdit at all but when I tried printing it on the terminal (it still wouldn't show there btw) the terminal scrolled down, recognizing that there is new data in it but just not displaying it. Get what I am saying?
import os
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
# create objects
label = QLabel(self.tr("enter the data "))
self.le = QLineEdit()
self.te = QTextEdit()
# layout
layout = QVBoxLayout(self)
layout.addWidget(label)
layout.addWidget(self.le)
layout.addWidget(self.te)
self.setLayout(layout)
# create connection
self.mytext = str(self.le.text())
self.connect(self.le, SIGNAL("returnPressed(void)"),
self.display)
def display(self):
QApplication.instance().processEvents()
msg = QMessageBox.about(self, 'msg', '%s' % self.mytext)
print(self.mytext)
self.te.append(self.mytext)
self.le.setText("")
if __name__ == "__main__":
main()
You are currently reading the QLineEdit in the constructor, and at that moment the QLineEdit is empty, you must do it in the slot:
def display(self):
mytext = self.le.text()
msg = QMessageBox.about(self, 'msg', '%s' % mytext)
self.te.append(mytext)
self.le.clear()
Note: use clear() to clean the QLineEdit

Categories

Resources