Режимы отображения
координат
Масштабирование изображения
В Windows любые
операции вывода являются графическими. Подсистема GDI делает вывод анпаратно
независимым. Это означает, что информация, выводимая на любое физическое устройство,
будет выглядеть почти одинаково. На экране монитора, на плоттере и на принтере
будут отражены все детали текста или изображения. Кроме того, GDI поддерживает
логические устройства вывода, такие как память или диск. Для осуществления вывода
в среде Windows необходимо сначала создать так называемый контекст устройства
(device context). Контекст устройства — это объект, определенный в GDI,
который содержит содержат подробную информацию об устройстве, куда предполагается
направить графический вывод. Например: основной цвет и цвет фона, используемая
палитра, шрифт по умолчанию и т. д.
MFC имеет специальный
набор классов, упрощающий процедуру общения с контекстом устройства. Класс CDC
содержит большую часть функций, которые могут понадобиться для управления выводом.
Классы, производные от CDC, обеспечивают специальные возможности, например класс
cciientoc обеспечивает доступ к клиентской области окна, где в основном разворачиваются
события, управляемые программистом. Класс CPaintDC позволяет управлять процессом
перерисовки окон, обеспечивая вызовы функций BeginPaint и EndPaint в ответ на
сообщение WM_PAINT. Создание рисунка в окне производится с помощью функций API,
инкапсулированных в одноименных методах класса CDC
Если мы зададимся
целью масштабировать изображение, то есть дать пилы-юним-;-лю возможность изменять
его размеры, то нам следует научиться управлять режимами и параметрами отображения
логической системы координат в физическую. Вы уже знаете, что Win32 API использует
четыре координатных пространства (spaces): World space, Page space, Device
space, и Physical Device space. Однако большая часть документации и книг но
MFC оперируют только двумя терминами: логическая и физическая системы координат.
Мы будем использовать термин, логическая система координат, имея в виду Page
space.
Заметьте, что
и World space и Page space измеряют плоскую область, размах которой по обеим
координатам равняется 2
32 логических единиц, то есть более 4 миллиардов
единиц. Page space работает совместно с Device space, чтобы обеспечить приложение
единицами, не зависящими от типа физического устройства, такими как миллиметры
и дюймы-(inches). Конечным координатным пространством, Physical Device space
обычно является клиентская область окна приложения, или весь экран монитора,
или страница бумаги принтера (плоттера). Размеры области физического устройства
изменяются в зависимости от марки, технологии и т. д. Чтобы верно передать детали
изображения, созданного в логической системе, в физическое устройство, система
преобразовывает их путем отображения (mapping) прямоугольной области
из одного координатного пространства в другое. При копировании каждой точки
прямоугольной области из одного пространства в другое Windows применяет алгоритм
трансформации, который в конечном счете изменяет размеры, ориентацию и форму
всего объекта.
Так как рисование
производится в логической структуре, называемой контекстом устройства, то рисунок
не зависит от конкретного устройства, в которое будет осуществлен вывод изображения.
Функциональность контекста устройства поддерживается классом CDC или одним из
его потомков. Для выбора режима отображения используются методы этого класса:
SetMapMode — задание
режима отображения координат;
SetWindowOrg — задание
выделенной точки (начала отображения) в логической системе координат;
Setviewportorg — задание
выделенной точки (начала отображения) в физической системе координат;
SetwindowExt — характеристика
протяженности окна вдоль двух логических координат;
SetviewportExt — характеристика
протяженности окна вдоль двух физических координат.
Восемь существующих
режимов отображения координат задаются с помощью символьных констант, определенных
в файле Wingdi.h:
#define
MM_TEXT 1
// 1 лог. ед. - 1 пиксел
#define
MM_LOMETRIC 2 // 1 лог. ед. - 0,1 мм
#define
MM_HIMETRIC 3 // 1 лог. ед. - 0,01 мм
#define
MM_LOENGLISH 4 // 1 лог. ед. - 0,01 дюйма
#define
MM_HIENGLISH 5 // 1 лог. ед. - 0,001 дюйма
#define
MMJTWIPS 6 //1 лог. ед. - 1/1440 дюйма
//==========
Преобразования по формуле ==========//
#define
MM_ISOTROPIC 7 // Растяжение одинаково
#define
MM_ANISOTROPIC 8 // Растяжение различно
По умолчанию
действует режим ММ_ТЕХТ, в котором ось Y имеет направление сверху вниз. Последующие
пять режимов предполагают, что ось Y направлена снизу-вверх. В двух последних
режимах преобразование координат из логической системы в физическую выполняется
в соответствии с формулами, которые приведены ниже. При этом используются следующие
обозначения:
Viewport — область вывода,
задаваемая физическими координатами;
Window — окно, задаваемое
логическими координатами.
Выделенные
точки window и viewport совмещаются, а изображение растягивается или сжимается
так, что горизонтальная координата каждой точки изображения при выводе в физическое
устройство вычисляется по формуле линейного преобразования:
Dx=Vox+(Vex/Wex)*(Lx-Wox)
где: D
x
— аппаратная (device) или физическая Х-координата точки,
L
x
- логическая (logical) Х-координата точки,
V
ex
- протяженность области вывода, задаваемая SetVievvportExt,
W
ex
— протяженность окна, задаваемая SetWindowExt,
V
ox
— X начала координат области вывода (SetViewportOrg),
W
ox
— X начала координат логического окна (SetWindowOrg).
Аналогичная
формула справедлива для Y-координаты точки. Опробуем формулу на произвольном
наборе данных. (Такого типа вопросы вы можете встретить на сертификационном
экзамене Microsoft.) Предположим, что в режиме MM_ANISOTROPIC заданы такие параметры
отображения:
//======
Выделенная точка в логическом окне
pDC->SetWindowOrg
(300, 0) ;
//======
Выделенная точка в физическом окне
pDC->SetViewportOrg
(200, 200);
//======
Протяженность логического окна pDC->SetWindowExt (100, 100);
//======
Протяженность физического окна
pDC->SetViewportExt
(50, -200);
Какие координаты
в окне на экране будут иметь точки, заданные оператором: CPoint pl(0, 0), р2(100,
100)? Ответ: они преобразуются в аппаратные (физические) координаты: (50, 200)
и (100, 0). Проверим первую координату подстановкой в формулу:
200 +
50/100(0-300)=50
Таким образом,
если мы хотим увеличить или уменьшить изображение, то нужно изменять следующие
величины:
Vex / Wex—
коэффициент растяжения (сжатия) вдоль оси X,
Vey / Wey—
коэффициент растяжения (сжатия) вдоль оси Y.
Эти формулы
работают независимо только в режиме MM_ANISOTROPIC. Несколько иначе они работают
в режиме MM_ISOTROPIC и вовсе не работают в остальных шести режимах.
В режиме MM_ISOTROPIC
система обеспечивает одинаковое расширение (сжатие) по обеим осям, поэтому результат
вычислений по приведенным формулам зависит от соотношения величин коэффициентов
растяжения (сжатия). Теперь видно, что режим MM_ANISOTROPIC обеспечивает наибольшую
свободу и гибкость в преобразовании координат. Числитель и знаменатель в формулах
для коэффициентов растяжения (сжатия) задаются по отдельности с помощью методов
класса CDC. Метод SetviewportExt задает числитель обоих отношений, следовательно,
определяет свойства физического устройства, а метод SetwindowExt задает знаменатель,
то есть задает свойства логической системы координат. Имена функций вводят нас
в заблуждение, так как на самом деле эти функции ничего не задают и не определяют.
Работая в паре, они дают способ задания двух вещественных коэффициентов растяжения
(сжатия) путем задания четырех целых чисел. Параметры функций должны быть целыми
или объектами класса csize, которые тоже создаются из двух целых. Значительно
проще было бы задать два вещественных числа и использовать их в качестве коэффициентов.
Объяснение наблюдаемой реальности, видимо, кроется в истории разработки функций
API (сложности с floating point-арифметикой).
Масштабирование
изображения
Зададимся целью
внести изменения в приложение My, которое мы создали в предыдущей главе, таким
образом, чтобы изображение геометрической фигуры всегда было в центре окна и
следило за изменением размеров окна приложения, меняясь пропорционально. Напомним,
что фигурой является многоугольник с произвольным количеством вершин, и он выводится
в контекст устройства Windows с помощью объекта класса CDC. Для того чтобы упростить
процесс слежения за размерами окна представления, введем в число членов класса
CMyView новую переменную, которая будет хранить текущие размеры окна.
Вызовите контекстное
меню над именем класса CMyView в окне Class View и выберите команду Add >
Add Variable.
Заполните поля окна мастера
так, чтобы он создал private-переменную m_s zView типа csize.
Убедитесь в том, что
мастер Add Variable выполнил свою работу. В окне Class View вы должны увидеть
ее в числе данных класса.
Повторите эти же действия
и введите в состав CMyView private-переменную UINT m_nLogZoom, в которой мы
будем хранить коэффициент увеличения, используемый при переходе из пространства
World в пространство Page.
В пространстве Page мы
увеличим изображение в 100 раз. Измените уже созданный В конструкторе CMyView
инициализатор m_nLogZoom (0) на m_nLogZoom (100).
Примечание
Если вы помните (об этом
я уже говорил в связи с конструктором документа), наш многоугольник «в миру»
задан вещественными координатами и имеет размах в 2 единицы, так как мы вписали
его в окружность единичного радиуса, то есть диаметром в 2 единицы. Там же
мы преобразовали все координаты в логическую систему, увеличив размеры многоугольника
в 100 раз и перевернув его (так как ось Y экрана направлена вниз). Кроме того,
мы сдвинули изображение вправо и вниз, чтобы попасть в центр «листа ватмана».
Логические координаты (уже целого типа) мы занесли в массив m_Points, который
использован в функции OnDraw класса представления при изображении многоугольника.
Теперь надо изменить ситуацию. Обычно документ хранит истинные («мировые»)
координаты объектов, а класс представления преобразовывает их в логические
и изображает в физическом устройстве с помощью рассмотренных преобразований.
Так делают, потому что пользователя не интересуют логические (Page) координаты.
Он должен видеть и иметь возможность редактировать реальные (World) координаты
объекта.
Чтобы реализовать
указанный подход, надо заменить в классе документа массив целых координат на
массив вещественных, а в классе CMyView создать еще один массив, но уже целых
координат. Используя уже знакомую технику, введите в состав класса CMyView private-переменную
vector<CPoint>
m_Points;
Ее имя совпадает
с именем массива координат в документе, но это не помеха, если используешь ООП.
Классы ограничивают область действия имен, скрывая их. В интерфейсе класса документа
(файл MyDoc.h) замените объявление
vector<CPoint>
m_Points;
на
VECPTS
m_Points;
Этой заменой
мы оставили для контейнера то же имя, но изменили тип его элементов. Тип данных
VECPTS — вектор точек с вещественными (World) координатами — пока не определен,
но мы собираемся его декларировать и определить для того, чтобы было удобно
хранить реальные координаты объекта. Для начала создадим свой собственный класс
CDPoint, инкапсулирующий функциональность точки с вещественными координатами.
Вставьте в начало файла MyDoc.h после директивы препроцессора #pragma once,
но до объявления класса CMyDoc декларацию нового класса
1:
//======
Новый класс "Вещественная точка"
class
CDPoint
{
public:
double
x;
double
у; // Вещественные координаты
//======
Конструктор по умолчанию
CDPoint()
{
х=0.;
у=0.;
}
//======
Конструктор копирования
CDPoint(const
CDPointS pt)
{
x
= pt.x;
y
= pt.y;
}
//======
Конструктор с параметрами
CDPoint(double
xx, double yy)
{
x
= x x;
у
= yy;
}
//======
Операция умножения (увеличение в п раз)
CDPoint
operator*(UINT n)
{
return
CDPoint (x*n, y*n);
}
//======
Операция присваивания
CDPointS
operator=(const CDPointS pt)
{
x
= pt.x;
у
= pt.y;
return
*this; // Возвращаем свой объект
}
//======
Операция сложения двух точек
CDPoint
operator*(CDPointS pt)
{
return
CDPoint(x + pt.x, у + pt.y);
}
//======
Операция вычитания двух точек
CDPoint
operator-(CDPointS pt)
{
return
CDPoint(x - pt.x, у - pt.y);
}
//
Метод приведения к типу CPoint (целая точка)
CPoint
Tolnt()
{
return
CPoint(int(x),int(у)); }
//======
Операция сложения с записью результата
void
operator+=(CDPointS pt) { x += pt.x; у += pt.y; }
//======
Операция вычитания с записью результата
void
operator-=(CDPoint& pt) { x — pt.x; у -= pt.y; }
//
Операция вычисления нормы вектора, заданного точкой
double
operator!() {
return fabs(x) + fabs(y); } };
При использовании
контейнера объектов класса полезно декларировать новый тип данных:
typedef
vector<CDPoint, allocator<CDPoint> > VECPTS;
Вставьте эту
строку сразу после объявления класса CDPoint. Данное объявление позволяет просто
создавать множество контейнеров для хранения точек с вещественными координатами.
Представим, что документ должен хранить контуры нескольких деталей сложной конструкции.
Тогда каждая деталь конструкции может быть объявлена объектом типа VECPTS, то
есть она является контейнером, содержащим координаты точек своего контура.
Кроме трех
конструкторов для удобства пользования в классе CDPoint заданы правила сложения
и вычитания точек. Метод Tolnt позволяет создать стандартную точку Windows (CPoint)
из нашей вещественной точки. Операция умножения точки на целое число без знака
(CDPoint operator* (UINT n);) позволяет увеличивать координаты объекта, что
мы используем при переходе из World- в Page-пространство. Операция вычисления
нормы вектора, начало которого находится в точке (0,0), а конец в данной точке,
полезна при оценке степени близости двух точек. В одном из следующих уроков
она нам пригодится. Тело конструктора документа можно упростить, так как теперь
он помнит реальные координаты объекта и необходимость преобразовывать координаты
в пространство Page исчезла. Это преобразование мы сделаем в классе CMyView:
CMyDoc::CMyDoc()
{
//======
Вспомогательные переменные
double
pi = 4. * atan(l.), //====== Углы
al
= pi / 10., a2 = 3. * a1,
//=====
2 характерные точки
xl
= cos(al), yl = sin(al), x2 = cos(a2), y2 = sin(a2);
//====
Вещественные (World) координаты углов звезды
m_Points.push_back(CDPoint(
0., 1.) ) ;
m_Points.push_back(CDPoint(-x2,
-y2));
m_Points.push_back(CDPoint(
xl, yl)) ;
m_Points.push_back(CDPoint(-xl,
yl));
m_Points.push_back(CDPoint(
x2, -y2));
}
Масштабирование
изображения можно упростить, если следить за текущими размерами клиентской области
окна. При изменении пользователем размеров окна-рамки в момент отпускания кнопки
мыши система посылает приложению сообщение WM_SIZE, на которое должен среагировать
класс CMyView и запомнить в переменной m_szView новые размеры.
Сейчас мы введем
в состав CMyView новую функцию отклика, которая будет вызываться в ответ на
приход сообщения WM_SIZE. Она должна иметь имя OnSize (так устроена MFC) и иметь
особый спецификатор afx_msg, который невидим компилятором (препроцессор заменит
его пустым местом), но нужен инструментам Studio.Net. Спецификатор несет информацию
о том, что функция OnSize особая — она является эффективно реализованным обработчиком
сообщения (message handler). В Studio.Net процесс создания функций-обработчиков
и виртуальных функций сильно изменен. Теперь это делается не с помощью ее инструмента
ClassWizard, следы которого однако присутствуют в студии, а в окне Properties.
Выделите имя класса CMyView
в окне Class View и перейдите на страницу Properties, выбрав соответствующую
вкладку.
Обратите внимание на
панель инструментов окна Properties. Она динамически изменяется в зависимости
от выбора (selection) в других окнах. Сейчас на ней должна быть кнопка с подсказкой
Messages. Нажмите эту кнопку.
В появившемся списке
сообщений найдите сообщение WM_S IZE. В правой ячейке (типа Combo box) таблицы
выберите <Add> OnSize.
Вновь перейдите в окно
Class View, найдите новую функцию-обработчик OnSize в составе класса CMyView
и сделайте на ней двойной щелчок.
Фокус ввода переходит
в окно редактора текста для файла MyView.cpp. Введите изменения так, чтобы
функция приобрела вид:
void
CMyView::OnSize(UINT
nType, int ex,
int cy)
{
//==========
Вызов родительской версии
CView::OnSize(nType,
ex, cy) ;
if
(cx==0 || cy==0)
return;
//=========
Запоминаем размеры окна представления
m_szView
= CSize (ex, су); } ;
Проверка if
(cx==0...) необходима потому, что каркас приложения вызывает OnSize несколько
раз и иногда с нулевыми размерами. Обратите внимание на то, что мастер вставок
добавил также и прототип (объявление) функции обработки в интерфейс класса CMyView
(см. файл MyView.h):
public:
afx_msg
void OnSize(UINT nType, int ex, int cy) ;
Теперь покажем,
как с помощью Studio.Net следует определять в классе собственные версии виртуальных
функций. Мы собираемся однократно (при открытии окна) преобразовать «мировые»
координаты в логические и запомнить их. Это удобно сделать внутри виртуальной
функции OnlnitialUpdate, которая унаследована от класса cview. Она вызывается
каркасом приложения в тот момент, когда окно еще не появилось на экране, но
уже существует его Windows-описатель (HWND) и объект класса CMyView прикреплен
(attached) к окну. Напомним также, что документ имеет и поддерживает динамический
список всех своих представлений.
В окне Class View поставьте
курсор на имя класса CMyView и щелкните правой клавишей мыши.
Перейдите в окно Properties,
щелкнув вкладку, с помощью подсказок отыщите на панели инструментов именно
этого окна кнопку Overrides и нажмите ее.
Появится длинный список
виртуальных функций родительских классов, которые можно переопределить в классе
ему view. Найдите в нем функцию OnlnitialUpdate и выберите в правой половине
таблицы действие <Add>.
Результат ищите
в конце файла MyView.cpp. Внесите изменения в тело функции:
void
CMyView::OnlnitialUpdate()
{
CView::OnlnitialUpdate();
//
Создаем ссылку на контейнер World-координат точек
VECPTSS
pts = GetDocument()->m_Points;
UINT
size = pts.size ();
//======
Задаем размер контейнера логических точек
m_Points.
resize (size);
for
(UINT i=0; i < size;
m_Points[i]
= (pts[i] * m_nLogZoom) .Tolnt () ;
}
Здесь мы добываем
из документа World-координаты объекта, умножаем их на коэффициент m_nLogZoom
и преобразуем к целому типу. Обратите внимание на использование операций и методов
вновь созданного класса CDPoint и на то, что переменная pts создана и инициализирована
как ссылка на контейнер точек документа. Теперь осталось изменить коды для перерисовки
представления так, чтобы воспользоваться техникой масштабирования, которую мы
обсудили в начале главы:
void
CMyView: :OnDraw(CDC*
pDC)
{
CMyDoc*
pDoc = GetDocument () ;
ASSERT_VALID(pDoc)
;
//======
Узнаем размер контейнера точек
UINT
nPoints = m_Points.size () ;
if
(! nPoints) // Уходим, если он пуст return;
//
Сохраняем текущее состояние контекста pDC->SaveDC() ;
//
Создаем перо Windows для прорисовки контура
CPen
pen (PS_SOLID,2,RGB(0, 96,0) ) ;
//======
Выбираем его в контекст устройства
pDC->SelectObject
(spen);
//
Создаем кисть Windows для заливки внутренности
CBrush
brush (RGB (240, 255, 250) );
pDC->SelectObject
(Sbrush);
//======
Задаем режим преобразования координат
pDC->SetMapMode(MM_ISOTROPIC)
;
//======
Сдвиг в логической системе
pDC->SetWindowOrg(0,0)
;
//======
Сдвиг в физической системе
pDC->SetViewportOrg
(m_szView.cx/2, m_szView. су/2) ;
//======
Знаменатель коэффициента растяжения
pDC->SetWindowExt
(3*m_nLogZoom, 3*m_nLogZoora) ;
//======
Числитель коэффициента растяжения
pDC->SetViewportExt
(m_szView.cx, -m_szView.cy) ;
//======
Изображаем полигон
pDC->Polygon
(Sra_Points [0] , nPoints) ;
//
Восстанавливаем контекст (предыдущие инструменты GDI)
pDC->RestoreDC
(-1) ;
}
Коэф(рициент
3 в параметрах SetWindowExt моделирует ситуацию, когда лист ватмана в 3 раза
превышает размер детали, на нем изображенной. Знак минус в параметре SetViewportExt
позволяет компенсировать изменение направления оси Y при переходе из Page space
в Device space. При рисовании мы используем логические (Page) координаты, которые
хранятся в классе CMyView.
Рис. 2.1.
Изображение объекта в режиме MM_SOTROPIC
Рис.
2.2. Изображение объекта в режиме MM_ANISOTROPIC
Запустите приложение
(Ctrl+F5) и попробуйте изменять размеры окна, по отдельности сжимая его до предела
вдоль обеих координат. Наблюдайте за тем, как размеры звезды следят за размерами
окна. Отметьте, что фигура остается в центре окна, а ее пропорции остаются без
изменений. Затем задайте режим отображения MM_ANISITROPIC и вновь запустите
приложение. Проведите те же манипуляции с окном и отметьте, что пропорции объекта
теперь искажаются. Тем не менее оба режима используются на практике. Например,
нет смысла изменять пропорции какой-либо конструкции или ее детали, но вполне
допустимо изменять пропорции графика функции.
Анализируя
трансформации звезды, имейте в виду, что ее размах 2 абстрактные, не определенные
нами единицы в world-пространстве и 200 логических единиц в Page-пространстве.
Само Page-пространство мы задали с помощью выражения 3*m_nLogZoom, то есть 300
на 300 единиц. При переходе к Device-координатам система растягивает или сжимает
его так, чтобы оно занимало все окно. При этом она сдвигает изображение и центр
звезды (0,0) попадает в центр окна (m_szview. сх/2, m_szView. су/2). Полезно
поэкспериментировать и с другими режимами преобразования координат. При этом
следует помнить, что все они, кроме режима ММ_ТЕХТ, предполагают ось Y направленной
вверх.