powered by simpleCommunicator - 2.0.61     © 2026 Programmizd 02
Целевая тема:
Создать новую тему:
Автор:
Закрыть
Цитировать
Форумы / Delphi [игнор отключен] [закрыт для гостей] / Логика Undo/Redo
15 сообщений из 15, страница 1 из 1
Логика Undo/Redo
    #39606267
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Не совсем вопрос по Delphi, скорее общий вопрос проектирования, но вот интересно мнение тех, кто уже либо делал или совершенно точно знает, как сделать это правильно :-)

Само по себе Undo/Redo для абстрактоного набора редактируемых классов с современным-то RTTI реализовать не представляется уж очень большой проблемой. Вопрос возникает о том, как "помечать" объекты для отката их вставки/удаления?

Вижу пока два способа и оба неправильные:

Генерировать текстовый "путь" до объекта изменения. Примерно как Parent.GetPathToMe(Self). И по путю к нему можно легко будет найти изменённый объект. Что-нибудь вроде: "Child[4].Properties.Setting1". Минус такого подхода вижу в возне со строками: на каждый чих генерировать такую ерунду. Не то чтобы это должно что-то замедлить или съесть много памяти, но всё равно "программист" во мне говорит, что это неправильно.


Генерировать целочисленные ID (или даже GUID) для объектов и хранить карту объектов (TDictionary). Вроде как проблему мгновенного нахождения обьекта решает, но возникает некоторое количество других вопросов: Нужно централизовано генерить ID, если ID это не GUID, нужно держать карту объектов обновлённой. Т.е. если что-то восстановил через Undo, то нужно регистрировать заново все восстановленные объекты в карте. Тоже самое относится к вставке из буфера обмена - нужно генерировать новые ID и регистрировать вставленные объекты.

Какие есть ещё варианты?
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606279
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dimonka,

журналирование (это вы почти описали в п. 1)

персистентные структуры данных (это то, что вы пытаетесь сформулировать в п.2)

первый способ проще
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606284
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)Dimonka,

журналирование (это вы почти описали в п. 1)

персистентные структуры данных (это то, что вы пытаетесь сформулировать в п.2)

первый способ проще

А можно ответ как-то развернуть или хотя бы ссылкой-другой броситься?
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606302
Гаджимурадов Рустам
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dimonka> А можно ответ как-то развернуть или хотя бы ссылкой-другой броситься?

Я думаю, нужно четче и подробнее изложить задачу.
Потому что если вопрос просто в том, как хранить
ссылки/идентификаторы объектов и навигация по
ним - это вопрос очень простой и известный.
Posted via ActualForum NNTP Server 1.5
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606309
Фотография JayDi
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Отличные статьи от российского onlyoffice:
https://habrahabr.ru/company/teamlab/blog/169841/
https://habrahabr.ru/company/teamlab/blog/327454/

Статьи от DevExpress про реализацию undo/redo:
https://habrahabr.ru/company/devexpress/blog/104163/
https://habrahabr.ru/company/devexpress/blog/104168/
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606337
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Гаджимурадов РустамЯ думаю, нужно четче и подробнее изложить задачу.
Потому что если вопрос просто в том, как хранить
ссылки/идентификаторы объектов и навигация по
ним - это вопрос очень простой и известный.
Ну вот захотелось узнать это простое - ткните носом, а то пока по ссылкам ни тема идентификаторов и тема навигации совсем не раскрыта.
Про навигацию я тоже совсем забыл.. Пользователь-то должен видеть, что откатывается назад.
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606341
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dimonka,

журналирование надеюсь понятно, выше ссылки дали как оно может реализовываться

с персистентными структурами если на пальцах: в любой момент можно сделать полноценный клон объекта.

Например , есть словарь (используется Path coping)
Код: pascal
1.
TMapAVL2=specialize PrsOrdMapAVL <TKey,integer,TCmp>;



для реализации UNDO\REDO достаточно сохранить клон объекта и при необходимости заменить текущий на сохранённый вариант
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606347
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)журналирование надеюсь понятно, выше ссылки дали как оно может реализовываться По ссылкам выше разбираются разные вопросы, но я в упор не увидел, где разбирается вопрос "локализации" объекта в структуре.

Тупой пример: Есть TObjectList_1 у него десятый элемент TObjectList_2, а у того есть четвёртый элемент TObjectList_3.

Как сохранить ссылку на откат, если я удалю TObjectList_3, а потом ещё TObjectList_2, а заодно и TObjectList_1?

