Статья
Версия для печати
Обсудить на форуме
DO-UNDO (откат)


(Издание второе :) )
Кто не знает, как работает CTRL+Z / CTRL+Y ? Жить без этих сочетаний клавиш довольно неудобно. Лично у меня, если случается работать с программой, в которой нет этих функций, возникает ностальгия по ним (а ещё не хватает их в жизни ;) , но это из области фантастики).
В этой статье я хочу предложить класс CDoUndo2, призванный помогать выполнять откат (а точнее - откат и возврат то есть операции UNDO и DO). Класс позволяет сохранять N предыдущих действий "X", а также прошагать назад-вперёд по цепочке этих действий.
Класс  и пример работы с ним написаны в среде MS Visual C++  с использованием MFC, однако сам класс к этому всему не привязан, его можно использовать везде, где применяется язык C++.  Файлы реализации и пример работы можно найти по ссылке в конце статьи.

Система стеков DO/UNDO работает так (X - текущее состояние среды , (N) - количество элементов в стеке DO или UNDO):

   Действие      UNDO(N)                        DO(N)   
   Начало работы      0            X            0   
   Новое действие      1      <-      X            0   
   Новое действие      2      <-      X            0   
   Откат      1      ->      X      ->      1   
   Возврат      2      <-      X            0   

Сохраняемые объекты могут быть любыми. А кроме того, в стек могут сохраняться разнотипные объекты (например, в многодокументном приложении - изменение текста одного окна и изменение графики другого). Вот два типовых применений DO/UNDO (но наверняка их больше) :

1. Сохранение состояния рабочей среды (или её части). Перед изменением среды текущее состояние заносится в стек UNDO, затем производятся изменения. Сохранение и восстановление состояние - это просто копирование переменных.

2. Сохранение произведённых действий. Это применение программист должен моделировать сам, так как только он знает, как действия изменяют рабочую среду. Каждое произведённое действие в некотором виде сохраняется в стек. Если действие обратимо (изменили координаты объекта), то достаточно сохранить только описание действия. Если действие необратимо (удаление), то кроме описания действия придётся сохранить часть данных, необходимую для восстановления состояния.


Во время работы программы со стеками DO / UNDO обычно возможно совершить 3 действия:

1. Совершается некое новое изменение среды

При этом состояние среды изменилось с Xprev (предыдущее состояние) на Xcurr (текущее состояние). Состояние Xprev сохраняется в стек UNDO.
 Тут программист сам волен выбирать, как это сделать:
1) либо отслеживать действия пользователя, изменяющие среду, и сохранять состояние_среды_до_изменения в стек, а затем вносить изменения в текущее состояние. Пример: пользователь дал команду переместить объекет на 5 пикселей вправо. Сохраняем текущее положение объекта, затем сдвигаем объект.
2) либо где то помнить предыдущее состояние и сохранять его уже после изменения текущего состояния среды. Так сделано в примере проекта, приложенного к статье - всегда помним предыдущее состояние окна с текстом в переменной. Когда окошко сообщает о том, что его текст поменяли, сохраняем предыдущее состояние из переменной в стек, а затем обновляем содержимое переменным новым значением из окна.

Обратите внимание, что при совершении нового действия всегда автоматически очищается стек DO, так как если бы этого не происходило, то очередной возврат нарушил бы логику совместной работы стеков DO/UNDO.

2. Совершается операция UNDO

Если в стеке UNDO есть элементы, то: текущее состояние среды Xcurr помещаем в стек DO. Затем извлекаем из UNDO последний помещённый элемент и используем этот элемент, чтобы восстановить состояние среды (или её часть), которое элемент хранит.

3. Совершается операция DO

Если в стеке DO есть элементы, то: текущее состояние среды Xcurr помещаем в стек UNDO. Затем извлекаем из DO последний помещённый элемент и используем этот элемент, чтобы восстановить состояние среды (или её часть), которое элемент хранит.



Организация класса CDoUndo2


