Balkendiagramm mit WPF
Seit letztem Jahr habe ich an meinem kleinen Programm gebastelt, mit dem man die RKI Daten sichten kann.
Diesmal habe ich die Darstellung eines Balkendiagramms gewählt, welches mit wenigen Schritten realisierbar ist.
Anforderung
Ein neues Steuerelement soll so erstellt werden, dass es im eigenen Projekt immer wieder verwendet werden kann. Hier soll eine Liste mit Werten möglich sein, in das Bar Diagramm zu ‚binden‘. Die Anzahl der Elemente bestimmt die Breite des einzelnen Balken und der Wert wiederum die Höhe.
Neues Steuerelement anlegen
Das Anlegen des neue Steuerelements sollte in einem eigenen Order erfolgen. Mit rechter Maustaste und dann im Kontext-Menü Add -> User Control (WPF)… beginnen wir mit dem Benutzersteuerelement.
Gebt einen Namen für das Steuerelement ein, hier empfehle ich am Ende Control hinzuzufügen. In einigen Projekten werden z.B. benutzerdefinierte Steuerelemente und Ansichten (Views) unterschieden.
Nach dem Erstellen sollte unter der .XAML- auch eine .XAML.cs-Datei bestehen. In diese werden die Dependency Properties hinzugefügt, mit dem es später möglich ist, die Liste mit Werten zu ‚binden‘.
Zunächst brauchen wir im XAML Code ein StackPanel, in dem die Werte als Balken abgebildet werden. Idealerweise kann das Grid gegen ein Border Element ausgetauscht werden. Auch ein Rand in der Farbe eurer Wahl sollte nicht fehlen. Früher oder später stellt man fest, dass der Rand zur Orientierung hilft, das Diagramm in seinen Grenzen zu lesen.
Das StackPanel wird direkt in das Border Element untergeordnet. Hier muss die Ausrichtung der Orientierung und die Flow Direction von links nach rechts festgelegt werden. Da die Balken von links beginnen, muss das Ausrichten horizontal nach links ausgerichtet sein. Zur Erleichterung bekommt das StackPanel einen Namen zugewiesen, der über den Codeabschnitt verwendet wird. Das erleichtert das Hinzufügen der Balken.
<UserControl x:Class="ExampleBarDiagram.Diagram.BarDiagramControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
SizeChanged="UserControl_SizeChanged">
<Border BorderThickness="1"
BorderBrush="Chartreuse"
Margin="0">
<StackPanel x:Name="SimpleDiagram"
Orientation="Horizontal"
FlowDirection="LeftToRight"
HorizontalAlignment="Left" />
</Border>
</UserControl>
Für die Liste mit dem Werten empfiehlt sich das Anlegen eines Datenobjektes mit dem Namen DiagramLevelItem. Dieser kann zum einen den double Wert sowie einen Text aufnehmen. Neben dem Anlegen der Property wird die Static readonly Dependency angelegt und ist nur für diese Klasse gültig. In der PropertyMetadata wird eine neue Liste erstellt, falls diese nicht zugewiesen wurde, aber versucht wird diese abzurufen. Mit dem zweiten Parameter wird eine Static Methode übergeben, die ausgeführt wird, wenn sich der Wert der Property geändert hat.
public partial class BarDiagramControl
{
public List<DiagramLevelItem> DiagramLevelItemsSource
{
get => (List<DiagramLevelItem>)this.GetValue(DiagramLevelItemsSourceProperty);
set => this.SetValue(DiagramLevelItemsSourceProperty, value);
}
public static readonly DependencyProperty DiagramLevelItemsSourceProperty =
DependencyProperty.RegisterAttached("DiagramLevelItemsSource",
typeof(List<DiagramLevelItem>),
typeof(BarDiagramControl),
new PropertyMetadata(new List<DiagramLevelItem>(), UpdateDiagram));
private static void UpdateDiagram(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BarDiagramControl control)
{
SetValuesToBarDiagram(control);
}
}
...
Der Hauptteil für das Ausfüllen des StackPanel wird schließlich mit der Methode SetValuesToBarDiagram(BarDiagramControl control) ermöglicht. Hier wird für die aktuelle Höhe die Skalierung der einzelnen Werte in Balken verwendet, damit die Balken sich an die verwendete Größe des Steuerelements anpassen.
Die einzelnen Balken sind schlicht aus Rectangles erstellt, die mit einer Farbe gefüllt sind. Ausgerichtet sind sie immer nach unten und die Höhe wird aus dem skalierten Wert gesetzt. Die Breite selbst wird in Abhängigkeit der Breite des Steuerelementes und der Anzahl der Werte festgelegt.
Mit dem gegeben Namen des StackPanels, kann über die Property Children das neue Rectangle Instanz hinzugefügt werden.
...
private static void SetValuesToBarDiagram(BarDiagramControl control)
{
if (control.DiagramLevelItemsSource == null)
{
return;
}
control.SimpleDiagram.Children.Clear();
var heightScale = control.ActualHeight / 100d;
var widthPerResult = control.ActualWidth / control.DiagramLevelItemsSource.Count;
foreach (var diagramLevelItem in control.DiagramLevelItemsSource)
{
var heightValue = diagramLevelItem.Value * heightScale;
var barItem = new Rectangle
{
Fill = new SolidColorBrush(Colors.DarkGreen),
VerticalAlignment = VerticalAlignment.Bottom,
Width = widthPerResult,
Height = heightValue,
ToolTip = diagramLevelItem.ToolTipText
};
control.SimpleDiagram.Children.Add(barItem);
}
}
...
Damit die Balken beim Verändern der Größe des Steuerelementes angepasst werden, wird auf das Event SizeChanged zurückgegriffen. Ab hier wird dann auch sichtbar, warum der Codeanteil separat in einer Methode aufgeführt wurde.
...
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) => SetValuesToBarDiagram(this);
}
Fast fertig
Im Grunde ist das neue Steuerelement - Diagram fertig und kann im Projekt eingesetzt werden. In diesem Beispiel wird das Steuerelement in das MainWindow.xaml hinzugefügt. Das Projekt muss einmal gebaut werden, damit das Steuerelement über die Toolbox, per klicken und ziehen in die Zielansicht gezogen werden kann.
Im Beispiel wurde für MainWindow.xaml ein ViewModel angelegt, dass das Interface von INotifyPeropertyChanged implementiert. Neben der Liste mit den Werten (DiagramLevelItem) kommt eine Property CommandCreateNewListWithValues mit dem Interface ICommand.
public class MainWindowViewModel : INotifyPropertyChanged
{
private List<DiagramLevelItem> _items;
private ICommand _commandCreateNewListWithValues;
public List<DiagramLevelItem> Items
{
get => this._items;
set
{
if (!Equals(value, this._items))
{
this._items = value;
this.OnPropertyChanged();
}
}
}
public ICommand CommandCreateNewListWithValues
{
get => this._commandCreateNewListWithValues;
set
{
if (Equals(value, this._commandCreateNewListWithValues)) return;
this._commandCreateNewListWithValues = value;
this.OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Das ViewModel muss zuerst in den Window.DataContext eingetragen werden. Nun kann die Liste mit dem Diagram über die DependencyProperty DiagramLevelItemsSource gebunden werden. Als zweites kommt noch ein Button hinzu, der über Command die zweite Property vom Typ IComand bindet.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ExampleBarDiagram"
xmlns:Diagram="clr-namespace:ExampleBarDiagram.Diagram"
x:Class="ExampleBarDiagram.MainWindow"
mc:Ignorable="d"
Title="Example Bar Diagram" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Diagram:BarDiagramControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" DiagramLevelItemsSource="{Binding Items}"/>
<Button Content="create new list with values" Grid.Row="1" Command="{Binding CommandCreateNewListWithValues}" />
</Grid>
</Window>
Über den Behind code MainWindow.xaml.cs wird im Konstruktor nun das ViewModel aus dem DataKontext abgerufen. Hier muss zum einen für den Button Command die Instanz erstellt werden und, zum anderen über dem Parameter wiederum das ViewModel übergeben werden. Hierfür erstellen wir die Klasse ButtonCommandCreateNewListWithValues.
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
var viewModel = (MainWindowViewModel) this.DataContext;
viewModel.CommandCreateNewListWithValues = new ButtonCommandCreateNewListWithValues(viewModel);
}
}
Die Klasse ButtonCommandCreateNewListWithValues implementiert das Interface IComand. Über den Konstruktor wird das übergebene ViewModel in den Private Member referenziert. Die Methode CanExecute wird eigentlich nicht verwendet und gibt da her nur true zurück.
Mit der Methode Execute wird mit der Random Klasse die Werte erzeugt. Da das Diagramm für Werte bis 100 ausgelegt ist, sollte der maximale Wert des Einzelnen auch bei 100 sein. Die Anzahl der angelegten Werte sollte sich darauf begrenzen, wie viele Pixel in der Breite für das Diagram zur Verfügung stehen. Die Liste mit den Werten wird erst am Ende zugewiesen, da sonst die PropertyChanged nicht ausgeführt wird.
Schlusswort
Die Ausführung ist schon sehr kompakt und lässt sich auch noch etwas kürzer fassen. Einige Stellen habe ich aus gewissen Gründen mehr ausgebaut als nötig. Denn ich will in weiteren Blog Einträgen beschreiben, wie man dieses Steuerelement weiter ausbauen kann und dies natürlich in kurzen Schritten.
Das Beispiel ist eine entkoppelte Darstellung des verwenden ‚Steuerelement‘ Diagram, welches ich bereits in einem anderem kleinen privaten Projekt verwende.
Wie immer habe ich die Solution auf Github hochgeladen:
Kommentare