kealon(Ruslan)для реализации UNDO\REDO достаточно сохранить клон объекта и при необходимости заменить текущий на сохранённый вариант Это неинтересный вариант.
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606352
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dimonka,
>>Как сохранить ссылку на откат, если я удалю TObjectList_3, а потом ещё TObjectList_2, а заодно и TObjectList_1?
написать для каждого такого действия ручками код UNDO\REDO
>>Это неинтересный вариант.
почему?
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606354
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)Dimonka,
>>Как сохранить ссылку на откат, если я удалю TObjectList_3, а потом ещё TObjectList_2, а заодно и TObjectList_1?
написать для каждого такого действия ручками код UNDO\REDO Покажи пример этого кода, хотя бы псевдокодом. Опиши хоть в общих словах какие действия этих специализированные UNDO/REDO "ручками" должны выполнять?

kealon(Ruslan)>>Это неинтересный вариант.
почему? Потому что дублировать сотни раз одни и те же не изменяющиеся состояния бессмысленно.
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606355
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dimonka,

а зачем их дублировать?
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606356
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
kealon(Ruslan)Dimonka,

а зачем их дублировать?

Объясни тогда логику это метода, может быть я что-то не понял. На примере с тремя TObjectList.
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606357
Фотография makhaon
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Dimonka,

Можешь посмотреть реализацию многоуровневого Undo/Redo:
https://github.com/Makhaon/TCoolMemo
Код из старой версии нашего ПО.
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606358
kealon(Ruslan)
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
DimonkaОбъясни тогда логику это метода, может быть я что-то не понял. На примере с тремя TObjectList.
TObjectList не относится к персистентным структурам

в том и сложность, нужно что бы всё текущее состояние документа было сохранено в 1-й персистентной структуре, тогда ты её можешь спокойно клонировать
...
Рейтинг: 0 / 0
Логика Undo/Redo
    #39606539
Фотография Dimonka
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Выдавил из себя пока нечто такое:

TUndoObjectPathRec(ObjType: integer; ObjIndex: integer;) будет достаточно, чтобы идентифицировать положение объекта относительно родительского объекта.
Конечное нахождение объекта делать из цепочки TUndoObjectPathRec - TUndoObjectPath. Красивее и универсальнее способа пока не придумал..

Код: pascal
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
329.
330.
331.
332.
333.
334.
335.
336.
337.
338.
339.
340.
341.
342.
343.
344.
345.
346.
347.
348.
349.
350.
351.
352.
353.
354.
355.
356.
357.
358.
359.
360.
361.
362.
363.
364.
365.
366.
367.
unit DUNDO.Manager;

interface
uses
  generics.collections, sysutils;

const
  IID_IUndoableObject: TGUID = '{9FA74FBE-5558-4D2F-A736-6194EA68C2BC}';
