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;
}
Related
I'm trying to use the following code to copy a portion of the screen to a new location on my Windows form.
private void Form1_Paint(object sender, PaintEventArgs e)
{
var srcPoint = new Point(0,0);
var dstPoint = new Point(Screen.PrimaryScreen.Bounds.Width/2, Screen.PrimaryScreen.Bounds.Height/2);
var copySize = new Size(100, 100);
e.Graphics.CopyFromScreen(srcPoint, dstPoint, copySize, CopyPixelOperation.SourceCopy);
}
The CopyFromScreen function appears to ignore any clips set before it.
e.SetClip(new Rectangle(srcPoint.X, srcPoint.Y, 20, 20));
Am I doing something wrong or is this just the wrong approach.
For context: I'm trying to mitigate a UI widescreen game issue by copying the HUD at the edges and to be centered closer to the middle.
I am aware of FlawlessWidescreen, but it doesn't support many less popular games. I suppose poking around in memory (what flawless does) could also work but is almost always against TOS.
Edit: final goal is to copy some arbitrary path as the shape rather than a simple rectangle (I was hoping from an image mask).
Edit #2:
So I have an irregular shape being drawn every 100ms. It turns out it just bogs the game down until I slow it down to every 500ms. But still the game isn't smooth. Is this operation of copying and drawing an image just going to be too heavy of an operation in GDI+? I was thinking it was simple enough to not bog anything down.
Thoughts before I mark the answer as accepted?
I guess it is indeed the wrong approach.
The ClippingRegion is only used for clipping the DrawXXX and FillXXX commands, including DrawImage (!).
The CopyFromScreen however will use the given Points and Size and not clip the source.
For a Rectangle region this is no problem since you can achieve the same result by choosing the right Point and Size values.
But once you aim at using more interesting clipping regions you will have to use an intermediate Bitmap onto which you copy from the screen and from which you can then use DrawImage into the clipped region.
For this you can create more or less complicated GraphicsPaths.
Here is a code example:
After putting the cliping coordinates into a Rectangleor GraphicsPath clip you can write something like this:
e.Graphics.SetClip(clip);
using (Bitmap bitmap = new Bitmap(ClientSize.Width, ClientSize.Height))
{
using (Graphics G = Graphics.FromImage(bitmap))
G.CopyFromScreen(dstPoint, srcPoint,
copySize, CopyPixelOperation.SourceCopy);
e.Graphics.DrawImage(bitmap, 0, 0);
}
Shouldn't it be
e.Graphics.Clip = myRegion;
I am drawing lines on a background image in a c# panel. The panel is anchored to the form so as the form resizes the panel resizes. The background image is set to be stretched so all you see as you resize the form is the background image.
My initial problem:
The lines drawn on the panel (via the OnPaint event) stay where they were originally drawn as the image resizes.
My current solution:
Record the location of the line and redraw it on a new bitmap by scaling the X and Y coordinates (works fine).
My new problem:
As you continually resize the window and draw lines you can't calculate the scaling factor from any point in time and apply it to all lines since the lines were originall drawn in different size images.
The two options I think I have:
After I redraw the line go through my array of lines and update the coordinate information so it now matches the current scale.
Or
In addition to storing the coordinate information of the line also store the size information of the panel at the time it was drawn so I can always calculate the scale for each line based on when it was drawn and the new panel size.
What I'm hoping for:
If you have thoughts on either of the two approaches that would be greatly appreciated....Even better would be to point me in the direction of a far better method to do this (I am fairly new to graphics processing in c#).
Can't write a comment, much as I want to. You do have a few options:
Draw your lines directly on the original Bitmap. This might not be an option for you, depending on the task.
Do it as you're doing now, keeping track of the lines' coordinates, updating them on resize, and redrawing them on Paint - if you use this, you'll be able to move and delete them, too,
Or do it by introducing a "scale factor" (float) which you update on every resize, and in your Paint event handler you draw everything using that scale factor. As you create a line, you calculate its coordinates using the scale factor BACK TO an unified coordinate system (scale factor 1) and then you don't have to modify your coordinates at all. This might be easy to debug due to that unified coordinate system. This is what I'd recommend, but it again depends on your task.
Draw to a full transparent Bitmap of the same size as your original image, use a scale factor like in the previous option. On creating a line, calculate its coordinates in the unified coordinate system, draw it on the Bitmap, and then on every Paint, draw the entire Bitmap over your original one. This, again, might not be an option if you need to delete or move your lines, or if you're tight on memory, or you don't want your lines to be blurred when you scale up, but somehow many ppl like this because it's like a "layer in Photoshop". :)
I have a BufferedGraphics instance and I draw some graphs on it. I'd like to create a function called DrawLegends that takes an instance of BufferedGraphics and draws two strings as legend.
I can create a PointF instance that points to (0, 0), but I want to put the legend on the bottom. How should I proceed with that? Can I do it with the BufferedGraphics instance or would I also need the panel that I'm drawing on?
The important thing is that you need to know the dimensions (mainly height) of the drawing canvas (i.e. the panel). This will be used to ultimately calculate the position of the legend. So if you don't have the height information stored elsewhere then yes, you will have to use the panel to some degree
At the end of the day pretty much all objects which are drawn to the screen can be manually drawn on, as under the covers they have or expose a graphics object to paint onto when you feel like it.
So if you do your drawing on a graphics object or whatever you are currently using, then when you are done drawing just paint that graphics object onto whatever control you want to display it in. As you can treat graphics objects a bit like images. There is no reason why you cannot pass in the underlying controls graphics object you want to paint onto rather than making your own graphics object, but if you have a method which does:
void DrawGraph(string xLegend, string yLegend, IList<XYValues> values, Graphics graphics);
Then you can draw onto that graphics object with the data, call invalidate and your done.
I've been working on a custom control and I've run into an issue with TextRenderer acting a bit surprisingly. In my OnPaint event I apply transform to the Graphics object to compensate for the scroll position like this:
e.Graphics.Transform = new System.Drawing.Drawing2D.Matrix(1, 0, 0, 1, this.AutoScrollPosition.X, this.AutoScrollPosition.Y);
Then I pass the graphic object to all sub elements of the control so that they paint themselves onto it. One of this elements should draw text string onto the graphics surface. And this is where I've got an issue. This line seems to work correctly when scrolling:
e.Graphics.DrawString(this.Text, this.Font, brush, new PointF(this.Rectangle.X, this.Rectangle.Y));
But when I use TextRenderer I get a completely different result. Heres the text line that supposed to draw the text:
TextRenderer.DrawText(e.Graphics, this.Text, this.Font, this.Rectangle, this.TextColor, TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.PreserveGraphicsTranslateTransform);
I thought that these two lines should produce the same result. But for some reason the second one applies the graphics transform differently and as a result, when I scroll the control all the text lines move around with different speed than the rest of the elements on the drawing surface. Could someone explain me why this is happening?
Here's my best guess at this: TextRenderer.DrawText is GDI-based and therefore resolution-dependant. Graphics.DrawString is GDI+ and therefore resolution-independant. See also this article.
Since you say that the texts "move around with different speed", probably what happens is that the GDI call uses a different "default" resolution than the one your Graphics object has. That'd mean that you'd have to adjust your AutoScrollCoordinates to respect the difference between your Graphics object resolution and the "default" GDI resolution.
I use the following code to draw line:
Graphics g = this.CreateGraphics();
Pen p = new Pen(Color.Black,3);
g.DrawLine(p,...);
// ...
Why the straight line is zigzag kind of, not straight and smooth at all. How could I make it straight and smoother?
You need to enable anti-aliasing. Set Graphics.SmoothingMode to AntiAlias as described here: http://msdn.microsoft.com/en-us/library/system.drawing.graphics.smoothingmode.aspx
Override the OnPaint() method of your form or implement the Paint event of a control. Use the passed e.Graphics object to draw. It will be properly initialized to draw anti-aliased lines. And can be double-buffered so it doesn't flicker. Call Invalidate() to force a repaint.
Using Control.CreateGraphics() is wrong in 99.9% of all cases. Whatever you draw cannot persist. It will be gone when you minimize and restore the window. Or when you partly move it off the screen and back. Or when you overlap another window on yours on XP and any machine that doesn't have Aero enabled. CreateGraphics() is only suitable for animations at frame rates larger than ~20 fps.