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.
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.
Câu trả lời:
Tôi biết về hai bộ phận chính của các loại hoàn tác
Đố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 đó.
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 UndoData
mả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 UndoLevel
từng cái một. Theo mặc định, UndoLevel
bằng chỉ số của thành viên cuối cùng của UndoData
mả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 UndoLevel
từ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 UndoLevel
không bằng độ dài (trừ đi một) của UndoData
mả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 UndoDataItem
cấ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.
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 .
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:
delete
tiếp theo là mộtinsert
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 delete
bạ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. Redo
di 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ó.
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.
Nếu các hành động có thể đảo ngược. Ví dụ: Thêm 1, làm cho người chơi di chuyển, v.v. hãy xem cách sử dụng Mẫu lệnh để thực hiện hoàn tác / làm lại . Theo liên kết, bạn sẽ tìm thấy một ví dụ chi tiết về cách làm điều đó.
Nếu không, hãy sử dụng Trạng thái đã lưu như được giải thích bởi @Lazer.
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ứ.
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 .
Thêm vào cuộc thảo luận, tôi đã viết một bài đăng trên blog về cách thực hiện UNDO và REDO dựa trên suy nghĩ về những gì trực quan: http://adamkulidjian.com/undo-and-redo.html
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:
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 Undoable
giao 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 .