Share via


How can I paint an image in the caption bar?

Question

Friday, July 26, 2019 5:18 PM

Hi - I want to paint a small image in the caption bar of various windows, just to the left of the Close button on the right-hand side (these are all floating modeless dialogs in fact) in my (Visual C++ 2017, MFC) application.  I originally thought I'd just need to add a small paint in a handler for WM_NCPAINT.  That did not work - not on my PC, running Windows 10.   All paints have zero effect.  I then learned that since Vista and Aero, painting in the caption bar has got much more complicated.  It appears that I probably have to do something clever with the Desktop Window Manager (DWM) API.  I found this page: /en-us/windows/win32/dwm/customframe (Custom Window Frame Using DWM), but was unable to follow the instructions (I couldn't work out what I was supposed to put in the handler for WM_NCCALCSIZE for example).  I also found an article called "Setting up a custom title bar on Vista/Windows 7" (https://delphihaven.wordpress.com/2010/04/19/setting-up-a-custom-titlebar/). The code in that case is Delphi.  I tried to work out how to do what they do in C++, but failed there too. 

Has anybody done this?  Added an image or an icon to the caption bar?  If so, could you give me some pointers?  Or best of all a working sample?

Many thanks

Simon

Simon

All replies (13)

Friday, July 26, 2019 5:45 PM

The first link, the Microsoft's Custom Window Frame Using DWM is complete enough to get what you need to do from it.

The WM_NCCALCSIZE for example is in the CustomCaptionProc under the if((message == WM_NCCALCSIZE) &&()) statement. Everything in appendix A, B and C is needed and used in one window procedure.

If it is confusing you, WndProc is the window procedure and it calls everything else in the appendices.

This is a signature. Any samples given are not meant to have error checking or show best practices. They are meant to just illustrate a point. I may also give inefficient code or introduce some problems to discourage copy/paste coding. This is because the major point of my posts is to aid in the learning process.


Friday, July 26, 2019 6:20 PM

As Darran Rowe said, the DWM MSDN sample works fine if you follow all the steps

I tested in C++, C# and VB.NET and it works in all languages

A test in C++ with a .png and a button in the caption =>


Saturday, July 27, 2019 8:08 PM

Thanks for the replies, Darran and Castorix31, but I'm still struggling.  Just to be clear:  I want my windows to be absolutely unchanged in every way, except that I want to be able to paint an image in the caption bar.  But everything else should be completely unchanged.  So how do I achieve that?

Darran says that all I need is in the samples in the appendices.  Well I did copy and paste the sample code into a new project and (with a little difficulty and some guesses about the values of undefined macros) managed to get it to build.  The resulting window looks like this:



So how do I now reinstate the normal caption bar and windows frame?

I don't understand what I'm doing.  Given that I don't want to change the size of the Windows frame, is there any reason for me to call DwmExtendFrameIntoClientArea?

In the sample code, the section handling WM_NCCALCSIZE doesn't seem to do anything.  It has this code:

NCCALCSIZE_PARAMS *pncsp = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);

pncsp->rgrc[0].left   = pncsp->rgrc[0].left   + 0;
pncsp->rgrc[0].top    = pncsp->rgrc[0].top    + 0;
pncsp->rgrc[0].right  = pncsp->rgrc[0].right  - 0;
pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0;

Is this supposed to tell you what you need to do?  Substitute some values for 0? If so, what values should I be using if I don't want the window to change in any way (except for my painted image)?

The background brush in the sample is set to "(HBRUSH)(COLOR_WINDOW+1)".  What's that about?  

All help very much appreciated.

Simon


Sunday, July 28, 2019 9:00 AM

A sample based on MSDN code, drawing an icon on the left of caption buttons =>

#include <Windowsx.h>
#include <windows.h>
#include <tchar.h>

#include <dwmapi.h>
#pragma comment(lib,"Dwmapi")

// https://docs.microsoft.com/en-us/windows/desktop/dwm/customframe

HINSTANCE hInst;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int nWidth = 600, nHeight = 400;

#define RECTWIDTH(rc)            (rc.right - rc.left)
#define RECTHEIGHT(rc)            (rc.bottom - rc.top)

