Choppy movement of large collection of items on a canvas - c#

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;

Related

How to implement a delayed PointerEnter event into C# UWP project

I have been trying to implement a feature into my application where when a user hovers over a Grid item housed within a GridView for a couple seconds it will display a medium sized popup Grid with more details. I have tried a few methods I found online, but I have been running into errors that I am not understanding the root cause of, and I have the feeling there is a much simpler approach to add this feature than what I have found online so far.
First, I tried a solution I adapted by using a derivative class of Grid. I ran into some issues with that that (detailed a bit more in my last question) with the main issue being that my Timer no longer would trigger and items using my "HoverGrid" data template within the GridView template would no longer show Image as a child item of the HoverGrid. So, I abandoned that approach.
Then I tried to implement the Timer directly in my Page's code-behind, which seemed to work (partially) as it is properly triggering PointerEnter, PointerExit, and TimerElapsed events, however, when trying to manipulate anything UI related in the TimerElapsed event I would get:
System.Exception: 'The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))'
Here is the XAML:
<Page
x:Class="myproject.Pages.MyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Height="950"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid x:Name="ContentGrid">
<ListView
x:Name="AreaPanels"
Margin="0,10,0,0"
HorizontalContentAlignment="Stretch"
SelectionMode="None">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<controls:AdaptiveGridView
CanDragItems="True"
DesiredWidth="140"
ItemHeight="140">
<controls:AdaptiveGridView.ItemTemplate>
<DataTemplate x:Name="IconTextTemplate" x:DataType="Image">
<Grid
PointerEntered="ImageHoverStart"
PointerExited="ImageHoverEnd">
<Image
Opacity="1"
Source="/Assets/Placeholders/sample_image.jpg"
Stretch="UniformToFill" />
</Grid>
</DataTemplate>
</controls:AdaptiveGridView.ItemTemplate>
<Grid />
<Grid />
<Grid />
<Grid />
</controls:AdaptiveGridView>
</ListView>
</Grid>
</Page>
Code-behind (C#):
using System.Diagnostics;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
namespace myproject.Pages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MyPage : Page
{
//Variables for Grid functions
public object hoveredGridItem = null;
public PointerRoutedEventArgs hoveredGridItemArgs = null;
public System.Timers.Timer hoverTimer = new System.Timers.Timer();
public event System.Timers.ElapsedEventHandler TimerElapsed
{ add { hoverTimer.Elapsed += value; } remove { hoverTimer.Elapsed -= value; } }
public MyPage()
{
this.InitializeComponent();
hoverTimer.Enabled = true;
hoverTimer.Interval = 2000;
TimerElapsed += OnTimerElapsed;
}
private void ImageHoverStart(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("Fired: ImageHoverStart");
hoverTimer.Start();
hoveredGridItem = sender;
hoveredGridItemArgs = e;
}
private void ImageHoverEnd(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("Fired: ImageHoverEnd");
hoverTimer.Stop();
hoveredGridItem = null;
hoveredGridItemArgs = null;
}
private void OnTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
{
Debug.WriteLine("Timer elapsed!");
hoverTimer.Stop();
if (hoveredGridItem.GetType().ToString() == "Windows.UI.Xaml.Controls.Grid")
{
Debug.WriteLine(hoveredGridItem.ToString());
Debug.WriteLine(hoveredGridItemArgs.ToString());
//Get the hovered image and associated arguments that were stored
Grid itm = (Grid)hoveredGridItem;
PointerRoutedEventArgs f = hoveredGridItemArgs;
//Get image position and bounds
//GeneralTransform transform = itm.TransformToVisual(Window.Current.Content);
//Point coordinatePointToWindow = transform.TransformPoint(new Point(0, 0));
//Rect winBounds = Window.Current.Bounds;
//Testing other UI items
itm.Visibility = Visibility.Visible;
// other UI stuff ...
}
}
}
}
I tried to make references to some UI elements such as Window.Content.Current and other elements (as a workaround) but was still getting the same System.Exception. I understand this has something to do with TimerElapsed being on a different thread than the UI thread and looked around for how to fix this but was not able to fix it.
My two issues are that I was not able to fix the thread marshalling issue (some issues with running things async) but more importantly that the solution seemed a bit convoluted, maybe more difficult than it needs to be.
At first to fix the threading issue you have to use the Dispatcher of the UIElement:
await itm.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
itm.Visibility = Visibility.Visible;
});
And second, have you thought about using a tooltip for this? It should be very easy to implement:
<Grid Background="Transparent">
<Image Opacity="1" Source="/Assets/StoreLogo.png" Stretch="UniformToFill" />
<!--Tooltip implementation:-->
<ToolTipService.ToolTip>
<Image Source="/Assets/StoreLogo.png"/>
</ToolTipService.ToolTip>
</Grid>

