Класс
графика
С помощью Studio.Net
введите в состав проекта новый generic-класс CGraph, не указывая имени базового
класса и не включая флажок Virtual destructor. В файл декларации нового класса
введите вручную вспомогательный класс CDPoint, необходимость в котором мы обсуждали
ранее. Затем добавьте объявление структуры TData, которая собирает воедино все
данные, используемые при построении графика. Начальная буква Т в имени класса
осталась со времен работы в среде Borland. Там принято все классы именовать
начиная с буквы Т (Туре), означающей создание нового типа данных. Но в отличие
от старой реализации графика, которая, возможно, знакома читателю по книге «Технологии
программирования на языке C++» (Издательство СПбГТУ, 1997), мы введем в класс
CGraph некоторые новые возможности:
#
pragma
once
class
CDPoint
{
public:
//===
Две вещественные координаты точки на плоскости
double
x, у;
//=======
Стандартный набор конструкторов и операций
CDPoint
() {
х=0.; у=0.;
}
CDPoint(
double
xx
, double
yy)
{
х=хх;
У=УУ;
}
CDPoints
operator=(const
CDPointi pt) {
x
= pt.x;
У
= pt.y; return *this;
}
CDPoint(
const
CDPointS pt) {
*this
- pt;
} };
//===== Вспомогательные данные, характеризующие
//== последовательность координат вдоль одной из осей
struct
TData (
//===== Порядок в нормализованном представлении числа
int Power; //===== Флаг оси X
bool
bХ;
double
//=======
Экстремумы
Min,
Max,
//======= Множитель -(10 в степени Power)
{
Factor,
//=======
Шаг вдоль оси (мантисса)
Step,
//=======
Реальный шаг
dStep,
//====
Первая и последняя координаты (мантиссы)
Start,
End,
//
=======
Первая и последняя координаты
dStart,
dEnd; };
//===== Класс, реализующий функции плоского графика
class
CGraph {
public:
//=====
Данные, характеризующие данные вдоль осей
TData
m_DataX, m_DataY;
//=====
Контейнер точек графика
vector
<CDPoint>& m_Points;
//=====
Текущие размеры окна графика
CSize
m_Size;
//=====
Экранные координаты центра окна
CPoint
m_Center;
//=====
Заголовок и наименования осей
CString
m_sTitle, m_sX, m_sY;
//=====
Перо для рисования
CPen
m_Pen;
//=====
Два типа шрифтов
CFont
m_TitleFont, m_Font;
//=====
Высота буквы (зависит от шрифта)
int
m_LH,
//=====
Толщина пера
m_Width;
//=====
Цвет пера COLORREF m_Clr;
//=======
Методы для управления графиком
CGraph(vector<CDPoint>&
pt, CString sTitle, CString sX, CString sY) ;
virtual
-CGraph();
//=====
Заполнение TData для любой из осей
void
Scale(TDataS data);
//=====
Переход к логическим координатам точек
int MapToLogX (double d);
int MapToLogY (double d);
//=====
Изображение в заданном контексте
void
Draw (CDC *pDC);
//=====
Изображение одной линии
void
DrawLine(CDC *pDC) ;
//=====
Подготовка цифровой метки на оси
CString MakeLabel(bool bx, doubles d);
};
Класс CGraph
сделан с учетом возможности развития его функциональности, так чтобы вы могли
добавить в него нечто и он мог бы справиться с несколькими кривыми одновременно.
Фактически он представляет собой упрощенную версию того класса, которым мы пользуемся
для отображения результатов расчета поля в двухмерной постановке. Отметьте,
что структура TData используется как для последовательности абсцисс, так и ординат.
Алгоритм нормирования
абсцисс и ординат проще создать, чем кратко и понятно описать. Тем не менее
попробуем дать ключ к тому, что происходит. Мы хотим, чтобы размеры графика
отслеживали размеры окна, а числа, используемые для разметки осей, из любого
разумного диапазона, как можно дольше оставались читабельными. Задача трудновыполнимая,
если динамически не изменять шрифт. В данной реализации мы не будем подбирать,
а используем только два фиксированных шрифта: для оцифровки осей и для вывода
заголовка графика. Обычно при построении графиков числа, используемые для оцифровки
осей (мантиссы), укладываются в некоторый разумный диапазон и принадлежат множеству
чисел, кратных по модулю 10, стандартным значениям шага мантиссы (2, 2.5, 5
и 10). Операцию выбора шага сетки, удовлетворяющую этим условиям, удобно выполнить
в глобально определенной функции, не принадлежащей классу CGraph. Это дает возможность
использовать функцию для нужд других алгоритмов и классов. Ниже приведена функция
gScale, которая выполняет подбор шага сетки. Мы постепенно дадим содержимое
всего файла Graph.срр, поэтому вы можете полностью убрать существующие коды
заготовки. Начало файла имеет такой вид:
#include
"StdAfx.h"
#include
"graph.h"
//=====
Доля окна, занимаемая графиком
#define
SCAT,F,_X 0 . 6
#define
SCALE_Y 0.6
//===
Внешняя функция нормировки мантисс шагов сетки
void
gScale
(double
span,
doubles
step)
{
//==
Переменная span определяет диапазон изменения
//==
значаний одной из координат точек графика
//==
Вычисляем порядок числа, описывающего диапазон
int
power = int(floor(loglO(span)));
//=====
Множитель (zoom factor)
double
factor = pow(10, power);
//=====
Мантисса диапазона (теперь 1 < span < 10)
span
/= factor;
//=====
Выбираем стандартный шаг сетки if (span<1.99)
step=.2;
else
if
(span<2.49)
step=.25;
else
if
(span<4.99)
step=.5;
else
if
(span<10.)
step=
1.;
//===== Возвращаем реальный шаг сетки (step*10~power)
step *= factor;
}
Результатом
работы функции gScale является значение мантиссы дискретного шага сетки, которая
наносится на график и оцифровывает оду из осей. Самым сложным местом в алгоритме
разметки осей является метод CGraph:: Scale. Он по очереди работает для обеих
осей и поэтому использует параметр с данными типа TData, описывающими конкретную
ось. Особенностью алгоритма является реализация идеи, принадлежащей доценту
СПбГТУ Александру Калимову и заключающейся в том, чтобы как можно дольше не
переходить к экспоненциальной форме записи чисел. Обычно Калимов использует
форму с фиксированной запятой в диапазоне 7 порядков изменения чисел (10~
3
+10
4
),
и это дает максимально удобный для восприятия формат, повышая читабельность
графика:
void CGraph::Scale (TDatai data)
{
//=====
С пустой последовательностью не работаем
if
(m_Points.empty())
return;
//=====
Готовимся искать экстремумы
data.Max
= data.bX ? m_Points [0] .х : m_Points [0] .у;
data.Min
= data.Max;
//=====
Поиск экстремумов
for
(UINT j=0; j<ra_Point5.size(); j++)
{
double d = data.bX ?
m_Points
[ j] .x
: m_Points [ j] . y;
if (d < data.Min) data.Min = d;
if (d > data.Max) data.Max = d;
}
//===== Максимальная амплитуда двух экстремумов
double
ext = max(fabs(data.Min),fabs(data.Max));
//===== Искусственно увеличиваем порядок экстремума
//===== на 3 единицы, так как мы хотим покрыть 7 порядков,
//===== не переходя к экспоненцеальной форме чисел
double
power = ext > 0.? loglO(ext) +3. : 0.;
data.Power
= int(floor(power/7.));
//===== Если число не укладывается в этот диапазон
if
(data.Power != 0)
//=====
то мы восстанавливаем значение порядка
data.Power
= int(floor(power)) - 3;
//=====
Реальный множитель
data.Factor
= pow(10,data.Power);
//=====
Диапазон изменения мантиссы
double
span = (data.Max - data.Min)/data.Factor;
//=====
Если он нулевой, if (span == 0.)
span
= 0.5; // то искусственно раздвигаем график
// Подбираем стандартный шаг для координатной сетки
gScale
(span, data.Step);
//===== Шаг с учетом искусственных преобразований
data.dStep
= data.Step * data.Factor;
//== Начальная линия сетки должна быть кратна шагу
//==== и быть меньше минимума
data.dStart
= data.dStep *
int (floor(data.Min/data.dStep));
data.Start
= data.dStart/data.Factor;
//===== Вычисляем последнюю линию сетки
for
(data.End = data.Start;
data.End
< data.Min/data.Factor + span-le-10;
data.End
+= data.Step)
data.dEnd = data.End*data.Factor;
}