С помощью 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+104), и это дает максимально удобный для восприятия формат, повышая читабельность графика:
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;
}