16+
ComputerPrice
НА ГЛАВНУЮ СТАТЬИ НОВОСТИ О НАС




Яндекс цитирования


Версия для печати

Модуль поиска не установлен.

Реализация операций "Отменить" и "Повторить" c помощью STL на примере простейшего графического редактора

14.01.2005

Александр Теут

Наша книга, по сути, не ставит перед собой иной задачи, кроме как научить вас летать на этом "стелс" под названием "STL", в то время как всё, с чем вам приходилось иметь дело раньше, - это примитивный "кукурузник".
Отладка в C++.
Крис Х. Паппас и Уильям Х. Мюррей III

Введение

Обязательным элементом пользовательского интерфейса любого графического (и не только) редактора являются операции "Отменить" (Undo) и "Повторить" (Redo). Для их реализации требуется организовать как хранение данных (в нашем случае изображений), так и доступ к ним. Готовое и главное эффективное решение подобной задачи обеспечивает стандартная библиотека шаблонов (STL).

STL была разработана Александром Степановым и Мень Ли, сотрудниками "Хьюлетт-Паккард", как библиотека шаблонов. Она включает в себя огромное количество средств для хранения и обработки данных. Следует заметить, что, хотя применение STL позволяет существенно увеличить производительность труда программиста, но, в то же время, на первоначальном этапе требует от него некоторых (сравнительно небольших!) усилий при освоении базовых понятий, которые отсутствуют в стандартном С++, а именно: контейнер, итератор, алгоритм и некоторых других.

Кратко остановимся >на этих понятиях

Контейнеры отвечают за организацию данных в памяти и реализованы в виде шаблонов классов, что позволяет использовать их для хранения данных различного типа.

Алгоритмы представляют собой шаблоны функций, которые можно применять к контейнерам для обработки их содержимого тем или иным способом. Например, имеются алгоритмы для сортировки, копирования, поиска и т.д. В то же время они не являются элементами классов контейнеров и допускают своё применение к обычным массивам C++.

Итераторы являются обобщёнными указателями, которые ссылаются на элементы контейнера, позволяя тем самым применять к ним различные алгоритмы, иными словами, они соединяют алгоритмы с контейнерами.

В данной статье мы будем использовать контейнер deque (double-ended queue), представляющий собой очередь с двумя концами. Выбор именно этого контейнерного класса, а не, например, vector, основывается, главным образом, на выводах, приведённых в статье, которую можно найти на сайте CodeProject по адресу http://codeproject.com/vcpp/stl/vector_vs_deque.asp

Средой разработки выберем Borland C++ Builder v. 5 (всё применимо и для 6 версии).

Построение формы проекта и объявление переменных

Наш проект будет представлять собой простейшее графическое приложение, которое позволит пользователю рисовать на форме с помощью мышки и выполнять три функции редактирования: "Отменить", "Повторить", "Очистить историю". Вид формы приложения приведён на рисунке.

Как видно из рисунка, на форме размещены следующие компоненты: 4 кнопки типа BitBtn, Panel и PaintBox (рис. 1). Само рисование осуществляется на канве компонента PaintBox1, который, в свою очередь, расположен на компоненте Panel1.

Кнопки BitBtn1 и BitBtn2 выполняют операции "Отменить" и "Повторить" соответственно.

Кнопка BitBtn3 будет реализовывать полную очистку истории изображений.

Щелчок по кнопке BitBtn4 приведёт к закрытию приложения.

Чтобы иметь возможность работать с контейнером типа deque, в cpp - файл формы добавим заголовок:

#include <deque>

В дальнейшем мы собираемся использовать так называемые "умные указатели" (smart pointer), для чего необходимо подключить библиотеку memory:

#include <memory>

И ещё несколько дополнительных приготовлений. В h-файле формы, перед объявлением класса, введём новые типы, исключительно для компактности последующего кода:

typedef Graphics::TBitmap Bitmapes;
typedef std::deque<Bitmapes*>PointerBitmapes;

Очистку ресурсов при закрытии приложения поручим деструктору формы. Для этого в классе TForm1 объявим прототип его деструктора:

__fastcall TForm1:: ~TForm1();

Перейдём к объявлению переменных. Всех их можно расположить в секции private-класса формы.

Непосредственно для организации рисования вводится булевая переменная. Её единственная задача - сигнализировать о нажатии левой кнопки мыши:

