31
votes

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: alt text

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

alt text

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

alt text

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:

alt text

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:

alt text

Notes

  • Win32 native
  • not .NET (i.e. native)
  • not Winforms (i.e. native)
  • GDI+ (i.e. native)

See also

6
Needs more freehand circles.Matt Ball
I must be missing something because the answer that jumps out at me is, "Use 254 for the alpha value". Do you really need that extra 1/256th opacity?Tergiver
What happens if you use ExtTextOut instead of FillRectangle? That is the 'normal' way to draw rectangles in GDI, not sure about GDI+.Tergiver
What @Tergiver is referring to is that ExtTextOut has been used as a performance boost. It, along with PatBlt (was?) faster than FillRect. It doesn't require creation and management of a brush.Ian Boyd

6 Answers

1
votes

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);
}
1
votes

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.

1
votes

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.

0
votes

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);
}
0
votes

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.

0
votes

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.