Translate high-scale WPF canvas is not smooth from source

Imagine you have a canvas that you want to scale to a very high value, and then enable panning.

A good example is a geographic tool that should allow panning with the β€œscaling” of levels from the scale of the entire earth to a level of several meters.

I found that if you scale to more than, say, 500,000, the translation becomes very unstable, but ONLY if you look far from the canvas on the origin of 0.0!

I tried to translate canvas using RenderTransform. And I tried it, literally moving another window over a scalable canvas. I also saw the same problem in the application for another application.

The following sample code provides panning (clicking and dragging) at two different zoom locations. If you are implementing the code, you can click one button to increase it to 0.0, where you will find a nice, smooth mouse movement. Then use another button to increase to 200, 200 and smoothly pan more!

Any idea why this is or how it can be fixed?

XAML for sample:

<Window x:Class="TestPanZoom.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="500" Width="500" Loaded="Window_Loaded"> <Grid PreviewMouseLeftButtonDown="Grid_PreviewMouseLeftButtonDown" MouseMove="Grid_MouseMove"> <Canvas Name="canvas1"></Canvas> <Button Height="31" Name="button1" Click="button1_Click" Margin="12,12,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" Width="270"> Zoom WAY in to 0,0 and get smooth panning </Button> <Button Height="31" Name="button2" Click="button2_Click" Margin="12,49,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" Width="270"> Zoom WAY in to 200, 200 -- NO smooth panning </Button> </Grid> </Window> 

