Eine Mini-Paint Anwendung

1. Aufgabe

In dieser Fallstudie entwickeln wir ein Programm MiniPaint zum freihändigen Malen von Figuren mit der Maus, etwaige Ähnlichkeiten mit der Microsoft Paint Anwendung sind dabei nicht ganz unbeabsichtigt. Die Oberfläche der Anwendung sollte in etwa wie in Abbildung 1 gezeigt aussehen:

Ein einfaches Programm zum Zeichnen von Figuren.

Abbildung 1. Ein einfaches Programm zum Zeichnen von Figuren.


Die Oberfläche der Anwendung setzt sich aus einer Reihe von geschachtelten Layoutcontainern zusammen. In der ersten Ebene befindet sich ein Grid-Objekt mit zwei Spalten. In der linken Spalte ist ein StackPanel-Objekt eingebettet, das in vertikaler Anordnung zwei GroupBox-Objekte und zwei Schaltflächen enthält. Die rechte Spalte besteht aus einem Canvas-Objekt, das man unter anderem zum freihändigen Zeichnen von Figuren benutzen kann.

2. Lösung

Bevor wir mit der eigentlichen Umsetzung der Aufgabenstellung in ein WPF-Programm beginnen, ist die Frage zu beantworten, wie in einem Canvas-Objekt freihändig gezeichnete Figuren überhaupt dargestellt werden können. Es gibt hierzu gleich mehrere Antworten: Wenn Sie zu Übungszwecken etwas mehr Aufwand investieren wollen, sind Line-Objekte eine Möglichkeit. Viele hinreichend kurze Line-Objekte werden mit dem bloßen Auge so gut wie überhaupt nicht als gerade Linien wahrgenommen. Mit etwas weniger Aufwand stemmen Sie die Aufgabe mit Polyline-Objekten. Ein solches Objekt verbindet beliebig lange Listen von Punkten miteinander, pro gezeichneter Figur benötigen Sie nur ein solches Objekt. Im Prinzip arbeitet ein Polyline-Objekt genauso wie viele Line-Objekte, der Programmieraufwand ist aber geringer.

Bei der Gestaltung der Anwendungsoberfläche in XAML müssen Sie sich dazu noch nicht festgelegt haben, ihre XAML-Markup-Beschreibung entnehmen Sie Listing 1:

01: <Window x:Class="MiniPaint.MainWindow"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         Title="Mini Paint Application" Height="300" Width="600">
05:     <Grid>
06:         <!-- defining columns -->
07:         <Grid.ColumnDefinitions>
08:             <ColumnDefinition Width="Auto" />
09:             <ColumnDefinition Width="*" />
10:         </Grid.ColumnDefinitions>
11: 
12:         <!-- first column: tool bar -->
13:         <StackPanel Grid.Column="0" Background="LightGray">
14:             <!-- group box for color options -->
15:             <GroupBox Header="Color">
16:                 <StackPanel>
17:                     <RadioButton
18:                         Name="blackRadioButton" Margin="3" Content="Black"
19:                         Checked="ColorRadioButton_Checked" IsChecked="True">
20:                     </RadioButton>
21:                     <RadioButton
22:                         Name="redRadioButton" Margin="3" Content="Red"
23:                         Checked="ColorRadioButton_Checked" IsChecked="False">
24:                     </RadioButton>
25:                     <RadioButton
26:                         Name="greenRadioButton" Margin="3" Content="Green"
27:                         Checked="ColorRadioButton_Checked" IsChecked="False">
28:                     </RadioButton>
29:                     <RadioButton
30:                         Name="blueRadioButton" Margin="3" Content="Blue"
31:                         Checked="ColorRadioButton_Checked" IsChecked="False">
32:                     </RadioButton>
33:                 </StackPanel>
34: 
35:             </GroupBox>
36: 
37:             <!-- group box for size options -->
38:             <GroupBox Header="Size">
39:                 <StackPanel>
40:                     <RadioButton
41:                         Name="smallRadioButton" Margin="3" Content="Small"
42:                         Checked="RadioButton_Checked" IsChecked="False">
43:                     </RadioButton>
44:                     <RadioButton
45:                         Name="mediumRadioButton" Margin="3" Content="Medium"
46:                         Checked="RadioButton_Checked" IsChecked="True">
47:                     </RadioButton>
48:                     <RadioButton
49:                         Name="largeRadioButton" Margin="3" Content="Large"
50:                         Checked="RadioButton_Checked" IsChecked="False">
51:                     </RadioButton>
52:                 </StackPanel>
53:             </GroupBox>
54: 
55:             <!-- buttons -->
56:             <Button Name="clearButton" Click="Button_Click"
57:                     Width="80" Margin="3,10,3,3">Clear</Button>
58:             <Button Name="undoButton" Click="Button_Click"
59:                     Width="80" Margin="3">Undo</Button>
60:         </StackPanel>
61: 
62:         <!-- second column: painting area -->
63:         <Border Grid.Column="1" BorderThickness="1"
64:                 BorderBrush="Black" Margin="2"> 
65:             <Canvas
66:                 Background="White" Name ="paintCanvas"
67:                 MouseDown="Canvas_MouseDown" MouseMove="Canvas_MouseMove"
68:                 MouseLeave="Canvas_MouseLeave" MouseEnter="Canvas_MouseEnter"
69:                 MouseUp="Canvas_MouseUp">
70:             </Canvas>
71:         </Border>
72: 
73:     </Grid>
74: </Window>

