I have WPF form, and in XAML I have label which on holding MouseDown I can move around form and on MouseRelease leave it in new position.
This is working as expected.
XAML code:
<Canvas>
<Label Content="Label"
Background="ForestGreen"
Padding="12,7"
Canvas.Left="{Binding XPosition}"
Canvas.Top="{Binding YPosition}"
MouseDown="Label_MouseDown"
MouseUp="Label_MouseUp"
MouseMove="Label_MouseMove"/>
</Canvas>
C#
public partial class frmTables : Window, INotifyPropertyChanged
{
private Point BasePoint = new Point(0.0, 0.0);
private double DeltaX = 0.0;
private double DeltaY = 0.0;
private bool moving = false;
private Point PositionInLabel;
public frmTables()
{
InitializeComponent();
this.DataContext = this;
}
public double XPosition
{
get { return BasePoint.X + DeltaX; }
}
public double YPosition
{
get { return BasePoint.Y + DeltaY; }
}
private void Label_MouseDown(object sender, MouseButtonEventArgs e)
{
Label l = e.Source as Label;
if (l != null)
{
l.CaptureMouse();
moving = true;
PositionInLabel = e.GetPosition(l);
lblCoord.Content = "MouseDown";
}
}
private void Label_MouseMove(object sender, MouseEventArgs e)
{
if (moving)
{
Point p = e.GetPosition(null);
DeltaX = p.X - BasePoint.X - PositionInLabel.X;
DeltaY = p.Y - BasePoint.Y - PositionInLabel.Y;
RaisePropertyChanged("XPosition");
RaisePropertyChanged("YPosition");
lblCoord.Content = DeltaX + ":" + DeltaY;
}
}
private void Label_MouseUp(object sender, MouseButtonEventArgs e)
{
Label l = e.Source as Label;
if (l != null)
{
l.ReleaseMouseCapture();
BasePoint.X += DeltaX;
BasePoint.Y += DeltaY;
DeltaX = 0.0;
DeltaY = 0.0;
moving = false;
lblCoord.Content = BasePoint.X + ":" + BasePoint.Y;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
This is all working as expected until I change XAML, and create two labels during runtime from code behind:
<Canvas>
<ItemsControl Name="btnTableImageList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="Label"
Background="ForestGreen"
Padding="12,7"
Canvas.Left="{Binding XPosition}"
Canvas.Top="{Binding YPosition}"
MouseDown="Label_MouseDown"
MouseUp="Label_MouseUp"
MouseMove="Label_MouseMove"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
Everything else remains the same, except generating those labes from code behind and changed XAML. Now if I hold MouseDown on label and move, nothing happens but MouseDown, and MouseMove are working since I can see test messages in lblCoord.Content.
If you need I can show you label generation code, but it's nothing special, just a class with a for-loop to create certain number of labels and I am calling that on WindowLoaded with btnTableImageList.ItemsSource = tableLbl.CreateTableLabels();.
Anyone have idea why is this happening, or to be more precise, what am I doing wrong?
The standard ItemsPanel of an ItemsControl is a StackPanel, so you will not see any effect when changing Canvas.Left/Top. You can modify the ItemsPanel to provide a Canvas instead.
<Canvas>
<ItemsControl Name="btnTableImageList">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Content="Label"
Background="ForestGreen"
Padding="12,7"
Canvas.Left="{Binding XPosition}"
Canvas.Top="{Binding YPosition}"
MouseDown="Label_MouseDown"
MouseUp="Label_MouseUp"
MouseMove="Label_MouseMove"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
NOTE: there is still something missing; for a complete solution see Setting Canvas properties in an ItemsControl DataTemplate
I think what you're trying to do here is a big step beyond your first piece of code. Because it touches on a number of wpf concepts you probably haven't looked at.
Stackpanel is only part of the thing you need to understand here.
I recommend you download and install snoop.
When you run it you get a weird toolbar with some scope sight things on it.
Drag one over your window and a new window appears.
Hover over where one of your labels is and press shift+ctrl at the same time.
You will see the tree of controls.
Your label will be inside a contentpresenter and it is this which goes directly in the canvas. It is this you need to set canvas.top and canvas.left on.
You will notice when you try and manipulate that control that working in this was is a nuisance.
Why is this so hard? ( you are likely to think ).
That's because you're intended to use binding and templating rather than directly add controls.
Hence, set the datacontext of the window to a viewmodel, bind an observablecollection of viewmodels from there to the itemssource of the itemscontrol. Each of these latter viewmodels would then expose left and top properties which are used to position your piece of ui ( label ).
Here's a working sample illustrates the approach of binding and templating:
https://1drv.ms/u/s!AmPvL3r385QhgooJ94uO6PopIDs4lQ
That's templating into textboxes and stuff rather than doing exactly what you're trying to do.
Related
I am writing this because I had difficulties in implementing a specific function during WPF implementation.
For data model
latitude and longitude (location in current window)
MainViewModel is
I am managing it as an observableCollection.
In xaml, the upper and longitude values for each model list were displayed,
The part that implements the button control in the form of an image to move according to the location information that is continuously updated in xaml is blocked, so I am posting this.
In addition, I want to display a line for the azimuth or each component, but I also want to implement this so that the line moves according to the changing value.
Is there a method that is usually used for these changing values or is there a method that is mainly used in practice?
In the case of Winform, I used the method of drawing a line using a Graphics object, but if anyone knows how to display it in real time by moving it in real time in C# WPF and binding the position value, I would appreciate it if you could share it.
In my previous answer I demonstrated a way to position a line based on a property in the viewmodel.
However, this only answers part of your question.
In the example below I will demonstrate how to have an ObservableCollection with positions that will be reflected on screen by showing the positions as text at the screen location corresponding with their values.
To start with, while the ObservableCollection can be used as an Itemsource, we must make sure the Items themselves are suitable to be used as viewmodels:
So, you could declare a Positionclass as follows (using 'CommunityToolkit.Mvvm'):
using CommunityToolkit.Mvvm.ComponentModel;
namespace ViewModels;
public partial class Position : ObservableObject
{
[ObservableProperty]
private double _x;
[ObservableProperty]
private double _y;
public Position(double x = 0.0, double y = 0.0)
{
X = x;
Y = y;
}
}
Now you can declare a MainViewModel containing a ObservableCollection<Position> property that will be used as an itemsource. For demonstration purposes, the values for the positions will be changed continuously based upon a DispatchTimer:
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.ObjectModel;
using System.Windows.Threading;
namespace PresentationLayer.ViewModels;
public class MainViewModel : ObservableObject
{
private DispatcherTimer _timer = new();
private Random _random = new();
// binding properties
public ObservableCollection<Position> TextPositions { get; private set; } = new();
public MainViewModel()
{
TextPositions.Add(new(10, 10));
TextPositions.Add(new(20, 100));
TextPositions.Add(new(50, 300));
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Tick += OntimerTick;
_timer.Start();
}
private void OntimerTick(object? sender, EventArgs e)
{
foreach (var item in TextPositions)
{
item.X = (item.X + _random.Next(30)) % 600;
item.Y = (item.Y + _random.Next(20)) % 350;
}
}
}
Finally, this MainViewModel can be used as datacontext for a WPF-Window displaying the positions in an ItemsControl using a datatemplate to construct the Textvalues to be displayed.
The positioning is handled using an ItemsControl.ItemContainerStyle attribute binding the position of the items.
Note: for this to work, you need to specify the ItemsPanel should be of type Canvas since the binding for the Location uses Canvas.Top and Canvas.Left.
The MainWindow (using a MainViewModelinstance as datacontext):
<Canvas>
<ItemsControl ItemsSource="{Binding TextPositions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate >
<DataTemplate>
<Border BorderBrush="Blue" BorderThickness="1" CornerRadius="2" Background="LightSkyBlue">
<WrapPanel>
<TextBlock Text="{Binding X}" />
<TextBlock Text=", "/>
<TextBlock Text="{Binding Y}" />
</WrapPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
For the positioning of controls or display-elements, you can use binding to a property in your ViewModel.
In the example below I am using a Canvas and bind the Canvas.Top attribute of a line to the LinePos property in the Viewmodel.
An alternative could be to bind the Rendertransform instead...
<Canvas>
<Line X1="0" Y1="10" X2="500" Y2="10" Stroke="Blue" StrokeThickness="3" Canvas.Top="{Binding LinePos}" />
</Canvas>
And the ViewModel:
public class MainViewModel : ObservableObject
{
private int _linePos;
private DispatcherTimer _timer = new();
// binding properties
public int LinePos
{
get => _linePos;
set => SetProperty(ref _linePos, value);
}
public MainViewModel()
{
_timer.Interval = TimeSpan.FromMilliseconds(10);
_timer.Tick += OnTimerTick;
_timer.Start();
}
private void OnTimerTick(object? sender, EventArgs e)
{
LinePos = (LinePos+1) % 500;
}
}
I have an ItemsControl that wraps its ItemPresenter with a ScrollViewer. That ItemPresenter displays a ListView. Therefore I have a collection within a collection.
Now, I want only the ScrollViewer to have scrolling functionality so I have gone ahead and removed the scrolling functionality from the inner ListView.
The problem is that my scrolling event is being messed up by the ListView. As soon as my finger touches the content area it selects the ListViewItems instead of scrolling.
How can I tell through routed events if the user is trying to click or scroll? and if it is scroll, how do I prevent it from selecting the ListViewItems?
<ItemsControl ItemsSource="{Binding Countries}" >
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer PanningMode="VerticalOnly">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Cities}">
<ListView.Template>
<ControlTemplate>
<ItemsPresenter/>
</ControlTemplate>
</ListView.Template>
</ListView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
There isn't any way to look into the future to see if a user is going to begin scrolling after touching the screen or simply lift their finger off immediately afterwards. A Future API would be nice, though.
Anyhow, you could just check to see if the user has moved their finger at all after touching the ListView. If so, begin treating the "touch" like a "scroll" rather than a "click" by manually scrolling the ScrollViewer and deselecting the ListView items.
Something like this:
private bool _touchDown = false;
private double _initOffset = 0;
private double _scrollDelta = 5;
private void ListView_PreviewTouchDown(object sender, TouchEventArgs e)
{
_touchDown = true;
_initOffset = e.GetTouchPoint(this).Y;
}
private void ListView_PreviewTouchMove(object sender, TouchEventArgs e)
{
if (_touchDown && Math.Abs(r.GetTouchpoint(this).Y - _initOffset) > _scrollDelta)
{
My_ScrollViewer.ScrollToVerticalOffset(r.GetTouchpoint(this).Y - _initOffset);
My_ListView.UnselectAll();
}
}
private void ListView_PreviewTouchUp(object sender, TouchEventArgs e)
{
_touchDown = false;
_initOffset = 0;
}
Disclaimer: I just wrote this in notepad. It has problems, but it gets the concept across.
Im trying to bind my usercontrols (that have a move on mouse left btn logic inside its code behind) margin inside a datatemplate of an itemscontrol. However when being bound inside the datatemplate it doesnt update itself when a move on the grid behind it (which itself is a usercontrol aswell and has a move on mouse middle button logic inside its code behind).
The code behind of those is: (where in the grid it reacts to middle mouse)
private void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
isDragging = true;
var draggableControl = sender as UserControl;
lasttransform = new Point(0, 0);
clickPosition = e.GetPosition(this.Parent as UIElement);
draggableControl.CaptureMouse();
}
private void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (isDragging == true)
{
isDragging = false;
var draggable = sender as UserControl;
draggable.ReleaseMouseCapture();
completetransform = Point.Add(completetransform,new Vector( lasttransform.X, lasttransform.Y));
lasttransform = new Point(0, 0);
}
}
private void Control_MouseMove(object sender, MouseEventArgs e)
{
var draggableControl = sender as UserControl;
if (isDragging && draggableControl != null && draggableControl.GetType() == typeof(BaseNode))
{
Point currentPosition = e.GetPosition(this.Parent as UIElement);
currentPosition = new Point(currentPosition.X, currentPosition.Y);
var transform = draggableControl.RenderTransform as TranslateTransform;
if (transform == null)
{
transform = new TranslateTransform();
draggableControl.RenderTransform = transform;
}
transform.X = completetransform.X + (currentPosition.X - clickPosition.X);
transform.Y = completetransform.Y + (currentPosition.Y - clickPosition.Y);
lasttransform = new Point((currentPosition.X - clickPosition.X), (currentPosition.Y - clickPosition.Y));
}
}
}
When i now want to show my controls in some window like:
<Grid>
<my:EventGrid x:Name="XDisplayedEventGrid" Margin="-20" DataContext="{Binding DisplayedEventGrid}"/>
<Grid Background="Red" Width="100" Height="100" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="{Binding DisplayedEventGrid.ActualTransform, Converter={StaticResource ResourceKey=PointToMarginConverter}}"/>
<my:BaseNode HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="100" Margin="{Binding DisplayedEventGrid.ActualTransform, Converter={StaticResource ResourceKey=PointToMarginConverter}}"/>
<ItemsControl ItemsSource="{Binding DisplayedNodes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<my:BaseNode HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="100" Margin="{Binding ElementName=XDisplayedEventGrid, Path=ActualTransform, Converter={StaticResource ResourceKey=PointToMarginConverter}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Where 'my:EventGrid 'is the movable grid, 'my:BaseNode' is a control to test everything and DataTemplate> my:BaseNode .../> /DataTemplate> is the actual displaying control for these usercontrols.
What should happen:
1) When you move the grid behind the visual brush makes it look like you can scroll unlimitedly.
2) The 'Grid Background="Red"' stays at its place on the eventgrid.
3) The Margin is bound to the eventgrid's moved coordinates which is translated to a margin by a converter, so for the user it looks like the controls stay on place on the grid and get moved out of view if the user looks at another portion of the grid.
4) When a node is moved its RenderTransform is set so it can be placed by and bound to a margin while being able to reposition it locally with the RenderTransform.
My Problem now is, that when the test-node has no DataContext bound it has the behavior of the grid and moves accordingly, but if it has one set it acts like the node inside the datatemplate and stays at 0,0 of the window.
The Datacontext of the node is an empty class deriving of a standard implementation of INotifyPropertyChanged.
When not having a datacontext bound inside the datatemplate it still has the same wrong behavior.
Where is my error and
are there any better controls to store externally marged usercontrols?
Thanks
ShinuSha
My last question was marked as duplicate so I try to show what is the difference.
Is there any way to bind Layers and don't create a Canvas element for each one?
If not, then it almost works but:
Multiple canvases overlap. If I set Background property, canvases below will be not visible. Even if Background is set to Transparent, then the Mouse Events are taken only by the Canvas on top.
If I set ClipToBounds property to True (and don't set Width&Height) Markers are not visible. The Width and Height is not the same as main canvas. How to bind these Properties to Main Canvas Width and Height. I know that every Layer will have the same dimensions, so I don't think it would be good to store duplicate information in every Layer.
EDIT:
Sorry for misunderstanding. I try to be more clarify:
Problems (questions) I want to solve are:
Is there any way to bind Layers and don't create a Canvas element for each one?
Now I have mainCanvas + multiple innerCanvases. Could it be just mainCanvas? Does it have any influence on rendering performance?
How to set Width and Height of inner Canvases, so they will have the same dimensions as main Canvas, without binding?
mainCanvas automatically fills all the space, but innerCanvases don't. ClipToBounds=True must be set on innerCanvases.
Tried HorizontalAligment=Stretch but it is not working.
The overlapping: Okay, I think I missed something.
If I don't set Background at all it works fine, as it should. It was just funny for me that not setting Background doesn't work the same as Background=Transparent.**
Sorry for my English.
EDIT: Thanks for your answer
I think it will be better if I don't complicate my code, at least for now. I found out how to bind to ActualWidth as you said:
<Canvas Width="{Binding ElementName=mainCanvas, Path=ActualWidth}"/>
or set ClipToBounds=True on mainCanvas, not the inner ones. I just wanted markers that have Positions X, Y outside mainCanvas dimensions to not be visible. Thats why I needed to set Width, Height of innerCanvases.
Everything is working now, marked as answer.
Here is my code:
ViewModel.cs
public class ViewModel
{
public ObservableCollection<LayerClass> Layers
{ get; set; }
public ViewModel()
{
Layers = new ObservableCollection<LayerClass>();
for (int j = 0; j < 10; j++)
{
var Layer = new LayerClass();
for (int i = 0; i < 10; i++)
{
Layer.Markers.Add(new MarkerClass(i * 20, 10 * j));
}
Layers.Add(Layer);
}
}
}
LayerClass.cs
public class LayerClass
{
public ObservableCollection<MarkerClass> Markers
{ get; set; }
public LayerClass()
{
Markers = new ObservableCollection<MarkerClass>();
}
}
MarkerClass.cs
public class MarkerClass
{
public int X
{ get; set; }
public int Y
{ get; set; }
public MarkerClass(int x, int y)
{
X = x;
Y = y;
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private ViewModel _viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = _viewModel;
}
private void Ellipse_MouseEnter(object sender, MouseEventArgs e)
{
Ellipse s = (Ellipse)sender;
s.Fill = Brushes.Green;
}
private void Ellipse_MouseLeave(object sender, MouseEventArgs e)
{
Ellipse s = (Ellipse)sender;
s.Fill = Brushes.Black;
}
}
MainWindow.xaml
<Window x:Class="TestSO33742236WpfNestedCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:TestSO33742236WpfNestedCollection"
Loaded="Window_Loaded"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Path=Layers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="LightBlue">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type c:LayerClass}">
<ItemsControl ItemsSource="{Binding Path=Markers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="myCanvas"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="20" Height="20" Fill="Black" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<p:Style> <!-- Explicit namespace to workaround StackOverflow XML formatting bug -->
<Setter Property="Canvas.Left" Value="{Binding Path=X}"></Setter>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}"></Setter>
</p:Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Is there any way to bind Layers and don't create a Canvas element for each one?
Now I have mainCanvas + multiple innerCanvases. Could it be just mainCanvas? Does it have any influence on rendering performance?
Certainly it is possible to implement the code so that you don't have inner Canvas elements. But not by binding to Layers. You would have to maintain a top-level collection of all of the MarkerClass elements, and bind to that. Please see below for an example of this.
I doubt you will see much difference in rendering performance, but note that the implementation trades XAML code for C# code. I.e. there's less XAML but a lot more C#. Maintaining the mirror collection of items certainly would add to overhead in your own code (though it's not outside the realm of possibility that WPF does something similar internally…I don't know), but that cost comes when adding and removing elements. Rendering them should be at least fast as in the nested collection scenario.
Note, however, that I doubt it would be noticeably faster. Deep hierarchies of UI elements is the norm in WPF, and the framework is optimized to handle that efficiently.
In any case, IMHO it is better to let the framework handle interpreting high-level abstractions. That's why we use higher-level languages and frameworks in the first place. Don't waste any time at all trying to "optimize" code if it takes you away from a better representation of the data you're modeling. Only pursue that if and when you have implemented the code the naïve way, it works 100% correctly, and you still have a measurable performance problem with a clear, achievable performance goal.
How to set Width and Height of inner Canvases, so they will have the same dimensions as main Canvas, without binding?
mainCanvas automatically fills all the space, but innerCanvases don't. ClipToBounds=True must be set on innerCanvases. Tried HorizontalAligment=Stretch but it is not working.
What is it you are trying to achieve by having the inner Canvas objects boundaries expand to fill the parent element? If you really need to do this, you should be able to bind the inner Canvas elements' Width and Height properties to the parent's ActualWidth and ActualHeight properties. But unless the inner Canvas is given some kind of formatting or something, you would not be able to see the actual Canvas object and I wouldn't expect the width and height of those elements to have any practical effect.
The overlapping: Okay, I think I missed something.
If I don't set Background at all it works fine, as it should. It was just funny for me that not setting Background doesn't work the same as Background=Transparent.
It makes sense to me that having no background fill at all would be different than having a background fill that has the alpha channel set to 0.
It would significantly complicate WPF's hit-testing code to have to check each pixel of each element under the mouse to see if that pixel is transparent. I suppose WPF could have special-cased the solid-brush fill scenario, but then people would complain that a solid-but-transparent brush disables hit-testing while other brushes with transparent pixels don't, even where they are transparent.
Note that you can format an object without having it participate in hit-testing. Just set its IsHitTestVisible property to False. Then it will can render on the screen, but will not respond to or interfere with mouse clicks.
Here's the code example of how you might implement the code using just a single Canvas object:
XAML:
<Window x:Class="TestSO33742236WpfNestedCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:TestSO33742236WpfNestedCollection"
Loaded="Window_Loaded"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Path=Markers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="LightBlue"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type c:MarkerClass}">
<Ellipse Width="20" Height="20" Fill="Black" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<p:Style>
<Setter Property="Canvas.Left" Value="{Binding Path=X}"></Setter>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}"></Setter>
</p:Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Window>
C#:
class ViewModel
{
public ObservableCollection<MarkerClass> Markers { get; set; }
public ObservableCollection<LayerClass> Layers { get; set; }
public ViewModel()
{
Markers = new ObservableCollection<MarkerClass>();
Layers = new ObservableCollection<LayerClass>();
Layers.CollectionChanged += _LayerCollectionChanged;
for (int j = 0; j < 10; j++)
{
var Layer = new LayerClass();
for (int i = 0; i < 10; i++)
{
Layer.Markers.Add(new MarkerClass(i * 20, 10 * j));
}
Layers.Add(Layer);
}
}
private void _LayerCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
ObservableCollection<LayerClass> layers = (ObservableCollection<LayerClass>)sender;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
_InsertMarkers(layers, e.NewItems.Cast<LayerClass>(), e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
_RemoveMarkers(layers, e.OldItems.Count, e.OldStartingIndex);
_InsertMarkers(layers, e.NewItems.Cast<LayerClass>(), e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
_RemoveMarkers(layers, e.OldItems.Count, e.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
Markers.Clear();
break;
}
}
private void _RemoveMarkers(ObservableCollection<LayerClass> layers, int count, int removeAt)
{
int removeMarkersAt = _MarkerCountForLayerIndex(layers, removeAt);
while (count > 0)
{
LayerClass layer = layers[removeAt++];
layer.Markers.CollectionChanged -= _LayerMarkersCollectionChanged;
Markers.RemoveRange(removeMarkersAt, layer.Markers.Count);
}
}
private void _InsertMarkers(ObservableCollection<LayerClass> layers, IEnumerable<LayerClass> newLayers, int insertLayersAt)
{
int insertMarkersAt = _MarkerCountForLayerIndex(layers, insertLayersAt);
foreach (LayerClass layer in newLayers)
{
layer.Markers.CollectionChanged += _LayerMarkersCollectionChanged;
Markers.InsertRange(layer.Markers, insertMarkersAt);
insertMarkersAt += layer.Markers.Count;
}
}
private void _LayerMarkersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ObservableCollection<MarkerClass> markers = (ObservableCollection<MarkerClass>)sender;
int layerIndex = _GetLayerIndexForMarkers(markers);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Markers.InsertRange(e.NewItems.Cast<MarkerClass>(), _MarkerCountForLayerIndex(Layers, layerIndex));
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
Markers.RemoveRange(layerIndex, e.OldItems.Count);
Markers.InsertRange(e.NewItems.Cast<MarkerClass>(), _MarkerCountForLayerIndex(Layers, layerIndex));
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
Markers.RemoveRange(layerIndex, e.OldItems.Count);
break;
}
}
private int _GetLayerIndexForMarkers(ObservableCollection<MarkerClass> markers)
{
for (int i = 0; i < Layers.Count; i++)
{
if (Layers[i].Markers == markers)
{
return i;
}
}
throw new ArgumentException("No layer found with the given markers collection");
}
private static int _MarkerCountForLayerIndex(ObservableCollection<LayerClass> layers, int layerIndex)
{
return layers.Take(layerIndex).Sum(layer => layer.Markers.Count);
}
}
static class Extensions
{
public static void InsertRange<T>(this ObservableCollection<T> source, IEnumerable<T> items, int insertAt)
{
foreach (T t in items)
{
source.Insert(insertAt++, t);
}
}
public static void RemoveRange<T>(this ObservableCollection<T> source, int index, int count)
{
for (int i = index + count - 1; i >= index; i--)
{
source.RemoveAt(i);
}
}
}
Caveat: I have not tested the code above thoroughly. I've only run it in the context of the original code example, which only ever adds pre-populated LayerClass objects to the Layers collection, and so only the Add scenario has been tested. There could be typographical or even significant logic bugs, though of course I've tried to avoid that. As with any code you find on the Internet, use at your own risk. :)
Is it possible to implement mouse click and drag selection box in WPF. Should it be done through simply drawing a rectangle, calculating coordinates of its points and evaluating position of other objects inside this box? Or are there some other ways?
Could you give a bit of sample code or a link?
Here is sample code for a simple technique that I have used in the past to draw a drag selection box.
XAML:
<Window x:Class="DragSelectionBox.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
>
<Grid
x:Name="theGrid"
MouseDown="Grid_MouseDown"
MouseUp="Grid_MouseUp"
MouseMove="Grid_MouseMove"
Background="Transparent"
>
<Canvas>
<!-- This canvas contains elements that are to be selected -->
</Canvas>
<Canvas>
<!-- This canvas is overlaid over the previous canvas and is used to
place the rectangle that implements the drag selection box. -->
<Rectangle
x:Name="selectionBox"
Visibility="Collapsed"
Stroke="Black"
StrokeThickness="1"
/>
</Canvas>
</Grid>
</Window>
C#:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
bool mouseDown = false; // Set to 'true' when mouse is held down.
Point mouseDownPos; // The point where the mouse button was clicked down.
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
// Capture and track the mouse.
mouseDown = true;
mouseDownPos = e.GetPosition(theGrid);
theGrid.CaptureMouse();
// Initial placement of the drag selection box.
Canvas.SetLeft(selectionBox, mouseDownPos.X);
Canvas.SetTop(selectionBox, mouseDownPos.Y);
selectionBox.Width = 0;
selectionBox.Height = 0;
// Make the drag selection box visible.
selectionBox.Visibility = Visibility.Visible;
}
private void Grid_MouseUp(object sender, MouseButtonEventArgs e)
{
// Release the mouse capture and stop tracking it.
mouseDown = false;
theGrid.ReleaseMouseCapture();
// Hide the drag selection box.
selectionBox.Visibility = Visibility.Collapsed;
Point mouseUpPos = e.GetPosition(theGrid);
// TODO:
//
// The mouse has been released, check to see if any of the items
// in the other canvas are contained within mouseDownPos and
// mouseUpPos, for any that are, select them!
//
}
private void Grid_MouseMove(object sender, MouseEventArgs e)
{
if (mouseDown)
{
// When the mouse is held down, reposition the drag selection box.
Point mousePos = e.GetPosition(theGrid);
if (mouseDownPos.X < mousePos.X)
{
Canvas.SetLeft(selectionBox, mouseDownPos.X);
selectionBox.Width = mousePos.X - mouseDownPos.X;
}
else
{
Canvas.SetLeft(selectionBox, mousePos.X);
selectionBox.Width = mouseDownPos.X - mousePos.X;
}
if (mouseDownPos.Y < mousePos.Y)
{
Canvas.SetTop(selectionBox, mouseDownPos.Y);
selectionBox.Height = mousePos.Y - mouseDownPos.Y;
}
else
{
Canvas.SetTop(selectionBox, mousePos.Y);
selectionBox.Height = mouseDownPos.Y - mousePos.Y;
}
}
}
}
I wrote an article about this:
https://www.codeproject.com/Articles/148503/Simple-Drag-Selection-in-WPF
You can get this functionality pretty easily by adding an InkCanvas and set its EditingMode to Select. Although it's primarily intended for Tablet PC ink collection and rendering, it's very easy to use it as a basic designer surface.
<Window Width="640" Height="480" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<InkCanvas EditingMode="Select">
<Button Content="Button" Width="75" Height="25"/>
<Button Content="Button" Width="75" Height="25"/>
</InkCanvas>
</Window>
This project created a custom MultiSelector which supports several selection methods including a rectangular "lasso" style:
Developing a MultiSelector by Teofil Cobzaru
It is far too long to reproduce here. The key elements of the design, IIRC, were to create a custom ItemContainer which knows how to interact with its MultiSelector parent. This is analagous to ListBoxItem / ListBox.
This is probably not the simplest possible approach, however if you are already using some type of ItemsControl to host the items which may need to be selected, it could fit into that design pretty easily.
MouseDown logic:
MouseRect.X = mousePos.X >= MouseStart.X ? MouseStart.X : mousePos.X;
MouseRect.Y = mousePos.Y >= MouseStart.Y ? MouseStart.Y : mousePos.Y;
MouseRect.Width = Math.Abs(mousePos.X - MouseStart.X);
MouseRect.Height = Math.Abs(mousePos.Y - MouseStart.Y);