실행 취소 / 다시 실행 구현


84

텍스트 편집기에서와 같이 실행 취소 / 다시 실행 기능을 구현하는 방법에 대해 생각해보십시오. 어떤 알고리즘을 사용해야하며 무엇을 읽을 수 있는지. 감사.


3
소프트웨어가 작동하는 영역 (텍스트 처리? 그래픽? 데이터베이스?)과 플랫폼 / 프로그래밍 언어에 대한 세부 정보를 추가 할 수 있습니다.
Pekka

답변:


94

실행 취소 유형의 두 가지 주요 구분에 대해 알고 있습니다.

  • 상태 저장 : 실행 취소의 한 범주는 실제로 기록 상태를 저장하는 곳입니다. 이 경우 발생하는 일은 모든 지점에서 상태를 메모리의 특정 위치에 저장하는 것입니다. 실행 취소를 원할 때 현재 상태를 교체하고 이미 메모리에 있던 상태로 교체하면됩니다. 예를 들어 Adobe Photoshop에서 History를 사용하거나 Google Chrome에서 닫힌 탭을 다시 여는 방법입니다.

대체 텍스트

  • 상태 생성 : 다른 범주는 상태 자체를 유지하는 대신 작업이 무엇인지 기억하는 것입니다. 실행을 취소해야하는 경우 해당 특정 작업을 논리적으로 반대로 수행해야합니다. 간단한 예를 들어, 실행 취소를 지원하는 일부 텍스트 편집기에서 Ctrl+ 를 수행 B하면 굵은 작업으로 기억됩니다 . 이제 각 작업에는 논리적 반전 매핑이 있습니다. 따라서 Ctrl+ 를 수행하면 Z역 행동 테이블에서 조회하여 실행 취소 조치가 다시 Ctrl+ 임을 발견 B합니다. 그것이 수행되고 이전 상태를 얻습니다. 따라서 여기서 이전 상태는 메모리에 저장되지 않고 필요할 때 생성되었습니다.

텍스트 편집기의 경우 이러한 방식으로 상태를 생성하는 것은 계산 집약적이지 않지만 Adobe Photoshop과 같은 프로그램의 경우 계산 집약적이거나 불가능할 수 있습니다. 예를 들어 -Blur 작업의 경우 De-Blur 작업을 지정 하지만 데이터가 이미 손실 되었기 때문에 원래 상태 로 되돌릴 수 없습니다. 따라서 상황에 따라 논리적 역행 동의 가능성과 그 가능성에 따라이 두 가지 광범위한 범주 중에서 선택한 다음 원하는 방식으로 구현해야합니다. 물론 자신에게 맞는 하이브리드 전략을 가질 수 있습니다.

또한 Gmail과 마찬가지로 작업 (메일 보내기)이 처음에 수행되지 않기 때문에 시간 제한적으로 실행 취소가 가능한 경우도 있습니다. 그래서, 당신은 거기에서 "실행 취소"하는 것이 아니라 단지 액션 자체를 "하지 않는"것입니다.


9
때로는 저장 상태와 "전달"작업을 혼합하여 유지하는 것이 도움이 될 수 있습니다. 간단한 접근 방식으로, 5 개의 작업마다 "주 저장 상태"를 유지하고 마지막 "주 저장 상태"이후에도 저장 상태를 유지한다면 저장 상태를 되돌려 처음 몇 번의 실행 취소 작업을 수행 할 수 있습니다. 다음 실행 취소는 이전 주요 저장에서 4 개의 작업을 반복합니다. 좀 더 일반적인 접근 방식은 서로 다른 수준의 저장 상태에 대해 2의 거듭 제곱 진행을 사용하는 것이므로 O (1) 순방향 반복을 사용하여 N 수준 실행 취소에 대해 lg (N) 저장 상태를 저장해야합니다.
supercat 2010

대답은이 하이브리드 접근 방식도 추가해야합니다. 이것은 이전 상태를 생성하여 실행 취소하는 것이 너무 복잡하고 편집중인 데이터가 너무 큰 경우 매우 실행 가능합니다. 둘 사이의 균형. 고정 된 4 길이 또는 2의 제곱 길이 진행 대신 많은 전략을 채택 할 수 있습니다. 이전 상태를 생성하는 것이 너무 복잡 할 때마다 저장 상태와 같습니다.
zookastos

