Скасувати / Повторити реалізацію


84

Подумайте, як реалізувати функцію скасування / повторення - як у текстових редакторах. Які алгоритми я повинен використовувати і що я можу прочитати. Дякую.


3
Можливо, додайте трохи детальніше про область, в якій працює ваше програмне забезпечення (обробка тексту? Графіка? База даних?) Та, можливо, платформи / мови програмування.
Pekka

Відповіді:


94

Я знаю близько двох основних підрозділів типів скасування

  • ЗБЕРЕГТИ ДЕРЖАВУ: Одна категорія скасування - це місце, де ви фактично зберігаєте стан історії. У цьому випадку трапляється так, що в кожен момент ви продовжуєте зберігати стан у якомусь місці пам'яті. Коли ви хочете відмінити, ви просто поміняєте місцями поточний стан і поміняєте місцями, які вже були в пам'яті. Ось як це робиться за допомогою історії в Adobe Photoshop або, наприклад, повторного відкриття закритих вкладок у Google Chrome.

текст заміщення

  • ГЕНЕРАТИВНА ДЕРЖАВА: Інша категорія - там, де замість того, щоб підтримувати самі штати, ви просто пам’ятаєте, якими були дії. коли вам потрібно відмінити, вам потрібно зробити логічний зворотну дію цієї конкретної дії. Для простого прикладу, коли ви робите Ctrl+ Bв текстовому редакторі, що підтримує скасування, це запам'ятовується як сміливий дію. Тепер з кожною дією відбувається відображення її логічних зворотів. Отже, коли ви робите знак Ctrl+ Z, він переглядає таблицю зворотних дій і виявляє, що дія скасування знову є знаком Ctrl+ B. Це виконується, і ви отримуєте свій попередній стан. Отже, тут ваш попередній стан не зберігався в пам'яті, а генерувався тоді, коли вам це було потрібно.

Для текстових редакторів створення стану таким чином не надто інтенсивне обчислення, але для таких програм, як Adobe Photoshop, це може бути занадто обчислювально або просто неможливо. Наприклад - для дії " Розмиття" ви вкажете дію " Розмиття" , але це ніколи не призведе до вихідного стану, оскільки дані вже втрачені. Отже, залежно від ситуації - можливості логічної зворотної дії та її доцільності, вам потрібно вибрати між цими двома широкими категоріями, а потім реалізувати їх так, як ви хочете. Звичайно, можна мати гібридну стратегію, яка підходить саме вам.

Крім того, іноді, як і в Gmail, можливе скасування часу, оскільки дія (надсилання пошти) ніколи не робиться спочатку. Отже, ви там не "скасовуєте", ви просто "не робите" саму дію.


9
Іноді може бути корисним зберігати суміш станів збереження та "вперед" дій. Як простий підхід, якщо кожні 5 дій зберігати "основний стан збереження", а також зберігати стану збереження після останнього "основного стану збереження", можна було б виконати кілька перших операцій скасування, повернувши стан збереження, і можна було виконати наступне скасування, повторивши 4 дії з попереднього основного збереження. Дещо більш загальним підходом було б використання прогресії потужності двох для різних рівнів стану збереження, таким чином вимагаючи зберігання lg (N) станів збереження для скасування N-рівня, використовуючи ітерації O (1) вперед.
supercat

Відповідь повинен додати і цей гібридний підхід. Це дуже можливо, коли скасування шляхом створення попереднього стану занадто складне, а дані, що редагуються, занадто великі. Баланс між ними. Можна застосувати багато стратегій замість фіксованої прогресії довжиною 4 або довжиною 2. Як стан збереження, коли генерування попереднього стану є занадто складним.
zookastos

20

