Temat: Nowy interfejs R -> .NET
Dziś bawiłem się w skalowanie obrazków.
Najpierw tworzymy metodę, która wyśle do R skrypt generujący potrzebny wykres i zapisujący go do pliku we wskazanym katalogu. Ponieważ zapisujemy go do PNG, można ustawić przezroczyste tło, co pozwoli lepiej dopasować wykres do naszych schematów kolorystycznych.
Z racji tego, że generujemy skrypt tekstowy, możemy go dowolnie parametryzować sklejając po prostu literały. Niestety, w pewnym momencie możemy "stracić kontrolę" nad tymi wszystkimi sklejeniami i całość będzie trudna do ogarnięcia, nie mówiąc o późniejszych poprawkach (widać to już w kodzie przykładowym poniżej). Należy zdefiniować klasę, która będzie wystawiać właściwości (panujemy nad typami), enumeracje (kolory, grubości linii, wzorki, nazwy czcionek, etc.), a w gdzieś w środku będzie metoda generateScript składająca całość w skrypt R. I warto robić to krok po kroku, wykorzystując zasadę "jedno zadanie jedna metoda". Innymi słowy metoda appendAndFormatTitle(string title, FontNames fontName, ColorNames colorName) zajmuje się tytułem i niczym więcej.
Polecenia powinny być od siebie oddzielone średnikami bądź znakami nowej linii (i lepiej niech to będzie
System.Environment.NewLine).
Pamiętamy, że przekazując ścieżki do R musimy użyć konstrukcji
@"c:\\", a nie
"c:\\, bądź
@"c:\".
Po wykonaniu kodu R, we wskazanym katalogu pojawi się bitmapka, którą trzeba załadować i podać jako źródło do własności "Image" jakiejś kontrolki (w przykładzie - PictureBox).
Tu pojawia się problem. Otóż aplikacja, poprzez klasę Bitmap, trzyma własność pliku i R nie da rady go nadpisać przy kolejnym wykonaniu skryptu. Nie wyrzuci jednak błędu, pozornie wszystko będzie działać, a obrazek nie będzie odświeżany. Radzimy sobie z tym najpierw wczytując bitmapkę do strumienia, którym następnie zasilamy obiekt klasy Bitmap a na koniec strumień zamykamy. Dzięki temu dostęp do pliku zostaje zwolniony i wszystko gra.
void drawChart(REngine engine)
{
StringBuilder plotCommmand = new StringBuilder();
plotCommmand.Append(@"CairoPNG('c:\\tmp\\r.png', width=" + this.pictureBox1.Width.ToString() + ", height=" + this.pictureBox1.Height.ToString() + ", bg='transparent');");
plotCommmand.Append("plot(1:" + (this.pictureBox1.Width / 10).ToString() + ",col='blue', main='Rozmiary wykresu: "+ this.pictureBox1.Width.ToString() + " x " + this.pictureBox1.Height.ToString() + "');");
plotCommmand.Append("graphics.off();");
Console.WriteLine(plotCommmand.ToString());
engine.EagerEvaluate(plotCommmand.ToString());
using (System.IO.StreamReader str = new System.IO.StreamReader("c:\\tmp\\r.png"))
{
this.pictureBox1.Image = new Bitmap(str.BaseStream);
str.Close();
}
this.pictureBox1.Invalidate();
}
Teraz zajmujemy się zmianą rozmiaru kontrolki. Kontrolka powinna być zadokowana, albo mieć odpowiednio zakotwiczona. Można iść na łatwiznę i napisać od razu:
private void Form1_Resize(object sender, EventArgs e)
{
drawChart(this.rEngine);
}
i nawet będzie to działać. Po co jednak zarzynać procesor generowaniem obrazków i tworzeniem obiektów co kilka pikseli? Filmik pokazuje, ile bezsensownej roboty się dzieje (okno konsoli na dole ekranu). Jest to jednak niezły test wydajnościowy ;) Przy StatConnector możemy o zapomnieć o takiej szybkości reakcji. W rzeczywistości wszystko działa szybciej, nie mogłem ustawić framerate w kompresorze video...
http://www.youtube.com/watch?v=Rta2lw-Y53s
Zamiast tego, do rozmiarów okna wykorzystujemy zdarzenie
ResizeEnd. Pojawia się jednak kolejny problem- maksymalizacja i minimalizacja okna.
Zdarzenie ResizeEnd, nie wiedzieć czemu, nie jest odpalane po zakończeniu mini-/maksymalizacji okna. Reaguje zaś na to właśnie Resize. Stąd, do obsługi "krąwędziowej zmiany rozmiarów okna" stosujemy ResizeEnd, a do min/max okna - Resize z odrobiną logiki.
FormWindowState oldFormWindowState;
private void Form1_Resize(object sender, EventArgs e)
{
if (this.WindowState == FormWindowState.Maximized)
{
this.OnResizeEnd(null);
this.oldFormWindowState = FormWindowState.Maximized;
}
if (this.WindowState == FormWindowState.Normal && this.oldFormWindowState == FormWindowState.Maximized)
{
this.oldFormWindowState = FormWindowState.Normal;
this.OnResizeEnd(null);
}
}
private void Form1_ResizeEnd(object sender, EventArgs e)
{
drawChart(this.rEngine);
}
I to wreszcie działa, jak należy, tj. płynnie i bez zarzynania komputera, co prezentuje poniższy filmik (od razu na fulscreen):
http://www.youtube.com/watch?v=K2h-wfR5txY
Przy okazji, pod koniec filmiku, widać także paskudną obsługę wyjątków R. Leci wyjątek ogólny ParseException, bez żadnego opisu, nawet bez treści wyjątku. Jest ona wyrzucana dopiero na konsolę:
Error in plot.new() : figure margins too large.
Wyjątki w R rzuca funkcja
stop(...):
> funkcja_z_bledem <- function() {
+ stop("Błąd!");
+ }
> funkcja_z_bledem()
Error in funkcja_z_bledem() : Błąd!
>
Na koniec - eRowy smaczek :)
Przypominam, że w R świetnie współpracuje z Latexem (pakiet sweave) i potrafi tworzyć PDF oraz DVI. Można zatem generować PDFy (i DVI) i wczytywać je do osadzonej w aplikacji kontrolki przeglądarki*
----------
* .NET posiada wrapper do IE, ale jest też dostępny silnik Gecko, a więc Mozilla (i działa pod MONO). Testowałem to jeszcze w 2010 i działało OK. Niestety, w tym roku Mozilla ogłosiła, że prace nad osadzalną kontrolką zostają wstrzymane:
http://lwn.net/Articles/436412/