Code for sample:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace TestPanZoom { /// <summary> /// Interaction logic for Window1.xaml /// Demo of an issue with translate transform when scale is very high /// but ONLY when you are far from the canvas 0,0 origin. /// Why? Is their a fix? /// </summary> public partial class Window1 : Window { Point m_clickPoint; public Window1() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { // Add a 2x2 black ellipse centered at 0,0 Ellipse el = new Ellipse(); el.Fill = Brushes.Black; el.Width = 2; el.Height = 2; el.HorizontalAlignment = HorizontalAlignment.Left; el.VerticalAlignment = VerticalAlignment.Top; el.Margin = new Thickness(0 - el.Width / 2, 0 - el.Height / 2, 0, 0); canvas1.Children.Add(el); // Add a 1x1 red rectangle with its top/left corner at 0,0 Rectangle r = new Rectangle(); r.Fill = Brushes.Red; r.Width = 1; r.Height = 1; r.HorizontalAlignment = HorizontalAlignment.Left; r.VerticalAlignment = VerticalAlignment.Top; r.Margin = new Thickness(0, 0, 0, 0); canvas1.Children.Add(r); // Add a 2x2 purple ellipse at a point 200,200 Point otherPoint = new Point(200, 200); el = new Ellipse(); el.Fill = Brushes.Purple; el.Width = 2; el.Height = 2; el.HorizontalAlignment = HorizontalAlignment.Left; el.VerticalAlignment = VerticalAlignment.Top; el.Margin = new Thickness(otherPoint.X - el.Width / 2, otherPoint.Y - el.Height / 2, 0, 0); canvas1.Children.Add(el); // Add a 1x1 blue rectangle with its top/left corner at 200,200 r = new Rectangle(); r.Fill = Brushes.Blue; r.Width = 1; r.Height = 1; r.HorizontalAlignment = HorizontalAlignment.Left; r.VerticalAlignment = VerticalAlignment.Top; r.Margin = new Thickness(otherPoint.X, otherPoint.Y, 0, 0); canvas1.Children.Add(r); } private void Grid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { m_clickPoint = e.GetPosition(this); } // Pan with the mouse when left-mouse is down private void Grid_MouseMove(object sender, MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { Point mousePosition = e.GetPosition(this); double xDiff = mousePosition.X - m_clickPoint.X; double yDiff = mousePosition.Y - m_clickPoint.Y; TranslateTransform tt = new TranslateTransform(xDiff, yDiff); TransformGroup tg = new TransformGroup(); tg.Children.Add(canvas1.RenderTransform); tg.Children.Add(tt); canvas1.RenderTransform = tg; m_clickPoint = e.GetPosition(this); } } private void button1_Click(object sender, RoutedEventArgs e) { TransformGroup tg = new TransformGroup(); double scale = 1000000; double xCenter = 0; double yCenter = 0; double xOffset = (canvas1.ActualHeight / 2.0 - xCenter); double yOffset = (canvas1.ActualWidth / 2.0 - yCenter); ScaleTransform st = new ScaleTransform(scale, scale); st.CenterX = xCenter; st.CenterY = yCenter; TranslateTransform tt = new TranslateTransform(xOffset, yOffset); tg.Children.Add(st); tg.Children.Add(tt); canvas1.RenderTransform = tg; } private void button2_Click(object sender, RoutedEventArgs e) { TransformGroup tg = new TransformGroup(); double scale = 1000000; double xCenter = 200; double yCenter = 200; double xOffset = (canvas1.ActualHeight / 2.0 - xCenter); double yOffset = (canvas1.ActualWidth / 2.0 - yCenter); ScaleTransform st = new ScaleTransform(scale, scale); st.CenterX = xCenter; st.CenterY = yCenter; TranslateTransform tt = new TranslateTransform(xOffset, yOffset); tg.Children.Add(st); tg.Children.Add(tt); canvas1.RenderTransform = tg; } } } 
+3
wpf transform scale
source share
1 answer

This is caused by Canvas itself. This is not very good for more advanced rendering. Found that you need to use the Visual class. It's a little trickier, but you get the advantage of low-level rendering.

Download Solution

Here is the code: MainWindow.xaml

 <Window x:Class="VisualTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="VisualLayer" Height="350.4" Width="496.8" xmlns:local="clr-namespace:VisualTest" > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <StackPanel Orientation="Vertical"> <Button Name="button1" Click="button1_Click" Margin="5" Padding="5,0"> Zoom WAY in to 0,0 </Button> <Button Name="button2" Click="button2_Click" Margin="5" Padding="5,0"> Zoom WAY in to 200, 200 </Button> <Button Name="button3" Click="button3_Click" Margin="5" Padding="5,0"> Zoom back </Button> </StackPanel> <local:DrawingCanvas Grid.Column="1" x:Name="drawingSurface" Background="White" ClipToBounds="True" MouseLeftButtonDown="drawingSurface_MouseLeftButtonDown" MouseLeftButtonUp="drawingSurface_MouseLeftButtonUp" MouseMove="drawingSurface_MouseMove"> </local:DrawingCanvas> </Grid> 

MainWindow.xaml.cs

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace VisualTest { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { // Variables for dragging shapes. private bool isDragging = false; private Vector clickOffset; private DrawingVisual selectedVisual; // Drawing constants. private Brush drawingBrush = Brushes.Black; private Brush selectedDrawingBrush = Brushes.LightGoldenrodYellow; private Pen drawingPen = new Pen(Brushes.SteelBlue, 3); private Size squareSize = new Size(10, 10); public MainWindow() { InitializeComponent(); DrawingVisual v = new DrawingVisual(); DrawSquare(v, new Point(0, 0)); drawingSurface.AddVisual(v); v = new DrawingVisual(); DrawSquare(v, new Point(200, 200)); drawingSurface.AddVisual(v); } private void button1_Click(object sender, RoutedEventArgs e) { TransformGroup tg = new TransformGroup(); double scale = 1000000; double xCenter = 0; double yCenter = 0; double xOffset = (drawingSurface.ActualHeight / 2.0 - xCenter); double yOffset = (drawingSurface.ActualWidth / 2.0 - yCenter); ScaleTransform st = new ScaleTransform(scale, scale); st.CenterX = xCenter; st.CenterY = yCenter; TranslateTransform tt = new TranslateTransform(xOffset, yOffset); tg.Children.Add(st); tg.Children.Add(tt); drawingSurface.RenderTransform = st; } private void button2_Click(object sender, RoutedEventArgs e) { TransformGroup tg = new TransformGroup(); double scale = 1000000; double xCenter = 200; double yCenter = 200; double xOffset = (drawingSurface.ActualHeight / 2.0 - xCenter); double yOffset = (drawingSurface.ActualWidth / 2.0 - yCenter); ScaleTransform st = new ScaleTransform(scale, scale); st.CenterX = xCenter; st.CenterY = yCenter; TranslateTransform tt = new TranslateTransform(xOffset, yOffset); tg.Children.Add(st); tg.Children.Add(tt); drawingSurface.RenderTransform = st; } private void button3_Click(object sender, RoutedEventArgs e) { ScaleTransform st = new ScaleTransform(1, 1); drawingSurface.RenderTransform = st; } private void drawingSurface_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Point pointClicked = e.GetPosition(drawingSurface); DrawingVisual visual = drawingSurface.GetVisual(pointClicked); if (visual != null) { // Calculate the top-left corner of the square. // This is done by looking at the current bounds and // removing half the border (pen thickness). // An alternate solution would be to store the top-left // point of every visual in a collection in the // DrawingCanvas, and provide this point when hit testing. Point topLeftCorner = new Point( visual.ContentBounds.TopLeft.X , visual.ContentBounds.TopLeft.Y ); DrawSquare(visual, topLeftCorner); clickOffset = topLeftCorner - pointClicked; isDragging = true; if (selectedVisual != null && selectedVisual != visual) { // The selection has changed. Clear the previous selection. ClearSelection(); } selectedVisual = visual; } } // Rendering the square. private void DrawSquare(DrawingVisual visual, Point topLeftCorner) { using (DrawingContext dc = visual.RenderOpen()) { Brush brush = drawingBrush; dc.DrawRectangle(brush, null, new Rect(topLeftCorner, squareSize)); } } private void drawingSurface_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { isDragging = false; } private void ClearSelection() { Point topLeftCorner = new Point( selectedVisual.ContentBounds.TopLeft.X , selectedVisual.ContentBounds.TopLeft.Y ); DrawSquare(selectedVisual, topLeftCorner); selectedVisual = null; } private void drawingSurface_MouseMove(object sender, MouseEventArgs e) { if (isDragging) { Point pointDragged = e.GetPosition(drawingSurface) + clickOffset; DrawSquare(selectedVisual, pointDragged); } } } } 

DrawingCanvas.cs

 using System; using System.Collections.Generic; using System.Text; using System.Windows.Media; using System.Windows.Controls; using System.Windows; namespace VisualTest { public class DrawingCanvas : Panel { private List<Visual> visuals = new List<Visual>(); protected override Visual GetVisualChild(int index) { return visuals[index]; } protected override int VisualChildrenCount { get { return visuals.Count; } } public void AddVisual(Visual visual) { visuals.Add(visual); base.AddVisualChild(visual); base.AddLogicalChild(visual); } public void DeleteVisual(Visual visual) { visuals.Remove(visual); base.RemoveVisualChild(visual); base.RemoveLogicalChild(visual); } public DrawingVisual GetVisual(Point point) { HitTestResult hitResult = VisualTreeHelper.HitTest(this, point); return hitResult.VisualHit as DrawingVisual; } private List<DrawingVisual> hits = new List<DrawingVisual>(); public List<DrawingVisual> GetVisuals(Geometry region) { hits.Clear(); GeometryHitTestParameters parameters = new GeometryHitTestParameters(region); HitTestResultCallback callback = new HitTestResultCallback(this.HitTestCallback); VisualTreeHelper.HitTest(this, null, callback, parameters); return hits; } private HitTestResultBehavior HitTestCallback(HitTestResult result) { GeometryHitTestResult geometryResult = (GeometryHitTestResult)result; DrawingVisual visual = result.VisualHit as DrawingVisual; if (visual != null && geometryResult.IntersectionDetail == IntersectionDetail.FullyInside) { hits.Add(visual); } return HitTestResultBehavior.Continue; } } } 

It is necessary to rewrite the drag and drop system. The idea is simple, but the implementation is a bit complicated.

+1
source share

All Articles