本例中建立的視窗部件支援Rich文本的編輯,并且支援文法高亮(基于QSyntaxHighlighter)。
核心代碼如下:
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()
if __name__ == "__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_()
複制