mbg033
mbg033

Reputation: 521

WINAPI Edit control with custom border

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:

enter image description here

Should I subclass edit control and do custom painting in WM_NCPAINT or something like that?

Upvotes: 4

Views: 4969

Answers (2)

Kees
Kees

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

Jonathan Potter
Jonathan Potter

Reputation: 37202

I guess you have two options:

  • As you said, you could sub-class and override WM_NCPAINT, etc to provide your own non-client area
  • Alternatively, you could simply turn off the border styles on the edit control and make the parent window responsible for drawing the frame.

With 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

Related Questions