I want to create textEdit with line number on the left side in PyQt like Notepad++. I tried this adding another textEdit but scrolling is stuck. I searched and found this question, but there is no good solution for it.
The entire text can be selected using selectAll(). QPlainTextEdit holds a QTextDocument object which can be retrieved using the document() method. You can also set your own document object using setDocument().
The QTextEdit class provides a widget that is used to edit and display both plain and rich text.
Is this what you are looking for CodeEditor example in pyqt based on the c++ http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
Putting it together for python3 (Im using PyQt4 not 5 but I guess it is similar) (and using QPlainTextEdit not QTextEdit see QTextEdit vs QPlainTextEdit ):
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import numpy as np
class LineNumberArea(QWidget):
def __init__(self, editor):
super().__init__(editor)
self.myeditor = editor
def sizeHint(self):
return Qsize(self.editor.lineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.myeditor.lineNumberAreaPaintEvent(event)
class CodeEditor(QPlainTextEdit):
def __init__(self):
super().__init__()
self.lineNumberArea = LineNumberArea(self)
self.connect(self, SIGNAL('blockCountChanged(int)'), self.updateLineNumberAreaWidth)
self.connect(self, SIGNAL('updateRequest(QRect,int)'), self.updateLineNumberArea)
self.connect(self, SIGNAL('cursorPositionChanged()'), self.highlightCurrentLine)
self.updateLineNumberAreaWidth(0)
def lineNumberAreaWidth(self):
digits = 1
count = max(1, self.blockCount())
while count >= 10:
count /= 10
digits += 1
space = 3 + self.fontMetrics().width('9') * digits
return space
def updateLineNumberAreaWidth(self, _):
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
def updateLineNumberArea(self, rect, dy):
if dy:
self.lineNumberArea.scroll(0, dy)
else:
self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(),
rect.height())
if rect.contains(self.viewport().rect()):
self.updateLineNumberAreaWidth(0)
def resizeEvent(self, event):
super().resizeEvent(event)
cr = self.contentsRect();
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(),
self.lineNumberAreaWidth(), cr.height()))
def lineNumberAreaPaintEvent(self, event):
mypainter = QPainter(self.lineNumberArea)
mypainter.fillRect(event.rect(), Qt.lightGray)
block = self.firstVisibleBlock()
blockNumber = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
# Just to make sure I use the right font
height = self.fontMetrics().height()
while block.isValid() and (top <= event.rect().bottom()):
if block.isVisible() and (bottom >= event.rect().top()):
number = str(blockNumber + 1)
mypainter.setPen(Qt.black)
mypainter.drawText(0, top, self.lineNumberArea.width(), height,
Qt.AlignRight, number)
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
blockNumber += 1
def highlightCurrentLine(self):
extraSelections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
lineColor = QColor(Qt.yellow).lighter(160)
selection.format.setBackground(lineColor)
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extraSelections.append(selection)
self.setExtraSelections(extraSelections)
if __name__ == "__main__":
app = QApplication(sys.argv)
txt = CodeEditor()
txt.show()
sys.exit(app.exec_())
It's my code of PyQt5 and python3 which combined @Dan-Dev and @Axel Schneider. You can directly run it, or simply import use code from QCodeEditor import QCodeEditor
.
#!/usr/bin/python3
# QcodeEditor.py by acbetter.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import Qt, QRect, QSize
from PyQt5.QtWidgets import QWidget, QPlainTextEdit, QTextEdit
from PyQt5.QtGui import QColor, QPainter, QTextFormat
class QLineNumberArea(QWidget):
def __init__(self, editor):
super().__init__(editor)
self.codeEditor = editor
def sizeHint(self):
return QSize(self.editor.lineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.codeEditor.lineNumberAreaPaintEvent(event)
class QCodeEditor(QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.lineNumberArea = QLineNumberArea(self)
self.blockCountChanged.connect(self.updateLineNumberAreaWidth)
self.updateRequest.connect(self.updateLineNumberArea)
self.cursorPositionChanged.connect(self.highlightCurrentLine)
self.updateLineNumberAreaWidth(0)
def lineNumberAreaWidth(self):
digits = 1
max_value = max(1, self.blockCount())
while max_value >= 10:
max_value /= 10
digits += 1
space = 3 + self.fontMetrics().width('9') * digits
return space
def updateLineNumberAreaWidth(self, _):
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
def updateLineNumberArea(self, rect, dy):
if dy:
self.lineNumberArea.scroll(0, dy)
else:
self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
if rect.contains(self.viewport().rect()):
self.updateLineNumberAreaWidth(0)
def resizeEvent(self, event):
super().resizeEvent(event)
cr = self.contentsRect()
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))
def highlightCurrentLine(self):
extraSelections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
lineColor = QColor(Qt.yellow).lighter(160)
selection.format.setBackground(lineColor)
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extraSelections.append(selection)
self.setExtraSelections(extraSelections)
def lineNumberAreaPaintEvent(self, event):
painter = QPainter(self.lineNumberArea)
painter.fillRect(event.rect(), Qt.lightGray)
block = self.firstVisibleBlock()
blockNumber = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
# Just to make sure I use the right font
height = self.fontMetrics().height()
while block.isValid() and (top <= event.rect().bottom()):
if block.isVisible() and (bottom >= event.rect().top()):
number = str(blockNumber + 1)
painter.setPen(Qt.black)
painter.drawText(0, top, self.lineNumberArea.width(), height, Qt.AlignRight, number)
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
blockNumber += 1
if __name__ == '__main__':
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
codeEditor = QCodeEditor()
codeEditor.show()
sys.exit(app.exec_())
It runs like this.
By the way, if you want use it in your Qt Designer, you should do it like this and place the *.ui file in the same directory of your QCodeEditor.py file's path unless you set the environment variable. And, you need convert your *.ui file to *.py file by the command pyuic5 -x *.ui -o *.py
. Hope Helpful~
here a version for PyQt5 (with Menu etc ...)
#!/usr/bin/python3
# -- coding: utf-8 --
from PyQt5.QtWidgets import QPlainTextEdit, QWidget, QVBoxLayout, QApplication, QFileDialog, QMessageBox, QHBoxLayout, \
QFrame, QTextEdit, QToolBar, QComboBox, QLabel, QAction, QLineEdit, QToolButton, QMenu, QMainWindow
from PyQt5.QtGui import QIcon, QPainter, QTextFormat, QColor, QTextCursor, QKeySequence, QClipboard, QTextCharFormat, QPalette
from PyQt5.QtCore import Qt, QVariant, QRect, QDir, QFile, QFileInfo, QTextStream, QRegExp, QSettings
import sys, os
lineBarColor = QColor("#ACDED5")
lineHighlightColor = QColor("#ACDED5")
class NumberBar(QWidget):
def __init__(self, parent = None):
super(NumberBar, self).__init__(parent)
self.editor = parent
layout = QVBoxLayout()
self.setLayout(layout)
self.editor.blockCountChanged.connect(self.update_width)
self.editor.updateRequest.connect(self.update_on_scroll)
self.update_width('1')
def update_on_scroll(self, rect, scroll):
if self.isVisible():
if scroll:
self.scroll(0, scroll)
else:
self.update()
def update_width(self, string):
width = self.fontMetrics().width(str(string)) + 10
if self.width() != width:
self.setFixedWidth(width)
def paintEvent(self, event):
if self.isVisible():
block = self.editor.firstVisibleBlock()
height = self.fontMetrics().height()
number = block.blockNumber()
painter = QPainter(self)
painter.fillRect(event.rect(), lineBarColor)
painter.drawRect(0, 0, event.rect().width() - 1, event.rect().height() - 1)
font = painter.font()
current_block = self.editor.textCursor().block().blockNumber() + 1
condition = True
while block.isValid() and condition:
block_geometry = self.editor.blockBoundingGeometry(block)
offset = self.editor.contentOffset()
block_top = block_geometry.translated(offset).top()
number += 1
rect = QRect(0, block_top, self.width() - 5, height)
if number == current_block:
font.setBold(True)
else:
font.setBold(False)
painter.setFont(font)
painter.drawText(rect, Qt.AlignRight, '%i'%number)
if block_top > event.rect().bottom():
condition = False
block = block.next()
painter.end()
class myEditor(QMainWindow):
def __init__(self, parent = None):
super(myEditor, self).__init__(parent)
self.MaxRecentFiles = 5
self.windowList = []
self.recentFileActs = []
self.setAttribute(Qt.WA_DeleteOnClose)
# Editor Widget ...
QIcon.setThemeName('Faenza-Dark')
self.editor = QPlainTextEdit()
self.editor.setStyleSheet(stylesheet2(self))
self.editor.setFrameStyle(QFrame.NoFrame)
self.editor.setTabStopWidth(14)
self.extra_selections = []
self.fname = ""
self.filename = ""
# Line Numbers ...
self.numbers = NumberBar(self.editor)
self.createActions()
# Laying out...
layoutH = QHBoxLayout()
layoutH.setSpacing(1.5)
layoutH.addWidget(self.numbers)
layoutH.addWidget(self.editor)
### begin toolbar
tb = QToolBar(self)
tb.setWindowTitle("File Toolbar")
self.newAct = QAction("&New", self, shortcut=QKeySequence.New,
statusTip="Create a new file", triggered=self.newFile)
self.newAct.setIcon(QIcon.fromTheme("document-new"))
self.openAct = QAction("&Open", self, shortcut=QKeySequence.Open,
statusTip="open file", triggered=self.openFile)
self.openAct.setIcon(QIcon.fromTheme("document-open"))
self.saveAct = QAction("&Save", self, shortcut=QKeySequence.Save,
statusTip="save file", triggered=self.fileSave)
self.saveAct.setIcon(QIcon.fromTheme("document-save"))
self.saveAsAct = QAction("&Save as ...", self, shortcut=QKeySequence.SaveAs,
statusTip="save file as ...", triggered=self.fileSaveAs)
self.saveAsAct.setIcon(QIcon.fromTheme("document-save-as"))
self.exitAct = QAction("Exit", self, shortcut=QKeySequence.Quit,
toolTip="Exit", triggered=self.handleQuit)
self.exitAct.setIcon(QIcon.fromTheme("application-exit"))
### find / replace toolbar
self.tbf = QToolBar(self)
self.tbf.setWindowTitle("Find Toolbar")
self.findfield = QLineEdit()
self.findfield.addAction(QIcon.fromTheme("edit-find"), QLineEdit.LeadingPosition)
self.findfield.setClearButtonEnabled(True)
self.findfield.setFixedWidth(150)
self.findfield.setPlaceholderText("find")
self.findfield.setToolTip("press RETURN to find")
self.findfield.setText("")
ft = self.findfield.text()
self.findfield.returnPressed.connect(self.findText)
self.tbf.addWidget(self.findfield)
self.replacefield = QLineEdit()
self.replacefield.addAction(QIcon.fromTheme("edit-find-and-replace"), QLineEdit.LeadingPosition)
self.replacefield.setClearButtonEnabled(True)
self.replacefield.setFixedWidth(150)
self.replacefield.setPlaceholderText("replace with")
self.replacefield.setToolTip("press RETURN to replace the first")
self.replacefield.returnPressed.connect(self.replaceOne)
self.tbf.addSeparator()
self.tbf.addWidget(self.replacefield)
self.tbf.addSeparator()
self.tbf.addAction("replace all", self.replaceAll)
self.tbf.addSeparator()
layoutV = QVBoxLayout()
bar=self.menuBar()
self.filemenu=bar.addMenu("File")
self.separatorAct = self.filemenu.addSeparator()
self.filemenu.addAction(self.newAct)
self.filemenu.addAction(self.openAct)
self.filemenu.addAction(self.saveAct)
self.filemenu.addAction(self.saveAsAct)
self.filemenu.addSeparator()
for i in range(self.MaxRecentFiles):
self.filemenu.addAction(self.recentFileActs[i])
self.updateRecentFileActions()
self.filemenu.addSeparator()
self.filemenu.addAction(self.exitAct)
bar.setStyleSheet(stylesheet2(self))
editmenu = bar.addMenu("Edit")
editmenu.addAction(QAction(QIcon.fromTheme('edit-copy'), "Copy", self, triggered = self.editor.copy, shortcut = QKeySequence.Copy))
editmenu.addAction(QAction(QIcon.fromTheme('edit-cut'), "Cut", self, triggered = self.editor.cut, shortcut = QKeySequence.Cut))
editmenu.addAction(QAction(QIcon.fromTheme('edit-paste'), "Paste", self, triggered = self.editor.paste, shortcut = QKeySequence.Paste))
editmenu.addAction(QAction(QIcon.fromTheme('edit-delete'), "Delete", self, triggered = self.editor.cut, shortcut = QKeySequence.Delete))
editmenu.addSeparator()
editmenu.addAction(QAction(QIcon.fromTheme('edit-select-all'), "Select All", self, triggered = self.editor.selectAll, shortcut = QKeySequence.SelectAll))
layoutV.addWidget(bar)
layoutV.addWidget(self.tbf)
layoutV.addLayout(layoutH)
### main window
mq = QWidget(self)
mq.setLayout(layoutV)
self.setCentralWidget(mq)
# Event Filter ...
self.installEventFilter(self)
self.editor.setFocus()
self.cursor = QTextCursor()
self.editor.setPlainText("hello")
self.editor.moveCursor(self.cursor.End)
self.editor.document().modificationChanged.connect(self.setWindowModified)
# Brackets ExtraSelection ...
self.left_selected_bracket = QTextEdit.ExtraSelection()
self.right_selected_bracket = QTextEdit.ExtraSelection()
def createActions(self):
for i in range(self.MaxRecentFiles):
self.recentFileActs.append(
QAction(self, visible=False,
triggered=self.openRecentFile))
def openRecentFile(self):
action = self.sender()
if action:
if (self.maybeSave()):
self.openFileOnStart(action.data())
### New File
def newFile(self):
if self.maybeSave():
self.editor.clear()
self.editor.setPlainText("")
self.filename = ""
self.setModified(False)
self.editor.moveCursor(self.cursor.End)
### open File
def openFileOnStart(self, path=None):
if path:
inFile = QFile(path)
if inFile.open(QFile.ReadWrite | QFile.Text):
text = inFile.readAll()
try:
# Python v3.
text = str(text, encoding = 'utf8')
except TypeError:
# Python v2.
text = str(text)
self.editor.setPlainText(text)
self.filename = path
self.setModified(False)
self.fname = QFileInfo(path).fileName()
self.setWindowTitle(self.fname + "[*]")
self.document = self.editor.document()
self.setCurrentFile(self.filename)
### open File
def openFile(self, path=None):
if self.maybeSave():
if not path:
path, _ = QFileDialog.getOpenFileName(self, "Open File", QDir.homePath() + "/Documents/",
"Text Files (*.txt *.csv *.py);;All Files (*.*)")
if path:
inFile = QFile(path)
if inFile.open(QFile.ReadWrite | QFile.Text):
text = inFile.readAll()
try:
# Python v3.
text = str(text, encoding = 'utf8')
except TypeError:
# Python v2.
text = str(text)
self.editor.setPlainText(text)
self.filename = path
self.setModified(False)
self.fname = QFileInfo(path).fileName()
self.setWindowTitle(self.fname + "[*]")
self.document = self.editor.document()
self.setCurrentFile(self.filename)
def fileSave(self):
if (self.filename != ""):
file = QFile(self.filename)
print(self.filename)
if not file.open( QFile.WriteOnly | QFile.Text):
QMessageBox.warning(self, "Error",
"Cannot write file %s:\n%s." % (self.filename, file.errorString()))
return
outstr = QTextStream(file)
QApplication.setOverrideCursor(Qt.WaitCursor)
outstr << self.editor.toPlainText()
QApplication.restoreOverrideCursor()
self.setModified(False)
self.fname = QFileInfo(self.filename).fileName()
self.setWindowTitle(self.fname + "[*]")
self.setCurrentFile(self.filename)
else:
self.fileSaveAs()
### save File
def fileSaveAs(self):
fn, _ = QFileDialog.getSaveFileName(self, "Save as...", self.filename,
"Python files (*.py)")
if not fn:
print("Error saving")
return False
lfn = fn.lower()
if not lfn.endswith('.py'):
fn += '.py'
self.filename = fn
self.fname = os.path.splitext(str(fn))[0].split("/")[-1]
return self.fileSave()
def closeEvent(self, e):
if self.maybeSave():
e.accept()
else:
e.ignore()
### ask to save
def maybeSave(self):
if not self.isModified():
return True
if self.filename.startswith(':/'):
return True
ret = QMessageBox.question(self, "Message",
"<h4><p>The document was modified.</p>\n" \
"<p>Do you want to save changes?</p></h4>",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if ret == QMessageBox.Yes:
if self.filename == "":
self.fileSaveAs()
return False
else:
self.fileSave()
return True
if ret == QMessageBox.Cancel:
return False
return True
def findText(self):
ft = self.findfield.text()
if self.editor.find(ft):
return
else:
self.editor.moveCursor(1)
if self.editor.find(ft):
self.editor.moveCursor(QTextCursor.Start, QTextCursor.MoveAnchor)
def handleQuit(self):
print("Goodbye ...")
app.quit()
def set_numbers_visible(self, value = True):
self.numbers.setVisible(False)
def match_left(self, block, character, start, found):
map = {'{': '}', '(': ')', '[': ']'}
while block.isValid():
data = block.userData()
if data is not None:
braces = data.braces
N = len(braces)
for k in range(start, N):
if braces[k].character == character:
found += 1
if braces[k].character == map[character]:
if not found:
return braces[k].position + block.position()
else:
found -= 1
block = block.next()
start = 0
def match_right(self, block, character, start, found):
map = {'}': '{', ')': '(', ']': '['}
while block.isValid():
data = block.userData()
if data is not None:
braces = data.braces
if start is None:
start = len(braces)
for k in range(start - 1, -1, -1):
if braces[k].character == character:
found += 1
if braces[k].character == map[character]:
if found == 0:
return braces[k].position + block.position()
else:
found -= 1
block = block.previous()
start = None
# '''
cursor = self.editor.textCursor()
block = cursor.block()
data = block.userData()
previous, next = None, None
if data is not None:
position = cursor.position()
block_position = cursor.block().position()
braces = data.braces
N = len(braces)
for k in range(0, N):
if braces[k].position == position - block_position or braces[k].position == position - block_position - 1:
previous = braces[k].position + block_position
if braces[k].character in ['{', '(', '[']:
next = self.match_left(block,
braces[k].character,
k + 1, 0)
elif braces[k].character in ['}', ')', ']']:
next = self.match_right(block,
braces[k].character,
k, 0)
if next is None:
next = -1
if next is not None and next > 0:
if next == 0 and next >= 0:
format = QTextCharFormat()
cursor.setPosition(previous)
cursor.movePosition(QTextCursor.NextCharacter,
QTextCursor.KeepAnchor)
format.setBackground(QColor('white'))
self.left_selected_bracket.format = format
self.left_selected_bracket.cursor = cursor
cursor.setPosition(next)
cursor.movePosition(QTextCursor.NextCharacter,
QTextCursor.KeepAnchor)
format.setBackground(QColor('white'))
self.right_selected_bracket.format = format
self.right_selected_bracket.cursor = cursor
# '''
def paintEvent(self, event):
highlighted_line = QTextEdit.ExtraSelection()
highlighted_line.format.setBackground(lineHighlightColor)
highlighted_line.format.setProperty(QTextFormat
.FullWidthSelection,
QVariant(True))
highlighted_line.cursor = self.editor.textCursor()
highlighted_line.cursor.clearSelection()
self.editor.setExtraSelections([highlighted_line,
self.left_selected_bracket,
self.right_selected_bracket])
def document(self):
return self.editor.document
def isModified(self):
return self.editor.document().isModified()
def setModified(self, modified):
self.editor.document().setModified(modified)
def setLineWrapMode(self, mode):
self.editor.setLineWrapMode(mode)
def clear(self):
self.editor.clear()
def setPlainText(self, *args, **kwargs):
self.editor.setPlainText(*args, **kwargs)
def setDocumentTitle(self, *args, **kwargs):
self.editor.setDocumentTitle(*args, **kwargs)
def set_number_bar_visible(self, value):
self.numbers.setVisible(value)
def replaceAll(self):
print("replacing all")
oldtext = self.editor.document().toPlainText()
newtext = oldtext.replace(self.findfield.text(), self.replacefield.text())
self.editor.setPlainText(newtext)
self.setModified(True)
def replaceOne(self):
print("replacing all")
oldtext = self.editor.document().toPlainText()
newtext = oldtext.replace(self.findfield.text(), self.replacefield.text(), 1)
self.editor.setPlainText(newtext)
self.setModified(True)
def setCurrentFile(self, fileName):
self.curFile = fileName
if self.curFile:
self.setWindowTitle("%s - Recent Files" % self.strippedName(self.curFile))
else:
self.setWindowTitle("Recent Files")
settings = QSettings('Axel Schneider', 'PTEdit')
files = settings.value('recentFileList')
try:
files.remove(fileName)
except ValueError:
pass
files.insert(0, fileName)
del files[self.MaxRecentFiles:]
settings.setValue('recentFileList', files)
for widget in QApplication.topLevelWidgets():
if isinstance(widget, myEditor):
widget.updateRecentFileActions()
def updateRecentFileActions(self):
mytext = ""
settings = QSettings('Axel Schneider', 'PTEdit')
files = settings.value('recentFileList')
numRecentFiles = min(len(files), self.MaxRecentFiles)
for i in range(numRecentFiles):
text = "&%d %s" % (i + 1, self.strippedName(files[i]))
self.recentFileActs[i].setText(text)
self.recentFileActs[i].setData(files[i])
self.recentFileActs[i].setVisible(True)
for j in range(numRecentFiles, self.MaxRecentFiles):
self.recentFileActs[j].setVisible(False)
self.separatorAct.setVisible((numRecentFiles > 0))
def clearRecentFileList(self, fileName):
self.rmenu.clear()
def strippedName(self, fullFileName):
return QFileInfo(fullFileName).fileName()
def stylesheet2(self):
return """
QPlainTextEdit
{
background: #ECECEC;
color: #202020;
border: 1px solid #1EAE3D;
selection-background-color: #505050;
selection-color: #ACDED5;
}
QMenu
{
background: #F2F2F2;
color: #0E185F;
border: 1px solid #1EAE3D;
selection-background-color: #ACDED5;
}
"""
if __name__ == '__main__':
app = QApplication(sys.argv)
win = myEditor()
win.setWindowIcon(QIcon.fromTheme("application-text"))
win.setWindowTitle("Plain Text Edit" + "[*]")
win.setMinimumSize(640,250)
win.showMaximized()
if len(sys.argv) > 1:
print(sys.argv[1])
win.openFileOnStart(sys.argv[1])
app.exec_()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With