const int TOPEXTENDWIDTH = 32;
const int LEFTEXTENDWIDTH = 8;
const int RIGHTEXTENDWIDTH = 8;
const int BOTTOMEXTENDWIDTH = 8;

LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam);

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    hInst = hInstance;
    WNDCLASSEX wcex =
    {
        sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInst, LoadIcon(NULL, IDI_APPLICATION),
        LoadCursor(NULL, IDC_ARROW), (HBRUSH)GetStockObject(BLACK_BRUSH), NULL, TEXT("WindowClass"), NULL,        
    };
    if (!RegisterClassEx(&wcex))
        return MessageBox(NULL, L"Cannot register class !", L"Error", MB_ICONERROR | MB_OK);
    int nX = (GetSystemMetrics(SM_CXSCREEN) - nWidth) / 2, nY = (GetSystemMetrics(SM_CYSCREEN) - nHeight) / 2;
    HWND hWnd = CreateWindowEx(0, wcex.lpszClassName, TEXT("Test"), WS_OVERLAPPEDWINDOW, nX, nY, nWidth, nHeight, NULL, NULL, hInst, NULL);
    if (!hWnd)
        return MessageBox(NULL, L"Cannot create window !", L"Error", MB_ICONERROR | MB_OK);

    ShowWindow(hWnd, SW_SHOWNORMAL);
    UpdateWindow(hWnd);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

LRESULT CustomCaptionProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool* pfCallDWP)
{
    LRESULT lRet = 0;
    HRESULT hr = S_OK;
    bool fCallDWP = true; // Pass on to DefWindowProc?
    static HICON hIcon = NULL;

    fCallDWP = !DwmDefWindowProc(hWnd, message, wParam, lParam, &lRet);
    if (message == WM_CREATE)
    {
        RECT rcClient;
        GetWindowRect(hWnd, &rcClient);
        // Inform the application of the frame change.
        SetWindowPos(hWnd, NULL, rcClient.left, rcClient.top, RECTWIDTH(rcClient), RECTHEIGHT(rcClient), SWP_FRAMECHANGED);

        HMODULE hDLL = LoadLibrary(L"Setupapi.dll");
        if (hDLL)
        {
            hIcon = (HICON)LoadImage(hDLL, MAKEINTRESOURCE(2),  IMAGE_ICON, 32, 32, LR_DEFAULTCOLOR | LR_SHARED);
        }

        fCallDWP = true;
        lRet = 0;
    }

    // Handle window activation.
    if (message == WM_ACTIVATE)
    {
        // Extend the frame into the client area.
        MARGINS margins;
        margins.cxLeftWidth = LEFTEXTENDWIDTH; 
        margins.cxRightWidth = RIGHTEXTENDWIDTH;
        margins.cyBottomHeight = BOTTOMEXTENDWIDTH;
        margins.cyTopHeight = TOPEXTENDWIDTH;
        hr = DwmExtendFrameIntoClientArea(hWnd, &margins);
        if (!SUCCEEDED(hr))
        {
            // Handle error.
        }

        fCallDWP = true;
        lRet = 0;
    }

    if (message == WM_PAINT)
    {
        PAINTSTRUCT ps;
        BITMAP bm;
        RECT rect, rectCaptionButtonBounds, rectText;

        BeginPaint(hWnd, &ps);
        if (SUCCEEDED(DwmGetWindowAttribute(hWnd, DWMWA_CAPTION_BUTTON_BOUNDS, &rectCaptionButtonBounds, sizeof(rectCaptionButtonBounds))))
        {
            GetClientRect(hWnd, &rect);
            DrawIconEx(ps.hdc, rect.right - (rectCaptionButtonBounds.right - rectCaptionButtonBounds.left) - 32, 0, hIcon, 32, 32, 0, NULL, DI_NORMAL);
            SetBkMode(ps.hdc, TRANSPARENT);
            SetTextColor(ps.hdc, RGB(255, 255, 0));
            WCHAR wsText[255] = L"Test Custom Caption";
            SetRect(&rectText, LEFTEXTENDWIDTH, 0, RECTWIDTH(rect), TOPEXTENDWIDTH);
            DrawText(ps.hdc, wsText, -1, &rectText, DT_SINGLELINE | DT_VCENTER);
        }
        EndPaint(hWnd, &ps);

        fCallDWP = true;
        lRet = 0;
    }

    // Handle the non-client size message.
    if ((message == WM_NCCALCSIZE) && (wParam == TRUE))
    {
        // Calculate new NCCALCSIZE_PARAMS based on custom NCA inset.
        /*NCCALCSIZE_PARAMS *pncsp = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);

        pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0;
        pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0;
        pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0;
        pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0;*/

        lRet = 0;
        // No need to pass the message on to the DefWindowProc.
        fCallDWP = false;
    }

    // Handle hit testing in the NCA if not handled by DwmDefWindowProc.
    if ((message == WM_NCHITTEST) && (lRet == 0))
    {
        lRet = HitTestNCA(hWnd, wParam, lParam);

        if (lRet != HTNOWHERE)
        {
            fCallDWP = false;
        }
    }
    if (message == WM_DESTROY)
        PostQuitMessage(0);

    *pfCallDWP = fCallDWP;

    return lRet;
}

