2

I'm working on a Win32 app with layered windows. The window contents are drawn using Cairo. The transparency is very unpredictable however. Sometimes everything is correct; opaque parts are opaque, transparent parts transparent. Other times what should be opaque becomes transparent or vanishes completely.

This is the most minimal example I could reduce the problem to (based on this answer). It should draw a square in the upper left corner of the screen, with the left half transparent grey and the right half opaque black.

import core.sys.windows.windows;

import std.utf;
import std.stdio;

import cairo;
import cairo_win32;

const UINT WIDTH = 500;
const UINT HEIGHT = 500;

extern(Windows)
LRESULT WndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam) nothrow {
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

void main(string[] args) {
    WNDCLASS wndclass;

    wndclass.lpszClassName = "Test".toUTF16z;
    wndclass.lpfnWndProc   = &WndProc;
    RegisterClass(&wndclass);
    HINSTANCE hInstance = GetModuleHandle(NULL);
    HWND hwnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TOOLWINDOW, wndclass.lpszClassName, "Test".toUTF16z, WS_POPUP, 50, 50, WIDTH, HEIGHT, NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, SW_SHOWNA);

    HDC hdcScreen = GetDC(NULL);

    // Offscreen hdc for painting
    HDC hdcMem = CreateCompatibleDC(hdcScreen);
    HBITMAP hbmMem = CreateCompatibleBitmap(hdcScreen, WIDTH, HEIGHT);
    auto hOld = SelectObject(hdcMem, hbmMem);

    // Draw using offscreen hdc
    auto surface = cairo_win32_surface_create(hdcMem);
    auto cr = cairo_create(surface);

    // Transparent grey
    cairo_rectangle(cr, 0, 0, 250, HEIGHT);
    cairo_set_source_rgba(cr, 0.4, 0.4, 0.4, 0.8);
    cairo_fill(cr);

    // Black
    cairo_rectangle(cr, 250, 0, 250, HEIGHT);
    cairo_set_source_rgba(cr, 0, 0, 0, 1);
    cairo_fill(cr);
    
    cairo_destroy(cr);
    cairo_surface_destroy(surface);

    // Show on screen
    BLENDFUNCTION blend = { 0 };
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;

    RECT win_rect;
    GetWindowRect(hwnd, &win_rect);
    
    POINT ptZero = POINT(0, 0);
    POINT win_pos = POINT(win_rect.left, win_rect.top);
    SIZE win_dims = SIZE(WIDTH, HEIGHT);

    UpdateLayeredWindow(hwnd, hdcScreen, &win_pos, &win_dims, hdcMem, &ptZero, RGB(0, 0, 0), &blend, ULW_ALPHA);

    // Reset offscreen hdc to default bitmap
    SelectObject(hdcMem, hOld);

    // Cleanup
    DeleteObject(hbmMem);
    DeleteDC (hdcMem);
    ReleaseDC(NULL, hdcScreen);

    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

This works, as you can see here. Square on desktop, left half transparent, right half opaque

However, when I now switch the order of the two Cairo draw calls like this:

// Opaque black
cairo_rectangle(cr, 250, 0, 250, HEIGHT);
cairo_set_source_rgba(cr, 0.01, 0.01, 0.01, 1.0);
cairo_fill(cr);

// Transparent grey
cairo_rectangle(cr, 0, 0, 250, HEIGHT);
cairo_set_source_rgba(cr, 0.4, 0.4, 0.4, 0.8);
cairo_fill(cr);

Then the opaque right half becomes completely transparent.

enter image description here

What's going on here? I assume I must be doing something more fundamentally wrong, probably with the whole HDC and UpdateLayeredWindow business.

Update: I tried to create an image surface with CAIRO_FORMAT_ARGB32, used the exact same drawing operations and wrote it to a PNG file. In that case both images look correct and identical.

It seems like the first operation can't have an alpha of exactly 1.0. If I set the first rectangle's alpha to 1.0, the second (black opaque) rectangle vanishes. If I set the first rectangle's alpha to 0.99 instead, the second rectangle is drawn correctly.

Rojetto
  • 33
  • 4
  • 2
    Don't use `UpdateLayeredWindow()` and `WM_PAINT` together. Use one or the other, not both. They are completely different UI methodologies. `WM_PAINT` draws a UI dynamically on an as-needed basis, use `SetLayeredWindowAttributes()` to specify /transparency/translucency for it. `UpdateLayeredWindow()` takes a prepared BMP and renders it when needed, no per-paint drawing needed, just update the BMP when something changes. – Remy Lebeau Jun 29 '21 at 20:39
  • @RemyLebeau Thanks for the clarification! I updated the example accordingly and moved all drawing outside of WM_PAINT. However, the original questions about draw order still stands (wasn't fixed by the change). Any idea about that? – Rojetto Jun 29 '21 at 21:08
  • Upon further experimentation this seems like a Cairo bug to me. I've reported it [here](https://gitlab.freedesktop.org/cairo/cairo/-/issues/494). – Rojetto Jul 01 '21 at 17:59

0 Answers0