Aero: How to draw solid (opaque) colors on glass? - aero

Using GDI+ to draw various colors:
brush = new SolidBrush(color);
graphics.FillRectangle(brush, x, y, width, height);
You'll notice that no opaque color shows properly on glass:
How do i draw solid colors on glass?
You'll also notice that a fully opaque color is handled differently depending on what color it is:
opaque black: fully transparent
opaque color: partially transparent
opaque white: fully opaque
Can anyone point me to the documentation on the desktop compositor that explains how different colors are handled?
Update 3
You'll also notice that FillRectangle behaves differently than FillEllipse:
FillEllipse with an opaque color draws an opaque color
FillRectangle with an opaque color draws partially (or fully) transparent
Explanation for non-sensical behavior please.
Update 4
Alwayslearning suggested i change the compositing mode. From MSDN:
CompositingMode Enumeration
The CompositingMode enumeration specifies how rendered colors are combined with background colors. This enumeration is used by the Graphics::GetCompositingMode and 'Graphics::SetCompositingMode' methods of the Graphics class.
CompositingModeSourceOver
Specifies that when a color is rendered, it is blended with the background color. The blend is determined by the alpha component of the color being rendered.
CompositingModeSourceCopy
Specifies that when a color is rendered, it overwrites the background color. This mode cannot be used along with TextRenderingHintClearTypeGridFit.
From the description of CompositingModeSourceCopy, it sounds like it's not the option i want. From the limitations it imposes, it sounds like the option i want. And with composition, or transparency disabled it isn't the option i want, since it performs a SourceCopy, rather than SourceBlend:
Fortunately it's not an evil i have to contemplate because it doesn't solve my actual issue. After constructing my graphics object, i tried changed the compositing mode:
graphics = new Graphics(hDC);
graphics.SetCompositingMode(CompositingModeSourceCopy); //CompositingModeSourceCopy = 1
The result has no effect on the output:
Notes
Win32 native
not .NET (i.e. native)
not Winforms (i.e. native)
GDI+ (i.e. native)
See also
Aero: How to draw ClearType text on glass?
Windows Aero: What color to paint to make “glass” appear?
Vista/7: How to get glass color?