// Hit test the frame for resizing and moving.
LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
    // Get the point coordinates for the hit test.
    POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };

    // Get the window rectangle.
    RECT rcWindow;
    GetWindowRect(hWnd, &rcWindow);

    // Get the frame rectangle, adjusted for the style without a caption.
    RECT rcFrame = { 0 };
    AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);

    // Determine if the hit test is for resizing. Default middle (1,1).
    USHORT uRow = 1;
    USHORT uCol = 1;
    bool fOnResizeBorder = false;

    // Determine if the point is at the top or bottom of the window.
    if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + TOPEXTENDWIDTH)
    {
        fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
        uRow = 0;
    }
    else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - BOTTOMEXTENDWIDTH)
    {
        uRow = 2;
    }

    // Determine if the point is at the left or right of the window.
    if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + LEFTEXTENDWIDTH)
    {
        uCol = 0; // left side
    }
    else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - RIGHTEXTENDWIDTH)
    {
        uCol = 2; // right side
    }

    // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT)
    LRESULT hitTests[3][3] =
    {
        { HTTOPLEFT,    fOnResizeBorder ? HTTOP : HTCAPTION,    HTTOPRIGHT },
        { HTLEFT,       HTNOWHERE,     HTRIGHT },
        { HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
    };

    return hitTests[uRow][uCol];
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    bool fCallDWP = true;
    BOOL fDwmEnabled = FALSE;
    LRESULT lRet = 0;
    HRESULT hr = S_OK;

    // Winproc worker for custom frame issues.
    hr = DwmIsCompositionEnabled(&fDwmEnabled);
    if (SUCCEEDED(hr))
    {
        lRet = CustomCaptionProc(hWnd, message, wParam, lParam, &fCallDWP);
    }

    // Winproc worker for the rest of the application.
    if (fCallDWP)
    {
        //  lRet = AppWinProc(hWnd, message, wParam, lParam);
        lRet = DefWindowProc(hWnd, message, wParam, lParam);
    }
    return lRet;
}

Sunday, July 28, 2019 10:53 AM

Given that I don't want to change the size of the Windows frame, is there any reason for me to call DwmExtendFrameIntoClientArea?

This is all about getting Windows to allow you to draw into the non-client area.

There are technically two ways of doing this.

1) Use DwmExtendFrameIntoClientArea. This ends up looking like:

2) Use DwmSetWindowAttribute. This ends up looking like:

Both of these are with the default painting done by DefWindowProc.

Your eyes aren't deceiving you, the second is a black rectangle.

When you enable non-client rendering in any form, you are telling Windows that you are taking over drawing of the non-client area. DwmExtendFrameIntoClientArea tells Windows "I still want you to handle the borders" where DwmSetWindowAttribute says "I am handling everything". There is no option for "I want you to draw almost everything, but reserve a little bit of space for me here".

One of these is required due to how Windows works. Normally DWM has a bit of memory put aside for your applications client area which you draw to, and Windows has another bit of memory that Windows controls for the non-client area. Windows won't give you access to this non-client area, instead it makes the non-client area part of the client area. Basically instead of having the client area only cover the portion of the window that you draw to, it extends it out to cover the title bar and borders making the entire window the client area.

In the sample code, the section handling WM_NCCALCSIZE doesn't seem to do anything.

