|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionTrouble living on the edge? Blurred borders when you resize an image or bitmap? Can't get that custom control to draw properly? Transparent edges that should be opaque? Pixel creep? Problems with The This article demonstrates a workaround that seems to solve the problem. BackgroundWhen authoring custom controls, it's fairly common to do custom painting in the Purists may prefer to do most of their painting using graphic methods like Those of us who are on tight deadlines, or have better things to do than push pixels around, are much more likely to cheat and draw a bitmap (or do a screen-grab) and paint the resulting image, or the required parts of it, to our control. The upside of this is it's quick and easy - particularly if the control needs a lot of skins. The downside is if the bitmap needs to be resized, it may lose sharpness. If it needs to be stretched significantly, and you don't know the simple trick to avoid it, then the dreaded BorderBug comes to life and the edges misbehave. Why does it happen?The sample project shows four ways to copy part of an image to a control. The following code does it the "obvious" way, and draws the top control in our demo solution, which shows unexpected red and green edges. This is how it works: the following line of code defines our source image, which is the 120 x 120 pixel image containing nine colored boxes. Bitmap sourceImage = Resource1.SourceImage;
To draw our control, we define the source rectangle as the part of the source image that we want. We then define a destination rectangle, and execute the protected override void OnPaint(PaintEventArgs e)
{
// This rectangle defines the part
// we want to use from the source image
Rectangle sourceRectangle = new Rectangle(40, 40, 40, 40);
// This rectangle defines where we want to draw on the control
Rectangle destinationRectangle = new Rectangle(0, 0,
this.Width, this.Height);
// And this procedure draws it for us. Easy. OR IS IT?
e.Graphics.DrawImage(sourceImage, destinationRectangle,
sourceRectangle, GraphicsUnit.Pixel);
// Bother! It didn't work properly!
base.OnPaint(e);
}
What do we get?
What happened? Did we get the source rectangle wrong and include part of the red and green squares? Clearly not. If the destination and source rectangles are identical in size, or fairly close, then no significant red or green edges can be seen. The answer lies in the fact that If the destination rectangle is exactly the same size as the source, then a perfect copy is possible as no stretching takes place. You can see this by resizing the form in the demo solution. If you get the three yellow controls roughly the same size as the yellow square in the source image, they are virtually perfect. Only at larger sizes do they start to fail. Towards a solutionPost this as a problem in any forum, and I'll practically guarantee you'll be told to change a graphics setting such as the One solution that seems promising is to copy the desired part of the source image to an intermediate image of the same size. That should theoretically be a pixel-for-pixel copy as no stretching takes place. There should be no effects from stray red or green pixels, and we should end up with a clean image that can then be stretched into the destination control. The following code draws the middle control in our demo solution, which has black edges: protected override void OnPaint(PaintEventArgs e)
{
// This rectangle defines the part
// we want to use from the source image
Rectangle sourceRectangle = new Rectangle(40, 40, 40, 40);
// Define an intermediate image the same size
// as the part we are taking from the source image
Bitmap intermediateImage = new Bitmap(40, 40);
// Get the graphics object of the intermediate image,
// so we can draw to it.
Graphics graphics = Graphics.FromImage(intermediateImage);
// This rectangle defines where we want
// to draw in the intermediate image
Rectangle intermediateRectangle = new Rectangle(0, 0,
intermediateImage.Width, intermediateImage.Height);
// Draw the part we want in the intermediate image.
graphics.DrawImage(sourceImage, intermediateRectangle,
sourceRectangle, GraphicsUnit.Pixel);
graphics.Dispose(); // Let's be tidy!
// This rectangle defines where we want to draw on the control
Rectangle destinationRectangle = new Rectangle(0, 0,
this.Width, this.Height);
// Draw the intermediate image on the control
e.Graphics.DrawImage(intermediateImage, destinationRectangle,
intermediateRectangle, GraphicsUnit.Pixel);
intermediateImage.Dispose(); // Let's be tidy!
// Surely that nailed it? Apparently not!!!!!
// Now the edges are transparent.
base.OnPaint(e);
}
The code copies the part of the source image that we want into an intermediate image of the same size. It then stretches the intermediate image onto the control. Did it work?
No. What happened this time? We have black edges! A close inspection of the image and a little diagnostic analysis shows that what we actually have are transparent edges. They seem black because the background color of the control is black and it's showing through. (It's only black because I set it that way to show the problem. If the control background color and the form background color are the same, the control just looks as if it fades away at the edge.) Why does it do that? It seems the
We did not succeed, but we are getting closer. We have the image we want. We should be able to solve the problem by adjusting the transparency. A WorkaroundWe can simply repeat the above steps, but before we paint the final image into the control, we can reset the Alpha (transparency) values. The following code draws the bottom control in our demo solution, which has no edge problems: protected override void OnPaint(PaintEventArgs e)
{
...
// Now create another intermediate image,
// this time the size of our destination
Bitmap intermediateImage2 = new Bitmap(this.Width, this.Height);
// This rectangle defines where we want
// to draw on the second intermediate image
Rectangle intermediateRectangle2 = new Rectangle(0, 0,
intermediateImage2.Width, intermediateImage2.Height);
// Get the graphics object of the second
// intermediate image, so we can draw to it.
Graphics graphics2 = Graphics.FromImage(intermediateImage2);
// Draw the first intermediate image on the second
// intermediate image (this is where it gets stretched)
graphics2.DrawImage(intermediateImage, intermediateRectangle2,
intermediateRectangle, GraphicsUnit.Pixel);
intermediateImage.Dispose(); // Let's be tidy!
graphics2.Dispose(); // Let's be tidy!
// Remove the alpha channel from the second
// intermediate image by cloning it to itself
intermediateImage2 = intermediateImage2.Clone(intermediateRectangle2,
PixelFormat.Format24bppRgb);
// This rectangle defines where we want to draw on the control
Rectangle destinationRectangle =
new Rectangle(0, 0, this.Width, this.Height);
// Draw the second intermediate image on the control AT THE SAME SIZE
e.Graphics.DrawImage(intermediateImage2, destinationRectangle,
intermediateRectangle2, GraphicsUnit.Pixel);
intermediateImage2.Dispose(); // Let's be tidy!
...
}
This code copies our intermediate image, which is still the same size as the source image, into a second intermediate image which is the same size as the destination, i.e., it stretches it and the second intermediate image will have transparent edges. The tricky part of the code resets the alpha so that the second intermediate image becomes completely non-transparent again: intermediateImage2 = intermediateImage2.Clone(intermediateRectangle2,
PixelFormat.Format24bppRgb);
We have simply cloned the image to itself, using a pixel format that does not include an alpha channel. This strips-off the alpha information. If converted back to an alpha pixel format, all alpha values will default to 255. We have removed the transparency. We can now paint the image to the control at the same size so there will be no further stretching or edge effects. Did it work?
Of course, it did! But it's a bit of a hack and takes several steps. There must be a better way... A better way.The root cause of this unexpected behavior is that the method considers the origin of the source rectangle to be the middle of the upper-left pixel, not the top-left corner of that pixel. This seems a bit odd, but you can see why it might be desirable to define a pixel by its middle for other graphic manipulations, such as rotations. We can get around this problem by starting our rectangle half a pixel up and to the left. This compensates for the method starting half a pixel down and to the right, i.e. our origin is now the top-left of the origin pixel. It may seem a bit strange to select half-pixels since we tend to think of them as discrete units, but it's quite valid.Why else would there be a Here's the code: protected override void OnPaint(PaintEventArgs e)
{
// This rectangle defines the part we want to use from the source image,
// but this time we offset the start point half a pixel up and to the
// left!
RectangleF sourceRectangle = new RectangleF(39.5f, 39.5f, 40, 40);
// This rectangle defines where we want to draw on the control
RectangleF destinationRectangle = new RectangleF(0, 0, this.Width,
this.Height);
// Set the interpolation mode to NearestNeighbor. (It should work now!)
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
// And this procedure draws it for us.
e.Graphics.DrawImage(sourceImage, destinationRectangle, sourceRectangle,
GraphicsUnit.Pixel);
// YES!!! A fast and easy solution
base.OnPaint(e);
}
Note that we have also set the interpolation mode to
...and it does. This is the best solution to the problem. AcknowledgementsThe first time I heard this peculiarity of But the biggest acknowledgement must go to GDI+ guru darrellp for his invaluable guidance through the GDI+ jungle. Thanks Darrell. History
| ||||||||||||||||||||