Zgodnie z wynikami wcześniejszych poszukiwań, do rysowania grafów w projekcie zamierzam używać biblioteki GraphX. Co prawda, jak słusznie zauważył w jednym z komentarzy Karol, istnieje bardzo ciekawa alternatywa. Syncfusion udostępnia bowiem swoje biblioteki za darmo w ramach licencji Community, dla projektów Open Source. A mają tam naprawdę mnóstwo świetnych kontrolek. I zapewne docelowo ich użyję - do zbudowania ładnego GUI. Jednak do rysowania grafów na razie wciąż spróbuję użyć GraphX, bo wydaje mi się bardziej zoptymalizowany do rysowania dużych zbiorów danych.
Na początku optymistyczne spróbowałem użyć tych kontrolek od razu w projekcie F#. Jednak odbiłem się od ściany licznych mniejszych lub większych problemów. Musiałem zatem zrobić krok wstecz i przygotować minimalny, działający przykład w starym, dobrym C#. W tym celu odchudziłem jak się tylko dało przykład .\GraphX\Examples\SimpleGraph i stworzyłem nowy projekt WPF. Jak zatem używa się GraphX?
Po dodaniu paczki NuGetowej o nazwie GraphX, jesteśmy gotowi do dodania kontrolki w widoku:
<Window x:Class="WpfGraphX.MainWindow"
...
xmlns:controls="http://schemas.panthernet.ru/graphx/"
xmlns:views="clr-namespace:WpfGraphX.Views">
<Grid>
<controls:ZoomControl x:Name="zoomctrl" Grid.Row="1">
<views:GraphView x:Name="Area"/>
</controls:ZoomControl>
</Grid>
</Window>
ZoomControl to wygodna kontrolka oferująca funkcjonalność nawigacji po grafie, więc postanowiłem jej nie wyrzucać z przykładu. Ponadto musimy teraz zdefiniować sporo własnych typów, odpowiadających za reprezentowanie naszego grafu. Po pierwsze, powyżej widoczny GraphView:
public class GraphView : GraphArea<DataVertex,
DataEdge,
BidirectionalGraph<DataVertex, DataEdge>>
{
}
Gdzie GraphArea jest kontrolką (dziedziczącą po Canvas) odpowiedzialną za rysowanie. Jak widać, definiujemy ją podając typy reprezentujące węzły oraz krawędzie grafu. Oraz definiując graf jako dwukierunkowy - klasa BidirectionalGraph jest zdefiniowana w zależnym module QuickGraph.
Same klasy węzłów i krawędzi są proste na potrzeby przykładowej aplikacji:
public class DataVertex : VertexBase
{
public DataVertex()
{
}
public DataVertex(string text)
{
Text = text;
}
public string Text { get; set; }
public override string ToString()
{
return Text;
}
}
public class DataEdge : EdgeBase<DataVertex>
{
public DataEdge(DataVertex source, DataVertex target, double weight = 1)
: base(source, target, weight)
{
}
public DataEdge()
: base(null, null, 1)
{
}
public string Text { get; set; }
public override string ToString()
{
return Text;
}
}
Teraz już możemy zainicjalizować graf. Uwaga - na razie wydaje mi się, że GraphX jednak nie wspiera ładnego bindowania danych. Na chwilę obecną widzę to tak, że graf musimy sobie "wyrzeźbić" w Code Behind. Tak przynajmniej to na razie wygląda w moim przykładzie:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SetupGraph();
GraphArea.GenerateGraph(true, true);
GraphArea.ShowAllEdgesArrows(true);
GraphArea.ShowAllEdgesLabels(true);
zoomctrl.ZoomToFill();
}
private void SetupGraph()
{
var graphLogic = new GraphLogic() { Graph = GenerateGraph() };
GraphArea.LogicCore = graphLogic;
}
private BidirectionalGraph<DataVertex, DataEdge> GenerateGraph()
{
var dataGraph = new Graph();
var vertex1 = new DataVertex("V1");
var vertex2 = new DataVertex("V2");
dataGraph.AddVertex(vertex1);
dataGraph.AddVertex(vertex2);
var dataEdge = new DataEdge(vertex1, vertex2) { Text = "Hello" };
dataGraph.AddEdge(dataEdge);
return dataGraph;
}
}
Powyższy kod właściwie sam się opisuje, aczkolwiek pewna niespodzianka się w nim kryje. Chodzi o właściwość
LogicCore, której podajemy obiekt implementujący "logikę grafu", czyli wszystko to co odpowiada za sposób jego "obliczania": algorytmy layoutu, sposobu traktowania krawędzi itd. W przykładzie posługuję się klasą
GXLogicCore, implementującą najwyraźniej wszystko co potrzeba "z pudełka":
public class GraphLogic : GXLogicCore<DataVertex,
DataEdge,
BidirectionalGraph<DataVertex, DataEdge>>
{
}
Po wykonaniu powyższego kodu wszystko zadziała elegancko i naszym oczom ukaże się następujące okienko:
Wszystko świetnie, tylko niestety, tak jak już wspominałem, przeniesienie wprost tego kodu do projektu w F# sprawia sporo problemów. Mówiąc prościej – nie działa i wciąż nie wiem czemu. Jak już mi się to uda, to z miłą chęcią napiszę o tym kolejny wpis…