How to make Scaling Images have clickable regions in WPF - c#

Using an image inside of a viewbox (inside a DockPanel), I've created a image that scales with my window in a wpf application.
<Viewbox HorizontalAlignment="Left" Name="viewbox1" VerticalAlignment="Top" Stretch="Uniform" StretchDirection="UpOnly">
<Image Height="438" Name="image1" Stretch="Uniform" Width="277" Source="/MyAPP;component/Images/TicTacToeBoardForExample.png" MouseDown="image1_MouseDown" />
</Viewbox>
How can I make certain regions of the picture run different code when clicked?
I'm desiring to do this in such a fashion that no matter what size the image scales (upon window resizing), the exact regions desired, scale perfectly with the image so that, when clicked, it always triggers their corresponding code-to-be-run for that region.

Already an old question, but as I had a similar requirement, I implemented the suggestion from Terry and want to share it with you. I use 10x10 Buttons/regions.
The View:
<Grid>
<Image Stretch="Fill" Panel.ZIndex="1" Source="{Binding BitMapSource}" />
<ItemsControl Panel.ZIndex="2" ItemsSource="{Binding RectangleItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Opacity="0.3" Content="{Binding Tag}"></Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
The ViewModel:
public class MainWindowViewModel : PropertyChangedBase
{
public MainWindowViewModel()
{
InitRectangles();
}
public BitmapSource BitMapSource => new BitmapImage(new Uri(#"C:\tmp\images\plus.png"));
public ObservableCollection<RectangleItem> RectangleItems { get; set; }
private void InitRectangles()
{
RectangleItems = new ObservableCollection<RectangleItem>();
for (var i = 0; i < 10; i++)
{
for (var j = 0; j < 10; j++)
{
RectangleItems.Add(new RectangleItem
{
X = j * 10,
Y = i*10,
});
}
}
}
}
And the RectangleItem (Model)
public class RectangleItem
{
public string Tag => $"{X}, {Y}";
public double X { get; set; }
public double Y { get; set; }
}

Off memory you could do something like this:
public void image1_MouseDown(object sender, MouseEventArgs e)
{
var pos = e.GetPosition(viewbox1);
if (/* pos in range 1 */) DoTheThingInRange1();
else if (/*pos in range 2*/) DoTheThingInRange2();
else if (/*pos in range 3...*/) DoTheThingInRange3();
//so on...
}
HTH

Your exact requirement is not clear, But it would be helpful if you take a look at the TranslatePoint and PointToScreen methods on the FrameworkElement.

i once needed to do a similar thing.
What is did was put buttons over the image. Then u put their opacity to 0 or 3.0 or something, so that u don't see the buttons, but can still click them.
When properly added, the buttons can resize along with the image.

Related

Why is data binding to canvas not updating UI

I am trying to data bind a collection of lines, and perform a sort function on them and update the UI once the sort has been completed (would like to show the differences in sort algorithms).
I have a basic WPF application which consists of an ItemsControl which is bound to a collection of objects. These objects are bound correctly when the screen is first rendered, however once the sort operation has been completed, the underlying list has been sorted correctly, but the UI has not been redrawn?
Here is my XAML
<Grid>
<Button Content="Sort" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="12" MinWidth="80" Click="Button_Click"/>
<ItemsControl x:Name="mainControl" ItemsSource="{Binding Values}" ItemTemplate="{StaticResource LineDataTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter" />
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
there is a xaml data template
<DataTemplate x:Key="LineDataTemplate">
<Line X1="{Binding X1}" Y1="{Binding Y1}"
X2="{Binding X2}" Y2="{Binding Y2}"
Stroke="DarkGray" StrokeThickness="3"/>
</DataTemplate>
The main data context contains a list of this Line object
public class Line
{
public int X1 { get; set; }
public int Y1 { get; set; }
public int X2 { get; set; }
public int Y2 { get; set; }
}
And when the datacontext is initialised I create some random lines
private void RandomiseLines()
{
var rnd = new Random();
var startingPoint = 2;
Values = new List<Line>();
for (int i = 0; i < 3; i++)
{
Values.Add(new Line() { X1 = startingPoint, Y1 = 420, X2 = startingPoint, Y2 = (420 - rnd.Next(1, 300)) });
startingPoint += 4;
}
}
Then I have a button on the UI which calls through and (for now) calls a basic sort using linq
Values = Values.OrderBy(x => x.Y2).ToList();
The data context, where this list is held implements the INotifiedProperty changed interface, and once the list is sorted I make a call to the Property changed event. Although the underlying list get sorted the UI does not seem to be redrawing, I have tried using an ObservableCollection and wrapping in Dispatcher but I do not seem to have any binding errors or exceptions being thrown. Can anyone please explain why this does not get updated?
Edit: Added expected result
The expected result would be the ItemsControl redrawing itself and the lines would be in the new sorted order
You better use Rectangle instead of Line, because it doesn't rely on coordinates for positioning. You just give them a shared Width but a variable Hight. The ItemsPanel should be a StackPanel with the StackPanel.Orientation set to Horizontal. The Value collection must be a ObservableCollection<double>. Then it should behave as expected.
This way the order of the bars will reflect the order of the collection.
The main view
<StackPanel>
<Button Content="Sort"
Click="Button_Click" />
<ItemsControl x:Name="mainControl"
ItemsSource="{Binding Values}"
ItemTemplate="{DynamicResource LineDataTemplate}">
<ItemsControl.Resources>
<DataTemplate x:Key="LineDataTemplate" DataType="system:Double">
<Rectangle Width="5"
Height="{Binding}"
VerticalAlignment="Bottom"
Fill="DarkGray"
Margin="0,0,3,0" />
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
The Button.Click event handler
private void Button_Click(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as TestViewModel;
var orderedValues = viewModel.Values.OrderBy(value => value).ToList();
viewModel.Values = new ObservableCollection<double>(orderedValues);
}
The view model
private void RandomiseLines()
{
var rnd = new Random();
Values = new ObservableCollection<double>();
for (int i = 0; i < 3; i++)
{
Values.Add(rnd.Next(1, 300));
}
}

TwoWay Binding a PointCollection

I'm attempting to simulate a kind of simple CAD-like behaviour using WPF, where a polygon is drawn onto a canvas, and the points of the polygon can be moved around.
I've managed to get it so that a PointCollection property is displayed on the canvas by setting up an ItemsControl with an Ellipse for each Point in the collection. The polygon displays correctly, so I know the Binding is working Source-To-Target. I've set up some simple events to be able to 'pick up' points of the polygon, and move them around. The events work fine, and the polygon does change on the canvas. However, any changes I make to the point positions are not getting pushed to the Source Property.
Any clues on how to get the Binding to work in the opposite direction? Is this sort of thing (Two-Way binding inside ItemsControl) even possible? Or perhaps there's another approach to this!
Thanks
XAML
<Canvas Width="500" Height="300" MouseMove="MoveMouse">
<ItemsControl ItemsSource="{Binding Path=MyPath, Mode=TwoWay, NotifyOnSourceUpdated=True }">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<Ellipse Width="20"
Height="20"
Canvas.Left="{Binding Path=X, Mode=TwoWay, NotifyOnSourceUpdated=True }"
Canvas.Top="{Binding Path=Y, Mode=TwoWay, NotifyOnSourceUpdated=True }"
Fill="Red"
Margin="-10,-10,0,0"
MouseDown="ClickPoint" />
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
MainWindow.cs
public partial class MainWindow : Window
{
public PointCollection MyPath { get; set; } = new PointCollection{ new Point(10, 10), new Point(100, 100), new Point(200, 100) };
public Ellipse selectedPoint;
public MainWindow()
{
InitializeComponent();
}
private void MoveMouse(object sender, MouseEventArgs e)
{
if (selectedPoint == null) return;
//Move selected point to mouse position
selectedPoint.SetValue(Canvas.LeftProperty, e.GetPosition(sender as UIElement).X);
selectedPoint.SetValue(Canvas.TopProperty, e.GetPosition(sender as UIElement).Y);
//Print out MyPath points to check if source has changed
Console.WriteLine("");
foreach (Point p in MyPath)
Console.Write(p + " - ");
}
private void ClickPoint(object sender, MouseButtonEventArgs e)
{
//Pick up point, if no point is currently selected
selectedPoint = selectedPoint == null ? (Ellipse)sender : null;
}
}

Control.AddRange(...) is slow

Project:
I have a parent panel which holds a ComboBox and FlowLayoutPanel. The FlowLayoutPanel holds a variable number of child panels (a custom control that inherits from UserControl). Each child panel contains some labels, two ComboBoxes, a button, and a DataGridView with 3 ComboBox columns and a button column. The DataGridView may have 1-6 rows. The FlowLayoutPanel is populated with child panels when an item is selected from the ComboBox on the parent panel.
Problem:
Populating the FlowLayoutPanel with about 50 child panels takes about 2.5 seconds. Specifically, I've determined that the call to FlowLayoutPanel.Controls.AddRange() is the culprit.
Relevant Code: I can't post all of my code here (too much code plus parts of it are confidential), but I'll do my best to explain what is happening.
Parent Panel:
private void displayInformation(Suite suite)
{
this.SuspendLayout();
// Get dependencies.
List<SuiteRange> dependents = new List<SuiteRange>(suite.dependencies.Keys);
dependents.Sort(SuiteRange.Compare);
// Create a ChildPanel for each dependent.
List<ChildPanel> rangePanels = new List<ChildPanel>();
foreach (SuiteRange dependent in dependents)
{
ChildPanel sdp = new ChildPanel();
sdp.initialize(initialSuite.name, dataAccess);
sdp.displayInformation(dependent, suite.dependencies[dependent]);
rangePanels.Add(sdp);
}
// Put the child panels in the FlowLayoutPanel.
flpDependencyGroups.SuspendLayout();
// Takes ~2.5 seconds
flpDependencyGroups.Controls.AddRange(rangePanels.ToArray());
flpDependencyGroups.ResumeLayout();
// Takes ~0.5 seconds
updateChildPanelSizes();
this.ResumeLayout();
}
Things I've tried:
Call SuspendLayout() / ResumeLayout() on the parent panel and/or FlowLayoutPanel. Minimal performance increase (~0.2 seconds).
Use Control.FlatStyle.Flat on ComboBoxes, Buttons, and DataGridView columns. Minimal performance increase (~0.1 seconds).
Verified that none of my controls use a transparent background color.
Set ChildPanel.DoubleBuffered and ParentPanel.DoubleBuffered to true.
Remove the FlowLayoutPanel from its parent before calling AddRange() and re-adding it after.
Things that might be relevant:
The panels and controls use anchors (as opposed to autosize or dock).
My controls are manually populated and do not use the DataSource property.
EDIT: Solution:
#HighCore's answer is the correct solution. Unfortunately I won't be implementing it at this time (it could happen down the road) because I found a workaround. The workaround doesn't really solve the problem, just masks it, hence why I'm not posting this as an answer. I discovered that the form loads in half the time if the Dependencies tab isn't on top (i.e. the Product Lists tab is selected). This reduces loading time to about 1 second, which is acceptable. When data is being loaded and the Dependencies tab is on top, I switch to the Product Lists tab, throw up a dark grey box over the tab control that says "Loading..." in the middle, load the data, and then switch back to the Dependencies tab.
Thanks all for your comments and suggestions, it was greatly appreciated.
Posting this answer because the OP requested it:
This is how you'd do something like that in WPF:
<UserControl x:Class="WpfApplication7.ListBoxSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<Button Content="Load" Click="Load_Click" DockPanel.Dock="Top"/>
<ListBox ItemsSource="{Binding}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1" Padding="5"
Background="#FFFAFAFA">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Dependent Versions" FontWeight="Bold"
Grid.ColumnSpan="2" HorizontalAlignment="Center"/>
<TextBlock Text="From:" FontWeight="Bold"
Grid.Row="1" HorizontalAlignment="Center"/>
<TextBlock Text="To (exclusive):" FontWeight="Bold"
Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center"/>
<ComboBox SelectedItem="{Binding From}"
ItemsSource="{Binding FromOptions}"
Grid.Row="2" Margin="5"/>
<ComboBox SelectedItem="{Binding To}"
ItemsSource="{Binding ToOptions}"
Grid.Row="2" Grid.Column="1" Margin="5"/>
<DataGrid ItemsSource="{Binding ChildItems}"
AutoGenerateColumns="False" CanUserAddRows="False"
Grid.Column="2" Grid.RowSpan="4">
<DataGrid.Columns>
<DataGridTextColumn Header="XXXX" Binding="{Binding XXXX}"/>
<DataGridTextColumn Header="Dependee From" Binding="{Binding DependeeFrom}"/>
<DataGridTextColumn Header="Dependee To" Binding="{Binding DependeeTo}"/>
<DataGridTemplateColumn Width="25">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="X"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Content="Delete"
Grid.Column="3"
HorizontalAlignment="Right" VerticalAlignment="Top"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UserControl>
Code Behind (only boilerplate to support the example)
public partial class ListBoxSample : UserControl
{
public ListBoxSample()
{
InitializeComponent();
}
public void LoadData()
{
Task.Factory.StartNew(() =>
{
var list = new List<DataItem>();
for (int i = 0; i < 100000; i++)
{
var item = new DataItem()
{
From = "1",
To = "2",
ChildItems =
{
new ChildItem()
{
DependeeFrom = i.ToString(),
DependeeTo = (i + 10).ToString(),
XXXX = "XXXX"
},
new ChildItem()
{
DependeeFrom = i.ToString(),
DependeeTo = (i + 10).ToString(),
XXXX = "XXXX"
},
new ChildItem()
{
DependeeFrom = i.ToString(),
DependeeTo = (i + 10).ToString(),
XXXX = "XXXX"
}
}
};
list.Add(item);
}
return list;
}).ContinueWith(t =>
{
Dispatcher.Invoke((Action) (() => DataContext = t.Result));
});
}
private void Load_Click(object sender, System.Windows.RoutedEventArgs e)
{
LoadData();
}
}
Data Items:
public class DataItem
{
public List<ChildItem> ChildItems { get; set; }
public List<string> FromOptions { get; set; }
public List<string> ToOptions { get; set; }
public string From { get; set; }
public string To { get; set; }
public DataItem()
{
ChildItems = new List<ChildItem>();
FromOptions = Enumerable.Range(0,10).Select(x => x.ToString()).ToList();
ToOptions = Enumerable.Range(0, 10).Select(x => x.ToString()).ToList();
}
}
public class ChildItem
{
public string XXXX { get; set; }
public string DependeeFrom { get; set; }
public string DependeeTo { get; set; }
}
Then you put that in an existing winforms UI using an ElementHost:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var elementHost = new ElementHost
{
Dock = DockStyle.Fill,
Child = new ListBoxSample()
};
Controls.Add(elementHost);
}
}
Result:
Notice that I added 100,000 records. Still, response time (both when scrolling and interacting with the UI) is immediate due to WPF's built in UI Virtualization.
Also notice that I'm using DataBinding which removes the need to manipulate UI elements in procedural code. This is important because the WPF Visual Tree is a complex structure, and DataBinding is the preferred approach in WPF always.
Also notice by resizing the form that the UI is completely resolution independent. You can customize it further by making the ComboBoxes fixed and having the DataGrid stretch to the remaining space. See WPF Layouts.
WPF Rocks. - see how much you can achieve with so little code, and without spending lots of $$$ in third party controls. You should really forget winforms forever.
You will need to target .Net 3.0 at a minimum, but 4.0/4.5 is highly recommended because WPF had several issues in earlier versions, which were fixed in 4.0.
Make sure you reference PresentationCore.dll, PresentationFramework.dll, WindowsBase.dll, System.Xaml.dll and WindowsFormsIntegration.dll, all of which belong to the .Net Framework itself (no 3rd parties)

