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.
Related
I'm currently building a product configurator in C#/WPF. The Product is something like a custom outlet strip. It's made out of an aluminum profile with variable length. You can choose the outlets and place them along the aluminum profile.
We already have the data model. We have the class "profile" with properties like "length" and the class "items" with properties like "position", "type" etc.
I made a funktion to visualise the data. I have a MainCanvas where im drawing the profile as rectangle and for the witdth I'm using the property "width". The items that belong to that product are in a list. I'm drawing the items with using a for-loop to insert a wpf for each plug in the list "items".
When I'm changing the data, I have to clear the canvas and redraw. I think the next step will be binding the variables of the class to the WPF properties, for example the position of a plug or the lenth of the profile.
Later, the plugs should be select/dragable to change their position (in a grid of 5mm , for example). That's the pint where I'm stuck. I know I can select and drag the plugs with actions like "OnLeftMouseButton". The question now is: In what type of container should I put the plugs in?
I thought I could wrap the WPF-Code of a plug inside a UserControl. Is that the right approach for that? As far as I know, I can make a UserControl selectable with Hittest. The Hittestresult will be put in a list selected. For displaying the selection I could use the Borderthickness/Brush of the UserControl. For dragging, I could change the Position with a ManupilationDelta (and change the binded Position-Variable). Because the count of the plugs is variable, I have to generate the UserControls from C#-Code.
I know that Hittest is not easy to implement working with UserControls, because they are not really "visible".
I'm kind of new to C# and I have a hard time finding someone on the internet with similar problems or projects, maybe because I'm searching for the wrong words. Are my assumptions correct? What WPF controls would you use for that?
I used this answer and add few modifications.
I used an ItemControl to create the view containing your different objects. That way, you can add images just by adding object to a list. The container is a Canvas, but it can be anything since the position is controlled by RenderTransform :
<ItemsControl Name="MainView" ItemsSource="{Binding ListObjects}">
<ItemsControl.ItemsPanel >
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MVDragableObject}">
<local:DragableObject/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here is the code behind. Some basic ModelView:
public class MVObjectManager
{
public ObservableCollection<MVDragableObject> ListObjects { get; set; }
public MVObjectManager()
{
ListObjects = new ObservableCollection<MVDragableObject>();
}
}
public class MVDragableObject
{
}
And the code to fill and bind the container. You can notice that I added 3 items to the collection:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MVObjectManager Manager=new MVObjectManager();
Manager.ListObjects.Add(new MVDragableObject());
Manager.ListObjects.Add(new MVDragableObject());
Manager.ListObjects.Add(new MVDragableObject());
MainView.DataContext = Manager;
}
}
I defined a very simple UserControl. It is up to you to customize it:
<UserControl x:Class="StackFill.DragableObject"
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"
mc:Ignorable="d">
<Grid>
<Rectangle Fill="Red" Height="30" Width="30"/>
</Grid>
</UserControl>
And here is the code behind that manage the Drag and Drop behavior:
public partial class DragableObject : UserControl
{
public DragableObject()
{
InitializeComponent();
this.MouseLeftButtonDown += new MouseButtonEventHandler(DragableObject_MouseLeftButtonDown);
this.MouseLeftButtonUp += new MouseButtonEventHandler(DragableObject_MouseLeftButtonUp);
this.MouseMove += new MouseEventHandler(DragableObject_MouseMove);
}
protected bool isDragging;
private Point clickPosition;
private void DragableObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
isDragging = true;
var draggableControl = sender as UserControl;
clickPosition = e.GetPosition(this.Parent as UIElement);
draggableControl.CaptureMouse();
}
private void DragableObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
isDragging = false;
var draggable = sender as UserControl;
draggable.ReleaseMouseCapture();
}
private void DragableObject_MouseMove(object sender, MouseEventArgs e)
{
var draggableControl = sender as UserControl;
if (isDragging && draggableControl != null)
{
Point currentPosition = e.GetPosition(this.Parent as UIElement);
var transform = draggableControl.RenderTransform as TranslateTransform;
if (transform == null)
{
transform = new TranslateTransform();
draggableControl.RenderTransform = transform;
}
transform.X = snapPosition(currentPosition.X - clickPosition.X, 10);
transform.Y = snapPosition(currentPosition.Y - clickPosition.Y, 10);
}
}
private double snapPosition(double position, double gridSize)
{
return (Math.Truncate(position / gridSize) * gridSize);
}
}
You can control the snap precision by changing the gridSize argument.
I'm working on a dynamic C# WPF application (on Windows 10) that uses a fullscreen Grid. Controls are added to the grid dynamically at runtime (which are managed in a Dictionary<>) and I recently added code to move the controls along the grid with the mouse (also at runtime) using a TranslateTransform (which I am now doubting the viability of).
Is there a way I can prevent the controls from overlapping or "sharing space" on the grid when moving them? In other words, adding some sort of collision detection. Would I use an if statement to check the control margin ranges or something? My move events are shown below:
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
// Orientation variables:
public bool _isInDrag = false;
public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform();
public Point _anchorPoint;
public Point _currentPoint;
public MainWindow()
{
InitializeComponent();
}
public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
_isInDrag = false;
e.Handled = true;
}
}
public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
_isInDrag = true;
e.Handled = true;
}
public static void Control_MouseMove(object sender, MouseEventArgs e)
{
if (_isInDrag)
{
_currentPoint = e.GetPosition(null);
TranslateTransform tt = new TranslateTransform();
bool isMoved = false;
if (PointDict.ContainsKey(sender))
{
tt = PointDict[sender];
isMoved = true;
}
tt.X += _currentPoint.X - _anchorPoint.X;
tt.Y += (_currentPoint.Y - _anchorPoint.Y);
(sender as UIElement).RenderTransform = tt;
_anchorPoint = _currentPoint;
if (isMoved)
{
PointDict.Remove(sender);
}
PointDict.Add(sender, tt);
}
}
}
MainWindow.xaml (example):
<Window x:Name="MW" x:Class="MyProgram.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyProgram"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="1080" d:DesignWidth="1920" ResizeMode="NoResize" WindowState="Maximized" WindowStyle="None">
<Grid x:Name="MyGrid" />
<Image x:Name="Image1" Source="pic.png" Margin="880,862,0,0" Height="164" Width="162" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
<TextBox x:Name="Textbox1" Margin="440,560,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
</Window>
Edit: It seems that moving a control with a TranslateTransform does not change the margin for that control. Not sure why.
Edit 2: Not getting much traction. If anyone needs clarification on anything, please ask.
Edit 3: Pretty sure I can't use TranslateTransform because it does not change the margin of a given control. Is there an alternative?
Edit 4: Added some 'boilerplate' code for those who want to copy & paste. Let me know if you have any questions about it.
TL;DR: Demo from the bottom of this answer
When you want to modify your UI without adding event handlers to every single control, the way to go is with Adorners. Adorners are (as the name implies) controls that adorn another control to add additional visuals or as in your case functionality. Adorners reside in an AdornerLayer which you can either add yourself or use the one that every WPF Window already has. The AdornerLayer is on top of all your other controls.
You never mentioned what should happen when the user lets go of the mouse button when controls overlap so I just reset the control to its original position if that happens.
At this point I'd usually explain what to keep in mind when moving controls but since your original example even contains the CaptureMouse people usually forget, I think you'll understand the code without further explanation :)
A couple of things you might want to add / improve:
A snap to grid feature (pixel precise movement can be a bit overwhelming for the average user)
Take RenderTransform, LayoutTransform and non-rectangular shapes (if needed) into account when calculating the overlap
Move the editing functionality (enable, disable, etc.) into a separate control and add a dedicated AdornerLayer
Disable interactive controls (Buttons, TextBoxes, ComboBoxes, etc.) in edit-mode
Cancel movement when the user presses Esc
Restrict movement to the bounds of the parent container done
Move the active Adorner to the top of the AdornerLayer
Let the user move multiple controls at once (typically by selecting them with Ctrl)
Previously unanswered question:
Are you saying controls are no longer assigned a margin when using TranslateTransform?
Not at all - You could use a combination of Grid.Row, Grid.Column, Margin, RenderTransform and LayoutTransform but then it would be a nightmare to determine where the control is actually displayed. If you stick with one (In this case for example Margin or LayoutTransform) it is much easier to work with and keep track of. If you ever find yourself in a situation where you need more than one at the same time, you would have to find the actual position by determining the corners of the control by transforming (0, 0) and (ActualWidth, ActualHeight) with TransformToAncestor. Trust me, you don't want to go there - keep it simple, stick with one of them.
The below code is not the "holy grail of how to move things" but it should give you an idea of how to do it and what else you could do with it (resize, rotate, remove controls, etc.). The layouting is based purely on the Left and Top margin of the controls. It shouldn't be to hard to swap out all Margins for LayoutTransforms if you prefer that, as long as you keep it consistent.
Move Adorner
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
public class MoveAdorner : Adorner
{
// The parent of the adorned Control, in your case a Grid
private readonly Panel _parent;
// Same as "AdornedControl" but as a FrameworkElement
private readonly FrameworkElement _child;
// The visual overlay rectangle we can click and drag
private readonly Rectangle _rect;
// Our own collection of child elements, in this example only _rect
private readonly UIElementCollection _visualChildren;
private bool _down;
private Point _downPos;
private Thickness _downMargin;
private List<Rect> _otherRects;
protected override int VisualChildrenCount => _visualChildren.Count;
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
public MoveAdorner(FrameworkElement adornedElement) : base(adornedElement)
{
_child = adornedElement;
_parent = adornedElement.Parent as Panel;
_visualChildren = new UIElementCollection(this,this);
_rect = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
StrokeThickness = 1,
};
SetColor(Colors.LightGray);
_rect.MouseLeftButtonDown += RectOnMouseLeftButtonDown;
_rect.MouseLeftButtonUp += RectOnMouseLeftButtonUp;
_rect.MouseMove += RectOnMouseMove;
_visualChildren.Add(_rect);
}
private void SetColor(Color color)
{
_rect.Fill = new SolidColorBrush(color) {Opacity = 0.3};
_rect.Stroke = new SolidColorBrush(color) {Opacity = 0.5};
}
private void RectOnMouseMove(object sender, MouseEventArgs args)
{
if (!_down) return;
Point pos = args.GetPosition(_parent);
UpdateMargin(pos);
}
private void UpdateMargin(Point pos)
{
double deltaX = pos.X - _downPos.X;
double deltaY = pos.Y - _downPos.Y;
Thickness newThickness = new Thickness(_downMargin.Left + deltaX, _downMargin.Top + deltaY, 0, 0);
//Restrict to parent's bounds
double leftMax = _parent.ActualWidth - _child.ActualWidth;
double topMax = _parent.ActualHeight - _child.ActualHeight;
newThickness.Left = Math.Max(0, Math.Min(newThickness.Left, leftMax));
newThickness.Top = Math.Max(0, Math.Min(newThickness.Top, topMax));
_child.Margin = newThickness;
bool overlaps = CheckForOverlap();
SetColor(overlaps ? Colors.Red : Colors.Green);
}
// Check the current position for overlaps with all other controls
private bool CheckForOverlap()
{
if (_otherRects == null || _otherRects.Count == 0)
return false;
Rect thisRect = GetRect(_child);
foreach(Rect otherRect in _otherRects)
if (thisRect.IntersectsWith(otherRect))
return true;
return false;
}
private Rect GetRect(FrameworkElement element)
{
return new Rect(new Point(element.Margin.Left, element.Margin.Top), new Size(element.ActualWidth, element.ActualHeight));
}
private void RectOnMouseLeftButtonUp(object sender, MouseButtonEventArgs args)
{
if (!_down) return;
Point pos = args.GetPosition(_parent);
UpdateMargin(pos);
if (CheckForOverlap())
ResetMargin();
_down = false;
_rect.ReleaseMouseCapture();
SetColor(Colors.LightGray);
}
private void ResetMargin()
{
_child.Margin = _downMargin;
}
private void RectOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
_down = true;
_rect.CaptureMouse();
_downPos = args.GetPosition(_parent);
_downMargin = _child.Margin;
// The current position of all other elements doesn't have to be updated
// while we move this one so we only determine it once
_otherRects = new List<Rect>();
foreach (FrameworkElement child in _parent.Children)
{
if (ReferenceEquals(child, _child))
continue;
_otherRects.Add(GetRect(child));
}
}
// Whenever the adorned control is resized or moved
// Update the size of the overlay rectangle
// (Not 100% necessary as long as you only move it)
protected override Size MeasureOverride(Size constraint)
{
_rect.Measure(constraint);
return base.MeasureOverride(constraint);
}
protected override Size ArrangeOverride(Size finalSize)
{
_rect.Arrange(new Rect(new Point(0,0), finalSize));
return base.ArrangeOverride(finalSize);
}
}
Usage
private void DisableEditing(Grid theGrid)
{
// Remove all Adorners of all Controls
foreach (FrameworkElement child in theGrid.Children)
{
var layer = AdornerLayer.GetAdornerLayer(child);
var adorners = layer.GetAdorners(child);
if (adorners == null)
continue;
foreach(var adorner in adorners)
layer.Remove(adorner);
}
}
private void EnableEditing(Grid theGrid)
{
foreach (FrameworkElement child in theGrid.Children)
{
// Add a MoveAdorner for every single child
Adorner adorner = new MoveAdorner(child);
// Add the Adorner to the closest (hierarchically speaking) AdornerLayer
AdornerLayer.GetAdornerLayer(child).Add(adorner);
}
}
Demo XAML
<Grid>
<Button Content="Enable Editing" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnEnable_Click"/>
<Button Content="Disable Editing" HorizontalAlignment="Left" Margin="115,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnDisable_Click"/>
<Grid Name="grid" Background="AliceBlue" Margin="10,37,10,10">
<Button Content="Button" HorizontalAlignment="Left" Margin="83,44,0,0" VerticalAlignment="Top" Width="75"/>
<Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="207,100,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
<Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="33,134,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
</Grid>
</Grid>
Expected Result
When editing is disabled controls cannot be moved, interactive controls can be clicked / interacted with without obstruction. When editing mode is enabled, each control is overlayed with an adorner that can be moved. If the target position overlaps with another control, the adorner will turn red and the margin will be reset to the initial position if the user lets go of the mouse button.
There is no other way then to check if there control exists on place where you are moving.
Since you are moving UI elements a lot it is better to use canvas instead of grid where you can layout elements with Top and Left parameters.
Here is modified code of yours that do that
public partial class MainWindow : Window
{
public bool _isInDrag = false;
public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform>();
public Point _anchorPoint;
public Point _currentPoint;
public MainWindow()
{
InitializeComponent();
}
public void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
Panel.SetZIndex(element, 0);
_isInDrag = false;
e.Handled = true;
}
}
public void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
Panel.SetZIndex(element, 10);
_isInDrag = true;
e.Handled = true;
}
public void Control_MouseMove(object sender, MouseEventArgs e)
{
if (_isInDrag)
{
_currentPoint = e.GetPosition(null);
FrameworkElement fw = sender as FrameworkElement;
if (fw != null)
{
FrameworkElement fwParent = fw.Parent as FrameworkElement;
if (fwParent != null)
{
Point p = new Point(_currentPoint.X - _anchorPoint.X + Canvas.GetLeft((sender as UIElement)), _currentPoint.Y - _anchorPoint.Y + Canvas.GetTop((sender as UIElement)));
List<HitTestResult> lst = new List<HitTestResult>()
{
VisualTreeHelper.HitTest(fwParent , p),
VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y)),
VisualTreeHelper.HitTest(fwParent, new Point(p.X, p.Y + fw.Height)),
VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y +fw.Height)),
};
bool success = true;
foreach (var item in lst)
{
if (item != null)
{
if (item.VisualHit != sender && item.VisualHit != fwParent && fw.IsAncestorOf(item.VisualHit) == false)
{
success = false;
break;
}
}
}
if (success)
{
Canvas.SetTop((sender as UIElement), p.Y);
Canvas.SetLeft((sender as UIElement), p.X);
_anchorPoint = _currentPoint;
}
}
}
}
}
}
Xaml
<Window x:Class="ControlsOverlapWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ControlsOverlapWpf"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="500" d:DesignWidth="500" ResizeMode="NoResize" WindowState="Normal" WindowStyle="None">
<Canvas Background="Pink">
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
</Canvas>
</Window>
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;
}
}
}
I followed this link Detect when WPF listview scrollbar is at the bottom?
But ScrollBar.Scroll doesn't exist for ListView in Windows 10 .. how to achieve this requirement in windows 10
Thanks
Use the methods described here How can I detect reaching the end of a ScrollViewer item (Windows 8)?
If you want incremental loading there's a built-in solution for that. Otherwise go and traverse the visual tree for the scrollviewer object in the template.
You can do it by simply detecting VerticalOffset and ScrollableHeight of your ScrollViewer. Here is my simple code.
XAML:
<!--Apply ScrollViwer over the ListView to detect scrolling-->
<ScrollViewer Name="ContentCommentScroll" Grid.Row="2"
ViewChanged="ContentCommentScroll_ViewChanged" ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<!--My Dynamic ListView-->
<ListView Name="MyDataList" ItemsSource="{Binding MyList}"
ItemTemplate="{StaticResource MyDataTemplate}"
ItemContainerStyle="{StaticResource myStyle}"/>
</ScrollViewer>
And code in its XAML.CS:
// Hold ScrollViwer
public ScrollViewer listScrollviewer = new ScrollViewer();
// Binded event, which will trigger on scrolling of ScrollViewer
private void ContentCommentScroll_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
Scrolling(ContentCommentScroll);
}
private void Scrolling(DependencyObject depObj)
{
ScrollViewer myScroll= GetScrollViewer(depObj);
// Detecting if ScrollViewer is fully vertically scrolled or not
if (myScroll.VerticalOffset == myScroll.ScrollableHeight)
{
// ListView reached at of its end, when you are scrolling it vertically.
// Do your work here here
}
}
public static ScrollViewer GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer) return depObj as ScrollViewer;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result != null) return result;
}
return null;
}
You can also apply similar logic to detect that if ListView Horizontally reached to its end.
You can wrap it with a ScrollViewer and name it. Then you can use some other methods.
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;