Yes, and reading the documentation is your friend here. WM_NCCALCSIZE states that the first rectangle is the size of the client area. Because doing what you are doing means that the entire window is the client area, this stops Windows from drawing the standard frame.

This makes explicit:

"Starting with Windows Vista, removing the standard frame by simply returning 0 when the wParam is TRUE does not affect frames that are extended into the client area using the DwmExtendFrameIntoClientArea function. Only the standard frame will be removed."

From the WM_NCCALCSIZE documentation, but you don't need it.

The background brush in the sample is set to "(HBRUSH)(COLOR_WINDOW+1)". What's that about?

Well, The WNDCLASS(EX) structure documentation gives information. The names are standard system colours, and you can see a list of these in GetSysColor's documentation. The +1 most likely comes from the fact that this is a handle. With the values provided by GetSysColor, you can see this as an index into an array of colours. These low values were then obviously made into handles to make their usage nice and easy. However, COLOR_SCROLLBAR is at index 0, and 0 is the value for the null handle which tells Windows that an error occurred. So Microsoft decided to just bump all of the values by 1 and use those as the handle values.

This gives a nice easy way to get the system colours without doing extra calls and it is related to constants defined in the headers.

This is a signature. Any samples given are not meant to have error checking or show best practices. They are meant to just illustrate a point. I may also give inefficient code or introduce some problems to discourage copy/paste coding. This is because the major point of my posts is to aid in the learning process.


Sunday, July 28, 2019 3:20 PM

Thanks Castorix31 and Darran.  I appreciate your patience and your replies are helpful.  However, what both of you have shown is how to take over control of painting the non-client area (or what would usually be the non-client area) - on the basis that if I want to paint into the caption bar, that's what I have to do.  But I want to be able to paint in the caption bar - and leave everything else unchanged.  On my default settings in Windows 10, for example, I get a thin blue frame, and outside that I get a pleasing 'glow' which overlays whatever the window is in front of. 

Presumably if I changed the display "Personalise"  options and colours, I would get a different look.  How can I put everything back to whatever it would have been if I hadn't taken over painting the non-client area?  I need an example of how to do this (presumably using theme data?).  Even the dimensions of the border area might vary depending on the user settings.  There's a lot to re-construct.

Darran says: 

>> When you enable non-client rendering in any form, you are telling Windows that you are taking over drawing of the non-client area. DwmExtendFrameIntoClientArea tells Windows "I still want you to handle the borders".

Correct me if I'm wrong, but it seems like I have to extend the client area to cover the whole window, if I'm going to paint in the caption bar area.  And once I've done that, Windows will not handle the borders for me - even if I've called DwmExtendFrameIntoClientArea and have not called DwmSetWindowAttribute.  For example, Castorix31 uses DwmExtendFrameIntoClientArea, but Windows does not handle the borders.  They are plain purple in his build and blue in mine (except for the caption which is purple in his build and white in mine).

Incidentally, if I build the code that you provided, Castorix31, I don't get quite the same result as you get.  I get a very similar window, but with a white caption bar and no text.  The icon image, and the minimize, maximize and close icons, are all there and look fine.  But not the text.  If I offset the text a little to move it down the page, then the only part that appears it the bit below the caption bar (see image below).  For some weird reason I can draw an icon on the caption bar but not text.   That's using the code you posted.  The only change I made was to offset the text 18 pixels down the page, and switch the text colour to red, to make it stand out more.

I'm still very keen to solve my original problem if at all possible.  But at this point, I'm still feeling quite a long way off.

Simon


Monday, July 29, 2019 8:56 AM

Hello,

Thank you for posting here.

>>But I want to be able to paint in the caption bar -and leave everything else unchanged.

After testing several methods, I think your need seems a little hard to meet. Painting in the caption bar and leaving everything else unchanged seem to contradict each other, the two can not have both. 

**>> I get a very similar window, but with a white caption bar and no text. **

I don't know whether the title bar I designed meets your requirements. Forgive my poor drawing skills. The general effect of the page is shown in the figure. The custom title bar can insert text and icons. The icons can also respond to closing, maximizing and minimizing. The code is lieted below.

//dlg.h

public:
    CRect m_rtButtExit;    
    CRect m_rtButtMax;   
    CRect m_rtButtMin;    
    CRect m_rtButtHel;    
    CRect m_rtIcon;       
