When scrolling an image in a browser using the scrollbars - the image scrolls quickly and smoothly. On the other hand, making a tight loop with Graphics.DrawImage, incrementing the location's X-coordinate by 1 each iteration - returns a slow motion. (It's also somewhat jerky even after making the Control DoubleBuffered.)
How can I get fast rendering like a browser's?
EDIT
void DoNow()
{
Rectangle rec1 = new Rectangle(Point.Empty, panel1.BackgroundImage.Size);
Rectangle rec2 = new Rectangle(Point.Empty, panel1.BackgroundImage.Size);
using (Graphics g = Graphics.FromImage(panel1.BackgroundImage))
{
for (int i = 0; i < 100; i++)
{
rec2.Location = new Point(rec2.Location.X + 1, rec2.Location.Y);
g.DrawImage(image, rec1, rec2, GraphicsUnit.Pixel);
panel1.Refresh();
}
}
}
It seems from the comments like the answer to my question is that browsers use hardware acceleration which is unavailable to Winforms. (Please feel free to correct me if I'm wrong.)
The easiest solution is to use a Panel object with the scroll bars set to Auto. Place a picturebox as a child object with the size mode set to Auto. The picture box will expand to the size of the image that you assign to it. Since the picture box will expand to be larger than the panel, the panel's scroll bars will appear. When you scroll the image using the panel, it will be smooth.
If you still want to manually provide the drawing yourself, you are getting the "flicker" because the Invalidate method automatically performs background erasing which is not performed during the OnPaint event. You need to subclass the parent control that you are drawing on and override the OnPaintBackground event. Like below:
protected override void OnPaintBackground(PaintEventArgs pevent)
{
// base.OnPaintBackground(pevent);
}
Also, remember to perform ALL drawing during the OnPaint event and use the e.Graphics object.
Related
What I'm doing
I am working on a C#/.NET 4.7.2/WinForms app that draws a significant number of filled rectangles on a form using Graphics.FillRectangle.
Currently, the rectangles are drawn in the form's Paint event. After all the rectangles are drawn, a crosshair is drawn based on mouse position.
Whenever the mouse moves, Invalidate is called on the form to force a repaint so that the crosshair appears in its new position.
The problem
This is inefficient because the rectangles don't change, only the crosshair position, yet the rectangles are being redrawn every time. The CPU usage during mouse move is significant.
What next
I believe that the solution to this problem is to draw the rectangles to a buffer first (outside of the Paint event). Then, the Paint event only needs to render the buffer plus draw a crosshair on top.
Since I am new to GDI+ and manual buffering, I am wary of going down the wrong track. Google searches reveal plenty of articles on manual buffering, but each article seems to take a different approach which adds to my confusion.
I would be grateful for suggested approaches that favour simplicity and efficiency. If there is an idiomatic .NET way of doing this — the way it's meant to be done — I'd love to know.
Here's a quick and easy solution that doesn't require any buffering. To replicate this, start with a fresh Windows Forms project. I only draw two rectangles, but you can have as many as you want.
If you create a new WinForms project with these two member variables and these two handlers, you will get a working sample.
First, a couple of member variables for your form:
private bool _started = false;
private Point _lastPoint;
The started flag will turn to true after the first mouse move. The _lastPoint field will track the point at which the last cross-hairs was drawn (that's mostly why _started exists).
The Paint handler will draw the cross hairs every time it's called (you'll see why this is ok with the MouseMove handler):
private void Form1_Paint(object sender, PaintEventArgs e)
{
var graphics = e.Graphics;
var clientRectangle = this.ClientRectangle;
//draw a couple of rectangles
var firstRectangle = clientRectangle;
firstRectangle.Inflate(-20, -40);
graphics.FillRectangle(Brushes.Aqua, firstRectangle);
var secondRectangle = clientRectangle;
secondRectangle.Inflate(-100, -4);
graphics.FillRectangle(Brushes.Red, secondRectangle);
//draw Cross-Hairs
if (_started)
{
//horizontal
graphics.DrawLine(Pens.LightGray, new Point(clientRectangle.X, _lastPoint.Y),
new Point(ClientRectangle.Width + clientRectangle.X, _lastPoint.Y));
//vertical
graphics.DrawLine(Pens.LightGray, new Point(_lastPoint.X, clientRectangle.Y),
new Point(_lastPoint.X, ClientRectangle.Height + clientRectangle.Y));
}
}
Now comes the MouseMove handler. It's where the magic happens.
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
var clientRectangle = this.ClientRectangle;
var position = e.Location;
if (clientRectangle.Contains(position))
{
Rectangle horizontalInvalidationRect;
Rectangle verticalInvalidationRect;
if (_started)
{
horizontalInvalidationRect = new Rectangle(clientRectangle.X,
Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X - 1, clientRectangle.X),
clientRectangle.Y, 3, clientRectangle.Height);
Invalidate(horizontalInvalidationRect);
Invalidate(verticalInvalidationRect);
}
_started = true;
_lastPoint = position;
horizontalInvalidationRect = new Rectangle(clientRectangle.X,
Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X, clientRectangle.X - 1),
clientRectangle.Y, 3, clientRectangle.Height);
Invalidate(horizontalInvalidationRect);
Invalidate(verticalInvalidationRect);
}
}
If the cursor is within the form, I do a bunch of work. First I declare two rectangles that I will be using for invalidate. The horizontal one will be a rectangle that fills the width of the client rectangle, but is only 3 pixels high, centered on the Y coordinate of the area that I want to invalidate. The vertical one is as high as the client rectangle, but only 3 pixels wide. It's centered on the X coordinate of the area that I want to invalidate.
When the Paint handler runs, it virtually paints the entire client area, but only the pixels in the total invalidated area actually get drawn on the screen. Anything outside the invalidate area is left alone.
So, when the mouse moves, I create two rectangles (one vertical, one horizontal) that surround where the last set of cross-hairs were (so that when the pixels in those rectangles are drawn (including the background), the old cross-hairs are effectively erased) and then I create two new rectangles surrounding where the current cross-hairs should go (causing the background and the new cross-hairs to be drawn).
You are going to want to learn about invalidation rectangles if you have a complicated drawing app. For example, when the form is resized, what you want to do is invalidate only the newly unveiled rectangle(s), so that the whole drawing doesn't need to be rendered.
This works, but picking a color (or a brush) for the cross-hairs so that they always show can be difficult. Using my other suggestion (that you draw the lines twice (one to erase, one to draw) using an INVERT (i.e. XOR) brush is faster, and it always shows.
Moving Label inside parent's MouseMove-events causes trailing redraw.
I am (mis)using System.Windows.Forms.Label objects to realize
A more flexible tooltip,
Displaying a cross hair "cursor" extending to the borders of a parent control.
To that end I create the labels as members of the parent control (btw. it's a custom graphics control with mouse wheel zoom, panning etc.), add them to the parent's Controls-list, and move them inside the MouseMove-event of the parent. In order to ensure crisp movement behavior, I even call Label.Refresh when assigning the Location property of the Labels.
void GraphViewMouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (lastLocation != e.Location)
{
HasMoved = true;
if (IsPanning && e.Button == MouseButtons.Left)
{
if (isSelecting) PerformSelection(e.Location);
else PerformMove(e.Location,lastLocation);
}
lastLocation = e.Location;
if (Width > 0 && Height > 0)
{
PointD pos = CursorPosition;
// nothing interesting, basically like what is done with the crossHair's below
coordinateToolTip.Locate(lastLocation, pos.X, pos.Y);
crossHairX.Location = new Point(lastLocation.X, 0);
crossHairY.Location = new Point(0, lastLocation.Y);
crossHairX.Refresh();
crossHairY.Refresh();
}
}
}
Everything works almost fine (the labels move parallel to the mouse cursor) except that when I move the mouse a little faster, several copies of the Label(s) remain in the client area of the parent for some time like a visual echo.
That is, whereas the redraw of the labels at their new location appears to occur immediately, it seems to take some fraction of a second until the background of the parent (some complicated graphics, the complete redraw of which takes several seconds, but is definitely not redrawn during said simple mouse move) is restored to what has been behind the moving Label(s) originally. This is of course functionally irrelevant, but it simply looks ugly because it raises the impression that the code is inefficient.
Is there a way to enforce (faster) redraw of the cached background graphics? What determines the delay before the screen content behind a control is restored?
Edit: as to the suggestion of setting DoubleBuffered=true: according to MSDN this should already be true by default. Indeed it doesn't make a difference if I set it true for the Label. On the other hand, setting it false introduces additional flickering as expected. In either case the restoration of the background remains delayed, like described above.
#Hans Passant: Thanks for telling us "When you move a control, its parent's OnPaintBackground() method needs to run to over-draw the pixels that were left behind by the control in its previous position."
For me the cp.ExStyle |= 0x02000000; // Turn on WS_EX_COMPOSITED did not work.
I am creating movable, Resizable Panel and when I moved it fast from left to right it was not displayed right "the half of the panel was often gone". I did this and now it works exactly like when I move a Windows Window.
This is in the MyPanel Class which I made and Inherits from Panel
protected override void OnMove(EventArgs e)
{
this.Parent.Invalidate();
base.OnMove(e);
}
So when I move the Movable panel with the mouse it wil Invalidate the "Parrent" in my case the Form and now it moves fine.
i have a class that draws waveforms of audio. I'm drawing it in OnPaint function. Now i need to draw a line that shows where on the waveform we are at current moment. I can calculate the position but when i try to draw it i need to call Invalidate() and that forces form to redraw all that waveform chart data (a lot of points).
So is there a way to put some transparent element over this one and then call Invalidate() only on that element? i was trying with picture box but no sucess...
//main form
private void timer100ms_Tick(object sender, EventArgs e)
{
customWaveViewer1.currentPosition = (int)((stream.Position / (float)stream.Length) * customWaveViewer1.Width);
customWaveViewer1.overlayLabel.Invalidate(false);
}
//drawing function in my class
private void overlayLabelInvalidate(object sender, PaintEventArgs e)
{
Pen pen = new Pen(Color.FromArgb(255, 0, 0, 0));
e.Graphics.DrawLine(pen, currentPosition, 0, currentPosition, this.Height);
}
//constructor
public CustomWaveViewer()
{
InitializeComponent();
this.DoubleBuffered = true;
this.PenColor = Color.DodgerBlue;
this.PenWidth = 1;
overlayLabel = new PictureBox();
overlayLabel.Size = new System.Drawing.Size(this.Width, this.Height);
overlayLabel.Location = new Point(this.Left, this.Top);
overlayLabel.Visible = true;
overlayLabel.BackColor=Color.FromArgb(0,0,0,0);
overlayLabel.Paint += new System.Windows.Forms.PaintEventHandler(this.overlayLabelInvalidate);
Controls.Add(overlayLabel);
}
Actually what you are saying is not exactly true.
In the painteventargs there is a rectangle indicating the small portion of the window that needs to be repainted.
Also when you invalidate, you don't necessarily need to invalidate the whole form.
In your case you might want to invalidate only the old and new position of the marker that indicates where you are in the waveform.
So in your algorithm of your paint method, it is really up to you to make it efficient and only paint that part of the window that really needs repainting and to skip the part that does not need repainting.
It really can make a huge difference.
To make it even more look professional, set double buffering on.
No need to hastle with bitmaps of the whole image you have yourself, that is just what double buffering is all about, and forms can do it for you.
I copied following excerpt from https://msdn.microsoft.com/en-us/library/windows/desktop/dd145137(v=vs.85).aspx
BeginPaint fills a PAINTSTRUCT structure with information such as the dimensions of the portion of the window to be updated and a flag indicating whether the window background has been drawn. The application can use this information to optimize drawing. For example, it can use the dimensions of the update region, specified by the rcPaint member, to limit drawing to only those portions of the window that need updating. If an application has very simple output, it can ignore the update region and draw in the entire window, relying on the system to discard (clip) any unneeded output. Because the system clips drawing that extends outside the clipping region, only drawing that is in the update region is visible.
In this case there is no simple output and taking this into account is adviced.
I am not saying that creating a bitmap will not work. I am saying that optimizing your drawing logic will solve it just as well.
The information above still stands as windows forms is built on top of the old win32 api.
I have a User Control with completely custom drawn graphics of many objects which draw themselves (called from OnPaint), with the background being a large bitmap. I have zoom and pan functionality built in, and all the coordinates for the objects which are drawn on the canvas are in bitmap coordinates.
Therefore if my user control is 1000 pixels wide, the bitmap is 1500 pixels wide, and I am zoomed at 200% zoom, then at any given time I would only be looking at 1/3 of the bitmap's width. And an object which has a rectangle starting at point 100,100 on the bitmap, would appear at point 200,200 on the screen provided you were scrolled to the far left.
Basically what I need to do is create an efficient way of redrawing only what needs to be redrawn. For example, if I move an object, I can add the old clip rectangle of that object to a region, and union the new clip rectangle of that object to that same region, then call Invalidate(region) to redraw those two areas.
However doing it this way means I have to constantly convert the objects bitmap coordinates into screen coordinates before supplying them to Invalidate. I have to always assume that the ClipRectangle in PaintEventArgs is in screen coordinates for when other windows invalidate mine.
Is there a way that I can make use of the Region.Transform and Region.Translate capabilities so that I do not need to convert from bitmap to screen coordinates? In a way that it won't interfere with receiving PaintEventArgs in screen coordinates? Should I be using multiple regions or is there a better way to do all this?
Sample code for what I'm doing now:
invalidateRegion.Union(BitmapToScreenRect(SelectedItem.ClipRectangle));
SelectedItem.UpdateEndPoint(endPoint);
invalidateRegion.Union(BitmapToScreenRect(SelectedItem.ClipRectangle));
this.Invalidate(invalidateRegion);
And in the OnPaint()...
protected override void OnPaint(PaintEventArgs e)
{
invalidateRegion.Union(e.ClipRectangle);
e.Graphics.SetClip(invalidateRegion, CombineMode.Union);
e.Graphics.Clear(SystemColors.AppWorkspace);
e.Graphics.TranslateTransform(AutoScrollPosition.X + CanvasBounds.X, AutoScrollPosition.Y + CanvasBounds.Y);
DrawCanvas(e.Graphics, _ratio);
e.Graphics.ResetTransform();
e.Graphics.ResetClip();
invalidateRegion.MakeEmpty();
}
Since a lot of people are viewing this question I will go ahead and answer it to the best of my current knowledge.
The Graphics class supplied with PaintEventArgs is always hard-clipped by the invalidation request. This is usually done by the operating system, but it can be done by your code.
You can't reset this clip or escape from these clip bounds, but you shouldn't need to. When painting, you generally shouldn't care about how it's being clipped unless you desperately need to maximize performance.
The graphics class uses a stack of containers to apply clipping and transformations. You can extend this stack yourself by using Graphics.BeginContainer and Graphics.EndContainer. Each time you begin a container, any changes you make to the Transform or the Clip are temporary and they are applied after any previous Transform or Clip which was configured before the BeginContainer. So essentially, when you get an OnPaint event it has already been clipped and you are in a new container so you can't see the clip (your Clip region or ClipRect will show as being infinite) and you can't break out of those clip bounds.
When the state of your visual objects change (for example, on mouse or keyboard events or reacting to data changes), it's normally fine to simply call Invalidate() which will repaint the entire control. Windows will call OnPaint during moments of low CPU usage. Each call to Invalidate() usually will not always correspond to an OnPaint event. Invalidate could be called multiple times before the next paint. So if 10 properties in your data model change all at once, you can safely call Invalidate 10 times on each property change and you'll likely only trigger a single OnPaint event.
I've noticed you should be careful with using Update() and Refresh(). These force a synchronous OnPaint immediately. They're useful for drawing during a single threaded operation (updating a progress bar perhaps), but using them at the wrong times could lead to excessive and unnecessary painting.
If you want to use clip rectangles to improve performance while repainting a scene, you need not keep track of an aggregated clip area yourself. Windows will do this for you. Just invalidate a rectangle or a region that requires invalidation and paint as normal. For example, if an object that you are painting is moved, each time you want to invalidate it's old bounds and it's new bounds, so that you repaint the background where it originally was in addition to painting it in its new location. You must also take into account pen stroke sizes, etc.
And as Hans Passant mentioned, always use 32bppPArgb as the bitmap format for high resolution images. Here's a code snippet on how to load an image as "high performance":
public static Bitmap GetHighPerformanceBitmap(Image original)
{
Bitmap bitmap;
bitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format32bppPArgb);
bitmap.SetResolution(original.HorizontalResolution, original.VerticalResolution);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.DrawImage(original, new Rectangle(new Point(0, 0), bitmap.Size), new Rectangle(new Point(0, 0), bitmap.Size), GraphicsUnit.Pixel);
}
return bitmap;
}
I'm making a program where I have a lot of panels and panels in panels.
I have a few custom drawn controls in these panels.
The resize function of 1 panel contains code to adjust the size and position of all controls in that panel.
Now as soon as I resize the program, the resize of this panel gets actived. This results in a lot of flickering of the components in this panel.
All user drawn controls are double buffered.
Can some one help me solve this problem?
To get rid of the flicker while resizing the win form, suspend the layout while resizing. Override the forms resizebegin/resizeend methods as below.
protected override void OnResizeBegin(EventArgs e) {
SuspendLayout();
base.OnResizeBegin(e);
}
protected override void OnResizeEnd(EventArgs e) {
ResumeLayout();
base.OnResizeEnd(e);
}
This will leave the controls intact (as they where before resizing) and force a redraw when the resize operation is completed.
Looking at the project you posted, the flickering is really bad when you have the first tab selected, with the gradient-filled group boxes. With the second or third tab showing, there's hardly any flicker, if at all.
So clearly the problem has something to do with the controls you're showing on that tab page. A quick glance at the code for the custom gradient-filled group box class gives away the more specific cause. You're doing a lot of really expensive processing each time you draw one of the groupbox controls. Because each groupbox control has to repaint itself each time the form is resized, that code is getting executed an unbelievable number of times.
Plus, you have the controls' background set to "Transparent", which has to be faked in WinForms by asking the parent window to draw itself first inside the control window to produce the background pixels. The control then draws itself on top of that. This is also more work than filling the control's background with a solid color like SystemColors.Control, and it's causing you to see the form's pixels being drawn while you resize the form, before the groupboxes have a chance to paint themselves.
Here's the specific code I'm talking about from your custom gradient-filled groupbox control class:
protected override void OnPaint(PaintEventArgs e)
{
if (Visible)
{
Graphics gr = e.Graphics;
Rectangle clipRectangle = new Rectangle(new Point(0, 0), this.Size);
Size tSize = TextRenderer.MeasureText(Text, this.Font);
Rectangle r1 = new Rectangle(0, (tSize.Height / 2), Width - 2, Height - tSize.Height / 2 - 2);
Rectangle r2 = new Rectangle(0, 0, Width, Height);
Rectangle textRect = new Rectangle(6, 0, tSize.Width, tSize.Height);
GraphicsPath gp = new GraphicsPath();
gp.AddRectangle(r2);
gp.AddRectangle(r1);
gp.FillMode = FillMode.Alternate;
gr.FillRectangle(new SolidBrush(Parent.BackColor), clipRectangle);
LinearGradientBrush gradBrush;
gradBrush = new LinearGradientBrush(clipRectangle, SystemColors.GradientInactiveCaption, SystemColors.InactiveCaptionText, LinearGradientMode.BackwardDiagonal);
gr.FillPath(gradBrush, RoundedRectangle.Create(r1, 7));
Pen borderPen = new Pen(BorderColor);
gr.DrawPath(borderPen, RoundedRectangle.Create(r1, 7));
gr.FillRectangle(gradBrush, textRect);
gr.DrawRectangle(borderPen, textRect);
gr.DrawString(Text, base.Font, new SolidBrush(ForeColor), 6, 0);
}
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
if (this.BackColor == Color.Transparent)
base.OnPaintBackground(pevent);
}
And now that you've seen the code, red warning flags ought to go up. You're creating a bunch of GDI+ objects (brushes, pens, regions, etc.), but failing to Dispose any of them! Almost all of that code should be wrapped in using statements. That's just sloppy coding.
Doing all of that work costs something. When the computer is forced to devote so much time to rendering controls, other things lag behind. You see a flicker as it strains to keep up with the resize. It's no different than anything else that overloads a computer (like a computing the value of pi), it's just really easy to do so when you use as many custom drawn controls like you do here. Transparency is hard in Win32, and so is a lot of custom 3D painting. It makes the UI look and feel clunky to the user. Yet another reason that I don't understand the rush away from native controls.
You really only have three options:
Deal with the flicker. (I agree, this is not a good option.)
Use different controls, like the standard, built-in ones. Sure, they may not have a fancy gradient effect, but that's going to look broken half of the time anyway if the user has customized their Windows theme. It's also reasonably hard to read black text on a dark gray background.
Change the painting code within your custom controls to do less work. You may be able to get by with some simple "optimizations" that don't cost you any of the visual effects, but I suspect this is unlikely. It's a tradeoff between speed and eye candy. Doing nothing is always faster.
I successfully eliminate flicker when form resize using this code. Thanks.
VB.NET
Public Class Form1
Public Sub New()
Me.SetStyle(ControlStyles.UserPaint Or ControlStyles.OptimizedDoubleBuffer Or ControlStyles.AllPaintingInWmPaint Or ControlStyles.SupportsTransparentBackColor, True)
End Sub
Private Sub Form1_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
Me.Update()
End Sub
End Class
C#
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Resize += Form1_Resize;
this.SetStyle(ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.SupportsTransparentBackColor, true);
}
private void Form1_Resize(object sender, System.EventArgs e)
{
this.Update();
}
}
So I ran into this same problem - my control with a transparent background was repainting like 34 times, and what worked for me was:
On my form that contained the control
protected override void OnResize(EventArgs e)
{
myControl.Visible = false;
base.OnResize(e);
myControl.Visible = true;
}
And the same in the control:
protected override void OnResize(EventArgs e)
{
this.Visible = false;
base.OnResize(e);
this.Visible = true;
}
This reduced the amount of repainting to 4, which effectively eliminated any flicker when the control was being resized.
Maybe a good solution for you will be to use Form.ResizeBegin and Form.ResizeEnd events.
On ResizeBegin set main panel visibility to false, on ResizeEnd set main panel visibility to true.
This way panels will not be redrawn while someone is resizing your form.
While hooking into ResizeBegin and ResizeEnd is the right idea, instead of hiding the main panel's visibility I'd instead delay any resize calculations until ResizeEnd. In this case, you don't even need to hook into ResizeBegin or Resize - all the logic goes into ResizeEnd.
I say this for two reasons. One, even though the panel is hidden, the resize operation will likely still be expensive and so the form won't feel as responsive as it should unless the resize calculations are delayed. Two, hiding the pane's contents while resizing can be jarring to the user.
I had the same problem.
It seams that this is happening because you are using rounded corners. When I set CornerRadius property to 0, the flickering was gone.
So far I have only found the following workaround. Not the nicest one, but it stops the flickering.
private void Form_ResizeBegin(object sender, EventArgs e)
{
rectangleShape.CornerRadius = 0;
}
private void Form_ResizeEnd(object sender, EventArgs e)
{
rectangleShape.CornerRadius = 15;
}
I Had A Similar Issue Like This. My Entire Form Was Resizing Slowly And The Controls Painted Them In An Ugly Manner. So This Helped Me:
//I Added This To The Designer File, You Can Still Change The WindowState In Designer View If You Want. This Helped Me Though.
this.WindowState = FormWindowState.Maximized;
And In The Resize Event, Add This Code To The Beginning
this.Refresh();