Hoàn tác / Làm lại triển khai


81

Hãy cho tôi một số suy nghĩ về cách triển khai chức năng hoàn tác / làm lại - giống như chúng tôi có trong trình soạn thảo văn bản. Tôi nên sử dụng những thuật toán nào và những gì tôi có thể đọc. cảm ơn.


3
Có thể thêm một số chi tiết về lĩnh vực phần mềm của bạn đang hoạt động (xử lý văn bản? Đồ họa? Cơ sở dữ liệu?) Và có thể là nền tảng / ngôn ngữ lập trình.
Pekka

Câu trả lời:


92

Tôi biết về hai bộ phận chính của các loại hoàn tác

  • LƯU TRẠNG THÁI: Một danh mục hoàn tác là nơi bạn thực sự lưu trạng thái lịch sử. Trong trường hợp này, điều xảy ra là tại mọi thời điểm bạn tiếp tục lưu trạng thái vào một số vị trí của bộ nhớ. Khi bạn muốn hoàn tác, bạn chỉ cần hoán đổi trạng thái hiện tại và hoán đổi trạng thái đã có trong bộ nhớ. Đây là cách nó được thực hiện với Lịch sử trong Adobe Photoshop hoặc mở lại các tab đã đóng trong Google Chrome chẳng hạn.

văn bản thay thế

  • TẠO trạng thái: Danh mục khác là nơi thay vì duy trì chính các trạng thái, bạn chỉ cần nhớ các hành động là gì. khi bạn cần hoàn tác, bạn cần thực hiện ngược lại hợp lý của hành động cụ thể đó. Ví dụ đơn giản, khi bạn thực hiện dấu Ctrl+ Btrong một số trình soạn thảo văn bản hỗ trợ hoàn tác, nó được ghi nhớ là hành động In đậm . Bây giờ với mỗi hành động là một ánh xạ của các đảo ngược logic của nó. Vì vậy, khi bạn thực hiện dấu Ctrl+ Z, nó sẽ tra cứu từ bảng hành động nghịch đảo và thấy rằng hành động hoàn tác là dấu Ctrl+ Bmột lần nữa. Điều đó được thực hiện và bạn có được trạng thái trước đó của mình. Vì vậy, ở đây trạng thái trước đó của bạn không được lưu trữ trong bộ nhớ mà được tạo ra khi bạn cần.

Đối với các trình soạn thảo văn bản, việc tạo trạng thái theo cách này không quá chuyên sâu về tính toán nhưng đối với các chương trình như Adobe Photoshop, nó có thể quá chuyên sâu về tính toán hoặc đơn giản là không thể. Ví dụ - đối với hành động Làm mờ , bạn sẽ chỉ định hành động Bỏ làm mờ , nhưng điều đó không bao giờ có thể đưa bạn về trạng thái ban đầu vì dữ liệu đã bị mất. Vì vậy, tùy thuộc vào tình huống - khả năng xảy ra một hành động nghịch đảo hợp lý và tính khả thi của nó, bạn cần phải lựa chọn giữa hai loại rộng này, và sau đó thực hiện chúng theo cách bạn muốn. Tất nhiên, có thể có một chiến lược kết hợp phù hợp với bạn.

Ngoài ra, đôi khi, giống như trong Gmail, có thể hoàn tác có giới hạn thời gian vì hành động (gửi thư) không bao giờ được thực hiện ngay từ đầu. Vì vậy, bạn không "hoàn tác" ở đó, bạn chỉ "không thực hiện" chính hành động đó.