Choppy movement of large collection of items on a canvas

This question is directly related to a question I recently posted, but I feel that the direction has changed enough to warrant a new one. I am trying to figure out the best way to move a large collection of images on a canvas in real-time. My XAML currently looks like this:
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:Entity}">
<Canvas>
<Image Canvas.Left="{Binding Location.X}"
Canvas.Top="{Binding Location.Y}"
Width="{Binding Width}"
Height="{Binding Height}"
Source="{Binding Image}" />
</Canvas>
</DataTemplate>
</UserControl.Resources>
<Canvas x:Name="content"
Width="2000"
Height="2000"
Background="LightGreen">
<ItemsControl Canvas.ZIndex="2" ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The Entity class:
[Magic]
public class Entity : ObservableObject
{
public Entity()
{
Height = 16;
Width = 16;
Location = new Vector(Global.rand.Next(800), Global.rand.Next(800));
Image = Global.LoadBitmap("Resources/Thing1.png");
}
public int Height { get; set; }
public int Width { get; set; }
public Vector Location { get; set; }
public WriteableBitmap Image { get; set; }
}
To move the object:
private Action<Entity> action = (Entity entity) =>
{
entity.Location = new Vector(entity.Location.X + 1, entity.Location.Y);
};
void Timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
});
}
If I have fewer than about 400 entries in the Entities collection, movement is smooth, but I'd like to be able to increase that number by quite a bit. If I go above 400, movement becomes increasingly choppy. At first I thought it was an issue with the movement logic (which at this point isn't really much of anything), but I have found that that's not the problem. I added another collection with 10,000 entries and added that collection to the same timer loop as the first but did not include it in the XAML, and the UI didn't react any differently. What I find odd, however, is that if I add 400 entries to the collection and then 400 more with Image set to null, movement becomes choppy even though half of the items aren't drawn.
So, what can I do, if anything, to be able to draw and smoothly move more images on a canvas? Is this a situation where I may want to shy away from WPF & XAML? If you need more code, I will gladly post it.
Update: Per Clemens' suggestion, my Entity DataTemplate now looks like this:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}"
Height="{Binding Height}"
Source="{Binding Image}">
<Image.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}" />
</Image.RenderTransform>
</Image>
</DataTemplate>
There may be a boost in performance by using this, but if there is it is very subtle. Also, I have noticed that if I use a DispatcherTimer for the loop and set it up as:
private DispatcherTimer dTimer = new DispatcherTimer();
public Loop()
{
dTimer.Interval = TimeSpan.FromMilliseconds(30);
dTimer.Tick += Timer_Tick;
dTimer.Start();
}
void Timer_Tick(object sender, EventArgs e)
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
}
... The movement is smooth even with several thousand items, but very slow, regardless of the interval. If a DispatcherTimer is used and Timer_Tick looks like this:
void Timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
});
}
... the movement is very choppy. What I find odd is that a Stopwatch shows that the Task.Factory takes between 1000 and 1400 ticks to iterate over the collection if there are 5,000 entries. The standard foreach loop takes over 3,000 ticks. Why would Task.Factory perform so poorly when it is twice as fast? Is there a different way to iterate through the collection and/or a different timing method that might allow for smooth movement without any major slowdowns?
Update: If anybody can help me improve the performance of real-time movement of objects on a canvas or can suggest another way in WPF to achieve similar results, 100 bounty awaits.
Having so many controls move on the screen this frequently will never yield smooth results. You need to a completely different approach - rendering on your own. I'm not sure this would suit you, as now you will not be able to use control features per each item (e.g. to receive events, have tooltips or use data templates.) But with such a large amount of items, other approaches are impractical.
Here's a (very) rudimentary implementation of what that might look like:
Update: I've modified the renderer class to use the CompositionTarget.Rendering event instead of a DispatcherTimer. This event fires every time WPF renders a frame (normally around 60 fps). While this would provide smoother results, it is also more CPU intensive, so be sure to turn off the animation when it's no longer needed.
public class ItemsRenderer : FrameworkElement
{
private bool _isLoaded;
public ItemsRenderer()
{
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_isLoaded = true;
if (IsAnimating)
{
Start();
}
}
private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
_isLoaded = false;
Stop();
}
public bool IsAnimating
{
get { return (bool)GetValue(IsAnimatingProperty); }
set { SetValue(IsAnimatingProperty, value); }
}
public static readonly DependencyProperty IsAnimatingProperty =
DependencyProperty.Register("IsAnimating", typeof(bool), typeof(ItemsRenderer), new FrameworkPropertyMetadata(false, (d, e) => ((ItemsRenderer)d).OnIsAnimatingChanged((bool)e.NewValue)));
private void OnIsAnimatingChanged(bool isAnimating)
{
if (_isLoaded)
{
Stop();
if (isAnimating)
{
Start();
}
}
}
private void Start()
{
CompositionTarget.Rendering += CompositionTargetOnRendering;
}
private void Stop()
{
CompositionTarget.Rendering -= CompositionTargetOnRendering;
}
private void CompositionTargetOnRendering(object sender, EventArgs eventArgs)
{
InvalidateVisual();
}
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof (ImageSource), typeof (ItemsRenderer), new FrameworkPropertyMetadata());
public ImageSource ImageSource
{
get { return (ImageSource) GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
public static readonly DependencyProperty ImageSizeProperty =
DependencyProperty.Register("ImageSize", typeof(Size), typeof(ItemsRenderer), new FrameworkPropertyMetadata(Size.Empty));
public Size ImageSize
{
get { return (Size) GetValue(ImageSizeProperty); }
set { SetValue(ImageSizeProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof (IEnumerable), typeof (ItemsRenderer), new FrameworkPropertyMetadata());
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
protected override void OnRender(DrawingContext dc)
{
ImageSource imageSource = ImageSource;
IEnumerable itemsSource = ItemsSource;
if (itemsSource == null || imageSource == null) return;
Size size = ImageSize.IsEmpty ? new Size(imageSource.Width, imageSource.Height) : ImageSize;
foreach (var item in itemsSource)
{
dc.DrawImage(imageSource, new Rect(GetPoint(item), size));
}
}
private Point GetPoint(object item)
{
var args = new ItemPointEventArgs(item);
OnPointRequested(args);
return args.Point;
}
public event EventHandler<ItemPointEventArgs> PointRequested;
protected virtual void OnPointRequested(ItemPointEventArgs e)
{
EventHandler<ItemPointEventArgs> handler = PointRequested;
if (handler != null) handler(this, e);
}
}
public class ItemPointEventArgs : EventArgs
{
public ItemPointEventArgs(object item)
{
Item = item;
}
public object Item { get; private set; }
public Point Point { get; set; }
}
Usage:
<my:ItemsRenderer x:Name="Renderer"
ImageSize="8 8"
ImageSource="32.png"
PointRequested="OnPointRequested" />
Code Behind:
Renderer.ItemsSource = Enumerable.Range(0, 2000)
.Select(t => new Item { Location = new Point(_rng.Next(800), _rng.Next(800)) }).ToArray();
private void OnPointRequested(object sender, ItemPointEventArgs e)
{
var item = (Item) e.Item;
item.Location = e.Point = new Point(item.Location.X + 1, item.Location.Y);
}
You can use the OnPointRequested approach to get any data from the item (such as the image itself.) Also, don't forget to freeze your images, and pre-resize them.
A side note, regarding threading in the previous solutions. When you use a Task, you're actually posting the property update to another thread. Since you've bound the image to that property, and WPF elements can only be updated from the thread on which they were created, WPF automatically posts each update to the Dispatcher queue to be executed on that thread. That's why the loop ends faster, and you're not timing the actual work of updating the UI. It's only adding more work.
In a first optimization approach you may reduce the number of Canvases to just one by removing the Canvas from the DataTemplate and setting Canvas.Left and Canvas.Top in an ItemContainerStyle:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Image}"/>
</DataTemplate>
<ItemsControl ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Location.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Location.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Then you may replace setting Canvas.Left and Canvas.Top by applying a TranslateTransform:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="RenderTransform">
<Setter.Value>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
Now this could similarly be applied to the Image control in the DataTemplate instead of the item container. So you may remove the ItemContainerStyle and write the DataTemplate like this:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Image}">
<Image.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Image.RenderTransform>
</Image>
</DataTemplate>
Try using TranslateTransform instead of Canvas.Left and Canvas.Top. The RenderTransform and TranslateTransform are efficient in scaling/moving existing drawing objects.
That's an issue I had to solve when developping a very simple Library called Mongoose.
I tried it with a 1000 images and its totally smooth (I don't have code that automatically moves images, I move them manually by drag and dropping on the Surface, but you should have the same result with code).
I wrote a quick sample you can run by using the library (you just need an attached view model with a collection of anything called PadContents) :
MainWindow.xaml
<Window x:Class="Mongoose.Sample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:mwc="clr-namespace:Mongoose.Windows.Controls;assembly=Mongoose.Windows"
Icon="Resources/MongooseLogo.png"
Title="Mongoose Sample Application" Height="1000" Width="1200">
<mwc:Surface x:Name="surface" ItemsSource="{Binding PadContents}">
<mwc:Surface.ItemContainerStyle>
<Style TargetType="mwc:Pad">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Image Source="Resources/MongooseLogo.png" Width="30" Height="30" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</mwc:Surface.ItemContainerStyle>
</mwc:Surface>
</Window>
MainWindow.xaml.cs
using System.Collections.ObjectModel;
using System.Windows;
namespace Mongoose.Sample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public ObservableCollection<object> PadContents
{
get
{
if (padContents == null)
{
padContents = new ObservableCollection<object>();
for (int i = 0; i < 500; i++)
{
padContents.Add("Pad #" + i);
}
}
return padContents;
}
}
private ObservableCollection<object> padContents;
}
}
And here is what it looks like for 1000 images :
The full code is available on Codeplex so even if you don't want to reuse the library, you can still check the code to see how achieved it.
I rely on a few tricks, but mostly the use of RenderTransform and CacheMode.
On my computer it's ok for up to 3000 images. If you want to do more, you'll probably have to think of other ways to achieve it though (maybe with some kind of virtualization)
Good luck !
EDIT:
By adding this code in the Surface.OnLoaded method :
var messageTimer = new DispatcherTimer();
messageTimer.Tick += new EventHandler(surface.messageTimer_Tick);
messageTimer.Interval = new TimeSpan(0, 0, 0, 0, 10);
messageTimer.Start();
And this method in the Surface class :
void messageTimer_Tick(object sender, EventArgs e)
{
var pads = Canvas.Children.OfType<Pad>();
if (pads != null && Layout != null)
{
foreach (var pad in pads)
{
pad.Position = new Point(pad.Position.X + random.Next(-1, 1), pad.Position.Y + random.Next(-1, 1));
}
}
}
You can see that it's totally ok to move each object separately.
Here is a samll example with 2000 objects
The issue here is the rendering/creation of so many controls.
The first question is whether you need to show all the images on the canvas. If so, I'm sorry but I can't help (if you need to draw all items then there's no way around it).
But if not all items are visible on the screen at one time - then you have hope in the shape of Virtualization. You'd need to write your own VirtualizingCanvas that inherits VirtualizingPanel and creates only the items that are visible. This will also allow you to recycle the containers which in turn will remove a lot of the load.
There's an example of a virtualizing canvas here.
Then you'd need to set the new canvas as your items panel, and set up the items to have the necessary information for the canvas to work properly.
A few thoughts that come to mind:
Freeze your bitmaps.
Hard set the size of your bitmaps when you read them to be identical to the size you're displaying them in, and set the BitmapScalingMode to LowQuality.
Track your progress while updating your entities and cut out early if you can't and grab them next frame. This will require tracking their last frame too.
// private int _lastEntity = -1;
// private long _tick = 0;
// private Stopwatch _sw = Stopwatch.StartNew();
// private const long TimeSlice = 30;
// optional: this._sw.Restart();
var end = this._sw.ElapsedMilliseconds + TimeSlice - 1;
this._tick++;
var ee = this._lastEntity++;
do {
if (ee >= this._entities.Count) ee = 0;
// entities would then track the last time
// they were "run" and recalculate their movement
// from 'tick'
action(this._entities[ee], this._tick);
if (this._sw.ElapsedMilliseconds > end) break;
} while (ee++ != this._lastEntity);
this._lastEntity = ee;

How do I bind a simple array?

I'm making a scrabble-like game in WPF C#. As of current I've got the AI Algorithm working with operates on a "Model Game Board", a variable string[15,15]. However over the past few days I'm stuck at producing a GUI to display this Array as a GameBoard.
As of current I've got the following XAML Code:
My "MainWindow" which contains:
A button:
<Button Click="Next_TurnClicked" Name="btnNext_Turn">Next Turn</Button>
A UserControl: Which is the Game Board(GameBoard is also another UserControl) and the Player's Rack
<clr:Main_Control></clr:Main_Control>
Then Inside my UserControl I have:
<DockPanel Style ="{StaticResource GradientPanel}">
<Border Style ="{StaticResource ControlBorder}" DockPanel.Dock="Bottom">
<ItemsControl VerticalAlignment="Center" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Height ="Auto" Name="ListBox" Orientation="Horizontal" HorizontalAlignment="Center">
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<clr:Rack_Cell_Sender x:Name="Player_Tile_1" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_2" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_3" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_4" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_5" ></clr:Rack_Cell_Sender>
</ItemsControl>
</Border>
<Viewbox Stretch="Uniform">
<Border Margin="5" Padding="10" Background="#77FFFFFF" BorderBrush="DimGray" BorderThickness="3">
<Border BorderThickness="0.5" BorderBrush="Black">
<clr:GameBoard>
</clr:GameBoard>
</Border>
</Border>
</Viewbox>
</DockPanel>
clr:GameBoard is a ItemsControl with its ItemsPanelTemplate as a UniformGrid
<Style TargetType="ItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True" Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Rows="15" Columns="15" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Words:Cell>
</Words:Cell>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<ItemsControl Name="BoardControl" ItemsSource="{DynamicResource CellCollectionData}">
</ItemsControl>
So my question is:
How do I get my array[,] to the ItemsControl? I have no ideas how to DataBind, I've read a couple of tutorials but I'm only starting to get what a DataContext is.
How will I refresh the Board after each turn? I don't want the board updating before btnNext_Turn is clicked.
How do I Update the ModelBoard after user inputs new word into UI and btn_Next_Turn is clicked?
I'm a beginner in coding and this is my first real project in C# and WPF. My teacher knows neither WPF or C# so you guys on the StackoverFlow community has been a great help over the past few weeks, especially helping me out on SQL.
Please, any help will be much appreciated!
UPDATE:
Thanks Erno for the quick response! Yea, I got an error for EventHandler so I swapped it out for PropertyChangedEventHandler and that stopped the errors.
public partial class GameBoard : UserControl
{
TileCollection RefreshTiles = new TileCollection();
public GameBoard()
{
InitializeComponent();
}
public void getBoard()
{
string[,] ArrayToAdd = InnerModel.ModelBoard;
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Tile AddTile = new Tile();
AddTile.Charater = ArrayToAdd[i, j];
AddTile.X = i;
AddTile.Y = j;
RefreshTiles.Add(AddTile);
}
}
}
}
So when I run debug I can see the RefreshTiles Collection being filled. However how do I bind the Collection to the ItemsControl?
Do I set the DataContext of the UserControl to RefreshTiles?
this.DataContext = RefreshTiles
then in XAML
<ItemsControl Name ="BoardControl" ItemsSource="{Binding}">
Update:
So apparently if I set the bind up in the MainWindow it works perfectly however this does not work when i try binding from a Usercontrol? I set a breakpoint in "RefreshArray" and I can see it being populated, however the UserControl does not update?
public partial class UserControl1 : UserControl
{
public char GetIteration
{
get { return MainWindow.Iteration; }
set { MainWindow.Iteration = value; }
}
CellCollection NewCells = new CellCollection();
public UserControl1()
{
InitializeComponent();
this.DataContext = NewCells;
PopulateCells();
}
private void PopulateCells()
{
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell NewCell = new Cell();
NewCell.Character = "A";
NewCell.Pos_x = i;
NewCell.Pos_y = j;
NewCells.Add(NewCell);
}
}
}
public void RefreshArray()
{
NewCells.Clear();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell ReCell = new Cell();
ReCell.Character = GetIteration.ToString();
ReCell.Pos_x = i;
ReCell.Pos_y = j;
NewCells.Add(ReCell);
}
}
this.DataContext = NewCells;
}
}
public partial class MainWindow : Window
{
UserControl1 Control = new UserControl1();
public static char Iteration = new char();
public MainWindow()
{
InitializeComponent();
}
private void Next_Click(object sender, RoutedEventArgs e)
{
Iteration = 'B';
Control.RefreshArray();
}
}
This doesn't work while the one below does work
public partial class MainWindow : Window
{
char Iteration = new char();
CellCollection NewCells = new CellCollection();
public MainWindow()
{
InitializeComponent();
PopulateCells();
this.DataContext = NewCells;
Iteration++;
}
private void PopulateCells()
{
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell NewCell = new Cell();
NewCell.Character = "A";
NewCell.Pos_x = i;
NewCell.Pos_y = j;
NewCells.Add(NewCell);
}
}
}
private void RefreshArray()
{
NewCells.Clear();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell ReCell = new Cell();
ReCell.Character = Iteration;
ReCell.Pos_x = i;
ReCell.Pos_y = j;
NewCells.Add(ReCell);
}
}
}
private void Next_Click(object sender, RoutedEventArgs e)
{
RefreshArray();
}
}
There is no short answer to this question, so I'll give an outline feel free to ask more questions to get more details where needed:
To use databinding make sure the items in the collection implement INotifyPropertyChanged. Currently you are using an array of strings. String do not implement INotifyPropertyChanged.
Also make sure the collection implements INotifyCollectionChanged. Currently you are using an two dimensional array. An array does not implement this interface.
A solution would be to create a class named Tile that implements INotifyPropertyChanged and stores the character as a string and additionally stores its position on the board in an X and Y property:
public class Tile : INotifyPropertyChanged
{
private string character;
public string Character
{
get
{
return character;
}
set
{
if(character != value)
{
character = value;
OnPropertyChanged("Character");
}
}
}
private int x; // repeat for y and Y
public int X
{
get
{
return x;
}
set
{
if(x != value)
{
x = value;
OnPropertyChanged("X");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var p = PropertyChanged;
if(p != null)
{
p(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Create a class Tiles like this:
public class Tiles : ObservableCollection<Tile>
{
}
and replace the two dimensional array with this collection.
One way of binding an items control to the collection requires you to set the DataContext of the items control to an instance of the collection and specifying the property ItemsSource="{Binding}"
Use the Character, X and Y properties in the itemtemplate to display the text and position the tile.
This way the bindings will automagically update the view when you manipulate the Tiles collection when adding or removing tiles and the board will also be updated when a tile changes (either position or content)
Emo's approach is a good one; I'd suggest a slightly different tack.
First, set your existing program aside. You'll come back to it later.
Next, implement a prototype WPF project. In this project, create a class that exposes Letter, Row, and Column properties, and another class that exposes a collection of these objects. Write a method that fills this collection with test data.
In your main window, implement an ItemsControl to present this collection. This control needs four things:
Its ItemsPanel must contain a template for the panel it's going to use to arrange the items it contains. In this case, you'll be using a Grid with rows and columns of a predefined size.
Its ItemContainerStyle must contain setters that tell the ContentPresenter objects that the template generates which row and column of the grid they belong in.
Its ItemTemplate must contain a template that tells it what controls it should put in the ContentPresenters.
Its ItemsSource must be bound to the collection of objects.
A minimal version looks like this:
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.Row" Value="{Binding Row}"/>
<Setter Property="Grid.Column" Value="{Binding Column}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate TargetType="{x:Type MyClass}">
<TextBlock Text="{Binding Letter}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Set the DataContext of the main window to a populated instance of your collection, and you should see it lay the letters out in a 4x4 grid.
How this works: By setting ItemsSource to {Binding}, you're telling the ItemsControl to get its items from the DataContext. (Controls inherit their DataContext from their parent, so setting it on the window makes it available to the ItemsControl).
When WPF renders an ItemsControl, it creates a panel using the ItemsPanelTemplate and populates it with items. To do this, it goes through the items in the ItemsSource and, for each, generates a ContentPresenter control.
It populates the Content property of that control using the template found in the ItemTemplate property.
It sets properties on the ContentPresenter using the style found in the ItemContainerStyle property. In this instance, the style sets the Grid.Row and Grid.Column attached properties, which tell the Grid where to put them when it draws them on the screen.
So the actual objects that get created look like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Row="0" Grid.Column="0">
<ContentPresenter.Content>
<TextBlock Text="A"/>
</ContentPresenter.Content>
</ContentPresenter>
<ContentPresenter Grid.Row="1" Grid.Column="1">
<ContentPresenter.Content>
<TextBlock Text="A"/>
</ContentPresenter.Content>
</ContentPresenter>
</Grid>
(No actual XAML gets created, but the above XAML is a pretty good representation of the objects that do.)
Once you have this working, you now have a bunch of relatively straightforward problems to solve:
How do you make this look more like what you want it to look like? The answer to this is going to involve making a more elaborate ItemTemplate.
How do you sync the collection that this is presenting up with the array in your application? This depends (a lot) on how your application is designed; one approach is wrapping the collection in a class, having the class create an instance of your back-end object model, and having the back-end object model raise an event every time its collection changes, so that the class containing the collection of objects that are being presented in the UI knows to create a new front-end object and add it to its collection.
How does the user select a cell in the grid to put a tile into? The answer to this is probably going to involve creating objects for all the cells, not just the ones that contain letters, implementing a command to gets executed when the user clicks on the cell, and changing the ItemTemplate so that it can executes this command when the user clicks on it.
If the contents of a cell change, how does the UI find out about it? This is going to require implementing INotifyPropertyChanged in your UI object class, and raising PropertyChanged when Letter changes.
The most important thing about the approach I'm recommending here, if you haven't noticed, is that you can actually get the UI working almost completely independently of what you're doing in the back end. The UI and the back end are coupled together only in that first class you created, the one with the Row, Column, and Letter properties.
This, by the way, is a pretty good example of the Model/View/ViewModel pattern that you've probably heard about if you're interested in WPF. The code you've written is the model. The window is the view. And that class with the Row, Column, and Letter properties is the view model.

Categories

Resources