type
  TUndoObjectPathRec = record
    ObjType: integer;
    ObjIndex: integer;
    constructor New(OType: integer; OIndex: integer);
  end;

  TUndoObjectPath = TList<TUndoObjectPathRec>;

  IUndoableObject = interface
    ['{9FA74FBE-5558-4D2F-A736-6194EA68C2BC}']
     function Insert(Obj: TObject; Path: TUndoObjectPath): boolean;
     function Delete(Path: TUndoObjectPathRec): boolean;
     function Move(OldPath, NewPath: TUndoObjectPathRec): boolean;
     function Clone: IUndoableObject;
     function GetPath(Obj: TObject): TUndoObjectPathRec;
     function GetParent: IUndoableObject;
     function GetSelf: TObject;
  end;

  IUndoableHost = interface
     function GetObject(Path: TUndoObjectPath): TObject;
     procedure AddProperty(Obj: IUndoableObject; const PropName: string; Value: Variant);
     procedure AddObjDelete(ParentObj: IUndoableObject; DeletedObj: IUndoableObject);
     procedure AddObjInsert(ParentObj: IUndoableObject; NewLocation: TUndoObjectPathRec);
     procedure AddObjReorder(ParentObj: IUndoableObject; OldLocation, NewLocation: TUndoObjectPathRec);
  end;

  TUndoElement = class
  type
    TUndoChangeType = (uctUnknown, uctPropertyChange, uctAdd, uctDelete, uctMove);
  private
    FPath: TUndoObjectPath;
  protected
    class function GetChangeType: TUndoChangeType; virtual;
    procedure CreatePath(Obj: IUndoableObject);
  public
    constructor Create(Obj: IUndoableObject); virtual;
    destructor Destroy; override;
    function Process(AnObject: TObject): boolean; virtual; abstract;
    property ChangeType: TUndoChangeType read GetChangeType;
    property Path: TUndoObjectPath read FPath;
  end;

  TUndoProperty = class(TUndoElement)
  private
    FPropName: string;
    FValue: Variant;
  protected
    class function GetChangeType: TUndoElement.TUndoChangeType; override;
  public
    function Process(AnObject: TObject): boolean; override;
    property PropName: string read FPropName write FPropName;
    property Value: Variant read FValue write FValue;
  end;

  TUndoAddObject = class(TUndoElement)
  protected
    FLocation: TUndoObjectPathRec;
    class function GetChangeType: TUndoElement.TUndoChangeType; override;
  public
    function Process(AnObject: TObject): boolean; override;
    property Location: TUndoObjectPathRec read FLocation write FLocation;
  end;

  TUndoMoveObject = class(TUndoAddObject)
  private
    FOldLocation: TUndoObjectPathRec;
  protected
    class function GetChangeType: TUndoElement.TUndoChangeType; override;
  public
    function Process(AnObject: TObject): boolean; override;
    property OldLocation: TUndoObjectPathRec read FOldLocation write FOldLocation;
  end;

  TUndoDeleteObject = class(TUndoAddObject)
  private
    FObj: TObject;
    procedure SetObj(const Value: TObject);
  protected
    class function GetChangeType: TUndoElement.TUndoChangeType; override;
  public
    destructor Destroy; override;
    function Process(AnObject: TObject): boolean; override;
    property Obj: TObject read FObj write SetObj;
  end;

  TUndoStack = TObjectStack<TUndoElement>;

  TUndoManager = class
  type
    TCurrentState = (csDoing, csUndoing);
  private
    FUndoStack: TUndoStack;
    FRedoStack: TUndoStack;
    FCurrentState: TCurrentState;
    FHostObject: IUndoableHost;
    function GetCanUndo: boolean;
    function GetRedo: boolean;
    procedure PushElement(UndoElement: TUndoElement);
  public
    constructor Create(Host: IUndoableHost); virtual;
    destructor Destroy; override;
    procedure AddProperty(Obj: IUndoableObject; const PropName: string; Value: Variant);
    procedure AddObjDelete(ParentObj: IUndoableObject; DeletedObj: IUndoableObject);
    procedure AddObjInsert(ParentObj: IUndoableObject; NewLocation: TUndoObjectPathRec);
    procedure AddObjReorder(ParentObj: IUndoableObject; OldLocation, NewLocation: TUndoObjectPathRec);
    function DoUndo: boolean;
    function DoRedo: boolean;
    procedure Clear;
    property CanUndo: boolean read GetCanUndo;
    property CanRedo: boolean read GetRedo;
    property HostObject: IUndoableHost read FHostObject;
  end;

implementation

uses DUNDO.RTTI;

{ TUndoElement }

constructor TUndoElement.Create(Obj: IUndoableObject);
begin
  FPath := TUndoObjectPath.Create;
  CreatePath(Obj);
end;

procedure TUndoElement.CreatePath(Obj: IUndoableObject);
var
  Parent: IUndoableObject;
begin
  Parent := Obj.GetParent;
  while Parent <> nil do
  begin
    FPath.Add(Parent.GetPath(Obj.GetSelf));
    Obj := Parent;
    Parent := Obj.GetParent;
  end;
end;

destructor TUndoElement.Destroy;
begin
  FreeAndNil(FPath);
  inherited;
end;

class function TUndoElement.GetChangeType: TUndoChangeType;
begin
  result := uctUnknown;
end;

{ TUndoProperty }

class function TUndoProperty.GetChangeType: TUndoElement.TUndoChangeType;
begin
  result := uctPropertyChange;
end;

function TUndoProperty.Process(AnObject: TObject): boolean;
begin
  result := SetObjectPropertyValue(AnObject, PropName, Value);
end;

{ TUndoManager }

procedure TUndoManager.AddObjDelete(ParentObj, DeletedObj: IUndoableObject);
var
  UndoElement: TUndoDeleteObject;
begin
  UndoElement := TUndoDeleteObject.Create(ParentObj);
  UndoElement.Obj := DeletedObj.GetSelf;
  PushElement(UndoElement);
end;

procedure TUndoManager.AddObjInsert(ParentObj: IUndoableObject;
  NewLocation: TUndoObjectPathRec);
var
  UndoElement: TUndoAddObject;
begin
  UndoElement := TUndoAddObject.Create(ParentObj);
  UndoElement.Location := NewLocation;
  PushElement(UndoElement);
end;

procedure TUndoManager.AddObjReorder(ParentObj: IUndoableObject; OldLocation,
  NewLocation: TUndoObjectPathRec);
var
  UndoElement: TUndoMoveObject;
begin
  UndoElement := TUndoMoveObject.Create(ParentObj);
  UndoElement.Location := NewLocation;
  UndoElement.OldLocation := OldLocation;
  PushElement(UndoElement);
end;