bool bMouseDown

Само рисование будет происходить сначала на канве объекта типа Bitmapes:

std::auto_ptr< Bitmapes > ShowBitmap,

а затем перерисовываться на канву компонента PaintBox1 - стандартный приём для уменьшения мерцания изображения при перерисовке.

Объявление переменной ShowBitmap подобным образом позволяет в дальнейшем не заботиться об уничтожении объекта, на который она указывает. Это должно произойти автоматически, а, следовательно, не приведёт к утечке ресурсов при закрытии нашего приложения (правда, начиная с Win 2000, это больше дань хорошему стилю программирования: намусорил - прибери за собой сам! См. книгу Джеффри Рихтера: Windows).

Наконец, осталось объявить две переменные контейнерного типа:

std :: deque < Bitmapes > ptr_undo
std :: deque < Bitmapes > ptr_redo;

В ptr_undo будут содержаться переменные типа Bitmapes, необходимые для реализации операции "Отменить". В свою очередь, эти переменные являются указателями на объекты, представляющие собой отдельные снимки изображений канвы.

Формирование такого снимка начинается с нажатия левой кнопки мышки в области компонента PaintBox1 и заканчивается путём её отпускания.

Аналогичный смысл имеет переменная ptr_redo, но уже в отношении операции "Повторить".

Реализация

Далее переходим в файл реализации (cpp).

Для начала заметим, что, программируя в среде C++ Builder, следует избегать помещения инициализирующей части кода в обработчике события формы OnCreate (в отличие от Delphi), а придерживаться стандартной практики C++, где для подобной цели используется конструктор класса (обоснование этого утверждения можно найти в статье: http://www.bcbdev.com/articles/suggest.htm#oncreate).

Запишем следующий код в конструкторе формы, снабдив его комментариями:

// Динамически создаём объект ShowBitmap в инициализирующей части
// конструктора, кстати, только здесь это допустимо сделать таким образом
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner), ShowBitmap(new Graphics::TBitmap)
{


// Дополнительная мера, чтобы устранить мерцание во время рисования
Panel1 -> DoubleBuffered = true;

// Формирование начального изображения
ShowBitmap->Width=PaintBox1->Width;
ShowBitmap->Height=PaintBox1->Height;
ShowBitmap->Canvas->Brush->Color=Panel1->Color;
ShowBitmap->Canvas->FillRect(Rect (0,0,ShowBitmap->Width,ShowBitmap->Height));

// Размещение его во внутренней памяти и добавление в контейнер
Bitmapes* bitmap=new Bitmapes;
bitmap -> Assign(ShowBitmap.get());
ptr_undo.push_back(bitmap);

// Внимание! Объект bitmap здесь уничтожать нельзя
}

Опишем функции-члены контейнера deque, которые понадобятся нам в дальнейшем.

Чтобы добавить элемент в конец контейнера, применяют функцию:

push_back(переменная)

Обратная операция - удаление последнего элемента из контейнера - производится с помощью функции:

pop_back()

Для получения доступа к последнему элементу в контейнере имеется функция:

back()

Чтобы определить текущий размер контейнера, используют функцию:

size()

Содержит ли контейнер хотя бы один элемент, позволяет узнать функция:

empty()

Приведённых выше функций вполне достаточно, чтобы реализовать поставленную задачу.

Заполнение контейнера ptr_undo будет осуществляться в обработчике OnMouseUp компонента PaintBox1:

Bitmapes* bitmap = new Bitmapes;
bitmap -> Assign(ShowBitmap.get());
ptr_undo.push_back(bitmap); ,

в котором сначала динамически создаётся очередной объект bitmap, затем в него копируется текущее изображение из объекта ShowBitmap. И, наконец, указатель на bitmap, помещается в контейнер.

Обратите внимание, мы не уничтожаем объект bitmap в данном обработчике, поскольку в этом случае соответствующий указатель из ptr_undo будет ссылаться на уже не существующий объект. При попытке воспользоваться этим указателем произойдёт исключительная ситуация, и, как следствие, аварийное завершение программы.

Операция "Отменить" выполняется в три этапа.

Первый этап: добавление указателя на текущий объект в контейнер ptr_redo:

ptr_redo.push_back(ptr_undo.back());

Второй этап: удаление указателя на текущий объект из контейнера ptr_undo:

ptr_undo.pop_back();

