WPF Tips for graphically displaying a sequence on/off durations - wpf

I'm working on an application where users can select between multiple sequences of on/off durations. Sequences always start with the on period and can have a varying length (but always in on/off pairs): e.g.
var sequences = new []
{
new int[] { 10, 15 }, // 10 ms on, 15 ms off
new int[] { 15, 10, 5, 10 } // 15 ms on, 10 ms off, 5 ms on, 10 ms off
};
The sequences have a maximum duration of 10 seconds and will be repeated. One special sequence defines no on/off durations: it is always on (though I might be able to change that to {1,0} or something).
Instead of displaying the numbers on screen I'd like to show a little graphical representation for the full 10 second duration (repeating shorter sequences) so the user can compare patterns. These will be displayed in a combo box that resizes with the window. For the examples above it would look something like the following (where X is a filled in background)
xx xx xx xx xx xx xx...
xxx x xxx x xxx x xxx x ...
I suppose I'll have to use a value converter (if only for the special value), but am uncertain what the best/easiest way of creating the graphs is, especially with the resize requirement and repeating the shorter sequences. A canvas, something else?
I'd greatly appreciate any tips!

I would follow this basic approach:
Write a value converter that takes each sequence and repeats the sequence into the full 10 seconds, encoding each chunk of time with a class that specifies whether the period is 'On' and the duration.
For each sequence, bind to the ItemsSource of an ItemsControl. For the ItemsPanel, use a StackPanel with Horizontal orientation. For the ItemTemplate, use a Rectangle or whatever other visual you'd like for a chunk of time, with the Width bound to the duration. You've also included a handy 'IsOn' property now so that you can easily visualize the On/Off state. Don't worry about scaling the Width at this point.
Place the ItemsControl in a ViewBox, which can be allowed to stretch to its parent container. Now you have a visual that provides the correct proportions of duration and scales with size.
Here's a bare-bones implementation (no error handling or any attempt to make it pretty):
UDPATE: Fixed a bug that didn't properly truncate repeating sequence at 10 seconds.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace TestWpf
{
public class SeqSegment
{
public bool IsOn { get; set; }
public int Duration { get; set; }
}
public class SeqConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var result = new List<SeqSegment>();
var seq = (int[]) value;
int time = 0;
int i = 0;
bool isOn = true;
while (time < 10000)
{
result.Add(new SeqSegment { Duration = Math.Min(seq[i], 10000 - time), IsOn = isOn });
isOn = !isOn;
time += seq[i];
i++;
if (i >= seq.Length)
i = 0;
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public IEnumerable<int[]> TestSequences
{
get
{
yield return new[] {10, 5000, 10, 8};
yield return new[] {500, 5000};
yield return new[] {50, 400, 30, 10};
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
}
XAML:
<Window x:Class="TestWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:TestWpf="clr-namespace:TestWpf" Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<TestWpf:SeqConverter x:Key="SeqConverter"/>
<DataTemplate x:Key="SeqSegTemplate">
<Rectangle x:Name="Rect" Width="{Binding Duration}" Fill="Blue"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsOn}" Value="True">
<Setter TargetName="Rect" Property="Fill" Value="Green"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<DataTemplate x:Key="SeqTemplate">
<Viewbox Height="50" Stretch="Fill">
<ItemsControl ItemsSource="{Binding Converter={StaticResource SeqConverter}}" ItemTemplate="{StaticResource SeqSegTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Height="1"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Viewbox>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding TestSequences}" ItemTemplate="{StaticResource SeqTemplate}"/>
</Grid>
</Window>

Related

How to bind int to Visibility in WPF?

I have BudgetControlType Properties that has 1 .. 7 value
if(BudgetControlType ==1)
dataComboBox1.Visibility=Visibility.Visiblile;
dataComboBox2 to dataComboBox7 =Visibility.Hidden;
if(BudgetControlType ==2)
dataComboBox1.Visibility=Visibility.Visiblile;
dataComboBox2.Visibility=Visibility.Visiblile;
dataComboBox3 to dataComboBox7 =Visibility.Hidden;
and so on...
How to do this in xaml?
Here is another approach I have used in the past using WPFConverters.
<TabItem.Visibility>
<Binding Path="SomeObservableCollection.Count">
<Binding.Converter>
<converters:ConverterGroup>
<converters:ExpressionConverter Expression="{}{0} > 0" />
<BooleanToVisibilityConverter />
</converters:ConverterGroup>
</Binding.Converter>
</Binding>
</TabItem.Visibility>
The ConvertGroup allows for multiple converters to be run sequentially.
The ExpressionConverter lets you define an arbitrary expression. In my case I want the TabItem to be visible if the collection count is greater than zero. Being defined in xaml means escaping characters and a somewhat awkward syntax but it works well enough!
The BooleanToVisibilityConverter converts the boolean result from the expression to our desired visibility.
For Elham, BudgetControlType could be bound to as long as it implemented INotifyPropertyChanged. An equals expression is done like this (I'm returning true if the bound value equals 7):
<converters:ExpressionConverter Expression="{}{0} == 7" />
You can use 1,2,4,8,... and convert it to Visibility
for example if your int number is 6 (2+4) then Control with paramerter 2 and Control with parameter 4 is Visible!
public class IntToVisibilityConverter:IValueConverter
{
private int val;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int intParam = (int)parameter;
val = (int)value;
return ((intParam & val) != 0) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
And in xaml :
<ComboBox Visibility="{Binding Path=MyEnum,UpdateSourceTrigger=PropertyChanged, Converter={StaticResource IntToVisibilityConverter}, ConverterParameter=1}"/>
<ComboBox Visibility="{Binding Path=MyEnum,UpdateSourceTrigger=PropertyChanged, Converter={StaticResource IntToVisibilityConverter}, ConverterParameter=2}"/>
<ComboBox Visibility="{Binding Path=MyEnum,UpdateSourceTrigger=PropertyChanged, Converter={StaticResource IntToVisibilityConverter}, ConverterParameter=4}"/>
The best way I'd say would be to go with properties on your ViewModel, and bind to them.
example (you'll have to massage it a bit, but it's fairly simple from here) :
public Visibility dtcb1 { get; set; }
// all the rest till 7
// Somewhere in your logit / constructor :
dtcb1 = BudgetControlType == 1 ? Visible : Hidden;
// and so on
And on your xaml you'll bind your visibility to dtcb1
You can make the property boolean, and use a boolean to visibility converter as well (as per this answer for example, or just google yourself)

WPF How to set desired color for Shadow?

This is sample code to draw ellipse, with shadow enabled. I set both Fill and shadow color as same. But in view shadow color is different. This may be WPF feature but in my scenario i want to set desired shadow color for the object.
<Window x:Class="Test.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Canvas>
<Ellipse Width="200" Height="300" Fill="#7D00FE">
<Ellipse.Effect>
<DropShadowEffect
ShadowDepth="5"
Color="#7D00FE"/>
</Ellipse.Effect>
</Ellipse>
</Canvas>
</Grid>
</Window>
It seems like that DropShadowEffect somehow affects the Color when it renders itself. This problem seems to be non-existing for primary colors (so named Colors, like Red, Blue, Aqua, etc. - but you don't have to use the name, you can specify them through #AARRGGBB format as well.)
I could not figure out the exact modification it does, nor can I offer a workaround (except to use named colors...), but I thought maybe it's worth noting it in an answer.
See this other questions, which probably point to the same "bug" or undocumented feature of DropShadowEffect:
DropShadowEffect with DynamicResource as color has weak
visibility
WPF DropShadowEffect - Unexpected Color Difference
Update:
So, this is cheating, but for your specific question, it might solve the issue:
<Grid>
<Canvas>
<Ellipse Width="200" Height="300" Fill="#7D00FE">
<Ellipse.Effect>
<DropShadowEffect
ShadowDepth="5"
Color="#BA00FE"/>
</Ellipse.Effect>
</Ellipse>
</Canvas>
</Grid>
With a little invested work, one might be able to come up with a converter, that can convert a Color to an other Color, which will be the desired DropShadowEffect Color for the given Color. If I will have a little time I will come back to this.
My intuition suggests that the problem might be in the shader code for that particular effect, and that the output might differ on different hardware (and/or driver version), but currently I can not prove this.
Update:
I was wrong about named colors, it does not work for all of those, e.g.: Green is flawed, but the problem is not - solely - dependent on the green component of the Color. Intriguing.
Update 2:
So here is the converter I talked about earlier:
using System;
using System.Windows.Data;
using System.Windows.Media;
namespace MyCustomConverters
{
public class ColorToShadowColorConverter: IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Only touch the shadow color if it's a solid color, do not mess up other fancy effects
if (value is SolidColorBrush)
{
Color color = ((SolidColorBrush)value).Color;
var r = Transform(color.R);
var g = Transform(color.G);
var b = Transform(color.B);
// return with Color and not SolidColorBrush, otherwise it will not work
// This means that most likely the Color -> SolidBrushColor conversion does the RBG -> sRBG conversion somewhere...
return Color.FromArgb(color.A, r, g, b);
}
return value;
}
private byte Transform(byte source)
{
// see http://en.wikipedia.org/wiki/SRGB
return (byte)(Math.Pow(source / 255d, 1 / 2.2d) * 255);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("ColorToShadowColorConverter is a OneWay converter.");
}
#endregion
}
}
And here is how it should be used:
Resources part:
<namespaceDefinedByXmlnsProperty:ColorToShadowColorConverter x:Key="ColorConverter" />
Real usage:
<Ellipse Width="50" Height="100" Fill="#7D00FE">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="50"
Color="{Binding Fill, RelativeSource={RelativeSource
Mode=FindAncestor, AncestorType={x:Type Ellipse}},
Converter={StaticResource ColorConverter}}"/>
</Ellipse.Effect>
</Ellipse>
Thanks for Michal Ciechan for his answer, as it guided me in the right direction.
Somewhere it is converting the DropShadowEffect into a specific Sc value.
The closer to 1 you are, the less the difference (hence FF/255/1 works absolutely fine) because nth root of 1 is 1
From looking into this and researching about on ScRGB, the gamma value of ScRGB is around 2.2. Therefore when converting from RGB to ScRGB, you may need to divide by 255, then nth(2.2) root of the value to come up with the final value.
E.g.
value 5E is 94
94 / 255 = 0.36862745098039215686274509803922
2.2root of 94/255 = 0.635322735100355
0.635322735100355 * 255 = ~162 = A2
Therefore when you set the Green of the foreground to 5E, you need to set the DropShadowEffect to A2.
This is just my observation and what i came up with from my research.
Why did MS implement it like this? I HAVE NO IDEA
Sources:
RGB/XYZ Matrices
Wikipedia sRGB
Therefore in your example to have the same colour you need to use #B800FE
As explained in Ciechan's answer(thanks to Mr Ciechan), Microsoft converts the DropShadowEffect into a specific Sc value.
So how to solve it?
Just let Microsoft do the calculation back by entering the RGB value into sRGB.
//Where the variable color is the expected color.
Color newColor = new Color();
newColor.ScR = color.R;
newColor.ScG = color.G;
newColor.ScB = color.B;
//the converted color value is in newColor.R, newColor.G, newColor.B
Refer to Update 2 in #qqbenq's answer, for technical details for the binding converter(thanks to #qqbenq).
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Only touch the shadow color if it's a solid color, do not mess up other fancy effects
if (value is SolidColorBrush)
{
Color color = ((SolidColorBrush)value).Color;
//Where the variable color is the expected color.
Color newColor = new Color();
newColor.ScR = (float)color.R / 255;
newColor.ScG = (float)color.G / 255;
newColor.ScB = (float)color.B / 255;
return newColor;
}
return value;
}
Here is a formula improved for #qqbenq 's answer.
The changes are in the Transform function. It is much more accurate and the difference is around 1 value.
Therefore in questioner example to have the same colour you need to use #BA00FF and you will get #7D00FF (questioner requested for #7D00FE).
Source of reference for the formula found in https://www.nayuki.io/page/srgb-transform-library
private byte Transform(byte source)
{
// see http://en.wikipedia.org/wiki/SRGB
return (byte)(Math.Pow(source / 255d, 1 / 2.2d) * 255);
double x = (double)source / 255;
if (x <= 0)
return 0;
else if (x >= 1)
return 1;
else if (x < 0.0031308f)
return (byte)(x * 12.92f * 255);
else
return (byte)((Math.Pow(x, 1 / 2.4) * 1.055 - 0.055) * 255);
}

