Implementierung eines einfachen Tacho-Control (Gauge-Control) in WPF Teil 2


dw1

Im ersten Teil des Artikels (https://blog.ppedv.de/post/2016/09/05/Implementierung-eines-einfachen-Tacho-Control-(Gauge-Control)-in-WPF-Teil-1.aspx) haben wir die Grundfunktionalität für unser Tacho-Control implementiert. Im diesem Teil wollen wir uns voll und ganz der Skala des Controls widmen. Diese werden wir mithilfe eines Custom Panel verwirklichen. Der vollständige Code kann unter https://github.com/d-wolf/GaugeUserControlWPF eingesehen und heruntergeladen werden.

GaugeOverview
Abb. 1: Fertiges Tacho-Control mit Skala

 

Schauen wir uns zunächst einmal an wie ein Panel, wie z.B. das Stackpanel, funktioniert. In einem Panel werden prinzipiell immer 2 wichtige Schritte in folgender Reihenfolge getätigt:

  1. Measure: Abfrage jedes Elementes innerhalb des Panels nach benötigter Größe und Position.
  2. Arrange: Bestimmen der tatsächlichen Größe und Position nach vorgaben des Layout-Algorithmus (Stackpanel positioniert alle Kinder untereinander).

Schauen wir uns den Code für ein einfaches Custom Panel an, so spiegeln sich diese 2 Schritte in den Methoden aus Abb. 2. wieder.

   1: class MyCustomPanel : Panel
   2: {
   3:     protected override Size MeasureOverride(Size availableSize)
   4:     {
   5:         return base.MeasureOverride(availableSize);
   6:     }
   7:  
   8:     protected override Size ArrangeOverride(Size finalSize)
   9:     {
  10:         return base.ArrangeOverride(finalSize);
  11:     }
  12: }
Abb. 2: Measure und Arrange eines Custom Panel

 

Für das Tacho-Control benötigen wir ein eigenes Panel, welches uns Objekte in einem bestimmten Radius um einen Mittelpunkt anordnet. Hierfür erstellen Wir uns eine Klasse namens CirclePanel im Ordner Panels (siehe Abb. 3).

02_projectStructureFinal
Abb. 3: Zukünftige Projektstruktur mit CirclePanel

 

Unser CirclePanel folgt weitestgehend der Implementierung von https://blogs.msdn.microsoft.com/mim/2013/04/16/winrt-create-a-custom-itemspanel-for-an-itemscontrol/.

 

Zusätzlich zu unserem Panel benötigen wir 2 Binding-Converter. Der DoubleToHalfOfDoubleConverter soll lediglich einen gebundenen Double-Wert durch 2 teilen. Diesen werden wir dazu benutzen, den Radius unseres CirclePanel auf die Hälfte der Breite des Layout zu binden. Der Code für den Converter wird in Abb. 4 dargestellt.

   1: /// <summary>
   2: /// Converter welcher den Übergebenen Wert halbiert
   3: /// </summary>
   4: public class DoubleToHalfOfDoubleConverter : IValueConverter
   5: {
   6:     /// <summary>
   7:     /// Teilt übergebenen Wert durch 2
   8:     /// </summary>
   9:     /// <param name="value">Wert</param>
  10:     /// <param name="targetType">Ziel Datentyp</param>
  11:     /// <param name="parameter">offset</param>
  12:     /// <param name="culture">Sprachraum</param>
  13:     /// <returns>Neuer Wert</returns>
  14:     public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  15:     {
  16:         double a = (double)value;
  17:         return a / 2;
  18:     }
  19:  
  20:     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  21:     {
  22:         throw new NotImplementedException();
  23:     }
  24: }
Abb. 4: Der DoubleToHalfOfDouble-Converter

 

Der zweite Converter (IntegerToPathListConverter) soll dazu dienen, einen Referenzpfad zu vervielfältigen und in eine Liste zu speichern. Diese wird uns anschließend als ItemSource für unser CirclePanel dienen. Der Code für den Converter befindet sich in Abb. 5.

   1: /// <summary>
   2: /// Generiert als value übergebene Anzahl an Pfaden für die Ticks des Messgerätes
   3: /// </summary>
   4: public class IntegerToPathListConverter : IMultiValueConverter
   5: {
   6:     /// <summary>
   7:     /// Generiert als value übergebene Anzahl an Pfaden für die Ticks des Messgerätes
   8:     /// </summary>
   9:     /// <param name="value">Anzahl der zu generierenden Pfade</param>
  10:     /// <param name="targetType">Zieltyp</param>
  11:     /// <param name="parameter">referenz auf zu duplizierenden Pfad</param>
  12:     /// <param name="culture">Sprachhraum</param>
  13:     /// <returns>Liste mit darzustellenden Pfaden</returns>
  14:     public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  15:     {
  16:         List<Path> l = new List<Path>();
  17:         int count = int.Parse(values[0].ToString());
  18:  
  19:         var path = values[1] as Path;
  20:  
  21:         for (int i = 0; i < count; i++)
  22:         {
  23:             l.Add(path.Clone());
  24:         }
  25:  
  26:         return l;
  27:     }
  28:  
  29:     public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  30:     {
  31:         throw new NotImplementedException();
  32:     }
  33: }
Abb. 5: Der IntegerToPathList-Converter

 

Leider implementieren Pfade keine Clone-Methode, welches es uns ermöglichen könnte, den Referenzpfad zu vervielfältigen. Hierfür können wir einfach die Klasse PathExtensions implementieren, welche die Klasse Path um eine Clone-Methode erweitert.

Nun braucht unser Control noch eine Eigenschaft TickPath vom Typ Path, welche dazu dienen soll, den Referenzpfad für unsere Skala setzen zu können. Die Dependency Property für unseren TickPath befindet sich in Abb. 6.

   1: /// <summary>
   2: /// Gibt den Referenzpfad für die Skala an
   3: /// </summary>
   4: public Path TickPath
   5: {
   6:    get { return (Path)GetValue(TickPathProperty); }
   7:    set { SetValue(TickPathProperty, value); }
   8: }
   9:  
  10: // Using a DependencyProperty as the backing store for TickPath.  This enables animation, styling, binding, etc...
  11: public static readonly DependencyProperty TickPathProperty =
  12:    DependencyProperty.Register("TickPath", typeof(Path), typeof(GaugeControl), new PropertyMetadata(null));
Abb. 6: Die TickPath-Eigenschaft

 

Schlussendlich müssen wir nur noch den XAML-Code unseres Controls anpassen, sodass wir unser Panel und unsere Converter verwenden. Den fertigen XAML-Code zeigt Abb. 7.

   1: <UserControl x:Class="GaugeUserControlLib.GaugeControl"
   2:              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
   5:              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
   6:              xmlns:local="clr-namespace:GaugeUserControlLib"
   7:              xmlns:converter="clr-namespace:GaugeUserControlLib.Converter"
   8:              xmlns:panels="clr-namespace:GaugeUserControlLib.Panels"
   9:              xmlns:sys="clr-namespace:System;assembly=mscorlib"
  10:              mc:Ignorable="d" 
  11:              d:DesignHeight="300" d:DesignWidth="300">
  12:     <UserControl.Resources>
  13:         <!--Converter um Radius des CirclePanel abhängig von der LayoutRoot breite zu berechnen-->
  14:         <converter:DoubleToHalfOfDoubleConverter x:Key="DoubleToHalfOfDoubleConverter"></converter:DoubleToHalfOfDoubleConverter>
  15:         <!--gibt abhängig vom angegebenen Int eine Liste an von Pfaden zurück (verfielfältigung von Pfadobjekten für Ticks)-->
  16:         <converter:IntegerToPathListConverter x:Key="IntegerToPathListConverter"></converter:IntegerToPathListConverter>
  17:         <!--Ressource für die Pfadbreite-->
  18:         <!--
  19:         <sys:Double x:Key="PathWidth">4</sys:Double>-->
  20:         <!--Referenzpfad für Ticks-->
  21:         <!--<Path x:Key="TickPath" Data="M0,0 H10" Width="{StaticResource PathWidth}" Fill="DimGray" Stroke="DimGray" StrokeThickness="6"></Path>-->
  22:     </UserControl.Resources>
  23:     <!--Container für Control. Behandelt den UserInput zum einstellen des Zeigers-->
  24:     <Grid x:Name="LayoutRoot" MouseMove="LayoutRoot_MouseMove" MouseDown="LayoutRoot_MouseDown" MouseUp="LayoutRoot_MouseUp">
  25:         <!--Dient als Host für das CirclePanle zur Anzeige der Ticks im Ziffernblatt-->
  26:         <ListBox x:Name="LBTicks" IsEnabled="False" BorderThickness="0">
  27:             <ListBox.ItemsSource>
  28:                 <MultiBinding Converter="{StaticResource IntegerToPathListConverter}">
  29:                     <Binding Path="TickCount" RelativeSource="{RelativeSource Mode=FindAncestor,
  30:                                                      AncestorType=UserControl}"/>
  31:                     <Binding Path="TickPath" RelativeSource="{RelativeSource Mode=FindAncestor,
  32:                                                      AncestorType=UserControl}"/>
  33:                 </MultiBinding>
  34:             </ListBox.ItemsSource>
  35:             <ListBox.ItemsPanel>
  36:                 <ItemsPanelTemplate>
  37:                     <!--Custom CirclePanel zur kreisförmigen anordnung der Ticks im Ziffernblatt-->
  38:                     <!--Radius ist abhängig von der Größe des Rootcontainers und der Breite der Ticks-->
  39:                     <panels:CirclePanel RenderTransformOrigin="0.5 0.5" Radius="{Binding ElementName=LayoutRoot, Path=ActualWidth, Converter={StaticResource DoubleToHalfOfDoubleConverter}}">
  40:                         <panels:CirclePanel.RenderTransform>
  41:                             <TransformGroup>
  42:                                 <!--initiale Rotation des Panels damit der erste Tick mitte links beginnt-->
  43:                                 <RotateTransform Angle="-180"></RotateTransform>
  44:                             </TransformGroup>
  45:                         </panels:CirclePanel.RenderTransform>
  46:                     </panels:CirclePanel>
  47:                 </ItemsPanelTemplate>
  48:             </ListBox.ItemsPanel>
  49:         </ListBox>
  50:         <!--repräsentiert den Zeiger, dünne Seite repräsentiert aktuellen Wert-->
  51:         <Polygon x:Name="Needle" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Points="0,3 0,4 7,7, 7.15,3.5 7,0" Height="4" Stroke="Black" Fill="Black" Width="{Binding ElementName=LayoutRoot, Path=ActualWidth}" Stretch="Fill" RenderTransformOrigin="0.5, 0.5">
  52:         </Polygon>
  53:         <!--kennzeichnet Mittelpunkt des Tacho-->
  54:         <Border Background="LightGray" Width="5" Height="5" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="2"/>
  55:     </Grid>
  56: </UserControl>