Класс работает совместно со структурой sDoUndo2_item.
Код:
//родитель для элементов, которые можно сохранять в стек DO/UNDO
//(нужно произвести свои элементы от этой структуры)
struct sDoUndo2_item
{
//скопировать данные из *pCopyFromITEM в *this
//(используется в CDoUndo2::s_stack)
virtual void sDU2i_CopyToThisFrom(const sDoUndo2_item* pCopyFromITEM_in)=0;

//используется для вызова new производной структуры
//(в CDoUndo2::s_stack используется в качестве new для производной структуры)
virtual sDoUndo2_item* sDU2i_NewObject() const =0;

//используется для вызова delete производной структуры
//(в CDoUndo2::s_stack используется в качестве delete для производной структуры)
virtual void sDU2i_DeleteThis() const =0;
};
От sDoUndo2_item должны быть произведены все объекты, которые предполагается хранить в стеках DO/UNDO. Структура содержит только чистые виртуальные методы, то есть в потомке их надо обязательно переопределить.
Типичное определение потомка от этой структуры:
Код:
struct sChildItem:public sDoUndo2_item
{
//хранимые данные
<type> m_var1;
<type> m_var2;
...
...

//виртуальные
virtual void sDU2i_CopyToThisFrom(const sDoUndo2_item* pCopyFromITEM_in)=0;
{
//копируем
*this=*(sChildItem*)pCopyFromITEM_in;

//(ИЛИ делаем почленное копирование)
//m_var1=pCopyFromITEM_in->m_var1;
//m_var2=pCopyFromITEM_in->m_var2;
//...
//...
}

virtual sDoUndo2_item* sDU2i_NewObject() const
{
return new sChildItem;
}

virtual void sDU2i_DeleteThis() const
{
delete this;
}
}}

То есть виртуальные методы структуры введены потому, что sDoUndo2_item понятия не имеет, как должен инициализироваться потомок при создании из кучи (да и какого размера он будет), как будет удалять память в кучу и как будет производить копирование из одного экземпляра типа в другой.
Таким образом, класс CDoUndo2 отвязывается от работы с определённым пользователем типом и работает только с указателем на тип sDoUndo2_item, что делает класс универсальным - можно хранить любые данные в одном и том же стеке.
В классе имеется описание структуры стека CDoUndo2::s_stack , который есть обычный FILO стек. Эта структура используется для объявления рабочих объектов класса  CDoUndo2:
Код:
s_stack m_UndoStack; //стек UNDO
s_stack m_DoStack; //стек DO

Сам класс CDoUndo2 занимается управлением совместной работой этих объектов, в конечном счёте реализуя систему DO/UNDO.

Интерфейс CDoUndo2 очень простой:

Код:
enum
{
e_minStSize = 10, //минимальный размер стека
e_maxStSize = 100, //максимальный размер стека
};

bool SetStacksSizez(int UNDO_size,int DO_size);
- задать глубину стеков. Предыдущее содержимое стеков удаляется.

Код:
int GetDoFilledNum();
int GetUndoFilledNum();
- получить текущее количество элементов в стеке DO или UNDO

Код:
void SaveItemBeforeNewAction(const sDoUndo2_item* const pITEM_in);
bool OperationDo(sDoUndo2_item* const pITEM_in_out);
bool OperationUndo(sDoUndo2_item* const pITEM_in_out);
- выполнение одного из трёх действий, описанных выше: записать состояние среды до изменения , выполнить DO, выполнить UNDO. Во всех трёх методах передаётся указатель на существующий экземпляр типа, произведённого от sDoUndo2_item.



Замечания по примеру работы класса CDoUndo2


В примере (смотрите файлы по ссылке в конце статьи, файлы класса в папке DoUndo2_files папки проекта) используется диалог с расположенном на нём текстовом окне и двум кнопкам - откат и возврат. На кнопках пишется количество элементов в стеке. Сохраняется каждое изменение текстового окна - ввод символа, удаление символа или выделенной группы символов, вставка из буфера обмена. Поскольку пример довольно упрощённый (по идее, надо бы произвести от CEdit дитё и в нём некоторые манипуляции :) ) , то окно с текстом ведёт себя несколько не так, как хотелось бы в идеале (могут возникнуть вопросы, поэтому я на это и обращаю ваше внимание).

Примеры косяков:

1) Уже описанные выше манипуляции с сохранением текущего состояния в промежуточных переменных, так как можем узнать о новом действии только после изменения содержимого окна - когда CEdit пришлёт сообщение EN_CHANGE. Храним текущий текст и положение курсора в переменных
Код:
CString CDoundoDlg::m_CurrentTextState;
DWORD CDoundoDlg::m_dwdCurrentCursorPos;

2) если выделить часть текста , затем вставить из буфера на это место другой текст, то сохранятся два действия вместо одного: сначала контрол удаляет выделенную часть и присылает сообщение EN_CHANGE (лишнее). Затем вставляет из буфера и снова шлёт EN_CHANGE (уже нужное). Это можно увидеть по тому, что цифра на кнопке отката увеличится на 2.


Файлы класса и примера можно взять здесь

C Уважением, Алексей Журавлёв

Автор: Алексей1153
Версия для печати
Обсудить на форуме