You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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_ADDUNDOACTION inserts a custom entry into Scintilla's undo stack. When the user presses Ctrl+Z, it runs your undo_callback to restore state1. Ctrl+Y triggers the redo_callback to 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

火山引擎 最新活动