Trifon Trifonov
Trifon Trifonov

Reputation: 75

ScrollBar to always show the bottom of a QTextBrowser streamed text

I am piping the "stdout" and "stderr" to an embedded QTextBrowser widget in a pyqt5 GUI. All works, but if I pipe a very long output (which happens all the time) the ScrollBar of the widget is always in last upper position, and thus I cant see the new output in a real time. This is really annoying! I tried everything I could find in the internet, but still with no success. It cannot be too hard.... Please open my eyes!

Code I am using is here (I think it was found on the StackOverflow):

import sys
from PyQt4 import QtCore, QtGui

import logging
logger = logging.getLogger(__name__)

class QtHandler(logging.Handler):

    def __init__(self):
        logging.Handler.__init__(self)

    def emit(self, record):
        record = self.format(record)
        XStream.stdout().write("{}\n".format(record))

handler = QtHandler()
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)



class XStream(QtCore.QObject):
    _stdout = None
    _stderr = None
    messageWritten = QtCore.pyqtSignal(str)
    def flush( self ):
        pass
    def fileno( self ):
        return -1
    def write( self, msg ):
        if ( not self.signalsBlocked() ):
            self.messageWritten.emit(msg)
    @staticmethod
    def stdout():
        if ( not XStream._stdout ):
            XStream._stdout = XStream()
            sys.stdout = XStream._stdout
        return XStream._stdout
    @staticmethod
    def stderr():
        if ( not XStream._stderr ):
            XStream._stderr = XStream()
            sys.stderr = XStream._stderr

        return XStream._stderr

class MyDialog(QtGui.QDialog):
    def __init__( self, parent = None ):
        super(MyDialog, self).__init__(parent)

        self._console = QtGui.QTextBrowser(self)      
        self._console.moveCursor(QtGui.QTextCursor.End)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(self._console)
        self.setLayout(layout)

        XStream.stdout().messageWritten.connect(self._console.insertPlainText)
        XStream.stderr().messageWritten.connect(self._console.insertPlainText)

        self.pipe_output()


    def pipe_output( self ):
        logger.debug('debug message')
        logger.info('info message')
        logger.warning('warning message')
        logger.error('error message')
        #print('Old school hand made print message')

if ( __name__ == '__main__' ):
    #app = None
    # if ( not QtGui.QApplication.instance() ):
    app = QtGui.QApplication([])
    dlg = MyDialog()
    dlg.show()

    #if ( app ):
    app.exec_()

Here is a print screen

enter image description here

Upvotes: 1

Views: 4201

Answers (2)

Trifon Trifonov
Trifon Trifonov

Reputation: 75

Thanks to titusjan and eyllanesc, I was able to get what I want!

The code that works is:

import sys
from PyQt5 import QtCore, QtGui

import logging
logger = logging.getLogger(__name__)

class QtHandler(logging.Handler):

    def __init__(self):
        logging.Handler.__init__(self)

    def emit(self, record):
        record = self.format(record)
        XStream.stdout().write("{}\n".format(record))

handler = QtHandler()
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

class XStream(QtCore.QObject):
    _stdout = None
    _stderr = None
    messageWritten = QtCore.pyqtSignal(str)
    def flush( self ):
        pass
    def fileno( self ):
        return -1
    def write( self, msg ):
        if ( not self.signalsBlocked() ):
            self.messageWritten.emit(msg)
    @staticmethod
    def stdout():
        if ( not XStream._stdout ):
            XStream._stdout = XStream()
            sys.stdout = XStream._stdout
        return XStream._stdout
    @staticmethod
    def stderr():
        if ( not XStream._stderr ):
            XStream._stderr = XStream()
            sys.stderr = XStream._stderr

        return XStream._stderr

class LogMessageViewer(QtGui.QTextBrowser):

    def __init__(self, parent=None):
        super(LogMessageViewer,self).__init__(parent)
        self.setReadOnly(True)
        #self.setLineWrapMode(QtGui.QTextEdit.NoWrap)


    @QtCore.pyqtSlot(str)
    def appendLogMessage(self, msg):
        horScrollBar = self.horizontalScrollBar()
        verScrollBar = self.verticalScrollBar()
        scrollIsAtEnd = verScrollBar.maximum() - verScrollBar.value() <= 10

        self.insertPlainText(msg)

        if scrollIsAtEnd:
            verScrollBar.setValue(verScrollBar.maximum()) # Scrolls to the bottom
            horScrollBar.setValue(0) # scroll to the left

class MyDialog(QtGui.QDialog):
    def __init__( self, parent = None ):
        super(MyDialog, self).__init__(parent)

        self._console = LogMessageViewer(self)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(self._console)
        self.setLayout(layout)

        XStream.stdout().messageWritten.connect(self._console.appendLogMessage)
        XStream.stderr().messageWritten.connect(self._console.appendLogMessage)


if ( __name__ == '__main__' ):

    app = QtGui.QApplication([])
    dlg = MyDialog()
    dlg.show()

    app.exec_()

And:

enter image description here

Upvotes: 2

titusjan
titusjan

Reputation: 5546

I have made something similar in the past, a log message viewer that scrolls to the bottom when new messages are added. It is based on a QTextEdit, but since the QTextBrowser also has a verticalScrolBar method I believe you can easily get it to work for the QTextBrower.

class LogMessageViewer(QtWidgets.QTextEdit):

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setReadOnly(True)
        self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)


    @pyqtSlot(str)
    def appendLogMessage(self, msg):
        horScrollBar = self.horizontalScrollBar()
        verScrollBar = self.verticalScrollBar()
        scrollIsAtEnd = verScrollBar.maximum() - verScrollBar.value() <= 10

        self.append(msg)

        if scrollIsAtEnd:
            verScrollBar.setValue(verScrollBar.maximum()) # Scrolls to the bottom
            horScrollBar.setValue(0) # scroll to the left

Note that it only scrolls automatically if the current scroll position is already within 10 pixels of the bottom. This allows you to scroll up and see earlier text without being interrupted by new output. Simply scroll back to the bottom to get live updates again.

Upvotes: 5

Related Questions