I want to create a custom button using Win32. After searching, I found a group of methods, including using BS_OWNERDRAW
, and I got the desired result and this is when the button is rectangular, but when I wanted to create a rounded button, I faced a problem, which is the appearance of a white background behind the button and despite a lot of research, I did not find the right way to get rid of the white background. I hope you can help me.
This is the code I used:
#define UNICODE
#include <windows.h>
#include <gdiplus.h>
using namespace Gdiplus;
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// Define window properties variables
const wchar_t g_szClassName[] = L"myWindowClass";
WNDCLASSEX wc;
HWND hwnd;
MSG Msg;
// Registering the Window Class
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = MainWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = g_szClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wc))
{
MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK);
return 1;
}
// Creating the window
HWND hWnd = CreateWindowEx(0, L"myWindowClass", L"New Window",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
500, 350, NULL, NULL, GetModuleHandle(NULL), NULL);
if (hWnd == NULL)
{
MessageBox(NULL, L"Failed to create window", L"Error", MB_OK);
return 1;
}
// Show the window
ShowWindow(hWnd, SW_SHOW);
// Message loop
while (GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
return Msg.wParam;
}
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HWND buttonHandle;
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_CREATE:
{
// cretae the button
buttonHandle = CreateWindowEx(0, L"BUTTON", L"OK",
WS_VISIBLE | WS_CHILD | BS_OWNERDRAW,
50, 50, 200, 100, hwnd, (HMENU)1, NULL, NULL);
break;
}
case WM_PAINT:
{
// fill the window with a solid color
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
HBRUSH hBrush = CreateSolidBrush(RGB(30, 30, 30));
FillRect(hdc, &ps.rcPaint, hBrush);
DeleteObject(hBrush);
EndPaint(hwnd, &ps);
return 0;
}
case WM_DRAWITEM:
{
LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;
// Get button rectangle
RECT buttonRect = lpdis->rcItem;
// Create Gdiplus::Graphics object from device context
Gdiplus::Graphics graphics(lpdis->hDC);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); // Set anti-alias mode for smoother edges
// Define button color with alpha channel (transparency)
Gdiplus::Color buttonColor = Gdiplus::Color(255, 170, 70, 70);
// Create solid brush for button color
Gdiplus::SolidBrush brush(buttonColor);
int x = buttonRect.left;
int y = buttonRect.top;
int width = buttonRect.right - buttonRect.left;
int height = buttonRect.bottom - buttonRect.top;
// Draw a rounded rectangle
int cornerRadius = 40;
Gdiplus::GraphicsPath path;
path.AddArc(x, y, cornerRadius, cornerRadius, 180, 90);
path.AddArc(x + width - cornerRadius, y, cornerRadius, cornerRadius, 270, 90);
path.AddArc(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, cornerRadius, 0, 90);
path.AddArc(x, y + height - cornerRadius, cornerRadius, cornerRadius, 90, 90);
path.CloseFigure();
graphics.FillPath(&brush, &path);
break;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
The result was as follows
UPDATE:
Among the answers provided below is an answer from Remy Lebeau. The first method did not work.
The second method, where you use SetLayeredWindowAttributes
, gave a result closer to what is required, but with some impurities, as in the picture.
UPDATE 2:
The first method provided by Remy Lebeau is successful after adding WS_CLIPSIBLINGS
, but I lost the smooth edges feature, and this is a picture of the result :
We can get the same result using the second method if we do not use SetSmoothingMode
, but here we also lose the advantage of smooth edges.
First off, you are calling GdiplusStartup()
in the wrong place. By having it at the top of MainWndProc()
, you are calling it for every message that your window receives. You need to call GdiplusStartup()
only once, so move in into WinMain()
instead. And also, you need to call GdiplusShutdown()
before exiting from WinMain()
.
Now, that being said, your button has a default rectangular shape, but you are simply not drawing it as a rectangle. That is why you are seeing the white areas.
You can use CreateRoundRectRgn()
and SetWindowRgn()
to give the button a true non-rectangular shape to match your drawing.
For example:
#define UNICODE
#include <windows.h>
#include <gdiplus.h>
using namespace Gdiplus;
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
// Starting GDI+
if (GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL) != Status::Ok)
{
MessageBox(NULL, L"Failed to start GDI+", L"Error", MB_OK);
return 1;
}
// Registering the Window Class
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = MainWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));
wc.lpszMenuName = NULL;
wc.lpszClassName = L"myWindowClass";
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wc))
{
MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK);
GdiplusShutdown(gdiplusToken);
return 1;
}
// Creating the window
HWND hWnd = CreateWindowEx(0, wc.lpszClassName, L"New Window",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
500, 350, NULL, NULL, GetModuleHandle(NULL), NULL);
if (hWnd == NULL)
{
MessageBox(NULL, L"Failed to create window", L"Error", MB_OK);
GdiplusShutdown(gdiplusToken);
return 1;
}
// Show the window
ShowWindow(hWnd, SW_SHOW);
// Message loop
MSG Msg;
while (GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
GdiplusShutdown(gdiplusToken);
return Msg.wParam;
}
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_CREATE:
{
// create the button
HWND buttonHandle = CreateWindowEx(0, L"BUTTON", L"OK",
WS_CLIPSIBLINGS | WS_VISIBLE | WS_CHILD | BS_OWNERDRAW,
50, 50, 200, 100, hwnd, (HMENU)1, NULL, NULL);
// create a region to shape the button
HRGN hRgn = CreateRoundRectRgn(0, 0, 200, 100, 40, 40);
SetWindowRgn(buttonHandle, hRgn, TRUE);
break;
}
case WM_DRAWITEM:
{
LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;
// Get button rectangle
RECT buttonRect = lpdis->rcItem;
int x = buttonRect.left;
int y = buttonRect.top;
int width = buttonRect.right - buttonRect.left;
int height = buttonRect.bottom - buttonRect.top;
// Create Graphics object from device context
Graphics graphics(lpdis->hDC);
graphics.SetSmoothingMode(SmoothingModeAntiAlias); // Set anti-alias mode for smoother edges
// Create solid brush for button color
SolidBrush brush(Color(170, 70, 70));
// Draw a rounded rectangle
int cornerRadius = 40;
GraphicsPath path;
path.AddArc(x, y, cornerRadius, cornerRadius, 180, 90);
path.AddArc(x + width - cornerRadius, y, cornerRadius, cornerRadius, 270, 90);
path.AddArc(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, cornerRadius, 0, 90);
path.AddArc(x, y + height - cornerRadius, cornerRadius, cornerRadius, 90, 90);
path.CloseFigure();
graphics.FillPath(&brush, &path);
break;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
Alternatively, on Windows 8+ you can assign the WS_EX_LAYERED
window style to the button (prior to Win8, WS_EX_LAYERED
works only on top-level windows), and then use SetLayeredWindowAttributes()
or UpdateLayeredWindow()
to make the button have true transparency in the areas that you don't want to draw.
For example:
#define UNICODE
#include <windows.h>
#include <gdiplus.h>
using namespace Gdiplus;
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
// Starting GDI+
if (GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL) != Status::Ok)
{
MessageBox(NULL, L"Failed to start GDI+", L"Error", MB_OK);
return 1;
}
// Registering the Window Class
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = MainWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));
wc.lpszMenuName = NULL;
wc.lpszClassName = L"myWindowClass";
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wc))
{
MessageBox(NULL, L"Failed to register window class", L"Error", MB_OK);
GdiplusShutdown(gdiplusToken);
return 1;
}
// Creating the window
HWND hWnd = CreateWindowEx(0, wc.lpszClassName, L"New Window",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
500, 350, NULL, NULL, GetModuleHandle(NULL), NULL);
if (hWnd == NULL)
{
MessageBox(NULL, L"Failed to create window", L"Error", MB_OK);
GdiplusShutdown(gdiplusToken);
return 1;
}
// Show the window
ShowWindow(hWnd, SW_SHOW);
// Message loop
MSG Msg;
while (GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
GdiplusShutdown(gdiplusToken);
return Msg.wParam;
}
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_CREATE:
{
// create the button
HWND buttonHandle = CreateWindowEx(WS_EX_LAYERED, L"BUTTON", L"OK",
WS_VISIBLE | WS_CHILD | BS_OWNERDRAW,
50, 50, 200, 100, hwnd, (HMENU)1, NULL, NULL);
// set button transparent color (fuchsia)
SetLayeredWindowAttributes(buttonHandle, RGB(255, 0, 255), 255, LWA_COLORKEY);
break;
}
case WM_DRAWITEM:
{
LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;
// Get button rectangle
RECT buttonRect = lpdis->rcItem;
int x = buttonRect.left;
int y = buttonRect.top;
int width = buttonRect.right - buttonRect.left;
int height = buttonRect.bottom - buttonRect.top;
// Create Graphics object from device context
Graphics graphics(lpdis->hDC);
graphics.SetSmoothingMode(SmoothingModeAntiAlias); // Set anti-alias mode for smoother edges
// Create solid brush for button background color (fuchsia)
SolidBrush bkgBrush(Color(255, 0, 255));
// Draw the background
graphics.FillRectangle(&bkgBrush, x, y, width, height);
// Create solid brush for button color
SolidBrush buttonBrush(Color(170, 70, 70));
// Draw a rounded rectangle
int cornerRadius = 40;
GraphicsPath path;
path.AddArc(x, y, cornerRadius, cornerRadius, 180, 90);
path.AddArc(x + width - cornerRadius, y, cornerRadius, cornerRadius, 270, 90);
path.AddArc(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, cornerRadius, 0, 90);
path.AddArc(x, y + height - cornerRadius, cornerRadius, cornerRadius, 90, 90);
path.CloseFigure();
graphics.FillPath(&buttonBrush, &path);
break;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}