Reputation: 521
what is the proper way of implementing custom rounded border for EDIT control in pure WinAPI (no MFC)? I need an edit with border like this:
Should I subclass edit control and do custom painting in WM_NCPAINT or something like that?
Upvotes: 4
Views: 4969
Reputation: 23
This is an implementation that works for me. It subclass the "EDIT" class control and replaces the WM_NCPAINT handler to draw a rectangle with rounded corners for all edit boxes with the WS_BORDER or WS_EX_CLIENTEDGE style. It draws the border on the parent DC. The diameter of the corner is now fixed (10), I guess that should depend on the font size ...
Thanks to Darren Sessions for the GDI+ example how to draw the rounded rect: https://www.codeproject.com/Articles/27228/A-class-for-creating-round-rectangles-in-GDI-with
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")
inline void GetRoundRectPath(GraphicsPath* pPath, Rect r, int dia)
{
// diameter can't exceed width or height
if (dia > r.Width) dia = r.Width;
if (dia > r.Height) dia = r.Height;
// define a corner
Rect Corner(r.X, r.Y, dia, dia);
// begin path
pPath->Reset();
// top left
pPath->AddArc(Corner, 180, 90);
// top right
Corner.X += (r.Width - dia - 1);
pPath->AddArc(Corner, 270, 90);
// bottom right
Corner.Y += (r.Height - dia - 1);
pPath->AddArc(Corner, 0, 90);
// bottom left
Corner.X -= (r.Width - dia - 1);
pPath->AddArc(Corner, 90, 90);
// end path
pPath->CloseFigure();
}
inline void GetChildRect(HWND hChild, LPRECT rc)
{
GetWindowRect(hChild,rc);
SIZE si = { rc->right - rc->left, rc->bottom - rc->top };
ScreenToClient(GetParent(hChild), (LPPOINT)rc);
rc->right = rc->left + si.cx;
rc->bottom = rc->top + si.cy;
}
inline void DrawRoundedBorder(HWND hWnd, COLORREF rgba = 0xFF0000FF, int radius = 5)
{
BYTE* c = (BYTE*)&rgba;
Pen pen(Color(c[0], c[1], c[2], c[3]));
if (pen.GetLastStatus() == GdiplusNotInitialized)
{
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
pen.SetColor(Color(c[0], c[1], c[2], c[3]));
}
pen.SetAlignment(PenAlignmentCenter);
SolidBrush brush(Color(255, 255, 255, 255));
RECT rc = { 0 };
GetChildRect(hWnd, &rc);
// the normal EX_CLIENTEDGE is 2 pixels thick.
// up to a radius of 5, this just works out.
// for a larger radius, the rectangle must be inflated
if (radius > 5)
{
int s = radius / 2 - 2;
InflateRect(&rc, s, s);
}
GraphicsPath path;
GetRoundRectPath(&path, Rect(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top), radius * 2);
HWND hParent = GetParent(hWnd);
HDC hdc = GetDC(hParent);
Graphics graphics(hdc);
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.FillPath(&brush, &path);
graphics.DrawPath(&pen, &path);
ReleaseDC(hParent, hdc);
}
static WNDPROC pfOldEditWndProc = NULL;
static LRESULT CALLBACK EditRounderBorderWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_NCCREATE:
{
DWORD style = GetWindowLong(hWnd, GWL_STYLE);
if (style & WS_BORDER)
{
// WS_EX_CLIENTEDGE style will make the border 2 pixels thick...
style = GetWindowLong(hWnd, GWL_EXSTYLE);
if (!(style & WS_EX_CLIENTEDGE))
{
style |= WS_EX_CLIENTEDGE;
SetWindowLong(hWnd, GWL_EXSTYLE, style);
}
}
// to draw on the parent DC, CLIPCHILDREN must be off
HWND hParent = GetParent(hWnd);
style = GetWindowLong(hParent, GWL_STYLE);
if (style & WS_CLIPCHILDREN)
{
style &= ~WS_CLIPCHILDREN;
SetWindowLong(hParent, GWL_STYLE, style);
}
}
break;
case WM_NCPAINT:
if (GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_CLIENTEDGE)
{
DrawRoundedBorder(hWnd);
return 0;
}
}
return CallWindowProc(pfOldEditWndProc, hWnd, uMsg, wParam, lParam);
}
class CRoundedEditBorder
{
public:
CRoundedEditBorder()
{
Subclass();
}
~CRoundedEditBorder()
{
Unsubclass();
}
private:
void Subclass()
{
HWND hEdit = CreateWindow(L"EDIT", L"", 0, 0, 0, 200, 20, NULL, NULL, GetModuleHandle(NULL), NULL);
pfOldEditWndProc = (WNDPROC)GetClassLongPtr(hEdit, GCLP_WNDPROC);
SetClassLongPtr(hEdit, GCLP_WNDPROC, (LONG_PTR)EditRounderBorderWndProc);
DestroyWindow(hEdit);
}
void Unsubclass()
{
HWND hEdit = CreateWindow(L"EDIT", L"", 0, 0, 0, 200, 20, NULL, NULL, GetModuleHandle(NULL), NULL);
SetClassLongPtr(hEdit, GCLP_WNDPROC, (LONG_PTR)pfOldEditWndProc);
DestroyWindow(hEdit);
}
};
CRoundedEditBorder g_RoundedEditBorder;
LRESULT CALLBACK ParentWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY: PostQuitMessage(0); return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
#define WNDCLASSNAME L"RoundedEditBorderTestClass"
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
WNDCLASSEXW wcex = { sizeof(WNDCLASSEX), CS_HREDRAW|CS_VREDRAW,ParentWndProc,0,0,hInstance,NULL,NULL,CreateSolidBrush(GetSysColor(COLOR_BTNSHADOW)),NULL,WNDCLASSNAME,NULL };
RegisterClassExW(&wcex);
HWND hWnd = CreateWindowW(WNDCLASSNAME, L"Rounded Edit Border Test", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
CreateWindowEx(0, L"EDIT", L"no border", WS_CHILD | WS_VISIBLE, 10, 10, 200, 24, hWnd, NULL, GetModuleHandle(NULL), NULL);
CreateWindowEx(0, L"EDIT", L"no ex style", WS_CHILD | WS_VISIBLE | WS_BORDER, 10, 50, 200, 24, hWnd, NULL, GetModuleHandle(NULL), NULL);
CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"Ex_ClientEdge", WS_CHILD | WS_VISIBLE | WS_BORDER, 10, 90, 200, 24, hWnd, NULL, GetModuleHandle(NULL), NULL);
ShowWindow(hWnd, nCmdShow);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GdiplusShutdown(gdiplusToken);
return (int)msg.wParam;
}
Upvotes: 2
Reputation: 37202
I guess you have two options:
WM_NCPAINT
, etc to provide your own non-client areaWith option #1, you would need to override WM_NCCALCSIZE
to make the non-client area of the edit control larger (i.e. make the client area smaller), and then WM_NCPAINT
to render your custom frame. You may also need to handle WM_NCHITTEST
. And of course you'd need to make the control itself physically larger to account for the extra frame thickness.
It depends on your application design and how many controls like this you wish to use, but if it were me I would go with option #2. Modifying the standard drawing behaviour of system controls, many of which have decades of accumulated kludges and compatibility fixes attached to them, is often not as easy as you might expect.
If you make sure the WS_BORDER
and WS_EX_CLIENTEDGE
styles aren't set on the edit control, it will have no visible border of its own. Then all you have to do is have the parent window, when processing WM_PAINT
, draw the frame around it. Make sure you set the WS_CLIPCHILDREN
style on the parent window so that your custom drawing doesn't overwrite the edit control.
Either path would probably work in the end though so it's up to you which way you go.
Upvotes: 4