//dlg.CPP
void CMFCApplication64Dlg::DrawTitleBar(CDC *pDC)
{
    // TODO: Add your implementation code here.
    if (m_hWnd)
    {
        CBrush Brush(RGB(0, 170, 255));
        CBrush* pOldBrush = pDC->SelectObject(&Brush);

        CRect rtWnd, rtTitle, rtButtons;
        GetWindowRect(&rtWnd);

        rtTitle.left = GetSystemMetrics(SM_CXFRAME);
        rtTitle.top = GetSystemMetrics(SM_CYFRAME);
        rtTitle.right = rtWnd.right - rtWnd.left - GetSystemMetrics(SM_CXFRAME);
        rtTitle.bottom = rtTitle.top + GetSystemMetrics(SM_CYSIZE);


        int nTitleTopDown = 5;
        int nTitleTopDown1 = 0;
        CPoint point;

        point.x = rtWnd.Width();
        point.y = GetSystemMetrics(SM_CYSIZE) + GetSystemMetrics(SM_CYFRAME) + 15;
        pDC->PatBlt(0, nTitleTopDown1, point.x, point.y, PATCOPY);

        point.x = GetSystemMetrics(SM_CXFRAME) + 1;
        point.y = rtWnd.Height();
        pDC->PatBlt(0, nTitleTopDown1, point.x, point.y, PATCOPY);

        point.x = rtWnd.Width();
        point.y = GetSystemMetrics(SM_CYFRAME) + 1;
        pDC->PatBlt(0, rtWnd.Height() - point.y, point.x, point.y, PATCOPY);

        point.x = GetSystemMetrics(SM_CXFRAME) + 1;
        point.y = rtWnd.Height();
        pDC->PatBlt(rtWnd.Width() - point.x, 0, point.x, point.y, PATCOPY);

    

        pDC->SelectObject(pOldBrush);

        m_rtIcon.left = rtWnd.Width() - 130;
        m_rtIcon.top = GetSystemMetrics(SM_CYFRAME) + nTitleTopDown;
        m_rtIcon.right = m_rtIcon.left + 32;
        m_rtIcon.bottom = m_rtIcon.top + 32;
        ::DrawIconEx(pDC->m_hDC, m_rtIcon.left, m_rtIcon.top, m_hIcon,
            m_rtIcon.Width(), m_rtIcon.Height(), 0, NULL, DI_NORMAL);
        m_rtIcon.OffsetRect(rtWnd.TopLeft());
        
        
        SetBkMode(pDC->m_hDC,TRANSPARENT);
        SetTextColor(pDC->m_hDC, RGB(255, 0, 0));
        WCHAR wsText[255] = L"Test Custom Caption";
        CRect rectText;
        SetRect(&rectText, 10,5 , 200, 30);
        DrawText(pDC->m_hDC, wsText, 19, &rectText, DT_SINGLELINE | DT_VCENTER);

        CBitmap* pBitmap = new CBitmap;
        CBitmap* pOldBitmap;
        CDC* pDisplayMemDC = new CDC;
        pDisplayMemDC->CreateCompatibleDC(pDC);


        rtButtons.left = rtTitle.right - 24;
        rtButtons.top = rtTitle.top + nTitleTopDown;
        rtButtons.right = rtButtons.left + 19;
        rtButtons.bottom = rtButtons.top + 19;
        pBitmap->LoadBitmap(IDB_BITMAP1);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButtons.left, rtButtons.top, rtButtons.Width(), rtButtons.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        m_rtButtExit = rtButtons;
        m_rtButtExit.OffsetRect(rtWnd.TopLeft());
        pBitmap->DeleteObject();


        rtButtons.right = rtButtons.left - 3;
        rtButtons.left = rtButtons.right - 19;
        if (IsZoomed())
            pBitmap->LoadBitmap(IDB_BITMAP1);
        else
            pBitmap->LoadBitmap(IDB_BITMAP1);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButtons.left, rtButtons.top, rtButtons.Width(), rtButtons.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        m_rtButtMax = rtButtons;
        m_rtButtMax.OffsetRect(rtWnd.TopLeft());
        pBitmap->DeleteObject();

        rtButtons.right = rtButtons.left - 3;
        rtButtons.left = rtButtons.right - 19;
        pBitmap->LoadBitmap(IDB_BITMAP1);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButtons.left, rtButtons.top, rtButtons.Width(), rtButtons.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        m_rtButtMin = rtButtons;
        m_rtButtMin.OffsetRect(rtWnd.TopLeft());
        pBitmap->DeleteObject();


        rtButtons.right = rtButtons.left - 3;
        rtButtons.left = rtButtons.right - 19;
        pBitmap->LoadBitmap(IDB_BITMAP1);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButtons.left, rtButtons.top, rtButtons.Width(), rtButtons.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        m_rtButtHel = rtButtons;
        m_rtButtHel.OffsetRect(rtWnd.TopLeft());
        pBitmap->DeleteObject();

        int nOldMode = pDC->SetBkMode(TRANSPARENT);
        COLORREF clOldText = pDC->SetTextColor(RGB(255, 255, 255));
        pDC->SelectStockObject(SYSTEM_FIXED_FONT);
        rtTitle.left += 2;
        rtTitle.top += GetSystemMetrics(SM_CYSIZE) + nTitleTopDown;
        rtTitle.bottom = rtTitle.top + 30;
        CString m_strTitle;
        GetWindowText(m_strTitle);
        pDC->DrawText(m_strTitle, &rtTitle, DT_LEFT);
        pDC->SetBkMode(nOldMode);
        pDC->SetTextColor(clOldText);

        ReleaseDC(pDisplayMemDC);
        delete pDisplayMemDC;
        delete pBitmap;
    }
}


