I would like to know, how I could make the transition between two background colors more smooth. Is it possible to make some kind of fade transition?
I have created this little sample project to illustrate the behavior.
MainWindow.xaml
<Window x:Class="FadeTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Converter="clr-namespace:FadeTest" Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Converter:BackgroundPercentConverter x:Key="backgroundPercentConverter"/>
</Window.Resources>
<DockPanel>
<Label Content="{Binding PercentComplete}" Height="100" Width="200"
DockPanel.Dock="Top" Foreground="White" FontSize="28"
Background="{Binding PercentComplete, Converter={StaticResource backgroundPercentConverter}}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" />
<Button DockPanel.Dock="Bottom" Click="Button_Click" Width="100" Height="32">Click</Button>
</DockPanel>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyClass {PercentComplete = 0};
}
private void Button_Click(object sender, RoutedEventArgs e)
{
((MyClass) DataContext).PercentComplete++;
}
}
MyClass.cs
class MyClass : INotifyPropertyChanged
{
private int _percentComplete;
public int PercentComplete
{
get { return _percentComplete; }
set
{
if (value >= 10)
value = 0;
_percentComplete = value;
OnPropertyChanged("PercentComplete");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
BackgroundPercentConverter.cs
public class BackgroundPercentConverter : IValueConverter
{
private static readonly SolidColorBrush[] MyColors = new[]
{
new SolidColorBrush(Color.FromRgb(229, 29, 37)),
new SolidColorBrush(Color.FromRgb(252, 52, 0)),
new SolidColorBrush(Color.FromRgb(253, 81, 0)),
new SolidColorBrush(Color.FromRgb(255, 101, 1)),
new SolidColorBrush(Color.FromRgb(255, 133, 0)),
new SolidColorBrush(Color.FromRgb(254, 175, 0)),
new SolidColorBrush(Color.FromRgb(221, 182, 3)),
new SolidColorBrush(Color.FromRgb(173, 216, 2)),
new SolidColorBrush(Color.FromRgb(138, 191, 62)),
new SolidColorBrush(Color.FromRgb(47, 154, 69))
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((int)value < 0 || (int)value >= MyColors.Length)
return MyColors[0];
return MyColors[(int)value];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You could define a StoryBoard which has a ColorAnimation which defines a from and to colour i.e. the colours you want to interpolate smoothly between.
Your animation should target the Color property of a SolidColorBrush which can either be one that is defined as a Resource and is then referenced by your control (i.e. your Label uses {StaticResource xxxxxx} in the Background to refer to it, or your animation could target the Background in your Label directly IF the Background contains a Brush which is a SolidColorBrush (...normally that is the case...but that depends entirely on how the Template for the control is designed).
Then you would need to begin and pause the animation and seek to the position in the animation that corresponds to your percentage. E.g. you could set the Duration to 100 seconds, and then you could simply use your percentage value as the number of seconds in a TimeSpan. (Might have to set IsControllable=true).
You would start and pause the animation when the control is loaded, and then change the seek position in sync with your percentage change.
Note, it's probably a bad idea just having an animation running all the time, just to map a range value to a presentation colour, but it's another option to what you have.
Otherwise stick to your valueconverter and you could calculate the linear interpolation between the 2 colours yourself. See this link:
Algorithm: How do I fade from Red to Green via Yellow using RGB values?
Here are some links related to the animation:
http://social.msdn.microsoft.com/Forums/en/wpf/thread/d2517850-81cc-4d22-be7d-cad522540ad2
How to seek a WPF Storyboard to a specific frame/time offset when not running
Related
I am attempting to update the Rotation Angle of an image in a custom WPF UserControl named MarkerImage. I have a property on MarkerImage named Heading and when that changes, I want the Angle of the image to change. I've tried numerous methods, and they all set the initial angle correctly, but none of them are able to update the angle. Here is the XAML of the control:
Here are some of the methods I've tried:
1) Created a Dependency Property on MarkerImage named Heading.
public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register("Heading", typeof(uint), typeof(MarkerImage), new PropertyMetadata(default(uint)));
Then I set the DataContext of MarkerImage to {RelativeSource Self} and Bound the Angle property of the RotateTransform to that Heading property:
<UserControl x:Class="Pilot2ATC_EFB.Map.MarkerImage"
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"
xmlns:local="clr-namespace:Pilot2ATC_EFB.Map"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
d:DesignHeight="450" d:DesignWidth="800" Width="35" Height="35" x:Name="ctlMarkerImage">
<Image x:Name="userImage" HorizontalAlignment="Center" Height="34" Width="34" RenderTransformOrigin="0.5,0.5" Source="/myApplication;component/Resources/arrow.png" Margin="0,0,0,0" VerticalAlignment="Center">
<Image.RenderTransform>
<RotateTransform x:Name="rotateTransform" Angle="{Binding Path=Heading, UpdateSourceTrigger=PropertyChanged}"/>
</Image.RenderTransform>
</Image>
This correctly sets the Angle when the Control is created and the Heading property is set. However, as the Heading property is updated in the Set{} of the Heading property:
SetValue(HeadingProperty, value % 360);
the image angle does not change.
2)Attempted to set the Angle property directly from the property Heading Set code:
_Heading = value % 360;
rotateTransform.Angle = _Heading;
This again worked to set the initial Angle, but did not change the rotation of the image when Heading was updated.
3) Attempted to replace the RotateTransform with a new one each time the Heading changed:
userImage.RenderTransform = new RotateTransform(_Heading);
This also worked to set the initial Angle, but did nothing when the Heading value changed thereafter.
4) Attempted to do an animation of the Angle property when the Heading changed. (rotateTransform is the x:Name of the RotateTransform element):
var rotAnimation = new DoubleAnimation(Heading, TimeSpan.FromMilliseconds(1));
rotateTransform.BeginAnimation(RotateTransform.AngleProperty, rotAnimation);
And once again, the initial value was set correctly, but updated had no effect.
Here's the code where the Heading property is set when using the DependencyProperty.
public double Heading
{
get { return (double)GetValue(HeadingProperty); }
set
{
if (value < 0)
value = 360;
SetValue(HeadingProperty, value % 360);
}
}
Of course, I confirmed that the code was executing and the heading value was changing after the initial setting, but the image was not being rotated.
I have tried 10+ other variations of these and other suggestions in the forums, but they either threw errors, didn't set the initial value or had the same result as these 4.
It would seem that there is a definitive way to change the rotation angle of an image in a WPF UserControl dynamically from code behind or via Binding, but I am at a loss as to what that would be. ANY help will be greatly appreciated.
Here's a small app that seems to work and demonstrates the changing of the Angle property of a RotateTransform. In fact, there are many ways to do this. This is one that uses a DependencyProperty Approach.
Here's the Application Window XAML without the Window standard content:
<Grid Margin="0,0,2,1">
<local:ImageMarker x:Name="imgMarker" HorizontalAlignment="Left" Height="32"
Margin="45,35,0,0" VerticalAlignment="Top" Width="32"/>
<TextBox x:Name="txtHeading" HorizontalAlignment="Left" Height="23" Margin="174,44,0,0"
TextWrapping="Wrap" Text="0" VerticalAlignment="Top" Width="120" TextChanged="txtHeading_TextChanged"/>
</Grid>
Here's the Application Window code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private Marker marker;
private void txtHeading_TextChanged(object sender, TextChangedEventArgs e)
{
if(marker == null)
marker = new Marker(imgMarker);
if (txtHeading.Text.Length > 0)
{
double val;
var result = double.TryParse(txtHeading.Text, out val);
if (result)
marker.Heading = val;
}
}
}
Here is the Marker Object that will control the ImageMarker:
class Marker : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private ImageMarker imageMarker;
public Marker(ImageMarker imgMarker)
{
imageMarker = imgMarker;
}
private double _Heading;
public double Heading
{
get { return _Heading; }
set
{
if (value <= 0)
value = 360;
_Heading = value % 360;
imageMarker.Heading = _Heading;
//imageMarker.RenderTransform = new RotateTransform(_Heading);
}
}
}
Notice the commented out line in the Heading Setter. That method of changing the angle of the image also worked...No Binding required.
Here's the ImageMarker XAML:
<UserControl x:Class="TestRotateTransform.ImageMarker"
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"
xmlns:local="clr-namespace:TestRotateTransform"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" Width="32" Height="32">
<Image Source="Resources/arrow_up.png" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="{Binding Heading, RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1,AncestorType=UserControl}}" />
<TranslateTransform/>
</TransformGroup>
</Image.RenderTransform>
</Image>
And finally, here's the ImageMarker code behind with the addition of propertychanged, coercion and validation callbacks as suggested by Keith:
public partial class ImageMarker : UserControl
{
public ImageMarker()
{
InitializeComponent();
}
public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register("Heading",
typeof(double), typeof(ImageMarker), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnHeadingChanged),
new CoerceValueCallback(CoerceHeading)),new ValidateValueCallback(IsValidHeading));
private static void OnHeadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(HeadingProperty);
}
private static object CoerceHeading(DependencyObject d, object value)
{
double hdg = (double)value;
if (hdg <= 0)
hdg = 360;
return hdg % 360;
}
private static bool IsValidHeading(object value)
{
return true;
}
public double Heading
{
get { return (double)GetValue(HeadingProperty); } // { return _Heading; } //
set
{
SetValue(HeadingProperty, value % 360);
}
}
}
Thanks to Keith and Clemens for their suggestions. At least I know it can be done. Apparently, in my real project, something else is causing it to fail.
I need help with drawing in my combobox. I want to make a combobox of colors for picking. I found some stuff on the internet but none of them is working. So far I have this :
private void MyComb_DrawItem(object sender, DrawItemEventArgs e)
{
Graphics g = e.Graphics;
System.Drawing.Rectangle rect = e.Bounds;
ColorConverter converter = new ColorConverter();
if (e.Index >= 0)
{
string n = ((ComboBox)sender).Items[e.Index].ToString();
System.Drawing.Color c = (System.Drawing.Color)converter.ConvertFromString(n);
SolidBrush b = new SolidBrush(c);
g.FillRectangle(b, rect.X + 110, rect.Y + 5,
rect.Width - 10, rect.Height - 10);
}
}
This is my drawItem method
<Grid>
<ComboBox x:Name="MyComb" HorizontalAlignment="Left" Margin="66,81,0,0" VerticalAlignment="Top" Width="120" />
</Grid>
This is definition of combobox
Type colorType = typeof(System.Drawing.Color);
PropertyInfo[] propInfoList = colorType.GetProperties(BindingFlags.Static |
BindingFlags.DeclaredOnly | BindingFlags.Public);
foreach (PropertyInfo c in propInfoList)
{
MyComb.Items.Add(c.Name);
}
And here I am filling combobox with colors names and then I want to fill to combox with real colors according to the colors names.. But my draw item method is never called. I tried to create some DrawItem handler but, my combobox have no such thing... Then I read something about setting a DrawMode property of combobox, but my combobox doesn't that kind of property at all...
I am using net framework v.4.6.1
Can please anyone tell me, what am I missing ?
Thank you very much
The biggest problem you're having is that you're trying to use code examples that were written for the Winforms API, even though you are using the WPF API. For future reference, you really need to be more careful about identifying the context of tutorials and other resources you find online, to make sure they actually apply to your scenario.
As it happens, we have a number of related questions on Stack Overflow already:
WPF ComboBox as System.Windows.Media.Colors>
WPF - Bind ComboBox Item Foreground to Its Value
Very simple color picker made of combobox
These are all potentially useful to you, but are all based on the answer to this question:
How can I list colors in WPF with XAML?
Which was originally about just displaying the names of colors, and so took a short-cut, using the <ObjectDataProvider/> element in XAML. This led to the need to use a converter in the other questions, to convert either from a string value or a PropertyInfo instance to the appropriate color or brush.
In fact, if your code is already written to use some type of MVVM approach, and especially since you've already written code-behind to retrieve the color values from the Colors type, or at least tried to (one of the problems in your code is that you are using the Winforms Color type instead of the WPF Colors type…again, in Winforms that works fine, but the WPF API follows the Code Analysis/FxCop rules more closely, and the named colors are in the Colors type), it makes sense to just stick with that and provide a direct view model data structure to which you can bind.
In this approach, rather than providing a procedural implementation of the item drawing, you provide in XAML a declarative implementation describing what each item should look like.
Here is an example…
First, some simple view model data structures:
// Very simple container class
class ColorViewModel
{
public Brush Brush { get; }
public string Name { get; }
public ColorViewModel(Brush brush, string name)
{
Brush = brush;
Name = name;
}
}
// Main view model, acts as the data context for the window
class MainViewModel : INotifyPropertyChanged
{
public IReadOnlyList<ColorViewModel> Colors { get; }
private Brush _selectedColor;
public Brush SelectedColor
{
get { return _selectedColor; }
set { _UpdateField(ref _selectedColor, value); }
}
public MainViewModel()
{
Colors = typeof(Colors).GetProperties(BindingFlags.Static | BindingFlags.Public)
.Select(p => new ColorViewModel(new SolidColorBrush((Color)p.GetValue(null)), p.Name))
.ToList().AsReadOnly();
}
public event PropertyChangedEventHandler PropertyChanged;
private void _UpdateField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
With those in hand, the XAML is straight-forward:
<Window x:Class="TestSO47850587ColorComboBox.MainWindow"
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:l="clr-namespace:TestSO47850587ColorComboBox"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<StackPanel>
<ComboBox Width="100" ItemsSource="{Binding Colors}"
HorizontalAlignment="Left" Grid.IsSharedSizeScope="True"
SelectedValuePath="Brush" SelectedValue="{Binding SelectedColor}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type l:ColorViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="ComboBoxItem"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Background="{Binding Brush}" HorizontalAlignment="Stretch"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Rectangle HorizontalAlignment="Stretch" Height="24" Fill="{Binding SelectedColor}"/>
</StackPanel>
</Window>
The above displays the color names, using the actual color as the background. If all you really want is a rectangle, then you can replace the <TextBlock/> element with a <Rectangle/> element, binding to its Fill property. Naturally, you can achieve other visual effects, such as a rectangle with a margin. It's just a matter of configuring your data template according to your need.
The main point here is that you should embrace the data binding approach that defines good WPF programming, and that you should definitely not mistake Winforms code examples for WPF. :)
EDIT2: When the chart gets populated i am unable to change the values anymore. Even when i change the values in the list (where the ItemControls get the values from) the chart does not seem to update with the new values.
I call upon the GetDataGrafiek() method in the timer to update my chart every x seconds.
Grafiek graf = new Grafiek();
graf.GetDataGrafiek();
Is this due the Threading Timer running in a separate thread(IsAsynchronous method in the ObjectDataProvider) or do i need to access the DataContext of the ItemsControl in my timer method?
EDIT: I was unable able to populate the chart when the program was already running so i made the ObservableCollection<GrafiekBar> static (list that holds the Fill and Value of the bars) and initialized is as following:
public static ObservableCollection<GrafiekBar> listGrafiek = new ObservableCollection<GrafiekBar>()
{
new GrafiekBar() {Value = 0, Fill = (Brush)convertor.ConvertFromString(kleuren[0])},
new GrafiekBar() {Value = 0, Fill = (Brush)convertor.ConvertFromString(kleuren[1])},
new GrafiekBar() {Value = 0, Fill = (Brush)convertor.ConvertFromString(kleuren[2])}
};
From MSDN: ObjectDataProvider: "However, if you are binding to an object that has already been created, you need to set the DataContext in code, as in the following example."
I have a ItemsControl that is being displayed as a simple bar chart.
When i assign values (hardcoded in my codeBehind) the chart gets populated succesfully.
What i do is basically getting the largest value set it to 100% and calculate the length of the rest of the bars trough that.
Problem:
I do not want the chart bar values hardcoded but the bars have to change runTime.
For this i use a Threading.Timer that runs every second as long my program is running (other calculations happen in this timer as well).
The chart bar values get updates based on the calculations happening within this timer every x seconds.
I have tried everything and i cannot get values displayed when my program is running. I can only see bars when i hardcode them (see region of GetDataGrafiek() at the end of the thread). What exactly am i doing wrong / missing?
The GetDataGrafiek() (calculations to populate my chart) gets called in a ObjectDataProvider.
This method takes TimeSpan as input and then performs calculations so i get a Double Value (based on the 100% value explained above) which then gets placed in the Value of the bar (= width of the dateTemplate).
<ObjectDataProvider x:Key="odpLbGrafiek" ObjectType="{x:Type myClasses:GrafiekBar}" MethodName="GetDataGrafiek"/>
DataTemplate for my ItemsControl (this uses the Width for the value of the bars of my chart)
<DataTemplate x:Key="GrafiekItemTemplate">
<Border Width="Auto" Height="Auto">
<Grid>
<Rectangle StrokeThickness="0" Height="30"
Margin="15"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="{Binding Value}"
Fill="{Binding Fill}">
<Rectangle.LayoutTransform>
<ScaleTransform ScaleX="20" />
</Rectangle.LayoutTransform>
</Rectangle>
</Grid>
</Border>
</DataTemplate>
ItemsControl:
<ItemsControl x:Name="icGrafiek"
Margin="50,3,0,0"
ItemsSource="{Binding Source={StaticResource odpLbGrafiek}}"
ItemTemplate="{DynamicResource GrafiekItemTemplate}"
RenderTransformOrigin="1,0.5" HorizontalAlignment="Left" VerticalAlignment="Top" Grid.RowSpan="6">
<ItemsControl.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1" ScaleX="1"/>
<SkewTransform AngleY="0" AngleX="0"/>
<RotateTransform Angle="180"/>
<TranslateTransform/>
</TransformGroup>
</ItemsControl.RenderTransform>
</ItemsControl>
GetDataGrafiek() The region holds hardcoded values, when this is done my chart displays 6 bars succesfully. When i comment the region i no longer get any visible bars.
This method returns a list with Double values. Each value represents a bar that gets represented as Width in the DataTemplate, and the Fill just gives it a certain color.
ObservableCollection<GrafiekBar> listGrafiek = new ObservableCollection<GrafiekBar>();
public ObservableCollection<GrafiekBar> GetDataGrafiek()
{
var converter = new System.Windows.Media.BrushConverter();
#region ***TEST HARDCODED BAR VALUES***
int[] testWaardenUren = new int[] { 2, 1, 0, 1, 2, 0 };
int[] testWaardenMinuten = new int[] { 58, 2, 55, 55, 2, 20 };
for (int j = 0; j < 6; j++)
{
TimeSpan ts = new TimeSpan(testWaardenUren[j], testWaardenMinuten[j], 0);
GlobalObservableCol.regStilstanden[j].Value = ts;
GlobalObservableCol.regStilstanden[j].Name = "";
}
#endregion
totalMinutesMaxValue = GetLargestValueStilstanden(); //= "100%" value
//calculate % of stilstanden Values
for (int i = 0; i < GlobalObservableCol.regStilstanden.Count; i++)
{
Double totalMin = GlobalObservableCol.regStilstanden[i].Value.TotalMinutes;
totalMin = totalMin / totalMinutesMaxValue * 10;
valuesChartPercentage.Add(totalMin);
}
//the barChart (itemsControl) gets its final values here
for (int j = 0; j < GlobalObservableCol.regStilstanden.Count; j++)
{
GrafiekBar bar = new GrafiekBar();
bar.Value = valuesChartPercentage[j];
bar.Fill = converter.ConvertFromString(kleuren[j]) as Brush;
listGrafiek.Add(bar);
}
return listGrafiek;
}
GrafiekBar.cls
public class GrafiekBar : INotifyPropertyChanged
{
private double value;
private Brush fill;
public GrafiekBar()
{
}
public double Value
{
get { return this.value; }
set
{
this.value = value;
NotifyPropertyChanged("Value");
}
}
public Brush Fill
{
get { return this.fill; }
set
{
this.fill = value;
NotifyPropertyChanged("Fill");
}
}
//interface INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
//interface INotifyPropertyChanged
private void NotifyPropertyChanged(String info)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
The Threading timer that runs every second (that has the calculation logic and the getDataGrafiek() called there does a invoke to the GUI thread for updates.
private void MyTimerCallBack(object state)
{
DisplayWegingInfo();
App.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
new Action(() =>
{
//actions that involve updating the UI
CaculateTimeBetweenWegingen();
}));
}
Best Regards Peter.
It's a shot in the dark, 'cause I did not read through all your code, but here's a thought: you are binding to a static resource right? It gets read once and this is it, eh? Try DynamicResource instead.
If you are accessing the following code from another thread, this will cause an issue:
private void NotifyPropertyChanged(String info)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
As the other thread will raise an event which will cause the Dispatcher Thread (if it's subscribed to the event) to error.
Try instead
private void NotifyPropertyChanged(String info)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (PropertyChanged != null)
{
if (System.Threading.Thread.CurrentThread == System.Windows.Application.Current.Dispatcher.Thread)
PropertyChanged(this, new PropertyChangedEventArgs(info));
else
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => PropertyChanged(this,new PropertyChangedEventArgs(info));
}
}
Also, make sure you are using UpdateSourceMethod=PropertyChanged in WPF.
To put it simply, I have this within a ControlTemplate.Triggers condition EnterAction:
<ColorAnimation To="#fe7" Storyboard.TargetProperty="Background.Color" Duration="00:00:00.1" Storyboard.TargetName="brd"/>
But I want the 'to' colour (#fe7) to be customisable. This is a control derived from ListBox. I can create a DependencyProperty, but of course, I cannot bind the To property of the ColorAnimation to it because the Storyboard has to be frozen and you can't freeze something with bindings (as I understand it).
I tried using a {StaticResource} within the To, then populating the resource in the code-behind when the DependencyProperty was changed, by setting this.Resources["ItemColour"] = newValue; for instance. That didn't work perhaps obviously, it's a static resource after all: no new property values were picked up. DynamicResource gave the same problem relating to inability to freeze.
The property is only set once when the control is created, I don't have to worry about it changing mid-animation.
Is there a nice way of doing this? Do I have to resort to looking for property changes myself, dynamically invoking and managing storyboards at that point? Or overlaying two versions of the control, start and end colour, and animating Opacity instead? Both seem ludicrous..
Kieren,
Will this serve your purpose?
I have extended the Grid class called CustomGrid and created a TestProperty whose value when changed will change the background color of Grid:
public class CustomGrid : Grid
{
public bool Test
{
get
{
return (bool)GetValue(TestProperty);
}
set
{
SetValue(TestProperty, value);
}
}
public static readonly DependencyProperty TestProperty =
DependencyProperty.Register("Test", typeof(bool), typeof(CustomGrid),
new PropertyMetadata(new PropertyChangedCallback
((obj, propChanged) =>
{
CustomGrid control = obj as CustomGrid;
if (control != null)
{
Storyboard sb = new Storyboard() { Duration = new Duration(TimeSpan.FromMilliseconds(500)) };
Random rand = new Random();
Color col = new Color()
{
A = 100,
R = (byte)(rand.Next() % 255),
G = (byte)(rand.Next() % 255),
B = (byte)(rand.Next() % 255)
};
ColorAnimation colAnim = new ColorAnimation();
colAnim.To = col;
colAnim.Duration = new Duration(TimeSpan.FromMilliseconds(500));
sb.Children.Add(colAnim);
Storyboard.SetTarget(colAnim, control);
Storyboard.SetTargetProperty(colAnim, new PropertyPath("(Panel.Background).(SolidColorBrush.Color)"));
sb.Begin();
}
}
)));
}
This is the button click event that changes the color:
private void btnClick_Click(object sender, RoutedEventArgs e)
{
gridCustom.Test = (gridCustom.Test == true) ? false : true;
}
I am changing the background color of Grid because I don't have your Listbox.
Finally this is the xaml:
<Grid x:Name="grid" Background="White">
<local:CustomGrid x:Name="gridCustom" Background="Pink" Height="100" Margin="104,109,112,102" >
</local:CustomGrid>
<Button Content="Click Me" x:Name="btnClick" Height="45" HorizontalAlignment="Left" Margin="104,12,0,0" VerticalAlignment="Top" Width="145" Click="btnClick_Click" />
</Grid>
Will this serve your purpose? Let me know or I misunderstood the question?
EDIT:
See this code:
ColorAnimation's To property cannot be bound as you probably guessed. But that doesn't mean you can't change it's value. You can always get a reference to the ColorAnimation and change it's To value and it will all work out well. So from WPF world of binding we need to change a bit and bind the data how we used to do it in Winforms :). As an example see this:
This is the xaml:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" x:Class="ControlTemplateTriggers.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Storyboard x:Key="Storyboard">
<ColorAnimation From="Black" To="Red" Duration="00:00:00.500" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)" Storyboard.TargetName="gridCustom" />
</Storyboard>
</Window.Resources>
<Grid x:Name="grid" Background="White">
<Grid x:Name="gridCustom" Background="Pink" Height="100" Margin="104,109,112,102" />
<Button Content="Click Me" x:Name="btnClick" Height="45" HorizontalAlignment="Left" Margin="104,12,0,0" VerticalAlignment="Top" Width="145" Click="btnClick_Click" />
</Grid>
</Window>
This is the code behind:
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Media;
using System;
namespace Sample {
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
private void btnClick_Click(object sender, RoutedEventArgs e)
{
Storyboard sb = this.Resources["Storyboard"] as Storyboard;
if (sb != null)
{
ColorAnimation frame = sb.Children[0] as ColorAnimation;
Random rand = new Random();
Color col = new Color()
{
A = 100,
R = (byte)(rand.Next() % 255),
G = (byte)(rand.Next() % 255),
B = (byte)(rand.Next() % 255)
};
frame.To = col;
sb.Begin();
}
}
}
}
As you can see I am getting a reference to the storyboard and changing it's To property. Your approach to StaticResource obviously wouldn't work. Now what you can do is, in your DependencyProperty callback somehow get a reference to the Timeline that you want to animate and using VisualTreeHelper or something and then set it's To property.
This is your best bet.
Let me know if this solved your issue :)
can u put multiple DataTriggers with each having respective color for the "To" property...
Surely not..
What i understood is that u want color A on the Condition A and Color B on some other condition B....so if there's a property with multiple options u can put datatriggers for those condition only...like if Job done = Red, Half done = Green like wise..
If i misunderstood the problem please correct me..
I think i got ur question ...UR control is user configurable so what ever user select , control's background needs to be set to that color with animation right?
It turns out this is simply not possible.
<TextBlock Width="100" Text="The quick brown fox jumps over the lazy dog" TextTrimming="WordEllipsis">
<TextBlock.ToolTip>
<ToolTip DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock Text="{Binding Text}"/>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
How can I show the ToolTip only when the text is trimmed? Like the windows desktp shortcut icons.
Working off of Eyjafj...whatever's idea, I arrived at a working, mostly declarative solution that at least doesn't require a custom control. The first hurdle to overcome is getting at the TextBlock. Because the ToolTip is rendered outside of the visual tree, you can't use a RelativeSource binding or ElementName to get at the TextBlock. Luckily, the ToolTip class provides a reference to its related element via the PlacementTarget property. So you can bind the ToolTip's Visibility property to the ToolTip itself and use its PlacementTarget property to access properties of the TextBlock:
<ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource trimmedVisibilityConverter}}">
The next step is using a converter to look at the TextBlock we've bound to to determine if the ToolTip should be visible or not. You can do this using the ActualWidth and the DesiredSize. ActualWidth is exactly what it sounds like; the width your TextBlock has been rendered to on the screen. DesiredSize is the width your TextBlock would prefer to be. The only problem is, DesiredSize seems to take the TextTrimming into account and does not give you the width of the full, untrimmed text. To solve this, we can re-call the Measure method passing Double.Positive infinity to, in effect, ask how wide the TextBlock would be if it its width were not constrained. This updates the DesiredSize property and then we can do the comparison:
textBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)
return Visibility.Visible;
This approach is actually illustrated here as an attached behavior if you want to apply it automatically to TextBlocks or don't want to waste resources on creating ToolTips that will always be invisible. Here is the full code for my example:
The Converter:
public class TrimmedTextBlockVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return Visibility.Collapsed;
FrameworkElement textBlock = (FrameworkElement)value;
textBlock.Measure(new System.Windows.Size(Double.PositiveInfinity, Double.PositiveInfinity));
if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The XAML:
<UserControl.Resources>
<local:TrimmedTextBlockVisibilityConverter x:Key="trimmedVisibilityConverter" />
</UserControl.Resources>
....
<TextBlock TextTrimming="CharacterEllipsis" Text="{Binding SomeTextProperty}">
<TextBlock.ToolTip>
<ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource trimmedVisibilityConverter}}">
<ToolTip.Content>
<TextBlock Text="{Binding SomeTextProperty}"/>
</ToolTip.Content>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
I found the simplest solution to extend TextBlock and compare text lengths to determine whether to show tooltip, i.e.,
public class ToolTipTextBlock : TextBlock
{
protected override void OnToolTipOpening(ToolTipEventArgs e)
{
if (TextTrimming != TextTrimming.None)
{
e.Handled = !IsTextTrimmed();
}
}
private bool IsTextTrimmed()
{
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var formattedText = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection, typeface, FontSize, Foreground);
return formattedText.Width > ActualWidth;
}
}
Then simply use this custom text block in xaml as follows:
<local:ToolTipTextBlock Text="This is some text that I'd like to show tooltip for!"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Text,RelativeSource={RelativeSource Self}}"
MaxWidth="10"/>
Based on ideas on this page and with additional algorithmic corrections from another answer I made this very portable class that can be used very easily. Its purpose is to enable trimming and show a ToolTip over the TextBlock when the text is trimmed, like it is known from many applications.
The trimming detection has proven to be precise in my application. The tool tip is shown exactly when the trimming ellipsis is shown.
XAML usage
<!-- xmlns:ui="clr-namespace:Unclassified.UI" -->
<TextBlock Text="Demo" ui:TextBlockAutoToolTip.Enabled="True"/>
C# usage
var textBlock = new TextBlock { Text = "Demo" };
TextBlockAutoToolTip.SetEnabled(textBlock, true);
The complete class
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace Unclassified.UI
{
/// <summary>
/// Shows a ToolTip over a TextBlock when its text is trimmed.
/// </summary>
public class TextBlockAutoToolTip
{
/// <summary>
/// The Enabled attached property.
/// </summary>
public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached(
"Enabled",
typeof(bool),
typeof(TextBlockAutoToolTip),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnAutoToolTipEnabledChanged)));
/// <summary>
/// Sets the Enabled attached property on a TextBlock control.
/// </summary>
/// <param name="dependencyObject">The TextBlock control.</param>
/// <param name="enabled">The value.</param>
public static void SetEnabled(DependencyObject dependencyObject, bool enabled)
{
dependencyObject.SetValue(EnabledProperty, enabled);
}
private static readonly TrimmedTextBlockVisibilityConverter ttbvc = new TrimmedTextBlockVisibilityConverter();
private static void OnAutoToolTipEnabledChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
TextBlock textBlock = dependencyObject as TextBlock;
if (textBlock != null)
{
bool enabled = (bool)args.NewValue;
if (enabled)
{
var toolTip = new ToolTip
{
Placement = System.Windows.Controls.Primitives.PlacementMode.Relative,
VerticalOffset = -3,
HorizontalOffset = -5,
Padding = new Thickness(4, 2, 4, 2),
Background = Brushes.White
};
toolTip.SetBinding(UIElement.VisibilityProperty, new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),
Path = new PropertyPath("PlacementTarget"),
Converter = ttbvc
});
toolTip.SetBinding(ContentControl.ContentProperty, new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),
Path = new PropertyPath("PlacementTarget.Text")
});
toolTip.SetBinding(Control.ForegroundProperty, new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),
Path = new PropertyPath("PlacementTarget.Foreground")
});
textBlock.ToolTip = toolTip;
textBlock.TextTrimming = TextTrimming.CharacterEllipsis;
}
}
}
private class TrimmedTextBlockVisibilityConverter : IValueConverter
{
// Source 1: https://stackoverflow.com/a/21863054
// Source 2: https://stackoverflow.com/a/25436070
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var textBlock = value as TextBlock;
if (textBlock == null)
return Visibility.Collapsed;
Typeface typeface = new Typeface(
textBlock.FontFamily,
textBlock.FontStyle,
textBlock.FontWeight,
textBlock.FontStretch);
// FormattedText is used to measure the whole width of the text held up by TextBlock container
FormattedText formattedText = new FormattedText(
textBlock.Text,
System.Threading.Thread.CurrentThread.CurrentCulture,
textBlock.FlowDirection,
typeface,
textBlock.FontSize,
textBlock.Foreground,
VisualTreeHelper.GetDpi(textBlock).PixelsPerDip);
formattedText.MaxTextWidth = textBlock.ActualWidth;
// When the maximum text width of the FormattedText instance is set to the actual
// width of the textBlock, if the textBlock is being trimmed to fit then the formatted
// text will report a larger height than the textBlock. Should work whether the
// textBlock is single or multi-line.
// The width check detects if any single line is too long to fit within the text area,
// this can only happen if there is a long span of text with no spaces.
bool isTrimmed = formattedText.Height > textBlock.ActualHeight ||
formattedText.MinWidth > formattedText.MaxTextWidth;
return isTrimmed ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
}
Behaviors are love, behaviors are life.
public class TextBlockAutoToolTipBehavior : Behavior<TextBlock>
{
private ToolTip _toolTip;
protected override void OnAttached()
{
base.OnAttached();
_toolTip = new ToolTip
{
Placement = PlacementMode.Relative,
VerticalOffset = 0,
HorizontalOffset = 0
};
ToolTipService.SetShowDuration(_toolTip, int.MaxValue);
_toolTip.SetBinding(ContentControl.ContentProperty, new Binding
{
Path = new PropertyPath("Text"),
Source = AssociatedObject
});
AssociatedObject.TextTrimming = TextTrimming.CharacterEllipsis;
AssociatedObject.AddValueChanged(TextBlock.TextProperty, TextBlockOnTextChanged);
AssociatedObject.SizeChanged += AssociatedObjectOnSizeChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.RemoveValueChanged(TextBlock.TextProperty, TextBlockOnTextChanged);
AssociatedObject.SizeChanged -= AssociatedObjectOnSizeChanged;
}
private void AssociatedObjectOnSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
{
CheckToolTipVisibility();
}
private void TextBlockOnTextChanged(object sender, EventArgs eventArgs)
{
CheckToolTipVisibility();
}
private void CheckToolTipVisibility()
{
if (AssociatedObject.ActualWidth == 0)
Dispatcher.BeginInvoke(
new Action(
() => AssociatedObject.ToolTip = CalculateIsTextTrimmed(AssociatedObject) ? _toolTip : null),
DispatcherPriority.Loaded);
else
AssociatedObject.ToolTip = CalculateIsTextTrimmed(AssociatedObject) ? _toolTip : null;
}
//Source: https://stackoverflow.com/questions/1041820/how-can-i-determine-if-my-textblock-text-is-being-trimmed
private static bool CalculateIsTextTrimmed(TextBlock textBlock)
{
Typeface typeface = new Typeface(
textBlock.FontFamily,
textBlock.FontStyle,
textBlock.FontWeight,
textBlock.FontStretch);
// FormattedText is used to measure the whole width of the text held up by TextBlock container
FormattedText formattedText = new FormattedText(
textBlock.Text,
System.Threading.Thread.CurrentThread.CurrentCulture,
textBlock.FlowDirection,
typeface,
textBlock.FontSize,
textBlock.Foreground) {MaxTextWidth = textBlock.ActualWidth};
// When the maximum text width of the FormattedText instance is set to the actual
// width of the textBlock, if the textBlock is being trimmed to fit then the formatted
// text will report a larger height than the textBlock. Should work whether the
// textBlock is single or multi-line.
// The width check detects if any single line is too long to fit within the text area,
// this can only happen if there is a long span of text with no spaces.
return (formattedText.Height > textBlock.ActualHeight || formattedText.MinWidth > formattedText.MaxTextWidth);
}
}
Usage:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:behavior="clr-namespace:MyWpfApplication.Behavior"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<TextBlock Text="{Binding Text}">
<i:Interaction.Behaviors>
<behavior:TextBlockAutoToolTipBehavior />
</i:Interaction.Behaviors>
</TextBlock>
</Window>
The needed extension methods:
public static class UITools
{
public static void AddValueChanged<T>(this T obj, DependencyProperty property, EventHandler handler)
where T : DependencyObject
{
var desc = DependencyPropertyDescriptor.FromProperty(property, typeof (T));
desc.AddValueChanged(obj, handler);
}
public static void RemoveValueChanged<T>(this T obj, DependencyProperty property, EventHandler handler)
where T : DependencyObject
{
var desc = DependencyPropertyDescriptor.FromProperty(property, typeof (T));
desc.RemoveValueChanged(obj, handler);
}
}
I think that you can create a converter that compares between the ActualWidth of textblock and it's DesiredSize.Width, and return Visibility.
Posted an alternative answer with attached property here, which I think is nicer than using a converter or a derived TextBlock control.
I used this from #pogosoma but with the CalculateIsTextTrimmed function from #snicker which is perfect
private static void SetTooltipBasedOnTrimmingState(TextBlock tb)
{
Typeface typeface = new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch);
FormattedText formattedText = new FormattedText(tb.Text, System.Threading.Thread.CurrentThread.CurrentCulture, tb.FlowDirection, typeface, tb.FontSize, tb.Foreground)
{ MaxTextWidth = tb.ActualWidth };
bool isTextTrimmed = (formattedText.Height > tb.ActualHeight || formattedText.MinWidth > formattedText.MaxTextWidth);
ToolTipService.SetToolTip(tb, isTextTrimmed ? tb.ToolTip : null);
}