GMSAPR
.pdf121
вызывающая фильтр процедура получит полный контроль над областью изображения, к которой будет применено преобразование.
Реализуем на практике второй способ. При этом сам процесс преобразования изображения вынесем в отдельный поток (назовем его «рабочим» потоком) выполнения программы. Это даст пользователю возможность контролировать не только область применения фильтра, но и продолжительность выполнения операции. Например, если у пользователя не хватит терпения дождаться окончания преобразования, он сможет остановить работу.
Общая схема преобразования в этом случае будет выглядеть следующим образом:
1.Пришла команда выполнить преобразование — создаем рабочий поток;
2.Уведомляем объекты-облики о том, что начали преобразование. При этом облик запускает таймер и начинает периодически интересоваться, сколько процентов работы выполнено, показывает пользователю процент выполнения.
3.В рабочем потоке выполняется преобразование и увеличивается процент выполнения.
4.По окончании преобразования (или если пользователь прервал выполнение) в объекты-облики посылаются сообщения о завершении работы и показывается преобразованная картинка.
Поскольку данными в программе BMViewer заведует класс CBMDoc, именно в него и поместим фильтрующую функцию. Для создания рабочего потока потребуется добавить в класс CBMDoc несколько методов:
•Transform() — создает рабочий поток;
•ThreadProc() — функция потока, запускает фильтрующую функцию для конкретного объекта-документа;
•TransformLoop() — сама фильтрующая функция;
122
• InformAllViews() — передает сообщения всем обликам документа;
Не будем приводить здесь полный текст этих функций, ограничимся лишь методом TransformLoop():
void CBMDoc::TransformLoop()
{
if(m_pCurFilter==NULL) return; if(!CreateCompatibleBuffer()) return;
m_EventDoTransform.SetEvent(); // выставим флаг
// – идёт преобразование m_bEditable=FALSE; InformAllViews(UM_STARTTRANSFORM);
CRaster *pSBM=GetCurrentBMPtr(), // источник
*pDBM=GetBufferBMPtr(); // приемник m_pCurFilter->SetBuffers(pSBM, pDBM);
for(LONG y=0; y<pSBM->GetBMHeight(); y++)
{// Процент выполнения
InterlockedExchange(&m_lExecutedPercent,
100*y/pSBM->GetBMHeight());
// Проверим, не решили ли прервать преобразование if(!m_EventDoTransform.Lock(0))
{
InformAllViews(UM_ENDOFTRANSFORM, FALSE, 0); m_bEditable=TRUE;
return;
}
LONG x=0;
if( m_bEditHalf ) // Преобразовать только
// половину изображения
{
// Первую половину картинки копируем
123
// в буфер без преобразования x=pSBM->GetBMWidth()/2;
BYTE *pSPix=NULL, *pDPix=NULL; // Указатели на начало строк
if((pSPix=pSBM->GetPixPtr(0,y))!=NULL && (pDPix=pDBM->GetPixPtr(0, y))!=NULL)
//ВНИМАНИЕ! Предполагается,
//что 1 пиксел = 24 бита = 3 байта
memcpy(pDPix, pSPix, 3*x);
}
// Преобразование с использованием текущего фильтра for(; x<pSBM->GetBMWidth(); x++)
m_pCurFilter->TransformPix(x, y);
}
m_EventDoTransform.ResetEvent(); m_bEditable=TRUE;
SwapBM(); // сделать буфер текущим изображением
SetModifiedFlag(); // флаг «данные изменились»
InformAllViews(UM_ENDOFTRANSFORM, TRUE, 0); return;
};
В методе TransformLoop() мы сначала выставляем флаг события
«Выполняется преобразование» — объект m_EventDoTransform класса
CEvent (класс CEvent предназначен для обеспечения синхронизации между потоками). Затем сообщаем текущему фильтру, какое изображение будет исходным, и какое приемным (адреса объектов CRaster). Далее в цикле прогоняем через фильтр пикселы изображения. На текущий фильтр указывает переменная m_pCurFilter, которую мы завели в классе CBMDoc специально для этих целей. Тип этой переменной — «указатель на объект класса CFilter». Преобразование же данных выполняется с помощью метода
124
CFilter::TransformPix(). Класс CFilter как раз и является базовым для всех фильтров (о нем рассказано далее).
В процессе преобразования перед обработкой очередной строки пикселов вычисляется процент выполнения как процент уже обработанных строк изображения. Вычисленное значение записывается в переменную m_lExecutedPercent с помощью API-функции
InterlockedExchange() — эта функция позволяет предотвратить одновременное обращение к переменной из разных потоков. Далее проверяется, по-прежнему ли установлено событие m_EventDoTransform. И только затем обрабатываются пикселы строки. В нашей программе в демонстрационных целях мы позволяем пользователю посмотреть эффект преобразования на половине изображения — если установлен флаг m_bEditHalf, первая половина строки пикселов копируется в неизменном виде.
После того, как все пикселы изображения были обработаны, скидывается флаг m_EventDoTransform, буферное изображение становится активным и во все облики направляется сообщение UM_ENDOFTRANSFORM с параметром
TRUE, который говорит о том, что преобразование завершилось и надо обновить изображение в окне облика.
Посылаемые в облики сообщения о начале и окончании кодирования определены нами в файле BMDoc.h следующим образом:
#define |
UM_STARTTRANSFORM |
WM_USER+ 0x8000 |
#define |
UM_ENDOFTRANSFORM |
UM_STARTTRANSFORM+1 |
WM_USER — это специальная константа, начиная с которой (вплоть до значения 0xBFFF) программист может определять сообщения для использования в своем приложении без опасений о том, что они будут конфликтовать с Windows-сообщениями. Однако в документации MSDN сказано, что в диапазоне до 0x7FFF некоторые предопределенные Windows-
125
классы могут использовать значения в своих целях. Мы можем пойти дальше в своей осторожности и определить свои сообщения в диапазоне от 0x8000.
Для обработки наших сообщений в классе-облика потребуется сделать следующее:
1.В интерфейс класса CBMView (файл BMView.h) добавим объявление методов:
afx_msg LONG OnStartTransform(UINT wParam, LONG lParam); afx_msg LONG OnEndTransform(UINT wParam, LONG lParam);
2.В карту сообщений класса CBMView (файл BMView.cpp) добавим макрокоманды:
ON_MESSAGE(UM_STARTTRANSFORM, OnStartTransform)
ON_MESSAGE(UM_ENDOFTRANSFORM, OnEndTransform)
3.Добавим в класс CBMView (файл BMView.cpp) реализацию этих методов:
LONG CBMView::OnStartTransform(UINT wParam, LONG lParam)
{
OnStartTimer(); return 0;
}
LONG CBMView::OnEndTransform(UINT wParam, LONG lParam)
{
OnStopTimer();
if(wParam) // обновим изображение на виртуальном экране
UpdateVirtualScreen(); return 0;
}
Как видно из приведенного текста методов, при получении сообщения UM_STARTTRANSFORM в объекте-облике вызывается метод
OnStartTimer(). Этот метод создает таймер. Для обработки сообщений
126
WM_TIMER, которые начнет посылать таймер, в класс CBMView добавили метод OnTimer(). В этом методе будет выполняться запрос процента выполнения операции и обновляться информация о выполнении. Процент выполнения операции будем показывать в заголовке окна облика. Можно было бы, конечно, вывести индикатор выполнения (progress bar) в строку состояния, но как мы дальше увидим, наша программа позволит одновременно выполнять преобразования в нескольких рисунках, а тогда будет не совсем ясно, как делить единственный индикатор.
Приход сообщения UM_ENDOFTRANSFORM обрабатывается методом OnEndTransform(), который, в зависимости от значения аргумента
wParam:
TRUE — преобразование успешно закончено — выполняет обновление экрана;
FALSE — пользователь прервал операцию — не выполняет обновление экрана.
Далее им вызывается функция OnStopTimer(), которая останавливает таймер.
Выделение длительных операций обработки данных в отдельный поток позволяет пользователю сохранить контроль над выполнением программы, то есть программа перестает «подвисать» на таких задачах. В нашем приложении пользователь, запустив фильтрацию на одном из открытых изображений, может переключиться на просмотр и редактирование другого изображения. При необходимости пользователь может остановить выполнение преобразования, для этого в программе предусмотрим команду, которая бы сбрасывала флаг m_EventDoTransform. При сбросе этого флага цикл выполнения преобразования CBMDoc::TransformLoop() прерывается, потоковая функция завершается и рабочий поток прекращает свое существование.
127
6.4.2 Класс «Фильтр»
Спроектированная нами структура (см. рис. 5) подразумевает существование в программе некоторого объекта-фильтра. Фильтры выполняют разные преобразования, но с точки зрения фильтрующей функции они все одинаковы, и обращаться с ними она будет единообразно. Поэтому нам надо определить базовый класс CFilter для фильтра с минимальным, но основным набором методов, с помощью которых будет происходить общение. Интерфейс такого класса приведен ниже:
class CRaster;
class CFilter // Базовый виртуальный класс
{
protected:
CRaster *m_pSourceBM;
CRaster *m_pDestBM;
public:
// Устанавливает исходное и приемное изображения
void SetBuffers( CRaster *pSource, CRaster *pDest=NULL)
{
m_pSourceBM=pSource; m_pDestBM=pDest;
};
//Виртуальный метод преобразования пиксела
//будет переопределен в производных классах
virtual BOOL TransformPix(LONG x, LONG y){ return FALSE;};
};
Данные класса — два указателя на объекты-картинки класса CRaster:
•m_pSourceBM — адрес объекта «исходная картинка», откуда берутся данные для преобразования;
•m_pDestBM — адрес объекта «приемная картинка», куда
помещаются преобразованные данные. Методы класса:
128
•SetBuffers() — сообщает фильтру адреса исходного и приемного изображения;
•TransformPix() — преобразует данные одного пиксела с
координатами (x, y). Должен быть переопределен в производных классах.
Переменная-указатель на этот класс m_pCurFilter заведена в классе
CBMDoc. Этой переменной присваивается адрес текущего фильтра. В классе
CFilter уже объявлены необходимые для фильтрования методы, они и используются в методе CBMDoc::TransformLoop(). Так как метод
CFilter::TransformPix() объявлен виртуальным, в методе
TransformLoop() будет происходить вызов настоящего метода преобразования активного фильтра.
Для реализации точечных методов преобразования создадим класс
CDotFilter:
// Базовый класс для точечных фильтров class CDotFilter: public CFilter
{
protected:
// Таблицы преобразования для компонент цвета
BYTE BGRTransTable[3][256]; public:
// Метод преобразования пиксела
BOOL TransformPix(LONG x, LONG y);
};
Данными этого класса являются три таблицы преобразования компонентов RGB цвета. В принципе, для методов преобразования, рассмотренных далее, достаточно было бы определить одну таблицу, но более общим подходом будет все-таки формирование трех таблиц, потому что возможны преобразования, изменяющие цветовую гамму (оттенок) изображения. Тут-то три таблицы и пригодятся.
129
Для точечного фильтра переопределен метод TransformPix(). Он будет общим для большинства рассмотренных далее точечных фильтров. Реализация метода:
BOOL CDotFilter::TransformPix(LONG x, LONG y)
{
BYTE *pDPix=NULL, *pSPix=NULL; if(m_pSourceBM==NULL ) // Источник необходим
return FALSE;
//Если приемник не задан,
//то преобразование помещаем в источник
if(m_pDestBM==NULL) m_pDestBM=m_pSourceBM;
// Получаем указатели на пикселы в источнике и приемнике if((pDPix=m_pDestBM->GetPixPtr(x, y))==NULL ||
(pSPix=m_pSourceBM->GetPixPtr(x, y))==NULL) return FALSE;
// Преобразование: Порядок каналов — BGR *pDPix=BGRTransTable[0][*pSPix]; *(pDPix+1)=BGRTransTable[1][*(pSPix+1)]; *(pDPix+2)=BGRTransTable[2][*(pSPix+2)]; return TRUE;
};
В случае точечных преобразований, возможна такая реализация фильтрующей функции, когда исходное изображение одновременно является и приемником преобразования (однако, в этом случае сложнее сделать отмену преобразования). Эта ситуация отрабатывается внутри метода.
Хотя формат 24-битового цвета называют RGB, в файле формата BMP компоненты цвета хранятся в обратном порядке. Это не особо важно, но это надо знать и учитывать, что и делается при формировании нового значения цвета пиксела.
130
Все, что останется сделать в производных от CDotFilter классах, описывающих разные эффекты, — это реализовать инициализацию таблиц преобразования.
Для реализации пространственных (матричных) методов преобразования создадим класс CMatrixFilter. Интерфейс класса приведен ниже:
// Пространственные (матричные) фильтры
class CMatrixFilter: public CFilter // Базовый класс
{
protected:
int m_rangX; // размер матрицы по X и Y int m_rangY;
const int *m_pMatrix; // указатель на матрицу public:
// Метод преобразования пиксела
BOOL TransformPix(LONG x, LONG y);
};
Данные класса: размер матрицы преобразования и указатель на матрицу. Как правило, используются квадратные матрицы преобразования, но кто знает, может для чего-то будет полезна и не квадратная матрица. Поэтому указывается размер матрицы по горизонтали и вертикали. Размер матрицы определяет зону пикселов, окружающую пиксел (x, y), которая будет вовлечена
врасчет нового значения пиксела (x, y). Указателю на матрицу преобразования m_pMatrix, будет присваиваться адрес матрицы, которая будет использована
впреобразовании. Реализация метода CMatrixFilter::TransformPix():
BOOL CMatrixFilter::TransformPix(LONG x, LONG y)
{
BYTE *pDPix=NULL, *pSPix=NULL;
// Источник и приемник необходимы
if( m_pSourceBM==NULL || m_pDestBM==NULL) return FALSE;
// Определяем зону перекрытия изображения и матрицы