4

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?

mbg033
  • 501
  • 5
  • 18

2 Answers2

4

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.

Jonathan Potter
  • 36,172
  • 4
  • 64
  • 79
  • 3
    And don't forget to use `SetWindowRgn()` to give the edit control its rounded edges. It is not enough to just draw rounded edges (which, BTW, the HRGN will help you do), but you have to actually shape the edit window. – Remy Lebeau Feb 23 '15 at 23:39
  • Thanks guys, I followed option #2, but faced with issue: text became vertically misaligned if I remove WS_EX_CLIENTEDGE flag: http://gyazo.com/f125abcc5973cf537732eccd07481bda – mbg033 Feb 24 '15 at 19:18
  • Also, I couldn't have SetWindowRgn() with rounded region and border painting code work together.. I see rounded edit only if I disable border painting code http://gyazo.com/6b2d505b3b24882cb4d8c30fa9519345; If painting code enabled I see this: http://gyazo.com/5cbd8bd9537c93c12e00fdc2568d222d; code: https://gist.github.com/mbg033/91e262dfd5798f62000d – mbg033 Feb 24 '15 at 20:44
  • 1
    Looks like you should either make the edit control smaller vertically, or add some white padding to your custom frame at the top. – Jonathan Potter Feb 24 '15 at 21:39
  • @RemyLebeau: As long as you draw in the background colour behind the rounded corners there's no real reason to set the window region that I can see. – Jonathan Potter Feb 24 '15 at 21:40
  • 1
    @JonathanPotter: Think of what happens if the edit control is placed on a parent window with a non-trivial (ie not a single color) background. The corners have to reflect what is actually behind them, and the edit control cannot (and should not) know what that actually is. So it is better to make the corners truly transparent and let the OS handle the drawing in those areas. – Remy Lebeau Feb 24 '15 at 22:56
  • @RemyLebeau: Yes that's the one case when it would be useful, but depending on the OP's requirements may not be necessary. Particularly if the parent window is the one drawing the frame. KISS principle in action :) – Jonathan Potter Feb 24 '15 at 23:34
2

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;
}
Kees
  • 23
  • 2