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. :)
Related
I have this assignment where I must write an interactive program where the user clicks the screen and puts a dot at the spot of his mouse click and then when he puts a second dot they must connect with a line.
<Window x:Class="courseWorkOOP.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:local="clr-namespace:courseWorkOOP"
mc:Ignorable="d"
Title="Практикум ООП" Height="600" Width="800">
<Grid x:Name="myGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Canvas Name="myCanvas" Height="480" Width="640" MouseDown="Canvas_MouseDown_1" MouseMove="Canvas_MouseMove_1">
<Canvas.Background>
<SolidColorBrush Color="White" Opacity="0"/>
</Canvas.Background>
</Canvas>
<Button Click="btn_Click" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="200" Content="Clear"/>
<!--<StackPanel HorizontalAlignment="Left">
<Button Name="btn" Click="btn_Click">Clear</Button>
</StackPanel>-->
</Grid>
This is my XAML
private void Canvas_MouseDown_1(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
currentPoint = e.GetPosition(this);
Ellipse currDot = new Ellipse() { Width = 10, Height = 10,Fill=Brushes.Black };
myCanvas.Children.Add(currDot);
Canvas.SetLeft(currDot, e.GetPosition(this).X);
Canvas.SetTop(currDot, e.GetPosition(this).Y);
double coordinateX = Canvas.GetLeft(currDot);
double coordinateY = Canvas.GetTop(currDot);
Line myLine = new Line() { X1 = coordinateX, Y1 = coordinateY,X2=coordinateY,Y2=coordinateX,Stroke=Brushes.Green,StrokeThickness=4 };
myCanvas.Children.Add(myLine);
}
}
private void btn_Click(object sender, RoutedEventArgs e)
{
myCanvas.Children.Clear();
}
private void Canvas_MouseMove_1(object sender, System.Windows.Input.MouseEventArgs e)
{
//if (e.LeftButton == MouseButtonState.Pressed)
//{
// Line line = new Line();
// line.Stroke = SystemColors.WindowFrameBrush;
// line.StrokeThickness = 20;
// line.X1 = currentPoint.X;
// line.Y1 = currentPoint.Y;
// line.X2 = e.GetPosition(this).X;
// line.Y2 = e.GetPosition(this).Y;
// currentPoint = e.GetPosition(this);
// myCanvas.Children.Add(line);
//}
}
And this is the C# part.
My question is how do I get two dots to connect with a line?
I've tried ClickCount but it did nothing, I might've used it incorrectly.
Before that I've tried initializing an integer value inside Canvas_MouseDown_1 and then made an If statement that basically said draw a line between this and that every two clicks but that didn't work either.
In order to connect the dots with a line, you need to keep track of the previous click point, so that you can connect the "currentPoint" to the "previousPoint". You also need a flag to only create the line once you have at least one point on the canvas.
private Point previousPoint;
private Point currentPoint;
private bool hasPoints;
private void Canvas_MouseDown_1(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
previousPoint = currentPoint;
currentPoint = e.GetPosition(this.myCanvas);
currentPoint.X -= 5; // Use 5, which is half the width/height of the dot
currentPoint.Y -= 5; // Use 5, which is half the width/height of the dot
Ellipse currDot = new Ellipse() { Width = 10, Height = 10, Fill = Brushes.Black };
myCanvas.Children.Add(currDot);
Canvas.SetLeft(currDot, currentPoint.X);
Canvas.SetTop(currDot, currentPoint.Y);
if (hasPoints)
{
// Add 4 to the line position, due to the stroke thickness being 4
Line myLine = new Line() { X1 = previousPoint.X + 4, Y1 = previousPoint.Y + 4, X2 = currentPoint.X + 4, Y2 = currentPoint.Y + 4, Stroke = Brushes.Green, StrokeThickness = 4 };
myCanvas.Children.Add(myLine);
}
hasPoints = true;
}
}
Example output:
EDITED AFTER COMMENT BELOW: If you want every two dots connecting, just make this simple logical change:
if (hasPoints)
{
// Add 4 to the line position...
Line myLine = new Line() { X1 ...
myCanvas.Children.Add(myLine);
hasPoints = false;
}
else
{
hasPoints = true;
}
Output from above code:
Your approach is fundamentally wrong. Before writing a WPF program you should study the documentation, and especially the data binding section. Unfortunately, the Microsoft-provided documentation isn't super great, so you should also read what you can find here and elsewhere on the web about the primary design pattern used for WPF programs, MVVM.
If you follow the MVVM pattern, your program will be simpler to write and simpler to understand, because you won't be struggling to figure out all your UI interactions at the same time that you're also struggling to figure out all the underlying data. MVVM separates these concerns ("separation of concerns" being one of the most important software practices in any context). Here is an example of what that might look like for your example…
First, you know you will have two kinds of graphical objects, so create models for those:
class CanvasItemViewModel
{
public Point Location { get; }
public CanvasItemViewModel(Point location)
{
Location = location;
}
}
class PointViewModel : CanvasItemViewModel
{
public PointViewModel(Point location) : base(location) { }
}
class LineViewModel : CanvasItemViewModel
{
public double X2 { get; }
public double Y2 { get; }
public LineViewModel(PointViewModel start, PointViewModel end) : base(start.Location)
{
X2 = end.Location.X - start.Location.X;
Y2 = end.Location.Y - start.Location.Y;
}
}
In this case, I know ahead of time that I'm going to want to be able to treat points and lines the same, when it comes to positioning them in the canvas, so I use a common base class to represent where on the canvas they will be.
Since the line will be positioned via its Location property, I can leave the X1 and Y1 properties out, and just set the X2 and Y2, based on the points the line will connect.
With these elements taken care of, now I need a way to manage the points and lines. That looks like this:
class MainViewModel
{
public CompositeCollection CanvasItems { get; } = new CompositeCollection();
public MainViewModel()
{
CanvasItems.Add(new CollectionContainer { Collection = _lines });
CanvasItems.Add(new CollectionContainer { Collection = _points });
}
private readonly ObservableCollection<PointViewModel> _points = new ObservableCollection<PointViewModel>();
private readonly ObservableCollection<LineViewModel> _lines = new ObservableCollection<LineViewModel>();
public void AddPoint(Point point)
{
PointViewModel pointModel = new PointViewModel(point);
_points.Add(pointModel);
if (_points.Count > 1)
{
_lines.Add(new LineViewModel(_points[_points.Count - 2], pointModel));
}
}
public void Clear()
{
_points.Clear();
_lines.Clear();
}
}
Here again, I know that I want to display the points and lines in the same control, so the collections are combined into a single CompositeCollection for the benefit of the canvas that will be displaying them. I maintain two separate collections though, to make it easier to manage the collections in the model code.
The line collection is included first in the composite collection, so that the points will draw on top of the lines. Of course, if you want to see the entire line on top of the points, you would simply swap the order of the two collections in the composite collection.
Observable collections are used because the collection contents will be changing based on user input, and this allows WPF to be notified and respond as needed automatically, without additional work on your part. I.e. this is a fundamental aspect of data binding (along with implementing INotifyPropertyChanged, something that's not actually necessary in this example, but which is used heavily in a typical WPF program).
The model code itself does nothing but add points and lines to the collection when necessary, and provide a means to clear both collections.
Note that up to this point, there's nothing that is really dependent on the UI. The classes do use the WPF Point and CompositeCollection types, but this is mainly out of convenience. The implementation isn't really inherently tied to WPF, and those could be abstracted out relatively easily.
With all the basic data structures defined, now is the time to shift the focus to the UI, starting with the XAML:
<Window x:Class="TestSO66159694ClickPointsAndLines.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:TestSO66159694ClickPointsAndLines"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.Resources>
<!-- The templates will rely on the ItemsControl to correctly position them -->
<DataTemplate DataType="{x:Type l:PointViewModel}">
<Ellipse Width="10" Height="10" Fill="Black">
<Ellipse.RenderTransform>
<!-- Center the ellipse on its actual location -->
<TranslateTransform X="-5" Y="-5"/>
</Ellipse.RenderTransform>
</Ellipse>
</DataTemplate>
<DataTemplate DataType="{x:Type l:LineViewModel}">
<Line Stroke="Green" StrokeThickness="4"
X2="{Binding X2}" Y2="{Binding Y2}"/>
</DataTemplate>
</Grid.Resources>
<ItemsControl ItemsSource="{Binding CanvasItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" MouseDown="Canvas_MouseDown_1">
<Canvas.Background>
<SolidColorBrush Color="White" Opacity="0"/>
</Canvas.Background>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Location.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Location.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<Button Click="btn_Click" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="200" Content="Clear"/>
</Grid>
</Window>
This XAML has two main sections to it: the templates corresponding to the graphical elements, which give the actual visual representation for these; and the actual content of the window, which handles binding the collection and setting the event handlers for the user interface (i.e. mouse-down and button-click).
WPF will automatically find the template appropriate for each graphical element. The actual positioning of the element within the canvas is provided by the ItemContainerStyle; this is because an ItemsControl will wrap your actual content in a presenter which is the actual direct child of the canvas, for which the Canvas.Left and Canvas.Top properties apply.
The canvas itself is provided as the ItemsPanelTemplate for the ItemsControl object. The default for ItemsControl is StackPanel, but you can provide any panel template you want to customize the behavior.
Finally, there is the actual user input. With all of the ground-work done above, this is really simple:
public partial class MainWindow : Window
{
private readonly MainViewModel _mainViewModel = new MainViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = _mainViewModel;
}
private void Canvas_MouseDown_1(object sender, MouseButtonEventArgs e)
{
IInputElement canvas = sender as IInputElement;
Point canvasLocation = e.GetPosition(canvas);
_mainViewModel.AddPoint(canvasLocation);
}
private void btn_Click(object sender, RoutedEventArgs e)
{
_mainViewModel.Clear();
}
}
Since the main view model object has all the real data management logic, all the view itself (i.e. the window) needs to do is get the mouse position and pass that along to the main view model to deal with. Likewise, clearing the list is delegated to the main view model as well.
The view itself knows nothing about the underlying data structures, nor should it. The view's job is to provide the layer in your problem that interacts directly with the user, presenting to the user the underlying data in a form that is useful and comprehensible to the user, and taking user input and passing that along to the underlying data structures so that it can accomplish whatever it needs to do.
In addition to simplifying the overall design of the program, and making it easier to think about each discrete function of the program separately, doing it this way makes it trivial to adjust the way the visuals are presented to the user, without touching the C# code at all. One just needs to update the templates according to whatever visual aspect is desired.
And of course, when doing things correctly, as above, it's also simple to update the underlying data logic without having to meddle with the view. For example, it turns out your original question was not clear enough and you only want to connect every pair of dots the user clicks, but not make a continuous line. That's an easy enough change, simply by modifying the AddPoint() method by adding a single additional condition to the if statement that adds the line:
public void AddPoint(Point point)
{
PointViewModel pointModel = new PointViewModel(point);
_points.Add(pointModel);
if (_points.Count > 1 && _points.Count % 2 == 0)
{
_lines.Add(new LineViewModel(_points[_points.Count - 2], pointModel));
}
}
I.e. instead of just checking that there are two or more points with _points.Count > 1, also limit adding a line to only when a new pair of points has been added, by including _points.Count % 2 == 0. No need for new variables or anything like that. Just take into account the current state of things and act on that.
Note that one of the reasons this change is so easy is that the code above doesn't abuse the user interface API to store the state of your underlying data, and so you have immediate access to the number of points that have been added. This approach would be significantly more challenging if you had to figure out from the state of the UI how many points the user had already added (hence the different approach taken by the other answer, which is to add even more state to the UI to try to keep track of what the user's doing).
Again, good WPF programs always follow the principle of Separation of Concerns. Indeed, any good program does, but with WPF the framework actually makes it much easier to do so, and rewards you when you do. I encourage you to keep that in mind as you continue to learn about programming.
Consider the following XAML from my UserControl:
<TextBlock Text="HelloWorld" Loaded="TextBlock_OnLoaded" />
And the associated event handler:
private void TextBlock_OnLoaded(object sender, RoutedEventArgs e)
{
var xaml = XamlWriter.Save(sender);
Console.WriteLine(xaml);
}
When the TextBlock is loaded, the following output is written to the Console:
<TextBlock Text="HelloWorld" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
Now consider this alternative XAML:
<ListBox ItemsSource="{Binding SomeCollection}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="HelloWorld" Loaded="TextBlock_OnLoaded" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now when the TextBlock is loaded, the following output is written to the Console:
<TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
<TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
<TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
......
Notice that the TextProperty is no longer being serialized.
If the following TextProperty assignment is added before the call to XamlWriter.Save():
private void TextBlock_OnLoaded(object sender, RoutedEventArgs e)
{
var textBlock = sender as TextBlock;
if (textBlock != null)
{
textBlock.Text = textBlock.Text;
}
var xaml = XamlWriter.Save(sender);
Console.WriteLine(xaml);
}
Then when the TextBlock is loaded, the following output is written to the Console:
<TextBlock Text="HelloWorld" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
<TextBlock Text="HelloWorld" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
<TextBlock Text="HelloWorld" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
......
Notice that the TextProperty is again being serialized.
This blog post explains that "... if the property is backed by a DependencyProperty ... the property is only written if it is actually set."
It appears that the TextProperty is indeed being set in the first usage example, but not in the second usage example with the ListBox and DataTemplate.
Can anyone explain why this is the case, and how to overcome this obstacle?
My best guess is that the XAML parser is somehow setting the TextBlock state internally instead of calling SetValue on the dependency property, but I'm not sure why it would do this only for elements inside a DataTemplate.
XamlWriter.Save appears to only serialize locally-set values. In XAML, values can come from multiple levels of sources.
When you set TextBlock.Text directly, you are looking at a "local value" set (precedence 3). However, when you set it inside a data template, you are setting template properties (precedence 4). By writing
textBlock.Text = textBlock.Text;
you are actually transforming that into a local property set (precedence 3)!
If you look at the some of the source code involved in XamlWriter.Save, you can see (line 819) that it explicitly reads the local value of the property.
Unfortunately, I'm not sure what a good work-around is for this. XamlWriter has known limitations. You an try inheriting from XamlDesignerSerializationManager and calling the XamlWriter.Save(Object, XamlDesignerSerializationManager) overload, but it doesn't look very promising. More likely, you will have to either do what you do above, or write your own serialization routine (at least Microsoft has made their source readily available as a guide).
In light of NextInLine's answer, I've come up with the following work around:
public static IEnumerable<DependencyProperty> GetDependencyProperties(this DependencyObject obj)
{
var propertyDescriptors = TypeDescriptor.GetProperties(obj, new Attribute[]
{
new PropertyFilterAttribute(PropertyFilterOptions.All)
});
return (from PropertyDescriptor pd in propertyDescriptors
let dpd = DependencyPropertyDescriptor.FromProperty(pd)
where dpd != null
select dpd.DependencyProperty).ToList();
}
public static IEnumerable<DependencyProperty> GetUpdatedDependencyProperties(this DependencyObject obj)
{
return (from property in obj.GetDependencyProperties().Where(x => !x.ReadOnly)
let metaData = property.GetMetadata(obj.GetType())
let defaultValue = metaData.DefaultValue
let currentValue = obj.GetValue(property)
where currentValue != defaultValue
select property).ToList();
}
Which can be used like this:
foreach (var updatedProperty in dependencyObject.GetUpdatedDependencyProperties())
{
dependencyObject.SetValue(updatedProperty, dependencyObject.GetValue(updatedProperty));
}
This will force XamlWriter.Save(dependencyObject) to serialize all of the dependencyObject properties that were updated in XAML.
I have a longListSelector that create several canvas dynamically and I want to draw in each canvas by using data from my ObservableCollection Games.
Here is my base code of the main page:
<Grid x:Name="ContentPanel">
<phone:LongListSelector Name="myLLS" ItemSource="{Binding GamesVM}">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel>
<Canvas /> <!-- Here I want to draw -->
<TextBlock Text="{Binding Title}"/>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
</Grid>
public class GameVM : INotifyPropertyChanged {
private string _title;
public string Title {
get { return this._title; }
set {
if (this._title!= value) {
this._title= value;
this.OnPropertyChanged("Title");
}
}
}
public void Draw() {
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
myCanvas.Children.Add(stone);
}
}
I would like to execute my Draw method when my GamesVM collection is generated but I haven't access to the corresponding canvas at this time. Putting my Draw method in code behind doesn't help because I have no event to handle where I could get both data binding object and the canvas newly generated (except if I miss something...). So I have no "myCanvas" instance in my Draw method.
I have some ideas to do that but nothing work well.
Option 1
I can put my UIElement (Ellipse, Line, etc) in an ObservableCollection which is binded in an ItemsControl like this :
<ItemsControl ItemsSource="{Binding myUIElements}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
public void Draw() {
myUIElements = new ObservableCollection<UIElement>();
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
myUIElements.Add(stone);
}
It works but when I leave the page and come back, I get an Element is already the child of another element exception.
If I use VisualTreeHelper to find my ItemsControl and call Items.Clear() on it, I get an exception too beacuse Items is read-only.
Option 2
I can use a ContentControl instead of ItemsControl and create the canvas in my Draw method:
<ContentControl Content="{Binding myUICanvas"/>
public void Draw() {
myUICanvas = new Canvas();
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
myUICanvas.Children.Add(stone);
}
It works too but when I leave the page and come back, I get a Value does not fall within the expected range exception.
I understand that I can't bind UIElement because I can't clear them when the Framework try to set them again. What is the trick to say "Please, do not add the same element twice" ?
Option 3
I can try to draw directly in XAML and bind a ViewModel object instead of UIElement object.
<ItemsControl ItemsSource="{Binding myDatas}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="{Binding Diameter}" Fill="Black" ...>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
It could work in WPF but in my Windows Phone 8 app, I have no ItemContainerStyle property to set Canvas.Left and Canvas.Right. Beside I would have to use a CompositeCollection to deal with several kind of shapes but DataType is not recognized by Visual Studio.
Moreover, even if it works with Line UIElements, the render is slower than c# approach.
So, what is the best option and how to deal with my exceptions ?
For information, I give you which one I choose.
I take option 2 and avoid the come back error by redrawing a new Canvas each time. I change my Draw definition so it return me the new Canvas.
public class GameVM : INotifyPropertyChanged {
// Title and other properties
private Canvas _myUICanvas;
public Canvas myUICanvas
{
get {
_myUICanvas = Draw();
return _myUICanvas;
}
set {
// this is never called
_myUICanvas = value;
}
}
public Canvas Draw() {
Canvas newCanvas = new Canvas();
Ellispe stone = new Ellipse();
// [...] Add Fill, Strock, Width, Height properties and set Canvas.Left and Canvas.Top...
newCanvas.Children.Add(stone);
return newCanvas;
}
}
Like this, I can run my program without error and without reloading/recreating all the GameVM instances.
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.
How do I programmatically change the color of an ellipse that is defined in XAML based on a variable?
Everything I've read on binding is based on collections and lists -can't I set it simply (and literally) based on the value of a string variable? string color = "red" color = "#FF0000"
It's worth pointing out that the converter the other posts reference already exists, which is why you can do <Ellipse Fill="red"> in xaml in the first place. The converter is System.Windows.Media.BrushConverter:
BrushConverter bc = new BrushConverter();
Brush brush = (Brush) bc.ConvertFrom("Red");
The more efficient way is to use the full syntax:
myEllipse.Fill = new SolidColorBrush(Colors.Red);
EDIT in response to -1 and comments:
The code above works perfectly fine in code, which is what the original question was asking about. You also don't want an IValueConverter - these are typically used for binding scenarios. A TypeConverter is the right solution here (because you're one-way converting a string to a brush). See this article for details.
Further edit (having reread Aviad's comment): you don't need to explicitly use the TypeConverter in Xaml - it's used for you. If I write this in Xaml:
<Ellipse Fill="red">
... then the runtime automagically uses a BrushConverter to turn the string literal into a brush. That Xaml is essentially converted into the equivalent longhand:
<Ellipse>
<Ellipse.Fill>
<SolidColorBrush Color="#FFFF0000" />
</Ellipse.Fill>
</Ellipse>
So you're right - you can't use it in Xaml - but you don't need to.
Even if you had a string value that you wanted to bind in as the fill, you don't need to specify the converter manually. This test from Kaxaml:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<Page.Resources>
<s:String x:Key="col">Red</s:String>
</Page.Resources>
<StackPanel>
<Ellipse Width="20" Height="20" Fill="{Binding Source={StaticResource col}}" />
</StackPanel>
</Page>
Strangely, you can't just use the StaticResource col and still have this work - but with the binding it and automatically uses the ValueConverter to turn the string into a brush.
what you will need to do is implement a custom converter to convert the colour to the brush object. Something like this...
public class ColorToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
System.Drawing.Color col = (System.Drawing.Color)value;
Color c = Color.FromArgb(col.A, col.R, col.G, col.B);
return new System.Windows.Media.SolidColorBrush(c);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
SolidColorBrush c = (SolidColorBrush)value;
System.Drawing.Color col = System.Drawing.Color.FromArgb(c.Color.A, c.Color.R, c.Color.G, c.Color.B);
return col;
}
}
And then specify that converter in your binding
Fill="{Binding Colors.Red, Converter={StaticResource ColorToBrushConverter }"
use
System.Windows.Media
If the name of your ellipse in your XAML is my_ellipse,
write something like this:
my_ellipse.Fill = System.Windows.Media.Brushes.Red;
or this:
my_ellipse.Fill = (SolidColorBrush)new BrushConverter().ConvertFromString("#F4F4F5")
A quick work around for this (Although its not binding, and less efficient) is to check the status of an object/element and update the new/other object(s) based on the status, I'll provide a Basic Example Below.
You can do this in MVVM by getting the status of MainWindow Objects from a User Control and changing whatever you want from the MainWindow object status.
Note: this will not work for 2 themed applications and other scenarios where more than basic use case is needed. (Example: Dark Mode, Etc)
In my example, I am using data to draw a "text" on screen (FrontFog_Text, Which is why the text is using the .Fill Property in my case).
I am not using this in my application but came across this when testing a few things so I thought I would share in this thread while I search for my answer!
private void FrontFog_ToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
if (((MainWindow)App.Current.MainWindow).Theme_Control.IsChecked == true)
{
FrontFog_Text.Fill = new SolidColorBrush(System.Windows.Media.Colors.White);
}
else if (((MainWindow)App.Current.MainWindow).Theme_Control.IsChecked == false)
{
FrontFog_Text.Fill = new SolidColorBrush(System.Windows.Media.Colors.Black);
}
}