5

I'm trying to implement a simple owner-drawn button, which will just contain an image from a brush.

Here's my code (WTL, but it's quite straightforward):

case WM_CTLCOLORBTN:
    dc.SetBkMode(TRANSPARENT);
    POINT pt = { 0 };
    button.MapWindowPoints(m_hWnd, &pt, 1);
    dc.SetBrushOrg(-pt.x, -pt.y, NULL);
    return m_brushHeader;

Everything works fine so far, but for proper keyboard support, I have to add focus rectangle. So now I'm also handling the WM_DRAWITEM message:

case WM_DRAWITEM:
    if(lpDrawItemStruct->itemAction & (ODA_DRAWENTIRE | ODA_FOCUS))
    {
        if((lpDrawItemStruct->itemState & ODS_FOCUS) && 
            !(lpDrawItemStruct->itemState & ODS_NOFOCUSRECT))
        {
            dc.DrawFocusRect(&lpDrawItemStruct->rcItem);
        }
        else
        {
            // Need to remove the rectangle here!
        }
        break;
    }
    break;

The rectangle is properly added, but when the focus is moved to a different button, and I receive the ODA_DRAWENTIRE request, I have to clear it.

How do I clear the content of the HDC? I found only methods of filling it with color, etc. I need to make it empty/transparent, like it was before using DrawFocusRect.

P.S. The application uses visual styles, i.e. ComCtl32.dll Version 6.

