2

I have a layered window that I create myself with the WS_EX_LAYERED extended style and the UpdateLayeredWindow function.

Then I draw some text in it using GDI+ library, Graphics::DrawString method.

And the result is this: Screenshot of the layered window.

As you can see, the japanese, korean and chinese characters are completely transparent. They even make the window's white background transparent, which is not transparent at all.

The problem occurs only on Windows Vista and Windows 7 when Desktop Composition (Aero theme) is disabled.
On Windows 10 it works fine, as Desktop Composition is always enabled there.

Why does this strange effect happen only with East-Asian characters?
And how can this be solved?

HHR
  • 41
  • 5
  • 1
    You are drawing on a 32bpp bitmap? I'm guessing the font engine changes the alpha values for some reason. – Anders Jun 09 '19 at 11:35
  • @Anders, yes, it's a 32bit DIB section created with `CreateDIBSection`. – HHR Jun 09 '19 at 12:13
  • You could verify that the alpha is incorrectly set to 0 by using a custom fill function that only sets the alpha values back to 255 after writing the strings. – Anders Jun 09 '19 at 12:20
  • @Anders, that's going to be problematic because I do need some parts of the window to be completely transparent and also the white background can be replaced by any other color, even semi-transparent. – HHR Jun 09 '19 at 12:26
  • What's even more strange is that when I change the text color to white over a dark background, the problem DOES NOT happen. – HHR Jun 09 '19 at 12:27
  • If it writes the white text pixels as 0xffffffff it will be setting the alpha to 255 and the end result looks correct. Again, verify this by setting the alpha parts of each pixel to 255 after writing the buggy string. – Anders Jun 09 '19 at 12:53
  • @Anders, Do I have to manipulate the bit values directly for that? GDI's `SetPixel` doesn't support alpha channel and GDI+'s `Bitmap::SetPixel` requires a call to `Bitmap::FromHBITMAP` which [according to the documentation](https://learn.microsoft.com/en-us/windows/desktop/api/gdiplusheaders/nf-gdiplusheaders-bitmap-fromhbitmap) "does not preserve the alpha channel". – HHR Jun 10 '19 at 07:28
  • Yes, directly on the bits. You might have to flush GDI first as well. – Anders Jun 10 '19 at 09:46
  • [mcve] please so we can try to reproduce. – zett42 Jun 22 '19 at 22:04

1 Answers1

0

I don't have a Windows 7 machine to test on so I don't know if the alpha channel is the real issue but assuming that it is, you can work around it by setting the alpha channel back to the correct state after writing the buggy text:

enum { WIDTH = 255 * 3, HEIGHT = 25 };
#define CalcStride(w, bpp) ( ((((w) * (bpp)) + 31) & ~31) >> 3 )
#define PMC(c, a) ( (c) = ((int)(c) * (a) / 255) )
#define PM(q) PMC( (q).rgbRed, (q).rgbReserved), PMC( (q).rgbGreen, (q).rgbReserved), PMC( (q).rgbBlue, (q).rgbReserved)

RGBQUAD* GetPxPtr32(void*pBits, UINT x, UINT y)
{
    return ((RGBQUAD*) ( ((char*)pBits) + (y * CalcStride(WIDTH, 32)) )) + x;
}

void SaveAlpha32(void*pBits, BYTE*buf)
{
    for (UINT x = 0; x < WIDTH; ++x)
        for (UINT y = 0; y < HEIGHT; ++y)
            buf[(y * WIDTH) + x] = GetPxPtr32(pBits, x, y)->rgbReserved;
}

void RestoreAlpha32(void*pBits, const BYTE*buf)
{
    for (UINT x = 0; x < WIDTH; ++x)
        for (UINT y = 0; y < HEIGHT; ++y)
            GetPxPtr32(pBits, x, y)->rgbReserved = buf[(y * WIDTH) + x];
}