9
Đôi khi có thể hữu ích nếu giữ hỗn hợp trạng thái lưu và hành động "chuyển tiếp". Như một cách tiếp cận đơn giản, nếu một người giữ "trạng thái lưu chính" sau mỗi 5 hành động và cũng giữ các trạng thái lưu sau "trạng thái lưu chính" cuối cùng, người ta có thể thực hiện một số thao tác hoàn tác đầu tiên bằng cách hoàn nguyên trạng thái lưu và người ta có thể thực hiện hoàn tác tiếp theo bằng cách lặp lại 4 hành động từ lần lưu chính trước đó. Một cách tiếp cận hơi tổng quát hơn sẽ là sử dụng lũy ​​thừa lũy thừa hai cho các mức trạng thái lưu khác nhau, do đó yêu cầu lưu trữ các trạng thái lưu lg (N) để hoàn tác mức N, sử dụng các lần lặp chuyển tiếp O (1).
supercat

Câu trả lời cũng nên thêm phương pháp kết hợp này. Điều này rất khả thi khi việc hoàn tác bằng cách tạo trạng thái trước đó quá phức tạp và dữ liệu đang được chỉnh sửa quá lớn. Sự cân bằng giữa hai điều này. Người ta có thể áp dụng nhiều chiến lược thay vì diễn tiến độ dài cố định 4 độ dài hoặc lũy thừa 2. Giống như trạng thái lưu bất cứ khi nào tạo trạng thái trước đó là quá phức tạp.
zookastos

20

Tôi đã viết hai trình soạn thảo văn bản từ đầu và cả hai đều sử dụng một dạng chức năng hoàn tác / làm lại rất sơ khai. Theo "nguyên thủy", ý tôi là chức năng này rất dễ thực hiện, nhưng nó không kinh tế trong các tệp rất lớn (giả sử >> 10 MB). Tuy nhiên, hệ thống rất linh hoạt; chẳng hạn, nó hỗ trợ các mức hoàn tác không giới hạn.

Về cơ bản, tôi xác định một cấu trúc như

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

và sau đó xác định một mảng

var
  UndoData: array of TUndoDataItem;

Sau đó, mỗi thành viên của mảng này chỉ định một trạng thái đã lưu của văn bản. Bây giờ, trên mỗi lần chỉnh sửa văn bản (xuống phím ký tự, xóa lùi, xóa phím xuống, cắt / dán, di chuyển lựa chọn bằng chuột, v.v.), tôi (lại) bắt đầu hẹn giờ (giả sử) một giây. Khi được kích hoạt, bộ định thời lưu trạng thái hiện tại như một thành viên mới của UndoDatamảng.

Khi hoàn tác (Ctrl + Z), tôi khôi phục trình chỉnh sửa về trạng thái UndoData[UndoLevel - 1]và giảm UndoLeveltừng cái một. Theo mặc định, UndoLevelbằng chỉ số của thành viên cuối cùng của UndoDatamảng. Khi làm lại (Ctrl + Y hoặc Shift + Ctrl + Z), tôi khôi phục trình chỉnh sửa về trạng thái UndoData[UndoLevel + 1]và tăng UndoLeveltừng cái một. Tất nhiên, nếu bộ hẹn giờ chỉnh sửa được kích hoạt khi UndoLevelkhông bằng độ dài (trừ đi một) của UndoDatamảng, tôi sẽ xóa tất cả các mục của mảng này sau đó UndoLevel, như thường lệ trên nền tảng Microsoft Windows (nhưng Emacs thì tốt hơn, nếu tôi nhớ lại một cách chính xác - nhược điểm của phương pháp Microsoft Windows là, nếu bạn hoàn tác nhiều thay đổi và sau đó vô tình chỉnh sửa bộ đệm, nội dung trước đó (đã được hoàn tác) sẽ bị mất vĩnh viễn). Bạn có thể muốn bỏ qua việc giảm mảng này.