Understanding System.Windows.Data Error: 23 : Cannot convert

I've struggled to understand this error for some time and now it's finally breaking something. Here's a dumbed-down sketch of what I'm doing:
My datawrapper class:
class DummyWrapper<T> : INotifyPropertyChanged
{
public DummyWrapper() { }
private T _data;
public T Data
{
get { return _data; }
set
{
_data = value;
Notify("Data");
}
}
public static implicit operator T(DummyWrapper<T> d)
{
return d._data;
}
...
}
My XAML:
<DockPanel>
<ContentPresenter Name="cp" Visibility="Collapsed"/>
<Rectangle Name="foo" Fill="{Binding ElementName=cp, Path=Content}" Height="50" Width="50"/>
</DockPanel>
The pertinent bit of my codebehind:
DummyWrapper<Brush> dw = new DummyWrapper<Brush>(new SolidColorBrush((Color)ColorConverter.ConvertFromString("Red")));
cp.Content = dw;
And of course if this was all working the way I expected I wouldn't be here. The output window shows me this:
System.Windows.Data Error: 23 : Cannot convert 'WpfTestApplication.DummyWrapper`1[System.Windows.Media.Brush]'
from type 'DummyWrapper`1' to type 'System.Windows.Media.Brush'
for 'en-US' culture with default conversions; consider using Converter property of Binding.
... and it goes on in that vein for some time.
Is there something I can do to allow my DummyWrapper to be converted automatically (ie w/o supplying a converter in the binding) in this context?
Change your line of code to
var solidColorBrush = new SolidColorBrush((Color) ColorConverter.ConvertFromString("Red"));
DummyWrapper<Brush> dw = new DummyWrapper<Brush>(solidColorBrush);
cp.Content = (Brush)dw;
You don't want to use converters that's fine. Content is an object and doing an implicit operator will not just do that unless the Content is of type Brush. You have to explicitly cast it to Brush
System.Object is the type of the Content property.
Through inheritance there's already an implicit conversion to the base
type

