- PVSM.RU - https://www.pvsm.ru -
Привет! В связи со своей реальной задачей проанализировать возможности Qt и .NET для реализации так называемых «Назад» (Undo) и «Вперёд» (Redo), цель которых отменить действие и отменить отмену соответственно, я решил все свои мысли, идеи и задумки развернуть в этой статье, даже если они будут частично или совсем неверными (поэтому по возможности и интересу пишите в комментарии свои замечания). Хоть и на просторах Интернета спокойно можно найти хорошие (и не очень) библиотеки и примеры реализаций, более общего представления на эти вещи я нашёл не так скоро, да и то, только в ответе на StackOverflow [1], а этого было мне не достаточно. Во всём найденном есть моменты, которые меня порадовали, есть и которые огорчили. Пожалуй, стоит отменить все печали и радости… чтобы к ним снова вернуться… «Назад… в будущее»!
Интересно? Добро пожаловать!
Красная или синяя? Примерно к такому вопросу нужно будет прийти, после того, как решили реализовать в приложении Undo/Redo. Объясняю: есть два основных способа реализовать пошаговую отмену, для которых я присвоил следующие наименования: operation-oriented и value-oriented. Первый способ основан на создании операций (или транзакций), у которых есть два метода — сделать и вернуть всё как было. Второй способ не хранит никаких операций — он лишь записывает значения, которые изменились в определённый момент времени. И у первого и у второго способа есть свои плюсы и минусы.
UPD: Чтобы в дальнейшем было меньше вопросов, напомню, что Undo/Redo предназначено больше для хранения информации предыдущих вариантов документа (к примеру) во время редактирования. Записывать данные в БД или на диск будет долго, и это уже мало относится к цели Undo/Redo. Впрочем, если сильно надо — делайте, но лучше не стоит.
Реализуется на основе паттерна «Команда» (Command).
Этот метод заключается в том, чтобы хранить операции в специальном стеке. У стека есть позиция (можно сказать, итератор), которая указывает на последнюю операцию. При добавлении операции в стек — она выполниться (redo), позиция инкрементируется. Для отмены операции стек вызывает команду undo из последней операции, а потом сдвигает позицию последней операции ниже (сдвигает, но не удаляет). Если понадобится вернуть действие — сдвиг выше, выполнение redo. Если после отмены добавляется новая операция, то есть два решения: либо заменять операции выше позиции новыми (и тогда вернуться к прежним будет невозможно), либо начинать новую «ветку» в стеке, но отсюда возникает вопрос — к какой ветке потом идти? Впрочем, ответ на этот вопрос уже искать нужно не мне, так как это зависит от требований к программе.
И так, для самого просто Undo/Redo нам нужно: базовый класс (интерфейс) с чисто виртуальными (абстрактными) функциями undo() и redo(), также класс, который будет хранить указатели на объекты, произведённые от базового класса и, конечно же, сами классы, в которых будут переопределены функции undo() и redo(). Также можно (в некоторых случаях даже очень нужно) будет сделать функции совмещения операций в одну, для того, чтобы, допустим, отменять не каждую букву по отдельности, а слова и предложения, когда буквы станут таковыми, и тому подобное. Поэтому также желательно для каждой операции присваивать определённый тип, при различии которых нельзя будет склеить операции.
И так, плюсы:
Минусы:
Также можно прочитать вот эту статью на Wiki про паттерн команд (Command) [2], который и используется для реализации такого способа Undo/Redo, а также эту статью [3] на Хабрахабре.
Реализуется на основе паттерна «Хранитель» (Memento).
Принцип метода — знать о всех возможных переменных, которые могут измениться, и в начале возможных изменений поставить стэк «на запись», а в конце — сделать коммит изменений.
Тем не менее, записываться должны все изменения. Если записывается только изменения, произведённые пользователем, но не записывались изменения зависимостей — то тогда при отмене/возврате зависимости останутся без изменений. Конечно, можно хитрым способом каждый раз вызывать пересчёт зависимостей, но это уже больше похоже на первый способ и удобнее тогда будет он. О способах реализации будет рассказано ниже, а пока посмотрим на достоинства и недостатки.
Плюсы:
Минусы:
Также можно прочитать вот эту статью на Wiki про паттерн Хранителя (Memento) [4].
Если что и говорить о требовательности к памяти, то этот метод будет есть очень много. Представьте ситуацию, когда при наборе лишь одного символа сохранялся весь документ. И так каждый раз. Представили? А теперь забудьте об этом методе и более не вспоминайте, ибо это уже не Undo/Redo, а бэкапы.
UPD: И нет, здесь я не имел в виду паттерн Memento, который также может сохранять кроме частичного ещё полный снимок изменений/значений. Имеется в виду, что не желательно сохранять снимок всего документа, когда изменилось лишь пару значений. Если всё-таки этого не избежать, то это скорее vl-or, а в некоторых ситуациях, когда очень редко и по сложной схеме изменяется весь документ, вы можете отказаться от записи таких изменений (сказать пользователю, что откат изменений после этой операции будет недоступен).
Здесь разработчики на славу постарались. С помощью Qt можно легко и просто реализовать Undo/Redo. Записывайте рецепт. Нам понадобиться: QUndoStack [5], QUndoCommand [6], а также QUndoView [7] и QUndoGroup [8] по вкусу. Сначала от QUndoCommand наследуем собственные классы, в которых должны быть переопределены undo() и redo(), также желательно переопределить id() для определения типа операции, чтобы потом в переопределённой mergeWith(const QUndoCommand *command) можно было проверить обе операции на совместимость. После этого создаём объект класса QUndoStack, и помещаем в него все новые операции. Для удобства, можно взять QAction *undo [9] и QAction *redo [10] из функций стека, которые потом можно добавить в меню, или прикрепить к кнопке. А если нужно использовать несколько стеков, тогда в этом поможет QUndoGroup, если нужно отобразить список операций: QUndoView.
Также, в QUndoStack можно отмечать clear state (чистое состояние), которые, например, может означать сохранён ли документ на диск и т.д. Вполне удобная реализация op-or undo/redo.
Я реализовал самый простой пример на Qt.
Упс… Qt такого варианта не предоставил. Даже поиск по ключевым словам «Qt memento» не дал ничего. Ну и ладно, там и такого вполне достаточно, а если не достаточно, можно воспользоваться Native'ными методами.
Так как в Qt не посчитали нужным добавить value-oriented Undo/Redo, поэтому нужно будет искать либо готовые реализации (где можно встретить магическое для меня слово «Memento»), либо реализовывать придётся самим. В основном всё реализуется на основе шаблонов. Всё это можно без проблем найти. Я, например, нашёл вот этот проект [12] на GitHub. Тут реализованы сразу две идеи, можете взять и посмотреть, потестировать.
Для меня C# и .NET пока что тёмные леса далёкой Сибири, но тем не менее, он мне очень и очень нужен. Поэтому стоит рассказать хотя бы о том, что мне удалось нагуглить.
Самыми хорошими примерами для меня были:
Вскоре нашлась и такая вот старая статья [16].
Быть может, что-то сможете найти и вы, а возможно на основе этого взять и написать свой велосипед гениальный код. Дерзайте.
Вообще, для такого рода задач в .NET есть интерфейс IEditableObject [17], но придётся много чего реализовывать с нуля, хотя пример реализации есть прямо на MSDN. Тем не менее, мне очень понравилась библиотека DejaVu [18], ради которой даже написана целая статья [19] на Хабрахабре. Читайте, влюбляйтесь, пишите.
Есть ещё два примера, но они мне совсем не понравились:
И так, что нужно знать, чтобы выбрать между двумя методами реализации только одну? Во-первых, реализацию вашего проекта, написан ли (будет?) он на основе команд, или просто изменение множества значений (если ни то, ни другое — думаю, лучше переписывать проект). Во-вторых, требования к памяти и производительности, ибо возможно именно из-за них придётся отказаться от одного варианта в пользу другого. В-третьих, нужно точно знать, что должно сохранятся и как, а что не должно вообще. Вот, в принципе и всё.
Удачи в будущем!
Автор: faserg1
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/163099
Ссылки в тексте:
[1] StackOverflow: http://stackoverflow.com/questions/2746076/how-do-i-create-undo-in-c
[2] вот эту статью на Wiki про паттерн команд (Command): https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)
[3] эту статью: https://habrahabr.ru/post/114455/
[4] вот эту статью на Wiki про паттерн Хранителя (Memento): https://ru.wikipedia.org/wiki/%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)
[5] QUndoStack: http://doc.qt.io/qt-5/qundostack.html
[6] QUndoCommand: http://doc.qt.io/qt-5/qundocommand.html
[7] QUndoView: http://doc.qt.io/qt-5/qundoview.html
[8] QUndoGroup: http://doc.qt.io/qt-5/qundogroup.html
[9] undo: http://doc.qt.io/qt-5/qundostack.html#createUndoAction
[10] redo: http://doc.qt.io/qt-5/qundostack.html#createRedoAction
[11] исходники: https://www.dropbox.com/s/eyghspb1woesezj/UndoRedoSimple.zip?dl=0
[12] этот проект: https://github.com/d-led/undoredo-cpp
[13] статья: https://habrahabr.ru/company/devexpress/blog/104163/
[14] пост: http://www.dofactory.com/net/command-design-pattern
[15] пример: https://www.cambiaresearch.com/articles/82/generic-undoredo-stack-in-csharp
[16] старая статья: http://web.archive.org/web/20140630134645/http://rsdn.ru/article/dotnet/backforward.xml
[17] IEditableObject: https://msdn.microsoft.com/ru-ru/library/system.componentmodel.ieditableobject(v=vs.110).aspx
[18] DejaVu: https://dejavu.codeplex.com/
[19] статья: https://habrahabr.ru/post/80174/
[20] Пример 1: http://www.codeproject.com/Articles/10576/An-Undo-Redo-Buffer-Framework
[21] Пример 2: http://www.codeproject.com/Articles/18025/Generic-Memento-Pattern-for-Undo-Redo-in-C
[22] Источник: https://habrahabr.ru/post/306398/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.