Третий этап: перерисовка канвы PaintBox1 предыдущим изображением (указатель на него является к этому моменту последним в контейнере)

ShowBitmap->Assign(ptr_undo.back());
PaintBox1->Refresh();

И операцию "Восстановить" можно разбить на аналогичные этапы. Правда, выполняться они должны в другом порядке.

Сначала произойдёт перерисовка изображением, на которое указывает последний член контейнера ptr_redo

ShowBitmap->Assign(ptr_redo.back());
PaintBox1->Refresh();

Затем этот указатель помещается в контейнер ptr_undo
ptr_undo.push_back(ptr_redo.back());

И только затем удаляется из контейнера ptr_redo
ptr_redo.pop_back();

Рассмотрим операцию очистки контейнеров. Она будет проводиться в цикле и состоять из уничтожения образа изображения, с последующим удалением указателя на уже не существующий объект.

Таким образом, для контейнера ptr_undo имеем:

while(ptr_undo.size() > 1)
{
delete ptr_undo.back();
ptr_undo.pop_back();
}

Для контейнера ptr_redo:

while(!ptr_redo.empty())
{
delete ptr_redo.back();
ptr_redo.pop_back();
}

Осталось перерисовать канву PaintBox1 самым первым изображением, которое было добавлено в контейнер ещё в конструкторе нашей формы:

ShowBitmap -> Assign(ptr_undo.front());
PaintBox1 -> Refresh();

Похожие действия необходимо выполнить в деструкторе формы:

__fastcall TForm1::~TForm1()
{
while(!ptr_undo.empty())
{
delete ptr_undo.back();
ptr_undo.pop_back();
}
while(!ptr_redo.empty())
{
delete ptr_redo.back();
ptr_redo.pop_back();
}
}

Всё это позволит исключить утечку ресурсов во время работы приложения, что легко проверить, подключив к нашему проекту Code Guard.

Заключение

Контейнер deque, в применении к нашей задаче, является избыточным. Эффективнее использовать контейнер stack (он построен на базе deque, но имеет ограниченный интерфейс) или встроенный в C++ Builder класс TStack. Для справки в таблице 1 приведены соответствия между функциями этих классов

Таблица 1
Контейнер dequeКонтейнер stackКласс TStack
#include #include
std::dequeptr_undostd::stackptr_undoTStack* ptr_undo=new TStack
ptr_undo.push_back(:)ptr_undo.push(:)ptr_undo ->Push(:)
ptr_undo.pop_back()ckptr_undo.pop()ptr_undo ->Pop()
ptr_undo.empty()ptr_undo.empty()ptr_undo -> AtLeast(0)
ptr_undo.back()ptr_undo.top()ptr_undo -> Peek()
ptr_undo.size()ptr_undo.size()ptr_undo -> Count()

Принципиальное отличие можно заметить только в случае с классом TStack, а именно: экземпляр TStack создаётся динамически.

Однако преимущество контейнера deque проявится в те моменты, когда в программе потребуется получить доступ к произвольному изображению. Например, может возникнуть необходимость просмотра истории изображений, удаления или наоборот сохранения в файл, не обязательно последнего элемента (можно вспомнить, как организована работа с "историей" в Photoshop). В нашем случае, достаточно будет указать его индекс:

ptr_undo.at( i )
или
ptr_undo [ i ]
при этом на индекс налагаются простые условия:
i > 0 и i < ptr_undo.size() - 1

Кроме того, deque допускает вставку элементов, с помощью функции

insert (переменная).

В заключение приводим полный листинг описанного выше приложения.

Листинг:

h-файл

#ifndef undoH
#define undoH
//------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Buttons.hpp>
#include <ExtCtrls.hpp>

typedef Graphics::TBitmap Bitmapes;
typedef std::deque<Bitmapes*>PointerBitmapes;
//------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TPanel *Panel1;
TPaintBox *PaintBox1;
TBitBtn *BitBtn1;
TBitBtn *BitBtn2;
TBitBtn *BitBtn3;
TBitBtn *BitBtn4;
void __fastcall PaintBox1Paint(TObject *Sender);
void __fastcall PaintBox1MouseMove(TObject *Sender,
TShiftState Shift, int X, int Y);
void __fastcall PaintBox1MouseUp(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y);
void __fastcall PaintBox1MouseDown(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y);
void __fastcall BitBtn1Click(TObject *Sender);
void __fastcall BitBtn2Click(TObject *Sender);
void __fastcall BitBtn3Click(TObject *Sender);
void __fastcall BitBtn4Click(TObject *Sender);

