I am currently having trouble when rotating an image in WPF, using RotateTransform and LayoutTransform. When an image, that has a pixel height size greater than the monitors height and is rotated at 90º or 270º, the window size will be higher than the monitors screen resolution size.
Example screenshots:
Application running with image at 90º
Application running with image at 0º
I am using the code below (simplified), with mainWindow.img being a System.Windows.Control.Image:
static void Rotate(int degrees)
{
var rt = new RotateTransform { Angle = degrees };
mainWindow.img.LayoutTransform = rt;
}
It is for a a picture viewer project, the full source code is available at https://github.com/Ruben2776/PicView
I have tried shifting the Width and Height values of the image, but it produces an undesired result (skewed proportion).
The sizing calculation, for the image size, is made based on the user's screen height, using the following trimmed code:
int interfaceHeight = 90;
double maxWidth = Math.Min(MonitorInfo.Width, width);
double maxHeight = Math.Min((MonitorInfo.Height - interfaceHeight), height);
double AspectRatio = Math.Min((maxWidth / width), (maxHeight / height));
mainWindow.img.Width = (width * AspectRatio);
mainWindow.img.Height = (height * AspectRatio);
with height and width being the image's dimensions and MonitorInfo being a class that retrieves the current monitors resolution.
Update
Below is the minimal code for a sample WPF app illustrating the issue:
MainWindow.xaml
<Window x:Class="RotateTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
SizeToContent="WidthAndHeight"
Title="MainWindow" >
<Grid>
<Image x:Name="img" Stretch="Fill" Source="https://w.wallhaven.cc/full/nk/wallhaven-nkrwz1.jpg"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace RotateTest
{
public partial class MainWindow : Window
{
int Degrees;
public MainWindow()
{
InitializeComponent();
ContentRendered += MainWindow_ContentRendered;
KeyDown += MainWindow_KeyDown;
}
private void MainWindow_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
Rotate(true);
break;
case Key.Down:
Rotate(false);
break;
}
}
private void MainWindow_ContentRendered(object sender, EventArgs e)
{
int interfaceHeight = 90;
double maxWidth = Math.Min(SystemParameters.PrimaryScreenWidth, img.Source.Width);
double maxHeight = Math.Min(SystemParameters.PrimaryScreenHeight - interfaceHeight, img.Source.Height);
double AspectRatio = Math.Min((maxWidth / img.Source.Width), (maxHeight / img.Source.Height));
img.Width = (img.Source.Width * AspectRatio);
img.Height = (img.Source.Height * AspectRatio);
}
void Rotate(int degrees)
{
var rt = new RotateTransform { Angle = Degrees = degrees };
img.LayoutTransform = rt;
}
void Rotate(bool right)
{
switch (Degrees)
{
case 0:
if (right)
{
Rotate(270);
}
else
{
Rotate(90);
}
break;
case 90:
if (right)
{
Rotate(0);
}
else
{
Rotate(180);
}
break;
case 180:
if (right)
{
Rotate(90);
}
else
{
Rotate(270);
}
break;
case 270:
if (right)
{
Rotate(180);
}
else
{
Rotate(0);
}
break;
}
}
}
}
The root of the problem is that you are only calculating your scaling ratios based on the the image's size when it's not rotated. Once you rotate the image, img.ActualHeight effectively becomes its width and img.ActualWidth effectively becomes its height, and your calculation from when the image was un-rotated is no longer correct.
Here are the changes and additions I made to your code:
private double normalRatio;
private double rotatedRatio;
private void MainWindow_ContentRendered(object sender, EventArgs e)
{
double interfaceHeight = this.ActualHeight - img.ActualHeight;
normalRatio = Math.Min(SystemParameters.WorkArea.Width / img.Source.Width, (SystemParameters.WorkArea.Height - interfaceHeight) / img.Source.Height);
rotatedRatio = Math.Min(SystemParameters.WorkArea.Width / img.Source.Height, (SystemParameters.WorkArea.Height - interfaceHeight) / img.Source.Width);
ScaleImage();
}
private void ScaleImage()
{
double ratio = Degrees == 0 || Degrees == 180 ? normalRatio : rotatedRatio;
img.Width = (img.Source.Width * ratio);
img.Height = (img.Source.Height * ratio);
}
void Rotate(bool right)
{
if (right)
{
Degrees -= 90;
if (Degrees < 0) { Degrees += 360; }
}
else
{
Degrees += 90;
if (Degrees >= 360) { Degrees -= 360; }
}
ScaleImage();
Rotate(Degrees);
}
//I left the other methods, including Rotate(int degrees), the same as in your question
Here's an explanation of what I changed:
interfaceHeight is calculated by subtracting the height of the image from the height of the window, the difference being the aggrigate size of everything else.
Instead of using MonitorInfo, I'm using SystemParameters.WorkArea, because it takes into account the size and placement of the Windows taskbar.
I calculate two scale ratios: normalRatio, for when the image is not rotated or is vertically flipped (180°), and rotatedRatio, for when the image is rotated 90° in either direction. I calculate the later by swapping img.Source.Height and img.Source.Width.
I added a ScaleImage() method to do the actual image scaling based on the intended rotation, so I can call it from two different places.
I simplified Rotate(bool right) to calculate the new angle using math, instead of listing out each possible rotation.
The above results in an image that is always as big as possible for the screen while maintaining the original aspect ratio. It will grow and shrink as it's rotated to fit the screen. If you want the image to stay a constant size instead, just use Math.Min(normalRatio, rotatedRatio).
Note that the above only works if you call Rotate(bool right), not if you call Rotate(int degrees) directly. This is because the logic of using two ratios only works because there are only two possible sizes for the image (portrait and landscape), which is only the case if you restrict the rotation to increments of 90°. If you want to set the angle to something else, like 20°, the math to calculate the image's effective size becomes a bit more complicated and you would need to start calculating it dynamically based on the angle.
Related
I have a picturebox with an image in it. The image contains two ellipses face to face (black & blue).
What I want is to rotate the picturebox in a timer (for the effect) so the image to be "upside down" would look much more like they've changed place, which basically it's just rotating the picturebox like how the erath is moving around it's axis.
There are various kinds of rotations from a globe, depending on how you look at it.
If you look at it from above the poles it spins like a disk or a gear and you can find code for it here. This has the advantage that you can use any image and rotate it.
If you look at it from the side, facing the equator you can't easily use bitmaps, but using just two colors it will still look nice..
Here is an example of a 'globe-like' spinning rotation:
float angle = 0f;
float aSpeed = 4f; // <-- set your speed
Brush brush1 = Brushes.CadetBlue; // and your..
Brush brush2 = Brushes.DarkSlateBlue; // ..colors
private void timer1_Tick(object sender, EventArgs e)
{
angle += aSpeed;
if (angle + aSpeed > 360)
{
angle -= 360f;
Brush temp = brush1;
brush1 = brush2;
brush2 = temp;
}
pictureBox1.Invalidate();
}
private void pictureBox1_Click(object sender, EventArgs e)
{
timer1.Enabled = !timer1.Enabled;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Rectangle r = pictureBox1.ClientRectangle;
Rectangle r2 = r; // see below..
r.Inflate(-20, -20); // a little smaller than the panel or pBox
if (angle < 180)
{
e.Graphics.FillEllipse(brush1, r);
e.Graphics.FillPie(brush2, r, 270, 180);
r.Inflate(-(int)(r.Width * angle / 360f), 0);
e.Graphics.FillEllipse(brush2, r);
}
else
{
e.Graphics.FillEllipse(brush2, r);
e.Graphics.FillPie(brush1, r, 90, 180);
r.Inflate(-(int)(r.Width * angle / 360f), 0);
e.Graphics.FillEllipse(brush1, r);
}
}
}
This is created by three DrawXXX calls: a circle of one color and an ellipse and an arc, set to display a half circle of the same, second color.
Note: To make the angular speed uniform you may want to play with a little Math.Sin and/or an angle table..
If you look at it from any other angle and if you need to show rotating bitmaps in 3D you can't easily draw it but will need to resort to displaying frames..
But you can combine the disk rotation from the link with the code above and will get rather complex rotations, that look a lot like a 3D sphere.. Simply add the code before the drawing..
float bw2 = r2.Width / 2f;
float bh2 = r2.Height / 2f;
e.Graphics.TranslateTransform(bw2, bh2);
e.Graphics.RotateTransform(angle / 3);
e.Graphics.TranslateTransform(-bw2, -bh2);
..use the drawing from above instead of the DrawImage line and move the ResetTransform to the end. You will want to use a different or scaled angle!
I am trying to make a triangle move back and forth over an arc, the triangle shoud rotate while moving.
I have made a picture to explain it better:
https://app.box.com/s/mt9p66zlmtkkgkdvtb5h
The math looks right to me, can anyone tell me what I am doing wrong?
public partial class Form1 : Form
{
bool turn = false;
double angle = 0;
public Form1()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Brush solidBlackBrush = new SolidBrush(Color.Black); //En solid svart brush som brukes flere steder
Pen solidBackPen = new Pen(solidBlackBrush);//En solid svart pen som brukes flere steder
//Trekant = Norwegian for Triangle, Trekant is a class that draws a polygon shaped as a Triangle.
Trekant tre = new Trekant();
e.Graphics.DrawArc(solidBackPen, new Rectangle(new Point(50,50), new Size(100,100)) , 180, 180);
//X = a + r*Cos(angle) | Y = b + r*Sin(angle)
double x = (50+(100/2)) + (100/2) * Math.Cos(Trekant.DegreeToRadian(angle));
double y = (50+(100/2)) - (100/2) * Math.Sin(Trekant.DegreeToRadian(angle));
e.Graphics.TranslateTransform((float)x - 15, (float)y - 40);//Flytter 0 slik at pistolen havner på rett sted
e.Graphics.RotateTransform((float)-Trekant.RadianToDegree(Trekant.DegreeToRadian(angle-90)));
tre.Draw(e.Graphics);
}
private void timer1_Tick(object sender, EventArgs e)
{
if (angle == 0)
{
turn = false;
}
if (angle == 180)
{
turn = true;
}
if (turn)
{
angle -= 10;
}
if (!turn)
{
angle += 10;
}
this.Invalidate();
}
}
Without going into coding let's first set up the math..
Let say the half ellipse in the picture has a width of 2w and a height of h. And lets assume you want the movement to happen in n steps.
Then at each step s the rotation angle is s * 180f/n. The rotation point's x stays at w plus whatever offset ox the ellipse has, but will have to move its y vertically from offset oy, first by (w-h) * 2f / n down on each step and then up again by the same amounts..
The Drawing itself moves accordingly.
So you have a TranslateTransform for the rotation point, the RotateTransform, then another TranslateTransform to place the image, then the DrawImage and finally a ResetTransform.
I hope that helps. If that doesn't work, please update the question and we'll can get it right, I'm sure..
I'm trying to create a simple Window that allows viewing/zooming/moving an image. It is starting to behave similar to what it should with the image resizing to adjust to the window as we resize.
The weird thing is that as you reduce the width of the window, at a certain point, the height of the picture starts reducing when it shouldn't, and then it bounces into a different position. In the code, however, the height of the image remains the same so "something" else is altering the layout.
The other weird thing is that as you increase the width, the image almost remains centered except that it gradually tilts towards the right.
So my question is: what is screwing up my layout like this?
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NaturalGroundingPlayer.ImageViewerWindow"
x:Name="Window" Title="Image Viewer" Width="640" Height="480" SizeChanged="Window_SizeChanged">
<Grid x:Name="LayoutRoot">
<Canvas x:Name="ImgCanvas" ClipToBounds="True">
<ContentControl x:Name="ImgContentCtrl" Height="300" Width="400">
<Grid x:Name="ImgGrid">
<Image x:Name="ImgObject"/>
<Thumb x:Name="ImgThumb" Opacity="0" DragDelta="ImgThumb_DragDelta" MouseWheel="ImgThumb_MouseWheel"/>
</Grid>
</ContentControl>
</Canvas>
</Grid>
</Window>
public partial class ImageViewerWindow : Window {
public static ImageViewerWindow Instance(string fileName) {
ImageViewerWindow NewForm = new ImageViewerWindow();
NewForm.LoadImage(fileName);
NewForm.Show();
return NewForm;
}
private double scale;
public ImageViewerWindow() {
InitializeComponent();
}
public void LoadImage(string fileName) {
BitmapImage NewImage = new BitmapImage();
NewImage.BeginInit();
NewImage.UriSource = new Uri(fileName);
NewImage.EndInit();
ImgObject.Source = NewImage;
//ImgContentCtrl.Width = NewImage.Width;
//ImgContentCtrl.Height = NewImage.Height;
BestFit();
}
private void ImgThumb_DragDelta(object sender, DragDeltaEventArgs e) {
double ImgLeft = Canvas.GetLeft(ImgContentCtrl);
double ImgTop = Canvas.GetTop(ImgContentCtrl);
Canvas.SetLeft(ImgContentCtrl, (ImgLeft + e.HorizontalChange));
Canvas.SetTop(ImgContentCtrl, (ImgTop + e.VerticalChange));
}
private void ImgThumb_MouseWheel(object sender, MouseWheelEventArgs e) {
// Zoom in when the user scrolls the mouse wheel up and vice versa.
if (e.Delta > 0) {
// Limit zoom-in to 500%
if (scale < 5)
scale += 0.1;
} else {
// When mouse wheel is scrolled down...
// Limit zoom-out to 80%
if (scale > 0.8)
scale -= 0.1;
}
DisplayImage();
}
private void BestFit() {
// Set the scale of the ContentControl to 100%.
scale = 1;
// Set the position of the ContentControl so that the image is centered.
Canvas.SetLeft(ImgContentCtrl, 0);
Canvas.SetTop(ImgContentCtrl, 0);
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e) {
DisplayImage();
}
private void DisplayImage() {
double RatioWidth = LayoutRoot.ActualWidth / ImgObject.Source.Width;
double RatioHeight = LayoutRoot.ActualHeight / ImgObject.Source.Height;
if (RatioHeight > RatioWidth) {
ImgContentCtrl.Width = LayoutRoot.ActualWidth * scale;
ImgContentCtrl.Height = LayoutRoot.ActualHeight * RatioHeight * scale;
} else {
ImgContentCtrl.Height = LayoutRoot.ActualHeight * scale;
ImgContentCtrl.Width = LayoutRoot.ActualWidth * RatioWidth * scale;
}
}
}
Sjips, you first need to put any picture in it to display.
dkozl, that works indeed, I wasn't sure the Image control would respect proportions automatically as the demo I found wasn't respecting proportions while resizing. Simply putting this code in DisplayImage works perfect, even for repositioning properly when zooming. I guess I was much closer to the answer than I thought!
private void DisplayImage() {
ImgContentCtrl.Width = LayoutRoot.ActualWidth * scale;
Canvas.SetLeft(ImgContentCtrl, LayoutRoot.ActualWidth * (1 - scale) / 2);
ImgContentCtrl.Height = LayoutRoot.ActualHeight * scale;
Canvas.SetTop(ImgContentCtrl, LayoutRoot.ActualHeight * (1 - scale) / 2);
}
That being said, I still don't undestand the weird behaviors that happened before...
I am creating a hidden object game and am trying to mark the object when found with an eclipse. I've manually saved the top left and bottom right coordinates of each picture which were obtained via the GestureListener_Tap event.
The problem is when I tried to draw an eclipse bounded by the coordinates using this code
WriteableBitmapExtensions.DrawEllipse(writeableBmp, AnsX1, AnsY1, AnsX2, AnsY2, Colors.Red);
The location of the eclipse is always off to the top left. Marking the pixel locations using the following codes show that they are indeed located differently then what I would expect from the GestureListener_Tap.
writeableBmp.SetPixel(AnsX1, AnsY1, Colors.Red);
writeableBmp.SetPixel(AnsX2, AnsY2, Colors.Red);
My code for marking the location:
private void fadeOutAnimation_Ended(object sender, EventArgs e)
{
WriteableBitmap writeableBmp = new WriteableBitmap(bmpCurrent);
imgCat.Source = writeableBmp;
writeableBmp.GetBitmapContext();
WriteableBitmapExtensions.DrawEllipse(writeableBmp, AnsX1, AnsY1, AnsX2, AnsY2, Colors.Red);
writeableBmp.SetPixel(AnsX1, AnsY1, Colors.Red);
writeableBmp.SetPixel(AnsX2, AnsY2, Colors.Red);
// Present the WriteableBitmap
writeableBmp.Invalidate();
//Just some animation code
RadFadeAnimation fadeInAnimation = new RadFadeAnimation();
fadeInAnimation.StartOpacity = 0.2;
fadeInAnimation.EndOpacity = 1.0;
RadAnimationManager.Play(this.imgCat, fadeInAnimation);
}
What am I missing?
EDIT:
My answer below does not take into account the screen orientation changed. See my comment below the answer. How do you map pixel coordinates to image coordinate?
EDIT 2:
Found a correct solution. Updated my answer
From #PaulAnnetts comment I managed to transform the pixel coordinates. My inital mistake was to assume the image coordinates are the same as the pixel coordinates! I use the following code to convert.
private int xCoordinateToPixel(int coordinate)
{
double x;
x = writeableBmp.PixelWidth / imgCat.ActualWidth * coordinate;
return Convert.ToInt32(x);
}
private int yCoordinateToPixel(int coordinate)
{
double y;
y = writeableBmp.PixelHeight / imgCat.ActualHeight * coordinate;
return Convert.ToInt32(y);
}
EDIT:
Since PixelHeight and PixelWidth is fixed, and ActualHeight & ActualWidth isn't, I should convert pixel to coordinate in the GestureListerner_Tap event.
if ((X >= xPixelToCoordinate(AnsX1) && Y >= yPixelToCoordinate(AnsY1)) && (X <= xPixelToCoordinate(AnsX2) && Y <= yPixelToCoordinate(AnsY2)))
{...}
And my pixel to coordinate converters
private int xPixelToCoordinate(int xpixel)
{
double x = imgCat.ActualWidth / writeableBmp.PixelWidth * xpixel;
return Convert.ToInt32(x);
}
private int yPixelToCoordinate(int ypixel)
{
double y = imgCat.ActualHeight / writeableBmp.PixelHeight * ypixel;
return Convert.ToInt32(y);
}
I have a userControl library, which consists of the main Panel and a PictureBox, I want to make a zoomable PictureBox tool, I zoom in and out using mouseWheel event of the main Panel, the problem that I can't figure out how do I zoom in by the mouse position on the image, so whenever I zoom in, the zoom goes the Top-Left corner of the panel, so how do I fix that?
private double ZOOMFACTOR = 1.15; // = 15% smaller or larger
private int MINMAX = 5;
void picPanel_MouseWheel(object sender, MouseEventArgs e)
{
if (e.Delta > 0)
{
ZoomIn();
}
else
{
ZoomOut();
}
}
private void ZoomIn()
{
if ((picBox.Width < (MINMAX * this.Width)) &&
(picBox.Height < (MINMAX * this.Height)))
{
picBox.Width = Convert.ToInt32(picBox.Width * ZOOMFACTOR);
picBox.Height = Convert.ToInt32(picBox.Height * ZOOMFACTOR);
}
}
private void picBox_MouseEnter(object sender, EventArgs e)
{
if (picBox.Focused) return;
picBox.Focus();
}
Update :
I have tried this, it looks like working, but not exactly as it should be!! Any ideas?
private void ZoomIn()
{
if ((picBox.Width < (MINMAX * this.Width)) &&
(picBox.Height < (MINMAX * this.Height)))
{
picBox.Width = Convert.ToInt32(picBox.Width * ZOOMFACTOR);
picBox.Height = Convert.ToInt32(picBox.Height * ZOOMFACTOR);
Point p = this.AutoScrollPosition;
int deltaX = e.X - p.X;
int deltaY = e.Y - p.Y;
this.AutoScrollPosition = new Point(deltaX, deltaY);
}
}
This is the example of Zoom image on mouse position....
tested verified.
protected override void OnMouseWheel(MouseEventArgs ea)
{
// flag = 1;
// Override OnMouseWheel event, for zooming in/out with the scroll wheel
if (picmap1.Image != null)
{
// If the mouse wheel is moved forward (Zoom in)
if (ea.Delta > 0)
{
// Check if the pictureBox dimensions are in range (15 is the minimum and maximum zoom level)
if ((picmap1.Width < (15 * this.Width)) && (picmap1.Height < (15 * this.Height)))
{
// Change the size of the picturebox, multiply it by the ZOOMFACTOR
picmap1.Width = (int)(picmap1.Width * 1.25);
picmap1.Height = (int)(picmap1.Height * 1.25);
// Formula to move the picturebox, to zoom in the point selected by the mouse cursor
picmap1.Top = (int)(ea.Y - 1.25 * (ea.Y - picmap1.Top));
picmap1.Left = (int)(ea.X - 1.25 * (ea.X - picmap1.Left));
}
}
else
{
// Check if the pictureBox dimensions are in range (15 is the minimum and maximum zoom level)
if ((picmap1.Width > (imagemappan.Width)) && (picmap1.Height > (imagemappan.Height)))
{
// Change the size of the picturebox, divide it by the ZOOMFACTOR
picmap1.Width = (int)(picmap1.Width / 1.25);
picmap1.Height = (int)(picmap1.Height / 1.25);
// Formula to move the picturebox, to zoom in the point selected by the mouse cursor
picmap1.Top = (int)(ea.Y - 0.80 * (ea.Y - picmap1.Top));
picmap1.Left = (int)(ea.X - 0.80 * (ea.X - picmap1.Left));
}
}
}
}
The problem is that your control is acting like a viewport - the origin is top left, so every time you stretch the image you're doing it from that corner - the upshot is you wind up zooming into the top left corner, you need to offset the stretched image and centre the point the user zoomed in on.
image size: 200,200
user clicks 100,50 and zooms in x2
stretch the image
image size 400,400, and the place the user clicked is now effectively at 200,100
you need to slide the image 100 px left and 50 px up to correct for re-sizing the image
You'll need to override the paint event handler to draw the image offset:
RectangleF BmpRect = new RectangleF((float)(Offset.X), (float)(Offset.Y), (float)(ZoomedWidth), (float)(ZoomedHeight));
e.Graphics.DrawImage(Bmp, ViewPort , BmpRect, GraphicsUnit.Pixel);
Bmp is your image; ViewPort is a Rectangle defined by your pictureBox control
Here is a thread that might help.