I have a draggable user control within another user control indicating the distance of a vessel from another vessel. The placement of the object is bound to X, Y values in the back end. If the user loads a "mission", the location of this draggable object should snap to the position based on the X and Y values. Additionally, if the user drags the boat but then cancels, the position of the boat should snap back to its original position before it was dragged.
I find that if I bind to the Margin field, the margin is set correctly on the control but the draggable object stays in the same place, as if it is at 0,0,0,0 with the updated values (I can force it to move by editing the xaml and proving that any margin changes act as if the new values are 0,0,0,0:
public Thickness DaughtershipMargin
{
get { return _daughtershipMargin; }
set
{
_daughtershipMargin = value;
OnPropertyChanged();
}
}
I have also experimented with using a Canvas instead with similar results.
When initially dragging, the position is based on the relative position of the control based on a parent control - so it needs I need to do something similar
private void MouseDragElementBehavior_Dragging(object sender, MouseEventArgs e)
{
double x = System.Math.Round((e.GetPosition(colabdragObjectGrid).X - 280), 3);
double y = System.Math.Round((-1 * (e.GetPosition(colabdragObjectGrid).Y - 280)), 3);
if (x >= _minFollowDistance && x <= _maxFollowDistance && y >= _minFollowDistance && y <= _maxFollowDistance)
{
_parent.ProcessDragEvent(x.ToString(), y.ToString());
}
}
<Grid x:Name="colabdragObjectGrid" Margin="0,-10,0,0" Height="575" Width="575">
<local:colabdrag x:Name="colabdragObject" Height="100" Width="100" RenderTransformOrigin="0.5,0.5" Margin="{Binding DaughtershipMargin}">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior ConstrainToParentBounds="True" Dragging="MouseDragElementBehavior_Dragging"/>
</i:Interaction.Behaviors>
</local:colabdrag>
</Grid>
Edit:
I got this to work by creating a point relative to the draggable area and another point for the desired position based on that point, then using TranslateTranform to get the object where it needs to be
var pointA = (colabdragObject.TransformToAncestor(colabDragObjectGrid).Transform(new Point(X,Y))
var pointB = colabdragObjectGrid.TranslatePoint(pointA, colabDragObject);
colabdragObject.RenderTransform = new TranslateTransform(pointB.X, -1 * pointB.Y);
Related
I have a question regarding WPF.
I have a Canvas that serves as a visual editor! I have a few 'nodes' positioned in the Canvas using 'X' and 'Y' properties (Canvas.Left and Canvas.Top). Now, I need this Canvas to let the user Zoom (in & out) and Pan around, as he want's to.
I implemented kind of a hack to emulate that behavior. This is the Code that let's the user 'pan' around in the Canvas:
///In file MainWindow.xaml.cs
private void ZoomPanCanvas_MouseMove(object sender, MouseEventArgs e) {
if (IsMouseDown) {
///Change the Cursor to Scroll
if (mNetworkUI.Cursor != Cursors.ScrollAll)
mNetworkUI.Cursor = Cursors.ScrollAll;
var currPosition = e.GetPosition(mNetworkUI);
var diff = currPosition - MouseLastPosition;
var p = new Point(diff.X, diff.Y);
mNetworkUI.ViewModel.Network.SetTransformOffset(p);
MouseLastPosition = currPosition;
}
}
///In file NetworkViewModel.cs
public void SetTransformOffset(Point newOffset) {
for (int i = 0; i < Nodes.Count; i++) {
Nodes[i].X += newOffset.X;
Nodes[i].Y += newOffset.Y;
}
}
Where the 'Nodes' are my editor-nodes displayed in the Canvas. The Zooming (with respect to the mouse position works as follows:
///File MainWindow.xaml.cs
private void ZoomPanCanvas_MouseWheel(object sender, MouseWheelEventArgs e) {
///Determine the Scaling Factor and Scale the Rule-Editor
var factor = (e.Delta > 0) ? (1.1) : (1 / 1.1);
currrentScale = factor * currrentScale;
ScaleNetwork();
///Translate the Nodes to the desired Positions
var pos = e.GetPosition(mNetworkUI);
var transform = new ScaleTransform(factor, factor, pos.X, pos.Y);
var offSet = new Point(transform.Value.OffsetX, transform.Value.OffsetY);
mNetworkUI.ViewModel.Network.SetTransformOffset(offSet);
}
///Also in MainWindow.xaml.cs
private void ScaleNetwork() {
mNetworkUI.RenderTransform = new ScaleTransform(currrentScale, currrentScale);
mNetworkUI.Width = ZoomPanCanvas.ActualWidth / currrentScale;
mNetworkUI.Height = ZoomPanCanvas.ActualHeight / currrentScale;
}
So, in the 'panning' I calculate the difference to the last mouse position and use that vector to manipulate the nodes, not the Canvas itself.
When I zoom, I determine the new zoom, set a new RenderTransform, resize the Canvas to again fill the provided space and again re-position the nodes in the Canvas.
It works very well for now. I can 'pan & zoom' around how I want, but I realized, that with many nodes present in my 'network' (connected nodes), things get quite slow.
One reason is, that on every movement of a node some events are raised resulting in a noticable delay when panning.
How is such a thing (without fixed Canvas-size and Scrollbars) possible in a performant manner? Is there a control out there that I can use? Is this possible with the Extended WPF toolkit's ZoomBox control?
Thank you!
I've written a Viewport control for this exact functionality.
I've also packaged this up on nuget
PM > Install-Package Han.Wpf.ViewportControl
It extends a ContentControl which can contain any FrameworkElement and provides constrained zoom and pan functionality. Just make sure to add Generic.xaml to your app.xaml
<Application.Resources>
<ResourceDictionary Source="pack://application:,,,/Han.Wpf.ViewportControl;component/Themes/Generic.xaml" />
</Application.Resources>
Usage:
<Grid width="1200" height="1200">
<Button />
</Grid>
The source code for the control and theme is on my gist and can be found on my github along with a demo application that loads an image into the viewport control.
I'm doing some layouting on a toolbar-like control and need to hide texts of buttons when there's not enough space. I've successfully done this in Windows Forms already and now I've ported this logic to WPF. But there is a huge problem here: For my algorithm to work properly, I need to know the desired width of a container control (to know what size would be required if everything was visible) and the actual width of the control (to know how wide it really is and whether there's enough space for the desired width). The first one is available, albeit a bit backwards at times. (If there's more space available than required, the DesiredSize increases to fill it all out, although less would be fine.) The latter one is entirely unavailable!
I've tried with ActualWidth, but if the Grid is wider than the window, the ActualWidth is more than is actually visible. So this must be wrong already. I've then tried the RenderSize, but it's the same. Using Arrange after my Measure call leads to more weirdness.
I need to know how wide the control really is, and not how wide it believes itself to be. How can I determine that size?
Update: Okay, here's some code. It's already quite long for this question and still incomplete. This is from the Window's code-behind.
private void ToolGrid_LayoutUpdated(object sender, EventArgs e)
{
AutoCollapseItems();
}
private void AutoCollapseItems()
{
if (collapsingItems) return;
if (ToolGrid.ActualWidth < 10) return; // Something is wrong
try
{
collapsingItems = true;
// Collapse toolbar items in their specified priority to save space until all items
// fit in the toolbar. When collapsing, the item's display style is reduced from
// image and text to image-only. This is only applied to items with a specified
// collapse priority.
Dictionary<ICollapsableToolbarItem, int> collapsePriorities = new Dictionary<ICollapsableToolbarItem, int>();
// Restore the display style of all items that have a collpase priority.
var items = new List<ICollapsableToolbarItem>();
EnumCollapsableItems(ToolGrid, items);
foreach (var item in items)
{
if (item.CollapsePriority > 0)
{
item.ContentVisibility = Visibility.Visible;
collapsePriorities[item] = item.CollapsePriority;
}
}
// Group all items by their descending collapse priority and set their display style
// to image-only as long as all items don't fit in the toolbar.
var itemGroups = from kvp in collapsePriorities
where kvp.Value > 0
group kvp by kvp.Value into g
orderby g.Key descending
select g;
foreach (var grp in itemGroups)
{
//ToolGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
//ToolGrid.Arrange(new Rect(ToolGrid.DesiredSize));
//ToolGrid.UpdateLayout();
System.Diagnostics.Debug.WriteLine("Desired=" + ToolGrid.DesiredSize.Width + ", Actual=" + ToolGrid.ActualWidth);
if (ToolGrid.DesiredSize.Width <= ToolGrid.ActualWidth) break;
foreach (var kvp in grp)
{
kvp.Key.ContentVisibility = Visibility.Collapsed;
}
}
//ToolGrid.UpdateLayout();
}
finally
{
collapsingItems = false;
}
}
More code: Here's part of the Window XAML:
<Window>
<DockPanel>
<Grid Name="ToolGrid" DockPanel.Dock="Top" LayoutUpdated="ToolGrid_LayoutUpdated">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
...
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
From what I understood you are using Grid but you set the columns width to Auto, how about you use * for the Width of your Grid.Column istead of Auto. If Auto then Grid stretches its Width and Height to fit its content hence why your Grid.Width is greater than windows width. When you use * the column wont care about content but it will always be inside the windows boundaries.
Now after implementing *, you use the column.width/height, which is inside window boundaries as your final width/height and inside the Grid you can measure the desized size of your nested innner controls. Thats how you get the final size and the desized size of controls.
Show some more code/xaml and we will be able to help you furthermore.
Edited:
<Window>
<DockPanel x:Name="dockyPanel>
<Grid Name="ToolGrid" DockPanel.Dock="Top" LayoutUpdated="ToolGrid_LayoutUpdated">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
...
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
var itemGroups = from kvp in collapsePriorities
where kvp.Value > 0
group kvp by kvp.Value into g
orderby g.Key descending
select g;
double x = 0.0;
foreach (var grp in itemGroups)
{
// x will be increased by the sum of all widths of items
x += grp.SumOfAllWidthOfGroup;
// if x greater than available space then this group needs to collaps its items
if(x > this.dockyPanel.ActualWidth)
{
foreach (var kvp in grp)
{
kvp.Key.ContentVisibility = Visibility.Collapsed;
}
}
}
How about this? Will my pseudocode help you any further?
It's turned out that WPF won't give me predictable sizes of the Grid that contains all the auto-sized columns with the button elements in them. But the parent element of this Grid, no matter what it is and how it is layouted, provides usable information on that. So my solution is basically to insert another level of Grid container between what was already there and my actual toolbar layout grid and compare the different sizes of both. The central test to find out whether the grid would fit in the given space is now this:
foreach (var grp in itemGroups)
{
InnerToolGrid.UpdateLayout();
if (InnerToolGrid.RenderSize.Width - extentWidth <= ToolGrid.ActualWidth) break;
foreach (var kvp in grp)
{
kvp.Key.ContentVisibility = Visibility.Collapsed;
}
}
It iterates through all priority classes of elements that may be collapsed together (whereever they are located on the grid) and collapses all elements in a class (group). Then the inner grid's layout is updated to reflect the changes of the buttons and the inner grid's RenderSize is compared with the outer grid's ActualWidth. If it's smaller, it fits in and no more items need to be collapsed.
All of this is invoked from the LayoutUpdated event of the inner grid, still preventing recursion through a lock variable. This allows me to react on size changes of any button on the toolbar, for example when a text was updated.
Since the LayoutCompleted event seems to be triggered asynchronously, the lock variable must remain set until the next layout run has completed, and cannot be reset again at the end of the LayoutUpdated handler:
private bool collapsingItems;
private void InnerToolGrid_LayoutUpdated(object sender, EventArgs e)
{
if (collapsingItems) return;
// Prevent further calls before the layouting is completely finished
collapsingItems = true;
Dispatcher.BeginInvoke(
(Action) (() => { collapsingItems = false; }),
System.Windows.Threading.DispatcherPriority.Loaded);
// ...
}
So Here is the Problem, I am trying to get that circle to align on the number. When I do that in blend it shows me I have a Left (23), I try to do that programmaticly Canvas.SetLeft(thePanel,23) it overshoots. Better yet, if anyone knows of a control like this in silverlight let me know. What this does is when the user clicks on a number the green circle is suppose to go to that number so it looks like the user has selected it.
On your Circle object you have to set the Radius of the circle and the TranslateTransform attribute. Lets say your Circle has a radius of 15:
private const double Radious = 15.0;
private double _x = Radious;
private double _y = Radious;
private TranslateTransform _translation = new TranslateTransform();
and properties to handle the Circle's X and Y coordinates,
public double X
{
get { return this._x; }
set
{
this._x = value;
_translation.X = this._x - Radious;
}
}
public double Y
{
get { return this._y; }
set
{
this._y = value;
_translation.Y = this._y - Radious;
}
}
and in Silverlight you can get where the user has clicked on a Canvas, setting this code on the Click Event of the panel, and set the center of the circle to where the user has clicked:
//Get the points where it was clicked
Point clickPoint = e.GetPosition(Canvas);
MyCircle.X = clickPoint.X;
MyCircle.Y = clickPoint.Y;
Now, if you want them to always fall in fixed positions, you can set conditions that, if a user clicks around a number, then set the center of the circle to the center of the number, or just change the X value of your circle to move to the desired position.
The WPF Canvas has a coordinate system starting at (0,0) at the top-left of the control.
For example, setting the following will make my control appear on the top-left:
<Control Canvas.Left="0" Canvas.Top="0">
How can I change it to the standard cartesian coordinates?
Basically:
(0,0) at center
flip Y
I noticed this post is similar, but it does not talk about translating the coordinate system. I tried adding a TranslateTransform, but I can't make it work.
There is no need to create a custom Panel. Canvas will do just fine. Simply wrap it inside another control (such as a border), center it, give it zero size, and flip it with a RenderTransform:
<Border>
<Canvas HorizontalAlignment="Center" VerticalAlignment="Center"
Width="0" Height="0"
RenderTransform="1 0 0 -1 0 0">
...
</Canvas>
</Border>
You can do this and everything in the canvas will still appear, except (0,0) will be at the center of the containing control (in this case, the center of the Border) and +Y will be up instead of down.
Again, there is no need to create a custom panel for this.
It was very easy to do. I looked at the original Canvas's code using .NET Reflector, and noticed the implementation is actually very simple. The only thing required was to override the function ArrangeOverride(...)
public class CartesianCanvas : Canvas
{
public CartesianCanvas()
{
LayoutTransform = new ScaleTransform() { ScaleX = 1, ScaleY = -1 };
}
protected override Size ArrangeOverride( Size arrangeSize )
{
Point middle = new Point( arrangeSize.Width / 2, arrangeSize.Height / 2 );
foreach( UIElement element in base.InternalChildren )
{
if( element == null )
{
continue;
}
double x = 0.0;
double y = 0.0;
double left = GetLeft( element );
if( !double.IsNaN( left ) )
{
x = left;
}
double top = GetTop( element );
if( !double.IsNaN( top ) )
{
y = top;
}
element.Arrange( new Rect( new Point( middle.X + x, middle.Y + y ), element.DesiredSize ) );
}
return arrangeSize;
}
}
You can simply change the Origin with RenderTransformOrigin.
<Canvas Width="Auto" Height="Auto"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5,0.5">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="-1" ScaleX="1" />
</TransformGroup>
</Canvas.RenderTransform>
</Canvas>
The best thing is to write a custom Canvas, in which you can write ArrangeOverride in such a way that it takes 0,0 as the center.
Update : I had given another comment in the below answer (#decasteljau) I wont recommend deriving from Canvas, You can derive from Panel and add two Attached Dependancy properties Top and Left and put the same code you pasted above. Also doesnt need a constructor with LayoutTransform in it, And dont use any transform on the panel code use proper measure and arrange based on the DesiredSize of the panel So that you can get nice content resize behavior too. Canvas doesn't dynamically position items when the Canvas size changes.
I have a Canvas inside a ScrollViewer. I want to have the user to be able to grab the canvas and move it around, with the thumbs on the scrollbars updating appropriately.
My initial implementation calculates the offset on each mouse move, and updates the scrollbars:
// Calculate the new drag distance
Point newOffsetPos = e.GetPosition(MapCanvas);
System.Diagnostics.Debug.WriteLine(" newOffsetPos : " + newOffsetPos.X + " " + newOffsetPos.Y);
double deltaX = newOffsetPos.X - _offsetPosition.X ;
double deltaY = newOffsetPos.Y - _offsetPosition.Y ;
System.Diagnostics.Debug.WriteLine(" delta X / Y : " + deltaX + " " + deltaY);
System.Diagnostics.Debug.WriteLine(" sv offsets X / Y : " + _scrollViewer.HorizontalOffset + " " + _scrollViewer.VerticalOffset);
_scrollViewer.ScrollToHorizontalOffset(_scrollViewer.HorizontalOffset - deltaX);
_scrollViewer.ScrollToVerticalOffset(_scrollViewer.VerticalOffset - deltaY);
_offsetPosition = newOffsetPos;
While this works, it is not very smooth.
Is there a better way to do this? If Transforms are used, will the scrollbars update automagically when the Canvas is moved?
Thanks for any tips...
Actually this sort of problem is really a matter of using the right pattern to track the mouse. I've seen this issue in variety of cases not just in Silverlight.
The best pattern is to trap the original locations of both mouse and subject, then recalculate the new offset from the fixed original values. That way the mouse stays planted solid at a single point on the image being panned. Here is my simple Repro:-
Start with a fresh Silverlight Application in visual studio. Modify MainPage.Xaml thus:-
<UserControl x:Class="SilverlightApplication1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" Background="White">
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Auto">
<Image x:Name="Map" Source="test.jpg" Width="1600" Height="1200" />
</ScrollViewer>
</Grid>
</UserControl>
Add the following code to the MainPage.xaml.cs file:-
public MainPage()
{
InitializeComponent();
Map.MouseLeftButtonDown += new MouseButtonEventHandler(Map_MouseLeftButtonDown);
}
void Map_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point mapOrigin = new Point(Scroller.HorizontalOffset, Scroller.VerticalOffset);
Point mouseOrigin = e.GetPosition(Application.Current.RootVisual);
MouseEventHandler moveHandler = null;
MouseButtonEventHandler upHandler = null;
moveHandler = (s, args) =>
{
Point mouseNew = args.GetPosition(Application.Current.RootVisual);
Scroller.ScrollToHorizontalOffset(mapOrigin.X - (mouseNew.X - mouseOrigin.X));
Scroller.ScrollToVerticalOffset(mapOrigin.Y - (mouseNew.Y - mouseOrigin.Y));
};
upHandler = (s, args) =>
{
Scroller.MouseMove -= moveHandler;
Scroller.MouseLeftButtonUp -= upHandler;
};
Scroller.MouseMove += moveHandler;
Scroller.MouseLeftButtonUp += upHandler;
}
}
Give it a reasonably large test.jpg (doesn't need to be 1600x1200 Image will scale it).
You'll note that when dragging the mouse remains exactly over a fixed point in the image until you hit a boundary. Move the mouse as fast as you like it always tracks, this is because it doesn't depend on deltas being accurate and up-to-date. The only variable is the current mouse position, the other values remain fixed as they were at mouse down.
You could try this (or at least take a peek at how it's implemented).