20

필자는 처음부터 두 개의 텍스트 편집기를 작성했으며 둘 다 매우 원시적 인 형태의 실행 취소 / 다시 실행 기능을 사용합니다. "기본"이란 기능이 구현하기 매우 쉬웠지만 매우 큰 파일 (예 : >> 10MB)에서는 비 경제적이라는 것을 의미합니다. 그러나 시스템은 매우 유연합니다. 예를 들어 무제한 수준의 실행 취소를 지원합니다.

기본적으로 다음과 같은 구조를 정의합니다.

type
  TUndoDataItem = record
    text: /array of/ string;
    selBegin: integer;
    selEnd: integer;
    scrollPos: TPoint;
  end;

그런 다음 배열을 정의하십시오.

var
  UndoData: array of TUndoDataItem;

그런 다음이 배열의 각 구성원은 텍스트의 저장된 상태를 지정합니다. 이제 텍스트를 편집 할 때마다 (문자 키 내리기, 백 스페이스 내리기, 키 내리기, 잘라 내기 / 붙여 넣기, 마우스로 이동 한 선택 등) 1 초의 타이머를 (다시) 시작합니다. 트리거되면 타이머는 현재 상태를 UndoData배열 의 새 구성원으로 저장합니다 .

실행 취소 (Ctrl + Z), 나는 상태로 편집기를 복원 UndoData[UndoLevel - 1]하고 감소 UndoLevel하나. 기본적으로 UndoLevelUndoData배열 의 마지막 멤버의 인덱스와 같습니다 . 다시 실행 (Ctrl + Y 또는 Shift + Ctrl + Z) 할 때 편집기를 상태로 복원합니다.UndoData[UndoLevel + 1]UndoLevel 1 씩 증가시킵니다 . 물론 편집 타이머가 배열 UndoLevel의 길이 (마이너스 1)와 같지 않을 때 트리거되는 경우 Microsoft Windows 플랫폼에서 일반적으로 사용되는 것처럼 UndoData이 배열의 모든 항목을. UndoLevel올바르게-Microsoft Windows 접근 방식의 단점은 많은 변경 사항을 실행 취소 한 다음 실수로 버퍼를 편집하면 이전 콘텐츠 (취소되지 않은)가 영구적으로 손실된다는 것입니다. 이 어레이 축소를 건너 뛸 수 있습니다.

예를 들어 이미지 편집기와 같은 다른 유형의 프로그램에서 동일한 기술을 적용 할 수 있지만 물론 완전히 다른 UndoDataItem구조를 사용합니다. 많은 메모리를 필요로하지 않는 고급 접근 방식 은 실행 취소 수준 간의 변경 사항 만 저장 하는 것입니다 (즉, "alpha \ nbeta \ gamma"및 "alpha \ nbeta \ ngamma \ ndelta"를 저장하는 대신). "알파 \ n 베타 \ n 감마"및 "추가 \ n 델타"를 저장하십시오. 각 변경 사항이 파일 크기에 비해 작은 매우 큰 파일에서는 실행 취소 데이터의 메모리 사용량이 크게 줄어들지 만 구현하기가 까다 롭고 오류가 발생할 가능성이 더 높습니다.


@AlexanderSuraphel : 나는 그들이 "보다 진보 된"접근 방식을 사용한다고 생각합니다.
Andreas Rejbrand


8

조금 늦었지만 여기에 있습니다. 특별히 텍스트 편집기를 언급하면 ​​다음은 편집중인 모든 것에 적용 할 수있는 알고리즘을 설명합니다. 관련된 원칙은 각 변경 사항을 다시 생성하기 위해 자동화 할 수있는 작업 / 지침 목록을 유지하는 것입니다. 원본 파일을 변경하지 말고 (비어 있지 않은 경우) 백업용으로 보관하십시오.

원본 파일에 대한 변경 사항의 앞뒤 링크 목록을 유지합니다. 이 목록은 사용자가 실제로 변경 사항 을 저장할 때까지 임시 파일에 간헐적으로 저장 됩니다.이 경우 변경 사항을 새 파일에 적용하고 이전 파일을 복사하고 동시에 변경 사항을 적용합니다. 그런 다음 원본 파일의 이름을 백업으로 바꾸고 새 파일의 이름을 올바른 이름으로 변경하십시오. (저장된 변경 목록을 유지하거나 삭제하고 후속 변경 목록으로 바꿀 수 있습니다.)

