Prosty Paint w Silverlight w niecałe 120 linii

Co można zrobić ciekawego wykorzystując tyle linii? Niewiele ;-), ale… Rysowanie przy pomocy ołówka oraz figur: elipsa, prostokąt. Wszystko możliwe w 4 wybranych kolorach. Dodatkowo zaimplementowany mechanizm historii zdarzeń (undo-redo) bez limitu zapamiętanych „ruchów”.

 

Jak wygląda efekt końcowy?

Przyznam, że jeszcze nigdy pędzel nie sprawiał mi tyle radości…

Install Microsoft Silverlight

Kilka słów o GUI

Interfejs jest bardzo prosty i nie będę się zbytnio o nim rozpisywał. Zainteresowanych XAMLem odsyłam do kodu źródłowego.

Najważniejsza informacja: rysujemy po Canvasie. Wszystko co można narysować ogranicza się do klas z przestrzeni System.Windows.Shapes czyli Rectangle, Ellipse i Line do rysowania ołówkiem. Musimy obsłużyć 3 zdarzenia na naszym płótnie:

  • MouseLeftButtonDown – początek malowania
  • MouseMove – dorysowywanie kolejnych linii / usuwanie i wstawianie tymczasowych figur
  • MouseLeftButtonUp – koniec malowania, wstawienie obiektu do listy historii

Rozpoczynamy malowanie

A oto co potrzebujemy zapamiętać w trakcie działania programu:

// Bieżący kolor (Stroke)
SolidColorBrush _currentBrush = new SolidColorBrush(Colors.Blue);

// Czy trwa malowanie
bool _drawing = false;
// Punkt z MouseLeftButtonDown
Point _startPoint;

// Tymczasoway Shape przed zdarzeniem MLBUp
Shape _tempElement;

// Wszystkie linie do momentu zdarzenia MLBUp
// tworzą jeden obiekt historii
List<Line> _lines = new List<Line>();

// Historia powstawania obiektów
List<Shape[]> _history = new List<Shape[]>();

// Indeks aktualnego obiektu z historii
// undo -1 redo +1
int _historyIndex = -1;

MLBUp – MouseLeftButtonUp

Po kliknięciu lewym przyciskiem myszy (LPM) na Canvas nie dzieje się zbyt wiele.

private void MyCanvas_MouseLeftButtonDown(object sender,
		MouseButtonEventArgs e)
{
	_drawing = true;
	_startPoint = e.GetPosition(MyCanvas);
}

Flaga drawing na true oraz pobranie miejsca kliknięcia względem naszego płótna.

Odbierając zdarzenie MouseMove sprawdzamy czy właśnie trwa malowanie. Jeżeli tak, to w zależności od wybranej figury wywołujemy odpowiednią metodę. Ponieważ malowanie prostokąta niczym nie różni się od malowania elipsy, stworzyłem wspólny kod:

private void DrawShape(Shape shape, Point position)
{
	MyCanvas.Children.Remove(_tempElement);

	shape.Width = Math.Max(Math.Abs(_startPoint.X - position.X), 1);
	shape.Height = Math.Max(Math.Abs(_startPoint.Y - position.Y), 1);
	shape.StrokeThickness = 3;
	shape.StrokeStartLineCap = PenLineCap.Round;
	shape.StrokeEndLineCap = PenLineCap.Round;
	shape.Stroke = _currentBrush;

	// W zależności od pozycji kursora względem
	// punktu początkowego
	// ustalamy położenie lewego górnego rogu shape'a
	double x, y;
	if (_startPoint.X >= position.X)
		x = position.X;
	else
		x = _startPoint.X;

	if (_startPoint.Y >= position.Y)
		y = position.Y;
	else
		y = _startPoint.Y;

	Canvas.SetLeft(shape, x);
	Canvas.SetTop(shape, y);

	MyCanvas.Children.Add(shape);
	_tempElement = shape;
}

Pod shape kryje się w zależności od wyboru użytkownika albo Rectangle albo Ellipse. Position natomiast jest to aktualne położenie kursora myszy.

W przypadku linii nie tworzymy obiektów tymczasowych tylko kolekcjonujemy w jednej liście aż do zdarzenia MLBUp.

