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.
Related
I have this assignment where I must write an interactive program where the user clicks the screen and puts a dot at the spot of his mouse click and then when he puts a second dot they must connect with a line.
<Window x:Class="courseWorkOOP.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:courseWorkOOP"
mc:Ignorable="d"
Title="Практикум ООП" Height="600" Width="800">
<Grid x:Name="myGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Canvas Name="myCanvas" Height="480" Width="640" MouseDown="Canvas_MouseDown_1" MouseMove="Canvas_MouseMove_1">
<Canvas.Background>
<SolidColorBrush Color="White" Opacity="0"/>
</Canvas.Background>
</Canvas>
<Button Click="btn_Click" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="200" Content="Clear"/>
<!--<StackPanel HorizontalAlignment="Left">
<Button Name="btn" Click="btn_Click">Clear</Button>
</StackPanel>-->
</Grid>
This is my XAML
private void Canvas_MouseDown_1(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
currentPoint = e.GetPosition(this);
Ellipse currDot = new Ellipse() { Width = 10, Height = 10,Fill=Brushes.Black };
myCanvas.Children.Add(currDot);
Canvas.SetLeft(currDot, e.GetPosition(this).X);
Canvas.SetTop(currDot, e.GetPosition(this).Y);
double coordinateX = Canvas.GetLeft(currDot);
double coordinateY = Canvas.GetTop(currDot);
Line myLine = new Line() { X1 = coordinateX, Y1 = coordinateY,X2=coordinateY,Y2=coordinateX,Stroke=Brushes.Green,StrokeThickness=4 };
myCanvas.Children.Add(myLine);
}
}
private void btn_Click(object sender, RoutedEventArgs e)
{
myCanvas.Children.Clear();
}
private void Canvas_MouseMove_1(object sender, System.Windows.Input.MouseEventArgs e)
{
//if (e.LeftButton == MouseButtonState.Pressed)
//{
// Line line = new Line();
// line.Stroke = SystemColors.WindowFrameBrush;
// line.StrokeThickness = 20;
// line.X1 = currentPoint.X;
// line.Y1 = currentPoint.Y;
// line.X2 = e.GetPosition(this).X;
// line.Y2 = e.GetPosition(this).Y;
// currentPoint = e.GetPosition(this);
// myCanvas.Children.Add(line);
//}
}
And this is the C# part.
My question is how do I get two dots to connect with a line?
I've tried ClickCount but it did nothing, I might've used it incorrectly.
Before that I've tried initializing an integer value inside Canvas_MouseDown_1 and then made an If statement that basically said draw a line between this and that every two clicks but that didn't work either.
In order to connect the dots with a line, you need to keep track of the previous click point, so that you can connect the "currentPoint" to the "previousPoint". You also need a flag to only create the line once you have at least one point on the canvas.
private Point previousPoint;
private Point currentPoint;
private bool hasPoints;
private void Canvas_MouseDown_1(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
previousPoint = currentPoint;
currentPoint = e.GetPosition(this.myCanvas);
currentPoint.X -= 5; // Use 5, which is half the width/height of the dot
currentPoint.Y -= 5; // Use 5, which is half the width/height of the dot
Ellipse currDot = new Ellipse() { Width = 10, Height = 10, Fill = Brushes.Black };
myCanvas.Children.Add(currDot);
Canvas.SetLeft(currDot, currentPoint.X);
Canvas.SetTop(currDot, currentPoint.Y);
if (hasPoints)
{
// Add 4 to the line position, due to the stroke thickness being 4
Line myLine = new Line() { X1 = previousPoint.X + 4, Y1 = previousPoint.Y + 4, X2 = currentPoint.X + 4, Y2 = currentPoint.Y + 4, Stroke = Brushes.Green, StrokeThickness = 4 };
myCanvas.Children.Add(myLine);
}
hasPoints = true;
}
}
Example output:
EDITED AFTER COMMENT BELOW: If you want every two dots connecting, just make this simple logical change:
if (hasPoints)
{
// Add 4 to the line position...
Line myLine = new Line() { X1 ...
myCanvas.Children.Add(myLine);
hasPoints = false;
}
else
{
hasPoints = true;
}
Output from above code:
Your approach is fundamentally wrong. Before writing a WPF program you should study the documentation, and especially the data binding section. Unfortunately, the Microsoft-provided documentation isn't super great, so you should also read what you can find here and elsewhere on the web about the primary design pattern used for WPF programs, MVVM.
If you follow the MVVM pattern, your program will be simpler to write and simpler to understand, because you won't be struggling to figure out all your UI interactions at the same time that you're also struggling to figure out all the underlying data. MVVM separates these concerns ("separation of concerns" being one of the most important software practices in any context). Here is an example of what that might look like for your example…
First, you know you will have two kinds of graphical objects, so create models for those:
class CanvasItemViewModel
{
public Point Location { get; }
public CanvasItemViewModel(Point location)
{
Location = location;
}
}
class PointViewModel : CanvasItemViewModel
{
public PointViewModel(Point location) : base(location) { }
}
class LineViewModel : CanvasItemViewModel
{
public double X2 { get; }
public double Y2 { get; }
public LineViewModel(PointViewModel start, PointViewModel end) : base(start.Location)
{
X2 = end.Location.X - start.Location.X;
Y2 = end.Location.Y - start.Location.Y;
}
}
In this case, I know ahead of time that I'm going to want to be able to treat points and lines the same, when it comes to positioning them in the canvas, so I use a common base class to represent where on the canvas they will be.
Since the line will be positioned via its Location property, I can leave the X1 and Y1 properties out, and just set the X2 and Y2, based on the points the line will connect.
With these elements taken care of, now I need a way to manage the points and lines. That looks like this:
class MainViewModel
{
public CompositeCollection CanvasItems { get; } = new CompositeCollection();
public MainViewModel()
{
CanvasItems.Add(new CollectionContainer { Collection = _lines });
CanvasItems.Add(new CollectionContainer { Collection = _points });
}
private readonly ObservableCollection<PointViewModel> _points = new ObservableCollection<PointViewModel>();
private readonly ObservableCollection<LineViewModel> _lines = new ObservableCollection<LineViewModel>();
public void AddPoint(Point point)
{
PointViewModel pointModel = new PointViewModel(point);
_points.Add(pointModel);
if (_points.Count > 1)
{
_lines.Add(new LineViewModel(_points[_points.Count - 2], pointModel));
}
}
public void Clear()
{
_points.Clear();
_lines.Clear();
}
}
Here again, I know that I want to display the points and lines in the same control, so the collections are combined into a single CompositeCollection for the benefit of the canvas that will be displaying them. I maintain two separate collections though, to make it easier to manage the collections in the model code.
The line collection is included first in the composite collection, so that the points will draw on top of the lines. Of course, if you want to see the entire line on top of the points, you would simply swap the order of the two collections in the composite collection.
Observable collections are used because the collection contents will be changing based on user input, and this allows WPF to be notified and respond as needed automatically, without additional work on your part. I.e. this is a fundamental aspect of data binding (along with implementing INotifyPropertyChanged, something that's not actually necessary in this example, but which is used heavily in a typical WPF program).
The model code itself does nothing but add points and lines to the collection when necessary, and provide a means to clear both collections.
Note that up to this point, there's nothing that is really dependent on the UI. The classes do use the WPF Point and CompositeCollection types, but this is mainly out of convenience. The implementation isn't really inherently tied to WPF, and those could be abstracted out relatively easily.
With all the basic data structures defined, now is the time to shift the focus to the UI, starting with the XAML:
<Window x:Class="TestSO66159694ClickPointsAndLines.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO66159694ClickPointsAndLines"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.Resources>
<!-- The templates will rely on the ItemsControl to correctly position them -->
<DataTemplate DataType="{x:Type l:PointViewModel}">
<Ellipse Width="10" Height="10" Fill="Black">
<Ellipse.RenderTransform>
<!-- Center the ellipse on its actual location -->
<TranslateTransform X="-5" Y="-5"/>
</Ellipse.RenderTransform>
</Ellipse>
</DataTemplate>
<DataTemplate DataType="{x:Type l:LineViewModel}">
<Line Stroke="Green" StrokeThickness="4"
X2="{Binding X2}" Y2="{Binding Y2}"/>
</DataTemplate>
</Grid.Resources>
<ItemsControl ItemsSource="{Binding CanvasItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" MouseDown="Canvas_MouseDown_1">
<Canvas.Background>
<SolidColorBrush Color="White" Opacity="0"/>
</Canvas.Background>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding Location.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Location.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<Button Click="btn_Click" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="200" Content="Clear"/>
</Grid>
</Window>
This XAML has two main sections to it: the templates corresponding to the graphical elements, which give the actual visual representation for these; and the actual content of the window, which handles binding the collection and setting the event handlers for the user interface (i.e. mouse-down and button-click).
WPF will automatically find the template appropriate for each graphical element. The actual positioning of the element within the canvas is provided by the ItemContainerStyle; this is because an ItemsControl will wrap your actual content in a presenter which is the actual direct child of the canvas, for which the Canvas.Left and Canvas.Top properties apply.
The canvas itself is provided as the ItemsPanelTemplate for the ItemsControl object. The default for ItemsControl is StackPanel, but you can provide any panel template you want to customize the behavior.
Finally, there is the actual user input. With all of the ground-work done above, this is really simple:
public partial class MainWindow : Window
{
private readonly MainViewModel _mainViewModel = new MainViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = _mainViewModel;
}
private void Canvas_MouseDown_1(object sender, MouseButtonEventArgs e)
{
IInputElement canvas = sender as IInputElement;
Point canvasLocation = e.GetPosition(canvas);
_mainViewModel.AddPoint(canvasLocation);
}
private void btn_Click(object sender, RoutedEventArgs e)
{
_mainViewModel.Clear();
}
}
Since the main view model object has all the real data management logic, all the view itself (i.e. the window) needs to do is get the mouse position and pass that along to the main view model to deal with. Likewise, clearing the list is delegated to the main view model as well.
The view itself knows nothing about the underlying data structures, nor should it. The view's job is to provide the layer in your problem that interacts directly with the user, presenting to the user the underlying data in a form that is useful and comprehensible to the user, and taking user input and passing that along to the underlying data structures so that it can accomplish whatever it needs to do.
In addition to simplifying the overall design of the program, and making it easier to think about each discrete function of the program separately, doing it this way makes it trivial to adjust the way the visuals are presented to the user, without touching the C# code at all. One just needs to update the templates according to whatever visual aspect is desired.
And of course, when doing things correctly, as above, it's also simple to update the underlying data logic without having to meddle with the view. For example, it turns out your original question was not clear enough and you only want to connect every pair of dots the user clicks, but not make a continuous line. That's an easy enough change, simply by modifying the AddPoint() method by adding a single additional condition to the if statement that adds the line:
public void AddPoint(Point point)
{
PointViewModel pointModel = new PointViewModel(point);
_points.Add(pointModel);
if (_points.Count > 1 && _points.Count % 2 == 0)
{
_lines.Add(new LineViewModel(_points[_points.Count - 2], pointModel));
}
}
I.e. instead of just checking that there are two or more points with _points.Count > 1, also limit adding a line to only when a new pair of points has been added, by including _points.Count % 2 == 0. No need for new variables or anything like that. Just take into account the current state of things and act on that.
Note that one of the reasons this change is so easy is that the code above doesn't abuse the user interface API to store the state of your underlying data, and so you have immediate access to the number of points that have been added. This approach would be significantly more challenging if you had to figure out from the state of the UI how many points the user had already added (hence the different approach taken by the other answer, which is to add even more state to the UI to try to keep track of what the user's doing).
Again, good WPF programs always follow the principle of Separation of Concerns. Indeed, any good program does, but with WPF the framework actually makes it much easier to do so, and rewards you when you do. I encourage you to keep that in mind as you continue to learn about programming.
In MainWindow.xaml.cs I switch to a userControl using .Child like so:
Grid1.Visibility = Visibility.Hidden;
Stage.Child = new UserControlName();
Where Grid1 is the grid where the content sits in the main window, (which should be hidden because there's some content that will peek out from behind the usercontrol), and Stage is a border element where I want the UserControl to fill
<Grid >
<Border x:Name="Stage" Grid.RowSpan="4" Grid.ColumnSpan="2"/>
<Grid x:Name="Grid1" FocusManager.FocusedElement="{Binding ElementName=textBox}" Margin="10,10,10,10" Width="1200" Height="649" >
How can UserControlName be closed or hidden from its own xaml.cs file and the view be returned to the main window while simultaneously returning MainWindow's Grid1 to Visible?
I would implement that using Regions from PRISM (https://msdn.microsoft.com/en-us/library/ff921098(v=pandp.40).aspx). But if you created simple application and you don't want to invest time to learn PRISM you can for example extend constructor of UserControlName class by adding parameter of MainWindow type and invoke on this object some method that would change the view to your desire state.
Example of uglly code:
MainWindow:
private void Button_Click(object sender, RoutedEventArgs e)
{
grid1.Visibility = Visibility.Hidden;
br.Child = new UserControl2(this);
}
public void CloseView()
{
grid1.Visibility = Visibility.Visible;
br.Child = null;
}
UserControl:
private MainWindow window;
public UserControl2(MainWindow window)
{
this.window = window;
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
window.CloseView();
}
Another option is to use (for example) the Mvvm Light toolkit and messaging (https://msdn.microsoft.com/en-us/magazine/jj694937.aspx).
The child view would send a message to get closed, the main window would receive this message and hide the grid.
The advantage is that the main window and the child control are still uncoupled meaning that the child does not need to know anything about the main window.
Take this chance and invest some time in the Mvvm pattern and libraries such as Mvvm light.
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.
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;
I need to insert UIElements into a Grid that does not get generated until runtime. More specifically, I need to add UIElements to the RowDefinitions I create after I determine how many elements need to be displayed. Is there a way to contorl the Grid.Row and Grid.Column and Grid.RowSpan like in XAML for objects in C#? If I am going about this wrong, please let me know. I can not use a StackPanel (I am creating an dynamic accordian panel and it messes with the animation).
Right now what happens is that I generate the number of RowDefinitions at runtime and add UIElements as the children. This isn't working, all the UIElements end up in the first row layered on top of each other.
Here is an example of what I am trying:
public partial class Page : UserControl
{
string[] _names = new string[] { "one", "two", "three" };
public Page()
{
InitializeComponent();
BuildGrid();
}
public void BuildGrid()
{
LayoutRoot.ShowGridLines = true;
foreach (string s in _names)
{
LayoutRoot.RowDefinitions.Add(new RowDefinition());
LayoutRoot.Children.Add(new Button());
}
}
}
Thanks!
Apart from the Grid not really being the right tool for the job (a ListView would be) you need to tell the Button which row it belongs to.
public void BuildGrid()
{
LayoutRoot.ShowGridLines = true;
int rowIndex = 0;
foreach (string s in _names)
{
LayoutRoot.RowDefinitions.Add(new RowDefinition());
var btn = new Button()
LayoutRoot.Children.Add(btn);
Grid.SetRow(btn, rowIndex);
rowIndex += 1;
}
}
The best way to do what you're looking for is to follow the pattern below:
Page.xaml:
<UserControl x:Class="SilverlightApplication1.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<data:DataGrid x:Name="DataGridTest" />
</Grid>
</UserControl>
Page.xaml.cs:
public partial class Page : UserControl
{
string[] _names = new string[] { "one", "two", "three" };
public Page()
{
InitializeComponent();
BuildGrid();
}
public void BuildGrid()
{
DataGridTest.ItemsSource = _names;
}
}
This builds the rows dynamically from the contents of your string array. In future an even better way would be to use an ObservableCollection where T implements INotifyPropertyChanged. This will notify the DataGrid to update it's rows if you remove or add an item from the collection and also when properties of T change.
To further customize the UIElements used to display things you can use a DataGridTemplateColumn:
<data:DataGridTemplateColumn Header="Symbol">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding PutNameOfPropertyHere}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
</data:DataGridTemplateColumn>
I have never used this stuff, but shouldn't you be adding the Button to the RowDefinitions, instead of the Children? (just a logical observation)