I have a user control and I'm using ScaleTransform() to implement zoom.
However, in order to keep the center content in the center after the zoom, it is also necessary to scroll. For example, if I zoom in (make things bigger), the X and Y origin should increase so that most of the content does not move down and to the right. (That is, as I zoom in, some of the content should disappear to the left and top.)
Has anyone worked out the calculations of how much to scroll in the X and Y direction in response to a zoom?
For example:
e.Graphics.ScaleTransform(2.0F, 2.0F);
e.Graphics.TranslateTransform(?, ?);
What would be my arguments to TranslateTransform() be so that the center part of the content remains at the center?
Note: I am not displaying an image. I am drawing the graphic content to the surface of my user control.
Or perhaps there's an even easier way?
This should work and I can't imagine any easier way; it assumes you have decided on the center of the zooming. I have chosen to draw centered on the panel:
float zoom = 1f;
private void drawPanel1_Paint(object sender, PaintEventArgs e)
{
Point c = new Point(drawPanel1.ClientSize.Width / 2, drawPanel1.ClientSize.Height / 2);
// a blue sanity check for testing
e.Graphics.FillEllipse(Brushes.DodgerBlue, c.X - 3, c.Y - 3, 6, 6);
// the offsets you were looking for:
float ox = c.X * ( zoom - 1f);
float oy = c.Y * ( zoom - 1f);
// first move and then scale
e.Graphics.TranslateTransform(-ox, -oy);
e.Graphics.ScaleTransform(zoom, zoom);
// now we can draw centered around our point c
Size sz = new Size(300, 400);
int count = 10;
int wx = sz.Width / count;
int wy = sz.Height / count;
for (int i = 0; i < count; i++)
{
Rectangle r = new Rectangle(c.X - i * wx / 2 , c.Y - i * wy / 2, i * wx, i * wy );
e.Graphics.DrawRectangle(Pens.Red, r );
}
}
Note the order of moving and scaling!
I guess you are using some differet interface, but in my case, that's what got the job done (for leaving the mouse in it's original location on the draw after the mouse wheel event):
private void DrawPb_MouseWheel(object sender, MouseEventArgs e)
{
// e contains current mouse location and the wheel direction
int wheelDirection = e.Delta / Math.Abs(e.Delta); // is 'in' or 'out' (1 or -1).
double factor = Math.Exp(wheelDirection * Constants.ZoomFactor); // divide or multiply
double newX = e.X - e.X / factor; // what used to be x is now newX
double newY = e.Y - e.Y / factor; // same for y
Point offset = new Point((int)(-newX), (int)(-newY)); // the offset of the old point to it's new location
Graph.AddOffset(offset); // apply offset
}
Related
I'm attempting to write a matrix transform to convert chart points to device pixels in SkiaSharp. I have it functional as long as I use 0,0 as my minimum chart coordinates but if I need to to step up from a negative number, it causes the drawing to shift left and down. That is to say that the X Axis is shifted to the left off the window and the Y Axis is shift down off the window.
This is intended to be a typical line chart (minimum chart point at the lower left while minimum device point at the upper left). I have accounted for that already in the transform.
While stepping through code I can see that the coordinates returned from the Matrix are not what I expect them to be, so I believe the issue to be with my transform but I haven't been able to pinpoint it.
UPDATE: After further examination, I believe I was mistaken, it is not shifted, it's just not scaling properly to the max end of the screen. There is a bigger margin at the top and right side of the chart than there should be, but the bottom and left side are fine. I've been undable to determine why the scaling doesn't fill the canvas.
Below are my matrix methods:
private SKMatrix ChartToDeviceMatrix, DeviceToChartMatrix;
private void ConfigureTransforms(SKPoint ChartMin,
SKPoint ChartMax, SKPoint DeviceMin, SKPoint DeviceMax)
{
this.ChartToDeviceMatrix = SKMatrix.MakeIdentity();
float xScale = (DeviceMax.X - DeviceMin.X) / (ChartMax.X - ChartMin.X);
float yScale = (DeviceMin.Y - DeviceMax.Y) / (ChartMax.Y - ChartMin.Y);
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, DeviceMin.X, DeviceMax.Y);
this.ChartToDeviceMatrix.TryInvert(out this.DeviceToChartMatrix);
}
// Transform a point from chart to device coordinates.
private SKPoint ChartToDevice(SKPoint point)
{
return this.ChartToDeviceMatrix.MapPoint(point);
}
The code invoking this is:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
float strokeWidth = 1;
float margin = 10;
// SKPaint definitions omitted for brevity.
var ChartMin = new SKPoint(-10, -1); // Works fine if I change this to 0,0
var ChartMax = new SKPoint(110, 11);
var DeviceMin = new SKPoint(margin, margin);
var DeviceMax = new SKPoint(info.Width - margin, info.Height - margin);
const float stepX = 10;
const float stepY = 1;
const float tickX = 0.5;
const float tickY = 0.075F;
// Prepare the transformation matrices.
this.ConfigureTransforms(ChartMin, ChartMax, DeviceMin, DeviceMax);
// Draw the X axis.
var lineStart = new SKPoint(ChartMin.X, 0);
var lineEnd = new SKPoint(ChartMax.X, 0);
canvas.DrawLine(this.ChartToDevice(lineStart), this.ChartToDevice(lineEnd), axisPaint);
// X Axis Tick Marks
for (float x = stepX; x <= ChartMax.X - stepX; x += stepX)
{
var tickMin = new SKPoint(x, -tickY);
var tickMax = new SKPoint(x, tickY);
canvas.DrawLine(this.ChartToDevice(tickMin), this.ChartToDevice(tickMax), axisPaint);
}
// Draw the Y axis.
// The inversion of above, basically the same.
I was able to discover my own problem with enough time. I wasn't calculating the offset correct.
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, DeviceMin.X, DeviceMax.X);
Should have been:
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, -ChartMin.X * xScale + DeviceMin.Y, -ChartMin.Y * yScale + DeviceMax.Y);
Final Matrix method was:
private SKMatrix ChartToDeviceMatrix, DeviceToChartMatrix;
private void ConfigureTransforms(SKPoint ChartMin, SKPoint ChartMax, SKPoint DeviceMin, SKPoint DeviceMax)
{
this.ChartToDeviceMatrix = SKMatrix.MakeIdentity();
float xScale = (DeviceMax.X - DeviceMin.X) / (ChartMax.X - ChartMin.X);
float yScale = (DeviceMin.Y - DeviceMax.Y) / (ChartMax.Y - ChartMin.Y);
float xOffset = -ChartMin.X * xScale + DeviceMin.X;
float yOffset = -ChartMin.Y * yScale + DeviceMax.Y;
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, xOffset, yOffset);
this.ChartToDeviceMatrix.TryInvert(out this.DeviceToChartMatrix);
}
Basically i have a windows form that user will be able to draw different shapes(e.g square, circle and triangle), user will be able to highlight any of these shapes after drawing and then control that highlighted shape by moving or rotating it, i don't know how to rotate the shape. any one can help, this is my code (only to draw a square)
PS: user need to click twice on the form to draw the shape between those 2 points as shown below also i know i should be using onPaint method but this is the requirements of the task
Thanks
public Square(Point keyPt, Point oppPt) // constructor
{
this.keyPt = keyPt;
this.oppPt = oppPt;
}
// You will need a different draw method for each kind of shape. Note the square is drawn
// from first principles. All other shapes should similarly be drawn from first principles.
// Ideally no C# standard library class or method should be used to create, draw or transform a shape
// and instead should utilse user-developed code.
public void draw(Graphics g, Pen blackPen)
{
// This method draws the square by calculating the positions of the other 2 corners
double xDiff, yDiff, xMid, yMid; // range and mid points of x & y
// calculate ranges and mid points
xDiff = oppPt.X - keyPt.X;
yDiff = oppPt.Y - keyPt.Y;
xMid = (oppPt.X + keyPt.X) / 2;
yMid = (oppPt.Y + keyPt.Y) / 2;
// draw square
g.DrawLine(blackPen, (int)keyPt.X, (int)keyPt.Y, (int)(xMid + yDiff / 2), (int)(yMid - xDiff / 2));
g.DrawLine(blackPen, (int)(xMid + yDiff / 2), (int)(yMid - xDiff / 2), (int)oppPt.X, (int)oppPt.Y);
g.DrawLine(blackPen, (int)oppPt.X, (int)oppPt.Y, (int)(xMid - yDiff / 2), (int)(yMid + xDiff / 2));
g.DrawLine(blackPen, (int)(xMid - yDiff / 2), (int)(yMid + xDiff / 2), (int)keyPt.X, (int)keyPt.Y);
}
public void fillSquare(Graphics g, Brush redBrush)
{
float xDiff = oppPt.X - keyPt.X;
float yDiff = oppPt.Y - keyPt.Y;
float xMid = (oppPt.X + keyPt.X) / 2;
float yMid = (oppPt.Y + keyPt.Y) / 2;
var path = new GraphicsPath();
path.AddLines(new PointF[] {
keyPt,
new PointF(xMid + yDiff/2, yMid-xDiff/2),
oppPt
});
path.AddLines(new PointF[] {
keyPt,
new PointF(xMid - yDiff/2, yMid + xDiff/2),
oppPt
});
path.CloseFigure();
// Fill Triangle
g.FillPath(redBrush, path);
}
}
}
i have tried this method but something is missing i don't know what is it
private void itemRotation(PaintEventArgs e)
{
Pen blackpen = new Pen(Color.Black);
Graphics g = e.Graphics;
Font myFont = new System.Drawing.Font("Helvetica", 9);
Brush blackwriter = new SolidBrush(System.Drawing.Color.Black);
if (rotateItem)
{
for (int i = 0; i < shapes.Count; i++)
{
if (shapes[i].Selected)
{
if (shapes[i].ShapeType == (int)ShapeTypes.Square)
{
PointF center = new PointF(shapes[i].keyPt.X + (shapes[i].oppPt.X / 2.0F), shapes[i].keyPt.Y + (shapes[i].oppPt.Y / 2.0F));
shapes[i].keyPt = new Point(shapes[i].keyPt.X, shapes[i].keyPt.Y);
shapes[i].oppPt = new Point(shapes[i].oppPt.X, shapes[i].oppPt.Y);
Matrix myMatrix = new Matrix();
myMatrix.Rotate(30);
g.Transform = myMatrix;
((Square)shapes[i]).draw(g, blackpen);
g.DrawString("2nd pos", myFont, blackwriter, shapes[i].keyPt.X, shapes[i].oppPt.X);
}
}
}
}
}
Below is an example of how to draw the same shape (a GraphicsPath) into various locations and rotations.
The key here is the following two commands
e.Graphics.TranslateTransform(x, y);
e.Graphics.RotateTransform(-angle);
See results below:
and the code used to generate it:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
// This code defines a graphics shape using a GraphicsPath
// and draws multiple copies along a grid and with various
// rotation angle angles.
e.Graphics.SmoothingMode=SmoothingMode.AntiAlias;
var target = sender as PictureBox;
// Step 1 - Define a rectangle 20 by 12 pixels, center at origin.
var gp = new GraphicsPath();
gp.AddLines(new PointF[] {
new PointF(-10, -6),
new PointF( 10, -6),
new PointF( 10, 6),
new PointF(-10, 6) });
gp.CloseFigure();
// Step 2 - Define a 10×9 grid with two loops
float angle = 0;
for (int i = 0; i<9; i++)
{
// divide the control height into 10 divisions
float y = (i+1)*target.Height/10;
for (int j = 0; j<10; j++)
{
// divide the control width into 11 divisions
float x = (j+1)*target.Width/11;
// Save the default transformation state
var state = e.Graphics.Save();
// Traslate the origin to (x,y), each grid point
e.Graphics.TranslateTransform(x, y);
// Rotate shape by an angle (negative = CCW)
e.Graphics.RotateTransform(-angle);
// Draw the shape
e.Graphics.FillPath(Brushes.LightSeaGreen, gp);
e.Graphics.DrawPath(Pens.Black, gp);
// Restore the default transformation state
e.Graphics.Restore(state);
// Increment the angle by one degree.
// The idea is to show all 90 degrees of rotation in a 10×9 grid.
angle++;
}
}
}
At the moment I develope a ChartControl and it works just pretty well in my opinion,
but now I'm at a point where it would be nice to have the ability to zoom the drawed signal for better analyzing.
At the moment I calculate the needed points like this:
for (int i = 0; i < PointsCount; i++){
xAxisPoint = xAxisOP.X + i * (xAxisWidth / PointsCount);
yAxisPoint = yAxisHeight * data[i].Point / Divisor;
if(yAxisPoint > yAxisHeight){
yAxisPoint = yAxisHeight;
}
if(yAxisPoint < -yAxisHeight){
yAxisPoint = -yAxisHeight;
}
Points[i] = new PointF(xAxisPoint, yAxisOP.Y + yAxisPoint);
}
if(zoom){
graphics.ScaleTransform(0.2f*ZoomFactor, 0.2f*ZoomFactor);
}
using (Pen plotPen = new Pen(plotColor, 1)){
graphics.DrawLines(plotPen, Points);
}
But the problem is: When it zooms in, the zoom is way too big and is drawn outside the bounds of my control.
Is there a way to specify an area in which it should be Scaled (zoomed)?
For the final question: Is there a way to specify an area in which it should be scaled/zoomed? you need a combination of SetClip, TranslateTransform and ScaleTransform.
Here is an example.
It uses a
target rectangle zoomTgtArea where the zoomed graphics are displayed,
a mouse location zoomOrigin where the zoom origin is,
a float zoomFactor, a positive float.
Initial values:
Rectangle zoomTgtArea = new Rectangle(300, 500, 200, 200);
Point zoomOrigin = Point.Empty; // updated in MouseMove when button is pressed
float zoomFactor = 2f;
The trick to zoom in on only a part of the graphics is to display the graphics twice, once normally and once with the transformations of the Graphics object.
Let's try:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// normal drawing
DrawStuff(e.Graphics);
// for the movable zoom we want a small correction
Rectangle cr = pictureBox.ClientRectangle;
float pcw = cr.Width / (cr.Width - ZoomTgtArea.Width / 2f) ;
float pch = cr.Height / (cr.Height - ZoomTgtArea.Height / 2f) ;
// now we prepare the graphics object; note: order matters!
e.Graphics.SetClip(zoomTgtArea );
// we can either follow the mouse or keep the output area fixed:
if (cbx_fixed.Checked)
e.Graphics.TranslateTransform( ZoomTgtArea.X - zoomCenter.X * zoomFactor,
ZoomTgtArea.Y - zoomCenter.Y * zoomFactor);
else
e.Graphics.TranslateTransform( - zoomCenter.X * zoomFactor * pcw,
- zoomCenter.Y * zoomFactor * pch);
// finally zoom
e.Graphics.ScaleTransform(zoomFactor, zoomFactor);
// and display zoomed
DrawStuff(e.Graphics);
}
The DrawStuff I used is simple:
void DrawStuff(Graphics g)
{
bool isZoomed = g.Transform.Elements[0]!= 1
|| g.Transform.OffsetX != 0 | g.Transform.OffsetY != 0;
if (isZoomed) g.Clear(Color.Gainsboro); // pick your back color
// all your drawing here!
Rectangle r = new Rectangle(10, 10, 500, 800); // some size
using (Font f = new Font("Tahoma", 11f))
g.DrawString(text, f, Brushes.DarkSlateBlue, r);
}
Its only extra is clearing the background so the normal drawing won't shine through the zoomed version..
Let's see:
i i am relative new to c# and is trying to draw a curved line in c#. I would like to ask that is there any possible way to create an X and Y axis in order to show the coordinates of each point of the curved line.
Please do help me on this matter as i am stuck on how to execute it.
protected override void OnPaint(PaintEventArgs e)
{
float a = 1, b = 5, c = 1;
double x1, x2, x3,x4,x5,x6, y1, y2, y3,y4,y5, delta;
delta = (b * b) - (4 * a * c);
x1=0;
y1 = a * (x1 * x1) + (b * (x1)) + c;
x2 = 3;
y2 = a * (x2 * x2) + (b * (x2)) + c;
x3 = - 3;
y3 = a * (x3 * x3) + (b * (x3)) + c;
x4 = 5;
y4 = a * (x4 * x4) + (b * (x4)) + c;
x5 = -10;
y5 = a * (x5 * x5) + (b * (x5)) + c;
int cx1 = Convert.ToInt32(x1);
int cx2 = Convert.ToInt32(x2);
int cx3 = Convert.ToInt32(x3);
int cy1 = Convert.ToInt32(y1);
int cy2 = Convert.ToInt32(y2);
int cy3 = Convert.ToInt32(y3);
int cx4 = Convert.ToInt32(x4);
int cy4 = Convert.ToInt32(y4);
int cx5 = Convert.ToInt32(x5);
int cy5 = Convert.ToInt32(y5);
Graphics g = e.Graphics;
int deltaX = 100;
int deltaY = 100;
g.TranslateTransform(deltaX, deltaY);
float factor = 2.5f;
Matrix m = new Matrix();
m.Scale(factor, factor);
g.MultiplyTransform(m);
Pen aPen = new Pen(Color.Blue, 1);
Point point1 = new Point(cx1, cy1);
Point point2 = new Point(cx2, cy2);
Point point3 = new Point(cx3, cy3);
Point point4 = new Point(cx4, cy4);
Point point5 = new Point(cx5, cy5);
Point[] Points = { point5, point3, point1,point2,point4 };
g.DrawCurve(aPen, Points);
Maybe I misunderstand you, but it sounds like you want to make your GDI+ graphics scale with the window size (i.e. you want to scale the X and Y axis with the size of the window), right?
This is pretty simple, you just have to decide how big of a space you want to present in the window -- i.e. if you want to make the axis go from 0,0 on the top left, to 512x512 on the bottom right, then you would just need to scale the X axis by a factor of 512/width, and the Y axis by a factor of 512/height.
So you would do that by performing a ScaleTransform on your Graphics object. You'll need to use your Form's ClientSize to get the width and height. (The regular Form's .Width, and .Height properties, include all the borders and title bars, padding pixels, etc. -- so it's no good for this calculation.)
Then you will need to force an Invalidation during the form's Resize event (it will work without this, when you make the window smaller, but when you make it bigger, this will be required, or else it will only redraw the edges).
Another thing worth considering is turning on the form's DoubleBuffered property, the redraw will be much smoother.
So, let's assume you want to work in a virtual space of 512x512 "pixels" where 0 ,0 is always the top left, and 512,512 is the bottom right. You could add this code to the top of your OnPaint event handler:
float scaleX = 512f / ((float)this.ClientSize.Width);
float scaleY = 512f / ((float)this.ClientSize.Height);
e.Graphics.ScaleTransform(scaleX, scaleY);
Then add a handler for the Form's Resize event and add something like this:
this.Invalidate(true);
That should do the trick.
I need the precise position of my mouse pointer over a PictureBox.
I use the MouseMove event of the PictureBox.
On this PictureBox, I use the "zoom" property to show an image.
What is the correct way for getting the position of the mouse on the original (unzoomed) image?
Is there a way to find the scale factor and use it?
I think need to use imageOriginalSize/imageShowedSize to retrieve this scale factor.
I use this function:
float scaleFactorX = mypic.ClientSize.Width / mypic.Image.Size.Width;
float scaleFactorY = mypic.ClientSize.Height / mypic.Image.Size.Height;
Is possible to use this value to get the correct position of the cursor over the image?
I had to solve this same problem today. I wanted it to work for images of any width:height ratio.
Here's my method to find the point 'unscaled_p' on the original full-sized image.
Point p = pictureBox1.PointToClient(Cursor.Position);
Point unscaled_p = new Point();
// image and container dimensions
int w_i = pictureBox1.Image.Width;
int h_i = pictureBox1.Image.Height;
int w_c = pictureBox1.Width;
int h_c = pictureBox1.Height;
The first trick is to determine if the image is a horizontally or vertically larger relative to the container, so you'll know which image dimension fills the container completely.
float imageRatio = w_i / (float)h_i; // image W:H ratio
float containerRatio = w_c / (float)h_c; // container W:H ratio
if (imageRatio >= containerRatio)
{
// horizontal image
float scaleFactor = w_c / (float)w_i;
float scaledHeight = h_i * scaleFactor;
// calculate gap between top of container and top of image
float filler = Math.Abs(h_c - scaledHeight) / 2;
unscaled_p.X = (int)(p.X / scaleFactor);
unscaled_p.Y = (int)((p.Y - filler) / scaleFactor);
}
else
{
// vertical image
float scaleFactor = h_c / (float)h_i;
float scaledWidth = w_i * scaleFactor;
float filler = Math.Abs(w_c - scaledWidth) / 2;
unscaled_p.X = (int)((p.X - filler) / scaleFactor);
unscaled_p.Y = (int)(p.Y / scaleFactor);
}
return unscaled_p;
Note that because Zoom centers the image, the 'filler' length has to be factored in to determine the dimension that is not filled by the image. The result, 'unscaled_p', is the point on the unscaled image that 'p' correlates to.
Hope that helps!
If I have understood you correctly I believe you would want to do something of this nature:
Assumption: the PictureBox fits to the image width/height, there is no space between the border of the PictureBox and the actual image.
ratioX = e.X / pictureBox.ClientSize.Width;
ratioY = e.Y / pictureBox.ClientSize.Height;
imageX = image.Width * ratioX;
imageY = image.Height * ratioY;
this should give you the points ot the pixel in the original image.
Here is a simple function to solve this:
private Point RemapCursorPosOnZoomedImage(PictureBox pictureBox, int x, int y, out bool isInImage)
{
// original size of image in pixel
float imgSizeX = pictureBox.Image.Width;
float imgSizeY = pictureBox.Image.Height;
// current size of picturebox (without border)
float cSizeX = pictureBox.ClientSize.Width;
float cSizeY = pictureBox.ClientSize.Height;
// calculate scale factor for both sides
float facX = (cSizeX / imgSizeX);
float facY = (cSizeY / imgSizeY);
// use smaller one to fit picturebox zoom layout
float factor = Math.Min(facX, facY);
// calculate current size of the displayed image
float rSizeX = imgSizeX * factor;
float rSizeY = imgSizeY * factor;
// calculate offsets because image is centered
float startPosX = (cSizeX - rSizeX) / 2;
float startPosY = (cSizeY - rSizeY) / 2;
float endPosX = startPosX + rSizeX;
float endPosY = startPosY + rSizeY;
// check if cursor hovers image
isInImage = true;
if (x < startPosX || x > endPosX) isInImage = false;
if (y < startPosY || y > endPosY) isInImage = false;
// remap cursor position
float cPosX = ((float)x - startPosX) / factor;
float cPosY = ((float)y - startPosY) / factor;
// create new point with mapped coords
return new Point((int)cPosX, (int)cPosY);
}