Beispiel 1. XAML-Markup zur Gestaltung der MiniPaint-Anwendungsoberfläche.


Damit kommen wir zum programmiersprachlichen Anteil der Anwendung. Das Handling der diversen Optionsfelder (RadioButton-Objekte) und Schaltflächen ist trivial und kann im Quellcode betrachtet werden. Für das Zeichnen der gemalten Figuren sind zwei Ereignisse am Canvas-Objekt zu verfolgen: MouseDown und MouseMove. Den Umstand, ob die Maustaste beim Bewegen des Mauszeigers gedrückt ist oder nicht, erkennt man am Ereignisparameter des Typs MouseEventArgs. Die Eigenschaft LeftButton wiederum ist vom Aufzählungstyp MouseButtonState:

public enum MouseButtonState
{
    Released = 0,   // the button is released
    Pressed = 1,    // the button is pressed
}

Wir stellen als Erstes eine Lösung vor, die zwei aufeinander folgende MouseMove-Ereignisse als eine Linie zum Malen auffasst. Durch das vorangehende MouseDown-Ereignis wird der Startpunkt der Linie festgelegt. Die relevanten x- und y-Koordinaten werden jeweils in einem Line-Objekt zusammengefasst.

Für die Linienobjekte muss man kein zusätzliches Containerobjekt bereitstellen, sie lassen sich komfortabel in dem bereits vorhandenen Container für UIElementCollection-Objekte ablegen, den das Canvas-Objekt mit der Eigenschaft Children zur Verfügung stellt. Der eigentliche Nutzen des UIElementCollection-Objekts liegt natürlich darin, dass all seine Elemente im Canvas-Objekt immer gezeichnet werden, wenn dieses sichtbar ist. Damit kann das Malen der Linien einfach auf deren Ablage in einem UIElementCollection-Container zurückgeführt werden, siehe Listing 2:

001: public partial class MainWindow : Window
002: {
003:     private Point last;
004:     private Brush brush;
005:     private int thickness;
006:     private Stack<int> undo;
007: 
008:     public MainWindow()
009:     {
010:         InitializeComponent();
011: 
012:         this.undo = new Stack<int>();
013:         this.brush = Brushes.Black;  // drawing color
014:         this.thickness = 4;          // line thickness
015:     }
016: 
017:     private void Canvas_MouseDown(Object sender, MouseButtonEventArgs e)
018:     {
019:         // push start of line onto undo stack
020:         this.undo.Push(this.paintCanvas.Children.Count);
021: 
022:         this.last = e.GetPosition(this.paintCanvas);
023:     }
024: 
025:     private void Canvas_MouseMove(Object sender, MouseEventArgs e)
026:     {
027:         if (e.LeftButton == MouseButtonState.Pressed)
028:         {
029:             if (this.last.X != -1)
030:             {
031:                 Point current = e.GetPosition(this.paintCanvas);
032: 
033:                 Line l = new Line();
034:                 l.X1 = this.last.X;
035:                 l.Y1 = this.last.Y;
036:                 l.X2 = current.X;
037:                 l.Y2 = current.Y;
038:                 l.Stroke = this.brush;
039:                 l.StrokeThickness = this.thickness;
040: 
041:                 this.paintCanvas.Children.Add(l);
042:                 this.last = current;
043:             }
044:         }
045:     }
046: 
047:     private void Canvas_MouseUp(Object sender, MouseButtonEventArgs e)
048:     {
049:         // push end of line onto undo stack
050:         this.undo.Push(this.paintCanvas.Children.Count);
051:     }
052: 
053:     private void Canvas_MouseLeave(Object sender, MouseEventArgs e)
054:     {
055:         this.last = new Point(-1, -1);
056:     }
057: 
058:     private void Canvas_MouseEnter(Object sender, MouseEventArgs e)
059:     {
060:         this.last = e.GetPosition(this.paintCanvas);
061:     }
062: 
063:     private void Button_Click(Object sender, RoutedEventArgs e)
064:     {
065:         if (sender == this.clearButton)
066:         {
067:             this.paintCanvas.Children.Clear();
068:             this.undo.Clear();
069:         }
070:         else if (sender == this.undoButton)
071:         {
072:             if (this.undo.Count == 0)
073:                 return;
074: 
075:             // pop indexes of last line (start index is one below top of stack)
076:             int to = this.undo.Pop();
077:             int from = this.undo.Pop();
078: 
079:             // remove last line from UIElement collection
080:             this.paintCanvas.Children.RemoveRange(from, to);
081:         }
082:     }
083: 
084:     private void ColorRadioButton_Checked(Object sender, RoutedEventArgs e)
085:     {
086:         if (sender == this.blackRadioButton)
087:             this.brush = Brushes.Black;
088:         else if (sender == this.redRadioButton)
089:             this.brush = Brushes.Red;
090:         else if (sender == this.greenRadioButton)
091:             this.brush = Brushes.Green;
092:         else if (sender == this.blueRadioButton)
093:             this.brush = Brushes.Blue;
094:     }
095: 
096:     private void RadioButton_Checked(Object sender, RoutedEventArgs e)
097:     {
098:         if (sender == this.smallRadioButton)
099:             this.thickness = 2;
100:         else if (sender == this.mediumRadioButton)
101:             this.thickness = 4;
102:         else if (sender == this.largeRadioButton)
103:             this.thickness = 6;
104:     }
105: }

Beispiel 2. Code-Behind-Datei zur Gestaltung der MiniPaint-Oberfläche.


Ein Hinweis zu den Zeilen 22, 31 und 60 von Listing 2: Die Bestimmung der Koordinaten des Mauszeigers erfolgt in der WPF immer relativ zu einem IInputElement-Element: Aus diesem Grund gibt es die Methode GetPosition, sie ist zur Ortsbestimmung des Mauszeigers am fraglichen Objekt aufzurufen. Die bislang entwickelte Anwendung besitzt einen kleinen Schönheitsfehler: Alle gezeichneten Figuren gehen nach dem Schließen der Anwendung verloren. Wir ergänzen die Anwendung deshalb um ein Menü File mit den Menüeinträgen Save, Load und Exit, siehe Abbildung 2:

MiniPaint-Anwendung mit Menüleiste.

Abbildung 2. MiniPaint-Anwendung mit Menüleiste.


Für das Dateiformat greifen wir auf den W3C-Standard eXtensible Markup Language, kurz XML zurück. Exemplarisch könnte eine solche XML-Datei für zwei Figuren wie folgt aussehen:

<?xml version="1.0" encoding="utf-8"?>
<Pictures>
  <Picture PenColor="#FF000000" PenWidth="4">
    <Point>
      <X>42</X>
      <Y>86</Y>
    </Point>
    <Point>
      <X>43</X>
      <Y>88</Y>
    </Point>
    <Point>
      <X>44</X>
      <Y>90</Y>
    </Point>
    <Point>
      <X>45</X>
      <Y>93</Y>
    </Point>
  </Picture>

  <Picture PenColor="#FF000000" PenWidth="6">
    <Point>
      <X>108</X>
      <Y>62</Y>
    </Point>
    <Point>
      <X>116</X>
      <Y>66</Y>
    </Point>
    <Point>
      <X>118</X>
      <Y>68</Y>
    </Point>
    <Point>
      <X>124</X>
      <Y>72</Y>
    </Point>
</Pictures>

Ein weiterer Vorteil dieser Entwurfsentscheidung besteht darin, dass mit Hilfe der DOM-Klassen (Document Object Model) eine entsprechende Umsetzung vergleichsweise kompakt erfolgen kann, wie Sie Listing 3 entnehmen können:

001: public partial class MainWindow : Window
002: {
003:     ...
004: 
005:     public void Load(String fileName)
006:     {
007:         this.paintCanvas.Children.Clear();
008: 
009:         XDocument doc = XDocument.Load(fileName);
010:         XElement root = doc.Root;
011: 
012:         foreach (XElement xPicture in root.Elements())
013:         {
014:             // retrieve color attribute
015:             XAttribute xPenColor = xPicture.Attribute("PenColor");
016:             Color color = (Color)ColorConverter.ConvertFromString(xPenColor.Value);
017:             SolidColorBrush brush = new SolidColorBrush(color);
018: 
019:             // retrieve pen width attribute
020:             XAttribute xPenWidth = xPicture.Attribute("PenWidth");
021:             double strokeThickness = Double.Parse(xPenWidth.Value);
022: 
023:             Point p1 = new Point(-1, -1);
024:             Point p2;
025: 
026:             foreach (XElement xPoint in xPicture.Elements())
027:             {
028:                 XElement xElem = xPoint.Element("X");
029:                 double x = Double.Parse(xElem.Value,
030:                     NumberStyles.AllowDecimalPoint, NumberFormatInfo.InvariantInfo);
031:                 XElement yElem = xPoint.Element("Y");
032:                 double y = Double.Parse(yElem.Value,
033:                     NumberStyles.AllowDecimalPoint, NumberFormatInfo.InvariantInfo);
034: 
035:                 p2 = new Point(x, y);
036:                 if (p1.X != -1 && p1.Y != -1)
037:                 {
038:                     Line l = new Line();
039:                     l.X1 = p1.X;
040:                     l.Y1 = p1.Y;
041:                     l.X2 = p2.X;
042:                     l.Y2 = p2.Y;
043:                     l.Stroke = brush;
044:                     l.StrokeThickness = strokeThickness;
045: 
046:                     this.paintCanvas.Children.Add(l);
047:                 }
048:                 p1 = p2;
049:             }
050:         }
051:     }
052: 
053:     public void Save(String fileName)
054:     {
055:         XDocument xDoc = new XDocument();
056:         XElement xPictures = new XElement("Pictures");
057:         xDoc.Add(xPictures);
058: 
059:         for (int nextLine = 0; nextLine < this.paintCanvas.Children.Count; )
060:         {
061:             Line line = (Line)this.paintCanvas.Children[nextLine];
062: 
063:             XElement xPicture = new XElement("Picture");
064: 
065:             // create attributes
066:             double strokeThickness = line.StrokeThickness;
067:             Color color = ((SolidColorBrush)line.Stroke).Color;
068:             xPicture.SetAttributeValue("PenColor", color.ToString());
069:             xPicture.SetAttributeValue("PenWidth", strokeThickness.ToString());
070:             xPictures.Add(xPicture);
071: 
072:             XElement xFirstPoint = new XElement("Point");
073:             xFirstPoint.Add(new XElement("X", line.X1));
074:             xFirstPoint.Add(new XElement("Y", line.Y1));
075:             xPicture.Add(xFirstPoint);
076: 
077:             while (nextLine < this.paintCanvas.Children.Count)
078:             {
079:                 line = (Line)this.paintCanvas.Children[nextLine];
080: 
081:                 XElement xNextPoint = new XElement("Point");
082:                 xNextPoint.Add(new XElement("X", line.X2));
083:                 xNextPoint.Add(new XElement("Y", line.Y2));
084:                 xPicture.Add(xNextPoint);
085: 
086:                 nextLine++; 
087: 
088:                 // check for end of picture
089:                 if (nextLine < this.paintCanvas.Children.Count)
090:                 {
091:                     Line line2 = (Line)this.paintCanvas.Children[nextLine];
092: 
093:                     // end of first line must be begin of second line
094:                     if (line.X2 != line2.X1 || line.Y2 != line2.Y1)
095:                         break;
096:                 }
097:             }
098:         }
099: 
100:         xDoc.Save(fileName);
101:     }
102: }

Beispiel 3. Abspeicherung einer MiniPaint-Oberfläche im XML-Format.


Das Menü selbst lässt sich mit XAML-Mitteln sehr einfach zur Anwendung hinzufügen:

01: <Window x:Class="MiniPaint.MainWindow"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         Title="Mini Paint Application - Save and Load Menu"  Height="300" Width="600">
05:     <DockPanel>
06:         <Menu DockPanel.Dock="Top">
07:             <MenuItem Header="File">
08:                 <MenuItem Name="MenuItemSave" Header="Save" Click="MenuItem_Click"/>
09:                 <MenuItem Name="MenuItemOpen" Header="Open" Click="MenuItem_Click"/>
10:                 <Separator />
11:                 <MenuItem Name="MenuItemExit" Header="Exit" Click="MenuItem_Click"/>
12:             </MenuItem>
13:         </Menu>
14:         <Grid>
15:             <!-- wie zuvor -->
16:         </Grid>
17: 
18:     </DockPanel>
19: </Window>

Die Dialoge zum Lokalisieren einer Datei auf Ihrem Rechner sind in .NET im Namensraum Microsoft.Win32 versteckt. Das Öffnen bzw. Abspeichern einer Datei im Windows-Betriebssystem selbst ist mit Hilfe der beiden Klassen OpenFileDialog bzw. SaveFileDialog nicht sehr schwer, wie ein Blick auf die MenuItem_Click-Methode bestätigt:

01: private void MenuItem_Click(Object sender, RoutedEventArgs e)
02: {
03:     if (sender == this.MenuItemSave)
04:     {
05:         SaveFileDialog saveFileDialog = new SaveFileDialog();
06:         saveFileDialog.Filter = "XML files (*.xml)|*.xml";
07:         if (saveFileDialog.ShowDialog() == true)
08:         {
09:             String fileName = saveFileDialog.FileName;
10:             this.Save(fileName);
11:         }
12:     }
13:     else if (sender == this.MenuItemOpen)
14:     {
15:         OpenFileDialog openFileDialog = new OpenFileDialog();
16:         openFileDialog.Filter = "XML files (*.xml)|*.xml";
17:         if (openFileDialog.ShowDialog() == true)
18:         {
19:             String fileName = openFileDialog.FileName;
20:             this.Load(fileName);
21:         }
22:     }
23:     else if (sender == this.MenuItemExit)
24:     {
25:         Application.Current.Shutdown();
26:     }
27: }

Die beiden Methoden Save und Load aus dem letzten Codefragment haben wir bereits in Listing 3 vorgestellt. Wenn Sie die Aufgabenstellung mit weniger Aufwand lösen wollen, stellen Polyline-Objekte eine Arbeitserleichterung dar. Am XAML-Code ändert sich nichts, nur der Code-Behind-Anteil ist anders zu gestalten. In Listing 4 finden Sie die entsprechende Umgestaltung vor:

01: public partial class MainWindow : Window
02: {
03:     private Brush brush;
04:     private int thickness;
05:     private Polyline lastPolyline;
06: 
07:     public MainWindow()
08:     {
09:         this.InitializeComponent();
10: 
11:         this.brush = Brushes.Black;  // drawing color
12:         this.thickness = 4;          // line thickness
13:     }
14: 
15:     // helper method
16:     private void CreateNextPolyLine (Point start)
17:     {
18:         // create next polyline instance
19:         this.lastPolyline = new Polyline();
20:         this.lastPolyline.Stroke = this.brush;
21:         this.lastPolyline.StrokeThickness = this.thickness;
22:         this.lastPolyline.StrokeStartLineCap = PenLineCap.Round;
23:         this.lastPolyline.StrokeEndLineCap = PenLineCap.Round;
24: 
25:         // add first point
26:         this.lastPolyline.Points.Add(start);
27:         this.paintCanvas.Children.Add(this.lastPolyline);
28:     }
29: 
30:     private void Canvas_MouseDown(Object sender, MouseButtonEventArgs e)
31:     {
32:         Point current = e.GetPosition(this.paintCanvas);
33:         this.CreateNextPolyLine(current);
34:     }
35: 
36:     private void Canvas_MouseMove(Object sender, MouseEventArgs e)
37:     {
38:         if (e.LeftButton == MouseButtonState.Pressed)
39:         {
40:             Point current = e.GetPosition(this.paintCanvas);
41:             this.lastPolyline.Points.Add(current);
42:         }
43:     }
44: 
45:     private void Canvas_MouseEnter(Object sender, MouseEventArgs e)
46:     {
47:         if (e.LeftButton == MouseButtonState.Pressed)
48:         {
49:             Point current = e.GetPosition(this.paintCanvas);
50:             this.CreateNextPolyLine(current);
51:         }
52:     }
53: 
54:     private void Button_Click(Object sender, RoutedEventArgs e)
55:     {
56:         if (sender == this.clearButton)
57:         {
58:             this.paintCanvas.Children.Clear();
59:         }
60:         else if (sender == this.undoButton)
61:         {
62:             int count = this.paintCanvas.Children.Count;
63:             if (count == 0)
64:                 return;
65: 
66:             // remove last line from UIElement collection
67:             this.paintCanvas.Children.RemoveAt(count - 1);
68:         }
69:     }
70: 
71:     private void ColorRadioButton_Checked(Object sender, RoutedEventArgs e)
72:     {
73:         // see Listing 2
74:     }
75: 
76:     private void RadioButton_Checked(Object sender, RoutedEventArgs e)
77:     {
78:         // see Listing 2
79:     }
80: }