LRESULT CMFCApplication64Dlg::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT lrst = CDialog::DefWindowProc(message, wParam, lParam);
    if (!::IsWindow(m_hWnd))
        return lrst;

    if (message== WM_NCLBUTTONDOWN ||message==WM_NCMOUSEMOVE||message == WM_MOVE|| message == WM_PAINT|| message == WM_NCPAINT|| message == WM_NCACTIVATE|| message == WM_NOTIFY)
    {
        CDC* pWinDC = GetWindowDC();
        if (pWinDC)
            DrawTitleBar(pWinDC);
        ReleaseDC(pWinDC);
    }
    return lrst;
    
}


void CMFCApplication64Dlg::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp)
{
    lpncsp->rgrc[0].top += 5;
    CDialogEx::OnNcCalcSize(bCalcValidRects, lpncsp);
}


void CMFCApplication64Dlg::OnSize(UINT nType, int cx, int cy)
{
    CDialogEx::OnSize(nType, cx, cy);
    CRect wrc;
    GetWindowRect(&wrc);
    wrc.OffsetRect(-wrc.left, -wrc.top);
    wrc.DeflateRect(4, 4);
    CRgn rgn;
    BOOL bl = rgn.CreateRectRgnIndirect(&wrc);
    if (bl) SetWindowRgn(rgn, TRUE);
    rgn.Detach();
}


void CMFCApplication64Dlg::OnNcLButtonDown(UINT nHitTest, CPoint point)
{
    if (m_rtIcon.PtInRect(point))
    {
        ::MessageBox(this->m_hWnd, L"ICON", L"", MB_OK);
    }
    else if (m_rtButtExit.PtInRect(point))
    {
        SendMessage(WM_CLOSE);
    }
    else if (m_rtButtMin.PtInRect(point))
    {
        SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, MAKELPARAM(point.x, point.y));
    }
    else if (m_rtButtHel.PtInRect(point))
    {
        ::MessageBox(this->m_hWnd, L"HELP", L"", MB_OK);
    }
    else if (m_rtButtMax.PtInRect(point))
    {
        if (IsZoomed())
            SendMessage(WM_SYSCOMMAND, SC_RESTORE, MAKELPARAM(point.x, point.y));
        else
        {
            SendMessage(WM_SYSCOMMAND, SC_MAXIMIZE, MAKELPARAM(point.x, point.y));
            Invalidate();
        }
    }
    else if (!IsZoomed())
    {
        CDialogEx::OnNcLButtonDown(nHitTest, point);
    }
    
}


