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

WPF InkCanvas执行Strokes.Erase()后撤销操作实现问题

WPF InkCanvas触摸擦除后撤销/重做失效?看这里!

首先得指出你当前代码的核心问题:你依赖StrokesChanged事件的AddedRemoved集合来做撤销,但inkCanvas1.Strokes.Erase()根本不会触发这两个集合的更新

为什么?因为Erase()不是删除整个Stroke对象,而是直接修改现有Stroke的几何路径——把和擦除形状重叠的部分剪掉。这种修改属于Stroke内部的变化,不会触发StrokesChanged事件里的新增/删除通知,你的addedStrokesremovedStrokes其实一直是空的,多笔画擦除时自然逻辑混乱。

先回顾你的原代码

你的擦除实现:

private void inkCanvas1_OnTouchMove(object sender, TouchEventArgs touchEventArgs) { 
    StylusShape EraseShape = (StylusShape)new RectangleStylusShape(20, 20, 0); 
    List<Point> enumrator = new List<Point>(); 
    TouchPoint touchPoint = touchEventArgs.GetTouchPoint(this); 
    enumrator.Add(new Point(touchPoint.Position.X, touchPoint.Position.Y)); 
    inkCanvas1.Strokes.Erase(enumrator, EraseShape);//**此语句的反向操作** 
}

失效的撤销逻辑:

System.Windows.Ink.StrokeCollection addedStrokes; 
System.Windows.Ink.StrokeCollection removedStrokes; 
bool undoRedoInProcess = false; 
private void Strokes_StrokesChanged(object sender, System.Windows.Ink.StrokeCollectionChangedEventArgs e) { 
    if (undoRedoInProcess) { 
        addedStrokes = e.Added; 
        removedStrokes = e.Removed; 
    } 
} 
private void Undo() { 
    undoRedoInProcess = true; 
    inkCanvas1.Strokes.Remove(removedStrokes); 
    inkCanvas1.Strokes.Add(addedStrokes); 
    undoRedoInProcess = false; 
} 
private void Redo() { 
    undoRedoInProcess = true; 
    inkCanvas1.Strokes.Add(addedStrokes); 
    inkCanvas1.Strokes.Remove(removedStrokes); 
    undoRedoInProcess = false; 
}

解决方案:用快照栈实现可靠的撤销/重做

既然修改Stroke不会触发新增/删除事件,那我们换个思路:每次操作前保存整个StrokeCollection的快照,撤销时直接恢复上一个快照就行。

1. 定义全局栈变量

我们需要两个栈:一个存撤销历史,一个存重做历史,还要一个标记避免递归触发快照保存:

// 撤销栈:保存每次操作前的完整Stroke状态
private Stack<StrokeCollection> _undoStack = new Stack<StrokeCollection>();
// 重做栈:保存被撤销的操作状态,用于恢复
private Stack<StrokeCollection> _redoStack = new Stack<StrokeCollection>();
// 标记是否正在执行撤销/重做,防止重复记录快照
private bool _isUndoRedoInProgress = false;

2. 修改擦除逻辑,提前存快照

在执行擦除前,先把当前的StrokeCollection克隆一份存入撤销栈,同时清空重做栈(因为新操作会打断重做的历史链):

private void inkCanvas1_OnTouchMove(object sender, TouchEventArgs touchEventArgs) {
    if (_isUndoRedoInProgress) return;

    // 保存当前Stroke状态到撤销栈(用深拷贝,避免引用同一对象)
    _undoStack.Push(new StrokeCollection(inkCanvas1.Strokes));
    _redoStack.Clear(); // 新操作后,重做历史失效,清空

    StylusShape eraseShape = new RectangleStylusShape(20, 20, 0);
    TouchPoint touchPoint = touchEventArgs.GetTouchPoint(this);
    // 简化代码:Erase支持直接传Point数组,不用List
    inkCanvas1.Strokes.Erase(new[] { touchPoint.Position }, eraseShape);
}

3. 实现撤销/重做方法

撤销时,把当前状态存入重做栈,再从撤销栈弹出上一个快照恢复;重做则反过来:

private void Undo() {
    if (_undoStack.Count == 0 || _isUndoRedoInProgress) return;

    _isUndoRedoInProgress = true;
    // 把当前状态存到重做栈,方便后续重做
    _redoStack.Push(new StrokeCollection(inkCanvas1.Strokes));
    // 恢复上一个操作前的快照
    inkCanvas1.Strokes = _undoStack.Pop();
    _isUndoRedoInProgress = false;
}

private void Redo() {
    if (_redoStack.Count == 0 || _isUndoRedoInProgress) return;

    _isUndoRedoInProgress = true;
    // 把当前状态存到撤销栈
    _undoStack.Push(new StrokeCollection(inkCanvas1.Strokes));
    // 恢复被撤销的操作状态
    inkCanvas1.Strokes = _redoStack.Pop();
    _isUndoRedoInProgress = false;
}

4. 额外优化:支持绘制笔画的撤销(可选)

如果你还需要支持撤销绘制的笔画,只需要在StrokesChanged事件里处理新增Stroke的情况:

private void inkCanvas1_StrokesChanged(object sender, StrokeCollectionChangedEventArgs e) {
    if (_isUndoRedoInProgress || e.Added.Count == 0) return;
    
    // 绘制新笔画时,保存当前状态到撤销栈
    _undoStack.Push(new StrokeCollection(inkCanvas1.Strokes));
    _redoStack.Clear();
}

为什么这个方案靠谱?

  • 不管是擦除(修改Stroke)还是绘制(新增Stroke),都用完整快照记录状态,不会漏掉任何变化
  • 撤销和重做栈分离,逻辑清晰,不会出现互相干扰的情况
  • new StrokeCollection(original)做深拷贝,确保每个快照都是独立的对象,不会因为后续修改影响历史记录

内容的提问来源于stack exchange,提问作者Sandeep Jadhav

火山引擎 最新活动