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?
Related
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);
}
I have a DataGrid (dataGrid1) where records can be added and deleted.
Based on that dataGrid1, I want to make a new Grid with buttons in it based on ID and Types'. Cols will also have to given a DataSource of add dynamically, but that will be just while generating for the 1st time in Window_Loaded itself. Rows can be added/removed based on changes in dataGrid1. I want somethign like this :
On each Btn click, a new window will be opened for entry of the particular Type and for the particular ID. If the details are already entered, then the text of btn wil be "Update" else "Add".
What could be the best resource/control to perform this operations ? At present, I just did a Grid with 2 stable cols. Any ideas for the above to use Grid, DataGrid or something else. And adding/removing rows will be easy in which way and how.
Any help is appreciated.
Okay, let me try to take an example which is similar to your needs
Let's assume we use this class:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
And we are willing to display a DataGrid listing the ID, and having as a second column a Button, with the property MyString as content, which, when clicked, launches the ICommand MyCommand which opens in a new window whatever you want.
Here is what you should have on the View side:
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding MyID}" />
<DataGridTemplateColumn Header="Buttons">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding MyString}" Command="{Binding MyCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
This will show a DataGrid taking all the content in an IEnumerable<MyObject> named 'MyList', and shows two columns as defined before.
Now if you need to define the command.
First, I recommend you read this introductory link to MVVM and take the RelayCommand class (that's what we're gonna use for your problem)
So, in your ViewModel, the one which defines the MyList, here is how you should define some of the useful objects:
public ObservableCollection<MyObject> MyList { get; set; }
// blah blah blah
public void InitializeMyList()
{
MyList = new ObservableCollection<MyObject>();
for (int i = 0; i < 5; i++)
{
MyList.Add(InitializeMyObject(i));
}
}
public MyObject InitializeMyObject(int i)
{
MyObject theObject = new MyObject();
theObject.MyID = i;
theObject.MyString = "The object " + i;
theObject.MyCommand = new RelayCommand(param =< this.ShowWindow(i));
return theObject
}
private void ShowWindow(int i)
{
// Just as an exammple, here I just show a MessageBox
MessageBox.Show("You clicked on object " + i + "!!!");
}
This should be enough to create whatever you want. As you can see, every Button will call a method (ShowWindow) which is defined to show your new window, do whatever you need inside. The RelayCommand is actually just here, as its name says, to relay the command fired by the button to a method which contains the execution logic.
And... I think that's all you need. Sorry for the late answer BTW
EDIT - generating columns manually/dynamically
The following code is part of a code I had to do when I had a similar problem.
My problem was, I needed to change the columns displayed every time a ComboBox's SelectedItem would change. So I put this in a SelectionChanged event handler.
I don't know where exactly do you need to generate your columns, but I'll give you a general example.
Assume your ItemsSource is an ObservableCollection<MyNewObject>
MyNewObject is the following:
public class MyNewObject
{
public IList<string> MyStrings { get; set; }
}
You should put somewhere in your code (should be when you need to generate the column) the following code, which is generating a number of columns equal to the length of the first MyNewObject from the list (note: this is in code-behind, and the DataGrid you're working on is named dataGrid)
ObservableCollection<MyNewObject> source = dataGrid.ItemsSource as ObservableCollection<MyNewObject>;
if (source == null || source.Count == 0)
{
return;
}
MyNewObject firstObject = source[0];
for(int i = 0; i < firstObject.MyStrings.Count; i++)
{
// Creates one column filled with buttons for each string
DataGridTemplateColumn columnToAdd = new DataGridTemplateColumn();
columnToAdd.Width = 110; // I set a manual width, but you can do whatever you want
columnToAdd.Header = "Header number " + i;
// Create the template with a Button inside, bound to the appropriate string
DataTemplate dataTemplate = new DataTemplate(typeof(Button));
FrameworkElementFactory buttonElement = new FrameworkElementFactory(typeof(Button));
Binding binding = new Binding("MyStrings[" + i + "]");
binding.Mode = BindingMode.TwoWay;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
buttonElement.SetBinding(Button.ContentProperty, binding);
// Do the same here for your command, or for whatever you want to do when the user clicks on this button
dataTemplate.VisualTree = buttonElement;
columnToAdd.CellTemplate = dataTemplate;
dataGrid.Columns.Add(columnToAdd);
}
This will create one column for each string found in the first object. Then, enhance it with whatever command or display trick you need!
Is there some way to set the Height attribute of a WPF multi-select ListBox to be a multiple of the item height, similar to setting the size attribute of an html select element?
I have a business requirement to not have half an item showing at the bottom of the list (if it's a long list with a scrollbar), and not have extra white space at the bottom (if it's a short list with all items showing), but the only method I can find to do this is to just keep tweaking the Height until it looks about right.
(What else have I tried? I've asked colleagues, searched MSDN and StackOverflow, done some general Googling, and looked at what VS Intellisense offered as I edited the code. There's plenty of advice out there about how to set the height to fit the ListBox's container, but that's the opposite of what I'm trying to do.)
Yeah, one could imagine there would be an easier way to do it (a single snapToWholeElement property). I couldn't find this property as well.
To achieve your requirement, I've wrote a little logic. Basically, In my Windows object I've a public property lbHeight which is calculate the listbox height by calculating the height of each individual item.
First, let's take a look at the XAML:
<Window
x:Class="SO.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="120" SizeToContent="Height"
Title="SO Sample"
>
<StackPanel>
<ListBox x:Name="x_list" Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=lbHeight}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Border x:Name="x" Background="Gray" Margin="4" Padding="3">
<TextBlock Text="{Binding}" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Window>
Note that the ItemTemplate is somewhat non trivial. One important thing to notice is that I gave this item a Name - so I can find it later.
In the code-behind constructor I put some data in the list box:
public MainWindow( )
{
InitializeComponent( );
this.x_list.ItemsSource = Enumerable.Range( 0, 100 );
}
next, I'm implementing a findVisualItem - to find the root element of the data template. I've made this function a little generic, so it get a predicate p which identify whether this is the element I want to find:
private DependencyObject findVisualItem( DependencyObject el, Predicate<DependencyObject> p )
{
DependencyObject found = null;
if( p(el) ) {
found = el;
}
else {
int count = VisualTreeHelper.GetChildrenCount( el );
for( int i=0; i<count; ++i ) {
DependencyObject c = VisualTreeHelper.GetChild( el, i );
found = findVisualItem( c, p );
if( found != null )
break;
}
}
return found;
}
I'll use the following predicate, which returns true if the element I'm looking for is a border, and its name is "x". You should modify this predicate to match your root element of your ItemTemplate.
findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
);
Finally, the lbHeight property:
public double lbHeight
{
get {
FrameworkElement item = findVisualItem(
x_list,
el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
) as FrameworkElement;
if( item != null ) {
double h = item.ActualHeight + item.Margin.Top + item.Margin.Bottom;
return h * 12;
}
else {
return 120;
}
}
}
I've also made the Window implementing INotifyPropertyChanged, and when the items of the list box were loaded (Loaded event of ListBox) I fired a PropertyChanged event for the 'lbHeight' property. At some point it was necessary, but at the end WPF fetched the lbHeight property when I already have a rendered Item.
It is possible your Items aren't identical in Height, in which case you'll have to sum all the Items in the VirtualizedStackPanel. If you have a Horizontal scroll bar, you'll have to consider it for the total height of course. But this is the overall idea. It is only 3 hours since you published your question - I hope someone will come with a simpler answer.
This is done by setting parent control Height property to Auto, without setting any size to the Listbox itself (or also setting to Auto).
To limit the list size you should also specify MaxHeight Property
I have
a collection of StackPanel which each one includes a dynamic set of controls (based on database values), I want to set them as ItemsSource of some ComboBox
for example i have two database values which should be generated:
In DB i have these:
row 1=>Class [a] p [B] , [AB]vb
row 2=>Class tpy rbs=[sdfg],kssc[h] hm
and each one should generate as a ComboBox column like the fallowing:
In ComboBox I wanna generate these :
ComboBoxItem 1 :Class [a textBox] p [a textBox] , [a textBox]vb
ComboBoxItem 2 :Class tpy rbs=[a textBox].kssc[a textBox] hm
the fallowing code is doing this right:
Class ConvertToControlsFormat()
{
Regex exp = new Regex(#"\[\w*\]");
var source = new TestEntities().cmbSources;
foreach (var item in source)
{
StackPanel p = new StackPanel { Orientation = Orientation.Horizontal, FlowDirection = FlowDirection.LeftToRight };
int i = 0;
foreach (string txt in exp.Split(item.Title))
{
p.Children.Add(new TextBlock { Text = txt });
if (i < exp.Matches(item.Title).Count)
p.Children.Add(new TextBox { Text = exp.Matches(item.Title)[i].Value, Width = 30 });
}
cmb.Items.Add(p);
}
}
But I cant set TwoWay DataBindings for that, so I created a list of StackPanel as a field of cmbSource class (which is bound to ItemsSource of the ComboBox)
public partial class cmbSource
{
#region Primitive Properties
int iD;
public virtual int ID
{
get
{
if (Title != null)
ControlsCollection = SetControlsCollection(Title);
return iD;
}
set
{
iD = value;
}
}
private StackPanel SetControlsCollection(string ttl)
{
Regex exp = new Regex(#"\[\w*\]");
StackPanel p = new StackPanel { Orientation = Orientation.Horizontal, FlowDirection = System.Windows.FlowDirection.LeftToRight };
int i = 0;
foreach (string txt in exp.Split(ttl))
{
p.Children.Add(new TextBlock { Text = txt });
if (i < exp.Matches(ttl).Count)
p.Children.Add(new TextBox { Text = exp.Matches(ttl)[i].Value, Width = 30 });
}
return p;
}
public virtual string Title
{
get;
set;
}
public virtual StackPanel ControlsCollection
{
get;
set;
}
#endregion
}
but I have no idea of how bind it to ItemsSource of my ComboBox
Summery:I want to bind a list of controls to a ComboBox
any suggestions!? thank you.
EDIT
First: you do not bind a ComboBox to a collection of UI Elements. That is not the way WPF works. Container controls such as the Grid, StackPanel and Canvas can contain child controls. ItemsControls such as the ComboBox contain data objects and use DataTemplates to display the items.
Secondly: if the database can contain ANY data that could cause ANY UI to be needed you will need to generate the UI in code by creating StackPanels etc. adding controls and bindings as you do in your code examples.
Thirdly: the reason you can't bind is that the data from the database is a string that you split into parts; there is no way you can simply go back to the string.
Suggestion: the string in the database is probably (I hope) in some sort of format. Using that knowledge you could generate a new format string when you are parsing the database string. E.g., when the database contains foo [bar] you could generate {0} [bar]. On a save action from the user you could use that string to create the updated string for the database by using: String.Format("{0} [bar]", someControl.Text)
Extra: Please, next time, use better names and example texts; the question is unreadable like this. There is no way you can expect us to understand 2=>Class tpy rbs=[sdfg],kssc[h] hm
OLD ANSWER
Make a class Stuff, implementing INotifyPropertyChanged and having the properties Name and Value.
Load the database data into an ObservableCollection<Stuff> and bind the ComboBox to this collection.
Set the ItemTemplate of the combo box to a datatemplate like this:
<ComboBox ItemsSource="{Binding}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBox Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I'm having an issue trying to bind to a RectangleGeometry's Rect property. The basic idea here is that I'm attempting to bind a clip mask to control the visualized height of a pseudo-chart object. Here's the XAML:
<Path x:Name="_value" Fill="{DynamicResource PositiveColorBrush}" Data="F1 M10,55 C10,57.75 7.75,60 5,60 2.25,60 0,57.75 0,55 L0,5 C0,2.25 2.25,0 5,0 7.75,0 10,2.25 10,5 L10,55 z">
<Path.Clip>
<!-- SECOND NUMBER CONTROLS THE HEIGHT : SCALE OF 0-60 REVERSED -->
<!--<RectangleGeometry Rect="0,22.82,10,60"/>-->
<RectangleGeometry
Rect="{Binding Score, Converter={StaticResource ChartBarScoreConverter}}" />
</Path.Clip>
</Path>
Note the commented RectangleGeometry there. That works perfectly when I uncomment it and comment out the bound RectangleGeometry. Of course, it won't change size when the Score changes, though.
Now, if I place a breakpoint in the ChartBarScoreConverter, I get the proper value and return a new RectangleGeometry object of the exact same specs as the commented out one there. Here's the short code of the converter:
...
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
RectangleGeometry output = new RectangleGeometry();
double score = 60; //0
if (Common.IsNumeric(value))
{
score = System.Convert.ToDouble(value) * .60;//scale is 0-60
score = 60 - score;//reversed (=
}
output.Rect = new Rect(0, score, 10, 60);
return output;
}
...
When the app is run, it simply doesn't show the clip. As I said, I put a breakpoint in the converter and have verified that it's called and that an object of the correct size is returned... but it just doesn't appear in the view.
Any ideas?
Thanks,
Paul
Your converter is returning a RectangleGeometry which you're then trying to assign to the Rect property of type Rect on a RectangleGeometry. Get rid of the "output" object in the converter and just return the Rect itself.