QScintilla调用setText后如何实现撤销恢复至先前状态?
Hey there, let's work through your QsciScintilla problems step by step. First, here's your helper code formatted for clarity:
import textwrap import sys from pathlib import Path from PyQt5.Qsci import QsciScintilla from PyQt5.Qt import * # noqa def set_style(sci): # Set default font sci.font = QFont() sci.font.setFamily('Consolas') sci.font.setFixedPitch(True) sci.font.setPointSize(8) sci.font.setBold(True) sci.setFont(sci.font) sci.setMarginsFont(sci.font) sci.setUtf8(True) # Set paper sci.setPaper(QColor(39, 40, 34)) # Set margin defaults fontmetrics = QFontMetrics(sci.font) sci.setMarginsFont(sci.font) sci.setMarginWidth(0, fontmetrics.width("000") + 6) sci.setMarginLineNumbers(0, True) sci.setMarginsForegroundColor(QColor(128, 128, 128)) sci.setMarginsBackgroundColor(QColor(39, 40, 34)) sci.setMarginType(1, sci.SymbolMargin) sci.setMarginWidth(1, 12) # Set indentation defaults sci.setIndentationsUseTabs(False) sci.setIndentationWidth(4) sci.setBackspaceUnindents(True) sci.setIndentationGuides(True) sci.setFoldMarginColors(QColor(39, 40, 34), QColor(39, 40, 34)) # Set caret defaults sci.setCaretForegroundColor(QColor(247, 247, 241)) sci.setCaretWidth(2) # Set edge defaults sci.setEdgeColumn(80) sci.setEdgeColor(QColor(221, 221, 221)) sci.setEdgeMode(sci.EdgeLine) # Set folding defaults (http://www.scintilla.org/ScintillaDoc.html#Folding) sci.setFolding(QsciScintilla.CircledFoldStyle) # Set wrapping sci.setWrapMode(sci.WrapNone) # Set selection color defaults sci.setSelectionBackgroundColor(QColor(61, 61, 52)) sci.resetSelectionForegroundColor() # Set scrollwidth defaults sci.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1) # Current line visible with special background color sci.setCaretLineBackgroundColor(QColor(255, 255, 224)) # Set multiselection defaults sci.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True) sci.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1) sci.SendScintilla(QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True) def set_state1(sci): sci.clear_selections() base = "line{} state1" view.setText("\n".join([base.format(i) for i in range(10)])) for i in range(0, 10, 2): region = (len(base) * i, len(base) * (i + 1) - 1) if i == 0: view.set_selection(region) else: view.add_selection(region) def set_state2(sci): base = "line{} state2" view.setText("\n".join([base.format(i) for i in range(10)])) for i in range(1, 10, 2): region = (len(base) * i, len(base) * (i + 1) - 1) if i == 1: view.set_selection(region) else: view.add_selection(region) class Editor(QsciScintilla): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) set_style(self) def clear_selections(self): sci = self sci.SendScintilla(sci.SCI_CLEARSELECTIONS) def set_selection(self, r): sci = self sci.SendScintilla(sci.SCI_SETSELECTION, r[1], r[0]) def add_selection(self, r): sci = self sci.SendScintilla(sci.SCI_ADDSELECTION, r[1], r[0]) def sel(self): sci = self regions = [] for i in range(sci.SendScintilla(sci.SCI_GETSELECTIONS)): regions.append( sci.SendScintilla(sci.SCI_GETSELECTIONNSTART, i), sci.SendScintilla(sci.SCI_GETSELECTIONNEND, i) ) return sorted(regions)
Problem 1: Display Issue with State1 Selection Calculation
First, let's fix the display problem you're seeing. The root issue is that your selection region calculation doesn't account for newline characters (\n) added between lines. When you join lines with \n, each line (except the last) adds an extra character, which throws off your start/end positions. Also, set_state1 and set_state2 are hardcoded to use view instead of the passed sci instance—let's fix that too.
Here's the corrected set_state1 (apply the same logic to set_state2):
def set_state1(sci): sci.clear_selections() base = "line{} state1" lines = [base.format(i) for i in range(10)] sci.setText("\n".join(lines)) for i in range(0, 10, 2): # Calculate exact start position: sum lengths of all prior lines + newlines start = sum(len(line) + 1 for line in lines[:i]) end = start + len(base) if i == 0: sci.set_selection((start, end)) else: sci.add_selection((start, end)) def set_state2(sci): sci.clear_selections() base = "line{} state2" lines = [base.format(i) for i in range(10)] sci.setText("\n".join(lines)) for i in range(1, 10, 2): start = sum(len(line) + 1 for line in lines[:i]) end = start + len(base) if i == 1: sci.set_selection((start, end)) else: sci.add_selection((start, end))
Also, fix the sel() method in the Editor class—you had a syntax error with tuple creation:
def sel(self): sci = self regions = [] selection_count = sci.SendScintilla(sci.SCI_GETSELECTIONS) for i in range(selection_count): start = sci.SendScintilla(sci.SCI_GETSELECTIONNSTART, i) end = sci.SendScintilla(sci.SCI_GETSELECTIONNEND, i) regions.append((start, end)) # Fixed: wrap in parentheses return sorted(regions)
With these changes, your selections should align correctly with the lines in state1.
Problem 2: Undo to State1 After set_state2 (Preserving Undo History)
The big issue here is that setText() wipes Scintilla's undo history entirely. You want to use Python string operations instead of low-level Scintilla inserts/replaces, but still let users undo to state1 with Ctrl+Z. The solution is to manually register an undo action using SCI_ADDUNDOACTION, which lets you define custom undo/redo behavior.
Step 1: Add State Save/Restore Helpers
First, create functions to save and restore the full editor state (text + selections):
def save_editor_state(sci): """Save the current text and selection state of the editor""" return { "text": sci.text(), "selections": sci.sel() } def restore_editor_state(sci, state): """Restore the editor to a previously saved state""" # Set the text first sci.setText(state["text"]) # Clear existing selections and restore saved ones sci.clear_selections() for idx, (start, end) in enumerate(state["selections"]): if idx == 0: sci.set_selection((start, end)) else: sci.add_selection((start, end))
Step 2: Register Undo/Redo Actions in Your Main Code
Modify your main block to save state1, switch to state2, then add an undo action that reverts to state1:
if __name__ == '__main__': app = QApplication(sys.argv) view = Editor() # Set initial state and save it set_state1(view) state1 = save_editor_state(view) # Define undo callback: restores state1 when Ctrl+Z is pressed def undo_callback(): restore_editor_state(view, state1) return 0 # Return 0 to confirm the undo was handled # Define redo callback: restores state2 when Ctrl+Y is pressed state2 = None def redo_callback(): restore_editor_state(view, state2) return 0 # Switch to state2 and save its state set_state2(view) state2 = save_editor_state(view) # Add the custom undo action to Scintilla's history # Syntax: SCI_ADDUNDOACTION(flags, undo_func, redo_func, user_data) view.SendScintilla(QsciScintilla.SCI_ADDUNDOACTION, 0, undo_callback, redo_callback, 0) view.move(1000, 100) view.resize(800, 300) view.show() app.exec_()
How This Works
SCI_ADDUNDOACTIONinserts a custom entry into Scintilla's undo stack. When the user presses Ctrl+Z, it runs yourundo_callbackto restore state1. Ctrl+Y triggers theredo_callbackto go back to state2.- This approach lets you use Python string operations (via
setText()) to manage your document state, while still integrating seamlessly with Scintilla's undo system—just like Sublime Text. - We save both text and selections because restoring just the text isn't enough; you want the multi-selection state to be preserved too.
内容的提问来源于stack exchange,提问作者BPL