void CMFCApplication64Dlg::OnNcMouseMove(UINT nHitTest, CPoint point)
{
    CDC* pDC = GetWindowDC();
    CDC* pDisplayMemDC = new CDC;
    pDisplayMemDC->CreateCompatibleDC(pDC);
    CBitmap* pBitmap = new CBitmap;
    CBitmap* pOldBitmap;
    CRect rtWnd, rtButton;

    if (pDC)
    {
        GetWindowRect(&rtWnd);
        if (m_rtButtExit.PtInRect(point))
            pBitmap->LoadBitmap(IDB_BITMAP1);
        else
            pBitmap->LoadBitmap(IDB_BITMAP1);
        rtButton = m_rtButtExit;
        rtButton.OffsetRect(-rtWnd.left, -rtWnd.top);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButton.left, rtButton.top, rtButton.Width(), rtButton.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        pBitmap->DeleteObject();

        if (m_rtButtMax.PtInRect(point))
        {
            if (IsZoomed())
                pBitmap->LoadBitmap(IDB_BITMAP1);
            else
                pBitmap->LoadBitmap(IDB_BITMAP1);
        }
        else
        {
            if (IsZoomed())
                pBitmap->LoadBitmap(IDB_BITMAP1);
            else
                pBitmap->LoadBitmap(IDB_BITMAP1);
        }
        rtButton = m_rtButtMax;
        rtButton.OffsetRect(-rtWnd.left, -rtWnd.top);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButton.left, rtButton.top, rtButton.Width(), rtButton.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        pBitmap->DeleteObject();

        if (m_rtButtMin.PtInRect(point))
            pBitmap->LoadBitmap(IDB_BITMAP1);
        else
            pBitmap->LoadBitmap(IDB_BITMAP1);
        rtButton = m_rtButtMin;
        rtButton.OffsetRect(-rtWnd.left, -rtWnd.top);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButton.left, rtButton.top, rtButton.Width(), rtButton.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        pBitmap->DeleteObject();

        if (m_rtButtHel.PtInRect(point))
            pBitmap->LoadBitmap(IDB_BITMAP1);
        else
            pBitmap->LoadBitmap(IDB_BITMAP1);
        rtButton = m_rtButtHel;
        rtButton.OffsetRect(-rtWnd.left, -rtWnd.top);
        pOldBitmap = (CBitmap*)pDisplayMemDC->SelectObject(pBitmap);
        pDC->BitBlt(rtButton.left, rtButton.top, rtButton.Width(), rtButton.Height(), pDisplayMemDC, 0, 0, SRCCOPY);
        pDisplayMemDC->SelectObject(pOldBitmap);
        pBitmap->DeleteObject();
    }

    ReleaseDC(pDisplayMemDC);
    ReleaseDC(pDC);
    delete pDisplayMemDC;
    delete pBitmap;
    CDialogEx::OnNcMouseMove(nHitTest, point);
}

Best Regards,

Suarez Zhou


Monday, July 29, 2019 11:29 AM

Hi Suarez

Thank you for posting that.  Again, I found it helpful.  First though - I realised that in my previous post, I had slightly exaggerated the problem by mentioning the glow you get round windows.  That is one thing you don't have to reproduce.  You just seem to get it regardless.  Also, windows frames in Windows 10, by default at least, are pretty minimalistic on the whole.  So it's quite hard to tell what you have to do to reproduce the existing caption.  I had been thinking of the blue frame painting as a space that need to be filled with something else.  But actually maybe that blue really is the frame - albeit one that has been increased in size from what it should be.  It's still the case that I need at least to find out what size to set the margins to though.

Your implementation was a helpful starting point for me, Suarez, because mine is an MFC app, and I was able to build an MFC sample, incorporating your code, and it works.  But in your implementation you have don't call any Desktop Window Manager (DWM) API calls at all.  Which leaves everything for me to do.  Like previous samples, your code uses hard-coded sizes.  For example, in OnNcCalcSize you add 5 lpncsp->rgrc[0].top.  In previous examples, margin values are also hard-coded numbers.  Surely all these numbers need to be picked from Windows in some way?  If so, from where and how?

I want to use the DWM api (and the theme api if necessary?) to do as much of the work as possible.  And I want all the frame and caption bar sizes etc, to be picked up from Windows, so that if a user does something that changes these in some way, they should still be correct.