Databinding shapes and lines in WPF

I am working on an application that needs to be able to manipulate shapes and lines in WPF. My original thought was to databind a collection to ListBox and use Rectangles in the datatemplate, setting each of the fill properties to the image. This has worked well for the majority of shapes, except for circles and a few rectangles. Since re-sizing an image causes pixelation and the lines to change sizes, the result is less than stellar.
I have spent some time browsing SO and a few other sites regarding Path elements, but haven't found anything that really meets my needs. My guess is I will need to generate paths differently for each type of shape and databind them using a converter similar to Path drawing and data binding or use http://www.telerik.com/help/wpf/raddiagram-overview.html or similar rad tool.
My questions: Is there an easier way of accomplishing this or any other examples?
EDIT: I also need to be able to add text. Not sure how I can do that with a path...maybe a ContentControl?
You can draw all manner of shapes by databinding a Path.Data to a Geometry. You can generate the Geometry from a list of points. A converter is perfect for this adaptation.
For example, I draw spirals by databinding the Path.Data property to a StreamGeometry which I generate off of a list of points managed by the view model, and it works quite well for my needs:
// ViewModel ...
public class ViewModel
{
[Notify]
public IList<Point> Points { get; set; }
}
// Converter ...
public class GeometryConverter : IValueConverter
{
public Object Convert(Object value, Type targetType, Object parameter, CultureInfo culture)
{
if (value == null || value == DependencyProperty.UnsetValue)
{
return value;
}
var points = (IList<Point>)value;
var i = 0;
var newPath = new StreamGeometry();
using (var context = newPath.Open())
{
var begun = false;
for (var i = 0; i < points.Count; i++)
{
var current = points[i];
if (!begun)
{
begun = true;
context.BeginFigure(current, true, false);
}
else
{
context.ArcTo(current, new Size(radius, radius), angle, false, SweepDirection.Counterclockwise, true, true);
}
}
}
newPath.Freeze();
return newPath.GetFlattenedPathGeometry();
}
}
XAML:
<Canvas>
<Path StrokeThickness="{Binding StrokeWidth}"
Canvas.Top="{Binding Top}"
Canvas.Left="{Binding Left}"
Data="{Binding Points, Converter={StaticResource GeometryConverter}}">
<Path.Stroke>
<SolidColorBrush Color="{Binding CurrentColor}" />
</Path.Stroke>
</Path>
</Canvas>
As for the text, wouldn't it be better to bind TextBlock elements and arrange those on a 'Canvas` as needed?

WPF Edit hours and minutes of a DateTime

I have 3 TextBoxes to edit a DateTime.
What is important to notice is that those 2 TextBoxes are editing the hour and minutes of the first TextBox DateTime value.
One to edit the Date and 2 to edit hour and minutes.
How would you do that? The code below doesn't reflect the DateTime changes when editing the hour or minute, because it does ToString("HH") and the DateTime value is lost:
<TextBox Text="{Binding MyDateTime}" />
<!--This cannot work : it's just for clearing purposes -->
<TextBox Text="{Binding MyDateTime, StringFormat=\{0:HH\}}}" />
<TextBox Text="{Binding MyDateTime}, StringFormat=\{0:mm\}}" />
Of course I can have a ViewModel as DataContext where I would solve it programmatically.
But I just want to know if there is any possiblity directly in XAML
It is not easily possible with XAML only. There are several possibilities how to solve this:
1. Write custom control or user control that can do this
You could wirte a custom control / user control (e.g. DateTimeTextBox) that has a Property DateTime Value that your xaml can bind against and that contains logic to convert to the datetime value entered in one of its two textboxes. Instead of two textboxes you could also have something like maskedtextbox.
2. Two dedicated properties in the ViewModel
If you go with MVVM you could give your ViewModel two dedicated properties int DateTimeHours int DateTimeMinutes and bind against that:
<TextBox Text="{Binding MyDateTimeHours}" />
<TextBox Text="{Binding MyDateTimeMinutes}" />
Your ViewModel would then merge the two properties to a single DateTime value.
You will need to use a converter with 2way binding and a converter parameter. Save the original hour in the Converter. Then you can update the binding source appropriately.
I appreciate this is an old post, but I came across this today and found another way to handle bindings with date and time.
I created a partial class called dateTime with 4 properties one for the date, 1 for hour and another for minutes. Then a readonly dateTime property that returns a completed date.
Partial Public Class dateTimeValue
Property Dt As Date
Property Hr As Integer
Property Mn As Integer
ReadOnly Property dateTime As Date
Get
Dim d = Dt
d = d.AddHours(d.Hour * -1).AddHours(Hr)
d = d.AddMinutes(d.Minute * -1).AddMinutes(Mn)
d = d.AddSeconds(d.Second * -1)
Return d
End Get
End Property
End Class
Then in the XAML I used a grid layout with the bindings to a DateTimePicker and a couple of ComboBoxes.
<UniformGrid Columns="2">
<TextBlock Text="Date"/>
<DatePicker SelectedDate="{Binding dateTime.Dt}"/>
<TextBlock Text="Time"/>
<WrapPanel>
<ComboBox SelectedValue="{Binding dateTime.Hr}" SelectedValuePath="Content">
<ComboBoxItem>00</ComboBoxItem>
<ComboBoxItem>01</ComboBoxItem>
<ComboBoxItem>02</ComboBoxItem>
.........
<ComboBoxItem>22</ComboBoxItem>
<ComboBoxItem>23</ComboBoxItem>
</ComboBox>
<TextBlock Text=":"/>
<ComboBox SelectedValue="{Binding dateTime.Mn}" SelectedValuePath="Content">
<ComboBoxItem>00</ComboBoxItem>
<ComboBoxItem>15</ComboBoxItem>
<ComboBoxItem>30</ComboBoxItem>
<ComboBoxItem>45</ComboBoxItem>
</ComboBox>
</WrapPanel>
<Button Click="saveTask" Content="Save Task"/>
</UniformGrid>
Then to display the correct date and time in say a textblock you can use
<TextBlock Text="{Binding dateTime.dateTime, StringFormat={}{0:dd MMM yyyy - HH:mm}}"/>
Hope this helps someone else out.
100% MVVM
public class DateTimeConverter : IValueConverter
{
private DateTime _target = DateTime.Now;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var source = value as DateTime?;
if (source is null) return null;
_target = source.Value;
switch (parameter as string)
{
case "y": return source.Value.Year;
case "m": return source.Value.Month;
case "d": return source.Value.Day;
case "h": return source.Value.Hour;
case "i": return source.Value.Minute;
default: return null;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
switch (parameter as string)
{
case "y": return new DateTime(System.Convert.ToInt32(value), _target.Month, _target.Day, _target.Hour, _target.Minute, 0);
case "m": return new DateTime(_target.Year, System.Convert.ToInt32(value), _target.Day, _target.Hour, _target.Minute, 0);
case "d": return new DateTime(_target.Year, _target.Month, System.Convert.ToInt32(value), _target.Hour, _target.Minute, 0);
case "h": return new DateTime(_target.Year, _target.Month, _target.Day, System.Convert.ToInt32(value), _target.Minute, 0);
case "i": return new DateTime(_target.Year, _target.Month, _target.Day, _target.Hour, System.Convert.ToInt32(value), 0);
default: return _target;
}
}
}

Resources