Trong một loại chương trình khác, chẳng hạn, một trình chỉnh sửa hình ảnh, kỹ thuật tương tự có thể được áp dụng, nhưng tất nhiên, với một UndoDataItemcấu trúc hoàn toàn khác . Một cách tiếp cận nâng cao hơn, không yêu cầu nhiều bộ nhớ, là chỉ lưu các thay đổi giữa các cấp độ hoàn tác (nghĩa là, thay vì lưu "alpha \ nbeta \ gamma" và "alpha \ nbeta \ ngamma \ ndelta", bạn có thể lưu "alpha \ nbeta \ ngamma" và "ADD \ ndelta", nếu bạn hiểu ý tôi). Trong các tệp rất lớn mà mỗi thay đổi là nhỏ so với kích thước tệp, điều này sẽ làm giảm đáng kể mức sử dụng bộ nhớ của dữ liệu hoàn tác, nhưng thực hiện khó hơn và có thể dễ xảy ra lỗi hơn.


@AlexanderSuraphel: Tôi đoán họ sử dụng cách tiếp cận "nâng cao hơn".
Andreas Rejbrand

14

Có một số cách để thực hiện việc này, nhưng bạn có thể bắt đầu xem mẫu Command . Sử dụng danh sách các lệnh để quay lại (Hoàn tác) hoặc chuyển tiếp (làm lại) thông qua các hành động của bạn. Một ví dụ trong C # có thể được tìm thấy ở đây .


8

Hơi muộn, nhưng đây là: Bạn đề cập cụ thể đến trình soạn thảo văn bản, những gì tiếp theo giải thích một thuật toán có thể được điều chỉnh cho bất cứ điều gì bạn đang chỉnh sửa. Nguyên tắc liên quan là giữ một danh sách các hành động / hướng dẫn có thể được tự động hóa để tạo lại mỗi thay đổi bạn đã thực hiện. Không thực hiện các thay đổi đối với tệp gốc (nếu không trống), hãy giữ nó như một bản sao lưu.

Giữ một danh sách liên kết xuôi-ngược về những thay đổi bạn thực hiện đối với tệp gốc. Danh sách này được lưu không liên tục vào một tệp tạm thời, cho đến khi người dùng thực sự Lưu các thay đổi: khi điều này xảy ra, bạn áp dụng các thay đổi cho tệp mới, sao chép tệp cũ và áp dụng đồng thời các thay đổi; sau đó đổi tên tệp gốc thành bản sao lưu và đổi tên tệp mới thành tên chính xác. (Bạn có thể giữ danh sách thay đổi đã lưu hoặc xóa nó và thay thế bằng danh sách thay đổi tiếp theo.)

Mỗi nút trong danh sách liên kết chứa các thông tin sau:

  • gõ của sự thay đổi: bạn có chèn dữ liệu, hoặc bạn có dữ liệu xóa: để "thay đổi" có nghĩa là dữ liệu một deletetiếp theo là mộtinsert
  • vị trí trong tệp: có thể là một cặp lệch hoặc dòng / cột
  • bộ đệm dữ liệu: đây là dữ liệu liên quan đến hành động; nếu insertđó là dữ liệu đã được chèn vào; nếu delete, dữ liệu đã bị xóa.

Để triển khai Undo, bạn làm việc ngược lại từ phần cuối của danh sách liên kết, sử dụng con trỏ hoặc chỉ mục 'nút hiện tại': nơi thay đổi insert, bạn thực hiện xóa nhưng không cập nhật danh sách được liên kết; và nơi deletebạn chèn dữ liệu từ dữ liệu trong bộ đệm danh sách liên kết. Thực hiện việc này cho mỗi lệnh 'Hoàn tác' từ người dùng. Redodi chuyển con trỏ 'nút hiện tại' về phía trước và thực hiện thay đổi theo nút. Nếu người dùng thực hiện thay đổi đối với mã sau khi hoàn tác, hãy xóa tất cả các nút sau chỉ báo 'nút hiện tại' đến đuôi và đặt đuôi bằng với chỉ báo 'nút hiện tại'. Các thay đổi mới của người dùng sau đó được chèn vào sau đuôi. Và đó là về nó.


8

