WPF InkCanvas执行Strokes.Erase()后撤销操作实现问题
WPF InkCanvas触摸擦除后撤销/重做失效?看这里!
首先得指出你当前代码的核心问题:你依赖StrokesChanged事件的Added和Removed集合来做撤销,但inkCanvas1.Strokes.Erase()根本不会触发这两个集合的更新!
为什么?因为Erase()不是删除整个Stroke对象,而是直接修改现有Stroke的几何路径——把和擦除形状重叠的部分剪掉。这种修改属于Stroke内部的变化,不会触发StrokesChanged事件里的新增/删除通知,你的addedStrokes和removedStrokes其实一直是空的,多笔画擦除时自然逻辑混乱。
先回顾你的原代码
你的擦除实现:
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




