Quantcast
Channel: ppedv Team Blog
Viewing all articles
Browse latest Browse all 1198

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

$
0
0

In diesem Artikel möchte ich euch zeigen, wie wir ein einfaches Tacho-Control als User Control in WPF implementieren können (siehe Abb. 1). Insgesamt wird sich die Implementierung des Controls in zwei Teile gliedern. Im ersten Teil geht es darum, die Grundfunktionalität für das Tacho-Control zu herzustellen. D.h. wir werden unserm Control ein Userinterface verpassen, sowie alle Notwendigen Eigenschaften und Methoden für die Nutzerinteraktion. Im 2. Teil werden wir uns detaillierter mit Custom Panels beschäftigen und wie wir mit dessen Hilfe eine Skala für unser Control erstellen können. Um die Schritte und den Quellcode besser nachvollziehen zu können, könnt ihr das komplette Beispiel unter https://github.com/d-wolf/GaugeUserControlWPF einsehen bzw. herunterladen.

 

GaugeOverview
Abb. 1: Vorschau des fertigen Tacho-Control mit gebundenem Slider

 

Als erstes legen Wir uns ein Projekt an und erstellen eine WPF User Control Library mit der Struktur aus Abb. 2.

01_projectStructureStart
Abb. 2: Struktur der WPF User Control Library

 

