вторник, 25 ноября 2014 г.

Построение графов при помощи NodeXL

В связи с производственной необходимостью, возникла потребность в компоненте для построения графов. Сходу были найдены вот эти три проекта:
http://graphx.codeplex.com/
http://graphsharp.codeplex.com/
http://nodexl.codeplex.com/
Т.к. по картинкам мне больше понравился третий, то его и пробовал. Он оказался неплох. Поэтому под катом рассказ о том, как при помощи NodeXL строить графики в WPF приложениях.

На всякий пожарный ссылка на скачивание. Качаем, распакуем, присоединяем dll-ки в проект:
Все, можно начинать использовать.
Для показа графа используется контрол NodeXLControl из пространства имен Smrf.NodeXL.Visualization.Wpf. Можно его как добавить через XAML, так и создать из кода и поместить в какой-нибудь контейнер. Дополнительных настроек не требуется.
Для данной статьи, я создал пустой WPF проект и на главную форму поместил вот такую разметку:

<Window x:Class="WpfApplication9.MainWindow"
        xmlns:node="clr-namespace:Smrf.NodeXL.Visualization.Wpf;assembly=Smrf.NodeXL.Control.Wpf"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <node:NodeXLControl x:Name="nxGraph" />
    </Grid>
</Window>

Ну а теперь, собственно построение графа. Информация о графе собрана в свойстве с говорящим именем Gpaph. Ну а вершины, соответственно, в свойстве Vertices графа. Чтобы постоянно не писать весь путь с имени контрола, можно это свойство скопировать в переменную и работать с ней:

IVertexCollection oVertices = nxGraph.Graph.Vertices;

Интерфейс у компонента достаточно понятный, например, добавление вершин осуществляется методом Add возвращающем ссылку на свежесозданную вершину:

IVertex first = oVertices.Add();
 
А вот дальше начинается проблема из-за желания разработчика сделать все максимально универсально. Настройка свойств вершины идет через метод SetValue первым параметром в который необходимо передавать строку с именем настраиваемого свойства. Сильно радует наличие предопределенного класса ReservedMetadataKeys с перечнем этих строковых значений в виде readonly полей. Т.е. настройка вершины имеет вид:

// Цвет вершины
first.SetValue(ReservedMetadataKeys.PerVertexLabelFillColor, Color.FromArgb(255, 255, 255, 11));
// Тип отображения
first.SetValue(ReservedMetadataKeys.PerVertexShape, VertexShape.Label);
// Текст вершины
first.SetValue(ReservedMetadataKeys.PerVertexLabel, "Первая"); 
Да, вы правильно поняли, что второй параметр метода SetValue типа object и во многих случаях придется  угадывать какого типа он должен быть реально по замыслу разработчиков.
Ну и еще пара вершин оформленных по другому:

IVertex second = oVertices.Add();
second.SetValue(ReservedMetadataKeys.PerColor, Color.FromArgb(255, 255, 0, 255));
// Задаем радиус вершины
second.SetValue(ReservedMetadataKeys.PerVertexRadius, 20F);
// И говорим что вершина - шарик
second.SetValue(ReservedMetadataKeys.PerVertexShape, VertexShape.Sphere);
IVertex third = oVertices.Add();
// Здесь мы тоже говорим, что сфера
third.SetValue(ReservedMetadataKeys.PerVertexShape, VertexShape.Sphere);
// Но задаем надпись
third.SetValue(ReservedMetadataKeys.PerVertexLabel, "Label");
Ок, давайте запустим наше приложение. Но перед этим вызовем метод отрисовки графа:

nxGraph.DrawGraph(); 
Вот так это выглядит:
Мы можем задавать расположение вершин принудительно.  У каждой вершины есть свойство Location, вот только проблема, это свойство из библиотеки WinForms. Нет, мы можем подключить бибилотеку System.Drawing.dll и написать что-нибудь вида:

third.Location = new System.Drawing.PointF(60, 60);

И оно даже заработает:
Но в большинстве случаев, мы можем просто при построении графа довериться авторасположению вершин:

nxGraph.DrawGraph(true);

Выглядит:
Только нужно не забывать, что при выборе такого способа все установленные вручную Location будут игнорироваться.
Хорошо, вершины разместили, переходим к ребрам. Список всех ребер лежит в свойстве Edges уже упоминавшегося свойства Graph:

IEdgeCollection oEdges = nodeXLControl.Graph.Edges;

Работа с ребрами очень похожа на работу с вершинами, только при создании ребра мы передаем две вершины которые ребро будет соединять и признак направленности ребра:

IEdge oEdge1 = oEdges.Add(first, second, true);
Ну а свойства уже по привычной схеме:

// Цвет
oEdge1.SetValue(ReservedMetadataKeys.PerColor, Color.FromArgb(255, 55, 125, 98));
// Толщина
oEdge1.SetValue(ReservedMetadataKeys.PerEdgeWidth, 3F);
// Подпись
oEdge1.SetValue(ReservedMetadataKeys.PerEdgeLabel, "Из первой во вторую");
Аналогично для остальных вершин:

// Первую и третью вершину соеденит ненаправленное ребро
IEdge oEdge2 = oEdges.Add(first, third, false);
// Цвет
oEdge1.SetValue(ReservedMetadataKeys.PerColor, Color.FromArgb(255, 55, 125, 98));
// Вторую с третьей - направленное
IEdge oEdge3 = oEdges.Add(second, third, true);
// Линия будет штрих-пунктирная
oEdge3.SetValue(ReservedMetadataKeys.PerEdgeStyle, EdgeStyle.DashDotDot);
Смотрится симпотично:
Компонент представляет возможность группировать вершины, но смотрится это не фонтан, т.к. все связи к сгруппированным вершинам отображаются. Но давайте покажу.

// Создаем группу, по умолчанию свернутую (второй параметр)
GroupInfo oGroup = new GroupInfo("GroupFirstAndSecond"true"Тут две вершины");
// Вершины объединенные группой
oGroup.Vertices.AddFirst(first);
oGroup.Vertices.AddLast(second);
// Добавляем группу в граф
nxGraph.Graph.SetValue(ReservedMetadataKeys.GroupInfo, new GroupInfo[] { oGroup });
Обратили внимание на признак свернутости? Так вот, он игнорируется... Если сейчас запустить, то мы увидим все тоже самое, что и на предыдущей картинке.
Поэтому предлагаю перейти к событиям и на них покажу работу с группами.
Событий у вершин, групп и т.д. нет. Все события собраны в контроле. Например, чтобы обрабатывать клик на первой или второй вершине со сворачиванием группы мне придется подписаться на событие:

nxGraph.VertexClick += nxGraph_VertexClick;
И в обработчике добавить проверку:

void nxGraph_VertexClick(object sender, VertexEventArgs vertexEventArgs)
{
    if (vertexEventArgs.Vertex.ID < 3) // Первая или вторая вершина
    {
        Dispatcher.BeginInvoke((Action)(() => nxGraph.CollapseGroup("GroupFirstAndSecond"true)));
    }
    if (vertexEventArgs.Vertex.ID == 3)
    {
        nxGraph.ExpandGroup("GroupFirstAndSecond"true);
    }
}
Вызов через Dispatcher вынужденная мера, т.к. после сворачивания вершины из графа скрываются и обработка клика падает.
Вот так выглядит граф после сворачивания двух вершин:
На сегодня все. Может в следующий раз покажу какой-нибудь пример приближенный к реальности на основе этого контрола.

Комментариев нет:

Отправить комментарий