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.
Related
I have a strange problem. I made my own user control deriving from UserControl. I override OnPaint. Now I draw something in OnPaint. Let's say at position 0, 0.
If I call base.OnPaint after my custom drawing everything is fine. But if I call base.OnPaint before the stuff I'm drawing, it seems to ignore the containing control and the location is relative to the form instead of relative to the client area of the parent control. So when I draw at position (0, 0) it will effectively be drawn at negative x and y and I will only see a part of it. The base.OnPaint is UserControl.OnPaint. So I don't call my code there.
Here is an example:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var rect = new System.Drawing.Rectangle(this.ClientSize.Width - 16,
this.ClientSize.Height - 16, 16, 16);
e.Graphics.FillRectangle(new System.Drawing.SolidBrush(System.Drawing.Color.Red), rect);
//base.OnPaint(e);
}
In this case the red rectangle is displayed somewhere inside the client area but not at the lower right corner. If I uncomment the last line and comment the first line the red rectangle is displayed at the lower right corner as expected.
I don't get it. I did this many times and it always worked. So I tried to find any differences. The only I found is that I don't add my control in the designer but add it programmatically to another control with theContainingControl.Controls.Add(myMessedUpControl);.
This also happens for every parent-child-level I add. So if I create another control (another class) and also override OnPaint the behavior is the same if I add it to another user control.
Does anyone had this behavior before? How can I fix this? The problem is that I want to call base.OnPaint first and also everyone suggest this. But as I said I can't without messing the coordinates up.
One note: The coordinates are really 0, 0 in the debugger at the draw calls like DrawLine, DrawImage oder DrawString. But the result is displayed at negative coordinates (relative to the client area). It looks like the client coordinates are interpreted as client coordinates of the form. But I don't know why.
Found the problem
In my project there is a graphical overlay class which connects Paint event handlers to all controls in my form (the whole hierarchy). In this handler a transformation is performed. This graphical overlay kept me sleepless so many times. I guess I will remove it.
The Graphics object has a lot of mutable state. The order of operations matters if you mess with this mutable state - for example, you can use the Transform matrix to change the offset of everything rendered on the surface.
It sounds like your ascendant changes one of those during its own OnPaint handler without resetting it back. Try doing a e.Graphics.ResetTransform(); before you start your own painting. Make sure all the other state is also the way you want it (clip, DPI, ...).
In a panel's OnPaint event, I'm creating a circle like this:
e.Graphics.FillEllipse(
new SolidBrush(Color.FromArgb(128, 0, 0, 0)),
new Rectangle(0, 0, 100, 100));
It creates a decent enough circle that looks like this (the green part is just the form's background):
But I need it bigger. So I started playing around with the Rectangle, but no matter what combination of arguments I pass to it, I can't make a bigger circle. I always end up making so weird half/quarter shape thing.
So how do the arguments work in this case?
When you increase the Rectangle's Size, make sure the container control's (i.e. Panel) Size is big enough to fully display the Rectangle, and hence the circle too.
My goal is to draw text in a single layout with certain ranges different sizes and opacities.
The ID2D1RenderTarget::DrawTextLayout method seems to be the way to go.
The documentation for the defaultForegroundBrush parameter:
The brush used to paint any text in textLayout that does not already
have a brush associated with it as a drawing effect (specified by the
IDWriteTextLayout::SetDrawingEffect method).
According to the Remarks section of the IDWriteTextLayout::SetDrawingEffect method,
An ID2D1Brush, such as a color or gradient brush, can be set as a
drawing effect if you are using the ID2D1RenderTarget::DrawTextLayout
to draw text and that brush will be used to draw the specified range
of text.
This drawing effect is associated with the specified range and will be
passed back to the application by way of the callback when the range
is drawn at drawing time.
It sounds like ID2D1RenderTarget::DrawTextLayout will definitely use any brush set by IDWriteTextLayout::SetDrawingEffect. This unmanaged C++ answer seems to corroborate this idea.
However, in practice, DrawTextLayout ignores any SolidColorBrush I set using SetDrawingEffect. I get styles and sizes in the appropriate ranges, but everything is painted using the default brush.
I worked around this by implementing a custom text renderer (gist) which is dead simple and drew exactly what I expected from ID2D1RenderTarget::DrawTextLayout as per the documentation. I would have been satisfied but the performance of a TextRendererBase and DrawGlyphRun are more than 25% slower than ID2D1RenderTarget::DrawTextLayout.
What might be causing this issue? Can I use color as the documentation suggests and still use ID2D1RenderTarget::DrawTextLayout?
instead of:
layout.SetDrawingEffect(myBrush, new TextRange(1, 5));
call it like this:
layout.SetDrawingEffect(myBrush.NativePointer, new TextRange(1, 5));
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 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;
}