How can I repaint a panel in smooth?
I am using a timer that is invalidating the panel(panel1.Invalidate();) every 300ms, and then with the panel1_Paint event I am adding images to that panel the issue is that it looks like is jumping and I need to be moving one image on it as fast as I can.
This is a link of the screen-cast issue: http://screencast.com/t/HdtIV99YN
private void panel1_Paint(object sender, PaintEventArgs e)
{
PaintMapObstacles(e);
PaintMapStartAndGoal(e);
if (trayectoryIndex < 1000)
{
MapPoint point = GetTrayectoryPoint(0, trayectoryIndex);
e.Graphics.DrawImage(new Bitmap("robot.jpg"), point.X*squareSize, point.Y*squareSize, 60, 60);
trayectoryIndex++;
}
}
private void PaintMapStartAndGoal(PaintEventArgs e)
{
MapPoint start = new MapPoint { X = 0, Y = 0 };
MapPoint goal = new MapPoint { X = 7, Y = 8 };
// e.Graphics.DrawImage(new Bitmap("start.jpg"), start.X * squareSize, start.Y * squareSize, 60, 60);
e.Graphics.DrawImage(new Bitmap("goal.jpg"), goal.X * squareSize, goal.Y * squareSize, 60, 60);
isFirstRun = true;
}
private void PaintMapObstacles(PaintEventArgs e)
{
foreach (MapPoint mpoint in obstacles.Obstacles)
{
e.Graphics.DrawImage(new Bitmap("obstacle.png"), mpoint.X * squareSize, mpoint.Y * squareSize, 60, 60);
}
}
private void PanelTimer_Tick(object sender, EventArgs e)
{
panel1.Invalidate();
}
It is called "flicker", an artifact that's always around when you repaint a window from scratch. It is especially noticeable in your program because your painting code is so inefficient. You see the window's background getting drawn, erasing the old painting. Then slowly getting the bitmaps drawn back onto the background. The erasure step is visible to the eye and looks like flicker.
The general cure for flicker is double-buffering, composing the window content into a bitmap first, then quickly blitting the bitmap to the screen. It is a built-in feature for Winforms, the DoubleBuffered property turns it on. Double-buffering is not enabled by default for the Panel class, it was designed to be a container control that doesn't do painting on its own beyond drawing the background. A PictureBox will work just as well in your case, it has double-buffering enabled by default. Or you can turn on double-buffering for the Panel class, shown here.
You do want to eventually address the problems with your painting code, beyond it being very slow, it can crash your program with an OutOfMemoryException. A problem caused by the way you use the Bitmap class, it should be disposed after you used it. Always use the using statement for System.Drawing objects.
You'll make it a lot faster by creating the bitmaps just once, the form constructor is the best place. You make it really fast by prescaling the bitmaps to fit the grid and paying attention to the pixel format. PixelFormat.Format32bppPArgb is directly compatible with the frame buffer format of almost any modern video adapter, the bitmap can be directly copied into the frame buffer without conversion. Ten times faster than all the other formats. Conversion code is here.
Related
In my application, there are 2 windows and both contain a PictureBox. The first (pb1) allows interaction and the image can be changed through click- and mouseMove-events. These events call pb1.Invalidate(); which works fine.
I want the second PictureBox (pb2) to redraw as well so I call pb2.Invalidate() from the paint-event of pb1. [Just for context, the second PictureBox shows nearly the same Image but on a bigger scale and some parts of the drawing will be left out in the future so I use the same Method in both paint events which decides what to draw and what not]
It works but it's "laggy" and I want it to be as smooth as the paint on the first PictureBox. I reduced the paint event just to a grid for test purposes.
Both windows are double buffered.
I tried replacing the picture boxes with SKGLControls from SkiaSharp (which should have better performance). The example code still uses the SkiaEvents so don't be confused if the problem occurs with both controls.
I tried to use .Update() or .Refresh() instead of .Invalidate() but i guess its to much to handle, the application just crashes..
Here is the method that is called by both OnPaint events
public void Update(SKPaintGLSurfaceEventArgs e, bool bigscreen)
{
SKCanvas canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Beige);
//Zoom to specified area
SKMatrix matrix = SKMatrix.Identity;
if (!bigscreen)
{
matrix = matrix.PostConcat(SKMatrix.CreateScale(canvasSize / (float)zoomArea.Width, canvasSize / (float)zoomArea.Height));
}
else
{
matrix = matrix.PostConcat(SKMatrix.CreateScale(bigCanvasSize / (float)zoomArea.Width, bigCanvasSize / (float)zoomArea.Height));
}
matrix = matrix.PreConcat(SKMatrix.CreateTranslation(-zoomArea.X, -zoomArea.Y));
canvas.SetMatrix(matrix);
DrawGrid(canvas);
}
and the grid-draw method
private void DrawGrid(SKCanvas canvas)
{
using (SKPaint paint = new SKPaint() { IsAntialias = true,Color=SKColors.LightGray,StrokeWidth = 1})
{
canvas.DrawLine(0, 0, 0, gridCanvas.Height, paint); //Size gridCanvas is always the same at the moment and defines the space where the grid is drawn
canvas.DrawLine(0, 0, gridCanvas.Width, 0, paint);
for (int i = 0; i <= (gridCanvas.Width - gridoffsetX) / pxPerSquare; i++)
{
canvas.DrawLine(i * pxPerSquare + gridoffsetX, 0, i * pxPerSquare + gridoffsetX, gridCanvas.Height, paint);
}
for (int i = 0; i <= (gridCanvas.Height - gridoffsetY) / pxPerSquare; i++)
{
canvas.DrawLine(0, i * pxPerSquare + gridoffsetY, gridCanvas.Width, i * pxPerSquare + gridoffsetY, paint);
}
}
}
and finally the original Paint Event
private void Pb1_PaintSurface(object sender, SKPaintGLSurfaceEventArgs e)
{
win2.UpdateDrawing(); //Just calls .Invalidate() on pb2
painter.Update(e, false);
}
examplePicture
So my question is: Is there a way to make both controls draw at nearly the same time without delay, although I don't understand why the first PictureBox draws in real time and the second doesn't...
Thanks!
after searching for day i found this page right after posting, which helped me:
Onpaint events (invalidated) changing execution order after a period normal operation (runtime)
I want to make an arrow in c#, which goes from A position to B position in 5 seconds for example. I want to put a map image in the form and when i click on a button i want to draw an arrow from A position to B position in an interval of seconds. i have made an arrow when it is in a horizontal position, but when i try to make it oblique it draws me a triangle instead of an arrow and i don't know how to fix it.
here i made an arrow from a position 12 with a width of 300
and i try to make the same with an oblique arrow but when i put different positions it draws me a triangle not an arrow.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace WindowsFormsApp4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(400, 273);
this.Text = "";
this.Resize += new System.EventHandler(this.Form1_Resize);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
}
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRectangle(Brushes.White, this.ClientRectangle);
Pen p = new Pen(Color.Black, 5);
p.StartCap = LineCap.Round;
for(int i=1; i<=300;i++)
{
System.Threading.Thread.Sleep(2);
g.DrawLine(p, 12, 30, i, 30);
Cursor.Current = Cursors.Default;
}
p.EndCap = LineCap.ArrowAnchor;
g.DrawLine(p, 12, 30, 310, 30);
p.Dispose();
}
private void Form1_Resize(object sender, System.EventArgs e)
{
Invalidate();
}
}
}
The fundamental problem with your code is that you are doing the entire animation loop inside the Paint event handler. This means that the window is never clear out between each line you draw, so you get all of the copies of the line you're drawing, start to finish, laid on top of each other in the same view.
It is not clear from your question exactly what you expect to see on the screen. However, another potential problem with your code is that the moving end point of the line does not start at the start point of the line, but rather at a point with the same Y coordinate where you want the line to end. This means that the arrow end of the line traverses a horizontal line leading to the final end point, rather than gradually extending from the start point of the line.
There is also the minor point that you seem to be confused about what the DrawLine() method does. You state that the width of your line is 300, but in fact the second argument of the DrawLine() method is just another point. The width of the line is defined by the Pen you use to draw the line. The width of the box containing the line is defined by the start and end point, but in this case is not 300, but rather (at the final length of the line) the difference between your start X coordinate and end X coordinate (i.e. 288).
The fundamental problem described above can be addressed by running a loop outside of the Paint event handler, which updates values that describe the line, and then call Invalidate() so that the Paint event handler can be called to draw just the current state of the animation.
On the assumption that what you really wanted was for a line to extend out from the start point, rather than traverse a horizontal line, the example I show below implements the animation that way as well, in addition to fixing the fundamental issue. I did nothing to change the length or width of the line.
public Form1()
{
InitializeComponent();
this.DoubleBuffered = true;
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(400, 273);
this.Resize += new System.EventHandler(this.Form1_Resize);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
var task = AnimateLine();
}
private readonly Point _lineStart = new Point(12, 30);
private readonly Point _lineFinalEnd = new Point(300, 60);
private const int _animateSteps = 300;
private Point _lineCurrentEnd;
private bool _drawArrow;
private async Task AnimateLine()
{
Size size = new Size(_lineFinalEnd) - new Size(_lineStart);
for (int i = 1; i <= _animateSteps; i++)
{
await Task.Delay(2);
Size currentSize = new Size(
size.Width * i / _animateSteps, size.Height * i / _animateSteps);
_lineCurrentEnd = _lineStart + currentSize;
Invalidate();
}
_drawArrow = true;
Invalidate();
}
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen p = new Pen(Color.Black, 5))
{
p.StartCap = LineCap.Round;
p.EndCap = _drawArrow ? LineCap.ArrowAnchor : p.EndCap;
g.DrawLine(p, _lineStart, _lineCurrentEnd);
}
}
Note that the repeated erasing and redrawing of the window would make the window flicker. This is a basic issue with any sort of animation, and the fix is to enable double-buffering for the window, hence the this.DoubleBuffered = true; statement added to the constructor.
Some other points worth mentioning:
The await Task.Delay() call is used so that the loop can yield the UI thread with each iteration of the loop, which allows the UI thread to raise the Paint event, as well as allows any other UI activity to still work during the animation. You can find lots more information about that C# feature in the How and When to use async and await article, and of course by reading the documentation.
Whether you use Thread.Sleep() or Task.Delay(), specifying a delay of 2 ms isn't very useful. The Windows thread scheduler does not schedule threads to that degree of precision. A thread that sleeps for 2 ms could be woken up as much as 50 ms later in the normal case, and even later if the CPU is under heavy load. Nor does a 2 ms delay provide a useful animation frame rate; that would be a 500 Hz refresh rate, which is easily 10x or more faster than the human brain needs in order to perceive a smooth animation.My example above does nothing to try to address this issue, but you should explore implementing the loop slightly differently, such that instead of the number of animation steps, you specify a reasonable animation interval (say, every 50 or 100 ms), make an attempt to delay that interval, but then use a Stopwatch to actually measure what the real delay was and compute the progress within the animation based on the actual time elapsed. This will allow you to have precise control over the total duration of the animation, as well as somewhat precise control over the refresh rate used for the animation.
For a project, I'm making a game and in it I have a scrolling map. The map moves left and right and is redrawn in a picturebox so that I can have a large map in a small picturebox. The top portion of the map is transparent so that I can change the sky colour later on. However when I move the map, the transparent part glitches out.
Original map before moving
After moving the map a bit
As you can see, everything above the tree line gets stretched, that is because that is where the transparency starts. The picturebox's parent is the form and the form is light blue, which is why the background is light blue.
Here is my code for moving the picture/redrawing it onto the picturebox:
private void timerTick_Tick(object sender, EventArgs e)
{
move();
//Draws new portion of the map
g.DrawImage(image, new Rectangle(0, 0, pbMap.Width, pbMap.Height), new Rectangle(imageX, imageY, pbMap.Width, pbMap.Height), GraphicsUnit.Pixel);
//Refreshes
pbMap.Image = bmp;
}
private void move()
{
//Right arrow events
if (right)
{
imageX += mapSpeed;
//Makes sure the picture stays within borders
if (imageX >= (imageWidth - pbMap.Width))
{
imageX = imageWidth - pbMap.Width;
}
}
//Left arrow events
if (left)
{
imageX -= mapSpeed;
//Makes sure the picture stays within borders
if (imageX <= 0)
{
imageX = 0;
}
}
}
Can anyone help explain the glitching?
Try calling g.Clear() with your sky color before the g.DrawImage() call. I think it's just drawing on top of itself and that's causing the smearing.
To me it seems like you are redrawing over and over without clearing the display from the previous draw! What type of framework are you using to develop that? Does it have a custom drawing class? As tesserex suggested more specifically call g.Clear() and u will be fine.
You shouldn't need g.Clear in this case because you're Re-Drawing a new image every time.
My bet is that imageX is greater then (imageWidth - pbMap.Width) so it will not enter the IF, therefore it will redraw the same as before.
Note: I don't know how you create g but if you use .CreateGraphics() don't forget to Dispose()
Cheers
In my program, I'm coding a basic image editor. Part of this allows the user to draw a rectangular region and I pop up a display that shows that region zoomed by 3x or so (which they can adjust further with the mouse wheel). If they right click and drag this image, it will move the zoom region around on the original image, basically acting as a magnifying glass.
The problem is, I'm seeing some serious performance issues even on relatively small bitmaps. If the bitmap showing the zoomed region is around 400x400 it's still updating as fast as mouse can move and is perfectly smooth, but if I mouse wheel the zoom up to around 450x450, it immediately starts chunking, only down to around 2 updates per second, if that. I don't understand why such a small increase incurs such an enormous performance problem... it's like I've hit some internal memory limit or something. It doesn't seem to matter the size of the source bitmap that is being zoomed, just the size of the zoomed bitmap.
The problem is that I'm using Graphics.DrawImage and a PictureBox. Reading around this site, I see that the performance for both of these is typically not very good, but I don't know enough about the inner workings of GDI to improve my speed. I was hoping some of you might know where my bottlenecks are, as I'm likely just using these tools in poor ways or don't know of a better tool to use in its place.
Here are some snippets of my mouse events and related functions.
private void pictureBox_MouseDown(object sender, MouseEventArgs e)
{
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
// slide the zoomed part to look at a different area of the original image
if (zoomFactor > 1)
{
isMovingZoom = true;
// try saving the graphics object?? are these settings helping at all??
zoomingGraphics = Graphics.FromImage(displayImage);
zoomingGraphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
zoomingGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
zoomingGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
zoomingGraphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
}
}
}
private void pictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (isMovingZoom)
{
// some computation on where they moved mouse ommitted here
zoomRegion.X = originalZoomRegion.X + delta.X;
zoomRegion.Y = originalZoomRegion.Y + delta.Y;
zoomRegionEnlarged = scaleToOriginal(zoomRegion);
// overwrite the existing displayImage to prevent more Bitmaps being allocated
createZoomedImage(image.Bitmap, zoomRegionEnlarged, zoomFactor, displayImage, zoomingGraphics);
}
}
private void createZoomedImage(Bitmap source, Rectangle srcRegion, float zoom, Bitmap output, Graphics outputGraphics)
{
Rectangle destRect = new Rectangle(0, 0, (int)(srcRegion.Width * zoom), (int)(srcRegion.Height * zoom));
outputGraphics.DrawImage(source, destRect, srcRegion, GraphicsUnit.Pixel);
if (displayImage != originalDisplayImage && displayImage != output)
displayImage.Dispose();
setImageInBox(output);
}
// sets the picture box image, as well as resizes the window to fit
void setImageInBox(Bitmap bmp)
{
pictureBox.Image = bmp;
displayImage = bmp;
this.Width = pictureBox.Width + okButton.Width + SystemInformation.FrameBorderSize.Width * 2 + 25;
this.Height = Math.Max(450, pictureBox.Height) + SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height * 2 + 20;
}
private void pictureBox_MouseUp(object sender, MouseEventArgs e)
{
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
if (isMovingZoom)
{
isMovingZoom = false;
zoomingGraphics.Dispose();
}
}
}
As you can see, I'm not declaring a new Bitmap every time I want to draw something, I'm reusing an old Bitmap (and the Bitmap's graphics object, though I don't know if there is much cost with calling Graphics.FromImage repeatedly). I tried adding Stopwatches around to benchmark my code, but I think DrawImage passes functionality to another thread so the function claims to be done relatively quickly. I'm trying to Dispose all my Bitmap and Graphics objects when I'm not using them, and avoid repeated calls to allocate/deallocate resources during the MouseMove event. I'm using a PictureBox but I don't think that's the problem here.
Any help to speed up this code or teach me what's happening in DrawImage is appreciated! I've trimmed some excess code to make it more presentable, but if I've accidentally trimmed something important, or don't show how I'm using something which may be causing problems, please let me know and I'll revise the post.
The way I handle issues like that is when receiving the Paint event, I draw the whole image to a memory bitmap, and then BLT it to the window.
That way, all visual flash is eliminated, and it looks fast, even if it actually is not.
To be more clear, I don't do any painting from within the mouse event handlers.
I just set up what's needed for the main Paint handler, and then do Invalidate.
So the painting happens after the mouse event completes.
ADDED: To answer Tom's question in a comment, here's how I do it. Remember, I don't claim it's fast, only that it looks fast, because the _e.Graphics.DrawImage(bmToDrawOn, new Point(0,0)); appears instantaneous. It just bips from one image to the next.
The user doesn't see the window being cleared and then repainted, thing by thing.
It gives the same effect as double-buffering.
Graphics grToDrawOn = null;
Bitmap bmToDrawOn = null;
private void DgmWin_Paint(object sender, PaintEventArgs _e){
int w = ClientRectangle.Width;
int h = ClientRectangle.Height;
Graphics gr = _e.Graphics;
// if the bitmap needs to be made, do so
if (bmToDrawOn == null) bmToDrawOn = new Bitmap(w, h, gr);
// if the bitmap needs to be changed in size, do so
if (bmToDrawOn.Width != w || bmToDrawOn.Height != h){
bmToDrawOn = new Bitmap(w, h, gr);
}
// hook the bitmap into the graphics object
grToDrawOn = Graphics.FromImage(bmToDrawOn);
// clear the graphics object before drawing
grToDrawOn.Clear(Color.White);
// paint everything
DoPainting();
// copy the bitmap onto the real screen
_e.Graphics.DrawImage(bmToDrawOn, new Point(0,0));
}
private void DoPainting(){
grToDrawOn.blahblah....
}
I have 2 problems while trying to print from a WinForms application. The first is a very very bad quality no matter what I try. The second is that I have a big page margin from the top left corner and the winform is cutting. Any ideas? This is my code:
Bitmap MemoryImage;
public void GetPrintArea(Panel pnl)
{
MemoryImage = new Bitmap(pnl.Width, pnl.Height);
Rectangle rect = new Rectangle(0, 0, pnl.Width, pnl.Height);
pnl.DrawToBitmap(MemoryImage, new Rectangle(0, 0, pnl.Width, pnl.Height));
}
protected override void OnPaint(PaintEventArgs e)
{
if (MemoryImage != null)
{
e.Graphics.DrawImage(MemoryImage, 0, 0);
base.OnPaint(e);
}
}
void printdoc1_PrintPage(object sender, PrintPageEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
Rectangle pagearea = e.PageBounds;
e.Graphics.DrawImage(MemoryImage, (pagearea.Width / 2) - (this.panel1.Width / 2), this.panel1.Location.Y);
}
public void Print(Panel pnl)
{
panel1 = pnl;
GetPrintArea(pnl);
printPreviewDialog1.Document = printdoc1;
printPreviewDialog1.ShowDialog();
}
private void button2_Click(object sender, EventArgs e)
{
Print(this.panel1);
}
This comes up over and over again. There just is no magic solution, although eventually the problem is likely to disappear. The emergence of "retina" displays is pivotal.
The core issue is that monitors have a resolution that's drastically worse than printers. A typical printer has a resolution of 600 dots per inch. Which makes it capable of printing 6600 x 5100 individual pixels on a piece of paper. Much, much more than what a monitor can display, a full HD monitor tops out at 1920 x 1080 pixels. Roughly a factor of 5 worse, give or take.
This works out poorly when you print what shows up on a monitor on a piece of paper and try to keep it the same size. Inevitably, because of the lack of pixels on the monitor, each one pixel from the monitor is printed as a 5x5 blob on paper. If you try to keep the pixel mapping one-to-one, you will get a razor-sharp copy on paper. But it has turned into a postage-stamp.
Inevitably, the printout looks very grainy due those pixel blobs. What looks especially poor is text. Operating systems use lots of tricks to make text look good on monitors with poor resolution. Anti-aliasing is standard, tricks like ClearType were designed to take advantage of monitor physics that can help increase the perceived resolution. This no longer works when the text is printed, those anti-aliasing pixels turn into blobs and become very visible, completely ruining the effect. Especially bad for ClearType text on a color printer, the red and blue color fringes now can be clearly seen.
The only decent approach is to render to a printer using the actual resolution and not the monitor resolution. Like using the PrintDocument class in .NET. Using a report generator can help avoid having to write the code for it.
You should draw yourself on the Graphics object you get when the PrintDocument prints. That gives you all the control you need. Still everything Hans Passant said applies here as well...
Keep in mind that this is the simplest implementation that merely demo's what can be achieved, I'm not claiming that this is the easiest/best/most productive way... my code doesn't take multiple pages, controls contained in contaimers or controls not of the type Label and PictureBox.
I used the Draw... methods from System.Drawing.Graphics
sligthly adapted from the code above to get this working:
public void GetPrintArea(Panel pnl, Graphics gr)
{
// scale to fit on width of page...
if (pnl.Width > 0)
{
gr.PageScale = gr.VisibleClipBounds.Width/pnl.Width;
}
// this should recurse...
// just for demo so kept it simple
foreach (var ctl in pnl.Controls)
{
// for every control type
// come up with a way to Draw its
// contents
if (ctl is Label)
{
var lbl = (Label)ctl;
gr.DrawString(
lbl.Text,
lbl.Font,
new SolidBrush(lbl.ForeColor),
lbl.Location.X, // simple based on the position in the panel
lbl.Location.Y);
}
if (ctl is PictureBox)
{
var pic = (PictureBox)ctl;
gr.DrawImageUnscaledAndClipped(
pic.Image,
new Rectangle(
pic.Location.X,
pic.Location.Y,
pic.Width,
pic.Height));
}
}
}
void printdoc1_PrintPage(object sender, PrintPageEventArgs e)
{
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality;
e.Graphics.InterpolationMode =Drawing2D.InterpolationMode.HighQualityBilinear;
e.Graphics.PixelOffsetMode = Drawing2D.PixelOffsetMode.HighQuality;
GetPrintArea(panel1, e.Graphics);
}
You can actually print sharper controls by applying scaling to them on a "vector" level instead of bitmap level.
This snapshot shows the result of the following technique (please don't mind my Win2000-ish UI :-) ):
What we do is to iterate through the control's ControlCollection in a very similar fashion as rene shows in his answer.
But - in addition we apply scale to location, size and font to the control itself before we draw its bitmap to a preset size bitmap which in this case is 5 times bigger (4 times would represent about 300 DPI which is the effective print resolution on most printers).
The reason for this is to keep the thin line on the control sharp on print, or we could just scale the bitmap itself which wouldn't give us any benefit resolution-wise. By scaling font we reduce the anti-alias effect and can provide a better print quality.
To do so you can first in the button's click event setup the following:
//this will produce 5x "sharper" print
MemoryImage = new Bitmap((Panel1.Width * 5), (Panel1.Height * 5));
Using Graphics g = Graphics.FromImage(MemoryImage) {
ScaleControls(Panel1, g, 5);
};
PrintPreviewDialog1.Document = printdoc1;
PrintPreviewDialog1.ShowDialog();
Now in the ScaleControls function, which is recursive, we scale location, size and font in order to make each control itself in higher resolution before we draw them to the bitmap:
private void ScaleControls(Control c, ref Graphics g, double s)
{
//To detach controls for panels, groupboxes etc.
List<Control> hold = null;
foreach (Control ctrl in c.Controls) {
if (ctrl is GroupBox || ctrl is Panel) {
//backup reference to controls
hold = new List<Control>();
foreach (Control gctrl in ctrl.Controls) {
hold.Add(gctrl);
}
ctrl.Controls.Clear();
}
//backup old location, size and font (see explanation)
Point oldLoc = ctrl.Location;
Size oldSize = ctrl.Size;
Font oldFont = ctrl.Font;
//calc scaled location, size and font
ctrl.Location = new Point(ctrl.Location.X * s, ctrl.Location.Y * s);
ctrl.Size = new Size(ctrl.Size.Width * s, ctrl.Height * s);
ctrl.Font = new Font(ctrl.Font.FontFamily, ctrl.Font.Size * 5,
ctrl.Font.Style, ctrl.Font.Unit);
//draw this scaled control to hi-res bitmap
using (Bitmap bmp = new Bitmap(ctrl.Size.Width, ctrl.Size.Height)) {
ctrl.DrawToBitmap(bmp, ctrl.ClientRectangle);
g.DrawImage(bmp, ctrl.Location);
}
//restore control's geo
ctrl.Location = oldLoc;
ctrl.Size = oldSize;
ctrl.Font = oldFont;
//recursive for panel, groupbox and other controls
if (ctrl is GroupBox || ctrl is Panel) {
foreach (Control gctrl in hold) {
ctrl.Controls.Add(gctrl);
}
ScaleControls(ctrl, g, s);
}
}
}
and finally in the event handler for printing:
double scale = MemoryImage.Width / e.PageBounds.Width;
e.Graphics.DrawImage(MemoryImage, 0, 0,
Convert.ToInt32(MemoryImage.Width / scale),
Convert.ToInt32(MemoryImage.Height / scale));
Now, in this example we scale the controls in-place. This is not ideal of course as they will appear to live their own life while we do a print preview.
Ideally we would clone each control as we iterated and discard it after we drew to bitmap. This would also eliminate the need to backup geometries. But for the example of principle I left it as-is. I'll leave it to you to clone etc.
The reason for detaching controls is that if we don't (as the code is now - this can surely be changed by providing another method of iterating, ie. pre-scale cloned controls) the non-scaled in the f.ex. GroupBox control will be printed first, then the scaled ones will show on top of those when they are iterated. This because we DrawToBitmap the GroupBox before scaling its controls. This is something I'll leave for you to handle.
The bitmap we're working on will not necessary fit the resolution of the print that the user end up with from setting up the print dialog, but we get a higher resolution to work with which in turn yield a better result than the poor screen bitmap resolution we have initially.
You would of course need to add support for special cases for other controls than Panel and GroupBox that can hold other controls, image controls and so forth.
I spent days looking for a way to print a panel and its contents in high quality. This didnt work, and I tried other peoples codes and they were all either wrong or just bad quality, until I found this:
http://rkinfopedia.blogspot.com/2008/07/printing-contents-of-panel-control.html
just put the eventhandler inside the print button click event handler, and include the print method in it too, like this:
private void button3_Click(object sender, EventArgs e)
{
printdoc1.PrintPage += new PrintPageEventHandler(printdoc1_PrintPage);
Print(panel1);
}
and place an if statement inside the override OnPaint method, like this:
protected override void OnPaint(PaintEventArgs e)
{
if (MemoryImage != null)
{
e.Graphics.DrawImage(MemoryImage, 0, 0);
base.OnPaint(e);
}
}
rest is fine as is, and youll finally get almost perfect print quality
Just wanted to share this gem, you are welcome internet stranger!
Thank you Mr. Rakesh!