python3+PyQt5 实现理解python语法并做高亮显示的纯文本编辑器
2017-03-31 11:22
726 查看
本文通过Python3+PyQt5实现《python Qt Gui 快速编程》这本书13章程序理解python语法并做高亮显示的纯文本编辑器,采QSyntaxHighlighter类库。
运行结果
#!/usr/bin/env python3 import os import sys from PyQt5.QtCore import (QEvent, QFile, QFileInfo, QIODevice, QRegExp, QTextStream,Qt) from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QMainWindow, QMessageBox, QTextEdit) from PyQt5.QtGui import QFont, QIcon,QColor,QKeySequence,QSyntaxHighlighter,QTextCharFormat,QTextCursor import qrc_resources __version__ = "1.1.0" class PythonHighlighter(QSyntaxHighlighter): Rules = [] Formats = {} def __init__(self, parent=None): super(PythonHighlighter, self).__init__(parent) self.initializeFormats() KEYWORDS = ["and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "exec", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "not", "or", "pass", "print", "raise", "return", "try", "while", "with", "yield"] BUILTINS = ["abs", "all", "any", "basestring", "bool", "callable", "chr", "classmethod", "cmp", "compile", "complex", "delattr", "dict", "dir", "divmod", "enumerate", "eval", "execfile", "exit", "file", "filter", "float", "frozenset", "getattr", "globals", "hasattr", "hex", "id", "int", "isinstance", "issubclass", "iter", "len", "list", "locals", "map", "max", "min", "object", "oct", "open", "ord", "pow", "property", "range", "reduce", "repr", "reversed", "round", "set", "setattr", "slice", "sorted", "staticmethod", "str", "sum", "super", "tuple", "type", "vars", "zip"] CONSTANTS = ["False", "True", "None", "NotImplemented", "Ellipsis"] PythonHighlighter.Rules.append((QRegExp( "|".join([r"\b%s\b" % keyword for keyword in KEYWORDS])), "keyword")) PythonHighlighter.Rules.append((QRegExp( "|".join([r"\b%s\b" % builtin for builtin in BUILTINS])), "builtin")) PythonHighlighter.Rules.append((QRegExp( "|".join([r"\b%s\b" % constant for constant in CONSTANTS])), "constant")) PythonHighlighter.Rules.append((QRegExp( r"\b[+-]?[0-9]+[lL]?\b" r"|\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b" r"|\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b"), "number")) PythonHighlighter.Rules.append((QRegExp( r"\bPyQt4\b|\bQt?[A-Z][a-z]\w+\b"), "pyqt")) PythonHighlighter.Rules.append((QRegExp(r"\b@\w+\b"), "decorator")) stringRe = QRegExp(r"""(?:'[^']*'|"[^"]*")""") stringRe.setMinimal(True) PythonHighlighter.Rules.append((stringRe, "string")) self.stringRe = QRegExp(r"""(:?"["]".*"["]"|'''.*''')""") self.stringRe.setMinimal(True) PythonHighlighter.Rules.append((self.stringRe, "string")) self.tripleSingleRe = QRegExp(r"""'''(?!")""") self.tripleDoubleRe = QRegExp(r'''"""(?!')''') @staticmethod def initializeFormats(): baseFormat = QTextCharFormat() baseFormat.setFontFamily("courier") baseFormat.setFontPointSize(12) for name, color in (("normal", Qt.black), ("keyword", Qt.darkBlue), ("builtin", Qt.darkRed), ("constant", Qt.darkGreen), ("decorator", Qt.darkBlue), ("comment", Qt.darkGreen), ("string", Qt.darkYellow), ("number", Qt.darkMagenta), ("error", Qt.darkRed), ("pyqt", Qt.darkCyan)): format = QTextCharFormat(baseFormat) format.setForeground(QColor(color)) if name in ("keyword", "decorator"): format.setFontWeight(QFont.Bold) if name == "comment": format.setFontItalic(True) PythonHighlighter.Formats[name] = format def highlightBlock(self, text): NORMAL, TRIPLESINGLE, TRIPLEDOUBLE, ERROR = range(4) textLength = len(text) prevState = self.previousBlockState() self.setFormat(0, textLength, PythonHighlighter.Formats["normal"]) if text.startswith("Traceback") or text.startswith("Error: "): self.setCurrentBlockState(ERROR) self.setFormat(0, textLength, PythonHighlighter.Formats["error"]) return if (prevState == ERROR and not (text.startswith(sys.ps1) or text.startswith("#"))): self.setCurrentBlockState(ERROR) self.setFormat(0, textLength, PythonHighlighter.Formats["error"]) return for regex, format in PythonHighlighter.Rules: i = regex.indexIn(text) while i >= 0: length = regex.matchedLength() self.setFormat(i, length, PythonHighlighter.Formats[format]) i = regex.indexIn(text, i + length) # Slow but good quality highlighting for comments. For more # speed, comment this out and add the following to __init__: # PythonHighlighter.Rules.append((QRegExp(r"#.*"), "comment")) if not text: pass elif text[0] == "#": self.setFormat(0, len(text), PythonHighlighter.Formats["comment"]) else: stack = [] for i, c in enumerate(text): if c in ('"', "'"): if stack and stack[-1] == c: stack.pop() else: stack.append(c) elif c == "#" and len(stack) == 0: self.setFormat(i, len(text), PythonHighlighter.Formats["comment"]) break self.setCurrentBlockState(NORMAL) if self.stringRe.indexIn(text) != -1: return # This is fooled by triple quotes inside single quoted strings for i, state in ((self.tripleSingleRe.indexIn(text), TRIPLESINGLE), (self.tripleDoubleRe.indexIn(text), TRIPLEDOUBLE)): if self.previousBlockState() == state: if i == -1: i = text.length() self.setCurrentBlockState(state) self.setFormat(0, i + 3, PythonHighlighter.Formats["string"]) elif i > -1: self.setCurrentBlockState(state) self.setFormat(i, text.length(), PythonHighlighter.Formats["string"]) def rehighlight(self): QApplication.setOverrideCursor(QCursor( Qt.WaitCursor)) QSyntaxHighlighter.rehighlight(self) QApplication.restoreOverrideCursor() class TextEdit(QTextEdit): def __init__(self, parent=None): super(TextEdit, self).__init__(parent) def event(self, event): if (event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab): cursor = self.textCursor() cursor.insertText(" ") return True return QTextEdit.event(self, event) class MainWindow(QMainWindow): def __init__(self, filename=None, parent=None): super(MainWindow, self).__init__(parent) font = QFont("Courier", 11) font.setFixedPitch(True) self.editor = TextEdit() self.editor.setFont(font) self.highlighter = PythonHighlighter(self.editor.document()) self.setCentralWidget(self.editor) status = self.statusBar() status.setSizeGripEnabled(False) status.showMessage("Ready", 5000) fileNewAction = self.createAction("&New...", self.fileNew, QKeySequence.New, "filenew", "Create a Python file") fileOpenAction = self.createAction("&Open...", self.fileOpen, QKeySequence.Open, "fileopen", "Open an existing Python file") self.fileSaveAction = self.createAction("&Save", self.fileSave, QKeySequence.Save, "filesave", "Save the file") self.fileSaveAsAction = self.createAction("Save &As...", self.fileSaveAs, icon="filesaveas", tip="Save the file using a new name") fileQuitAction = self.createAction("&Quit", self.close, "Ctrl+Q", "filequit", "Close the application") self.editCopyAction = self.createAction("&Copy", self.editor.copy, QKeySequence.Copy, "editcopy", "Copy text to the clipboard") self.editCutAction = self.createAction("Cu&t", self.editor.cut, QKeySequence.Cut, "editcut", "Cut text to the clipboard") self.editPasteAction = self.createAction("&Paste", self.editor.paste, QKeySequence.Paste, "editpaste", "Paste in the clipboard's text") self.editIndentAction = self.createAction("&Indent", self.editIndent, "Ctrl+]", "editindent", "Indent the current line or selection") self.editUnindentAction = self.createAction("&Unindent", self.editUnindent, "Ctrl+[", "editunindent", "Unindent the current line or selection") fileMenu = self.menuBar().addMenu("&File") self.addActions(fileMenu, (fileNewAction, fileOpenAction, self.fileSaveAction, self.fileSaveAsAction, None, fileQuitAction)) editMenu = self.menuBar().addMenu("&Edit") self.addActions(editMenu, (self.editCopyAction, self.editCutAction, self.editPasteAction, None, self.editIndentAction, self.editUnindentAction)) fileToolbar = self.addToolBar("File") fileToolbar.setObjectName("FileToolBar") self.addActions(fileToolbar, (fileNewAction, fileOpenAction, self.fileSaveAction)) editToolbar = self.addToolBar("Edit") editToolbar.setObjectName("EditToolBar") self.addActions(editToolbar, (self.editCopyAction, self.editCutAction, self.editPasteAction, None, self.editIndentAction, self.editUnindentAction)) self.editor.selectionChanged.connect(self.updateUi) self.editor.document().modificationChanged.connect(self.updateUi) QApplication.clipboard().dataChanged.connect(self.updateUi) self.resize(800, 600) self.setWindowTitle("Python Editor") self.filename = filename if self.filename is not None: self.loadFile() self.updateUi() def updateUi(self, arg=None): self.fileSaveAction.setEnabled( self.editor.document().isModified()) enable = not self.editor.document().isEmpty() self.fileSaveAsAction.setEnabled(enable) self.editIndentAction.setEnabled(enable) self.editUnindentAction.setEnabled(enable) enable = self.editor.textCursor().hasSelection() self.editCopyAction.setEnabled(enable) self.editCutAction.setEnabled(enable) self.editPasteAction.setEnabled(self.editor.canPaste()) def createAction(self, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, signal="triggered()"): action = QAction(text, self) if icon is not None: action.setIcon(QIcon(":/{0}.png".format(icon))) if shortcut is not None: action.setShortcut(shortcut) if tip is not None: action.setToolTip(tip) action.setStatusTip(tip) if slot is not None: action.triggered.connect(slot) if checkable: action.setCheckable(True) return action def addActions(self, target, actions): for action in actions: if action is None: target.addSeparator() else: target.addAction(action) def closeEvent(self, event): if not self.okToContinue(): event.ignore() def okToContinue(self): if self.editor.document().isModified(): reply = QMessageBox.question(self, "Python Editor - Unsaved Changes", "Save unsaved changes?", QMessageBox.Yes|QMessageBox.No| QMessageBox.Cancel) if reply == QMessageBox.Cancel: return False elif reply == QMessageBox.Yes: return self.fileSave() return True def fileNew(self): if not self.okToContinue(): return document = self.editor.document() document.clear() document.setModified(False) self.filename = None self.setWindowTitle("Python Editor - Unnamed") self.updateUi() def fileOpen(self): if not self.okToContinue(): return dir = (os.path.dirname(self.filename) if self.filename is not None else ".") fname = str(QFileDialog.getOpenFileName(self, "Python Editor - Choose File", dir, "Python files (*.py *.pyw)")[0]) if fname: self.filename = fname self.loadFile() def loadFile(self): fh = None try: fh = QFile(self.filename) if not fh.open(QIODevice.ReadOnly): raise IOError(str(fh.errorString())) stream = QTextStream(fh) stream.setCodec("UTF-8") self.editor.setPlainText(stream.readAll()) self.editor.document().setModified(False) except EnvironmentError as e: QMessageBox.warning(self, "Python Editor -- Load Error", "Failed to load {0}: {1}".format(self.filename, e)) finally: if fh is not None: fh.close() self.setWindowTitle("Python Editor - {0}".format( QFileInfo(self.filename).fileName())) def fileSave(self): if self.filename is None: return self.fileSaveAs() fh = None try: fh = QFile(self.filename) if not fh.open(QIODevice.WriteOnly): raise IOError(str(fh.errorString())) stream = QTextStream(fh) stream.setCodec("UTF-8") stream << self.editor.toPlainText() self.editor.document().setModified(False) except EnvironmentError as e: QMessageBox.warning(self, "Python Editor -- Save Error", "Failed to save {0}: {1}".format(self.filename, e)) return False finally: if fh is not None: fh.close() return True def fileSaveAs(self): filename = self.filename if self.filename is not None else "." filename,filetype = QFileDialog.getSaveFileName(self, "Python Editor -- Save File As", filename, "Python files (*.py *.pyw)") if filename: self.filename = filename self.setWindowTitle("Python Editor - {0}".format( QFileInfo(self.filename).fileName())) return self.fileSave() return False def editIndent(self): cursor = self.editor.textCursor() cursor.beginEditBlock() if cursor.hasSelection(): start = pos = cursor.anchor() end = cursor.position() if start > end: start, end = end, start pos = start cursor.clearSelection() cursor.setPosition(pos) cursor.movePosition(QTextCursor.StartOfLine) while pos <= end: cursor.insertText(" ") cursor.movePosition(QTextCursor.Down) cursor.movePosition(QTextCursor.StartOfLine) pos = cursor.position() cursor.setPosition(start) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, end - start) else: pos = cursor.position() cursor.movePosition(QTextCursor.StartOfBlock) cursor.insertText(" ") cursor.setPosition(pos + 4) cursor.endEditBlock() def editUnindent(self): cursor = self.editor.textCursor() cursor.beginEditBlock() if cursor.hasSelection(): start = pos = cursor.anchor() end = cursor.position() if start > end: start, end = end, start pos = start cursor.setPosition(pos) cursor.movePosition(QTextCursor.StartOfLine) while pos <= end: cursor.clearSelection() cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 4) if cursor.selectedText() == " ": cursor.removeSelectedText() cursor.movePosition(QTextCursor.Down) cursor.movePosition(QTextCursor.StartOfLine) pos = cursor.position() cursor.setPosition(start) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, end - start) else: cursor.clearSelection() cursor.movePosition(QTextCursor.StartOfBlock) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 4) if cursor.selectedText() == " ": cursor.removeSelectedText() cursor.endEditBlock() def main(): app = QApplication(sys.argv) app.setWindowIcon(QIcon(":/icon.png")) fname = None if len(sys.argv) > 1: fname = sys.argv[1] form = MainWindow(fname) form.show() app.exec_() main()
运行结果
相关文章推荐
- 实现Flex的TextArea文本中关键字的高亮显示
- python基于Tkinter库实现简单文本编辑器实例
- C#简单实现高亮语法编辑器
- Word插入代码实现语法彩色高亮显示的办法
- Python使用tkinter库实现文本显示用户输入功能示例
- 如何实现报表中高亮显示文本功能
- Flex中对文本实现高亮显示
- python基于Tkinter库实现简单文本编辑器实例
- python3+PyQt5 实现Rich文本的行编辑
- python+matplotlib实现鼠标移动三角形高亮及索引显示
- Python 删除整个文本中的空格,并实现按行显示
- JavaScript简单实现关键字文本搜索高亮显示功能示例
- 浅谈JavaScript实现关键字文本高亮显示
- Swing实现Java代码编辑器实现关键词高亮显示
- 设置vim语法高亮显示和自动缩进 (最后有自己的理解)
- 用js实现文本点击搜索,文本高亮显示
- python3+PyQt5 实现Tab标签页式编辑器
- 2017.8.7 用python实现简单文本编辑器
- 使用Python语言设计基于HTML的C语言语法加亮显示程序
- 实现鼠标悬停高亮显示---分别在gridview和datagrid中