Hai xu duy nhất của tôi là bạn muốn sử dụng hai ngăn xếp để theo dõi các hoạt động. Mỗi khi người dùng thực hiện một số thao tác, chương trình của bạn nên đặt các thao tác đó vào một ngăn xếp "đã thực hiện". Khi người dùng muốn hoàn tác các thao tác đó, chỉ cần bật các thao tác từ ngăn xếp "đã thực hiện" sang ngăn xếp "thu hồi". Khi người dùng muốn thực hiện lại các thao tác đó, bật các mục từ ngăn xếp "thu hồi" và đẩy chúng trở lại ngăn xếp "đã thực hiện".

Hy vọng nó giúp.



2

Bạn có thể nghiên cứu một ví dụ về khung hoàn tác / làm lại hiện có, lần truy cập đầu tiên của Google là trên codeplex (dành cho .NET) . Tôi không biết liệu điều đó tốt hơn hay tệ hơn bất kỳ khuôn khổ nào khác, có rất nhiều trong số chúng.

Nếu mục tiêu của bạn là có chức năng hoàn tác / làm lại trong ứng dụng của mình, bạn cũng có thể chỉ cần chọn một khung hiện có trông phù hợp với loại ứng dụng của bạn.
Nếu bạn muốn tìm hiểu cách xây dựng hoàn tác / làm lại của riêng mình, bạn có thể tải xuống mã nguồn và xem cả hai mẫu cũng như chi tiết cách kết nối mọi thứ.


2

Mẫu Memento được tạo ra cho việc này.

Trước khi tự mình thực hiện điều này, hãy lưu ý rằng điều này khá phổ biến và mã đã tồn tại - Ví dụ: nếu bạn đang viết mã trong .Net, bạn có thể sử dụng IEditableObject .



0

Một cách để triển khai tính năng hoàn tác / làm lại cơ bản là sử dụng cả mẫu thiết kế lưu niệm và lệnh.

Memento nhằm mục đích giữ trạng thái của một đối tượng sẽ được khôi phục sau này. Vật lưu niệm này phải càng nhỏ càng tốt nhằm mục đích tối ưu hóa.

Mẫu lệnh gói gọn trong một đối tượng (một lệnh) một số lệnh để thực thi khi được yêu cầu.

Dựa trên hai khái niệm này, bạn có thể viết lịch sử hoàn tác / làm lại cơ bản, như lịch sử sau được mã hóa trong TypeScript ( được trích xuất và điều chỉnh từ thư viện giao diện người dùng Interacto ).

Một lịch sử như vậy dựa trên hai ngăn xếp:

  • ngăn xếp cho các đối tượng có thể được hoàn tác
  • ngăn xếp cho các đối tượng có thể được làm lại

Nhận xét được cung cấp trong thuật toán. Chỉ cần lưu ý rằng trong một thao tác hoàn tác, ngăn xếp làm lại phải được xóa! Lý do là để ứng dụng ở trạng thái ổn định: nếu bạn quay lại quá khứ để thực hiện lại một số hành động bạn đã thực hiện, các hành động cũ của bạn sẽ không tồn tại nữa khi bạn thay đổi trong tương lai.

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);
        }
    }
}

Các Undoablegiao diện khá đơn giản:

export interface Undoable {
    /** Undoes the command */
    undo(): void;
    /** Redoes the undone command */
    redo(): void;
}

Bây giờ bạn có thể viết các lệnh hoàn tác hoạt động trên ứng dụng của bạn.

Ví dụ: (vẫn dựa trên các ví dụ Interacto), bạn có thể viết một lệnh như sau:

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 = '';
   }
}

Bây giờ bạn có thể thực thi và thêm lệnh vào thể hiện UndoHistory:

const cmd = new ClearTextCmd(...);
//...
undoHistory.add(cmd);

Cuối cùng, bạn có thể liên kết một nút hoàn tác (hoặc một phím tắt) với lịch sử này (điều tương tự đối với làm lại).

Các ví dụ như vậy được trình bày chi tiết trên trang tài liệu Interacto .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.