Разрабатывается приложение, воспроизводящее граф по его xml-описанию. Первоначально разрабатывается xml-описание графа, а затем создается приложение, позволяющее выбрать xml-файл и построить граф по данным этого файла. Отображенный граф можно переместить по горизонтали и вертикали. Так же в нем можно изменить цвет вершин и ребер, размер вершин и ширину ребер.
В качестве примера приводится xml-описание показанного на рис. 1 графа.
Рис. 1. Граф для демонстрации структуры xml-описания
<?xml version="1.0" encoding="utf-8" ?>
<graph>
<!-- Graph vertices -->
<!-- Vertex name and number -->
<!-- The coordinates of the vertex -->
<verts>
<graphVertex vName = "v1" vNumber = "1">
<x>125</x>
<y>200</y>
</graphVertex>
<graphVertex vName = "v2" vNumber = "2">
<x>125</x>
<y>175</y>
</graphVertex>
<graphVertex vName = "v3" vNumber = "3">
<x>50</x>
<y>150</y>
</graphVertex>
<graphVertex vName = "v4" vNumber = "4">
<x>200</x>
<y>150</y>
</graphVertex>
<graphVertex vName = "v5" vNumber = "5">
<x>125</x>
<y>100</y>
</graphVertex>
<graphVertex vName = "v6" vNumber = "6">
<x>50</x>
<y>50</y>
</graphVertex>
<graphVertex vName = "v7" vNumber = "7">
<x>200</x>
<y>50</y>
</graphVertex>
</verts>
<!-- Graph edges -->
<!-- Edge start vertex -->
<!-- Edge end vertex -->
<edge eName = "1">
<start>1</start>
<end>2</end>
</edge>
<edge eName = "2">
<start>2</start>
<end>3</end>
</edge>
<edge eName = "3">
<start>2</start>
<end>4</end>
</edge>
<edge eName = "4">
<start>2</start>
<end>5</end>
</edge>
<edge eName = "5">
<start>5</start>
<end>6</end>
</edge>
<edge eName = "6">
<start>5</start>
<end>7</end>
</edge>
<!-- Projection matrix -->
<ortho>
<xmi>0</xmi>
<xma>220</xma>
<ymi>0</ymi>
<yma>220</yma>
</ortho>
</graph>
Узлы verts, edge и ortho, находящиеся на верхнем уровне, содержат соответственно описания вершин графа, его ребер и параметров матрицы проецирования.
Для вершины указывается ее имя, номер (соответственно атрибуты vName и vNumber) и ее координаты.
Для ребра - вершины, которые это ребро соединяет.
Для отображения вершин употребляются точки, а ребер - линии.
Язык программирования Visual C#, проект Window Forms Application. Графика реализована с помощью библиотек Tao.OpenGL.dll и Tao.Platform.Windows.dll. Эти библиотеки нетрудно найти в интернете.
Для подключения библиотек в Solution Explorer (рис. 2) добавляется ссылка (References - правая кнопка мыши Add Reference).
Рис. 2. Добавление ссылки
Выбираемые ссылки показаны на рис. 3.
Рис. 3. Выбор ссылок Tao.OpenGL.dll и Tao.Platform.Windows.dll
Область графического вывода добавляется в форму после выбора Tollbox - General - simpleOpenGlControl (рис. 4).
Рис. 4. Добавление в форму области графического вывода
В рассматриваемом проекте эта область имеет имя GR (рис. 5)
Рис. 5. Имя области графического вывода
Для работы с xml-файлом создается объект XmlDocument:
XmlDocument xDoc = new XmlDocument();
Выбора цвета (рис. 6) осуществляется средствами добавленного в проект объекта ColorDialog.
Рис. 6. Выбираем цвет для glColor3f
Объект ColorDialog добавляется в проект (размещается непосредственно под формой, рис. 7) в результате выполнения цепочки ToolBox - Dialogs - Timer. Меню ToolBox, если оно отсутствует, можно показать посредством меню VIEW - ToolBox, или нажав Ctrl+Alt+X.
Рис. 7. Добавление объекта ColorDialog
Xml-файл открывается при помощи объекта OpenFileDialog:
OpenFileDialog OPF = new OpenFileDialog();
При этом используется фильтр по расширению:
OPF.Filter = "xml-файлы (*.xml)|*.xml";
Позволяет выбрать файл с xml-описанием графа, задать размер и цвет вершин графа, ширину и цвет ребер графа, а также переместить граф по горизонтали и вертикали (рис. 8).
Рис. 8. Интерфейс пользователя
Полный код формы приложения:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
using System.Xml;
// OpenGL
using Tao.OpenGl;
using Tao.Platform.Windows; // SimpleOpenGLControl
namespace GL_XML
{
public partial class Form1 : Form
{
// Параметры матрицы проецирования
double xmi = 0, xma = 0, ymi = 0, yma = 0;
double rV = 1, gV = 0, bV = 0; // Начальный цвет вершин красный
double rD = 0, gD = 1, bD = 0; // Начальный цвет ребер зеленый
string fl = ""; // Имя файла
// Предельные значения для числа вершин и ребер графа
public static int numberOfVerts = 100;
public static int numberOfEdges = 100;
// Массивы атрибутов вершин и ребер графа
string[,] attrValVrts = new string[2, numberOfVerts];
string[] attrValDgs = new string[numberOfEdges];
int nVrts = 0, nDgs = 0; // Число вершин и ребер в графе
// Массивы вершин, их координат и ребер в графе
string[] vN = new string[100];
double[,] xy = new double[2, 100]; // Массив координат вершин графа в мировой системе координат
double[,] xy2 = new double[2, 100]; // Массив координат вершин графа в видовой системе координат
string[,] vE = new string[2, 100];
public int kV = -1; // kV - индекс выбранной вершины графа
public Form1()
{
InitializeComponent();
GR.InitializeContexts();
colorDialog1.FullOpen = true;
//colorDialog1.Color = Color.Black; // Начальный цвет colorDialog
}
// Загрузка области графического вывода
private void GR_Load(object sender, EventArgs e)
{
Gl.glClearColor(1, 1, 1, 1);
Gl.glClear(Gl.GL_COLOR_BUFFER_BIT); // Очистка буфера цвета
Gl.glEnable(Gl.GL_POINT_SMOOTH);
}
// Подготовка в выводу изображения
private void stRth()
{
Gl.glClearColor(1, 1, 1, 1);
Gl.glClear(Gl.GL_COLOR_BUFFER_BIT); // Очистка буфера цвета
Gl.glMatrixMode(Gl.GL_PROJECTION); // Текущей стала матрица проецирования
Gl.glLoadIdentity();
Gl.glOrtho(xmi, xma, ymi, yma, -1, 1); // Прямоугольное проецирования
Gl.glMatrixMode(Gl.GL_MODELVIEW); // Текущей стала видовая матрица
Gl.glLoadIdentity();
Gl.glTranslated((double)DX.Value, (double)DY.Value, 0); // Матрица перемещения графа
}
// Рисует ребра графа
private void drwDgs()
{
double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
Gl.glLineWidth((float)LW.Value); // Ширина ребра
Gl.glColor3d(rD, gD, bD); // Цвет ребер
Gl.glBegin(Gl.GL_LINES);
for (int i = 0; i <= nDgs; i++)
{
for (int k = 0; k <= nVrts; k++)
{
if (vN[k] == vE[0, i])
{
x1 = xy[0, k];
y1 = xy[1, k];
}
if (vN[k] == vE[1, i])
{
x2 = xy[0, k];
y2 = xy[1, k];
}
}
Gl.glVertex2d(x1, y1);
Gl.glVertex2d(x2, y2);
}
Gl.glEnd();
}
// Рисует вершины графа
private void drwVrts()
{
Gl.glPointSize((float)PS.Value); // Размер вершины
Gl.glColor3d(rV, gV, bV); // Цвет вершин
Gl.glBegin(Gl.GL_POINTS);
double cV = 0.7;
for (int i = 0; i <= nVrts; i++)
{
if (i == kV) // kV - индекс выбранной вершины графа
Gl.glColor3d(cV, cV, cV); // Цвет выбранной вершины (серый)
else
Gl.glColor3d(rV, gV, bV); // Цвет прочих вершин
Gl.glVertex2d(xy[0, i], xy[1, i]);
}
Gl.glEnd();
}
// Кнопка Файл. Формруем массивы вершин, ребер и координат вершин
private void FileLoad()
{
nVrts = -1;
nDgs = -1;
OpenFileDialog OPF = new OpenFileDialog();
OPF.Filter = "xml-файлы (*.xml)|*.xml";
DialogResult dlg = OPF.ShowDialog(); // Выбираем xml-файл
if (dlg == DialogResult.OK)
{
fl = OPF.FileName;
label1.Text = "Выбран файл: " + fl;
XmlDocument xDoc = new XmlDocument();
xDoc.Load(fl);
// Корневой элемент xml-документа
XmlElement xRoot = xDoc.DocumentElement;
// Обходим узлы корневого элемента
foreach (XmlNode xnode in xRoot)
{
if (xnode.Name == "verts")
{
//nVrts = xnode.ChildNodes.Count; // Число вершин в графе
foreach (XmlNode childnode in xnode.ChildNodes)
{
XmlNode nmb = childnode.Attributes[1];
nVrts++;
// Сохраняем значения атрибутов вершины
attrValVrts[0, nVrts] = childnode.Attributes[0].Value;
attrValVrts[1, nVrts] = childnode.Attributes[1].Value;
vN[nVrts] = nmb.Value;
xy[0, nVrts] = Single.Parse(childnode.ChildNodes[0].InnerText);
xy[1, nVrts] = Single.Parse(childnode.ChildNodes[1].InnerText);
}
}
if (xnode.Name == "edge")
{
nDgs++;
// Сохраняем значение атрибута ребра
attrValDgs[nDgs] = xnode.Attributes[0].Value;
vE[0, nDgs] = xnode.ChildNodes[0].InnerText;
vE[1, nDgs] = xnode.ChildNodes[1].InnerText;
}
if (xnode.Name == "ortho")
{
xmi = Single.Parse(xnode.ChildNodes[0].InnerText);
xma = Single.Parse(xnode.ChildNodes[1].InnerText);
ymi = Single.Parse(xnode.ChildNodes[2].InnerText);
yma = Single.Parse(xnode.ChildNodes[3].InnerText);
}
}
graph();
//System.IO.StreamWriter txtFl = new System.IO.StreamWriter(@"C:\xy2.txt");
// xy - массив координат вершин графа в мировой системе координат
// xy2 - массив координат вершин графа в видовой системе координат
for (int k = 0; k <= nVrts; k++)
{
double x2 = xy[0, k] / xma * GR.Width;
double y2 = xy[1, k] / yma * GR.Height;
xy2[0, k] = x2;
xy2[1, k] = y2;
//txtFl.WriteLine("" + Math.Round(x2, 1) + "\t" + Math.Round(y2, 1));
}
//txtFl.Close();
}
}
// Кнопка Файл. Формируем массивы вершин, ребер и координат вершин
private void buttonFileLoad_Click(object sender, EventArgs e)
{
FileLoad();
}
private void graph()
{
if (String.IsNullOrEmpty(fl))
{
MessageBox.Show("Не выбран xml-файл");
return;
}
stRth(); // Подготовка к графическому выводу
drwDgs(); // Рисуем ребра
drwVrts(); // Рисуем вершины
Gl.glFlush(); // Отображаем примитивы на экране
GR.Invalidate();
}
// Изменен размер вершины
private void PS_ValueChanged(object sender, EventArgs e)
{
graph();
}
// Изменена ширина ребра
private void LW_ValueChanged(object sender, EventArgs e)
{
graph();
}
// Перемещение по X
private void DX_ValueChanged(object sender, EventArgs e)
{
graph();
}
// Перемещение по Y
private void DY_ValueChanged(object sender, EventArgs e)
{
graph();
}
// Диалог colorDialog выбора цвета
private void clrPck(ref double r, ref double g, ref double b)
{
if (colorDialog1.ShowDialog() == DialogResult.Cancel) return;
double c = 1.0f / 255.0f;
r = c * colorDialog1.Color.R;
g = c * colorDialog1.Color.G;
b = c * colorDialog1.Color.B;
}
// Цвет ребер
private void button2_Click_1(object sender, EventArgs e)
{
clrPck(ref rD, ref gD, ref bD);
graph();
}
// Цвет вершин
private void button3_Click_1(object sender, EventArgs e)
{
clrPck(ref rV, ref gV, ref bV);
graph();
}
private void button1_Click(object sender, EventArgs e)
{
Close();
}
// Обработчик перемещения мыши
private void GR_MouseMove(object sender, MouseEventArgs e)
{
double mX = e.X; // x-координата мыши
double mY = GR.Height - e.Y; // Высота окна вывода минус y-координата мыши
// Показываем координаты мыши в форме
labelMouse.Text = "Mouse: " + Convert.ToString(mX) + ", " + Convert.ToString(mY);
// kV - индекс выбранной вершины графа
if (kV > -1)
{
xy2[0, kV] = mX;
xy2[1, kV] = mY;
xy[0, kV] = mX / GR.Width * xma;
xy[1, kV] = mY / GR.Height * yma; ;
graph();
}
}
// Обработчик нажатия на левую кнопку мыши
private void GR_MouseClick(object sender, MouseEventArgs e)
{
double mX = e.X; // x-координата мыши
double mY = GR.Height - e.Y; // Высота окна вывода минус y-координата мыши
double dlt = 5.0; // Параметр критерия выбора вершины (в пикселях)
// Показываем координаты мыши в форме
labelMouse.Text = "Mouse: " + Convert.ToString(mX) + ", " + Convert.ToString(mY);
// kV - индекс выбранной вершины графа
if (kV > -1)
{
kV = -1; // kV равен -1, если нет выбранной вершины
}
else
{
kV = -1;
for (int k = 0; k <= nVrts; k++)
{
if (Math.Abs(xy2[0, k] - mX) < dlt && Math.Abs(xy2[1, k] - mY) < dlt)
{
kV = k;
break;
}
}
}
graph(); // Рисуем граф
}
// Добавляет дочерний элемент узла node с именем name и значением value типа double
private void AddChild(XmlDocument doc, XmlNode parent, string name, double value)
{
XmlNode child = doc.CreateElement(name);
child.InnerText = Convert.ToString(value);
parent.AppendChild(child);
}
// Добавляет дочерний элемент узла node с именем name и значением value типа string
private void AddChildStr(XmlDocument doc, XmlNode parent, string name, string value)
{
XmlNode child = doc.CreateElement(name);
child.InnerText = value;
parent.AppendChild(child);
}
// Добавляет атрибут узла node
private void AddAttr(XmlDocument doc, XmlNode node, String name, string value)
{
XmlAttribute attr = doc.CreateAttribute(name);
attr.Value = value;
node.Attributes.Append(attr);
}
// Формируем xml-документ
private void WriteXml(XmlDocument doc)
{
// Прежде сохраняем в xml параметры матрицы проецирования
XmlNode ortho = doc.CreateElement("ortho");
doc.DocumentElement.AppendChild(ortho);
AddChild(doc, ortho, "xmi", xmi);
AddChild(doc, ortho, "xma", xma);
AddChild(doc, ortho, "ymi", ymi);
AddChild(doc, ortho, "yma", yma);
// Сохраняем в xml сведения о графе
// Вершины
XmlNode verts = doc.CreateElement("verts");
doc.DocumentElement.AppendChild(verts);
for (int k = 0; k <= nVrts; k++)
{
// Создаем элемент с именем graphVertex
XmlNode vrt = doc.CreateElement("graphVertex");
verts.AppendChild(vrt);
// Формируем атрибуты элемента vrt
AddAttr(doc, vrt, "vName", attrValVrts[0, k]);
AddAttr(doc, vrt, "vNumber", attrValVrts[1, k]);
// Создаем дочерние элементы x и y и добавляем их к элементу vrt
AddChild(doc, vrt, "x", xy[0, k]);
AddChild(doc, vrt, "y", xy[1, k]);
}
// Ребра
for (int k = 0; k <= nDgs; k++)
{
// Создаем элемент с именем edge
XmlNode edge = doc.CreateElement("edge");
// Добавляем элемент к документу
doc.DocumentElement.AppendChild(edge);
AddAttr(doc, edge, "eName", attrValDgs[k]);
// Создаем дочерние элементы start и end и добавляем их к элементу edge
AddChildStr(doc, edge, "start", vE[0, k]);
AddChildStr(doc, edge, "end", vE[1, k]);
}
}
private void SaveXml(string fl)
{
XmlTextWriter textWriter = new XmlTextWriter(fl, Encoding.UTF8);
// Заголовок xml
textWriter.WriteStartDocument();
// Открывающий тэг
textWriter.WriteStartElement("graph");
// Закрывающий тэг
textWriter.WriteEndElement();
textWriter.Close();
// Создаем xml-документ
XmlDocument docXml = new XmlDocument();
docXml.Load(fl);
WriteXml(docXml);
// Сохраняем xml-документ в файле fl
docXml.Save(fl);
}
private void SaveFileToolStripMenuItem_Click(object sender, EventArgs e)
{
if (String.IsNullOrEmpty(fl))
{
MessageBox.Show("Не выбран xml-файл");
return;
}
SaveXml(fl);
}
private void SaveAsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (String.IsNullOrEmpty(fl))
{
MessageBox.Show("Не выбран xml-файл");
return;
}
SaveFileDialog OPF = new SaveFileDialog();
OPF.Filter = "xml-файлы (*.xml)|*.xml";
DialogResult dlg = OPF.ShowDialog();
if (dlg == DialogResult.OK)
{
fl = OPF.FileName;
SaveXml(fl);
label1.Text = "Граф сохранен в файле: " + fl;
}
}
private void closeToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private void newFileToolStripMenuItem_Click(object sender, EventArgs e)
{
FileLoad();
}
}
}
Добавленные в код обработчики событий мыши GR_MouseClick и GR_MouseMove позволяют выбрать вершину графа (GR_MouseClick) и перемещать ее вслед за мышиным курсором (GR_MouseMove) (рис. 9).
Рис. 9. Измененный граф
Предварительно при загрузке графа из файла создается массив xy2 видовых координат вершин (см. процедуру buttonFileLoad_Click). Начало видовой системы координат находится в левом нижнем углу окна вывода. В проекте окно OpenGL имеет имя GR.
При перемещении выделенной вершины в обработчике GR_MouseMove видовые координаты мыши и, следовательно, выделенной вершины преобразовываются в мировые. Граф перерисовывается.
После нажатия на левую кнопку мыши выделение вершины, если таковое было, снимается.
После доработки в форму было добавлено меню Файл (объект menuStrip1, рис. 10).
Рис. 10. Добавлено меню
Порядок добавления меню menuStrip1 иллюстрирует рис. 11.
Рис. 11. Выбор menuStrip в Toolbox
После введения подменю в форму появляется возможность формирования меню.
Каждому пункту подменю поставлена в соответствие процедура, исполняемая при ударе (Click) пункта мышкой.
Задание обработчика события Click подпункта Сохранить иллюстрирует рис. 12.
Рис. 12. Задание обработчика события Click
Введенные для сохранения графа процедуры см. в вышеприведенном коде программы.
Имеются недостатки, подлежащие устранению: