Noch einmal die Mini-Paint-Anwendung

1. Aufgabe

Wir gehen auf die Mini-Paint-Anwendung noch einmal ein (siehe Windows Presentation Foundation => Standardsteuerelemente => Mini-Paint). In einer einführenden Aufgabenstellung ging es um die prinzipielle Erreichung der gestellten Anforderungen. Jetzt steht die Architektur der Anwendung im Vordergrund – und damit im Speziellen die Strukturierung einzelner Teilfunktionalitäten im Kontext eigenständiger Steuerelemente. Die Projektschablone „WPF User Control“ kommt hierbei zum Einsatz.

Wenn es rein um die Gestaltung der Oberfläche geht, treten keine unüberwindbaren Schwierigkeiten auf. Interessant wird es erst, wenn die beteiligten Control-Objekte miteinander kommunizieren sollen. Wie gelangt die Auswahl einer Farbe innerhalb eines Farbauswahl-Steuerelements zum benachbarten Canvas-Objekt, in dem man Figuren mit eben dieser Farbe zeichnen möchte? Wir stellen im Folgenden zwei Realisierungen vor:

  • Elementarer Ansatz mit möglichst einfachen C#-Sprachmitteln.

  • Umfassender Ansatz konform zum Regelwerk einer WPF-Anwendung.

Der Versuch, die beiden Ansätze in möglichst neutralen Worten zu beschreiben, bringt es bereits zum Vorschein. Der erste Ansatz führt vergleichsweise schnell und unkompliziert ans Ziel, lässt aber eine Reihe zentraler Aspekte der WPF-Programmierung gänzlich außer Acht. Der zweite Ansatz hingegen passt sich perfekt an alle Konventionen einer WPF-Anwendung wie beispielsweise Dependency-Properties, Data Binding, Event Routing und Commands an. Für einen WPF-Neuling könnte dieser Ansatz allerdings eine nahezu unüberwindbare Einstiegshürde darstellen.

Mit dem von mir vorgestellten zwei-stufigen Ansatz ist es möglich, einfache Aspekte der komponenten-orientierten Programmierung wie etwa die Kapselung eines Steuerelements oder auch das Verschalten mehrerer Steuerelemente separat zu betrachten. Wer die Hürde des ersten Ansatzes erklommen hat, kann sich dann getrost den technologischen Niederungen der fortgeschrittenen WPF-Programmierung zuwenden.

2. Lösung

2.1. Elementarer Ansatz

Wir beginnen die Betrachtungen mit einem Übersichtsbild in Abbildung 1, dass die beteiligten Komponenten und ihre Verschaltungen skizziert:

Steuerelemente verschalten.

Abbildung 1. Steuerelemente verschalten.


Einzelne Steuerelemente wie beispielsweise eine Komponente zur Auswahl einer Farbe müssen Ereignisse zur Verfügung stellen. Jede Auswahl einer Farbe resultiert im Auslösen eines entsprechenden Ereignisses. Angemeldet auf dieses Ereignis ist eine Handler-Routine im umgebenden Hauptfenster. Dieses kennt alle in ihm platzierten Steuerelemente. Damit kann die Handler-Routine Eigenschaften (Properties) an dem eigentlichen Ziel der Auswahloperation verändern.

Gehen wir nun auf die Mini-Paint-Anwendung ein. Nachfolgend finden Sie für die Teilfunktionalitäten „Auswählen einer Farbe“, „Auswählen einer Linienbreite“ und „Figur zeichnen“ unabhängig voneinander realisierte Steuerelemente (Spezialisierungen der Klasse UserControl) vor:

01: <UserControl
02:     x:Class="WpfMiniPaint_UsingUserControls.PaintColorControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     d:DesignHeight="300" d:DesignWidth="300">
09:     <!-- group box for color options -->
10:     <GroupBox Header="Color">
11:         <StackPanel>
12:             <RadioButton
13:                 Name="blackRadioButton" Margin="3" Content="Black"
14:                 Checked="ColorRadioButton_Checked" IsChecked="True">
15:             </RadioButton>
16:             <RadioButton
17:                 Name="redRadioButton" Margin="3" Content="Red"
18:                 Checked="ColorRadioButton_Checked" IsChecked="False">
19:             </RadioButton>
20:             <RadioButton
21:                 Name="greenRadioButton" Margin="3" Content="Green"
22:                 Checked="ColorRadioButton_Checked" IsChecked="False">
23:             </RadioButton>
24:             <RadioButton
25:                 Name="blueRadioButton" Margin="3" Content="Blue"
26:                 Checked="ColorRadioButton_Checked" IsChecked="False">
27:             </RadioButton>
28:         </StackPanel>
29:     </GroupBox>
30: </UserControl>

Beispiel 1. Steuerelement PaintColorControl: XAML


01: namespace WpfMiniPaint_UsingUserControls
02: {
03:     public delegate void PaintColorHandler(Color color);
04: 
05:     public partial class PaintColorControl : UserControl
06:     {
07:         private Color color;
08: 
09:         public event PaintColorHandler PaintColorChanged;
10: 
11:         public PaintColorControl()
12:         {
13:             this.InitializeComponent();
14: 
15:             this.color = Colors.Black;
16:         }
17: 
18:         private void ColorRadioButton_Checked(Object sender, RoutedEventArgs e)
19:         {
20:             if (sender == this.blackRadioButton)
21:             {
22:                 if (this.color != Colors.Black)
23:                 {
24:                     this.color = Colors.Black;
25: 
26:                     if (this.PaintColorChanged != null)
27:                         this.PaintColorChanged.Invoke(this.color);
28:                 }
29:             }
30:             else if (sender == this.redRadioButton)
31:             {
32:                 if (this.color != Colors.Red)
33:                 {
34:                     this.color = Colors.Red;
35: 
36:                     if (this.PaintColorChanged != null)
37:                         this.PaintColorChanged.Invoke(this.color);
38:                 }
39:             }
40:             else if (sender == this.greenRadioButton)
41:             {
42:                 if (this.color != Colors.Green)
43:                 {
44:                     this.color = Colors.Green;
45: 
46:                     if (this.PaintColorChanged != null)
47:                         this.PaintColorChanged.Invoke(this.color);
48:                 }
49:             }
50:             else if (sender == this.blueRadioButton)
51:             {
52:                 if (this.color != Colors.Blue)
53:                 {
54:                     this.color = Colors.Blue;
55: 
56:                     if (this.PaintColorChanged != null)
57:                         this.PaintColorChanged.Invoke(this.color);
58:                 }
59:             }
60:         }
61:     }
62: }

Beispiel 2. Steuerelement PaintColorControl: CodeBehind


01: <UserControl
02:     x:Class="WpfMiniPaint_UsingUserControls.PaintSizeControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     d:DesignHeight="300" d:DesignWidth="300">
09:     <GroupBox Header="Size">
10:         <StackPanel>
11:             <RadioButton
12:                 Name="smallRadioButton" Margin="3" Content="Small"
13:                 Checked="RadioButton_Checked" IsChecked="False">
14:             </RadioButton>
15:             <RadioButton
16:                 Name="mediumRadioButton" Margin="3" Content="Medium"
17:                 Checked="RadioButton_Checked" IsChecked="True">
18:             </RadioButton>
19:             <RadioButton
20:                 Name="largeRadioButton" Margin="3" Content="Large"
21:                 Checked="RadioButton_Checked" IsChecked="False">
22:             </RadioButton>
23:         </StackPanel>
24:     </GroupBox>
25: </UserControl>

Beispiel 3. Steuerelement PaintSizeControl: XAML


01: namespace WpfMiniPaint_UsingUserControls
02: {
03:     public delegate void PaintSizeHandler(int x);
04: 
05:     public partial class PaintSizeControl : UserControl
06:     {
07:         private int size;
08: 
09:         public event PaintSizeHandler PaintSizeChanged;
10: 
11:         public PaintSizeControl()
12:         {
13:             this.InitializeComponent();
14: 
15:             this.size = 4;
16:         }
17: 
18:         private void RadioButton_Checked(Object sender, RoutedEventArgs e)
19:         {
20:             if (sender == this.smallRadioButton)
21:             {
22:                 if (this.size != 2)
23:                 {
24:                     this.size = 2;
25: 
26:                     if (this.PaintSizeChanged != null)
27:                         this.PaintSizeChanged(this.size);
28:                 }
29:             }
30:             else if (sender == this.mediumRadioButton)
31:             {
32:                 if (this.size != 4)
33:                 {
34:                     this.size = 4;
35: 
36:                     if (this.PaintSizeChanged != null)
37:                         this.PaintSizeChanged(this.size);
38:                 }
39:             }
40:             else if (sender == this.largeRadioButton)
41:             {
42:                 if (this.size != 6)
43:                 {
44:                     this.size = 6;
45: 
46:                     if (this.PaintSizeChanged != null)
47:                         this.PaintSizeChanged(this.size);
48:                 }
49:             }
50:         }
51:     }
52: }

Beispiel 4. Steuerelement PaintSizeControl: CodeBehind


01: <UserControl
02:     x:Class="WpfMiniPaint_UsingUserControls.PaintCanvasControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     d:DesignHeight="300" d:DesignWidth="300">
09:     <Canvas
10:         Background="White" Name="PaintCanvas"
11:         MouseDown="Canvas_MouseDown" MouseMove="Canvas_MouseMove"
12:         MouseLeave="Canvas_MouseLeave" MouseEnter="Canvas_MouseEnter"
13:         MouseUp="Canvas_MouseUp">
14:     </Canvas>
15: </UserControl>

Beispiel 5. Steuerelement PaintCanvasControl: XAML


001: namespace WpfMiniPaint_UsingUserControls
002: {
003:     public delegate void MousePositionHandler(double x, double y);
004: 
005:     public partial class PaintCanvasControl : UserControl
006:     {
007:         private Point last;
008:         private Brush brush;
009:         private int thickness;
010:         private Stack<int> undo;
011: 
012:         public event MousePositionHandler MousePositionChanged;
013: 
014:         // c'tor
015:         public PaintCanvasControl()
016:         {
017:             this.InitializeComponent();
018: 
019:             this.undo = new Stack<int>();
020: 
021:             this.brush = Brushes.Black;  // drawing color
022:             this.thickness = 4;          // line thickness
023:         }
024: 
025:         // public properties
026:         public int PenSize
027:         {
028:             set
029:             {
030:                 this.thickness = value;
031:             }
032:         }
033: 
034:         public Color PenColor
035:         {
036:             set
037:             {
038:                 if (value == Colors.Black)
039:                     this.brush = Brushes.Black;
040:                 if (value == Colors.Red)
041:                     this.brush = Brushes.Red;
042:                 if (value == Colors.Green)
043:                     this.brush = Brushes.Green;
044:                 else if (value == Colors.Blue)
045:                     this.brush = Brushes.Blue;
046:             }
047:         }
048: 
049:         // public interface
050:         public void Clear()
051:         {
052:             this.PaintCanvas.Children.Clear();
053:             this.undo.Clear();
054:         }
055: 
056:         public void Undo()
057:         {
058:             if (this.undo.Count == 0)
059:                 return;
060: 
061:             // pop indexes of last line (start index is one below top of stack)
062:             int to = this.undo.Pop();
063:             int from = this.undo.Pop();
064: 
065:             // remove last line from UIElement collection
066:             this.PaintCanvas.Children.RemoveRange(from, to);
067:         }
068: 
069:         // private event handler methods
070:         private void Canvas_MouseDown(Object sender, MouseButtonEventArgs e)
071:         {
072:             // push start of line onto undo stack
073:             this.undo.Push(this.PaintCanvas.Children.Count);
074: 
075:             this.last = e.GetPosition(this);
076:         }
077: 
078:         private void Canvas_MouseMove(Object sender, MouseEventArgs e)
079:         {
080:             Point current = e.GetPosition(this);
081: 
082:             if (this.MousePositionChanged != null)
083:                 this.MousePositionChanged.Invoke(current.X, current.Y);
084: 
085:             if (e.LeftButton == MouseButtonState.Pressed)
086:             {
087:                 if (this.last.X != -1)
088:                 {
089:                     Line l = new Line();
090:                     l.X1 = this.last.X;
091:                     l.Y1 = this.last.Y;
092:                     l.X2 = current.X;
093:                     l.Y2 = current.Y;
094:                     l.Stroke = this.brush;
095:                     l.StrokeThickness = this.thickness;
096:                     l.StrokeStartLineCap = PenLineCap.Round;
097:                     l.StrokeEndLineCap = PenLineCap.Round;
098: 
099:                     this.PaintCanvas.Children.Add(l);
100:                     this.last = current;
101:                 }
102:             }
103:         }
104: 
105:         private void Canvas_MouseUp(Object sender, MouseButtonEventArgs e)
106:         {
107:             // push end of line onto undo stack
108:             this.undo.Push(this.PaintCanvas.Children.Count);
109:         }
110: 
111:         private void Canvas_MouseLeave(Object sender, MouseEventArgs e)
112:         {
113:             this.last = new Point(-1, -1);
114:         }
115: 
116:         private void Canvas_MouseEnter(Object sender, MouseEventArgs e)
117:         {
118:             this.last = e.GetPosition(this);
119:         }
120:     }
121: }

Beispiel 6. Steuerelement PaintCanvasControl: CodeBehind


Damit bleibt nur noch die Hauptanwendung offen. Ihr gilt unser besonderes Interesse, da in ihr alle beteiligten Steuerelemente instanziiert und miteinander verschaltet werden:

01: <Window
02:     x:Class="WpfMiniPaint_UsingUserControls.MainWindow"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:local="clr-namespace:WpfMiniPaint_UsingUserControls"
06:     Title="MiniPaint Application" Height="340" Width="600">
07:     <Grid>
08:         <!-- defining columns -->
09:         <Grid.ColumnDefinitions>
10:             <ColumnDefinition Width="Auto" />
11:             <ColumnDefinition Width="*" />
12:         </Grid.ColumnDefinitions>
13: 
14:         <!-- first column: tool bar -->
15:         <StackPanel Grid.Column="0" Background="LightGray">
16:             <!-- group box for color options -->
17:             <local:PaintColorControl x:Name="PaintColor"/>
18: 
19:             <!-- group box for size options -->
20:             <local:PaintSizeControl x:Name="PaintSize"/>
21: 
22:             <!-- buttons -->
23:             <Button Name="ClearButton" Click="Button_Click"
24:                     Width="80" Margin="3,10,3,3">Clear</Button>
25:             <Button Name="UndoButton" Click="Button_Click"
26:                     Width="80" Margin="3">Undo</Button>
27:             
28:             <!-- mini status line -->
29:             <TextBox Width="80" Margin="3" Name="MouseStatus" IsEnabled="False"/>
30:         </StackPanel>
31: 
32:         <!-- second column: painting area -->
33:         <Border Grid.Column="1" BorderThickness="1"
34:                 BorderBrush="Black" Margin="2">
35:             <local:PaintCanvasControl x:Name="PaintCanvas"/>
36:         </Border>
37:     </Grid>
38: </Window>

Beispiel 7. Hauptanwendung MainWindow: XAML


01: namespace WpfMiniPaint_UsingUserControls
02: {
03:     public partial class MainWindow : Window
04:     {
05:         public MainWindow()
06:         {
07:             this.InitializeComponent();
08: 
09:             this.PaintColor.PaintColorChanged +=
10:                 new PaintColorHandler(this.PaintColor_PaintColorChanged);
11: 
12:             this.PaintSize.PaintSizeChanged +=
13:                 new PaintSizeHandler(this.PaintSize_PaintSizeChanged);
14: 
15:             this.PaintCanvas.MousePositionChanged +=
16:                 new MousePositionHandler(this.PaintCanvas_MousePositionChanged);
17:         }
18: 
19:         private void PaintCanvas_MousePositionChanged(double x, double y)
20:         {
21:             String s = String.Format("{0}, {1}", (int) x, (int) y);
22:             this.MouseStatus.Text = s;
23:         }
24: 
25:         private void PaintSize_PaintSizeChanged(int size)
26:         {
27:             this.PaintCanvas.PenSize = size;
28:         }
29: 
30:         private void PaintColor_PaintColorChanged(Color color)
31:         {
32:             this.PaintCanvas.PenColor = color;
33:         }
34: 
35:         private void Button_Click(Object sender, RoutedEventArgs e)
36:         {
37:             if (sender == this.ClearButton)
38:             {
39:                 this.PaintCanvas.Clear();
40:             }
41:             else if (sender == this.UndoButton)
42:             {
43:                 this.PaintCanvas.Undo();
44:             }
45:         }
46:     }
47: }

Beispiel 8. Hauptanwendung MainWindow: CodeBehind


In den Zeilen 19, 25, 30 und 35 von Listing 8 befinden sich die Ereignishandler des Hauptfensters. Hier treffen zum Beispiel Änderungen in der Farbauswahl (Zeile 32) oder bzgl. der Linienbreite (Zeile 27) ein und werden den Eigenschaften PenSize bzw. PenColor des Canvas-Steuerelements zugewiesen.

2.2. Fortgeschrittener Ansatz

In diesem Abschnitt überarbeiten wir die Lösungen des elementaren Ansatzes. Im Einzelnen geht es um

  • Eigenschafen (Properties) – Die „klassischen“ C#-Eigenschaften, auch als CLR-Eigenschaften (Common Language Runtime) bezeichnet, sind nicht dazu geeignet, in einem XAML-Umfeld für Datenbindung verwendet zu werden. Viele mit der WPF eingeführte Programmiertechniken setzen Dependency Properties, im deutschen als Abhängigkeitseigenschaft bezeichnet, voraus. Datenbindungen, Stile und auch Animationen bauen darauf auf. Neben diesen Abhängigkeitseigenschaften gibt es auch eine Spezialisierung davon, die so genannten Attached Properties oder auch angehängte Eigenschaften. Dabei handelt es sich um Eigenschaften, die nicht in der Klasse eines Elements definiert sind, sondern von einer hierarchisch übergeordneten Komponente bereitgestellt werden. Die Eigenschaften DockPanel.Dock oder Grid.Column sind typische angehängte Eigenschaften, die beispielsweise dazu da sind, ein Steuerelement an einer bestimmten Randposition eines Containers oder in einer bestimmten Zelle eines Gitternetzes zu positionieren.

    Um es aber einmal deutlich ausgesprochen zu haben: Die WPF unterstützt sowohl die Abhängigkeitseigenschaften wie auch die herkömmlichen CLR-Eigenschaften. Es lässt sich insbesondere einer Eigenschaft nicht auf den ersten Blick ansehen, ob sie als CLR- oder als Abhängigkeitseigenschaft implementiert ist. Die herkömmlichen CLR-Eigenschaften sind in der WPF aber stark in der Minderzahl.

  • Ereignisse (Routed Events) – Um Änderungen in einem Steuerelement nach außen zu transportieren, verwenden wir Ereignisse. Im Grunde genommen würde es ausreichen, Ereignisse in klassischer C#-Manier zur Verfügung zu stellen (Deklaration eines delegate-Typs, Definition einer entsprechenden Ereignisvariablen). Die WPF unterstützt allerdings auch die Möglichkeit, Ereignisse im Elementbaum entweder von unten nach oben „blubbern“ oder von oben nach unten „hangeln“ zu lassen. Wir sprechen konkret von Bubbling- und Tunneling-Ereignissen. Wir werden zu diesem Zweck so genannte Routed Events einsetzen.

  • Kommandos (Commands) – Kommandos gibt es in eigentlichen Sinne unmittelbar in der Sprache C# nicht. Sie sind ein WPF-Hilfsmittel, um Aktionen, die als Folge einer Bedienhandlung an einem WPF-Steuerelement in die Wege zu leiten sind, an einer zentralen Stelle zu hinterlegen. Dies ergibt dann Sinn, wenn dieselbe Folgeaktion sowohl durch eine Schaltfläche wie auch durch eine Menüanwahl ausgelöst werden kann.

An der Aufteilung unserer Mini-Paint-Anwendung in einzelne Teilfunktionalitäten hat sich nichts verändert. Die Steuerelemente selbst finden Sie dafür in den nachfolgenden Listings dieses Mal in echter WPF-Manier realisiert vor:

01: <UserControl
02:     x:Class="WpfMiniPaint_UsingUserControls_WPFConform.PaintColorControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     x:Name="PaintColorCtrl"
09:     d:DesignHeight="300" d:DesignWidth="300">
10:     <!-- group box for color options -->
11:     <GroupBox Header="Color">
12:         <StackPanel>
13:             <RadioButton
14:                 Name="BlackRadioButton" Margin="3" Content="Black"
15:                 Checked="ColorRadioButton_Checked" IsChecked="True">
16:             </RadioButton>
17:             <RadioButton
18:                 Name="RedRadioButton" Margin="3" Content="Red"
19:                 Checked="ColorRadioButton_Checked" IsChecked="False">
20:             </RadioButton>
21:             <RadioButton
22:                 Name="GreenRadioButton" Margin="3" Content="Green"
23:                 Checked="ColorRadioButton_Checked" IsChecked="False">
24:             </RadioButton>
25:             <RadioButton
26:                     Name="BlueRadioButton" Margin="3" Content="Blue"
27:                     Checked="ColorRadioButton_Checked" IsChecked="False">
28:             </RadioButton>
29:         </StackPanel>
30:     </GroupBox>
31: </UserControl>

Beispiel 9. Steuerelement PaintColorControl: XAML


01: namespace WpfMiniPaint_UsingUserControls_WPFConform
02: {
03:     public partial class PaintColorControl : UserControl
04:     {
05:         public static readonly DependencyProperty ColorProperty;
06: 
07:         static PaintColorControl()
08:         {
09:             ColorProperty = DependencyProperty.Register(
10:                 "Color",
11:                 typeof(Color),
12:                 typeof(PaintColorControl),
13:                 new UIPropertyMetadata(Colors.Black)
14:             );
15:         }
16: 
17:         public Color Color
18:         {
19:             get { return (Color)this.GetValue(ColorProperty); }
20:             set { this.SetValue(ColorProperty, value); }
21:         }
22: 
23:         public PaintColorControl()
24:         {
25:             this.InitializeComponent();
26:         }
27: 
28:         private void ColorRadioButton_Checked(Object sender, RoutedEventArgs e)
29:         {
30:             if (sender == this.BlackRadioButton)
31:             {
32:                 this.SetValue(ColorProperty, Colors.Black);
33:             }
34:             else if (sender == this.RedRadioButton)
35:             {
36:                 this.SetValue(ColorProperty, Colors.Red);
37:             }
38:             else if (sender == this.GreenRadioButton)
39:             {
40:                 this.SetValue(ColorProperty, Colors.Green);
41:             }
42:             else if (sender == this.BlueRadioButton)
43:             {
44:                 this.SetValue(ColorProperty, Colors.Blue);
45:             }
46:         }
47:     }
48: }

Beispiel 10. Steuerelement PaintColorControl: CodeBehind


01: <UserControl
02:     x:Class="WpfMiniPaint_UsingUserControls_WPFConform.PaintSizeControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     x:Name="PaintSizeCtrl"
09:     d:DesignHeight="300" d:DesignWidth="300">
10:     <GroupBox Header="Size">
11:         <StackPanel>
12:             <RadioButton
13:                 Name="SmallRadioButton" Margin="3" Content="Small" 
14:                 IsChecked="False" Checked="RadioButton_Checked">
15:             </RadioButton>
16:             <RadioButton
17:                 Name="MediumRadioButton" Margin="3" Content="Medium"
18:                 IsChecked="True" Checked="RadioButton_Checked">
19:             </RadioButton>
20:             <RadioButton
21:                 Name="LargeRadioButton" Margin="3" Content="Large"
22:                 IsChecked="False" Checked="RadioButton_Checked">
23:             </RadioButton>
24:         </StackPanel>
25:     </GroupBox>
26: </UserControl>

Beispiel 11. Steuerelement PaintSizeControl: XAML


01: namespace WpfMiniPaint_UsingUserControls_WPFConform
02: {
03:     public partial class PaintSizeControl : UserControl
04:     {
05:         public static readonly DependencyProperty SizeProperty;
06: 
07:         static PaintSizeControl()
08:         {
09:             SizeProperty = DependencyProperty.Register (
10:                 "Size",
11:                 typeof(int),
12:                 typeof(PaintSizeControl),
13:                 new UIPropertyMetadata(0));
14:         }
15: 
16:         public int Size
17:         {
18:             get { return (int)this.GetValue(SizeProperty); }
19:             set { this.SetValue(SizeProperty, value); }
20:         }
21: 
22:         public PaintSizeControl()
23:         {
24:             this.InitializeComponent();
25:         }
26: 
27:         private void RadioButton_Checked(Object sender, RoutedEventArgs e)
28:         {
29:             if (sender == this.SmallRadioButton)
30:             {
31:                 this.SetValue(SizeProperty, 2);
32:             }
33:             else if (sender == this.MediumRadioButton)
34:             {
35:                 this.SetValue(SizeProperty, 4);
36:             }
37:             else if (sender == this.LargeRadioButton)
38:             {
39:                 this.SetValue(SizeProperty, 6);
40:             }
41:         }
42:     }
43: }

Beispiel 12. Steuerelement PaintSizeControl: CodeBehind


01: <UserControl
02:     x:Class="WpfMiniPaint_UsingUserControls_WPFConform.PaintCanvasControl"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
06:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
07:     mc:Ignorable="d" 
08:     d:DesignHeight="300" d:DesignWidth="300">
09:     <Canvas
10:         Background="White" Name="PaintCanvas"
11:         MouseDown="Canvas_MouseDown" MouseMove="Canvas_MouseMove"
12:         MouseLeave="Canvas_MouseLeave" MouseEnter="Canvas_MouseEnter"
13:         MouseUp="Canvas_MouseUp">
14:     </Canvas>
15: </UserControl>

Beispiel 13. Steuerelement PaintCanvasControl: XAML


001: namespace WpfMiniPaint_UsingUserControls_WPFConform
002: {
003:     public partial class PaintCanvasControl : UserControl
004:     {
005:         public static readonly DependencyProperty PenSizeProperty;
006:         public static readonly DependencyProperty PenColorProperty;
007: 
008:         public static readonly RoutedEvent MousePositionChangedEvent;
009: 
010:         public static readonly RoutedUICommand DoClear;
011:         public static readonly RoutedUICommand DoUndo;
012: 
013:         private Point last;
014:         private Stack<int> undo;
015: 
016:         // class c'tor
017:         static PaintCanvasControl()
018:         {
019:             PenSizeProperty = DependencyProperty.Register(
020:                 "PenSize",
021:                 typeof(int),
022:                 typeof(PaintCanvasControl),
023:                 new UIPropertyMetadata(2));
024: 
025:             PenColorProperty = DependencyProperty.Register(
026:                 "PenColor",
027:                 typeof(Color),
028:                 typeof(PaintCanvasControl),
029:                 new UIPropertyMetadata(Colors.Black));
030: 
031:             MousePositionChangedEvent = EventManager.RegisterRoutedEvent(
032:                 "MousePositionChanged",
033:                 RoutingStrategy.Bubble,
034:                 typeof(RoutedPropertyChangedEventHandler<Point>),
035:                 typeof(PaintCanvasControl));
036: 
037:             DoClear = new RoutedUICommand();
038:             DoUndo = new RoutedUICommand();
039: 
040:             CommandBinding clearCommandBinding = new CommandBinding (
041:                 DoClear,
042:                 ExecutedClearCommand,
043:                 CanExecuteClearCommand);
044: 
045:             CommandBinding undoCommandBinding = new CommandBinding (
046:                 DoUndo,
047:                 ExecutedUndoCommand,
048:                 CanExecuteUndoCommand);
049: 
050:             CommandManager.RegisterClassCommandBinding(
051:                 typeof(PaintCanvasControl),
052:                  clearCommandBinding);
053: 
054:             CommandManager.RegisterClassCommandBinding(
055:                 typeof(PaintCanvasControl),
056:                  undoCommandBinding);
057:         }
058: 
059:         // c'tor
060:         public PaintCanvasControl()
061:         {
062:             this.InitializeComponent();
063: 
064:             this.last = new Point(-1, -1);
065:             this.undo = new Stack<int>();
066:         }
067: 
068:         private static void ExecutedClearCommand(Object sender, ExecutedRoutedEventArgs e)
069:         {
070:             PaintCanvasControl ctrl = (PaintCanvasControl)sender;
071:             ctrl.Clear();
072:         }
073: 
074:         private static void CanExecuteClearCommand(Object sender, CanExecuteRoutedEventArgs e)
075:         {
076:             e.CanExecute = true;
077:         }
078: 
079:         private static void ExecutedUndoCommand(Object sender, ExecutedRoutedEventArgs e)
080:         {
081:             PaintCanvasControl ctrl = (PaintCanvasControl)sender;
082:             ctrl.Undo();
083:         }
084: 
085:         private static void CanExecuteUndoCommand(Object sender, CanExecuteRoutedEventArgs e)
086:         {
087:             e.CanExecute = true;
088:         }
089: 
090:         public int PenSize
091:         {
092:             get { return (int)this.GetValue(PenSizeProperty); }
093:             set { this.SetValue(PenSizeProperty, value); }
094:         }
095: 
096:         public Color PenColor
097:         {
098:             get { return (Color)this.GetValue(PenColorProperty); }
099:             set { this.SetValue(PenColorProperty, value); }
100:         }
101: 
102:         // public interface
103:         public void Clear()
104:         {
105:             this.PaintCanvas.Children.Clear();
106:             this.undo.Clear();
107:         }
108: 
109:         public void Undo()
110:         {
111:             if (this.undo.Count == 0)
112:                 return;
113: 
114:             // pop indexes of last line (start index is one below top of stack)
115:             int to = this.undo.Pop();
116:             int from = this.undo.Pop();
117: 
118:             // remove last line from UIElement collection
119:             this.PaintCanvas.Children.RemoveRange(from, to);
120:         }
121: 
122:         // private event handler methods
123:         public event RoutedPropertyChangedEventHandler<Point> MousePositionChanged
124:         {
125:             add { this.AddHandler(MousePositionChangedEvent, value); }
126:             remove { this.RemoveHandler(MousePositionChangedEvent, value); }
127:         }
128: 
129:         private void Canvas_MouseDown(Object sender, MouseButtonEventArgs e)
130:         {
131:             // push start of line onto undo stack
132:             this.undo.Push(this.PaintCanvas.Children.Count);
133:             this.last = e.GetPosition(this.PaintCanvas);
134:         }
135: 
136:         private void Canvas_MouseMove(Object sender, MouseEventArgs e)
137:         {
138:             Point current = e.GetPosition(this.PaintCanvas);
139: 
140:             // raise mouse moved event
141:             if (this.last != current)
142:             {
143:                 RoutedPropertyChangedEventArgs<Point> args =
144:                     new RoutedPropertyChangedEventArgs<Point>(this.last, current);
145:                 args.RoutedEvent = PaintCanvasControl.MousePositionChangedEvent;
146:                 this.RaiseEvent(args);
147:             }
148: 
149:             if (e.LeftButton == MouseButtonState.Pressed)
150:             {
151:                 if (this.last.X != -1)
152:                 {
153:                     Line l = new Line();
154:                     l.X1 = this.last.X;
155:                     l.Y1 = this.last.Y;
156:                     l.X2 = current.X;
157:                     l.Y2 = current.Y;
158: 
159:                     Color color = PenColor;
160:                     Brush brush = new SolidColorBrush(color);
161:                     l.Stroke = brush;
162: 
163:                     int penSize = PenSize;
164:                     l.StrokeThickness = penSize;
165: 
166:                     l.StrokeStartLineCap = PenLineCap.Round;
167:                     l.StrokeEndLineCap = PenLineCap.Round;
168: 
169:                     this.PaintCanvas.Children.Add(l);
170:                     this.last = current;
171:                 }
172:             }
173:         }
174: 
175:         private void Canvas_MouseUp(Object sender, MouseButtonEventArgs e)
176:         {
177:             // push end of line onto undo stack
178:             this.undo.Push(this.PaintCanvas.Children.Count);
179:         }
180: 
181:         private void Canvas_MouseLeave(Object sender, MouseEventArgs e)
182:         {
183:             this.last = new Point(-1, -1);
184:         }
185: 
186:         private void Canvas_MouseEnter(Object sender, MouseEventArgs e)
187:         {
188:             this.last = e.GetPosition(this.PaintCanvas);
189:         }
190:     }
191: }

Beispiel 14. Steuerelement PaintCanvasControl: CodeBehind


In Listing 15 und Listing 16 finden das Hauptfenster der Anwendung vor. Der CodeBehind-Anteil der Anwendung (Listing 16) ist dieses Mal erheblich kürzer ausgefallen. Den Grund dafür können Sie im XAML-Anteil (Listing 15) der Anwendung entnehmen: Durch den Einsatz von Datenbindung (mit der Hilfe von Abhängigkeitseigenschaften) sowie von Kommandos lassen sich Zuordnungen von Ereignissen und Eigenschaften deklarativ beschreiben. Der imperative Anteil des Programms (C#-Quellcode) nimmt auf diese Weise ab und führt so zu einer Vereinfachung in der Erstellung der Anwendung. Die deklarativen Anspekte im XAML-Anteil (Datenbindung) finden Sie speziell in den Zeilen 50 und 51 von Listing 15 vor. Den Gebrauch von Kommandos wiederum können Sie in den Zeilen 32, 33, 37 und 38 studieren:

01: <Window
02:     x:Class="WpfMiniPaint_UsingUserControls_WPFConform.MainWindow"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:local="clr-namespace:WpfMiniPaint_UsingUserControls_WPFConform"
06:     Title="MainWindow" Height="340" Width="525">
07: 
08:     <Grid>
09:         <!-- defining columns -->
10:         <Grid.ColumnDefinitions>
11:             <ColumnDefinition Width="Auto" />
12:             <ColumnDefinition Width="*" />
13:         </Grid.ColumnDefinitions>
14: 
15:         <!-- defining columns -->
16:         <Grid.RowDefinitions>
17:             <RowDefinition Height="*" />
18:             <RowDefinition Height="Auto" />
19:         </Grid.RowDefinitions>
20: 
21:         <!-- first column: tool bar -->
22:         <StackPanel Grid.Column="0" Grid.Row="0" Background="LightGray">
23:             <!-- user control for color options -->
24:             <local:PaintColorControl x:Name="MyPaintColorControl"/>
25: 
26:             <!-- user control for size options -->
27:             <local:PaintSizeControl x:Name="MyPaintSizeControl"/>
28: 
29:             <!-- buttons -->
30:             <Button
31:                 Margin="3,10,3,3"
32:                 Command="{x:Static local:PaintCanvasControl.DoClear}"
33:                 CommandTarget="{Binding ElementName=MyPaintCanvasControl}">Clear</Button>
34: 
35:             <Button
36:                 Margin="3"
37:                 Command="{x:Static local:PaintCanvasControl.DoUndo}"
38:                 CommandTarget="{Binding ElementName=MyPaintCanvasControl}">Undo</Button>
39: 
40:             <!-- mini status line -->
41:             <TextBox Width="80" Margin="3" Name="MouseStatus" IsEnabled="False"/>
42:         </StackPanel>
43: 
44:         <!-- second column: painting area -->
45:         <Border Grid.Column="1" Grid.Row="0" BorderThickness="1"
46:                 BorderBrush="Black" Margin="2">
47:             <local:PaintCanvasControl
48:                 x:Name="MyPaintCanvasControl"
49:                 MousePositionChanged="PaintCanvasControl_MousePositionChanged"
50:                 PenSize="{Binding ElementName=MyPaintSizeControl, Path=Size}"
51:                 PenColor="{Binding ElementName=MyPaintColorControl, Path=Color}">
52:             </local:PaintCanvasControl>
53:         </Border>
54:     </Grid>
55: </Window>

Beispiel 15. Hauptanwendung MainWindow: XAML


01: namespace WpfMiniPaint_UsingUserControls_WPFConform
02: {
03:     public partial class MainWindow : Window
04:     {
05:         public MainWindow()
06:         {
07:             this.InitializeComponent();
08:         }
09: 
10:         private void PaintCanvasControl_MousePositionChanged(
11:             Object sender, RoutedPropertyChangedEventArgs<Point> e)
12:         {
13:             String s = String.Format("{0}, {1}",
14:                 (int)e.NewValue.X, (int)e.NewValue.Y);
15:             this.MouseStatus.Text = s;
16:         }
17:     }
18: }

Beispiel 16. Hauptanwendung MainWindow: CodeBehind