I'm creating what should be a very simple Win32 C++ app whose sole purpose it to ONLY display a semi-transparent PNG. The window shouldn't have any chrome, and all the opacity should be controlled in the PNG itself.
My problem is that the window doesn't repaint when the content under the window changes, so the transparent areas of the PNG are "stuck" with what was under the window when the application was initially started.
Here's the line where I setup the new window:
hWnd = CreateWindowEx(WS_EX_TOPMOST, szWindowClass, szTitle, WS_POPUP, 0, height/2 - 20, 40, 102, NULL, NULL, hInstance, 0);
For the call to RegisterClassEx, I have this set for the background:
wcex.hbrBackground = (HBRUSH)0;
Here is my handler for WM_PAINT message:
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
Gdiplus::Graphics graphics(hdc);
graphics.DrawImage(*m_pBitmap, 0, 0);
EndPaint(hWnd, &ps);
break;
}
One thing to note is that the application is always docked to the left of the screen and doesn't move. But, what's underneath the application may change as the user opens, closes or moves windows under it.
When the application first starts, it looks perfect. The transparent (and simi-transparent) parts of the PNG show through perfectly. BUT, when the background underneath the application changes, the background DOESN'T update, it just stays the same from when the application first started. In fact, WM_PAINT (or WM_ERASEBKGND does not get called when the background changes).
I've been playing with this for quite a while and have gotten close to getting 100% right, but not quite there. For instance, I've tried setting the background to (HBRUSH) NULL_BRUSH and I've tried handling WM_ERASEBKGND.
What can be done to get the window to repaint when the contents under it changes?
I was able to do exactly what I wanted by using the code from Part 1 and Part 2 of this series:
Displaying a Splash Screen with C++
Part 1: Creating a HBITMAP archive
Part 2: Displaying the window archive
Those blog posts are talking about displaying a splash screen in Win32 C++, but it was almost identical to what I needed to do. I believe the part that I was missing was that instead of just painting the PNG to the window using GDI+, I needed to use the UpdateLayeredWindow function with the proper BLENDFUNCTION parameter. I'll paste the SetSplashImage method below, which can be found in Part 2 in the link above:
void SetSplashImage(HWND hwndSplash, HBITMAP hbmpSplash)
{
// get the size of the bitmap
BITMAP bm;
GetObject(hbmpSplash, sizeof(bm), &bm);
SIZE sizeSplash = { bm.bmWidth, bm.bmHeight };
// get the primary monitor's info
POINT ptZero = { 0 };
HMONITOR hmonPrimary = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO monitorinfo = { 0 };
monitorinfo.cbSize = sizeof(monitorinfo);
GetMonitorInfo(hmonPrimary, &monitorinfo);
// center the splash screen in the middle of the primary work area
const RECT & rcWork = monitorinfo.rcWork;
POINT ptOrigin;
ptOrigin.x = 0;
ptOrigin.y = rcWork.top + (rcWork.bottom - rcWork.top - sizeSplash.cy) / 2;
// create a memory DC holding the splash bitmap
HDC hdcScreen = GetDC(NULL);
HDC hdcMem = CreateCompatibleDC(hdcScreen);
HBITMAP hbmpOld = (HBITMAP) SelectObject(hdcMem, hbmpSplash);
// use the source image's alpha channel for blending
BLENDFUNCTION blend = { 0 };
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;
// paint the window (in the right location) with the alpha-blended bitmap
UpdateLayeredWindow(hwndSplash, hdcScreen, &ptOrigin, &sizeSplash,
hdcMem, &ptZero, RGB(0, 0, 0), &blend, ULW_ALPHA);
// delete temporary objects
SelectObject(hdcMem, hbmpOld);
DeleteDC(hdcMem);
ReleaseDC(NULL, hdcScreen);
}
Use the SetLayeredWindowAttributesarchive function, this allows you to set a mask color that will become transparent, thus allowing the background to show through.
You will also need to configure your window with the layered flag, e.g.:
SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
After that it's fairly simple:
// Make red pixels transparent:
SetLayeredWindowAttributes(hwnd, RGB(255,0,0), 0, LWA_COLORKEY);
When your PNG contains semi-transparent pixels that you want to blend with the background, this becomes more complicated. You could try looking at the approach in this CodeProject article:
Cool, Semi-transparent and Shaped Dialogs with Standard Controls for Windows 2000 and Above
Related
What do I want to achive:
I want to create a window that behaves like a WS_OVERLAPPEDWINDOW | WS_SIZEBOX(can be minimized, maximized, closed from system context menu on Alt+Space, supports Window Snapping, has shadow if it is enabled in system Performance Options -> [x] Enable shadow under window and so on). It also should be borderless (without title bar, icon, application name in title bar and so on), just client region and nothing else.
So main goals are:
Has no border;
Supports default window functionality;
Drops shadow underneath.
Don't say that it is impossible. The most simple example of such a window is Telegram Desktop. They use Qt library. Don't say I should use it. I want to learn how they did this trick. Their window is borderless, without bugs (that are described below). I know that Qt uses OpenGL inside for their window. But I also know that it should be possible to draw with GDI+ only without OpenGL.
What I have already tried:
github:melak47/BorderlessWindow This window has a tiny border around. You can see it if you resize it really quickly holding by the top or left edge of the window. We can also set margins to be like MARGINS m{0,0,0,1} for DwmExtendFrameIntoClientArea(hWnd, &m);. But window will still remain the same border which could be seen if resize.
github:rossy/borderless-window This solution is better. It propose two variants to struggle with this problem.
The border here is hidden by enabling WS_EX_LAYERED window style with chroma keying by color (e.g. magenta). But it is obvious that now we need to perform checks every time we draw something on bitmap for a specified chroma color. If user draws clear magenta RGB(255, 0, 255) we can then replace it with RGB(254, 0, 255) to avoid holes inside. But I think it is a very bad solution that cause unneccessary checks for each pixel in bitmap.
The border is now considered as a part of client area. We disable WM_EX_LAYERED and color keying and using GDI+ to draw transparent colors on top border. The this is that you can't draw pure opaque color on this border (e.g. graphics.FillRect(&Gdiplus::SolidBrush{Gdiplus::Color{255, 255, 0, 0}}, Gdiplus::Rect{0, 0, windowWidht, windowHeight}) will not cover top border by red color - the border will be white or another color (I don't know what it depends on but I think it is window system color preference, like Personalize -> Colors -> [x] Title bars and window borders)). By the way you can cover it any color you want just by setting opacity of this color to 254. This will work. But the problem is still there: if we'll try to resize the window guess what.. we have this white border on top (or what you set this when MARGINS m{0,0,0,1} for DwmExtendFrameIntoClientArea(hWnd, &m);. And also notice that it will be a lot of pain when you will forget that you can't draw pure opaque color and draw something with GDI. So I come up to the next solution.
My idea is to draw with alpha value of 254 only the top most scanline of bitmap. Becase I also perform double buffering I can do it with AlphaBlend(hdc, 0, 0, width, 1, memHdc, 0, 0, width, 1, {AC_SRC_OVER, 0, 254, AC_SRC_ALPHA});. But this solution is still got it's own bug. When resize window we still got this border because it's been drawn faster than our window content. Also we got weird pixels on the left side if we resize the window by the top edge. And the last bug is that this alpha dissapears sometimes to and the top border is visible not only in undrawn client regions but also on top of validated regions.
The code of WM_PAINT procedure:
LRESULT wmPaint(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps = {};
HDC hdc = BeginPaint(hWnd, &ps);
auto [width, height] = dimensions.normal;
HDC memHdc = CreateCompatibleDC(hdc);
HBITMAP hBitmap = CreateCompatibleBitmap(hdc, width, height);
SelectObject(memHdc, hBitmap);
// Children perform their drawings on "back buffer" - memDc
handleChildrenDraw(memDc);
// Copy top line as transparent and other as always to paint over non-client area as it was client
BLENDFUNCTION bf = {};
bf.BlendOp = AC_SRC_OVER;
bf.SourceConstantAlpha = 254;
bf.AlphaFormat = AC_SRC_ALPHA;
auto alphaSuccess = AlphaBlend(hdc, 0, 0, width, 1, memHdc, 0, 0, width, 1, bf);
expect(alphaSuccess == TRUE);
auto copySuccess = BitBlt(hdc, 1, 0, width, height, memHdc, 1, 0, SRCCOPY);
expect(copySuccess == TRUE);
EndPaint(hWnd, &ps);
DeleteObject(memHdc);
DeleteObject(hBitmap);
return 0;
}
Sreenshot: window rendered with a white srtipe on top (border has been rendered for some reasons)
Question:
Can you please tell me how to fix this top border drawing bug or any better solution to draw borderless window with WinAPI? Thanks in advance.
I am trying to make make my ui in win32 everywhere white. The problem is that my tab control's background is not white , so the not the tab itself, but the party next to the tabs is grey.
I am handling WM_CTLCOLORSTATIC for the static controls, but it does not seems to work for the tab control.
case WM_CTLCOLORSTATIC:
{
HDC hEdit = (HDC)w_param;
SetBkMode(hEdit, TRANSPARENT);
SetTextColor(hEdit, RGB(0, 0, 0));
SetBkColor(hEdit, RGB(255, 255, 255));
// Do not return a brush created by CreateSolidBrush(...) because you'll get a memory leak
return (INT_PTR)GetStockObject(WHITE_BRUSH);
}
I hope there is an 'easy' way to make my entire ui white :)
Grz
You can not capture a message to draw on the grey background. The system draws everything in the WM_PRINTCLIENT. There is however a nice hack! The idea is from this post.
I do this (in my WM_PAINT handler):
Create a memory DC to draw into
Send a WM_PRINTCLIENT message to the tab control to get it to draw the tabs into your memory DC
Create a region which mirrors the shape of the tabs
Fill the parts of the memory DC outside this region (RGN_DIFF) with the desired background brush
Blt the result into the DC returned by BeginPaint
Call EndPaint and return, without calling the tab control's own WndProc of course :)
Step 3 is a bit fiddly as you have to know the location and shape of the tabs, but other
than that it's a pretty clean solution (see image below the following sample code). You
could probably use TransparentBlt to replace the system background colour instead.
I am using TransparentBlt in this solution:
create hdcMemTab, step 1
HBITMAP hBitmap, hBitmapOld ; //keep them till the end of the program
HDC hdcMemTab; //keep it till the end of the program
HDC hdc;
Rect rt;
hdc = CreateIC(TEXT("DISPLAY"), NULL, NULL, NULL);
hdcMemTab = CreateCompatibleDC(hdc);
GetWindowRect(hwnd_Tab, &rt);
rt.right = rt.right - rt.left;
rt.bottom = rt.bottom - rt.top;
rt.left = 0;
rt.top = 0;
hBitmap = CreateCompatibleBitmap(hdc, rt.right, rt.bottom);
hBitmapOld = SelectObject(hdcMemTab, hBitmap);
DeleteDC(hdc);
In WM_PAINT of the subclassed tab control:
RECT rt, rtTab;
HDC hdc = BeginPaint(hwnd, &ps);
GetWindowRect(hwnd_Tab, &rt);
rt.right = rt.right - rt.left;
rt.bottom = rt.bottom - rt.top;
rt.left = 0;
rt.top = 0;
//step 2
SendMessage(hwnd_Tab, WM_PRINTCLIENT, (WPARAM)hdcMemTab, PRF_CLIENT);
FillRect(hdc, &rt, gBrushWhite); //gBrushWhite has the desired background color
HRGN hRgn = CreateRectRgn(0, 0, 0, 0);
int n_items = TabCtrl_GetItemCount(hwnd_Tab);
//get tabs region, step 3
for(i = 0; i < n_items; i++){
TabCtrl_GetItemRect(hwnd_Tab, i, &rtTab);
HRGN hTabRgn = CreateRectRgn(rtTab.left, rtTab.top, rtTab.right, rt.bottom);
CombineRgn(hRgn, hRgn, hTabRgn, RGN_OR);
DeleteObject(hTabRgn);
}
GetRgnBox(hRgn, &rtTab);
DeleteObject(hRgn);
//step 5
TransparentBlt(hdc, 0, 0, rt.right, rt.bottom, hdcMemTab, 0, 0, rt.right, rt.bottom, RGB(240, 240, 240)); //(240, 240, 240) is the grey color
BitBlt(hdc, rtTab.left, rtTab.top, rtTab.right - 5, rtTab.bottom, hdcMemTab, rtTab.left, rtTab.top, SRCCOPY);
EndPaint(hwnd, &ps);
//step 6
return 0;
Sorry about my english. What I do is return 0 in WM_PRINTCLIENT message without doing anything, I mean, preventing WM_PRINTCLIENT from calling DefWindowProc. This causes the tabcontrol header to have the same background color as its parent window. The same goes for the TrackBar.
I only have it tested on windows 10, I would like to know if it works for win 7 too.
I'd like to create a Direct2D application that has a transparent background on which a few opaque complex controls are placed.
The problem can be broken into several sub-problems:
Architecture: Should the controls be implemented as child windows? I think that this is the correct approach, rather that creating Direct2D polygons that implement a child-window functionality.
I tried to implement this by initializing the parent window:
SetWindowLong(m_hwnd, GWL_EXSTYLE, GetWindowLong(m_hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
SetLayeredWindowAttributes(m_hwnd, 0, (255 * 50) / 100, LWA_ALPHA);
And create the child window as WS_CHILD. This resulted in a child on which all D2D drowings including background are transparent. I could not find a way to make the child opaque.
When I create the child window as WS_POPUP or WS_OVERLAPPED the opacity problem is solved but the child window is located on the desktop unrelated to the parent.
Layered Window?
I chose to work with Layered Windows but since I target at VistaSP2 and higher there might be better solutions.
I tried the solution offered here but I too failed to implement it.
Do you mean to create a 32-bit-per-pixel window?
(Sorry for being unable to comment, not enough rep over here)
In that case, you are FORCED to use UpdateLayeredWindow (and a CreateDIBSection call while you initialize), no matter what, every time you finished drawing the scene, after you finished drawing the scene, like:
// Draw to your D2D1 RenderTarget here
RECT rcWin = {0};
GetWindowRect(hWnd,&rcWin);
POINT ptw = {rcWin.left,rcWin.top};
SIZE pts = {rcWin.right-rcWin.left,rcWin.bottom-rcWin.top};
POINT ptsrc = {0};
HDC ScreenDC = GetDC(0);
UpdateLayeredWindow( hWnd, ScreenDC, &ptw, &pts, MemDC, &ptsrc, 0, &bf, ULW_ALPHA);
ReleaseDC(0,ScreenDC);
About the initialization:
RECT r = {0};
GetWindowRect(hWnd,&r);
HDC scrDC = GetDC(0);
MemDC = CreateCompatibleDC(scrDC);
ReleaseDC(0,scrDC);
if(!MemDC)
{ FailInit(); }
BITMAPINFO bmi = {0};
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biWidth = r.right-r.left;
bmi.bmiHeader.biHeight = r.bottom-r.top;
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
DIBSectionBitmap = CreateDIBSection(MemDC,&bmi,DIB_RGB_COLORS,0,0,0);
if(!DIBSectionBitmap)
return 0;
OldBmp = (HBITMAP)SelectObject(MemDC,DIBSectionBitmap);
// Now create the HWND D2D1 RenderTarget.
About the freeing of the resources:
// Free the D2D1 RenderTarget here
if(MemDC && OldBmp)
SelectObject(MemDC,OldBmp);
if(DIBSectionBitmap)
DeleteObject(DIBSectionBitmap);
if(MemDC)
DeleteDC(MemDC);
MemDC = 0;
OldBmp = 0;
DIBSectionBitmap = 0;
EDIT: MemDC, OldBmp and DIBSectionBitmap are Per-Window.
MemDC is a HDC.
OldBmp is a HBITMAP.
DIBSectionBitmap is a HBITMAP.
At this point you can draw your child windows as if they were part of your very own main window, with a per-pixel alpha precision, but you'll need to handle focusing and messaging on your own.
I register the window class like this:
WNDCLASSEX wctt;
wctt.cbSize = sizeof(WNDCLASSEX);
wctt.style = CS_DBLCLKS;
wctt.lpfnWndProc = WndProcTooltip;
wctt.cbClsExtra = 0;
wctt.cbWndExtra = 0;
wctt.hInstance = m_hAppInstance;
wctt.hIcon = NULL;
wctt.hCursor = LoadCursor(NULL, IDC_SIZE);
wctt.hbrBackground = NULL;
wctt.lpszMenuName = NULL;
wctt.lpszClassName = _T("myWindow");
wctt.hIconSm = NULL;
RegisterClassEx(&wctt)
As you can see I use wctt.hbrBackground = NULL; so it will have no background.
The window is created like this:
::CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
_T("myWindow"),
NULL,
WS_VISIBLE | WS_POPUP,
50,
50,
150,
100,
NULL,
NULL,
m_hAppInstance,
NULL);
In the paint section I draw icon on the window:
PAINTSTRUCT ps;
HDC hdc;
BITMAP bitmap;
ICONINFO iconinfo;
hdc = ::BeginPaint(hWnd, &ps);
::SetBkMode(hdc,TRANSPARENT);
::GetIconInfo(localIcon, &iconinfo);
::GetObject(iconinfo.hbmColor, sizeof(bitmap), &bitmap);
::DeleteObject(iconinfo.hbmColor);
::DeleteObject(iconinfo.hbmMask);
::DrawIconEx(hdc, 0,0, localIcon, bitmap.bmWidth, bitmap.bmHeight, 0, NULL, DI_NORMAL);
The icon size is smaller than the window size and I get on the background the current view on the window below the popup.
But now when I move the window (or minimize the window below the popup) the background is not changing.
I was trying to make a timer that each time do the flowing:
RECT rcClient;
GetClientRect(hWnd, &rcClient);
InvalidateRect(hWnd,&rcClient,TRUE);
This makes the print function run again but the background of the icon is not changing.
Should I do anything in WM_ERASEBKGND?
Does Anyone have any idea how to make it work?
thanks,
guy
It's not enough to just let the background stay unpainted; you also need to get the window below yours to repaint itself when necessary.
If the windows are part of the same hierarchy, created by the same thread, it is sufficient to give your window the WS_EX_TRANSPARENT extended style. This causes the window underneath to paint itself first so the background is always up-to-date.
Otherwise you need to use SetWindowRgn so that your window actually doesn't exist outside of the borders you wish to paint.
Look at Layered Window. This feature allows creating semi-transparent windows of different shapes.
Add WS_EX_LAYERED extended attribute in your window class.
You can control the transparency of your window with these two functions:
SetLayeredWindowAttributes:
bAlpha controls the opacity of the entire window, if you pass LWA_ALPHA in dwFlags.
When bAlpha is 0, the window is completely transparent. When bAlpha is 255, the window is opaque.
crKey sets the color that would transparent.
All pixels painted by the window in this color will be transparent.
UpdateLayeredWindow gives you precise control over window transparency, you can give different parts of window different levels of transparency.
If you're trying to create a non-rectangular window, this is not sufficient. Setting "no background" simply means the background will not be drawn, and you'll see whatever happens to be in memory at that location.
To create a non-rectangular window, have a look at the SetWindowRgn function.
I have the following code which sort of works provided you mask out the pink pixels however what I actually want is transparent bits like a PNG file so that I can avoid alpha blending issues and the need mask out a specific color everywhere the bitmap will be used.
CClientDC dc(pWnd);
CDC memDC;
if(!memDC.CreateCompatibleDC(&dc))
return NULL;
CRect bitmapRect;
bitmapRect.SetRectEmpty();
CFont* pOldFont = memDC.SelectObject(pWnd->GetFont());
CSize fontSize = memDC.GetTextExtent(imageText);
bitmapRect.right = fontSize.cx;
bitmapRect.bottom = fontSize.cy;
CBitmap bitmap;
if(!bitmap.CreateCompatibleBitmap(&dc, bitmapRect.Width(), bitmapRect.Height()))
return NULL;
CBitmap* pOldMemDCBitmap = memDC.SelectObject(&bitmap);
memDC.FillSolidRect(&bitmapRect, RGB(255,0,255));
//memDC.SetBkMode(TRANSPARENT); // doesn't work
//memDC.SetBkColor(TRANSPARENT); // doesn't work
memDC.SetTextColor(GetSysColor(COLOR_WINDOWTEXT));
//memDC.DrawText(imageText, bitmapRect, DT_TOP|DT_LEFT|DT_NOCLIP); // same difference
memDC.TextOut(0, 0, imageText);
memDC.SelectObject(pOldMemDCBitmap);
memDC.SelectObject(pOldFont);
memDC.DeleteDC();
CImageList bmImage;
bmImage.Create(bitmapRect.Width(), bitmapRect.Height(), ILC_COLOR32|ILC_MASK, 0, 1);
// this masks out the pink but for some windows blends edges of text causing pink text instead of black!
bmImage.Add(&bitmap, RGB(255,0,255));
Is just the bug filled beast that is MFC misbehaving or am I missing something?
Simple DrawText() with transparent background without MFC:
// in my case a user drawn button:
_windowHandle = CreateWindowEx(...);
SendMessage(_windowHandle, WM_SETFONT, (WPARAM)font, (LPARAM)NULL);
...
// WM_DRAWITEM
SetTextColor(hDC, RGB(216, 27, 27));
SetBkMode(hDC, TRANSPARENT);
RECT rect = { 0, 0, backgroundBitmap.bmWidth, backgroundBitmap.bmHeight };
DrawText(hDC, _text.c_str(), -1, &rect, DT_CENTER | DT_WORDBREAK);
--hfrmobile
About 10 minutes after asking this I read my own comment and realized that "some windows" means it was related to the windows being passed in. Specifically the font being used from said window. Fonts with default properties were exhibiting the strange blending.
At the end of the day I determined I needed to modify the font to turn off the things messing up my drawing code. I eventually narrowed it down to the one culprit causing the problem:
CClientDC dc(pWnd);
CDC memDC;
if(!memDC.CreateCompatibleDC(&dc))
return NULL;
LOGFONT tempFont;
CFont* winFont = pWnd->GetFont();
if (winFont)
winFont->GetLogFont(&tempFont);
else
{
// generate a likely font
SecureZeroMemory(&tempFont, sizeof(LOGFONT));
//TEXTMETRIC txt;
//GetTextMetrics(memDC, &txt);
//tempFont.lfHeight = txt.tmHeight * -1; // 16 is too big looking
tempFont.lfHeight = -12;
tempFont.lfWeight = FW_NORMAL;
tempFont.lfCharSet = DEFAULT_CHARSET;
wcscpy_s(tempFont.lfFaceName, L"Segoe UI"); // Win7 control default
}
tempFont.lfQuality = NONANTIALIASED_QUALITY; // this is the fix!!!
CFont newFont;
newFont.CreateFontIndirect(&tempFont);
CFont* pOldFont = memDC.SelectObject(&newFont);
// ... other stuff same as before ...
So I still FillSolidRect pink then draw my icons, text, whatever I want, etc. Then mask out the pink pixels. With the font quality adjustment it no longer blends pink into the font text and it looks good. The else case above creates a default font to use in case the CWnd* passed in doesn't have a valid one specified.