연결 목록의 각 노드에는 다음 정보가 포함됩니다.

  • 변경 유형 : 데이터를 삽입하거나 데이터를 삭제합니다. 데이터를 "변경"한다는 것은 a delete뒤에insert
  • 파일 내 위치 : 오프셋 또는 행 / 열 쌍일 수 있습니다.
  • 데이터 버퍼 : 작업과 관련된 데이터입니다. insert삽입 된 데이터 인 경우 인 경우 delete삭제 된 데이터입니다.

을 구현하려면 Undo'현재 노드'포인터 또는 색인을 사용하여 연결 목록의 끝에서 뒤로 작업합니다. 변경된 위치 insert에서 연결 목록을 업데이트하지 않고 삭제를 수행합니다. 그리고 그것은 어디 delete링크 된 목록 버퍼의 데이터에서 데이터를 삽입합니다. 사용자의 각 '실행 취소'명령에 대해이 작업을 수행하십시오. Redo'현재 노드'포인터를 앞으로 이동하고 노드별로 변경을 실행합니다. 사용자가 실행 취소 후 코드를 변경하는 경우 'current-node'표시기 뒤의 모든 노드를 tail까지 삭제하고 tail을 'current-node'표시기와 동일하게 설정합니다. 그런 다음 사용자의 새로운 변경 사항이 꼬리 뒤에 삽입됩니다. 그게 다입니다.


8

내 두 센트는 작업을 추적하기 위해 두 개의 스택을 사용하고 싶다는 것입니다. 사용자가 일부 작업을 수행 할 때마다 프로그램은 해당 작업을 "수행 된"스택에 배치해야합니다. 사용자가 이러한 작업을 실행 취소하려면 "수행 된"스택에서 "리콜"스택으로 작업을 팝하면됩니다. 사용자가 이러한 작업을 다시 실행하려면 "재 호출"스택에서 항목을 팝하고 "수행 된"스택으로 다시 푸시합니다.

도움이 되었기를 바랍니다.



2

기존 실행 취소 / 다시 실행 프레임 워크의 예를 연구 할 수 있습니다. 첫 번째 Google 히트 곡은 codeplex (.NET 용)입니다. 입니다. 다른 프레임 워크보다 더 좋은지 더 나쁜지는 모르겠습니다.

목표가 애플리케이션에 실행 취소 / 다시 실행 기능을 포함하는 것이라면 애플리케이션 종류에 적합한 기존 프레임 워크를 선택하는 것이 좋습니다.
자신 만의 실행 취소 / 재실행을 빌드하는 방법을 배우고 싶다면 소스 코드를 다운로드하고 패턴과 연결 방법을 자세히 살펴볼 수 있습니다.



1

토론에 덧붙여, 직관적 인 것에 대한 생각을 바탕으로 UNDO 및 REDO를 구현하는 방법에 대한 블로그 게시물을 작성했습니다. http://adamkulidjian.com/undo-and-redo.html


1
위에 링크 한 블로그 게시물에서 "Injection into the Middle"에 대해 논의 해 주셔서 감사합니다. 내 두뇌는 누군가가 하나 이상의 행동을 취소하고 새로운 행동을 취한 다음 다시 실행 취소 및 / 또는 다시 실행하는 경우 어떻게해야하는지 알아 내려고 매듭에 묶여있었습니다. 귀하의 그림은 사용자가 새로운 작업을 수행 한 후 대기열의 "다시 실행"부분을 지우는 것이 온 전성을 유지하는 합리적인 방법 인 이유를 이해하는 데 도움이되었습니다. 감사!
Dan Robinson

@DanRobinson 응답 해 주셔서 감사합니다. 깨끗하고 이해하기 쉬운 그림과 함께 블로그 게시물을 작성하는 데 많은 시간을 투자했기 때문에 차이가 생긴 것이 기쁩니다. :)
Adam

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 설명서 페이지 에 자세히 설명되어 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.