Beispiel 4. Realisierung der MiniPaint-Oberfläche mit Polyline-Objekten.


Noch deutlicher wird der verringerte Arbeitsaufwand, wenn es um das Laden und Abspeichern in XML-Dateien geht. Hierzu zum Abschluss die entsprechenden Umgestaltungen der zwei Methoden Load und Save in Listing 5:

01: public void Load(String fileName)
02: {
03:     this.paintCanvas.Children.Clear();
04: 
05:     XDocument doc = XDocument.Load(fileName);
06:     XElement root = doc.Root;
07: 
08:     foreach (XElement xPicture in root.Elements())
09:     {
10:         // retrieve color attribute
11:         XAttribute xPenColor = xPicture.Attribute("PenColor");
12:         Color color = (Color)ColorConverter.ConvertFromString(xPenColor.Value);
13: 
14:         // retrieve pen width attribute
15:         XAttribute xPenWidth = xPicture.Attribute("PenWidth");
16: 
17:         // create polyline instance
18:         Polyline polyline = new Polyline();
19:         polyline.Stroke = new SolidColorBrush(color);
20:         polyline.StrokeThickness = Double.Parse(xPenWidth.Value);
21:         polyline.StrokeStartLineCap = PenLineCap.Round;
22:         polyline.StrokeEndLineCap = PenLineCap.Round;
23: 
24:         foreach (XElement xPoint in xPicture.Elements())
25:         {
26:             XElement xElem = xPoint.Element("X");
27:             double x = Double.Parse(xElem.Value,
28:                 NumberStyles.AllowDecimalPoint, NumberFormatInfo.InvariantInfo);
29:             XElement yElem = xPoint.Element("Y");
30:             double y = Double.Parse(yElem.Value,
31:                 NumberStyles.AllowDecimalPoint, NumberFormatInfo.InvariantInfo);
32: 
33:             Point p = new Point(x, y);
34:             polyline.Points.Add(p);
35:         }
36: 
37:         this.paintCanvas.Children.Add(polyline);
38:     }
39: }
40: 
41: public void Save(String fileName)
42: {
43:     XDocument xDoc = new XDocument();
44:     XElement xPictures = new XElement("Pictures");
45:     xDoc.Add(xPictures);
46: 
47:     for (int p = 0; p < this.paintCanvas.Children.Count; p ++)
48:     {
49:         Polyline polyline = (Polyline)this.paintCanvas.Children[p];
50:         XElement xPicture = new XElement("Picture");
51: 
52:         // create attributes
53:         double strokeThickness = polyline.StrokeThickness;
54:         Color color = ((SolidColorBrush)polyline.Stroke).Color;
55:         xPicture.SetAttributeValue("PenColor", color.ToString());
56:         xPicture.SetAttributeValue("PenWidth", strokeThickness.ToString());
57:         xPictures.Add(xPicture);
58: 
59:         for (int i = 0; i < polyline.Points.Count; i++)
60:         {
61:             Point point = polyline.Points[i];
62:             XElement xPoint = new XElement("Point");
63:             xPoint.Add(new XElement("X", point.X));
64:             xPoint.Add(new XElement("Y", point.Y));
65:             xPicture.Add(xPoint);
66:         }
67:     }
68: 
69:     xDoc.Save(fileName);
70: }

Beispiel 5. Abspeicherung einer MiniPaint-Oberfläche im XML-Format unter Verwendung von Polyline-Objekten.