Kilka postów wcześniej, pokazałem jak narysować linię za pomocą RX oraz przechwytywania zdarzeń. Dla przypomnienia udało nam się narysować prostą linie z punktu (0,0) do aktualnej pozycji kursora:
public class MyCanvas : Canvas { private Point _endPoint; public MyCanvas() { var eventsSource = Observable.FromEventPattern<MouseEventArgs>(this, "MouseMove"). Where(e => e.EventArgs.LeftButton == MouseButtonState.Pressed). Select(e => e.EventArgs.GetPosition(this)); eventsSource.Subscribe(pos => { _endPoint = pos; InvalidateVisual(); }); } protected override void OnRender(DrawingContext dc) { base.OnRender(dc); dc.DrawLine(new Pen(Brushes.Black, 1), new Point(), _endPoint); } }
Następnym celem jest narysowanie linii od punktu gdzie użytkownik naciska lewy przycisk myszy do miejsca gdy zwolnienia myszkę. Dla uproszczenia, na razie nie będziemy rysować linii w czasie rzeczywistym tzn. kiedy użytkownik rusza myszką. Po prostu najpierw pobieramy punkt w zdarzeniu MouseDown, czekamy na MouseUp i po otrzymaniu sygnału rysujemy linie pomiędzy tymi dwoma punktami.
Rozwiązaniem jest użycie funkcji Zip, którą przedstawiałem w poprzednim poście:
var mouseDown = Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseDown"). Select(r => r.EventArgs.GetPosition(this)); var mouseUp = Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseUp"). Select(r => r.EventArgs.GetPosition(this)); mouseDown.Zip(mouseUp,(a,b)=>new{P1=a,P2=b}). Subscribe((v) => { start = v.P1; end = v.P2; InvalidateVisual(); })
Dlaczego nie CombineLatest? Jak wiemy, Zip czeka na drugą wartość. W praktyce oznacza to, gdy zostanie wywołane zdarzenie mouseDown, Zip będzie czekał aż do momentu gdy MouseUp jest wywołane. CombineLatest po prostu użyłoby ostatniego dostępnego zdarzenia (a nie tego po MouseDown), co oczywiście jest całkowicie błędne.
Nie jest jeszcze to idealne rozwiązanie. Przede wszystkim, gdy użytkownik zwolni myszkę za oknem, wtedy następna narysowana linia będzie nieprawidłowa. Powrócimy do tego na końcu wpisu ponieważ będzie potrzebne użycie funkcji TakeUntil oraz Repeat. Kolejnym krokiem jest zaimplementowanie rysowania w taki sposób, że linia jest renderowana już w momencie naciśnięcia przycisku. Innymi słowy, najpierw chcemy czekać na zdarzenie MouseDown, następnie połączyć je z aktualną pozycją kursora, a na końcu zakończyć zapytanie gdy zostanie wywołany MouseUp:
var mouseDown = Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseDown"). Select(r => r.EventArgs.GetPosition(this)); var mouseUp = Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseUp"). Select(r => r.EventArgs.GetPosition(this)); var mouseMove = Observable.FromEventPattern<MouseEventArgs>(this, "MouseMove"). Select(r => r.EventArgs.GetPosition(this)); mouseDown.CombineLatest(mouseMove, (a, b) => new { P1 = a, P2 = b }).TakeUntil(mouseUp).Repeat(). Subscribe((a) => { start = a.P1; end = a.P2; InvalidateVisual(); });
CombineLatest łączy zdarzenie zawierające pozycje kursora w momencie naciśnięcia przycisku (MouseDown,a) oraz również gdy użytkownik rusza myszą (MouseMove,b). Następnie chcemy te wartości przetwarzać aż do momentu MouseUp. Funkcja TakeUntil publikuje wartości aż do momentu zdarzenia przekazanego jako parametr – w tym przypadku jest to MouseUp. W momencie gdy MouseUp zostanie wywołany, wtedy zostaje wykonane OnCompleted i źródło danych kończy generowanie elementów. Oczywiście jedna sekwencja MouseDown, MouseMove, MouseUp nie jest wystarczająca i dlatego na końcu wywołujemy Repeat. Powoduje to, że cała sekwencja będzie powtarzana nieskończoną liczbę razy. Jeśli TakeUntil wciąż nie jest zrozumiały, proszę przeanalizować poniższy kod:
var cancelEvent = Observable.FromEventPattern(this,”Cancel”); var query = readDataSource.TakeUntil(cancelEvent);
Źródło readDataSource będzie generowało elementy aż do momentu przyjścia zdarzenia Cancel. TakeUnti zachowuje się po prostu jak zawór, który jest zakręcany gdy inne źródło wygeneruje element.
Na zakończenie, powróćmy do poprzedniego problemu o rysowaniu linii pomiędzy mouseDown oraz MouseUp. Mając już wiedzę o Repeat oraz TakeUntil możemy napisać:
var mouseDown = Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseDown"). Select(r => r.EventArgs.GetPosition(this)); var mouseUp = Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseUp"). Select(r => r.EventArgs.GetPosition(this)); mouseDown. TakeUntil(mouseUp). CombineLatest(mouseUp,(a,b)=>new{P1=a,P2=b}). Take(1). Repeat(). Subscribe((a) => { start = a.P1; end = a.P2; InvalidateVisual(); });
Po co nam Take(1)? Bez tego CombineLatest nigdy nie zakończy działania – będzie czekał na nowe elementy. Take(1) bierze po prostu pierwszą parę punktów i powoduje opublikowanie danych.