Abb. 7: Finaler XAML-Code des Tacho-Control

 

Nun ist unser einfaches Tacho-Control fertig. Eine beispielhafte Verwendung seht ihr in Abb. 8.

   1: <StackPanel Margin="20">
   2:         <gaugeLib:GaugeControl Value="{Binding ElementName=GaugeSlider, Path=Value, Mode=TwoWay}" Minimum="0" Maximum="20" Width="300" Height="300" TickFrequency="1" IsSnapToTickEnabled="True">
   3:             <gaugeLib:GaugeControl.TickPath>
   4:                 <Path Data="M0,0 H-10" Fill="DimGray" Stroke="DimGray" StrokeThickness="2"></Path>
   5:             </gaugeLib:GaugeControl.TickPath>
   6:         </gaugeLib:GaugeControl>
   7:         <StackPanel MaxWidth="320">
   8:             <TextBlock Text="{Binding ElementName=GaugeSlider, Path=Value, Mode=TwoWay}"></TextBlock>
   9:             <Slider Minimum="0" Maximum="20" TickFrequency="1" IsSnapToTickEnabled="True" TickPlacement="BottomRight" Foreground="Black" x:Name="GaugeSlider"></Slider>
  10:         </StackPanel>
  11:     </StackPanel>
Abb. 8: Verwendung des fertigen Tacho-Control

 

Viel Spaß damit!

Kommentare sind geschlossen