Подумайте, як реалізувати функцію скасування / повторення - як у текстових редакторах. Які алгоритми я повинен використовувати і що я можу прочитати. Дякую.
Подумайте, як реалізувати функцію скасування / повторення - як у текстових редакторах. Які алгоритми я повинен використовувати і що я можу прочитати. Дякую.
Відповіді:
Я знаю близько двох основних підрозділів типів скасування
Для текстових редакторів створення стану таким чином не надто інтенсивне обчислення, але для таких програм, як Adobe Photoshop, це може бути занадто обчислювально або просто неможливо. Наприклад - для дії " Розмиття" ви вкажете дію " Розмиття" , але це ніколи не призведе до вихідного стану, оскільки дані вже втрачені. Отже, залежно від ситуації - можливості логічної зворотної дії та її доцільності, вам потрібно вибрати між цими двома широкими категоріями, а потім реалізувати їх так, як ви хочете. Звичайно, можна мати гібридну стратегію, яка підходить саме вам.
Крім того, іноді, як і в Gmail, можливе скасування часу, оскільки дія (надсилання пошти) ніколи не робиться спочатку. Отже, ви там не "скасовуєте", ви просто "не робите" саму дію.
Я написав два текстові редактори з нуля, і вони обидва використовують дуже примітивну форму функцій скасування / повторення. Під "примітивним" я маю на увазі, що функціонал було дуже просто реалізувати, але що він є неекономічним у дуже великих файлах (скажімо >> 10 МБ). Однак система дуже гнучка; наприклад, він підтримує необмежений рівень скасування.
В основному, я визначаю структуру типу
type
TUndoDataItem = record
text: /array of/ string;
selBegin: integer;
selEnd: integer;
scrollPos: TPoint;
end;
а потім визначити масив
var
UndoData: array of TUndoDataItem;
Потім кожен член цього масиву вказує збережений стан тексту. Тепер, при кожному редагуванні тексту (клавіша вниз, зворотний простір вниз, клавіша видалення, вирізання / вставка, виділення, переміщене за допомогою миші тощо), я (повторно) запускаю таймер (скажімо) одну секунду. При спрацьовуванні таймер зберігає поточний стан як новий член UndoData
масиву.
При скасуванні (Ctrl + Z) я відновлюю стан редактора UndoData[UndoLevel - 1]
та зменшую його UndoLevel
на один. За замовчуванням UndoLevel
дорівнює індексу останнього члена UndoData
масиву. Повторно (Ctrl + Y або Shift + Ctrl + Z) я відновлюю редактор до стану UndoData[UndoLevel + 1]
і збільшую на UndoLevel
одиницю. Звичайно, якщо таймер редагування спрацьовує, коли UndoLevel
не дорівнює довжині (мінус один) UndoData
масиву, я очищаю всі елементи цього масиву післяUndoLevel
, як це звичайно на платформі Microsoft Windows (але Emacs краще, якщо я згадую правильно - недоліком підходу Microsoft Windows є те, що якщо ви скасуєте багато змін, а потім випадково відредагуєте буфер, попередній вміст (який було скасовано) назавжди втрачено). Можливо, ви захочете пропустити це зменшення масиву.
У програмах іншого типу, наприклад, редакторі зображень, може застосовуватися однакова техніка, але, звичайно, із зовсім іншою UndoDataItem
структурою. Більш просунутий підхід, який не вимагає стільки пам'яті, полягає у збереженні лише змін між рівнями скасування (тобто замість збереження "альфа \ nбета \ гамма" та "альфа \ нбета \ нгамма \ ндельта", ви можете збережіть "alpha \ nbeta \ ngamma" та "ADD \ ndelta", якщо розумієте, що я маю на увазі). У дуже великих файлах, де кожна зміна невелика порівняно з розміром файлу, це значно зменшить використання пам’яті даних скасування, але реалізувати це складніше і, можливо, більше схильне до помилок.
Є кілька способів зробити це, але ви можете почати розглядати шаблон Command . Використовуйте список команд, щоб перейти назад (скасувати) або вперед (повторити) за допомогою своїх дій. Приклад на C # можна знайти тут .
Трохи пізно, але тут випливає: Ви спеціально посилаєтесь на текстові редактори, що далі пояснює алгоритм, який можна адаптувати до того, що ви редагуєте. Принцип полягає в тому, щоб вести перелік дій / інструкцій, які можна автоматизувати для відтворення кожної внесеної зміни. Не вносіть змін до оригінального файлу (якщо він не порожній), збережіть його як резервну копію.
Зберігайте перелік пов’язаних змін, внесених у вихідний файл. Цей список періодично зберігається у тимчасовому файлі, поки користувач фактично не збереже зміни: коли це станеться, ви застосовуєте зміни до нового файлу, копіюючи старий і одночасно застосовуючи зміни; потім перейменуйте вихідний файл на резервну копію та змініть ім'я нового файлу на правильне. (Ви можете або зберегти збережений список змін, або видалити його та замінити наступним списком змін.)
Кожен вузол у зв’язаному списку містить таку інформацію :.
delete
слідують заinsert
insert
це були дані, які були вставлені; якщо delete
- дані, які було видалено.Для реалізації Undo
ви працюєте назад від хвоста пов'язаного списку, використовуючи покажчик або індекс "поточний вузол": там, де відбулася зміна insert
, ви виконуєте видалення, але без оновлення пов'язаного списку; а там, де це було, delete
ви вставляєте дані з даних у буфер зв’язаного списку. Зробіть це для кожної команди "Скасувати" від користувача. Redo
переміщує вказівник 'current-node' вперед і виконує зміни відповідно до вузла. Якщо користувач внесе зміни до коду після скасування, видаліть усі вузли після індикатора 'поточний вузол' до хвоста і встановіть хвіст рівним індикатору 'поточний вузол'. Потім нові зміни користувача вставляються після хвоста. І це все.
Мої два центи - це те, що ви хочете використовувати два стеки для відстеження операцій. Кожного разу, коли користувач виконує деякі операції, ваша програма повинна розміщувати ці операції у "виконаному" стеку. Коли користувач хоче відмінити ці операції, просто переведіть операції зі стека "виконано" в стек "відкликання". Коли користувач хоче повторити ці операції, виведіть елементи зі стеку "recall" і поверніть їх назад у "виконаний" стек.
Сподіваюся, це допоможе.
Якщо дії оборотні. наприклад, Додавання 1, змусити гравця рухатись тощо, побачити, як використовувати шаблон команди для реалізації скасування / повтору . За посиланням ви знайдете докладні приклади того, як це зробити.
Якщо ні, використовуйте Saved State, як пояснив @Lazer.
Ви можете вивчити приклад існуючого фреймворку скасування / повторення, перше звернення Google - це codeplex (для .NET) . Я не знаю, чи це краще чи гірше, ніж будь-який інший фреймворк, їх дуже багато.
Якщо ваша мета полягає у тому, щоб скасувати / повторити функціональність у вашій програмі, ви можете просто вибрати існуючу структуру, яка виглядає придатною для вашого типу додатка.
Якщо ви хочете навчитися створювати власні скасування / повторення, ви можете завантажити вихідний код і переглянути як шаблони, так і деталі, як підключити речі.
Для цього був створений зразок Memento .
Перш ніж впроваджувати це самостійно, зауважте, що це досить часто, і код уже існує - Наприклад, якщо ви кодуєте в .Net, ви можете використовувати IEditableObject .
Додавши до обговорення, я написав допис у блозі про те, як впроваджувати UNDO та REDO на основі думки про те, що інтуїтивно зрозуміло: http://adamkulidjian.com/undo-and-redo.html
Одним із способів реалізації основної функції скасування / повторення є використання шаблонів пам'яті та команд.
Memento спрямований на те, щоб зберегти стан об’єкта, який буде відновлено пізніше, наприклад. Ця пам’ятка повинна бути якомога меншою з метою оптимізації.
У командному моделі инкапсулирует як об'єкт (команди) деякі інструкції для виконання при необхідності.
На основі цих двох концепцій ви можете написати базову історію скасування / повторення, як-от наступну, кодовану в TypeScript ( витягнуту та адаптовану з інтерфейсної бібліотеки Interacto ).
Така історія спирається на два стеки:
Коментарі подаються в рамках алгоритму. Просто зауважте, що під час операції скасування, стек повторення повинен бути очищений! Причина полягає в тому, щоб застосувати додаток у стабільному стані: якщо ви повернетесь у минуле, щоб повторити деякі дії, які ви зробили, ваші колишні дії більше не існуватимуть, коли ви змінюєте майбутнє.
export class UndoHistory {
/** The undoable objects. */
private readonly undos: Array<Undoable>;
/** The redoable objects. */
private readonly redos: Array<Undoable>;
/** The maximal number of undo. */
private sizeMax: number;
public constructor() {
this.sizeMax = 0;
this.undos = [];
this.redos = [];
this.sizeMax = 30;
}
/** Adds an undoable object to the collector. */
public add(undoable: Undoable): void {
if (this.sizeMax > 0) {
// Cleaning the oldest undoable object
if (this.undos.length === this.sizeMax) {
this.undos.pop();
}
this.undos.push(undoable);
// You must clear the redo stack!
this.clearRedo();
}
}
private clearRedo(): void {
if (this.redos.length > 0) {
this.redos.length = 0;
}
}
/** Undoes the last undoable object. */
public undo(): void {
const undoable = this.undos.pop();
if (undoable !== undefined) {
undoable.undo();
this.redos.push(undoable);
}
}
/** Redoes the last undoable object. */
public redo(): void {
const undoable = this.redos.pop();
if (undoable !== undefined) {
undoable.redo();
this.undos.push(undoable);
}
}
}
Undoable
Інтерфейс досить простий:
export interface Undoable {
/** Undoes the command */
undo(): void;
/** Redoes the undone command */
redo(): void;
}
Тепер ви можете писати команди, які не можна відмінити, які діють на вашу програму.
Наприклад (все ще на основі прикладів Interacto), ви можете написати команду, подібну до цієї:
export class ClearTextCmd implements Undoable {
// The memento that saves the previous state of the text data
private memento: string;
public constructor(private text: TextData) {}
// Executes the command
public execute() void {
// Creating the memento
this.memento = this.text.text;
// Applying the changes (in many
// cases do and redo are similar, but the memento creation)
redo();
}
public undo(): void {
this.text.text = this.memento;
}
public redo(): void {
this.text.text = '';
}
}
Тепер ви можете виконати та додати команду до екземпляра UndoHistory:
const cmd = new ClearTextCmd(...);
//...
undoHistory.add(cmd);
Нарешті, ви можете прив’язати кнопку скасування (або ярлик) до цієї історії (те саме для повторення).
Такі приклади детально описані на сторінці документації Interacto .