private void Draw(Point point)
{
	Line line = new Line();
	line.Stroke = _currentBrush;
	line.StrokeEndLineCap = PenLineCap.Round;
	line.StrokeStartLineCap = PenLineCap.Round;
	line.StrokeThickness = 3;
	line.X1 = point.X;
	line.Y1 = point.Y;
	line.X2 = _startPoint.X;
	line.Y2 = _startPoint.Y;

	_startPoint = point;
	MyCanvas.Children.Add(line);

	// Wszystkie połączone ze sobą linie tworzą
	// jeden obiekt w historii
	_lines.Add(line);
}

Malowanie kończymy dopiero na zdarzenie MouseLeftButtonUp.

private void MyCanvas_MouseLeftButtonUp(object sender,
			MouseButtonEventArgs e)
{
	_historyIndex++;

	// Usuwamy z historii wszystkie obiekty, które zostały
	// usunięte z płótna poprzez undo
	for (int i = _history.Count - 1; i >= _historyIndex; i--)
		_history.RemoveAt(i);

	if (_currentFigure != 'p')
		_history.Add(new Shape[] { _tempElement });
	else
	{
		_history.Add(_lines.ToArray());
		_lines.Clear();
	}

	_drawing = false;
	// aby nie usunąć figury przy następnym zdarzeniu MLBDown
	_tempElement = null;
}

Kilka słów wyjaśnień odnośnie pierwszej pętli. Gdy mamy obiekty R1, R2, R3 i użytkownik kliknie Undo, następnie coś namaluje, automatycznie traci możliwość przywrócenia figury R3. Gdy kliknie Undo, Undo, Redo, Redo wszystko zostanie przywrócone.

Jak działa historia

W zasadzie najtrudniejsze elementy systemu zapamiętywania ruchów mamy już za sobą. Przy zdarzeniu Undo jedynie usuwamy z płótna element wskazywany przez _historyIndex i zmniejszamy jego wartość. Redo z kolei działa odwrotnie. Należy zwrócić uwagę na zakres możliwych wartości, aby nie odwoływać się do nieistniejących indeksów w historii.

Podsumowując

Stworzyliśmy bardzo prostą aplikacje do „obróbki” grafiki. Do Photoshopa całe szczęście brakuje nam już niewiele ;-). Nie mniej jednak jest to dobry start do Paint w wersji sieciowej. Zalecam przebudową programu do działania w np. MVVMLight, tak aby uporządkować kod. Figury można zamknąć w klasy implementujące odpowiedni interfejs. Swoją drogą niewykluczone, że się tym wszystkim niedługo zajmę. Jeżeli tak się stanie, powiadomię o efektach na blogu.

PS. Gdyby ktoś próbował doliczyć się linii kodu informuję, że wykorzystałem wbudowane narzędzie w VS 2010 Ultimate.

{filelink=4}

Share

4 myśli nt. „Prosty Paint w Silverlight w niecałe 120 linii

  1. Czy jest jakiś sens używania flagi „drawing”? W podanym przykładzie jej rola polega chyba tylko na ustawieniu i zmianie na false po wszystkim 🙂

    • Odbierając zdarzenie MouseMove sprawdzamy czy właśnie trwa malowanie
      Jeżeli nie sprawdzimy tego warunku to będziemy cały czas malować, gdy tylko użytkownik przejedzie myszką nad canvasem.

  2. Hej, fajnie. Ciekawe, robiłem bardzo podobną rzecz niedawno i no cóż. To nie rocket science, więc mój wynik jest bardzo podobny. Zastanawiają mnie tylko dwie rzeczy:
    – Dlaczego nie InkPresenter? Historia musiałaby działać inaczej, no ale może pozbyłbyś się jednoelementowych tablic ;).
    – Dlaczego porównywanie charów a nie prosty enum?

    Btw. jak byłem mniejszy strasznie irytowało mnie, że elipsy / koła w paincie i wszystkich painto pochodnych rysuje się tak… nieintuicyjnie. Serio. Chyba tylko programiści rysują koła / elipsy przez otaczający prostokąt :p.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Spam protection by WP Captcha-Free