procedure TUndoManager.AddProperty(Obj: IUndoableObject;
  const PropName: string; Value: Variant);
var
  UndoElement: TUndoProperty;
begin
  UndoElement := TUndoProperty.Create(Obj);
  UndoElement.PropName := PropName;
  UndoElement.Value := Value;
  PushElement(UndoElement);
end;

procedure TUndoManager.Clear;
begin
  FUndoStack.Clear;
  FRedoStack.Clear;
end;

procedure TUndoManager.PushElement(UndoElement: TUndoElement);
begin
  case FCurrentState of
    csDoing:
      FUndoStack.Push(UndoElement);
    csUndoing:
      FRedoStack.Push(UndoElement);
  end;
end;

constructor TUndoManager.Create(Host: IUndoableHost);
begin
  FHostObject := Host;
  FUndoStack := TUndoStack.Create();
  FRedoStack := TUndoStack.Create();
end;

destructor TUndoManager.Destroy;
begin
  FreeAndNil(FUndoStack);
  FreeAndNil(FRedoStack);
  inherited;
end;

function TUndoManager.DoRedo: boolean;
var
  UndoElement: TUndoElement;
  Obj: TObject;
begin
  UndoElement := FRedoStack.Extract;
  try
    Obj := FHostObject.GetObject(UndoElement.Path);
    result := UndoElement.Process(Obj);
  finally
    UndoElement.Free;
  end;
end;

function TUndoManager.DoUndo: boolean;
var
  UndoElement: TUndoElement;
  Obj: TObject;
begin
  FCurrentState := csUndoing;
  try
    UndoElement := FUndoStack.Extract;
    try
      Obj := FHostObject.GetObject(UndoElement.Path);
      result := UndoElement.Process(Obj);
    finally
      UndoElement.Free;
    end;
  finally
    FCurrentState := csDoing;
  end;
end;

function TUndoManager.GetCanUndo: boolean;
begin
  result := FUndoStack.Count > 0;
end;

function TUndoManager.GetRedo: boolean;
begin
  result := FRedoStack.Count > 0;
end;

{ TUndoObject }

destructor TUndoDeleteObject.Destroy;
begin
  if FObj <> nil then
    FreeAndNil(FObj);
  inherited;
end;

class function TUndoDeleteObject.GetChangeType: TUndoElement.TUndoChangeType;
begin
  result := uctDelete;
end;

function TUndoDeleteObject.Process(AnObject: TObject): boolean;
var
  OI: IUndoableObject;
begin
  result := false;
  if AnObject.GetInterface(IID_IUndoableObject, OI) then
    result := OI.Insert(Obj, FPath);
end;


procedure TUndoDeleteObject.SetObj(const Value: TObject);
var
  OI: IUndoableObject;
  Parent: IUndoableObject;
begin
  FObj := Value;
  if Value.GetInterface(IID_IUndoableObject, OI) then
  begin
    Parent := OI.GetParent;
    if Parent <> nil then
      FLocation := Parent.GetPath(Value);
    FObj := Parent.Clone.GetSelf;
  end;
end;

{ TUndoInsertObject }

class function TUndoAddObject.GetChangeType: TUndoElement.TUndoChangeType;
begin
  result := uctAdd;
end;

function TUndoAddObject.Process(AnObject: TObject): boolean;
var
  OI: IUndoableObject;
begin
  result := false;
  if AnObject.GetInterface(IID_IUndoableObject, OI) then
    result := OI.Delete(FLocation);
end;

{ TUndoObjectPathRec }

constructor TUndoObjectPathRec.New(OType, OIndex: integer);
begin
  ObjType := OType;
  ObjIndex := OIndex;
end;

{ TUndoReorderObject }

class function TUndoMoveObject.GetChangeType: TUndoElement.TUndoChangeType;
begin
  result := uctMove;
end;

function TUndoMoveObject.Process(AnObject: TObject): boolean;
var
  OI: IUndoableObject;
begin
  result := false;
  if AnObject.GetInterface(IID_IUndoableObject, OI) then
    result := OI.Move(FLocation, FOldLocation);
end;

end.



Из минусов - куча интерфайсных методов, ещё нужно учитывать, что TInterfacedObject может неожиданно самоосвободиться, если ему не отключить подсчёт RefCount.

Покритикуйте, если не лень.
...
Рейтинг: 0 / 0
15 сообщений из 15, страница 1 из 1
Форумы / Delphi [игнор отключен] [закрыт для гостей] / Логика Undo/Redo
Найденые пользователи ...
Разблокировать пользователей ...
Читали форум (0):
Пользователи онлайн (0):
x
x
Закрыть


Просмотр
0 / 0
Close
Debug Console [Select Text]