private: // User declarations
PointerBitmapes ptr_redo;
PointerBitmapes ptr_undo;
std::auto_ptr<Bitmapes> ShowBitmap;
bool bMouseDown;
public: // User declarations
__fastcall TForm1(TComponent* Owner);
__fastcall ~TForm1();
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

Cpp-файл

#include <vcl.h>
#include <memory>
#include <deque>
#pragma hdrstop
#include "undo.h"
//-----------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner), ShowBitmap(new Bitmapes)
{
ShowBitmap ->Width = PaintBox1 -> Width;
ShowBitmap -> Height = PaintBox1 -> Height;
ShowBitmap -> Canvas -> Brush -> Color = Panel1 -> Color;
ShowBitmap ->Canvas -> FillRect(Rect(0, 0, ShowBitmap -> Width,
ShowBitmap->Height));
Bitmapes* bitmap = new Bitmapes;
bitmap -> Assign(ShowBitmap.get());
ptr_undo.push_back(bitmap);
Panel1->DoubleBuffered = true;
}
//-------------Деструктор--------
__fastcall TForm1::~TForm1()
{
while(!ptr_undo.empty())
{
delete ptr_undo.back();
ptr _undo.pop_back();
}
while(!ptr_redo.empty())
{
delete ptr_redo.back();
ptr_redo.pop_back();
}
}
//-------------Перерисовка изображения на PaintBox1------
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
PaintBox1 -> Canvas -> Draw(0, 0, ShowBitmap.get());
}
//----------Начинаем рисование-------------------------------------------
void __fastcall TForm1::PaintBox1MouseDown(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y)
{
if (Button = = mbLeft)
{
bMouseDown = true;
ShowBitmap -> Canvas -> MoveTo(X,Y);
}
}
//------------Непосредственно рисование-------------------------------------------
void __fastcall TForm1::PaintBox1MouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if(bMouseDown = = true)
{
ShowBitmap -> Canvas -> LineTo(X,Y);
PaintBox1 -> Refresh();
}
}
//--------------Заканчиваем рисование-------------------------------
void __fastcall TForm1::PaintBox1MouseUp(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y)
{
if(Button = = mbLeft)
{
bMouseDown = false;
while( !ptr_redo.empty() )
{
delete ptr_redo.back();
ptr_redo.pop_back();
}
Bitmapes* bitmap=new Bitmapes;
bitmap -> Assign(ShowBitmap.get());
ptr_undo.push_back(bitmap);
BitBtn1 -> Enabled = true;
BitBtn2 -> Enabled = false;
BitBtn3 -> Enabled = true;
}
}
//------Операция "Отменить"--------------------------------
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
ptr_redo.push_back(ptr_undo.back());
ptr_undo.pop_back();
ShowBitmap -> Assign(ptr_undo.back());
PaintBox1 -> Refresh();
BitBtn1 -> Enabled = ptr_undo.size() > 1;
BitBtn2 -> Enabled = true;
}
//------Операция "Повторить" ------------------------------
void __fastcall TForm1::BitBtn2Click(TObject *Sender)
{
ShowBitmap -> Assign(ptr_redo.back());
PaintBox1 -> Refresh();
ptr_undo.push_back(ptr_redo.back());
ptr_redo.pop_back();
BitBtn1 -> Enabled = true;
BitBtn2 -> Enabled = ptr_redo.size() > 0;
BitBtn3 -> Enabled = BitBtn1 -> Enabled;
}
//---------Очистка истории изображений-------------------
void __fastcall TForm1::BitBtn3Click(TObject *Sender)
{
while( ptr_undo.size() > 1 )
{
delete ptr_undo.back();
ptr_undo.pop_back();
}
while( !ptr_redo.empty() )
{
delete ptr_redo.back();
ptr_redo.pop_back();
}
ShowBitmap -> Assign(ptr_undo.back());
PaintBox1 -> Refresh();
BitBtn1 -> Enabled = false;
BitBtn2 -> Enabled = false;
BitBtn3 -> Enabled = false;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
Close();
}



статьи
статьи
 / 
новости
новости
 / 
контакты
контакты