Incidentally, I still haven't worked out why I get a white caption using Castorix31's code, and he can output text to the caption bar area, but it gets painted over when I do it (but the icon doesn't!). 

>>Painting in the caption bar and leaving everything else unchanged seem to contradict each other,

Well it may be hard to do, but it's not a contradiction surely?

Simon


Monday, July 29, 2019 2:13 PM

Well it may be hard to do, but it's not a contradiction surely?

Well, let's have a little look at a sample shall we?

If you add an image of any kind to this title bar, how do you add it without impacting the text? That may not look like it because of the Windows 10 style, but the caption text goes right up to the minimise button.

This shows it better:

So at the very least you are always going to have to change the valid range of the caption.

This then has a direct impact on the WM_NCHITTEST message because you will now need to modify the return value if you want whatever you draw into the caption to be seen as something other than the title bar.

If you want to match the activation state of the window, then you will also need to track this from the WM_NCACTIVEATE message.

Since Windows doesn't implicitly know how to do this then it is up to you to do this.

In previous examples, margin values are also hard-coded numbers. Surely all these numbers need to be picked from Windows in some way? If so, from where and how?

Try reading the WM_NCCALCSIZE documentation.

It states in the NCCALCSIZE_PARAMS structure information that the values in the structure have specific meanings. Windows passes the proposed size and position of the window, the previous size and position of the window and the previous client area size.

When you return, it expects the new client area size, the valid destination size and the valid source size.

As I previously explained, when you use DwmExtendFrameIntoClientArea, the entire window is seen as the client area. Windows works everything we need out before it sends this message.

If you need to calculate this for whatever reason then you can get values for the caption/border sizes using GetSystemMetrics and from the theme data.

This is a signature. Any samples given are not meant to have error checking or show best practices. They are meant to just illustrate a point. I may also give inefficient code or introduce some problems to discourage copy/paste coding. This is because the major point of my posts is to aid in the learning process.


Monday, July 29, 2019 2:26 PM

Hi Darran - in an ideal world I would like to be able to shorten the title text so that it ended with ellipses, just before my image, if it would otherwise overlap it.  And I would ideally like to have tooltip for the image.  But i could live without either of those.  And I don't need the user to be able to click on it and have it do anything.  So the level of difference in behaviour to an ordinary window, ought to be pretty minimal - even in my ideal scenario.

Someone pointed out that DisplayFusion somehow manages to put an icon onto every window on the Desktop - not just its own windows, but anybody's.  See the two examples below:

If DisplayFusion can add these to windows in another application, without changing anything else, surely there must be some way I can do it to windows in my own application?  Is there a completely different approach that I've missed?

Simon


Monday, July 29, 2019 5:11 PM | 1 vote

Someone pointed out that DisplayFusion somehow manages to put an icon onto every window on the Desktop - not just its own windows, but anybody's.  See the two examples below:

If DisplayFusion can add these to windows in another application, without changing anything else, surely there must be some way I can do it to windows in my own application?  Is there a completely different approach that I've missed?

DisplayFusion creates Popup windows (class "DFTitleBarWindow:xxxx..")


Monday, July 29, 2019 6:04 PM

Thank you Castorix31!  I don't have DisplayFusion myself, so wasn't able to check it.  But a Popup window?  If you can do it that way, that's probably exactly what I need.  I will give it a go.  If anyone has any tips on what styles exactly I need, let me know.  Otherwise I will experiment.   I notice that the DisplayFusion images move perfectly with the underlying window.  I'm a bit surprised that Popups do that.  I wouldn't have thought they did.  But I suppose most popups are modal dialogs, so maybe the issue never arises, because when they're modal you can't move their parents?  Or maybe DisplayFusion had to hook something to solve the movement problem?

My image is not rectangular, so I will need the window to have a transparent background, but I'm guessing that that's not too hard.

I appreciate that this approach won't solve the problem that title bar text should ideally be truncated with ellipses if it would otherwise go under the image, but I can live without that.

Simon


Monday, July 29, 2019 8:44 PM

DisplayFusion uses hooks, but for your own application, you can handle WM_MOVE, WM_SIZE + activation (like WM_ACTIVATEAPP)

I used this method to position an overlay window (WS_EX_LAYERED  | WS_EX_TOOLWINDOW, like DF popups) over a main window.