В курсе основ программирования большой интерес у студентов вызывают темы, связанные с изучением графических возможностей языков программирования. Знание этого инструментария позволяет студенту создавать собственные графические подпрограммы и эффективно использовать их при разработке своих приложений компьютерной графики. Во-первых, собственные подпрограммы можно использовать в различных проектах, сведя к минимуму время на их внедрение в код другой программы. Во-вторых, этот материал наглядно демонстрирует взаимодействие математики и программирования. В-третьих, особого внимания заслуживает аспект сравнения графических возможностей разных языков. Знание отличий стандартных графических примитивов в разных языках позволяет, в частности, легко переписать определенный код программы с одного языка на другой.
Итак, наличие в современных языках высокого уровня широкого набора встроенных подпрограмм значительно облегчает разработку приложений компьютерной графики. К примеру, в семействе систем Delphi одной из таких подпрограмм является процедура Pie, которая предназначена для рисования сектора круга. Её заголовок имеет вид:
procedure Pie(X1: Integer; Y1: Integer;
X2: Integer; Y2: Integer;
X3: Integer; Y3: Integer;
X4: Integer; Y4: Integer);
Координаты (X1, Y1) и (X2, Y2) задают прямоугольную область, в которую вписан эллипс. На рис. 1 показана роль точек (X3,Y3) и (X4,Y4).
Сектор ограничен отрезками лучей, выходящих из центра эллипса и проходящих через точки A(X3,Y3) и B(X4,Y4). Сталкиваясь впервые с задачей изображения круговой диаграммы, начинающему программисту может быть непонятно, как всё же практически нарисовать «куски пирога» на основе некоторых исходных данных?
Попытаемся изобразить сектор, показанный на рис. 1, с помощью подпрограммы Pie().
Рис. 1. Прямоугольная область для рисования сектора
Фрагмент программы выглядит просто:
with Image1,Canvas do
begin
Pen.Color := clNavy; Brush.Color := clWhite;
Rectangle(0, 0, width, height); Brush.Color := clYellow;
Pie(1, 1, width-1, height-1, width-1, height div 2, width-1, 1)
end;
Результат рисования (рис. 2) совпал с ожидаемым результатом (рис. 1).
Рис. 2. Результат рисования
Однако рассмотренный пример совершенно не проясняет технику рисования реальной круговой диаграммы. Картина может стать более ясной, если обратиться к известной единичной окружности (см. рис. 3). Будем рисовать сектор от луча OA к лучу OB, двигаясь против часовой стрелки. Заметим, что точка B(X4,Y4) луча OB выбрана иначе: теперь она лежит на единичной окружности.
Из рис. 3 видно, что абсцисса X4 и ордината Y4 новой точки B вычисляются обычным образом: X4 = cos(α), Y4 = sin(α).
При рисовании на экране ЭВМ графическим пером геометрических объектов из декартовой системы координат следует помнить о системе координат графического экрана компьютера. Она изображена на рис. 4.
Рис. 3. Единичная окружность
Рис. 4. Графическая система координат
Проблема в том, что, рассматривая точку (X*,Y*) в декартовой системе координат, мы должны правильно отобразить её на графическом экране. Это можно проиллюстрировать на примере рисования графика функции (рис. 5).
Рис. 5. Декартова и графическая системы координат
Пусть XOY – исходная декартова система координат (рис. 5, слева), а X'O'Y' – система координат графического экрана (рис. 5, справа). Пусть в случае с функцией в системе XOY Y1 – максимальное значение функции f(x) на отрезке [X1,X2], а Y2 – минимальное значение функции f(x) на этом отрезке. В системе координат X'O'Y' для рисования графика функции используем часть прямоугольной области [I1,I2]х[J1,J2], где [I1,I2] – интервал по оси O'X', [J1,J2] – интервал по оси O'Y'. Тогда для расчета абсциссы X* из XOY в системе X'O'Y' необходимо проделать вычисления:
X** = I1 + Trunc((I2-I1) * (X*-X1) / (X2-X1)).
Аналогично выглядит выражение для ординаты Y* из XOY в системе X'O'Y':
Y** = J1 + Trunc((J2-J1) * (Y*-Y1) / (Y2-Y1)).
Запишем функции для проведения указанных вычислений:
function scr_x(x: real): integer;
begin scr_x := I1 + Trunc((x-x1) * (I2-I1) / (X2-X1)) end;
function scr_y(y: real): integer;
begin scr_y := J1 + Trunc((y-Y1) * (J2-J1) / (Y2-Y1)) end;
Покажем работу рассмотренного инструментария на конкретном примере.
Пусть требуется представить в виде круговой диаграммы процентное соотношение выпущенной за день продукции цеха кондитерской фабрики (в килограммах). Используем комбинированный тип данных:
type ValType=record Val: real; //показатель
Name: string; //наименование
MyColor: TColor; //цвет сектора
end;
В программу включим следующие переменные:
Var I1,J1,I2,J2: integer;
x1,x2,y1,y2: real;
Values: array of ValType;
Зададим значения для параметров задачи:
// Отрезки в системе координат X’O’Y’:
with Image1 do //область рисования на экране:
begin
I1 := 10; I2 := width - 10; J1 := 10; J2 := height - 10
end;
// Отрезки в системе координат XOY:
x1 := -1; x2 := 1; y1 := 1; y2 := -1;
Зададим исходные данные:
SetLength(Values,5);
Values[0].Val := 250; Values[0].Name := 'Пряники мятные';
Values[1].Val := 510; Values[1].Name := ’Печенье "К чаю"’;
Values[2].Val := 670; Values[2].Name := 'Зефир в шоколаде';
Values[3].Val := 120; Values[3].Name := 'Пастила ванильная';
Values[4].Val := 320; Values[4].Name := 'Хлебцы овсяные';
// Цвета секторов диаграммы формируем случайным образом:
randomize;
for i := 0 to nCount - 1 do
Values[i].MyColor := RGB(random(240) + 10,
random(240) + 10,random(240) + 10);
Собственно рисование диаграммы реализует подпрограмма Sectors().
Её вызов имеет вид: Sectors(Values);
Приведем текст подпрограммы.
procedure Sectors(Values: array of ValType);
var R,Ax,Ay,Bx,By: real;
alpha: real;
i,y_start: integer;
sum_val: real;
WorkValues: array of real;
WorkNCount: integer;
begin
WorkNCount := 0; SetLength(WorkValues, WorkNCount);
WorkNCount := Length(Values); SetLength(WorkValues, WorkNCount);
//Image1 – область для рисования диаграммы
with Form1,Image1,Canvas do
begin
Brush.Color := clCream; Pen.Color := clNavy;
Rectangle(0,0,width - 1,height - 1)
end;
//Перевод значений Values[i].Val в градусы:
sum_val := 0;
for i := 0 to WorknCount - 1 do
sum_val := sum_val + Values[i].Val;
if sum_val < 1E-5 then
begin
ShowMessage('Нет данных для построения диаграммы...'); Exit
end;
//От абсолютных величин Values[i].Val
//переходим к относительным величинам:
//1) к долям:
for i := 0 to WorknCount - 1 do
WorkValues[i] := Values[i].Val / sum_val;
//2) к градусам и далее – к радианам:
for i := 0 to WorknCount - 1 do
WorkValues[i] : = WorkValues[i] * 360 / 180 * pi;
R := (x2 - x1) / 2; //R - радиус
Bx := x2; By := (y1 + y2) / 2; alpha := 0;
for i := 0 to WorknCount - 1 do
begin
//накапливаем углы:
alpha := alpha+WorkValues[i];
Ax := Bx; Ay := By; //(Ax,Ay) – конечная точка
Bx := R * cos(alpha); By := R * sin(alpha); //(Bx,By) – конечная точка
if Values[i].Val > 1E-5 then //не ноль?
with Form1,Image1,Canvas do
begin
Brush.Style := bsSolid;
Brush.Color := Values[i].MyColor;
Pie(scr_x(x1),scr_y(y1), scr_x(x2), scr_y(y2),
scr_x(Ax), scr_y(Ay), scr_x(Bx), scr_y(By))
end
end;
with Form1, Image1, Canvas do
begin
Brush.Color := clCream; Pen.Color := clNavy;
Ellipse(scr_x((x1+x2)/2)-(I2-I1) div 10, scr_y((y1+y2)/2)-(J2-J1) div 10,
scr_x((x1+x2)/2)+(I2-I1) div 10, scr_y((y1+y2)/2)+(J2-J1) div 10)
end;
//Image2 – область для вывода легенды
//Image2 можно поместить в контейнер ScrollBox.
with Form1.Image2 do
begin
top := 5; Left := 2; y_start := Top + 2;
//высота строки легенды - 20 пикселей:
Height := 20 * WorknCount + 10; width := Form1.ScrollBox1.width - 24;
with Canvas do
begin
Font.Name := 'Comic Sans MS'; Font.Size := 10;
Font.Color := clNavy; Pen.Color := clNavy;
Brush.Color := clCream; Rectangle(0, 0, width-1, height-1);
for i := 0 to WorknCount - 1 do
begin
Pen.Color := clNavy; Brush.Color := Values[i].MyColor;
Rectangle(left + 2, y_start, left + 22, y_start + 15);
Brush.Color := clCream;
TextOut(left+27, y_start-2,'-'+Values[i].Name+' '+
FloatToStrF(Values[i].Val / sum_val * 100, ffFixed, 5, 1) + '%');
y_start := y_start + 20 //высота строки легенды - 20 пикселей
end
end
end
end;
После вызова подпрограммы Sectors() освобождаем память:
SetLength(Values, 0);
На рис. 6 показан результат работы программы.
Заметим, что теперь он не зависит ни от размеров прямоугольной области, ни от её местонахождения на форме приложения (см. рис. 7, 8).
Рис. 6. Результат работы программы на языке Delphi
Рис. 7. Изменение размеров областей рисования
Рис. 8. Изменение конфигурации областей рисования
При программировании этой же задачи на языке C# следует учесть особенности описания стандартных подпрограмм DrawPie() и FillPie().
Приведем описание одной из них:
Из описания видно, что первый параметр отвечает за перо, которым будет прорисован контур сектора. Следующие четыре параметра отвечают за прямоугольную область, в которой будет изображен сектор, причем последние два из них определяют не точку, а ширину области (width) и ее высоту (height). Последние два параметра в списке задают начальный угол (в градусах) и приращение угла (в градусах).
Простой пример демонстрирует возможности подпрограмм DrawPie() и FillPie() языка C# [1].
Описание переменных:
Pen MyPen;
Brush MyBrush;
Graphics g;
Фрагмент кода программы:
g = pictureBox1.CreateGraphics();
g.Clear(Color.FromArgb(255, 255, 112));
g.DrawRectangle(new Pen(Brushes.Green, 1), 1, 1,
pictureBox1.Width - 2, pictureBox1.Height - 2);
MyPen = new Pen(Brushes.Navy, 1);
MyBrush = new SolidBrush(Color.SlateBlue);
g.FillPie(MyBrush, 1, 1, pictureBox1.Width-2, pictureBox1.Height-2, 0, 90);
g.DrawPie(MyPen, 1, 1, pictureBox1.Width-2, pictureBox1.Height-2, 0, 90);
Область для рисования определяем с помощью переменной g класса Graphics и стандартного компонента pictureBox1 класса PictureBox. Результат рисования показан на рис. 9. Отметим еще одно важное отличие от предыдущего примера: сектор рисуется по часовой стрелке.
Рис. 9. Рисование сектора
Теперь приведем текст основной части программы для рисования диаграммы.
namespace PieDiagram
{
public partial class Form1 : Form
{
private int nCount;
private double x1, y1, x2, y2, sum_val;
private int I1, J1, I2, J2, n, y_start;
List<ValType> Values = new List<ValType>(0);
Random MyRandom = new Random();
Pen MyPen;
Brush MyBrush;
Graphics g, gg;
public Form1()
{
InitializeComponent();
I1 = 4; J1 = 4; I2 = pictureBox1.Width - 8; J2 = pictureBox1.Height - 8;
x1 = -1; x2 = 1; y1 = 1; y2 = -1;
}
public class ValType
{
public string Name;
public Double Val;
public Color MyColor;
}
public Color genRandomColor()
{
int red = MyRandom.Next(156) + 100;
int green = MyRandom.Next(156) + 100;
int blue = MyRandom.Next(156) + 100;
return Color.FromArgb(red, green, blue);
}
private int x_screen(double x)
{ return I1 + (int)((x - x1) * (I2 - I1) / (x2 - x1)); }
private int y_screen(double y)
{ return J1 - (int)((y - y1) * (J2 - J1) / (y1 - y2)); }
private void Sectors(List<ValType> Values, int nCount)
{
double Ax, Ay, Bx, By, alpha;
double[] WorkValues;
WorkValues = new double[nCount]; sum_val = 0;
for (int j = 0; j < nCount; j++) { sum_val += Values[j].Val; }
if (sum_val < 1E-5) { MessageBox.Show(“Нет данных…”); return;}
for (int j = 0; j < nCount; j++)
{
WorkValues[j] = Values[j].Val / sum_val * 360;
}
Bx = x2; By = 0; alpha = 0;
MyPen = new Pen(Brushes.Green, 1);
for (int j = nCount-1; j >=0; j--)
{
Ax = Bx; Ay = By;
if (Values[j].Val > 1E-5)
{
MyBrush = new SolidBrush(Values[j].MyColor);
g.FillPie(MyBrush, I1, J1, I2, J2, (int)Math.Truncate(alpha),
(int)Math.Round(WorkValues[j] + 0.5));
g.DrawPie(MyPen, I1, J1, I2, J2, (int)Math.Truncate(alpha),
(int)Math.Round(WorkValues[j] + 0.5));
alpha += WorkValues[j];
}
}
g.FillEllipse(new SolidBrush(Color.MintCream),
(int)((I1 + I2) / 2.0) - (int)((I2 - I1) / 10.0),
(int)((J1 + J2) / 2.0) - (int)((J2 - J1) / 10.0),
2 * (int)((I2 - I1) / 10.0), 2 * (int)((J2 - J1) / 10.0));
g.DrawEllipse(new Pen(Brushes.Navy),
(int)((I1 + I2) / 2.0) - (int)((I2 - I1) / 10.0),
(int)((J1 + J2) / 2.0) - (int)((J2 - J1) / 10.0),
2*(int)((I2 - I1) / 10.0), 2*(int)((J2 - J1) / 10.0));
}
private void DrawLegend()
{
gg.Clear(Color.FromArgb(255, 255, 112));
gg.DrawRectangle(new Pen(Brushes.Green, 1), 1, 1,
pictureBox2.Width - 2, pictureBox2.Height - 2);
y_start = pictureBox2.Top + 2;
MyPen = new Pen(Brushes.Green, 1);
for (int j = 0; j < nCount; j++)
{
gg.FillRectangle(new SolidBrush(Values[j].MyColor),
pictureBox2.Left + 2, y_start, 20, 15);
gg.DrawRectangle(MyPen, pictureBox2.Left + 2, y_start, 20, 15);
gg.DrawString("- " + Values[j].Name + " - " +
(Math.Round(Values[j].Val*100 / sum_val, 1)).ToString() + "%",
new Font("Tahoma", 10), new SolidBrush(Color.Black),
pictureBox2.Left + 27, y_start);
y_start += 20;
}
}
private void button1_Click(object sender, EventArgs e)
{
Values.Clear();
Values.Add(new ValType());
Values[0].Val = 250.0; Values[0].Name = "Пряники мятные";
Values[0].MyColor = genRandomColor();
Values.Add(new ValType());
Values[1].Val = 510.0; Values[1].Name = "Печенье «К чаю»";
Values[1].MyColor = genRandomColor();
Values.Add(new ValType());
Values[2].Val = 670.0; Values[2].Name = "Зефир в шоколаде";
Values[2].MyColor = genRandomColor();
Values.Add(new ValType());
Values[3].Val = 120.0; Values[3].Name = "Пастила ванильная";
Values[3].MyColor = genRandomColor();
Values.Add(new ValType());
Values[4].Val = 320.0; Values[4].Name = "Хлебцы овсяные";
Values[4].MyColor = genRandomColor();
nCount = Values.Count();
pictureBox2.Width = flowLayoutPanel1.Width - 24;
pictureBox2.Height = 20 * nCount + 10;
pictureBox2.Top = 6; pictureBox2.Left = 6;
gg = pictureBox2.CreateGraphics();
g.Clear(Color.FromArgb(255, 255, 112));
g.DrawRectangle(new Pen(Brushes.Green, 1),
1, 1, pictureBox1.Width - 2, pictureBox1.Height - 2);
Sectors(Values, nCount); DrawLegend();
}
private void flowLayoutPanel1_Scroll(object sender, ScrollEventArgs e)
{ DrawLegend(); }
private void Form1_Load(object sender, EventArgs e)
{ g = pictureBox1.CreateGraphics(); }
}
}
Результат работы последней программы показан на рис. 10.
Рис. 10. Результат работы программы на C#
Замечание 1. Компонент для вывода легенды pictureBox2 (так же как и в предыдущем примере) помещен в контейнер flowLayoutPanel1.
Замечание 2. Так как секторы диаграммы (в отличие от предыдущего примера) выводятся по часовой стрелке, то с целью получения такого же результата в этой программе изменен порядок их отображения на обратный: от последнего сектора – к первому:
for (int j = nCount-1; j >=0; j--)
{ . . . }
При этом порядок вывода в легенду остался прежним: от первого описания продукции – к последнему.
Подведем итоги.
Во-первых, в рамках настоящего материала показано, что рисовать графические объекты на экране в абсолютных экранных координатах просто нерационально. Малейшие изменения в компоновке информации на экране могут привести к неоправданной потере времени на корректировку программы. Это относится как к Windows-приложениям, так и к web-приложениям.
Во-вторых, рассмотренный инструментарий может быть использован во многих других случаях: при рисовании графиков функций, линейных диаграмм, радиальных диаграмм и других графических объектов.
В-третьих, показано, что в разных языках программирования похожие графические примитивы могут отличаться с точки зрения организации их вызова, что требует от будущего программиста внимательного отношения к параметрам стандартных подпрограмм.