I'm writing a WPF application that displays a XAML object (it's basically a map drawn in XAML). As part of its features, it should zoom in/out and pan. The panning works fine, and the zoom zooms, but I can't quite understand how to zoom to a specific point, like my mouse cursor, for example.
This is my current code:
internal void PerformZoom(float ZoomFactor, Point ZoomCenterPoint)
{
m_originalTransform = m_canvas.RenderTransform;
float newZoomFactor = m_oldZoomFactor + ZoomFactor;
float scaleToApply = (newZoomFactor / m_oldZoomFactor);
m_totalZoom = newZoomFactor;
var st = new ScaleTransform(scaleToApply, scaleToApply);
TransformGroup tg = new TransformGroup();
tg.Children.Add(m_originalTransform);
tg.Children.Add(st);
m_canvas.RenderTransform = tg;
m_oldZoomFactor = newZoomFactor;
}
[edit] Found the solution - Just edited the CenterX / CenterY properties of the transformation and it worked like a charm.
Thanks for all your help!
[edit2] Here's a viable solution (considering the mouse position):
public partial class MainWindow
{
private float currentZoom = 1f;
private const float StepSize = .2f;
public MainWindow()
{
InitializeComponent();
}
private void MainGrid_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var pos = 1;
if (e.Delta < 0)
{
pos = -1;
}
var mousePosition = e.MouseDevice.GetPosition(MainGrid);
currentZoom += StepSize * pos;
var transform = new ScaleTransform(currentZoom, currentZoom, mousePosition.X, mousePosition.Y);
MainGrid.RenderTransform = transform;
}
}
You will have to compose your ScaleTransform with a TranslateTransform which translates your component while zooming.
The offset given by the TranslateTransform depends of the behavior you wanna have (i.e. center on mouse, center on screens center...)
I wrote in the past a behavior which you can attach to a component : It makes it zoomable (centered on mouse, reacting to mousewheel)
It's pretty dirty and not sure to be efficient (i no longer use it)... and comments are in french :-/
see the source
[edit] In fact, I remember it was to scroll and scale a Panels background. But it shouldnt be so hard to modify for applying it to any object as the transformations are the same for images and elements.
Related
This question is asked before but since it doesn't work and my lack of reputation point(I tried to comment at question but I couldn't) I had to ask this question again.
This is the link of the quustion asked before;
How to zoom at a point in picturebox
I used the code which is shown in the link but when I run it the point or shape disappear.
here is my code;
public partial class Form1 : Form
{
private Matrix transform = new Matrix();
private double m_dZoomscale = 1.0;
public static double s_dScrollValue = .1;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Transform = transform;
Pen mypen = new Pen(Color.Red,5);
Rectangle rect = new Rectangle(10, 10, 30, 30);
e.Graphics.DrawRectangle(mypen, rect);
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
pictureBox1.Focus();
if (pictureBox1.Focused == true && mea.Delta != 0)
{
ZoomScroll(mea.Location, mea.Delta > 0);
}
}
private void ZoomScroll(Point location, bool zoomIn)
{
transform.Translate(-location.X, -location.Y);
if (zoomIn)
transform.Scale((float)s_dScrollValue, (float)s_dScrollValue);
else
transform.Scale((float)-s_dScrollValue, (float)-s_dScrollValue);
transform.Translate(location.X, location.Y);
pictureBox1.Invalidate();
}
The answer you are referencing cannot possibly work. I have no idea why it was accepted, nor up-voted. Except that at some time in the past, I apparently up-voted it as well. I don't know what I was thinking.
Anyway, that code has some problems:
It uses the mouse coordinates passed in directly, rather than converting them to the coordinate system for the PictureBox control. The coordinates passed to the OnMouseWheel() method are relative to the Form itself, so only if the PictureBox top-left coincides with the Form's upper-left corner would that work.
More problematically, the code is completely misusing the Matrix.Scale() method, passing a value that seems intended to be a delta for the scale, when in fact the Scale() method accepts a factor for the scale. This has two implications:
Passing a negative value is wrong, because negative values flip the coordinate system, rather than reducing the scale, and
Passing an increment value is wrong, because the value passed will be multiplied with the current scaling to get the new scaling.
Also problematic is that the code applies the matrix transformations in the wrong order, because the default order is "prepend", not "append" (I find the latter more natural to work with, but I assume there's some reason known to those who specialize in matrix math that explains why the default is the former).
There is also the relatively minor issue that, even ignoring the above, allowing the user to adjust the scale factor arbitrarily will eventually lead to an out-of-range value. It would be better for the code to limit the scale to something reasonable.
Here is a version of your code, modified so that it addresses all of these issues:
private Matrix transform = new Matrix();
private float m_dZoomscale = 1.0f;
public const float s_dScrollValue = 0.1f;
public Form1()
{
InitializeComponent();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Transform = transform;
Pen mypen = new Pen(Color.Red, 5);
Rectangle rect = new Rectangle(10, 10, 30, 30);
e.Graphics.DrawRectangle(mypen, rect);
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
pictureBox1.Focus();
if (pictureBox1.Focused == true && mea.Delta != 0)
{
// Map the Form-centric mouse location to the PictureBox client coordinate system
Point pictureBoxPoint = pictureBox1.PointToClient(this.PointToScreen(mea.Location));
ZoomScroll(pictureBoxPoint, mea.Delta > 0);
}
}
private void ZoomScroll(Point location, bool zoomIn)
{
// Figure out what the new scale will be. Ensure the scale factor remains between
// 1% and 1000%
float newScale = Math.Min(Math.Max(m_dZoomscale + (zoomIn ? s_dScrollValue : -s_dScrollValue), 0.1f), 10);
if (newScale != m_dZoomscale)
{
float adjust = newScale / m_dZoomscale;
m_dZoomscale = newScale;
// Translate mouse point to origin
transform.Translate(-location.X, -location.Y, MatrixOrder.Append);
// Scale view
transform.Scale(adjust, adjust, MatrixOrder.Append);
// Translate origin back to original mouse point.
transform.Translate(location.X, location.Y, MatrixOrder.Append);
pictureBox1.Invalidate();
}
}
With this code, you will find that no matter where you place the mouse before adjusting the mouse wheel, the rendered image will scale while keeping the point under the mouse fixed in place.
Note:
I took a look at some of the similar questions on Stack Overflow, and there are a few that might also be useful to you. Some of the answers overcomplicate things, in my opinion, but all should work. See:
Zoom To Point Not Working As Expected
Zoom in on a fixed point using matrices
Zooming graphics without scrolling
I've seen few questions about this problem, I tried every solution but none of them worked for my case.
My code is working; this image shows what happens when I click on Draw button.
I need to zoom on that drawing.Is it possible to code something like autocad feature "zoom/extent"?
Pen myPen = new Pen(Color.Black);
int centerpointx, centerpointy;
private void pictureBoxDraw_Paint(object sender, PaintEventArgs e)
{
centerpointx = pictureBoxDraw.Size.Width/2;
centerpointy = pictureBoxDraw.Size.Height/2;
myPen.Width = 2;
if (binary > 0)
{
var sizecrestgeo = 40;
var distancearraycrestgeo = new float[sizecrestgeo];
var elevationarraycrestgeo = new float[sizecrestgeo];
for (int i = 0; i < sizecrestgeo; i++)
{
distancearraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[0].Value.ToString());
elevationarraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[1].Value.ToString())*-1;
}
for (int i=0; i < sizecrestgeo-1; i++)
{
e.Graphics.DrawLine(myPen, distancearraycrestgeo[i]+centerpointx, elevationarraycrestgeo[i]+centerpointy, distancearraycrestgeo[i + 1]+centerpointx, elevationarraycrestgeo[i + 1]+centerpointy);
}
}
else
{
}
}
private void buttonDraw_Click_1(object sender, EventArgs e)
{
if (Hd > 0.0001)
{
binary = 1;
pictureBoxDraw.Invalidate();
}
else
{
MessageBox.Show("No data to draw, perform analysis first.");
}
}
private void buttoncleardraw_Click(object sender, EventArgs e)
{
binary = 0;
pictureBoxDraw.Invalidate();
}
}
This is not so hard, provided you know all the puzzle pieces.
Let's start with the obvious one:
You can scale the Graphics object to create zoomed graphics with ScaleTransform.
As I mentioned, this will include the widths of pens, font sizes and also any images you draw (though not the hatches of a HatchBrush).
You also asked about keeping the drawing 'centered'. This is a non-obvious concept: Just what is the center of your drawing surface??
When zooming (just like rotating) you always need to know the center point of the zoom (or the rotation.) By default this is the origin (0,0). I chose the center of the Panel. You may want to pick some other point..
Once you do you can move the origin of the graphics viewport to this point with TranslateTransform.
Once you have achieved all this you almost certainly will want to allow scrolling.
To do so you have two options:
You can keep AutoScroll = false and nest the canvas control inside another control, usually a Panel, which has AutoScroll = true; next make the canvas control big enough to always hold your drawing and you're done.
Or you can turn on AutoScroll for the canvas control and also set a large enough AutoScrollMinSize. If you then add the current scrolling position to the translation you are also done. Let's see this solution in action:
This is the code in the Paint event:
Size sz = panel3.ClientSize;
Point center = new Point(sz.Width / 2, sz.Height / 2);
Graphics g = e.Graphics;
// center point for testing only!
g.DrawEllipse(Pens.Orange, center.X - 3, center.Y - 3, 6, 6);
// you determine the value of the zooming!
float zoom = (trackBar1.Value+1) / 3f;
// move the scrolled center to the origon
g.TranslateTransform(center.X + panel3.AutoScrollPosition.X,
center.Y + panel3.AutoScrollPosition.Y);
// scale the graphics
g.ScaleTransform(zoom, zoom);
// draw some stuff..
using(Pen pen = new Pen(Color.Yellow, 0.1f))
for (int i = -100; i < 100; i+= 10)
g.DrawEllipse(Pens.Yellow, i-22,i-22,44,44);
A few notes:
I draw an orange circle in the center to show this point is invariant.
My coordinates go from the negative to the positive so you can see that this works nicely.
I draw with a tiny pen width; so the width of the drawing only changes once the resulting pen goes over 1 pixel. Anything draw will always be draw with 1 pxiel width, though.
I first translate and then scale so I don't have to calculate scaled poitions.
The only line in the TrackBar's Scroll event is to trigger the Paint event: panel3.Invalidate();
The only settings needed for the Panel are
panel3.AutoScroll = true;
panel3.AutoScrollMinSize = new Size(500, 500); // use the size you want to allow!
However to avoid flicker it is highly recommended to use a DoubleBuffered control, maybe a Panel subclass like this:
class DrawPanel : Panel
{
public DrawPanel() { DoubleBuffered = true; }
}
Update: Instead of a Panel, which is a Container control and not really meant to draw onto you can use a Picturebox or a Label (with Autosize=false); both have the DoubleBuffered property turned on out of the box and support drawing better than Panels do.
Graphics.ScaleTransform() is how you can zoom. Try using something like this inside your paint event handler:
e.Graphics.ScaleTransform(2.0F, 2.0F);
I've created one helper app to demonstrate my problem.
I've a rectangle which is filled with an image brush, this brush can be transformed within the rectangle using gesture manipulations.
I'm determining the position of top left corner of the image from the top left corner of the rectangle. I'm getting correct values while (only) translating the image but getting wrong values while using pinch gestures. If you zoom in too much and translate the image, then brush moves in opposite direction.
Below is how you can reproduce my problem with the helper app attached below:
Run the app, get the top left corners of both image and rectangle together by just moving(without pinching) the image until you get the position value as (0,0).
Next Pinch and move the image and get back the top left corners together, now you can see that value is not (0,0).
Download here
Here is my Manipulation Delta Event:
public virtual void Brush_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (e.PinchManipulation != null)
{
// Rotate
_currentAngle = previousAngle + AngleOf(e.PinchManipulation.Original) - AngleOf(e.PinchManipulation.Current);
// Scale
_currentScale *= e.PinchManipulation.DeltaScale;
// Translate according to pinch center
double deltaX = (e.PinchManipulation.Current.SecondaryContact.X + e.PinchManipulation.Current.PrimaryContact.X) / 2 -
(e.PinchManipulation.Original.SecondaryContact.X + e.PinchManipulation.Original.PrimaryContact.X) / 2;
double deltaY = (e.PinchManipulation.Current.SecondaryContact.Y + e.PinchManipulation.Current.PrimaryContact.Y) / 2 -
(e.PinchManipulation.Original.SecondaryContact.Y + e.PinchManipulation.Original.PrimaryContact.Y) / 2;
_currentPos.X = previousPos.X + deltaX;
_currentPos.Y = previousPos.Y + deltaY;
}
else
{
// Translate
previousAngle = _currentAngle;
_currentPos.X += e.DeltaManipulation.Translation.X;
_currentPos.Y += e.DeltaManipulation.Translation.Y;
previousPos.X = _currentPos.X;
previousPos.Y = _currentPos.Y;
}
e.Handled = true;
ProcesstTransform();
}
void ProcesstTransform()
{
CompositeTransform gestureTransform = new CompositeTransform();
gestureTransform.CenterX = _currentPos.X;
gestureTransform.CenterY = _currentPos.Y;
gestureTransform.TranslateX = _currentPos.X - outputSize.Width / 2.0;
gestureTransform.TranslateY = _currentPos.Y - outputSize.Height / 2.0;
gestureTransform.Rotation = _currentAngle;
gestureTransform.ScaleX = gestureTransform.ScaleY = _currentScale;
brush.Transform = gestureTransform;
}
First, find the location of the initial upper left corner relative to the center of the transform. This is pretty much straight subtraction. These can be pre-calculated since the before-transformation frame won't change. You do NOT want to pre-scale _brushSize by multiplying in _scale. That will end up scaling the brush twice.
Point origCentre = new Point(ManipulationArea.ActualWidth / 2, ManipulationArea.ActualHeight / 2);
Point origCorner = new Point(origCentre.X - _brushSize.Width / 2, origCentre.Y - _brushSize.Height /2);
Then apply the gestureTransform to the corner point:
Point transCorner = gestureTransform.Transform(origCorner);
XValue.Text = transCorner.X.ToString();
YValue.Text = transCorner.Y.ToString();
This will get things pretty close to accurate, barring some rounding errors and some weirdness from the way the translation is tracked both by changing the position and then by applying the transform. Typically you would only do the latter. I'll leave tracking that down as an exercise for the reader :)
Rob Caplan from Microsoft helped me to solve this issue.
Description
I am creating a "simulator" for different resolutions. Basically I have a control inside a panel. Then the user can choose an aspect ratio (more information below) and the control inside the simulator should get resized to match the desired ratio.
Problem
The problem is that I do not know what is a good way to calculate the size of the control without having to use a lot of CPU or trying or resizing. How can I know, if I have to resize the height or the width of the control to fit inside the simulator?
The simulator can grow or shrink. That is why I can not be sure, if the control inside of it will fill up the whole width of the simulator, or the height, but the aspect ratio should be always correct.
Hint
This is probably a very easy mathematical problem. I just don't find a solution right now (today is not my day)!
If you have any ideas or suggestions, feel free to ask and propose!
Example
Available Resolutions
3:2 (Like on the iPhone until iPhone 5; Anyone knows an other name?)
16:10 (WXGA)
16:9 (Widescreen)
4:3 (VGA)
Thanks for helping!
Here is a little testbed. Put a control on a form and assign the ViewPort variable to it. Here I chose a textBox1 but any control will do. Then pick a target aspect ratio, I chose TargetRatio_3_2.
Try it by resizing the form!
Note 1: I chose a form as the Container (your Simulator) for easier testing and therefore use its ClientRectangle to access its inner measures. Any other Control will basically work the same.
Not 2: I have added a few lines to place the ViewPort (your Control) in the center.
private void Form1_Resize(object sender, EventArgs e)
{
Control Container = this;
Control ViewPort = textBox1;
float ContainerRatio = 1f * Container.ClientRectangle.Width / Container.ClientRectangle.Height;
const float TargetRatio_3_2 = 3f / 2f;
const float TargetRatio_16_9 = 16f / 9f;
const float TargetRatio_4_3 = 4f / 3f;
const float TargetRatio_16_10 = 16f / 10f;
//..
float TargetRatio = TargetRatio_3_2;
if (ContainerRatio < TargetRatio)
{
ViewPort.Width = Container.ClientRectangle.Width;
ViewPort.Height = (int)(ViewPort.Width / TargetRatio);
ViewPort.Top = (Container.ClientRectangle.Height - ViewPort.Height) / 2;
ViewPort.Left = 0;
}
else
{
ViewPort.Height = Container.ClientRectangle.Height;
ViewPort.Width = (int)(ViewPort.Height * TargetRatio);
ViewPort.Top = 0;
ViewPort.Left = (Container.ClientRectangle.Width - ViewPort.Width) / 2;
}
}
I am new to WP7 / Silverlight / C# programming and I'm currently putting together a simple magnifying app to get to grips with a few things. In order to provide 'zoom in' and 'zoom out' functionality I successfully managed to use a ScaleTransform on the UIElement which contains the video feed.
As I have developed the app further I realised that I want to accomodate the orientation changing during the use of the app and ensure that the video feed responds accordingly (The app is fixed in landscape mode but a user may want to 'turn the phone over' for some reason). To address this I created and applied a RotateTransform and linked into the OrientationChanged event.
All of this works fine but when I try to zoom (using the ScaleTransform) after I have changed the orientation then the video feed zooms but flips the feed back to the 'default' orientation and therefore show upside down.
Given this I figured I needed to apply both transforms when zooming in, so I created a TransformGroup and added the ScaleTransform and RotateTransform to it. However the zoom function will not work with this and none of the transform's appear to be applied.
Has anybody else encountered problems when trying to apply a TransformGroup?
I've included a snippet of the zoom in and orientation code below for reference - apologies in advance if I am doing things the long way round but bear in mind I'm still learning.
Any help would be appreciated.
Thanks in advance,
Craig.
UIElement videocontainer;
RotateTransform rotatetransform = new RotateTransform();
void MainPage_OrientationChanged(object sender, OrientationChangedEventArgs e)
{
switch (Orientation)
{
case PageOrientation.LandscapeLeft:
rotatetransform.Angle = 0;
break;
default:
rotatetransform.Angle = 180;
break;
}
rotatetransform.CenterX = 320;
rotatetransform.CenterY = 240;
videocontainer.RenderTransform = rotatetransform;
private void ZoomIn_Click(object sender, EventArgs e)
{
if (zoom < 7)
{
switch (Orientation)
{
case PageOrientation.LandscapeLeft:
ScaleTransform myscaletransform1 = new ScaleTransform();
myscaletransform1.ScaleX = myscaletransform1.ScaleX * 1.25;
myscaletransform1.ScaleY = myscaletransform1.ScaleY * 1.25;
myscaletransform1.CenterX = 320;
myscaletransform1.CenterY = 240;
TransformGroup zoomintranformgroup1 = new TransformGroup();
zoomintranformgroup1.Children.Add(myscaletransform1);
zoomintranformgroup1.Children.Add(rotatetransform);
videocontainer.RenderTransform = zoomintransformgroup1;
zoom++;
break;
default:
ScaleTransform myscaletransform2 = new ScaleTransform();
myscaletransform2.ScaleX = myscaletransform2.ScaleX * 1.25;
myscaletransform2.ScaleY = myscaletransform2.ScaleY * 1.25;
myscaletransform2.CenterX = 320;
myscaletransform2.CenterY = 240;
TransformGroup zoomintranformgroup2 = new TransformGroup();
zoomintranformgroup2.Children.Add(myscaletransform2);
zoomintranformgroup2.Children.Add(rotatetransform);
videocontainer.RenderTransform = zoomintransformgroup2;
zoom++;
break;
}
}
else
{
return;
}
}
You can use the CompositeTransform class. This class combines all your used transformations and makes things easier.