void Draw(HDC hDC, HBITMAP hBM, void*pBits, UINT w, UINT h, bool isDwmActive)
{
    // Fill with white and a silly gradient alpha channel:
    for (UINT y = 0; y < h; ++y)
        for (UINT x = 0; x < w; ++x)
            (*(UINT32*)GetPxPtr32(pBits, x, y)) = 0xffffffff, GetPxPtr32(pBits, x, y)->rgbReserved = max(42, x % 255);

    BYTE *alphas = isDwmActive ? 0 : (BYTE*) LocalAlloc(LPTR, sizeof(BYTE) * w * h), fillWithRed = true;
    if (!isDwmActive) SaveAlpha32(pBits, alphas);
    HGDIOBJ hBmOld = SelectObject(hDC, hBM);
    RECT r = { 0, 0, WIDTH, HEIGHT };
    int cbk = SetBkColor(hDC, RGB(255, 0, 0));
    if (fillWithRed) ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &r, NULL, 0, NULL);
    int ctx = SetTextColor(hDC, RGB(0, 0, 0));
    int mode = SetBkMode(hDC, TRANSPARENT);
    DrawText(hDC, TEXT("Hello World Hello World Hello World Hello World Hello World"), -1, &r, DT_SINGLELINE|DT_VCENTER|DT_CENTER); // Plain GDI always destroys the alpha
    SetBkMode(hDC, mode), SetBkColor(hDC, cbk), SetTextColor(hDC, ctx);
    SelectObject(hDC, hBmOld), GdiFlush();
    if (!isDwmActive) RestoreAlpha32(pBits, alphas), LocalFree(alphas);
    for (UINT y = 0; y < h; ++y) for (UINT x = 0; x < w; ++x) PM(*GetPxPtr32(pBits, x, y));
}

int main()
{
    const INT w = WIDTH, h = HEIGHT, bpp = 32, x = 222, y = 222;
    HWND hWnd = CreateWindowEx(WS_EX_LAYERED|WS_EX_TOPMOST, WC_STATIC, 0, WS_VISIBLE|WS_POPUP, x, y, WIDTH, HEIGHT, 0, 0, 0, 0);
    SetWindowLong(hWnd, GWLP_WNDPROC, (LONG_PTR) DefWindowProc); // HACK
    BITMAPINFO bi;
    ZeroMemory(&bi, sizeof(bi));
    BITMAPINFOHEADER&bih = bi.bmiHeader;
    bih.biSize = sizeof(BITMAPINFOHEADER);
    bih.biWidth = w, bih.biHeight = -h;
    bih.biPlanes = 1, bih.biBitCount = bpp;
    bih.biCompression = BI_RGB;
    void*bits;
    HBITMAP hBmp = CreateDIBSection(NULL, &bi, DIB_RGB_COLORS, &bits, NULL, 0);
    HDC hDCScreen = GetDC(NULL), hDC = CreateCompatibleDC(hDCScreen);
    Draw(hDC, hBmp, bits, w, h, false);
    HGDIOBJ hBmOld = SelectObject(hDC, hBmp);
    BLENDFUNCTION blend = { 0 };
    blend.BlendOp = AC_SRC_OVER, blend.AlphaFormat = AC_SRC_ALPHA, blend.SourceConstantAlpha = 255;
    POINT location = { x, y }, srcpt = { 0, 0 };
    SIZE szWnd = { w, h };
    UpdateLayeredWindow(hWnd, hDCScreen, &location, &szWnd, hDC, &srcpt, 0, &blend, ULW_ALPHA);
    SelectObject(hDC, hBmOld), DeleteObject(hBmp);
    DeleteDC(hDC), ReleaseDC(NULL, hDCScreen);
    struct Closer { Closer(HWND h) { SetTimer(h, 1, 1000 * 11, TP); } static void CALLBACK TP(HWND h,UINT,UINT_PTR,DWORD) { ExitProcess(666); } } closer(hWnd); // HACK
    for (MSG msg; GetMessage(&msg, 0, 0, 0); ) DispatchMessage(&msg);
    return 666;
}

If you don't care about Vista without the platform update, you can try using Direct2D instead of GDI+.

Anders
  • 97,548
  • 12
  • 110
  • 164
  • Thanks. I'm still processing your answer as there's a lot of stuff happening in that code. I'll report back. – HHR Jun 10 '19 at 22:35
  • Only SaveAlpha32 and RestoreAlpha32 around the DrawText code is important, everything in main() can be ignored. – Anders Jun 10 '19 at 23:24
  • 1
    Done. It's indeed the alpha channel that is set to zero on those pixels. Saving and restoring the alphas would work, but I found by accident that if I change the text color from `0xFF000000` (opaque black) to `0xFE000000` (almost-opaque black) the problem goes away. And upon inspecting the alpha channel on those pixels, it ends up being `0xFF` and not `0xFE` which adds to the weirdness of the whole situation, but at least solves the problem. – HHR Jun 11 '19 at 00:41
  • If you want the per-pixel alpha to always be 255, why are you using UpdateLayeredWindow in the first place? The whole point of it is to be able to have per-pixel alpha variations. Use SetLayeredWindowAttributes if you only want to adjust the "transparency" of the whole window. – Anders Jun 11 '19 at 01:35
  • I need the background (not the text) to be translucent in some areas. That's why I use a layered window and a 32bpp DIB section. – HHR Jun 11 '19 at 02:38