Seems to work OK for me. With the lack of a full code example I'm assuming you've got your compositing mode wrong.
public void RenderGdiPlus()
{
List<string> colors = new List<string>(new string[] { "000000", "ff0000", "00ff00", "0000ff", "ffffff" });
List<string> alphas = new List<string>(new string[] { "00", "01", "40", "80", "c0", "fe", "ff" });
Bitmap bmp = new Bitmap(200, 300, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Graphics graphics = Graphics.FromImage(bmp);
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.None;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
SolidBrush backBrush = new SolidBrush(Color.FromArgb(254, 131, 208, 129));
graphics.FillRectangle(backBrush, 0, 0, 300, 300);
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
Pen pen = new Pen(Color.Gray);
for (int row = 0; row < alphas.Count; row++)
{
string alpha = alphas[row];
for (int column=0; column<colors.Count; column++)
{
string color = "#" + alpha + colors[column];
SolidBrush brush = new SolidBrush(ColorTranslator.FromHtml(color));
graphics.DrawRectangle(pen, 40*column, 40*row, 32, 32);
graphics.FillRectangle(brush, 1+40*column, 1+40*row, 31, 31);
}
}
Graphics gr2 = Graphics.FromHwnd(this.Handle);
gr2.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
gr2.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
gr2.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.None;
gr2.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
gr2.DrawImage(bmp, 0, 0);
}

I had a similar issue, but it involved drawing onto a layered window, rather than on Aero's glass. I haven't got any code with which I can test whether this solves your problem, but I figured it's worth a shot, since the symptoms of your problem are the same as mine.
As you have noticed, there seems to be some qwerks with FillRectangle, apparent by the differences between its behaviour and FillEllipse's.
Here are two work-arounds that I came up with, which each solve my issue:
Call FillRectangle twice
SolidBrush b(Color(254, 255, 0, 0));
gfx.FillRectangle(&b, Rect(0, 0, width, height));
gfx.FillRectangle(&b, Rect(0, 0, width, height));
Since the same area is being filled twice, they should blend and create RGB(255, 0, 0) regardless of the glass colour, which leads to a result of a 100% opaque shape. I do not prefer this method, as it requires every rectangle to be drawn twice.
Use FillPolygon instead
Just as with FillEllipse, FillPolygon doesn't seem to have the colour/opacity issue, unless you call it like so:
SolidBrush b(Color(255, 255, 0, 0));
Point points[4];
points[0] = Point(0, 0);
points[1] = Point(width, 0);
points[2] = Point(width, height);
points[4] = Point(0, height);
gfx.FillPolygon(&b, points, 4); //don't copy and paste - this won't work
For me, the above code resulted in a 100% transparent shape. I am guessing that this is either due to some form of optimisation that passes the call to FillRectangle instead. Or - most likely - there is some problem with FillPolygon, which is called by FillRectangle. Regardless, if you add an extra Point to the array, you can get around it:
SolidBrush b(Color(255, 255, 0, 0));
Point points[5];
points[0] = Point(0, 0);
points[1] = Point(0, 0); //<-
points[2] = Point(width, 0);
points[3] = Point(width, height);
points[4] = Point(0, height);
gfx.FillPolygon(&b, points, 5);
The above code indeed draws a 100% opaque shape for me. I hope this also resolves your issue.

Another day, another solution by me.
Draw everything you want to appear on glass into a bitmap.
Then, clear the form background with black color.
Immediately after this, draw the bitmap on your form.
However (as with any other solution not using DrawThemeTextEx):
Text rendering will not work correctly, because it always takes the back color of your form as an antialias/cleartype hint. Use DrawThemeTextEx instead, which also supports text with a glow effect behind.

I met the same issue with GDI.
GDI uses zero alpha channel value, so the simpliest solution is to fix alpha channel like this code does:
void fix_alpha_channel()
{
std::vector<COLORREF> pixels(cx * cy);
BITMAPINFOHEADER bmpInfo = {0};
bmpInfo.biSize = sizeof(bmpInfo);
bmpInfo.biWidth = cx;
bmpInfo.biHeight = -int(cy);
bmpInfo.biPlanes = 1;
bmpInfo.biBitCount = 32;
bmpInfo.biCompression = BI_RGB;
GetDIBits(memDc, hBmp, 0, cy, &pixels[0], (LPBITMAPINFO)&bmpInfo, DIB_RGB_COLORS);
std::for_each(pixels.begin(), pixels.end(), [](COLORREF& pixel){
if(pixel != 0) // black pixels stay transparent
pixel |= 0xFF000000; // set alpha channel to 100%
});
SetDIBits(memDc, hBmp, 0, cy, &pixels[0], (LPBITMAPINFO)&bmpInfo, DIB_RGB_COLORS);
}

I've found another way around it. Use LinearGradientBrush with both colors the same:
LinearGradientBrush brush(Point(0,0), Point(0,0), Color(255,231,45,56), Color(255,231,45,56));
g.FillRectangle(&brush, 25, 25, 30, 30);
This is perhaps slower than SolidBrush, but works fine.

Do you want a stupid solution? Here you get a stupid solution. At least it's just one line of code. And causing a small but ignorable side effect.
Assumption
When drawing solid, right angle rectangles, GDI+ tends to speed things up by drawing them in a faster method than drawing other stuff. This technique is called bitbliting. That is actually pretty clever since it is the fastest way to draw rectangles on a surface. However, the rectangles to be drawn must fulfill the rule that they are right angled.
This clever optimization was done before there was DWM, Aero, Glass and all the new fancy stuff.
Internally, bitblitting just copies the RGBA color data of pixels from one memory area to another (so to say from your drawing on your window). Sadly enough, the RGB format it writes is incompatible with glass areas, resulting in the weird transparency effects you observed.
Solution
So here comes a twist.
GDI+ can respect a transformation matrix, with which every drawing can be scaled, skewed, rotated or whatever. If we apply such a matrix, the rule that rectangles are right angled anymore is not guaranteed anymore. So, GDI+ will stop bitblitting these and draw them in a fashion similar to the ellipses.
But we also don't want to skew, scale or rotate our drawing. We simply apply the smallest transformation possible: We create a transformation matrix which moves every drawing down one pixel:
// If you don't get that matrix instance, ignore it, it's just boring math
e.Graphics.Transform = new Matrix(1f, 0.001f, 0f, 1f, 0f, 0f);
Now, bitblitting is off, rectangles are solid, violets are blue. If there would be just an easier way to control that, especially one not moving the drawings!
Thus said, if you want to draw on the first pixel row, use -1 as a Y coordinate.
You can decide if this really is a solution for you, or just ignore it.

Related

How do i get list of points which satisfy given QPainterPath?

I have a QGraphicsView in my Qt application on which user can draw curves. Curves consist of QGraphicsEllipseItem's and QGraphicsPathItem's, which connect the adjacent ellipses.
I want to get a list of QPoint's which satisfy the given curve. I tried creating local QPainterPath for this procedure which would represent the whole curve and iterating over all the points from it's rectangle to see which ones satisfy this curve. The code looks like:
QPainterPath curvePath = edges[index]->at(0)->path();
qreal left, right, bottom, top;
for(int i=1;i<edges[index]->size();i++)
{
curvePath.connectPath(edges[index]->at(i)->path());
}
QRectF curveRect = curvePath.boundingRect();
left = curveRect.left();
right = curveRect.right();
top = curveRect.top();
bottom = curveRect.bottom();
for(qreal i = left;i<right;i++)
for(qreal j = top;j<bottom;j++)
{
QPointF pointToCheck(i, j);
if(curvePath.contains(pointToCheck))
list.append(pointToCheck);
}
where edges is QList of QLists of QGraphicsPathItem's. It works fine in case of calculations (the point of applying this is to increase precision of calculation), but it really slows down my application since those calculations are made quite often.
Is there more efficient way to implement this?

Can I make QPainter fonts operate in the same units as everything else?

I started with this
void draw_text (QPainter & p, const QString & text, QRectF target)
{
float scale = calculate_font_scale (p, text, target); // about 0.0005
QFont f = p .font ();
float old_size = f .pointSizeF ();
f .setPointSizeF (old_size * scale);
p .setFont (f);
// this prints the new font size correctly
qWarning ("old: %f, new: %f", old_size, p .font () .pointSizeF ());
// but that doesn't seem to affect this at all
p .drawText (position, text);
}
The QPainter's font has size has been correctly updated, as the qWarning line indicates, but the text draws much, much to big. I think this is because the QPainter coordinate system has been zoomed-in quite a lot and it seems setPointSizeF only works with sizes of at least 1. By eye it seems that the font is one "unit" high so I'll buy that explanation, although it's stupid.
I experimented with using setPixelSize instead, and although p.fontMetrics().boundingRect(text) yields a sane-looking answer, it is given in pixel units. One requirement for the above-function is that the bounding rect of the text is horizontally and vertically centred with respect to the target argument, which is in coordinates of a vastly different scale, so the arithmetic is no longer valid and the text is drawn miles off-screen.
I want to be able to transform the coordinate system arbitrarily and if, at the point, one "unit" is a thousand pixels high and I'm drawing text in a 0.03x0.03 unit box then I want the font to be 30 pixels high, obviously, but I need all my geometry to be calculated in general units all the time, and I need fontMetrics::boundingRect to be in these same general units.
Is there any way out of this or do I have to dick around with pixel calculations to appease the font API?
You simply have to undo whatever "crazy" scaling there was on the painter.
// Save the state
p.save();
// Translate the center of `target` to 0,0.
p.translate(-target.center());
// Scale so that the target has a "reasonable" size
qreal dim = 256.0;
qreal sf = dim/qMin(target.height(), target.width());
p.scale(sf, sf);
// Draw your text
p.setPointSize(48);
p.drawText(QRectF(dim, dim), Qt::AlignCenter | Qt::WordWrap, text);
// Restore the state
p.restore();

Color value with alpha of zero shows up as black

I'm using .NET 4.0. I don't know if this is a framework bug or if it's a GDI+ thing. I just discovered it while writing an app to swap color channels.
Let me try to explain the problem. I'm reading pixels from one bitmap, swapping the channels, and writing them out to another bitmap. (Specifically, I'm setting the output image's RGB values equal to the input image's alpha, and output's alpha equal to the input's green channel… or, to put it succinctly, A => RGB and G => A.) The code is as follows:
for (int y = 0; y < input.Height; y++)
{
for (int x = 0; x < input.Width; x++)
{
Color srcPixel = input.GetPixel(x, y);
int alpha = srcPixel.A;
int green = srcPixel.G;
Color destPixel = Color.FromArgb(green, alpha, alpha, alpha);
output.SetPixel(x, y, destPixel);
}
}
Similarly, I've tried this:
int color = green << 24 | alpha << 16 | alpha << 8 | alpha;
Color destPixel = Color.FromArgb(color);
output.SetPixel(x, y, destPixel);
For the most part, it works.
The problem: regardless of what the RGB values are, when alpha is zero, the resultant RGB value is always pure black (R:0, G:0, B:0). I don't know if this is some sort of FromArgb() "optimization" — using .NET Reflector, I don't see FromArgb() doing anything strange — or if Bitmap.SetPixel is the culprit — more likely since it defers to native code and I can't look at it. Either way, when alpha is zero, the pixel is black. This is not the behavior I expected. I need to keep RGB channels intact.
At first I thought it was a pre-multiplied alpha issue, because I'm loading DDS files using my home-brewed DDS loader (which I built to spec and which has never given me any issues), but when I specify an explicit alpha of 255, like this:
Color destPixel = Color.FromArgb(255, alpha, alpha, alpha);
...the RGB channels show up correctly — i.e., none of them turns out black — so it's definitely something within GDI+ that erroneously assumes RGB values can be safely ignored if the alpha is zero… which, to me, seems like a pretty stupid assumption, but, whatever.
Further exacerbating the problem is that the Color type is immutable, which makes sense for a structure, but it means I can't create a color and then assign the alpha… which, if SetPixel() is the culprit, wouldn't matter anyway. (I've tested this by geting the pixel again immediately after setting it and seeing the same results: zero alpha = zero RGB).
So, my question: has anyone dealt with this issue and come up with a relatively simple workaround? In an effort to keep my dependencies down, I am loathe to import a third-party image library, but since GDI+ is making buggy assumptions about my color channels, I may not have a choice.
Thanks for your help.
EDIT: I solved this, but I can't post the answer for another seven hours. Awesome.
Sorry for the delay. Anyway, I should have worked on this a bit longer before posting, because I found a solution about five or ten minutes later. To be clear, I didn't find a solution to the stated GDI+ issue, but I found a suitable workaround. I thought about how, in other API's, I would lock a surface and transfer bytes directly to another surface, so I took that approach. After a little help from MSDN, here's my code (sans error handling):
Bitmap input = Bitmap.FromFile(filename) as Bitmap;
int byteCount = input.Width * input.Height * 4;
var inBytes = new byte[byteCount];
var outBytes = new byte[byteCount];
var inBmpData = input.LockBits(new Rectangle(0, 0, input.Width, input.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
Marshal.Copy(inBmpData.Scan0, inBytes, 0, byteCount);
for (int y = 0; y < input.Height; y++)
{
for (int x = 0; x < input.Width; x++)
{
int offset = (input.Width * y + x) * 4;
// byte blue = inBytes[offset];
byte green = inBytes[offset + 1];
// byte red = inBytes[offset + 2];
byte alpha = inBytes[offset + 3];
outBytes[offset] = alpha;
outBytes[offset + 1] = alpha;
outBytes[offset + 2] = alpha;
outBytes[offset + 3] = green;
}
}
input.UnlockBits(inBmpData);
Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
var outBmpData = output.LockBits(new Rectangle(0, 0, output.Width, output.Height), ImageLockMode.WriteOnly, output.PixelFormat);
Marshal.Copy(outBytes, 0, outBmpData.Scan0, outBytes.Length);
output.UnlockBits(outBmpData);
Notes: Marshal is under System.Runtime.InteropServices; BitmapData (inBmpData, outBmpData), ImageLockMode, and PixelFormat are under System.Drawing.Imaging.
Not only does this work perfectly, but it is phenomenally faster. I'll be using this technique from now on for all my channel swapping needs. (I've already used it in another, similar app.)
Sorry for the needless post. I at least hope this solution helps someone else.

Graphics.MeasureCharacterRanges giving wrong size calculations

I'm trying to render some text into a specific part of an image in a Web Forms app. The text will be user entered, so I want to vary the font size to make sure it fits within the bounding box.
I have code that was doing this fine on my proof-of-concept implementation, but I'm now trying it against the assets from the designer, which are larger, and I'm getting some odd results.
I'm running the size calculation as follows:
StringFormat fmt = new StringFormat();
fmt.Alignment = StringAlignment.Center;
fmt.LineAlignment = StringAlignment.Near;
fmt.FormatFlags = StringFormatFlags.NoClip;
fmt.Trimming = StringTrimming.None;
int size = __startingSize;
Font font = __fonts.GetFontBySize(size);
while (GetStringBounds(text, font, fmt).IsLargerThan(__textBoundingBox))
{
context.Trace.Write("MyHandler.ProcessRequest",
"Decrementing font size to " + size + ", as size is "
+ GetStringBounds(text, font, fmt).Size()
+ " and limit is " + __textBoundingBox.Size());
size--;
if (size < __minimumSize)
{
break;
}
font = __fonts.GetFontBySize(size);
}
context.Trace.Write("MyHandler.ProcessRequest", "Writing " + text + " in "
+ font.FontFamily.Name + " at " + font.SizeInPoints + "pt, size is "
+ GetStringBounds(text, font, fmt).Size()
+ " and limit is " + __textBoundingBox.Size());
I then use the following line to render the text onto an image I'm pulling from the filesystem:
g.DrawString(text, font, __brush, __textBoundingBox, fmt);
where:
__fonts is a PrivateFontCollection,
PrivateFontCollection.GetFontBySize is an extension method that returns a FontFamily
RectangleF __textBoundingBox = new RectangleF(150, 110, 212, 64);
int __minimumSize = 8;
int __startingSize = 48;
Brush __brush = Brushes.White;
int size starts out at 48 and decrements within that loop
Graphics g has SmoothingMode.AntiAlias and TextRenderingHint.AntiAlias set
context is a System.Web.HttpContext (this is an excerpt from the ProcessRequest method of an IHttpHandler)
The other methods are:
private static RectangleF GetStringBounds(string text, Font font,
StringFormat fmt)
{
CharacterRange[] range = { new CharacterRange(0, text.Length) };
StringFormat myFormat = fmt.Clone() as StringFormat;
myFormat.SetMeasurableCharacterRanges(range);
using (Graphics g = Graphics.FromImage(new Bitmap(
(int) __textBoundingBox.Width - 1,
(int) __textBoundingBox.Height - 1)))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
Region[] regions = g.MeasureCharacterRanges(text, font,
__textBoundingBox, myFormat);
return regions[0].GetBounds(g);
}
}
public static string Size(this RectangleF rect)
{
return rect.Width + "×" + rect.Height;
}
public static bool IsLargerThan(this RectangleF a, RectangleF b)
{
return (a.Width > b.Width) || (a.Height > b.Height);
}
Now I have two problems.
Firstly, the text sometimes insists on wrapping by inserting a line-break within a word, when it should just fail to fit and cause the while loop to decrement again. I can't see why it is that Graphics.MeasureCharacterRanges thinks that this fits within the box when it shouldn't be word-wrapping within a word. This behaviour is exhibited irrespective of the character set used (I get it in Latin alphabet words, as well as other parts of the Unicode range, like Cyrillic, Greek, Georgian and Armenian). Is there some setting I should be using to force Graphics.MeasureCharacterRanges only to be word-wrapping at whitespace characters (or hyphens)? This first problem is the same as post 2499067.
Secondly, in scaling up to the new image and font size, Graphics.MeasureCharacterRanges is giving me heights that are wildly off. The RectangleF I am drawing within corresponds to a visually apparent area of the image, so I can easily see when the text is being decremented more than is necessary. Yet when I pass it some text, the GetBounds call is giving me a height that is almost double what it's actually taking.
Using trial and error to set the __minimumSize to force an exit from the while loop, I can see that 24pt text fits within the bounding box, yet Graphics.MeasureCharacterRanges is reporting that the height of that text, once rendered to the image, is 122px (when the bounding box is 64px tall and it fits within that box). Indeed, without forcing the matter, the while loop iterates to 18pt, at which point Graphics.MeasureCharacterRanges returns a value that fits.
The trace log excerpt is as follows:
Decrementing font size to 24, as size is 193×122 and limit is 212×64
Decrementing font size to 23, as size is 191×117 and limit is 212×64
Decrementing font size to 22, as size is 200×75 and limit is 212×64
Decrementing font size to 21, as size is 192×71 and limit is 212×64
Decrementing font size to 20, as size is 198×68 and limit is 212×64
Decrementing font size to 19, as size is 185×65 and limit is 212×64
Writing VENNEGOOR of HESSELINK in DIN-Black at 18pt, size is 178×61 and limit is 212×64
So why is Graphics.MeasureCharacterRanges giving me a wrong result? I could understand it being, say, the line height of the font if the loop stopped around 21pt (which would visually fit, if I screenshot the results and measure it in Paint.Net), but it's going far further than it should be doing because, frankly, it's returning the wrong damn results.
I have a similar problem. I want to know how big the text I'm drawing is going to be, and where it's going to appear, EXACTLY. I haven't had the line-break problem, so I don't think I can help you there. I had the same problems you had with all the various measuring techniques available, including ending up with MeasureCharacterRanges, which worked okay for the left and right, but not at all for the height and top. (Playing with the baseline can work well for some rare applications though.)
I've ended up with a very inelegant, inefficient, but working solution, at least for my use case. I draw the text on a bitmap, check the bits to see where they ended up, and that's my range. Since I'm mostly drawing small fonts and short strings, it's been fast enough for me (especially with the memoization I added). Maybe this won't be exactly what you need, but maybe it can lead you down the right track anyway.
Note it requires compiling the project to allow unsafe code at the moment, as I'm trying to squeeze out every bit of efficiency from it, but that constraint could be removed if you wanted to. Also, it's not as thread safe as it could be right now, you could easily add that if you needed it.
Dictionary<Tuple<string, Font, Brush>, Rectangle> cachedTextBounds = new Dictionary<Tuple<string, Font, Brush>, Rectangle>();
/// <summary>
/// Determines bounds of some text by actually drawing the text to a bitmap and
/// reading the bits to see where it ended up. Bounds assume you draw at 0, 0. If
/// drawing elsewhere, you can easily offset the resulting rectangle appropriately.
/// </summary>
/// <param name="text">The text to be drawn</param>
/// <param name="font">The font to use when drawing the text</param>
/// <param name="brush">The brush to be used when drawing the text</param>
/// <returns>The bounding rectangle of the rendered text</returns>
private unsafe Rectangle RenderedTextBounds(string text, Font font, Brush brush) {
// First check memoization
Tuple<string, Font, Brush> t = new Tuple<string, Font, Brush>(text, font, brush);
try {
return cachedTextBounds[t];
}
catch(KeyNotFoundException) {
// not cached
}
// Draw the string on a bitmap
Rectangle bounds = new Rectangle();
Size approxSize = TextRenderer.MeasureText(text, font);
using(Bitmap bitmap = new Bitmap((int)(approxSize.Width*1.5), (int)(approxSize.Height*1.5))) {
using(Graphics g = Graphics.FromImage(bitmap))
g.DrawString(text, font, brush, 0, 0);
// Unsafe LockBits code takes a bit over 10% of time compared to safe GetPixel code
BitmapData bd = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
byte* row = (byte*)bd.Scan0;
// Find left, looking for first bit that has a non-zero alpha channel, so it's not clear
for(int x = 0; x < bitmap.Width; x++)
for(int y = 0; y < bitmap.Height; y++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.X = x;
goto foundX;
}
foundX:
// Right
for(int x = bitmap.Width - 1; x >= 0; x--)
for(int y = 0; y < bitmap.Height; y++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.Width = x - bounds.X + 1;
goto foundWidth;
}
foundWidth:
// Top
for(int y = 0; y < bitmap.Height; y++)
for(int x = 0; x < bitmap.Width; x++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.Y = y;
goto foundY;
}
foundY:
// Bottom
for(int y = bitmap.Height - 1; y >= 0; y--)
for(int x = 0; x < bitmap.Width; x++)
if(((byte*)bd.Scan0)[y*bd.Stride + 4*x + 3] != 0) {
bounds.Height = y - bounds.Y + 1;
goto foundHeight;
}
foundHeight:
bitmap.UnlockBits(bd);
}
cachedTextBounds[t] = bounds;
return bounds;
}
Ok so 4 years late but this question EXACTLY matched my symptoms and I've actually worked out the cause.
There is most certainly a bug in MeasureString AND MeasureCharacterRanges.
The simple answer is:
Make sure you divide your width restriction (int width in MeasureString or the Size.Width property of the boundingRect in MeasureCharacterRanges) by 0.72. When you get your results back multiply each dimension by 0.72 to get the REAL result
int measureWidth = Convert.ToInt32((float)width/0.72);
SizeF measureSize = gfx.MeasureString(text, font, measureWidth, format);
float actualHeight = measureSize.Height * (float)0.72;
or
float measureWidth = width/0.72;
Region[] regions = gfx.MeasureCharacterRanges(text, font, new RectangleF(0,0,measureWidth, format);
float actualHeight = 0;
if(regions.Length>0)
{
actualHeight = regions[0].GetBounds(gfx).Size.Height * (float)0.72;
}
The explanation (that I can figure out) is that something to do with the context is triggering a conversion in the Measure methods (that doesn't trigger in the DrawString method) for inch->point (*72/100). When you pass in the ACTUAL width limitation it is adjusting this value so the MEASURED width limitation is, in effect, shorter than it should be. Your text then wraps earlier than it is supposed to and so you get a longer height result than expected. Unfortunately the conversion applies to the actual height result as well so it's a good idea to 'unconvert' that value too.
Could you try removing the following line?
fmt.FormatFlags = StringFormatFlags.NoClip;
Overhanging parts of glyphs, and
unwrapped text reaching outside the
formatting rectangle are allowed to
show. By default all text and glyph
parts reaching outside the formatting
rectangle are clipped.
That's the best I can come up with for this :(
I also had some problems with the MeasureCharacterRanges method. It was giving me inconsistent sizes for the same string and even the same Graphics object. Then I discovered that it depends on the value of the layoutRect parametr - I can't see why, in my opinion it's a bug in the .NET code.
For example if layoutRect was completely empty (all values set to zero), I got correct values for the string "a" - the size was {Width=8.898438, Height=18.10938} using 12pt Ms Sans Serif font.
However, when I set the value of the 'X' property of the rectangle to a non-integer number (like 1.2), it gave me {Width=9, Height=19}.
So I really think there is a bug when you use a layout rectangle with non-integer X coordinate.
To convert from points to dpi as in screen resolution you need to divide by 72 and multiply by DPI, for example:
graphics.DpiY * text.Width / 72
Red Nightengale was really close, because graphics.DpiY is usually 96 for screen resolutions.

gdi+ Graphics::DrawImage really slow~~

I am using a GDI+ Graphic to draw a 4000*3000 image to screen, but it is really slow. It takes about 300ms. I wish it just occupy less than 10ms.
Bitmap *bitmap = Bitmap::FromFile("XXXX",...);
//--------------------------------------------
// this part takes about 300ms, terrible!
int width = bitmap->GetWidth();
int height = bitmap->GetHeight();
DrawImage(bitmap,0,0,width,height);
//------------------------------------------
I cannot use CachedBitmap, because I want to edit the bitmap later.
How can I improve it? Or is any thing wrong?
This native GDI function also draws the image into the screen, and it just take 1 ms:
SetStretchBltMode(hDC, COLORONCOLOR);
StretchDIBits(hDC, rcDest.left, rcDest.top,
rcDest.right-rcDest.left, rcDest.bottom-rcDest.top,
0, 0, width, height,
BYTE* dib, dibinfo, DIB_RGB_COLORS, SRCCOPY);
//--------------------------------------------------------------
If I want to use StretchDIBits, I need to pass BITMAPINFO, But how can I get BITMAPINFO from a Gdi+ Bitmap Object? I did the experiment by FreeImage lib, I call StretchDIBits using FreeImageplus object, it draw really fast. But now I need to draw Bitmap, and write some algorithm on Bitmap's bits array, how can I get BITMAPINFO if I have an Bitmap object? It's really annoying -___________-|
If you're using GDI+, the TextureBrush class is what you need for rendering images fast. I've written a couple of 2d games with it, getting around 30 FPS or so.
I've never written .NET code in C++, so here's a C#-ish example:
Bitmap bmp = new Bitmap(...)
TextureBrush myBrush = new TextureBrush(bmp)
private void Paint(object sender, PaintEventArgs e):
{
//Don't draw the bitmap directly.
//Only draw TextureBrush inside the Paint event.
e.Graphics.FillRectangle(myBrush, ...)
}
You have a screen of 4000 x 3000 resolution? Wow!
If not, you should draw only the visible part of the image, it would be much faster...
[EDIT after first comment] My remark is indeed a bit stupid, I suppose DrawImage will mask/skip unneeded pixels.
After your edit (showing StretchDIBits), I guess a possible source of speed difference might come from the fact that StretchDIBits is hardware accelerated ("If the driver cannot support the JPEG or PNG file image" is a hint...) while DrawImage might be (I have no proof for that!) coded in C, relying on CPU power instead of GPU's one...
If I recall correctly, DIB images are fast (despite being "device independent"). See High Speed Win32 Animation: "use CreateDIBSection to do high speed animation". OK, it applies to DIB vs. GDI, in old Windows version (1996!) but I think it is still true.
[EDIT] Maybe Bitmap::GetHBITMAP function might help you to use StretchDIBits (not tested...).
Just a thought; instead of retrieving the width and height of the image before drawing, why not cache these values when you load the image?
Explore the impact of explicitly setting the interpolation mode to NearestNeighbor (where, in your example, it looks like interpolation is not actually needed! But 300ms is the kind of cost of doing high-quality interpolation when no interpolation is needed, so its worth a try)
Another thing to explore is changing the colour depth of the bitmap.
Unfortunately when I had a similar problem, I found that GDI+ is known to be much slower than GDI and not generally hardware accelerated, but now Microsoft have moved on to WPF they will not come back to improve GDI+!
All the graphics card manufacturers have moved onto 3D performance and don't seem interested in 2D acceleration, and there's no clear source of information on which functions are or can be hardware accelerated or not. Very frustrating because having written an app in .NET using GDI+, I am not happy to change to a completely different technology to speed it up to reasonable levels.
i don't think they'll make much of a different, but since you're not actually needing to resize the image, try using the overload of DrawImage that doesn't (attempt) to resize:
DrawImage(bitmap,0,0);
Like i said, i doubt it will make any difference, because i'm sure that DrawImage checks the Width and Height of the bitmap, and if there's no resizing needed, just calls this overload. (i would hope it doesn't bother going through all 12 million pixels performing no actual work).
Update: My ponderings are wrong. i had since found out, but guys comment reminded me of my old answer: you want to specify the destination size; even though it matches the source size:
DrawImage(bitmap, 0, 0, bitmap.GetWidth, bitmap.GetHeight);
The reason is because of dpi differences between the dpi of bitmap and the dpi of the destination. GDI+ will perform scaling to get the image to come out the right "size" (i.e. in inches)
What i've learned on my own since last October is that you really want to draw a "cached" version of your bitmap. There is a CachedBitmap class in GDI+. There are some tricks to using it. But in there end i have a function bit of (Delphi) code that does it.
The caveat is that the CachedBitmap can become invalid - meaning it can't be used to draw. This happens if the user changes resolutions or color depths (e.g. Remote Desktop). In that case the DrawImage will fail, and you have to re-created the CachedBitmap:
class procedure TGDIPlusHelper.DrawCachedBitmap(image: TGPImage;
var cachedBitmap: TGPCachedBitmap;
Graphics: TGPGraphics; x, y: Integer; width, height: Integer);
var
b: TGPBitmap;
begin
if (image = nil) then
begin
//i've chosen to not throw exceptions during paint code - it gets very nasty
Exit;
end;
if (graphics = nil) then
begin
//i've chosen to not throw exceptions during paint code - it gets very nasty
Exit;
end;
//Check if we have to invalidate the cached image because of size mismatch
//i.e. if the user has "zoomed" the UI
if (CachedBitmap <> nil) then
begin
if (CachedBitmap.BitmapWidth <> width) or (CachedBitmap.BitmapHeight <> height) then
FreeAndNil(CachedBitmap); //nil'ing it will force it to be re-created down below
end;
//Check if we need to create the "cached" version of the bitmap
if CachedBitmap = nil then
begin
b := TGDIPlusHelper.ResizeImage(image, width, height);
try
CachedBitmap := TGPCachedBitmap.Create(b, graphics);
finally
b.Free;
end;
end;
if (graphics.DrawCachedBitmap(cachedBitmap, x, y) <> Ok) then
begin
//The calls to DrawCachedBitmap failed
//The API is telling us we have to recreate the cached bitmap
FreeAndNil(cachedBitmap);
b := TGDIPlusHelper.ResizeImage(image, width, height);
try
CachedBitmap := TGPCachedBitmap.Create(b, graphics);
finally
b.Free;
end;
graphics.DrawCachedBitmap(cachedBitmap, x, y);
end;
end;
The cachedBitmap is passed in by reference. The first call to DrawCachedBitmap it cached version will be created. You then pass it in subsequent calls, e.g.:
Image imgPrintInvoice = new Image.FromFile("printer.png");
CachedBitmap imgPrintInvoiceCached = null;
...
int glyphSize = 16 * (GetCurrentDpi() / 96);
DrawCachedBitmap(imgPrintInvoice , ref imgPrintInvoiceCached , graphics,
0, 0, glyphSize, glyphSize);
i use the routine to draw glyphs on buttons, taking into account the current DPI. The same could have been used by the Internet Explorer team to draw images when the user is running high dpi (ie is very slow drawing zoomed images, because they use GDI+).
/*
First sorry for ma English, and the code is partly in polish, but it's simple to understand.
I had the same problem and I found the best solution. Here it is.
Dont use: Graphics graphics(hdc); graphics.DrawImage(gpBitmap, 0, 0); It is slow.
Use: GetHBITMAP(Gdiplus::Color(), &g_hBitmap) for HBITMAP and draw using my function ShowBitmapStretch().
You can resize it and it is much faster! Artur Czekalski / Poland
*/
//--------Global-----------
Bitmap *g_pGDIBitmap; //for loading picture
int gRozXOkna, gRozYOkna; //size of working window
int gRozXObrazu, gRozYObrazu; //Size of picture X,Y
HBITMAP g_hBitmap = NULL; //for displaying on window
//------------------------------------------------------------------------------
int ShowBitmapStretch(HDC hdc, HBITMAP hBmp, int RozX, int RozY, int RozXSkal, int RozYSkal, int PozX, int PozY)
{
if (hBmp == NULL) return -1;
HDC hdc_mem = CreateCompatibleDC(hdc); //utworzenie kontekstu pamięciowego
if (NULL == hdc_mem) return -2;
//Trzeba połączyć BMP z hdc_mem, tzn. umieścić bitmapę w naszym kontekście pamięciowym
if (DeleteObject(SelectObject(hdc_mem, hBmp)) == NULL) return -3;
SetStretchBltMode(hdc, COLORONCOLOR); //important! for smoothness
if (StretchBlt(hdc, PozX, PozY, RozXSkal, RozYSkal, hdc_mem, 0, 0, RozX, RozY, SRCCOPY) == 0) return -4;
if (DeleteDC(hdc_mem) == 0) return -5;
return 0; //OK
}
//---------------------------------------------------------------------------
void ClearBitmaps(void)
{
if (g_hBitmap) { DeleteObject(g_hBitmap); g_hBitmap = NULL; }
if (g_pGDIBitmap) { delete g_pGDIBitmap; g_pGDIBitmap = NULL; }
}
//---------------------------------------------------------------------------
void MyOpenFile(HWND hWnd, szFileName)
{
ClearBitmaps(); //Important!
g_pGDIBitmap = new Bitmap(szFileName); //load a picture from file
if (g_pGDIBitmap == 0) return;
//---Checking if picture was loaded
gRozXObrazu = g_pGDIBitmap->GetWidth();
gRozYObrazu = g_pGDIBitmap->GetHeight();
if (gRozXObrazu == 0 || gRozYObrazu == 0) return;
//---Uworzenie bitmapy do wyświatlaia; DO IT ONCE HERE!
g_pGDIBitmap->GetHBITMAP(Gdiplus::Color(), &g_hBitmap); //creates a GDI bitmap from this Bitmap object
if (g_hBitmap == 0) return;
//---We need to force the window to redraw itself
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
}
//---------------------------------------------------------------------------
void MyOnPaint(HDC hdc, HWND hWnd) //in case WM_PAINT; DO IT MANY TIMES
{
if (g_hBitmap)
{
double SkalaX = 1.0, SkalaY = 1.0; //scale
if (gRozXObrazu > gRozXOkna || gRozYObrazu > gRozYOkna || //too big picture, więc zmniejsz;
(gRozXObrazu < gRozXOkna && gRozYObrazu < gRozYOkna)) //too small picture, można powiększyć
{
SkalaX = (double)gRozXOkna / (double)gRozXObrazu; //np. 0.7 dla zmniejszania; FOR DECREASE
SkalaY = (double)gRozYOkna / (double)gRozYObrazu; //np. 1.7 dla powiększania; FOR INCREASE
if (SkalaY < SkalaX) SkalaX = SkalaY; //ZAWSZE wybierz większe skalowanie, czyli mniejszą wartość i utaw w SkalaX
}
if (ShowBitmapStretch(hdc, g_hBitmap, gRozXObrazu, gRozYObrazu, (int)(gRozXObrazu*SkalaX), (int)(gRozYObrazu*SkalaX), 0, 0, msg) < 0) return;
Try using copy of Bitmap from file. FromFile function on some files returns "slow" image, but its copy will draw faster.
Bitmap *bitmap = Bitmap::FromFile("XXXX",...);
Bitmap *bitmap2 = new Bitmap(bitmap); // make copy
DrawImage(bitmap2,0,0,width,height);
I have made some researching and wasn't able to find a way to render images with GDI/GDI+ more faster than
Graphics.DrawImage/DrawImageUnscaled
and at the same time simple like it.
Till I discovered
ImageList.Draw(GFX,Point,Index)
and yeah it's really so fast and simple.

Resources