Paul
  • 6,061
  • 6
  • 39
  • 70
  • @HansPassant I tried that, but the thing is that I have a static text control over the button. If I draw the background inside `WM_DRAWITEM` instead of `WM_CTLCOLORBTN`, the button overlaps the static control. – Paul Dec 28 '13 at 14:10
  • Did you forget WS_CLIPCHILDREN? Your code is broken, *itemAction* is a *bitmask*, not a value. – Hans Passant Dec 28 '13 at 14:15
  • See the documentation for [`DrawFocusRect`](http://msdn.microsoft.com/en-us/library/windows/desktop/dd162479.aspx): *"Because `DrawFocusRect` is an XOR function, calling it a second time with the same rectangle removes the rectangle from the screen."* – IInspectable Dec 28 '13 at 14:19
  • @HansPassant `WS_CLIPCHILDREN` is set. The static control is not visible regardless of z-order. Thanks for the fix, edited the question and the code, but it made no difference. Isn't there any method of reverting the HDC to the original state? – Paul Dec 28 '13 at 14:19
  • @IInspectable OK, but when I receive an `ODA_DRAWENTIRE` command, I don't know whether the rect is drawn or not. It might be even half-drawn, if the window was partially covered. – Paul Dec 28 '13 at 14:21
  • Rendering is clipped to the visible region. You do not have to worry about whether or not the control is partially visible. You also do not need to know whether the focus rectangle was there or not. Calling `DrawFocusRect` *toggles* the focus mark. If you always toggle the focus rect things will magically work. – IInspectable Dec 28 '13 at 14:27
  • @IInspectable I don't understand, what do you mean by "always toggle the focus rect"? If I'd call `DrawFocusRect` on every `ODA_DRAWENTIRE`, all items will have the rect on startup. If I toggle only on `ODA_FOCUS`, well, it's not sent upon focus loss - `ODA_DRAWENTIRE` is sent instead. – Paul Dec 28 '13 at 14:31
  • Upon `ODA_FOCUS`, toggle the focus rect. During initial rendering, there is no `ODA_FOCUS`; use the `ODS_FOCUS` to decide whether or not to draw the initial rect. Your conditional is wrong, though: You are asking whether `ODA_DRAWENTIRE` is set, or `ODA_FOCUS`, or both. – IInspectable Dec 28 '13 at 14:38
  • @IInspectable, Again: `ODA_FOCUS` is not sent upon focus loss. Only `ODA_DRAWENTIRE` is sent. The easiest solution for this would be just clearing the HDC. I don't believe there's no function to do this. – Paul Dec 28 '13 at 14:41
  • [`DRAWITEMSTRUCT`](http://msdn.microsoft.com/en-us/library/windows/desktop/bb775802.aspx)`.itemAction`: *"**ODA_FOCUS** The control has lost or gained the keyboard focus."* – IInspectable Dec 28 '13 at 14:43
  • @IInspectable yes, I saw the documentation. Unfortunately, that's not happening. Verified with Spy++. – Paul Dec 28 '13 at 14:45
  • @IInspectable here's a relevant question about focusing in owner-drawn buttons: http://stackoverflow.com/q/8015470/2604492 (without an answer). See the bottom of it, there are Spy++ logs similar to the ones I see. – Paul Dec 28 '13 at 14:53
  • I just created a test app that performs as expected. It seems your dialog navigation is broken, if you don't get an `ODA_FOCUS` itemAction. – IInspectable Dec 28 '13 at 15:24
  • @IInspectable I also created a test project, and I get the same issue. Here it is, sources and binary: [link](http://ge.tt/api/1/files/64lqfaB1/0/blob?download). To reproduce, use the tab button, and watch the log. [Screenshot](http://i.imgur.com/1OQGtUW.png). Please, can you upload your test project? – Paul Dec 28 '13 at 15:46
  • 4
    You remove the focus rectangle by redrawing everything else. So redraw the bitmap via the brush. – Raymond Chen Dec 28 '13 at 16:19

1 Answers1

7

Update: I've been living in a time capsule for the past 15 years and initially posted an answer that doesn't address how to solve the issues revolving around Visual Styles (see below).

With Visual Styles enabled there is a change in behavior for the WM_DRAWITEM message: The DRAWITEMSTRUCTs itemAction field no longer has the ODA_FOCUS bit set on focus loss. The result is that the solution to remove the focus rectangle towards the bottom of this answer can no longer be applied.

To remove the focus rectangle with visual styles enabled requires rendering the control again. The following code snippet for the message handler shows how to do this:

switch ( message ) {
// ...
case WM_DRAWITEM: {
    const DRAWITEMSTRUCT& dis = *(DRAWITEMSTRUCT*)lParam;
    if ( dis.itemAction & ODA_DRAWENTIRE ) {
        // Render the control
        // ...

        // If the control has the input focus...
        if ( dis.itemState & ODS_FOCUS ) {
            // Render the focus rectangle
            DrawFocusRect( dis.hDC, &dis.rcItem );
        }
    }
}
// ...
}

Redrawing the entire control upon focus loss is not required. DrawFocusRect is rendered in XOR mode and can be removed by applying the same operation a second time.

The logic to render the focus rectangle consists of two parts:

  1. If itemAction contains ODA_FOCUS render the focus rectangle irrespective of any other state. This toggles the visibility.
  2. Otherwise, only render the focus rectangle, if itemState contains ODS_FOCUS. This is necessary so that the initial state is properly accounted for.

The following code demonstrates this strategy.

resource.h:

#define IDD_MAINDLG 101

DlgBasedWin32.rc (Declaring a simple dialog with just an OK and Cancel button):

#include "resource.h"
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_MAINDLG DIALOGEX 0, 0, 309, 176
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    CONTROL         "OK",IDOK,"Button",BS_OWNERDRAW | WS_TABSTOP,198,155,50,14
    CONTROL         "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,252,155,50,14
END

DlgBasedWin32.cpp (Creating the main dialog and message loop):

#include <windows.h>
#include "resource.h"

// Forward declarations of functions included in this code module:
INT_PTR CALLBACK DlgProc( HWND, UINT, WPARAM, LPARAM );

int APIENTRY _tWinMain( HINSTANCE hInstance,
                        HINSTANCE /*hPrevInstance*/,
                        LPTSTR    /*lpCmdLine*/,
                        int       /*nCmdShow*/)
{
    HWND hDlg = CreateDialogW( hInstance, MAKEINTRESOURCEW( IDD_MAINDLG ),
                               NULL, DlgProc );
    ShowWindow( hDlg, SW_SHOW );
    UpdateWindow( hDlg );

    MSG msg = { 0 };
    // Main message loop:
    while ( GetMessageW( &msg, NULL, 0, 0 ) )
    {
        if ( !IsDialogMessageW( hDlg, &msg ) ) {
            TranslateMessage( &msg );
            DispatchMessageW( &msg );
        }
    }

    return (int) msg.wParam;
}

DlgBasedWin32.cpp (Dialog message handler):

// Message handler for IDD_MAINDLG
INT_PTR CALLBACK DlgProc( HWND hDlg,
                          UINT message,
                          WPARAM wParam,
                          LPARAM lParam )
{
    switch ( message )
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if ( LOWORD( wParam ) == IDOK || LOWORD( wParam ) == IDCANCEL ) {
            DestroyWindow( hDlg );
            return (INT_PTR)TRUE;
        }
        break;

    case WM_DESTROY:
        PostQuitMessage( 0 );
        return (INT_PTR)TRUE;

    case WM_DRAWITEM: {
        WORD wID = (WORD)wParam;
        const DRAWITEMSTRUCT& dis = *(DRAWITEMSTRUCT*)lParam;
        // Focus change?
        if ( dis.itemAction & ODA_FOCUS ) {
            // Toggle focus rectangle
            DrawFocusRect( dis.hDC, &dis.rcItem );
        }
        else if ( dis.itemAction & ODA_DRAWENTIRE ) {
            // Not a focus change -> render rectangle if requested
            if ( dis.itemState & ODS_FOCUS ) {
                DrawFocusRect( dis.hDC, &dis.rcItem );
            }
        }
        return (INT_PTR)TRUE;
    }

    }

    return (INT_PTR)FALSE;
}

The code above displays a simple dialog with just an OK and Cancel button. The buttons have the BS_OWNERDRAW style set, and the WM_DRAWITEM handler merely renders the focus rectangle; the buttons remain otherwise invisible. Full keyboard and mouse support is implemented through IsDialogMessage and the default message handler, respectively.

IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • Thank you very much for the effort. I've tried your code on a new project, and it indeed worked, until I added a visual style manifest. For some reason, different messages are sent based on the common controls version. So I guess the only solution here would be drawing everything manually on `WM_DRAWITEM`, as Raymond suggested. – Paul Dec 28 '13 at 16:41
  • @Paul I'll have to see how/why visual styles change the behavior, and if there is a less tedious way to solve this. However, since Raymond is **never** wrong, his comment worried me a bit. Maybe visual styles indeed add complexity to this all. You should probably explicitly state the use of visual styles in your question, too. – IInspectable Dec 28 '13 at 16:48
  • "You should probably explicitly state the use of visual styles in your question, too." - I thought nowadays it's the rule rather than the exception. Also, I didn't think it changes the messages sent. Anyway, I updated the question. – Paul Dec 28 '13 at 16:56
  • 1
    @Paul Any self-respecting developer should probably implement visual styles today. For a living, I've been maintaining a system running on Windows XP Embedded with visual styles turned off for years, that visual styles don't exist in my little universe. My apologies. – IInspectable Dec 28 '13 at 17:05