Danach legen wir uns ein Klasse mit allen Mathematischen Hilfsmethoden an, welche wir für unser Tacho-Control benötigen werden. Für unsere kleine Bibliothek erstellen wir eine statische Klasse GeoMath im Ordner Common. Der vollständige Quellcode der Klasse ist in Abb. 3 zusehen.

   1:publicstaticclass GeoMath
   2: {
   3:/// <summary>
   4:/// Berechnet den Winkel zwischen zwei Punkten
   5:/// </summary>
   6:/// <param name="origin">Startpunkt</param>
   7:/// <param name="target">Endpunkt</param>
   8:/// <returns>Winkel zwischen 2 Punkten in Grad</returns>
   9:publicstaticdouble AngleBetween(Point origin, Point target)
  10:     {
  11:return RadianToDegree(Math.Atan2(origin.Y - target.Y, origin.X - target.X));
  12:     }
  13:  
  14:/// <summary>
  15:/// Wandelt grad in Bogenmaß um
  16:/// </summary>
  17:/// <param name="angle">Winkel in Grad</param>
  18:/// <returns>Winkel als Bogenmaß</returns>
  19:publicstaticdouble DegreeToRadian(double angle)
  20:     {
  21:return Math.PI * angle / 180.0;
  22:     }
  23:  
  24:/// <summary>
  25:/// Wandelt Bogenmaß in Grad um
  26:/// </summary>
  27:/// <param name="angle">Winkel als Bogenmaß</param>
  28:/// <returns>Winkel in Grad</returns>
  29:publicstaticdouble RadianToDegree(double angle)
  30:     {
  31:return angle * (180.0 / Math.PI);
  32:     }
  33:  
  34:/// <summary>
  35:/// Bildet wert auf neuen Wertebereich ab
  36:/// </summary>
  37:/// <param name="value">Abzubildender Wert</param>
  38:/// <param name="oldMin">altes Minimum</param>
  39:/// <param name="oldMax">altes Maximum</param>
  40:/// <param name="newMin">neues Minimum</param>
  41:/// <param name="newMax">neues maximum</param>
  42:/// <returns>auf neuen Bereich Abgebildeter Wert</returns>
  43:publicstaticdouble RemapValue(doublevalue, double oldMin, double oldMax, double newMin, double newMax)
  44:     {
  45:return (value - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin;
  46:     }
  47: }
Abb. 3: XAML-Code für unser Tacho-Control

 

Nachdem wir unsere Hilfsklasse angelegt haben, können wir uns nun der Benutzeroberfläche unseres Tacho-Control widmen. Dieses soll vorerst aus einem einfachen Zeiger bestehen (siehe Abb. 4).

   1:<UserControlx: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:mc:Ignorable="d"
   8:d:DesignHeight="300"d:DesignWidth="300">
   9:<!--Container für Control. Behandelt den UserInput zum einstellen des Zeigers-->
  10:<Gridx:Name="LayoutRoot"MouseMove="LayoutRoot_MouseMove"MouseDown="LayoutRoot_MouseDown"MouseUp="LayoutRoot_MouseUp"Background="Transparent">
  11:<!--repräsentiert den Zeiger, dünne Seite repräsentiert aktuellen Wert-->
  12:<Polygonx: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">
  13:</Polygon>
  14:<!--kennzeichnet Mittelpunkt des Tacho-->
  15:<BorderBackground="LightGray"Width="5"Height="5"HorizontalAlignment="Center"VerticalAlignment="Center"CornerRadius="2"/>
  16:</Grid>
  17:</UserControl>
Abb 4: XAML für das Tacho-Control

 

Nachdem wir das Userinterface definiert haben, müssen wir uns Gedanken machen, passende Eigenschaften für unser Control als Dependency Properties zu definieren. Diese sollen sich wie folgt gestalten:

  1. Minimum: Gibt die Untergrenze des einzustellenden Wertes an.
  2. Maximum: Gibt die Obergrenze des einzustellenden Wertes an.
  3. Value: Gibt den aktuell eingestellten Wert an oder legt diesen fest.
  4. TickFrequency: Gibt an, wie viele Ticks im Bereich von Minimum bis Maximum platziert werden sollen.
  5. IsSnapToTickEnabled: Legt fest, ob der Zeiger ausschließlich auf Werte der TickFrequency gesetzt werden kann.
  6. TickCount: Enthält die Anzahl der anzuzeigenden Ticks, berechnet aus Minimum, Maximum und TickFrequency. Diese Eigenschaft wird erst später für die Anzeige der Skala benötigt.

In Abb. 5 sehen wir die Implementierung aller zuvor aufgelisteten Dependency Properties sowie deren Callback Handler.

   1:/// <summary>
   2:/// aktiviert das automatische snappen an Tciks im Ziffernblatt
   3:/// </summary>
   4:publicbool IsSnapToTickEnabled
   5: {
   6:    get { return (bool)GetValue(IsSnapToTickEnabledProperty); }
   7:    set { SetValue(IsSnapToTickEnabledProperty, value); }
   8: }
   9:  
  10:// Using a DependencyProperty as the backing store for IsSnapToTickEnabled.  This enables animation, styling, binding, etc...
  11:publicstaticreadonly DependencyProperty IsSnapToTickEnabledProperty =
  12:    DependencyProperty.Register("IsSnapToTickEnabled", typeof(bool), typeof(GaugeControl), new PropertyMetadata(false));
  13:  
  14:  
  15:/// <summary>
  16:/// Frequenz für Skala des Messgerätes
  17:/// </summary>
  18:publicint TickFrequency
  19: {
  20:    get
  21:    {
  22:return (int)GetValue(TickFrequencyProperty);
  23:    }
  24:    set
  25:    {
  26:        SetValue(TickFrequencyProperty, value);
  27:    }
  28: }
  29:  
  30:/// <summary>
  31:/// Dependency Property für TickFrequency
  32:/// </summary>
  33:publicstaticreadonly DependencyProperty TickFrequencyProperty =
  34:    DependencyProperty.Register("TickFrequency", typeof(int), typeof(GaugeControl), new PropertyMetadata(12, OnTickFrequencyChanged));
  35:  
  36:/// <summary>
  37:/// Callback für Änderung der Tickfrequenz
  38:/// berechnet neue Tickfrequenz abhängig vom Minimum und Maximum
  39:/// </summary>
  40:/// <param name="d">Modifizierte DependencyObject</param>
  41:/// <param name="e">Infos über geänderte Attribute</param>
  42:privatestaticvoid OnTickFrequencyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  43: {
  44:    var t = d as GaugeControl;
  45:  
  46:if (t != null)
  47:    {
  48:double newTicks = ((t.Maximum - t.Minimum) / (int)e.NewValue);
  49:        t.TickCount = Convert.ToInt32(newTicks);
  50:    }
  51: }
  52:  
  53:/// <summary>
  54:/// Enthält die berechnete Anzahl der anzuzeigenden Ticks aus Tickfrequency, Minimum und Maximum
  55:/// </summary>
  56:privateint TickCount
  57: {
  58:    get { return (int)GetValue(TickCountProperty); }
  59:    set { SetValue(TickCountProperty, value); }
  60: }
  61:  
  62:// Using a DependencyProperty as the backing store for TickCount.  This enables animation, styling, binding, etc...
  63:publicstaticreadonly DependencyProperty TickCountProperty =
  64:    DependencyProperty.Register("TickCount", typeof(int), typeof(GaugeControl), new PropertyMetadata(null));
  65:  
  66:  
  67:  
  68:/// <summary>
  69:/// Aktueller Anzeigewert des Messgerätes
  70:/// </summary>
  71:publicdouble Value
  72: {
  73:    get { return (double)GetValue(ValueProperty); }
  74:    set { SetValue(ValueProperty, value); }
  75: }
  76:  
  77:/// <summary>
  78:/// Dependency Property für Aktuellen Anzeigewert
  79:/// </summary>
  80:publicstaticreadonly DependencyProperty ValueProperty =
  81:    DependencyProperty.Register("Value", typeof(double), typeof(GaugeControl), new PropertyMetadata(0.0, OnValueChanged));
  82:  
  83:/// <summary>
  84:/// Anpassung der Rotation des Zeigers unter Einbezug des Minimum und Maximum falls eine Wertänderung vorliegt
  85:/// </summary>
  86:/// <param name="d"></param>
  87:/// <param name="e"></param>
  88:privatestaticvoid OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  89: {
  90:    var t = d as GaugeControl;
  91:  
  92:if (t != null)
  93:    {
  94:double newValue = (double)e.NewValue;
  95:double newAngle = MathExtensions.RemapValue(newValue, t.Minimum, t.Maximum, 0, 360);
  96:        t.Needle.RenderTransform = new RotateTransform(newAngle);
  97:    }
  98: }
  99:  
 100:/// <summary>
 101:/// Untere Grenze des Wertebereichs für das Messgerät
 102:/// </summary>
 103:publicdouble Minimum
 104: {
 105:    get { return (double)GetValue(MinimumProperty); }
 106:    set { SetValue(MinimumProperty, value); }
 107: }
 108:  
 109:/// <summary>
 110:/// Dependency Property für Minimum
 111:/// </summary>
 112:publicstaticreadonly DependencyProperty MinimumProperty =
 113:    DependencyProperty.Register("Minimum", typeof(double), typeof(GaugeControl), new PropertyMetadata(0.0));
 114:  
 115:/// <summary>
 116:/// Obere Grenze des Wertebereichs für das Messgerät
 117:/// </summary>
 118:publicdouble Maximum
 119: {
 120:    get { return (double)GetValue(MaximumProperty); }
 121:    set { SetValue(MaximumProperty, value); }
 122: }
 123:  
 124:/// <summary>
 125:/// Dependency Property für Maximum
 126:/// </summary>
 127:publicstaticreadonly DependencyProperty MaximumProperty =
 128:    DependencyProperty.Register("Maximum", typeof(double), typeof(GaugeControl), new PropertyMetadata(360.0));
Abb. 5: Implementierung aller Dependency Properties in der GaugeControl.xaml.cs

 

Nun ist die Basis für unser Tacho-Control schon so gut wie fertig. Zum Schluss müssen wir nur noch die Nutzerinteraktion für unseren Zeiger behandeln, damit der aktuelle Wert auch mithilfe von diesem eigestellt werden kann. Dafür müssen wir lediglich den Code aus Abb. 5 unserer Klasse GaugeControl.xaml.cs hinzufügen.

   1:/// <summary>
   2:/// Flag um Zustand MousePressed festzuhalten
   3:/// </summary>
   4:privatebool mousePressed = false;
   5:  
   6:/// <summary>
   7:/// Iteraktionslogik für das Einstellen eines neuen Wertes mittels Maus
   8:/// </summary>
   9:/// <param name="sender">Auslöser</param>
  10:/// <param name="e">MouseEvent Argumente</param>
  11:privatevoid LayoutRoot_MouseMove(object sender, MouseEventArgs e)
  12: {
  13:// zustand Maus gedrückt
  14:if (mousePressed)
  15:     {
  16:         FrameworkElement fe = sender as FrameworkElement;
  17:  
  18:if (fe != null)
  19:         {
  20:             Point startPoint = new Point(fe.ActualWidth / 2, fe.ActualHeight / 2);
  21:             Point endPoint = Mouse.GetPosition(fe);
  22:  
  23:// Berechnung des Winkels zwischen Zentrum und aktueller Mausposition
  24:double newAngle = MathExtensions.AngleBetween(startPoint, endPoint);
  25:             newAngle = newAngle < 0 ? newAngle + 360 : newAngle;
  26:  
  27:if (this.IsSnapToTickEnabled)
  28:             {
  29:// Berechnung des Versatzen/Abstand zwischen Elementen anhand der Anzahl der Elemente
  30:double degreesOffset = 360.0 / this.TickCount;
  31:  
  32:for (int i = 0; i < this.TickCount; i++)
  33:                 {
  34:double leftAngle = degreesOffset * i;
  35:double rightAngle = leftAngle + degreesOffset;
  36:  
  37:if (newAngle >= leftAngle && newAngle <= rightAngle)
  38:                     {
  39:double distleft = Math.Abs(leftAngle - newAngle);
  40:double distright = Math.Abs(rightAngle - newAngle);
  41:  
  42:if (distleft <= distright)
  43:                             newAngle = leftAngle;
  44:else
  45:                             newAngle = rightAngle;
  46:                     }
  47:                 }
  48:             }
  49:  
  50:// Abbilden des Winkels auf Wertebereich
  51:double remappedValue = MathExtensions.RemapValue(newAngle, 0, 360, this.Minimum, this.Maximum);
  52:  
  53:this.Value = remappedValue;
  54:         }
  55:     }
  56:  
  57:     e.Handled = true;
  58: }
  59:  
  60:/// <summary>
  61:/// Initialisiert die Wertänderung des Controls mittels Maus
  62:/// </summary>
  63:/// <param name="sender">Auslöser</param>
  64:/// <param name="e">MouseEvent Argumente</param>
  65:privatevoid LayoutRoot_MouseDown(object sender, MouseButtonEventArgs e)
  66: {
  67:this.mousePressed = true;
  68:     var element = e.Source as IInputElement;
  69:     Mouse.Capture(element);
  70:     e.Handled = true;
  71: }
  72:  
  73:/// <summary>
  74:/// Schließt den Vorgang der Wertänderung mittels Maus ab
  75:/// </summary>
  76:/// <param name="sender">Auslöser</param>
  77:/// <param name="e">MouseEvent Argumente</param>
  78:privatevoid LayoutRoot_MouseUp(object sender, MouseButtonEventArgs e)
  79: {
  80:this.mousePressed = false;
  81:     Mouse.Capture(null);
  82:     e.Handled = true;
  83: }
Abb. 5: Eventbehandlung zum einstellen des Zeigers

 

Nachdem wir alle Schritte umgesetzt haben, sollte nun die Grundfunktionalität für unser Control gewährleistet sein. Wir können Minimum und Maximum einstellen, den Wert setzen bzw. an andere Controls wie z.B. einen Slider binden, eine Tick-Frequenz angeben und Einstellen ob nur Werte nach Tick-Frequenz erlaubt sein sollen. Im nachfolgenden Teil werden wir unser Control mit einem Custom Panel ausstatten, um eine Skala anzuzeigen.

 

Viel Spaß damit!


Viewing all articles
Browse latest Browse all 1198

Trending Articles


Vimeo 10.7.1 by Vimeo.com, Inc.


UPDATE SC IDOL: TWO BECOME ONE


KASAMBAHAY BILL IN THE HOUSE


Girasoles para colorear


Presence Quotes – Positive Quotes


EASY COME, EASY GO


Love with Heart Breaking Quotes


Re:Mutton Pies (lleechef)


Ka longiing longsem kaba skhem bad kaba khlain ka pynlong kein ia ka...


Vimeo 10.7.0 by Vimeo.com, Inc.


FORECLOSURE OF REAL ESTATE MORTGAGE


FORTUITOUS EVENT


Pokemon para colorear


Sapos para colorear


Smile Quotes


Letting Go Quotes


Love Song lyrics that marks your Heart


RE: Mutton Pies (frankie241)


Hato lada ym dei namar ka jingpyrshah jong U JJM Nichols Roy (Bah Joy) ngin...


Long Distance Relationship Tagalog Love Quotes