I've defined a control Template for a progressbar to make it look like a thermometer ... now i want it to change its color when reaching a certain value (for example .. when the progressbar has the value 70, its color should change to yellow)
Currently the color of PART_Indicator is bound to the background color of the progressbar .. the backgroundcolor is changed in the ValueChanged Eventhandler, and so the color of the indicator changes too... is there a possibility to do this within the template only, so i dont need to use the ValueChanged Eventhandler?
<ControlTemplate x:Key="myThermometer" TargetType="{x:Type ProgressBar}">
<ControlTemplate.Resources>
<RadialGradientBrush x:Key="brushBowl" GradientOrigin="0.5 0.5">
<GradientStop Offset="0" Color="Pink" />
<GradientStop Offset="1" Color="Red" />
</RadialGradientBrush>
</ControlTemplate.Resources>
<Canvas>
<Path Name="PART_Track" Stroke="Black" StrokeThickness="5" Grid.Column="0">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Union">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="10,40,130,20" RadiusX="5" RadiusY="5"/>
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="10,50" RadiusX="20" RadiusY="20"/>
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
<Path Fill="{TemplateBinding Background}">
<Path.Data>
<EllipseGeometry Center="10,50" RadiusX="17" RadiusY="17"/>
</Path.Data>
</Path>
<Path Name="PART_Indicator" Fill="{TemplateBinding Background}" Grid.Column="0">
<Path.Data>
<RectangleGeometry Rect="22,43,115,15" RadiusX="5" RadiusY="5"/>
</Path.Data>
</Path>
<Canvas Canvas.Top="35" Canvas.Right="375">
<Canvas.RenderTransform>
<RotateTransform CenterX="120" CenterY="120" Angle="-270" />
</Canvas.RenderTransform>
<TextBlock FontWeight="Bold" FontSize="16" Foreground="Black" Text="{TemplateBinding Tag}"/>
</Canvas>
</Canvas>
</ControlTemplate>
<ProgressBar x:Name="progressBar" Background="{Binding RelativeSource={RelativeSource Self},Path=Value,Converter={StaticResource IntToBrushConverter}}" />
public class IntToBrushConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double val = (double)value;
if (val > 50)
return Brushes.Blue;
else
return Brushes.Green;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("Not Implemented");
}
#endregion
}
more or less - I'd write a custom ValueConverter that converts your progress value to a Color and bind the fill of the progress bar using this converter.
XAML:
Fill="{TemplateBinding Progress,
Converter={StaticResource ProgressToColorConverter}}"
Code:
[ValueConversion(typeof(int), typeof(Color))]
public class ProgressToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int progress = (int)value;
if (progress < 60)
return Color.Green;
else
return Color.Red;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This code is just from the top of my head and hasn't been tested, but it should show the principles.
Related
I have an image with opacity mask and wish to add possibility to resize/reposition that opacity mask.
I'm a complete WPF newbie, so may be may thinking is completely wrong, feel free to throw any ideas at me :)
So I have this:
<Canvas Name="MainCanvas">
<Rectangle Width="197.016" Height="120.896" Canvas.Left="76.119" Canvas.Top="73.134" Name="SelectionRectangle"></Rectangle>
<Image Source="Chrysanthemum.jpg" Width="{Binding ActualWidth, ElementName=MainCanvas, Mode=OneWay}" Height="{Binding ActualHeight, ElementName=MainCanvas, Mode=OneWay}">
<Image.OpacityMask>
<RadialGradientBrush MappingMode="Absolute"
Center="{Binding ElementName=SelectionRectangle, Converter={StaticResource RectangleConverter}}"
GradientOrigin="{Binding ElementName=SelectionRectangle, Converter={StaticResource RectangleConverter}}"
RadiusY="{Binding ElementName=SelectionRectangle, Path=ActualHeight, Converter={StaticResource MathConverter}, ConverterParameter=#VALUE/2}"
RadiusX="{Binding ElementName=SelectionRectangle, Path=ActualWidth, Converter={StaticResource MathConverter}, ConverterParameter=#VALUE/2}">
<GradientStop Color="#7C15161F" Offset="1" />
<GradientStop Color="#FF40499E" Offset="0.999" />
<GradientStop Color="#FF182395" Offset="0" />
</RadialGradientBrush>
</Image.OpacityMask>
</Image>
</Canvas>
So there's image with RadialGradientBrush mask and I'm trying to bind RadialGradientBrush center to rectangles center using this converter:
public class RectangleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var rectangle = (Rectangle)value;
return new Point(Canvas.GetLeft(rectangle) + rectangle.ActualWidth / 2f, Canvas.GetTop(rectangle) + rectangle.ActualHeight / 2f);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
Everything compiles and runs, but works incorrectly, I guess because when Rectangle position changes, RadialGradientBrush Center binding does not know about that (at least breaking point in converter is not hit by changing rectangle's position).
What would be the easiest/recommended way to fix this?
I've found, that Multibinding can solve this problem:
<Canvas Name="MainCanvas">
<Rectangle Width="197.016" Height="120.896" Canvas.Left="76.119" Canvas.Top="73.134" Name="SelectionRectangle"></Rectangle>
<Image Source="Chrysanthemum.jpg" Width="{Binding ActualWidth, ElementName=MainCanvas, Mode=OneWay}" Height="{Binding ActualHeight, ElementName=MainCanvas, Mode=OneWay}">
<Image.OpacityMask>
<RadialGradientBrush MappingMode="Absolute"
RadiusY="{Binding ElementName=SelectionRectangle, Path=ActualHeight, Converter={StaticResource MathConverter}, ConverterParameter=#VALUE/2}"
RadiusX="{Binding ElementName=SelectionRectangle, Path=ActualWidth, Converter={StaticResource MathConverter}, ConverterParameter=#VALUE/2}">
<RadialGradientBrush.Center>
<MultiBinding Converter="{StaticResource RectangleConverter}">
<Binding ElementName="SelectionRectangle" Path="(Canvas.Left)"></Binding>
<Binding ElementName="SelectionRectangle" Path="ActualWidth"></Binding>
<Binding ElementName="SelectionRectangle" Path="(Canvas.Top)"></Binding>
<Binding ElementName="SelectionRectangle" Path="ActualHeight"></Binding>
</MultiBinding>
</RadialGradientBrush.Center>
<RadialGradientBrush.GradientOrigin>
<MultiBinding Converter="{StaticResource RectangleConverter}">
<Binding ElementName="SelectionRectangle" Path="(Canvas.Left)"></Binding>
<Binding ElementName="SelectionRectangle" Path="ActualWidth"></Binding>
<Binding ElementName="SelectionRectangle" Path="(Canvas.Top)"></Binding>
<Binding ElementName="SelectionRectangle" Path="ActualHeight"></Binding>
</MultiBinding>
</RadialGradientBrush.GradientOrigin>
<GradientStop Color="#7C15161F" Offset="1" />
<GradientStop Color="#FF40499E" Offset="0.999" />
<GradientStop Color="#FF182395" Offset="0" />
</RadialGradientBrush>
</Image.OpacityMask>
</Image>
</Canvas>
And converter:
public class RectangleConverter : IMultiValueConverter
{
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return new Point((double)values[0] + (double)values[1] / 2d, (double)values[2] + (double)values[3] / 2d);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return null;
}
}
Not sure if I'm right, but my explanation would be - in question I was binding to whole object, so it is not a dependency property, so mask was not notified about the changes.
Now multibinding let's to specify which exactly properties are interesting and value converter get's notification when any of them change.
Right now I am "cheating" and using the following:
<Rectangle x:Name="rectangle" Stroke="SlateGray"
Width="{TemplateBinding ActualWidth}" Height="{TemplateBinding ActualHeight}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
SizeChanged="rectangle_SizeChanged">
</Rectangle>
<x:Code>
<![CDATA[ private void rectangle_SizeChanged(object sender, SizeChangedEventArgs e)
{
Rectangle r = sender as Rectangle;
r.RadiusX = r.Height / 2;
r.RadiusY = r.Height / 2;
}
]]>
</x:Code>
This x:Code works perfectly at run time and accomplishes what I want. but I really want it to change instantly on the Artboard by doing something like:
<Rectangle x:Name="rectangle" Stroke="SlateGray"
Width="{TemplateBinding ActualWidth}" Height="{TemplateBinding ActualHeight}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
RadiusX=".5*({TemplateBinding ActualHeight})"
RadiusY=".5*({TemplateBinding ActualHeight})">
</Rectangle>
But there is no way to include this .5*(...) Is there another way to accomplish this?
To run code in a binding you use a converter class.
public class MultiplyConverter : IValueConverter
{
public double Multipler{ get; set; }
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
double candidate = (double)value;
return candidate * Multipler ;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Then add the converter in a Resources section.
<Window.Resources>
<local:MultiplyConverter x:Key="MultiplyConverter" Multipler="5"/>
</Window.Resources>
And add the coverter to your binding.
<Rectangle x:Name="rectangle" Fill="#FFA4A4E4"
RadiusX="{Binding ActualHeight, Converter={StaticResource MultiplyConverter}, ElementName=rectangle}"
RadiusY="{Binding ActualWidth, Converter={StaticResource MultiplyConverter}, ElementName=rectangle,}" />
You can use the Blend binding windows to automatically add the resource and binding.
<UserControl x:Class="WpfApplication2.ProgressBar"
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="300" d:DesignWidth="300">
<Grid>
<ProgressBar Minimum="0" Maximum="1" Value="0.5" LargeChange="0.1" SmallChange="0.01" Margin="2,2,12,2" Height="22">
<ProgressBar.Template>
<ControlTemplate>
<Border BorderThickness="2" BorderBrush="Black">
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0">
<LinearGradientBrush.EndPoint>
<Point Y="0" X="{Binding RelativeSource={RelativeSource AncestorType={x:Type ProgressBar}}, Path=ProgressBar.Value}"/>
</LinearGradientBrush.EndPoint>
<GradientStop Color="Transparent" Offset="1.01"/>
<GradientStop Color="#FF0000" Offset="1.0"/>
<GradientStop Color="#FFFF00" Offset="0.50"/>
<GradientStop Color="#00FF00" Offset="0.0"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Border>
</ControlTemplate>
</ProgressBar.Template>
</ProgressBar>
<TextBlock Text="50%" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</UserControl>
I get error: "A 'Binding' cannot be set on the 'X' property of type 'Point'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject."
Is there any clean workaround?
Since Point.X isn't a Dependency Property you can't bind it to something. You could bind the EndPointProperty though, and use a Converter that creates the Point for you. It could take the Y value as parameter for example
Xaml
<LinearGradientBrush.EndPoint>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ProgressBar}}"
Path="Value"
Converter="{StaticResource PointXConverter}"
ConverterParameter="0"/>
</LinearGradientBrush.EndPoint>
PointXConverter
public class PointXConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double progressBarValue = (double)value;
double yValue = System.Convert.ToDouble(parameter);
return new Point(progressBarValue, yValue);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Note: Probably not related to your question but if you would need to bind Y as well, you can use a MultiBinding like this
<LinearGradientBrush.EndPoint>
<MultiBinding Converter="{StaticResource PointConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ProgressBar}}"
Path="Value"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ProgressBar}}"
Path="Value"/>
</MultiBinding>
</LinearGradientBrush.EndPoint>
PointConverter
public class PointConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double xValue = (double)values[0];
double yValue = (double)values[1];
return new Point(xValue, yValue);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The following code binds a GradientStop to the Background.Color property of TemplatedParent. Everything works but I am getting a binding error in the output window:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=Background.Color; DataItem=null; target element is 'GradientStop' (HashCode=6944299); target property is 'Color' (type 'Color')
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WpfBindingTest.MainWindow"
x:Name="Window"
Title="MainWindow"
Width="100" Height="100">
<Window.Resources>
<ControlTemplate x:Key="GradientTemplate" TargetType="{x:Type ContentControl}">
<Border BorderThickness="1" BorderBrush="{TemplateBinding Background}">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="{Binding Path=Background.Color,
RelativeSource={RelativeSource TemplatedParent}}" Offset="1"/>
<GradientStop Color="White" Offset="0"/>
</LinearGradientBrush>
</Border.Background>
<ContentPresenter/>
</Border>
</ControlTemplate>
</Window.Resources>
<Grid x:Name="LayoutRoot">
<ContentControl Background="Green" Template="{StaticResource GradientTemplate}" >
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="X" />
</ContentControl>
</Grid>
</Window>
I also had the same error in the Visual Studio console output.
A possible explanation and workaround for this is reported here
Basically if you use a Converter that returns a LinearGradientBrush then you don't get the error
The code is something like this
[ValueConversion(typeof(System.Windows.Media.Color), typeof(LinearGradientBrush))]
class GradientConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var brush = new LinearGradientBrush();
var color = (Color)value;
brush.StartPoint = new Point(0.5, 0);
brush.EndPoint = new Point(0.5, 1);
brush.GradientStops.Add(new GradientStop(Colors.White, 0));
brush.GradientStops.Add(new GradientStop((Color)value, 1));
return brush;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
And in the XAML
<Border BorderThickness="1" BorderBrush="{TemplateBinding Background}" Background="{Binding Path=Background.Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource gradConv}}">
I have a question regarding how to best accomplish something in WPF MVVM. I have in my ViewModel a series of integers. For the sake of example, lets call them:
public int Yellow
{
get;set;
}
public int Red
{
get;set;
}
public int Green
{
get;set;
}
I also have some small images that are very simple: A Red circle, a Yellow circle, and a Green circle. The idea is to have an area on the view with a number of these images, based on the above properties. So if this instance of the view model has 3 Yellow, 2 Red, and 1 Green, I want 6 images in my ListBox, 3 of the yellow circle, 2 of the red, and 1 of the green. Right now, I have it working, but using some very clumsy code where I build the image list in the ViewModel using an ugly for-loop. Is there some more elegant way to accomplish this task in WPF? Ideally, I wouldn't want to have to reference the image in the ViewModel at all...
You could use an ImageBrush to tile a rectangle with an image, and bind the width of the rectangle to the number of copies of the image you want. Something like this:
<StackPanel Orientation="Horizontal">
<StackPanel.LayoutTransform>
<ScaleTransform ScaleX="20" ScaleY="20"/>
</StackPanel.LayoutTransform>
<Rectangle Width="{Binding Yellow}" Height="1">
<Rectangle.Fill>
<ImageBrush
ImageSource="Yellow.png"
Viewport="0,0,1,1"
ViewportUnits="Absolute"
TileMode="Tile"/>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="{Binding Red}" Height="1">
<Rectangle.Fill>
<ImageBrush
ImageSource="Red.png"
Viewport="0,0,1,1"
ViewportUnits="Absolute"
TileMode="Tile"/>
</Rectangle.Fill>
</Rectangle>
<Rectangle Width="{Binding Green}" Height="1">
<Rectangle.Fill>
<ImageBrush
ImageSource="Green.png"
Viewport="0,0,1,1"
ViewportUnits="Absolute"
TileMode="Tile"/>
</Rectangle.Fill>
</Rectangle>
</StackPanel>
Update: As Ray pointed out in his comment, if you are just trying to draw circles then you will get better zoom behavior by using a DrawingBrush than by using an Image:
<StackPanel Orientation="Horizontal">
<StackPanel.LayoutTransform>
<ScaleTransform ScaleX="20" ScaleY="20"/>
</StackPanel.LayoutTransform>
<StackPanel.Resources>
<EllipseGeometry x:Key="Circle" RadiusX="1" RadiusY="1"/>
</StackPanel.Resources>
<Rectangle Width="{Binding Yellow}" Height="1">
<Rectangle.Fill>
<DrawingBrush ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<GeometryDrawing
Brush="Yellow"
Geometry="{StaticResource Circle}"/>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<!-- etc. -->
A possibility would be to use a ValueConverter. It is very flexible, decoupled and helps to let the xaml simple. Here the code for such a value-converter:
public class ImageCountValueConverter : IValueConverter{
public string ImagePath {
get;
set;
}
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
if(null == value){
return Enumerable.Empty<string>();
} else if (value is int) {
List<string> list = new List<string>();
int v = (int)value;
for (int i = 0; i < v; i++) {
if (parameter is string) {
list.Add((string)parameter);
} else {
list.Add(ImagePath);
}
}
return list;
} else {
Type t = value.GetType();
throw new NotSupportedException("The \"" + t.Name+ "\" type is not supported");
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
throw new NotImplementedException();
}
}
The markup would look like this:
<StackPanel>
<ItemsControl ItemsSource="{Binding Yellow,Converter={StaticResource ImageCount_ValueConverter},ConverterParameter=/image/yellow.png}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Stretch="None"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding Red,Converter={StaticResource ImageCount_ValueConverter},ConverterParameter=/image/red.png}" >
...
The declaration would look something like:
<Window.Resources>
<local:ImageCountValueConverter x:Key="ImageCount_ValueConverter" ImagePath="/image/sampleImage.png"/>
</Window.Resources>
Options
Depending on your requirements you can also extend it or change it to work with ImageSource instead of strings or even provide a List<Brush> as output and then use a shape in your DataTemplate where the Brush is set through the Binding.