Я написав два текстові редактори з нуля, і вони обидва використовують дуже примітивну форму функцій скасування / повторення. Під "примітивним" я маю на увазі, що функціонал було дуже просто реалізувати, але що він є неекономічним у дуже великих файлах (скажімо >> 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", якщо розумієте, що я маю на увазі). У дуже великих файлах, де кожна зміна невелика порівняно з розміром файлу, це значно зменшить використання пам’яті даних скасування, але реалізувати це складніше і, можливо, більше схильне до помилок.


@AlexanderSuraphel: Я гадаю, вони використовують "більш просунутий" підхід.
Андреас Рейбранд,

14

Є кілька способів зробити це, але ви можете почати розглядати шаблон Command . Використовуйте список команд, щоб перейти назад (скасувати) або вперед (повторити) за допомогою своїх дій. Приклад на C # можна знайти тут .


8

Трохи пізно, але тут випливає: Ви спеціально посилаєтесь на текстові редактори, що далі пояснює алгоритм, який можна адаптувати до того, що ви редагуєте. Принцип полягає в тому, щоб вести перелік дій / інструкцій, які можна автоматизувати для відтворення кожної внесеної зміни. Не вносіть змін до оригінального файлу (якщо він не порожній), збережіть його як резервну копію.

Зберігайте перелік пов’язаних змін, внесених у вихідний файл. Цей список періодично зберігається у тимчасовому файлі, поки користувач фактично не збереже зміни: коли це станеться, ви застосовуєте зміни до нового файлу, копіюючи старий і одночасно застосовуючи зміни; потім перейменуйте вихідний файл на резервну копію та змініть ім'я нового файлу на правильне. (Ви можете або зберегти збережений список змін, або видалити його та замінити наступним списком змін.)

Кожен вузол у зв’язаному списку містить таку інформацію :.

  • тип змін: ви або вставляєте дані, або видаляєте дані: "змінити" дані означає, що deleteслідують заinsert
  • Позиція у файлі: може бути зміщенням або парою лінія / стовпець
  • буфер даних: це дані, що беруть участь у дії; якщо insertце були дані, які були вставлені; якщо delete- дані, які було видалено.

Для реалізації Undoви працюєте назад від хвоста пов'язаного списку, використовуючи покажчик або індекс "поточний вузол": там, де відбулася зміна insert, ви виконуєте видалення, але без оновлення пов'язаного списку; а там, де це було, deleteви вставляєте дані з даних у буфер зв’язаного списку. Зробіть це для кожної команди "Скасувати" від користувача. Redoпереміщує вказівник 'current-node' вперед і виконує зміни відповідно до вузла. Якщо користувач внесе зміни до коду після скасування, видаліть усі вузли після індикатора 'поточний вузол' до хвоста і встановіть хвіст рівним індикатору 'поточний вузол'. Потім нові зміни користувача вставляються після хвоста. І це все.


8

Мої два центи - це те, що ви хочете використовувати два стеки для відстеження операцій. Кожного разу, коли користувач виконує деякі операції, ваша програма повинна розміщувати ці операції у "виконаному" стеку. Коли користувач хоче відмінити ці операції, просто переведіть операції зі стека "виконано" в стек "відкликання". Коли користувач хоче повторити ці операції, виведіть елементи зі стеку "recall" і поверніть їх назад у "виконаний" стек.

Сподіваюся, це допоможе.


3

Якщо дії оборотні. наприклад, Додавання 1, змусити гравця рухатись тощо, побачити, як використовувати шаблон команди для реалізації скасування / повтору . За посиланням ви знайдете докладні приклади того, як це зробити.

Якщо ні, використовуйте Saved State, як пояснив @Lazer.


2

Ви можете вивчити приклад існуючого фреймворку скасування / повторення, перше звернення Google - це codeplex (для .NET) . Я не знаю, чи це краще чи гірше, ніж будь-який інший фреймворк, їх дуже багато.

Якщо ваша мета полягає у тому, щоб скасувати / повторити функціональність у вашій програмі, ви можете просто вибрати існуючу структуру, яка виглядає придатною для вашого типу додатка.
Якщо ви хочете навчитися створювати власні скасування / повторення, ви можете завантажити вихідний код і переглянути як шаблони, так і деталі, як підключити речі.


2

Для цього був створений зразок Memento .

Перш ніж впроваджувати це самостійно, зауважте, що це досить часто, і код уже існує - Наприклад, якщо ви кодуєте в .Net, ви можете використовувати IEditableObject .


1

Додавши до обговорення, я написав допис у блозі про те, як впроваджувати UNDO та REDO на основі думки про те, що інтуїтивно зрозуміло: http://adamkulidjian.com/undo-and-redo.html


1
Я вдячний вашій дискусії на тему "Впорскування в середину" у дописі в блозі, на який ви посилалися вище. Мій мозок був зв'язаний вузлами, намагаючись зрозуміти, що робити, якщо хтось скасував одну або кілька дій, потім зробив щось нове, а потім повернувся до скасування та / або переробки. Ваша ілюстрація допомогла мені зрозуміти, чому очищення частини "чергової" черги після нової дії з боку користувача є розумним способом збереження розуму. Дякую!
Ден Робінсон,

@DanRobinson Щиро дякую за вашу відповідь. Я витрачаю багато часу на те, щоб писати чистий і зрозумілий допис у блогах із картинками, тому я щасливий, що це змінило ситуацію. :)
Адам

0

Одним із способів реалізації основної функції скасування / повторення є використання шаблонів пам'яті та команд.

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 .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.