Low Allocation Drawing in WPF

I am having some serious issues with WPF and using DrawingContext, or specifically VisualDrawingContext coming from overriding OnRender on an element or if using DrawingVisual.RenderOpen().
The problem is this allocates a lot. For example, it seems to be allocating a byte[] buffer each time a drawing context is used.
Examples, of how drawing context is used.
using (var drawingContext = m_drawingVisual.RenderOpen())
{
// Many different drawingContext.Draw calls
// E.g. DrawEllipse, DrawRectangle etc.
}
or
override void OnRender(DrawingContext drawingContext)
{
// Many different drawingContext.Draw calls
// E.g. DrawEllipse, DrawRectangle etc.
}
This causes a lot of allocations, causing some unwanted garbage collections. So yes I need this, and please stay on topic :).
What are the options for drawing in WPF with zero or low number of managed heap allocations? Reusing objects is fine, but I have yet to find a way to do this... or doesn't then have issues with DependencyProperty and allocations around/inside it.
I do know about WritableBitmapEx but was hoping for a solution that does not involve rasterising to predefined bitmap, but instead proper "vector" graphics that can still be zoomed for example.
NOTE: CPU usage is a concern but much less than the massive garbage pressure caused by this.
UPDATE: I am looking for a solution for .NET Framework 4.5+, if there is anything in later versions e.g. 4.7 that might help answer this then that is fine. But it is for the desktop .NET Framework.
UPDATE 2: A brief description of the two main scenarios. All examples have been profiled with CLRProfiler, and it shows clearly that lots of allocations occur due to this and that this is a problem for our use case. Note that this is example code intended to convey the principles not the exact code.
A: This scenario is shown below. Basically, an image is shown and some overlay graphics are drawn via a custom DrawingVisualControl, which then uses using (var drawingContext = m_drawingVisual.RenderOpen()) to get a drawing context and then draws via that. Lots of ellipse, rectangles and text is drawn. This example also shows some scaling stuff, this is just for zooming etc.
<Viewbox x:Name="ImageViewbox" VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid x:Name="ImageGrid" SnapsToDevicePixels="True" ClipToBounds="True">
<Grid.LayoutTransform>
<ScaleTransform x:Name="ImageTransform" CenterX="0" CenterY="0"
ScaleX="{Binding ElementName=ImageScaleSlider, Path=Value}"
ScaleY="{Binding ElementName=ImageScaleSlider, Path=Value}" />
</Grid.LayoutTransform>
<Image x:Name="ImageSource" RenderOptions.BitmapScalingMode="NearestNeighbor" SnapsToDevicePixels="True"
MouseMove="ImageSource_MouseMove" />
<v:DrawingVisualControl x:Name="DrawingVisualControl" Visual="{Binding DrawingVisual}"
SnapsToDevicePixels="True"
RenderOptions.BitmapScalingMode="NearestNeighbor"
IsHitTestVisible="False" />
</Grid>
</Viewbox>
The `DrawingVisualControl is defined as:
public class DrawingVisualControl : FrameworkElement
{
public DrawingVisual Visual
{
get { return GetValue(DrawingVisualProperty) as DrawingVisual; }
set { SetValue(DrawingVisualProperty, value); }
}
private void UpdateDrawingVisual(DrawingVisual visual)
{
var oldVisual = Visual;
if (oldVisual != null)
{
RemoveVisualChild(oldVisual);
RemoveLogicalChild(oldVisual);
}
AddVisualChild(visual);
AddLogicalChild(visual);
}
public static readonly DependencyProperty DrawingVisualProperty =
DependencyProperty.Register("Visual",
typeof(DrawingVisual),
typeof(DrawingVisualControl),
new FrameworkPropertyMetadata(OnDrawingVisualChanged));
private static void OnDrawingVisualChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dcv = d as DrawingVisualControl;
if (dcv == null) { return; }
var visual = e.NewValue as DrawingVisual;
if (visual == null) { return; }
dcv.UpdateDrawingVisual(visual);
}
protected override int VisualChildrenCount
{
get { return (Visual != null) ? 1 : 0; }
}
protected override Visual GetVisualChild(int index)
{
return this.Visual;
}
}
B: The second scenario involves drawing a moving "grid" of data e.g. 20 rows of 100 columns, with elements consisting of a border and text with different colors to display some status. The grid moves depending on external input, and for now is only updated 5-10 times per second. 30 fps would be better. This, thus, updates 2000 items in an ObservableCollection tied to a ListBox (with VirtualizingPanel.IsVirtualizing="True") and the ItemsPanel being a Canvas. We can't even show this during our normal use case, since it allocates so much that the GC pauses become way too long and frequent.
<ListBox x:Name="Items" Background="Black"
VirtualizingPanel.IsVirtualizing="True" SnapsToDevicePixels="True">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ElementViewModel}">
<Border Width="{Binding Width_mm}" Height="{Binding Height_mm}"
Background="{Binding BackgroundColor}"
BorderBrush="{Binding BorderColor}"
BorderThickness="3">
<TextBlock Foreground="{Binding DrawColor}" Padding="0" Margin="0"
Text="{Binding TextResult}" FontSize="{Binding FontSize_mm}"
TextAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Canvas.Left" Value="{Binding X_mm}"/>
<Setter Property="Canvas.Top" Value="{Binding Y_mm}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"
Width="{Binding CanvasWidth_mm}"
Height="{Binding CanvasHeight_mm}"
/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
There is a lot of data binding here, and the box'ing of value types do incur a lot of allocations, but that is not the main problem here. It is the allocations done by WPF.
Some Inputs
Your piece of code is not available so I can suggest only.
When its comes to performance, use profiling tools available from Microsoft.
You can find the tools here
The one more important link where you can read is WPF graphics
Note:-
Try using Drawing Group
Use WindowsFormsHost as described in Walkthrough: Hosting a Windows Forms Control in WPF by Using XAML and GDI+ for drawing instead. This isn't a perfect solution, but the - for now - best alternative I have been able to find.
<Grid>
<WindowsFormsHost x:Name="WinFormsHost>
<custom:Canvas x:Name="Canvas" />
</WindowsFormsHost>
</Grid>
And then create a custom control and override OnPaint, something like:
public partial class Canvas
: UserControl
{
// Implementing custom double buffered graphics, since this is a lot
// faster both when drawing and with respect to GC, since normal
// double buffered graphics leaks disposable objects that the GC needs to finalize
protected BufferedGraphicsContext m_bufferedGraphicsContext =
new BufferedGraphicsContext();
protected BufferedGraphics m_bufferedGraphics = null;
protected Rectangle m_currentClientRectangle = new Rectangle();
public Canvas()
{
InitializeComponent();
Setup();
}
private void Setup()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
DoubleBuffered = false;
this.Dock = DockStyle.Fill;
}
private void DisposeManagedResources()
{
m_bufferedGraphicsContext.Dispose();
if (m_bufferedGraphics != null)
{
m_bufferedGraphics.Dispose();
}
}
protected override void OnPaintBackground(PaintEventArgs e)
{
// Background paint is done in OnPaint
// This reduces the "leaks" of System.Windows.Forms.Internal.DeviceContext
// and the amount of "GC" handles created considerably
// as found by using CLR Profiler
}
protected override void OnPaint(PaintEventArgs e)
{
// Specifically not calling base here since we draw entire area ourselves
// base.OnPaint(e);
// Should this be disposed?
using (e)
using (var targetGraphics = e.Graphics)
{
ReallocBufferedGraphics(targetGraphics);
// Use buffered graphics object
var graphics = m_bufferedGraphics.Graphics;
// Raise paint event
PaintEvent?.Invoke(this.ClientRectangle, e.ClipRectangle, graphics);
// Render to target graphics i.e. paint event args graphics
m_bufferedGraphics.Render(targetGraphics);
}
}
protected virtual void ReallocBufferedGraphics(Graphics graphics)
{
Rectangle newClientRectangle = this.ClientRectangle;
// Realloc if new client rectangle is not contained within the current
// or if no buffered graphics exists
bool reallocBufferedGraphics = ShouldBufferBeReallocated(newClientRectangle);
if (reallocBufferedGraphics)
{
if (m_bufferedGraphics != null)
{
m_bufferedGraphics.Dispose();
}
m_bufferedGraphics = m_bufferedGraphicsContext.Allocate(
graphics, newClientRectangle);
m_currentClientRectangle = newClientRectangle;
}
}
protected virtual bool ShouldBufferBeReallocated(Rectangle newClientRectangle)
{
return !m_currentClientRectangle.Contains(newClientRectangle) ||
m_bufferedGraphics == null;
}
/// <summary>
/// PaintEvent with <c>clientRectangle, clipRectangle, graphics</c> for the canvas.
/// </summary>
public event Action<Rectangle, Rectangle, Graphics> PaintEvent;
}
UPDATE: Updated Canvas control to truly be zero heap allocations.
The WinForms Canvas solution has some issues, especially the so-called "airspace" issues due to how WindowsFormsHost interacts with WPF. To keep it short, this means that no WPF visuals can be drawn on top of the host.
This can be solved by recognizing that since we have to double buffer anyway, we might as well buffer into a WriteableBitmap that can then be drawn as usual via an Image control.
This can be fascillitated by using a utility class like the below:
using System;
using System.Drawing;
using System.Windows;
using SWM = System.Windows.Media;
using SWMI = System.Windows.Media.Imaging;
public class GdiGraphicsWriteableBitmap
{
readonly Action<Rectangle, Graphics> m_draw;
SWMI.WriteableBitmap m_wpfBitmap = null;
Bitmap m_gdiBitmap = null;
public GdiGraphicsWriteableBitmap(Action<Rectangle, Graphics> draw)
{
if (draw == null) { throw new ArgumentNullException(nameof(draw)); }
m_draw = draw;
}
public SWMI.WriteableBitmap WriteableBitmap => m_wpfBitmap;
public bool IfNewSizeResizeAndDraw(int width, int height)
{
if (m_wpfBitmap == null ||
m_wpfBitmap.PixelHeight != height ||
m_wpfBitmap.PixelWidth != width)
{
Reset();
// Can't dispose wpf
const double Dpi = 96;
m_wpfBitmap = new SWMI.WriteableBitmap(width, height, Dpi, Dpi,
SWM.PixelFormats.Bgr24, null);
var ptr = m_wpfBitmap.BackBuffer;
m_gdiBitmap = new Bitmap(width, height, m_wpfBitmap.BackBufferStride,
System.Drawing.Imaging.PixelFormat.Format24bppRgb, ptr);
Draw();
return true;
}
return false;
}
public void Draw()
{
if (m_wpfBitmap != null)
{
m_wpfBitmap.Lock();
int width = m_wpfBitmap.PixelWidth;
int height = m_wpfBitmap.PixelHeight;
{
using (var g = Graphics.FromImage(m_gdiBitmap))
{
m_draw(new Rectangle(0, 0, width, height), g);
}
}
m_wpfBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
m_wpfBitmap.Unlock();
}
}
// If window containing this is not shown, one can Reset to stop draw or similar...
public void Reset()
{
m_gdiBitmap?.Dispose();
m_wpfBitmap = null;
}
}
And then binding the ImageSource to an Image in XAML:
<Grid x:Name="ImageContainer" SnapsToDevicePixels="True">
<Image x:Name="ImageSource"
RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True">
</Image>
</Grid>
And the handling resize on the Grid to make the WriteableBitmap match in size e.g.:
public partial class SomeView : UserControl
{
ISizeChangedViewModel m_viewModel = null;
public SomeView()
{
InitializeComponent();
this.DataContextChanged += OnDataContextChanged;
}
void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (m_viewModel != null)
{
this.ImageContainer.SizeChanged -= ImageSource_SizeChanged;
}
m_viewModel = e.NewValue as ISizeChangedViewModel;
if (m_viewModel != null)
{
this.ImageContainer.SizeChanged += ImageSource_SizeChanged;
}
}
private void ImageSource_SizeChanged(object sender, SizeChangedEventArgs e)
{
var newSize = e.NewSize;
var width = (int)Math.Round(newSize.Width);
var height = (int)Math.Round(newSize.Height);
m_viewModel?.SizeChanged(width, height);
}
}
This way you can use WinForms/GDI+ for drawing with zero heap allocations and even WriteableBitmapEx if you prefer. Note that you then get great DrawString support with GDI+ incl. MeasureString.
The drawback is this is rasterized and sometimes can have some interpolation issues. So be sure to also set UseLayoutRounding="True" on the parent window/user control.

How to implement a WrapPanel with a header and footer

I am trying to implement a custom control that acts similarly to a standard WrapPanel, but that allows you to specify a header and footer. Visually, this is what I am trying to accomplish:
I have created a custom control that seems to leave room for the header and footer items, but I am unable to get them to visually appear. This is my first attempt at any sort of custom control, so any help or input is appreciated!
C#
using System;
using System.Windows;
using System.Windows.Controls;
namespace MyProject.Extras
{
public class HeaderedFooteredPanel : Panel
{
public FrameworkElement Header
{
get { return (FrameworkElement) GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public FrameworkElement Footer
{
get { return (FrameworkElement)GetValue(FooterProperty); }
set { SetValue(FooterProperty, value); }
}
public static DependencyProperty HeaderProperty = DependencyProperty.Register(
nameof(Header),
typeof(FrameworkElement),
typeof(HeaderedFooteredPanel),
new PropertyMetadata((object)null));
public static DependencyProperty FooterProperty = DependencyProperty.Register(
nameof(Footer),
typeof(FrameworkElement),
typeof(HeaderedFooteredPanel),
new PropertyMetadata((object)null));
protected override Size MeasureOverride(Size constraint)
{
double x = 0.0;
double y = 0.0;
double largestY = 0.0;
double largestX = 0.0;
var measure = new Action<FrameworkElement>(element =>
{
element.Measure(constraint);
if (x > 0 && // Not the first item on this row
(x + element.DesiredSize.Width > constraint.Width) && // We are too wide to fit on this row
((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
{
y = largestY;
x = element.DesiredSize.Width;
}
else
{
/* 1) Always place the first item on a row even if width doesn't allow it
* otherwise:
* 2) Keep placing on this row until we reach our width constraint
* otherwise:
* 3) Keep placing on this row if the max height is reached */
x += element.DesiredSize.Width;
}
largestY = Math.Max(largestY, y + element.DesiredSize.Height);
largestX = Math.Max(largestX, x);
});
measure(Header);
foreach (FrameworkElement child in InternalChildren)
{
measure(child);
}
measure(Footer);
return new Size(largestX, largestY);
}
protected override Size ArrangeOverride(Size finalSize)
{
double x = 0.0;
double y = 0.0;
double largestY = 0.0;
double largestX = 0.0;
var arrange = new Action<FrameworkElement>(element =>
{
if (x > 0 && // Not the first item on this row
(x + element.DesiredSize.Width > finalSize.Width) && // We are too wide to fit on this row
((largestY + element.DesiredSize.Height) <= MaxHeight)) // We have enough room for this on the next row
{
y = largestY;
element.Arrange(new Rect(new Point(0.0, y), element.DesiredSize));
x = element.DesiredSize.Width;
}
else
{
/* 1) Always place the first item on a row even if width doesn't allow it
* otherwise:
* 2) Keep placing on this row until we reach our width constraint
* otherwise:
* 3) Keep placing on this row if the max height is reached */
element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
x += element.DesiredSize.Width;
}
largestY = Math.Max(largestY, y + element.DesiredSize.Height);
largestX = Math.Max(largestX, x);
});
arrange(Header);
foreach (FrameworkElement child in InternalChildren)
{
arrange(child);
}
arrange(Footer);
return new Size(largestX, largestY);
}
}
}
Usage in XAML:
<ItemsControl ItemsSource="{Binding SomeItems}" ItemTemplate="{StaticResource SomeTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<extras:HeaderedFooteredPanel>
<extras:HeaderedFooteredPanel.Header>
<TextBlock Text="Header" />
</extras:HeaderedFooteredPanel.Header>
<extras:HeaderedFooteredPanel.Footer>
<TextBlock Text="Footer" />
</extras:HeaderedFooteredPanel.Footer>
</extras:HeaderedFooteredPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
You write in the comments:
The DrawingContext supplied to the OnRender() method only seems to support very basic rendering commands. Surely you don't have to re-write the rendering code for a standard WPF control, but I am not seeing a way to draw them on my own
If by "basic" you mean you are restricted to only DrawingContext operations, then yes. That's exactly what it's for. That's actually the drawing API for WPF. At a higher level, you are dealing with visuals and framework elements, which hide the actual drawing activity. But to override the way such objects are drawn, will require diving down into that level of drawing, replacing it or supplementing it as necessary.
One significant difficulty that would probably arise (besides the more fundamental difficulty of dealing with drawing at that level) is that at that level, there is no such thing as data templates, and no way to access the rendering behaviors of other elements. You have to draw everything from scratch. This would wind up negating a big part of what makes WPF so useful: convenient and powerful control over the exact on-screen representation of data through the use of built-in controls and the properties that give you control over their appearance.
I have only rarely found that a custom Control sub-class is really needed. The only time this comes up is when you need to have complete control over the entire rendering process, to draw something that is simply not possible any other way, or to provide the required performance (at the expense of convenience). Much more often, nearly all of the time even, what you want to do is leverage the existing controls and get them to do all the heavy lifting for you.
In this particular case, I think the key to solving your problem is a type called CompositeCollection. Just like it sounds, it allows you build up a collection as a composite of other objects, including other collections. With this, you can combine your header and footer data into a single collection that can be displayed by an ItemsControl.
In some cases, just creating that collection and using it directly with an ItemsControl object might be sufficient for your needs. But if you want a whole, reusable user-defined control that understands the idea of a header and footer, you can wrap the ItemsControl in a UserControl object that exposes the properties you need, including a Header and Footer property. Here is an example of what that might look like:
XAML:
<UserControl x:Class="TestSO43008469HeaderFooterWrapPanel.HeaderFooterWrapPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestSO43008469HeaderFooterWrapPanel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl x:Name="wrapPanel1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
C#:
public partial class HeaderFooterWrapPanel : UserControl
{
private const int _kheaderIndex = 0;
private const int _kfooterIndex = 2;
private readonly CompositeCollection _composedCollection = new CompositeCollection();
private readonly CollectionContainer _container = new CollectionContainer();
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header", typeof(string), typeof(HeaderFooterWrapPanel),
new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kheaderIndex)));
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(
"Footer", typeof(string), typeof(HeaderFooterWrapPanel),
new PropertyMetadata((o, e) => _OnHeaderFooterPropertyChanged(o, e, _kfooterIndex)));
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource", typeof(IEnumerable), typeof(HeaderFooterWrapPanel),
new PropertyMetadata(_OnItemsSourceChanged));
private static void _OnHeaderFooterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, int index)
{
HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;
panel._composedCollection[index] = e.NewValue;
}
private static void _OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HeaderFooterWrapPanel panel = (HeaderFooterWrapPanel)d;
panel._container.Collection = panel.ItemsSource;
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public string Footer
{
get { return (string)GetValue(FooterProperty); }
set { SetValue(FooterProperty, value); }
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public HeaderFooterWrapPanel()
{
InitializeComponent();
_container.Collection = ItemsSource;
_composedCollection.Add(Header);
_composedCollection.Add(_container);
_composedCollection.Add(Footer);
wrapPanel1.ItemsSource = _composedCollection;
}
}
Noting, of course, that doing it that way, you would need to "forward" all of the various control properties that you want to be able to set, from the UserControl object to the ItemsPanel. Some, like Background, you can probably just set on the UserControl and have the desired effect, but others are specifically applicable to the ItemsControl, like ItemTemplate, ItemTemplateSelector, etc. You'll have to figure out which those are, and bind the properties, with the source being the UserControl and the target the ItemsControl inside, declaring as dependency properties in your UserControl class any that aren't already part of the UserControl type.
Here's a little sample program that shows how the above could be used:
XAML:
<Window x:Class="TestSO43008469HeaderFooterWrapPanel.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:TestSO43008469HeaderFooterWrapPanel"
xmlns:s="clr-namespace:System;assembly=mscorlib"
DataContext="{Binding RelativeSource={x:Static RelativeSource.Self}}"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<TextBlock Text="Header: "/>
<TextBox Text="{Binding Header, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<TextBlock Text="Footer: "/>
<TextBox Text="{Binding Footer, ElementName=headerFooterWrapPanel1, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<Button Content="Random List Change" Click="Button_Click" HorizontalAlignment="Left" Grid.Row="2"/>
<l:HeaderFooterWrapPanel x:Name="headerFooterWrapPanel1" ItemsSource="{Binding Items}"
Header="Header Item" Footer="Footer Item" Grid.Row="3">
<l:HeaderFooterWrapPanel.Resources>
<DataTemplate DataType="{x:Type s:String}">
<Border BorderBrush="Black" BorderThickness="1">
<TextBlock Text="{Binding}" FontSize="16"/>
</Border>
</DataTemplate>
</l:HeaderFooterWrapPanel.Resources>
</l:HeaderFooterWrapPanel>
</Grid>
</Window>
For the purpose of illustration, I've set the Window.DataContext property to the Window object itself. This isn't normally a good idea — it's better to have a proper view model to use as the data context — but for a simple program like this, it's fine. Similarly, the Header and Footer properties would normally be bound to some view model property instead of just tying one framework element's property to another.
C#:
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
public MainWindow()
{
InitializeComponent();
Items.Add("Item #1");
Items.Add("Item #2");
Items.Add("Item #3");
}
private static readonly Random _random = new Random();
private void Button_Click(object sender, RoutedEventArgs e)
{
switch (Items.Count > 0 ? _random.Next(2) : 0)
{
case 0: // add
Items.Insert(_random.Next(Items.Count + 1), $"Item #{_random.Next()}");
break;
case 1: // remove
Items.RemoveAt(_random.Next(Items.Count));
break;
}
}
}

Creating a endless centered carousel control in wpf

I'm trying to create a endless, centered carousel in WPF like in this concept image. The current solution I've come up with is using a listbox, loading all images into a ObservableCollection and then modifying it to create the illusion of movement.
I have two issues with this solution. First I can't seem to center it. The listbox is aligned to the left with no way of getting it to overflow on both sides. Regardless of the size of my window it should always show one console in the middle, one on each sides and a half one to indicate that there's more to choose from.
The second issue is not as important, but I'm looking for a proper way of doing this that may allow a more fluent transition between selections later on.
This is my current code:
XAML:
<Window x:Class="SystemMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<Button Content="left" Height="20" Click="Left_Click" DockPanel.Dock="Top" />
<Button Content="right" Height="20" Click="Right_Click" DockPanel.Dock="Top" />
<ListBox x:Name="LoopPanel" ItemsSource="{Binding Path=SampleData}" SelectedIndex="3" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.CanContentScroll="False">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
ObservableCollection<string> sampleData = new ObservableCollection<string>();
public ObservableCollection<string> SampleData
{
get
{
if (sampleData.Count <= 0)
{
sampleData.Add(#"Nintendo 64.png");
sampleData.Add(#"Nintendo Famicom.png");
sampleData.Add(#"Super Nintendo Entertainment System.png");
sampleData.Add(#"Nintendo Entertainment System.png");
sampleData.Add(#"Sony PlayStation.png");
}
return sampleData;
}
}
private void Right_Click(object sender, RoutedEventArgs e)
{
var firstItem = SampleData.First();
SampleData.Remove(firstItem);
SampleData.Insert(SampleData.Count, firstItem);
}
private void Left_Click(object sender, RoutedEventArgs e)
{
var lastItem = SampleData.Last();
SampleData.Remove(lastItem);
SampleData.Insert(0, lastItem);
}
}
Edit:
I found the following extension solving the issue I had with centering the listbox. Calling LoopPanel.ScrollToCenterOfView(sampleData[2]); seems to do the trick of centering the images... Any idea now on how to animate the transition? :)
public static class ItemsControlExtensions
{
public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Scroll immediately if possible
if (!itemsControl.TryScrollToCenterOfView(item))
{
// Otherwise wait until everything is loaded, then scroll
if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
itemsControl.TryScrollToCenterOfView(item);
}));
}
}
private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Find the container
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
if (container == null) return false;
// Find the ScrollContentPresenter
ScrollContentPresenter presenter = null;
for (Visual vis = container; vis != null && vis != itemsControl; vis = VisualTreeHelper.GetParent(vis) as Visual)
if ((presenter = vis as ScrollContentPresenter) != null)
break;
if (presenter == null) return false;
// Find the IScrollInfo
var scrollInfo =
!presenter.CanContentScroll ? presenter :
presenter.Content as IScrollInfo ??
FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
presenter;
// Compute the center point of the container relative to the scrollInfo
Size size = container.RenderSize;
Point center = container.TransformToAncestor((Visual)scrollInfo).Transform(new Point(size.Width / 2, size.Height / 2));
center.Y += scrollInfo.VerticalOffset;
center.X += scrollInfo.HorizontalOffset;
// Adjust for logical scrolling
if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
{
double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
if (orientation == Orientation.Horizontal)
center.X = logicalCenter;
else
center.Y = logicalCenter;
}
// Scroll the center of the container to the center of the viewport
if (scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
if (scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
return true;
}
private static double CenteringOffset(double center, double viewport, double extent)
{
return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
}
private static DependencyObject FirstVisualChild(Visual visual)
{
if (visual == null) return null;
if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
return VisualTreeHelper.GetChild(visual, 0);
}
}
I don't think I would do it how you're doing it. I.e. adding and removing items in a ListBox. Doesn't give you enough control on the positioning and you won't be able to do smooth animations of it rotating which with that kind of UI, I think that would be kind of expected :).
I'd probably have a Canvas instead with ClipToBounds=true. Then just calculate the positions, you aren't doing a rounded carousel, so positions are trivial and there is no zooming.
Lets say your images are all 100 x 100. So item0 will be # -50,0, item1 # 50,0 (well, technically probably 75,0 or whatever because you'd want some spacing between them, but you get the idea), etc. Because you are calculating the positions and have them absolute against the Canvas, the ClipToBound=true will clip the two on either end and you'll be able to animate the rotation.

How to make Scaling Images have clickable regions in WPF

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.

Categories

Resources