I was wanting to get an image to fill a picture box, but not leaving any whitespace. Thus cutting off parts of the image to fit when its not resized to the aspect ratio of the pictureBox. And to adjust as the user resizes the window/pictureBox. The existing options, Sizemode = Zoom leaves whitespace, as its afraid to cut off any of the image and Sizemode = StretchImage stretches the image, distorting it.
The only way I can think of doing this is creating an algorithm to resize the image, keeping the contrast ratio, and setting the width or length of the image to the pictureBox width or length and creating some runtime loop which runs the algorithm once a frame. It seems kind of performance heavy for what it does and kind of hackish. Is there a better option?
Edit:
For anyone coming by, I implemented Ivan Stoev's solution slightly differently:
class ImageHandling
{
public static Rectangle GetScaledRectangle(Image img, Rectangle thumbRect)
{
Size sourceSize = img.Size;
Size targetSize = thumbRect.Size;
float scale = Math.Max((float) targetSize.Width / sourceSize.Width, (float) targetSize.Height / sourceSize.Height);
var rect = new RectangleF();
rect.Width = scale * sourceSize.Width;
rect.Height = scale * sourceSize.Height;
rect.X = (targetSize.Width - rect.Width) / 2;
rect.Y = (targetSize.Height - rect.Height) / 2;
return Rectangle.Round(rect);
}
public static Image GetResizedImage(Image img, Rectangle rect)
{
Bitmap b = new Bitmap(rect.Width, rect.Height);
Graphics g = Graphics.FromImage((Image) b);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.DrawImage(img, 0, 0, rect.Width, rect.Height);
g.Dispose();
try
{
return (Image)b.Clone();
}
finally
{
b.Dispose();
b = null;
g = null;
}
}
public Form1()
{
InitializeComponent();
updateMainBackground();
}
void updateMainBackground()
{
Image img = Properties.Resources.BackgroundMain;
Rectangle newRect = ImageHandling.GetScaledRectangle(img, mainBackground.ClientRectangle);
mainBackground.Image = ImageHandling.GetResizedImage(img, newRect);
}
private void Form1_Resize(object sender, EventArgs e)
{
updateMainBackground();
}
}
If I understand correctly, you are seeking for a "Fill" mode (similar to Windows background picture). There is no standard way of doing that, but it's not so hard to make your own with the help of a small calculation and GDI+:
using System;
using System.Drawing;
using System.IO;
using System.Net;
using System.Windows.Forms;
namespace Samples
{
public class ImageFillBox : Control
{
public ImageFillBox()
{
SetStyle(ControlStyles.Selectable | ControlStyles.SupportsTransparentBackColor, false);
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.Opaque | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);
}
private Image image;
public Image Image
{
get { return image; }
set
{
if (image == value) return;
image = value;
Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (image == null)
e.Graphics.Clear(BackColor);
else
{
Size sourceSize = image.Size, targetSize = ClientSize;
float scale = Math.Max((float)targetSize.Width / sourceSize.Width, (float)targetSize.Height / sourceSize.Height);
var rect = new RectangleF();
rect.Width = scale * sourceSize.Width;
rect.Height = scale * sourceSize.Height;
rect.X = (targetSize.Width - rect.Width) / 2;
rect.Y = (targetSize.Height - rect.Height) / 2;
e.Graphics.DrawImage(image, rect);
}
}
}
static class Test
{
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var testForm = new Form();
testForm.Controls.Add(new ImageFillBox
{
Dock = DockStyle.Fill,
Image = GetImage(#"http://www.celebrityrockstarguitars.com/rock/images/Metall_1.jpg")
});
Application.Run(testForm);
}
static Image GetImage(string path)
{
var uri = new Uri(path);
if (uri.IsFile) return Image.FromFile(path);
using (var client = new WebClient())
return Image.FromStream(new MemoryStream(client.DownloadData(uri)));
}
}
}
According to the PictureBoxSizeMode documentation you can specify PictureBoxSizeMode.Zoom to get the image to keep its aspect ratio. It will zoom as big as possible without any part of the image overflowing the picture box.
And you can play with the Dock property (setting DockStyle.Full) to get the picture box to resize to the size of its container.
There is a fairly simple solution to this. PictureBox.SizeMode has a few settings that will help us out. Zoom will adjust the image to fit in the box and Normal will place the picture with out resizing. What we will want to do is check the height and width of the image and if it is larger than the PictureBox size we will Zoom, if not, we will place it in with Normal. See below:
if (image.Height > pctbx_ImageRecognition.Height || image.Width > pctbx_ImageRecognition.Width)
pctbx_ImageRecognition.SizeMode = PictureBoxSizeMode.Zoom;
else
pctbx_ImageRecognition.SizeMode = PictureBoxSizeMode.Normal;
Related
I have successfully printed a windows form, but all the text is slightly blurry. I have concluded that this is a result of the resolution of the screen being much less than the resolution the printer uses. Is there a fundamental flaw in my approach or is there a way to reformat the text prior to printing so that it comes out crisp?
void PrintImage(object o, PrintPageEventArgs e)
{
int x = SystemInformation.WorkingArea.X;
int y = SystemInformation.WorkingArea.Y;
int width = panel1.Width;
int height = panel1.Height;
Rectangle bounds = new Rectangle(x, y, width, height);
Bitmap img = new Bitmap(width, height);
this.DrawToBitmap(img, bounds);
Point p = new Point(100, 100);
e.Graphics.DrawImage(img, p);
}
private void BtnPrint_Click(object sender, EventArgs e)
{
btnPrint.Visible = false;
btnCancel.Visible = false;
if(txtNotes.Text == "Notes:" || txtNotes.Text == "")
{
txtNotes.Visible = false;
}
PrintDocument pd = new PrintDocument();
pd.PrintPage += new PrintPageEventHandler(PrintImage);
pd.Print();
}
Is there a fundamental flaw in my approach [...] ?
Yes.
You take the size of panel1 to calculate the size of the image. Later, you let this draw to the image, but this is the form, not the panel.
What makes you think that SystemInformation.WorkingArea is related to the window you want to print?
You should take a bit more care of disposable objects.
[...] is there a way to reformat the text prior to printing so that it comes out crisp?
There's not a general way which would allow you to scale all other controls as well.
However, instead of blurry text, you can get crisp pixelated text by scaling the bitmap up by a certain factor using the NearestNeighbor mechanism.
Here's the difference in a PDF generated without scaling (left) and a factor of 3 scaling (right) at the same zoom level in Acrobat Reader (click to enlarge):
Here's the scaling code, also without fixing any disposable issues:
this.DrawToBitmap(img, bounds);
Point p = new Point(100, 100);
img = ResizeBitmap(img, 3);
e.Graphics.DrawImage(img, p);
}
private static Bitmap ResizeBitmap(Bitmap source, int factor)
{
Bitmap result = new Bitmap(source.Width*factor, source.Height*factor);
result.SetResolution(source.HorizontalResolution*factor, source.VerticalResolution*factor);
using (Graphics g = Graphics.FromImage(result))
{
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.DrawImage(source, 0, 0, source.Width*factor, source.Height*factor);
}
return result;
}
I'm creating an image viewer using C# and having a small issue with scaling (zooming) PictureBox.
I have a PictureBox inside a Panel, and I can zoom (scale) an image using the controls at the top left and using mouse wheel just fine. However, at some specific zoom scales, the image doesn't cover the whole PictureBox.
For example, SO logo (100x116 pixels) at 100% and at 200%:
Image at the right is 199x131 pixels, while the PictureBox is 200x132.
I've set the BackColor of the PictureBox to Red to make the issue noticeable.
This doesn't always happen, just at specific zoom levels. Why is that? Am I doing something wrong?
I can set the BackColor of PictureBox to the BackColor of the Panel to give the illusion that the image covers the whole PictureBox, but I rather fix the problem. If I can't, I'll apply the tricky solution.
Relevant code:
float zoom = 1;
Image image = null;
public MainForm(string[] args)
{
InitializeComponent();
image = ImageBox.Image;
this.ImageBox.MouseWheel += ImageBox_MouseWheel;
if (args.Length > 0)
{
LoadImage(args[0]);
}
}
private void ImageBox_Paint(object sender, PaintEventArgs e)
{
// disable interpolation (sharper pixels)
e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
// https://msdn.microsoft.com/en-us/library/ms142046(v=vs.110).aspx
e.Graphics.DrawImage(image,
new Rectangle(0, 0, ImageBox.Width, ImageBox.Height),
0, 0, image.Width, image.Height, GraphicsUnit.Pixel);
}
private void LoadImage(string path)
{
image = Image.FromFile(path);
ImageBox.Width = (int)(image.Width * zoom);
ImageBox.Height = (int)(image.Height * zoom);
ImageBox.Image = image;
CenterImage();
}
private void ScaleImage()
{
ImageBox.Image = null;
ImageBox.Width = (int)(image.Width * zoom);
ImageBox.Height = (int)(image.Height * zoom);
ImageBox.Image = image;
CenterImage();
}
I've also created a repository, in case anyone wants to examine the app live.
You need to adjust the rectangle in ImageBox_Paint. Try this:
private void ImageBox_Paint(object sender, PaintEventArgs e)
{
int add = ImageBox.Width / 200;
// disable interpolation (sharper pixels)
e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
// https://msdn.microsoft.com/en-us/library/ms142046(v=vs.110).aspx
e.Graphics.DrawImage(image,
new Rectangle(0, 0, ImageBox.Width + add, ImageBox.Height + add),
0, 0, image.Width, image.Height, GraphicsUnit.Pixel);
}
Simply set the SizeMode property of the PictureBox to Zoom. Then adapting the size of the picturebox will automatically make the image stretch to its full size.
You don't even need that Paint event listener; it's inbuilt functionality. Just change the dimensions of the PictureBox to the calculated zoomed dimensions of the image and you're done.
so am implementing a project that can read image pan it, zoom it and do other stuff.. everything was going well until i tried implementing a draw with right mouse button.
the problem is when i draw a line, the line that appears on the image does not correspond to the line i drew on screen, meaning its shifted and i know its because of the re-sizing and zooming of the image, but when i draw lines on the image with its original size(the image) and with panning also ; i have no problem.
here's the code.
so first here is how i load the image when i click browse and select image
Myimage = new Bitmap(ImagePath);
resized = myImage.Size;
imageResize();
pictureBox.Paint += new System.Windows.Forms.PaintEventHandler(this.pictureBox_Paint);
pictureBox.Invalidate();
the imageResize function does the following:
void imageResize()
{
//calculated the size to fit the control i will draw the image on
resized.Height = someMath;
resized.Width = someMath;
}
then in the event handler for the pictureBox_Paint event i wrote:
private void pictureBox_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
// Create a local version of the graphics object for the PictureBox.
Graphics PboxGraphics = e.Graphics;
PboxGraphics.DrawImage(myImage, imageULcorner.X, imageULcorner.Y, resized.Width, resized.Height);
}
as you can see the resized size is not the original image size i did this because i wanted the image to show on the picturebox control centralized and filled now the next part IS WHERE MY PROBLEM BEGINS
i have to draw lines on image using right mouse button so i implemented pictureBox_MouseDown & pictureBox_MouseUp event handlers
// mouse down event handler
private void pictureBox_MouseDown(object sender, MouseEventArgs e)
{
else if (mouse.Button == MouseButtons.Right)
{
mouseDown = mouse.Location;
mouseDown.X = mouseDown.X - imageULcorner.X;
mouseDown.Y = mouseDown.Y - imageULcorner.Y;
draw = true;
}
}
here is the mouse up event handler
//Mouse UP
private void pictureBox_MouseUp(object sender, MouseEventArgs e)
{
else if (mouse.Button == MouseButtons.Right)
{
if (draw)
{
mouseLocationNow.X = mouse.X - imageULcorner.X;
mouseLocationNow.Y = mouse.Y - imageULcorner.Y;
//
// get graphics object of the image ( the original not the resized)
// as the resized image only appears when i draw on the graphics of the
// pictureBox control
// i know the problem lies here but how can i fix it
//
Graphics image = Graphics.FromImage(myImage);
Pen pen = new Pen(Color.Red, 2);
image.DrawLine(pen, mouseLocationNow, mouseDown);
pictureBox.Invalidate();
}
draw = false;
}
so in the end i want to be able to draw on the re-sized image and make it correspond to the real image and also to the screen where i draw the line
thanks and sorry for the long post but this problem has been driving me crazy.
Here is a PictureBox subclass that supports the ability to apply zooming not only to the Image but also to graphics you draw onto its surface.
It includes a SetZoom function to zoom in by scaling both itself and a Matrix.
It also has a ScalePoint function you can use to calculate the unscaled coordinates from the pixel coordinates you receive in the mouse events.
The idea is to use a Transformation Matrix to scale any pixels the Graphics object will draw in the Paint event.
I include a little code for the form for testing.
public partial class ScaledPictureBox : PictureBox
{
public Matrix ScaleM { get; set; }
float Zoom { get; set; }
Size ImgSize { get; set; }
public ScaledPictureBox()
{
InitializeComponent();
ScaleM = new Matrix();
SizeMode = PictureBoxSizeMode.Zoom;
}
public void InitImage()
{
if (Image != null)
{
ImgSize = Image.Size;
Size = ImgSize;
SetZoom(100);
}
}
public void SetZoom(float zoomfactor)
{
if (zoomfactor <= 0) throw new Exception("Zoom must be positive");
float oldZoom = Zoom;
Zoom = zoomfactor / 100f;
ScaleM.Reset();
ScaleM.Scale(Zoom , Zoom );
if (ImgSize != Size.Empty) Size = new Size((int)(ImgSize.Width * Zoom),
(int)(ImgSize.Height * Zoom));
}
public PointF ScalePoint(PointF pt)
{ return new PointF(pt.X / Zoom , pt.Y / Zoom ); }
}
Here is the code in the Form that does the testing:
public List<PointF> somePoints = new List<PointF>();
private void scaledPictureBox1_MouseClick(object sender, MouseEventArgs e)
{
somePoints.Add(scaledPictureBox1.ScalePoint(e.Location) );
scaledPictureBox1.Invalidate();
}
private void scaledPictureBox1_Paint(object sender, PaintEventArgs e)
{
// here we apply the scaling matrix to the graphics object:
e.Graphics.MultiplyTransform(scaledPictureBox1.ScaleM);
using (Pen pen = new Pen(Color.Red, 10f))
{
PointF center = new PointF(scaledPictureBox1.Width / 2f,
scaledPictureBox1.Height / 2f);
center = scaledPictureBox1.ScalePoint(center);
foreach (PointF pt in somePoints)
{
DrawPoint(e.Graphics, pt, pen);
e.Graphics.DrawLine(Pens.Yellow, center, pt);
}
}
}
public void DrawPoint(Graphics G, PointF pt, Pen pen)
{
using (SolidBrush brush = new SolidBrush(pen.Color))
{
float pw = pen.Width;
float pr = pw / 2f;
G.FillEllipse(brush, new RectangleF(pt.X - pr, pt.Y - pr, pw, pw));
}
}
Here are the results after drawing a few points showing the same points in four different zoom settings; the ScaledPictureBox is obviously placed in an AutoScroll-Panel. The lines show how to use the regular drawing commands..
Is it possible to Draw any Form (without overridding the Paint method) in grayscale.
If I show a Form in a Modal() Dialog, I wan't do show its parent as grayscale.
I noticed this in the Visual Studio Extension Manager. If a progressbar is downloading a package, the underlying window is grayed out.
I am thinking of this:
private void Button1_Click(object sender, EventArgs e)
{
using (var dialog = new Form2())
{
SetGrayscale(this, true);
dialog.ShowDialog();
SetGrayscale(this, false);
}
}
Update
Just setting Form.Enabled = false; is not what I intended. That does not look as good as a grayscale representation of my form.
I think the compiz window decorator for Linux did this with apps that are unresponsive.
As has already been said the way to do this is to overlay another control / form on top of your existing form and have it render a grayscale version of this on top, you could either do this using an additional form placed exactly over the original form, or using something like a Panel positioned on top of all other controls.
Here is a working example of how you might do this when placing another form exactly over the client area of the first. How to use it
using (Grayscale(this))
{
MessageBox.Show("Test");
}
Implementation
public static Form Grayscale(Form tocover)
{
var frm = new Form
{
FormBorderStyle = FormBorderStyle.None,
ControlBox = false,
ShowInTaskbar = false,
StartPosition = FormStartPosition.Manual,
AutoScaleMode = AutoScaleMode.None,
Location = tocover.PointToScreen(tocover.ClientRectangle.Location),
Size = tocover.ClientSize
};
frm.Paint += (sender, args) =>
{
var bmp = GetFormImageWithoutBorders(tocover);
bmp = ConvertToGrayscale(bmp);
args.Graphics.DrawImage(bmp, args.ClipRectangle.Location);
};
frm.Show(tocover);
return frm;
}
private static Bitmap ConvertToGrayscale(Bitmap source)
{
var bm = new Bitmap(source.Width, source.Height);
for (int y = 0; y < bm.Height; y++)
{
for (int x = 0; x < bm.Width; x++)
{
Color c = source.GetPixel(x, y);
var luma = (int)(c.R * 0.3 + c.G * 0.59 + c.B * 0.11);
bm.SetPixel(x, y, Color.FromArgb(luma, luma, luma));
}
}
return bm;
}
private static Bitmap GetControlImage(Control ctl)
{
var bm = new Bitmap(ctl.Width, ctl.Height);
ctl.DrawToBitmap(bm, new Rectangle(0, 0, ctl.Width, ctl.Height));
return bm;
}
private static Bitmap GetFormImageWithoutBorders(Form frm)
{
// Get the form's whole image.
using (Bitmap wholeForm = GetControlImage(frm))
{
// See how far the form's upper left corner is
// from the upper left corner of its client area.
Point origin = frm.PointToScreen(new Point(0, 0));
int dx = origin.X - frm.Left;
int dy = origin.Y - frm.Top;
// Copy the client area into a new Bitmap.
int wid = frm.ClientSize.Width;
int hgt = frm.ClientSize.Height;
var bm = new Bitmap(wid, hgt);
using (Graphics gr = Graphics.FromImage(bm))
{
gr.DrawImage(wholeForm, 0, 0,
new Rectangle(dx, dy, wid, hgt),
GraphicsUnit.Pixel);
}
return bm;
}
}
Note that:
The implementation of Paint is fairly poor - really it should use double buffering so that the grayscale image is pre-rendered to a buffered graphics context so the Paint method just needs to paint the pre-drawn buffer contents. See Custom Drawing Controls in C# – Manual Double Buffering
ConvertToGrayscale is a tad on the slow side, but can probably be sped up
Things will go wrong if someone manages to move the original form for any reason
The image is static, if the base control gets redrawn then ideally the top form should redraw too. I'm not sure how best to detect when a portion of another form has been invalidated.
If I find the time I'll try and fix some of those problems, but the above at least gives you the general idea.
Note that in WPF this would be a lot easier.
Sources:
How to convert a colour image to grayscale
Get the image of a control or form, or a form's client area in C#
I don't think there is a way to do it directly - I think all forms are rendered with sRGB.
A hacky way could be to overlay the form with a copy of it as an image (this is simple to do with Control.DrawToBitMap) and then pass it through a simple GDI matrix to desaturate https://web.archive.org/web/20141230145627/http://bobpowell.net/grayscale.aspx.
Try something like this which would work for most simple controls (you would need to recurse into containers to switch all controls correctly).
private void button1_Click(object sender, EventArgs e)
{
using (var dialog = new Form())
{
Dictionary<Control, Tuple<Color, Color>> oldcolors = new Dictionary<Control, Tuple<Color, Color>>();
foreach (Control ctl in this.Controls)
{
oldcolors.Add(ctl, Tuple.Create(ctl.BackColor, ctl.ForeColor));
// get rough avg intensity of color
int bg = (ctl.BackColor.R + ctl.BackColor.G + ctl.BackColor.B) / 3;
int fg = (ctl.ForeColor.R + ctl.ForeColor.G + ctl.ForeColor.B) / 3;
ctl.BackColor = Color.FromArgb(bg, bg, bg);
ctl.ForeColor = Color.FromArgb(fg, fg, fg);
}
dialog.ShowDialog();
foreach (Control ctl in this.Controls)
{
ctl.BackColor = oldcolors[ctl].Item1;
ctl.ForeColor = oldcolors[ctl].Item2;
}
}
}
I have an image .I want to crop 10 px from left and 10px from right of the image.I used the below code to do so
string oldImagePath="D:\\RD\\dotnet\\Images\\photo1.jpg";
Bitmap myOriginalImage = (Bitmap)Bitmap.FromFile(oldImagePath);
int newWidth = myOriginalImage.Width;
int newHeight = myOriginalImage.Height;
Rectangle cropArea = new Rectangle(10,0, newWidth-10, newHeight);
Bitmap target = new Bitmap(cropArea.Width, cropArea.Height);
using (Graphics g = Graphics.FromImage(target))
{
g.DrawImage(myOriginalImage,cropArea);
}
target.Save("D:\\RD\\dotnet\\Images\\test.jpg");
But this is not giving me the results which i expect. This outputs an image which has 10 px cropped from the right and a resized image.Instead of cropiing it is resizing the width i think.So the image is shrinked(by width). Can any one correct me ? Thanks in advance
Your new width should be reduced by twice the crop margin, since you'll be chopping off that amount from both sides.
Next, when drawing the image into the new one, draw it at a negative offset. This causes the area that you aren't interested in to be clipped off.
int cropX = 10;
Bitmap target = new Bitmap(myOriginalImage.Width - 2*cropX, myOriginalImage.Height);
using (Graphics g = Graphics.FromImage(target))
{
g.DrawImage(myOriginalImage, -cropX, 0);
}
My guess is this line
Rectangle cropArea = new Rectangle(10,0, newWidth-10, newHeight);
should be
Rectangle cropArea = new Rectangle(10,0, newWidth-20, newHeight);
Set the width of the new rectangle to be 20 less than the original - 10 for each side.
Some indication what result it is giving you would be helpful in confirming this.
Corey Ross is correct. Alternately, you can translate along the negative x axis and render at 0.0, 0.0. Should produce identical results.
using (Graphics g = Graphics.FromImage(target))
{
g.TranslateTransform(-cropX, 0.0f);
g.DrawImage(myOriginalImage, 0.0f, 0.0f);
}
You need to use the overload that has you specify both Destination Rectangle, and Source Rectangle.
Below is an interactive form of this using a picture box on a form. It allows you to drag the image around. I suggest making the picture box 100 x 100 and have a much larger image such as a full screen window you've captured with alt-prtscr.
class Form1 : Form
{
// ...
Image i = new Bitmap(#"C:\Users\jasond\Pictures\foo.bmp");
Point lastLocation = Point.Empty;
Size delta = Size.Empty;
Point drawLocation = Point.Empty;
bool dragging = false;
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (!dragging)
{
lastLocation = e.Location;
dragging = true;
}
delta = new Size(lastLocation.X - e.Location.X, lastLocation.Y - e.Location.Y);
lastLocation = e.Location;
if (!delta.IsEmpty)
{
drawLocation.X += delta.Width;
drawLocation.Y += delta.Height;
pictureBox1.Invalidate();
}
}
else
{
dragging = false;
}
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Rectangle source = new Rectangle(drawLocation,pictureBox1.ClientRectangle.Size);
e.Graphics.DrawImage(i,pictureBox1.ClientRectangle,source, GraphicsUnit.Pixel);
}
//...
Okay, I totally fail at explaining this, but hang on:
The DrawImage function requires the location of the image, as well as it's position. You need a second position for cropping as how the old relates to the new, not vice versa.
That was entirely incomprehensible, but here is the code.
g.DrawImage(myOriginalImage, -cropArea.X, -cropArea.Y);
I hope that explains it more then I did.