Introduction
In many projects that are related to process automation, it is necessary to display equipments and the current state in a visual representation. Many equipments, for example, a tank, have some kind of meter that should display a current value between a range (min / max). Let's have a look at how we can do that in a dynamic way using the WPF Viewbox
and some simple calculations.
Structure
For this example, I used the following structure:
- The image we see above is a rendered WPF
UserControl
that should represent a tank. It does the following:
- It has code-behind properties for min and max parameters and for the current level.
- There is also a property that scales the current level from min / max to 0 / 1 (so 0...100%).
- The control is instantiated several times in the main window of the application.
The Equipment User Control
First, let's have a look at the XAML:
<UserControl x:Class="Technewlogic.Samples.MinMaxScaling.Equipment"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="clr-namespace:Technewlogic.Samples.MinMaxScaling"
x:Name="me">
<Grid Margin="5" DataContext="{Binding ElementName=me}">
<Rectangle x:Name="backgroundRect" Fill="LightGray"
Stroke="DarkGray" StrokeThickness="2"
RadiusX="5" RadiusY="5" />
<DockPanel LastChildFill="True" Margin="5">
-->
<Grid Margin="3" DockPanel.Dock="Left">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Top"
HorizontalAlignment="Right">
<TextBlock Text="Max Level:" Margin="2"
VerticalAlignment="Center" />
<TextBox Text="{Binding Max, UpdateSourceTrigger=PropertyChanged}"
Width="30" Margin="2" />
</StackPanel>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
HorizontalAlignment="Right">
<TextBlock Text="Current Level:" FontWeight="Bold"
Margin="2" VerticalAlignment="Center" />
<TextBox Text="{Binding CurrentLevel,
UpdateSourceTrigger=PropertyChanged}"
Width="30" Margin="2" />
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom"
HorizontalAlignment="Right">
<TextBlock Text="Min Level:" Margin="2"
VerticalAlignment="Center" />
<TextBox Text="{Binding Min, UpdateSourceTrigger=PropertyChanged}"
Width="30" Margin="2" />
</StackPanel>
</Grid>
-->
<Border x:Name="LevelSection" CornerRadius="3"
BorderBrush="DarkGray" Background="Gray"
BorderThickness="1" Margin="3">
<Viewbox Stretch="Fill">
<StackPanel Orientation="Horizontal">
<Rectangle Fill="Red" Height="1" Width="0" />
<Rectangle Fill="Blue" Width="1"
VerticalAlignment="Bottom" Height="{Binding ScaledValue}" />
</StackPanel>
</Viewbox>
</Border>
</DockPanel>
</Grid>
</UserControl>
Quite simple!
In the "Enter Data Section", we just bind some textboxes to the Min
, Max
, and CurrentLevel
properties of our control, so that we can play a little bit with the values and see what happens.
Let's have a look at what happens in the "Level Section": here, we have a border that is the last element of its parent DockPanel
, so this means that the border completely fills the remaining space of the DockPanel
. Inside the border, we have a Viewbox
that should stretch its content to fill the remaining space (in other words, the content of the Viewbox
should cover the complete remaining space of our UserControl
).
What we want to achieve is that the level meter has a Dark Gray background (coming from the border) and then have a blue bar showing the scaled value.
The trick is that inside the Viewbox
, we "limit" everything to 1 (Height
and Width
) to have a "logical" or "normalized" size inside the Viewbox
. There is an invisible Rectangle
with Height
= 1 to limit the height, and our blue level Rectangle
with Width
= 1 (to limit the width). The Viewbox
then scales everything to "real" size.
The only thing left open is the scaling in the code-behind of our UserControl
.
private double _min = 0d;
public double Min
{
get { return _min; }
set
{
_min = value;
OnPropertyChanged("Min");
OnPropertyChanged("ScaledValue");
}
}
private double _max = 100d;
public double Max
{
get { return _max; }
set
{
_max = value;
OnPropertyChanged("Max");
OnPropertyChanged("ScaledValue");
}
}
private double _currentLevel = default(double);
public double CurrentLevel
{
get { return _currentLevel; }
set
{
_currentLevel = value;
OnPropertyChanged("CurrentLevel");
OnPropertyChanged("ScaledValue");
}
}
public double ScaledValue
{
get
{
var scaledValue = CurrentLevel / (Max - Min) - (Min / (Max - Min));
return scaledValue;
}
}
Notice that on each change of Min
, Max
, or CurrentLevel
, we also notify that the ScaledValue
has changed, because this property depends on those. Mathematically, we normalize from Min
/ Max
to 0 / 1:
That's it!