Home > Mobile >  Properly handling owner drawn Win32 button hovering
Properly handling owner drawn Win32 button hovering

Time:10-18

I want to add multiple color themes to my Win32 application, this means that I have to manually handle all the control drawing manually by using the BS_OWNERDRAW style flag. I then handle all the drawing in the WM_DRAWITEM message through the LPDRAWITEMSTRUCT structure stored in the lParam. Here's the problem though, by introducing owner drawing I also have to handle click and hover events, which in and of itself isn't a problem but it becomes one because it turns out that the LPDRAWITEMSTRUCT has no message being sent to indicate whether or not a control is being hovered. The itemState member do have messages like ODS_SELECTED, ODS_FOCUS and so on, but no flag for hovering only.

That being said, there does exist a flag named ODS_HOTLIGHT which according to MSDN should do the job, the problem however is that this flag never occurs for buttons, only menu items. I've tried using the SetWindowSubclass function by giving each control it's separate WindowProc callback where you can track the mouse leaving and entering the control. This works, but that also means that I need to transition all drawing commands over to the subclass procedure, which seems rather stupid to me, since the WM_DRAWITEM is intended for custom drawing in the first place.

You could always send WM_DRAWITEM manually via SendMessage but that also means that I need to provide an LPDRAWITEMSTRUCT manually which hasn't worked out that great. In essence I do not know what the best approach is to react to these hover events and draw them accordingly, the LPDRAWITEMSTRUCT does not provide such a flag and thus I have no idea what other approach to use. How should I tackle this?

The way I'm currently handling the WM_DRAWITEM message (buttons only):

case WM_DRAWITEM: {
  LPDRAWITEMSTRUCT pDIS = (LPDRAWITEMSTRUCT)lParam;
  HPEN borderPen = CreatePen(PS_INSIDEFRAME, 0, RGB(0, 0, 0));
  HGDIOBJ oldPen = SelectObject(pDIS->hDC, borderPen);
  HGDIOBJ oldBrush;

  if (pDIS->itemState & ODS_SELECTED) {
    oldBrush = SelectObject(pDIS->hDC, buttonHoverBrush);
  }
  else {
    oldBrush = SelectObject(pDIS->hDC, buttonDefaultBrush);
  }

  // Rounded button
  RoundRect(pDIS->hDC, pDIS->rcItem.left, pDIS->rcItem.top, pDIS->rcItem.right, pDIS->rcItem.bottom, 5, 5);

  //Clean up
  SelectObject(pDIS->hDC, oldPen);
  SelectObject(pDIS->hDC, oldBrush);
  DeleteObject(borderPen);

  // Calculate button dimensions
  int buttonWidth = pDIS->rcItem.right - pDIS->rcItem.left;
  int buttonHeight = pDIS->rcItem.bottom - pDIS->rcItem.top;

  WCHAR staticText[128];
  int len = SendMessage(pDIS->hwndItem, WM_GETTEXT, ARRAYSIZE(staticText), (LPARAM)staticText);
  HFONT buttonFont = (HFONT)SendMessage(pDIS->hwndItem, WM_GETFONT, 0, 0);

  SIZE buttonDim;
  HFONT oldFont = (HFONT)SelectObject(pDIS->hDC, buttonFont);
  GetTextExtentPoint32(pDIS->hDC, staticText, len, &buttonDim);

  SetTextColor(pDIS->hDC, RGB(255, 255, 255));
  SetBkMode(pDIS->hDC, TRANSPARENT);
  TextOut(pDIS->hDC, buttonWidth / 2 - buttonDim.cx / 2, buttonHeight / 2 - buttonDim.cy / 2, staticText, len);

  wasHandled = TRUE;
  result = TRUE;
  break;
}

The code listed above creates a button with a given background color and centers text within it. It also changes color to the hover rush when clicked. What I want to happen is that the button changes its color immediately upon hover and not upon click. Thank you in advance!

CodePudding user response:

I've tried using the SetWindowSubclass function by giving each control it's separate WindowProc callback where you can track the mouse leaving and entering the control. This works, but that also means that I need to transition all drawing commands over to the subclass procedure, which seems rather stupid to me, since the WM_DRAWITEM is intended for custom drawing in the first place.

Unfortunately, that is precisely what you will likely need to do.

For instance, you can have the button's subclass procedure handle the WM_MOUSEMOVE message to detect when the mouse is over the button, and handle the WM_MOUSELEAVE message to detect when the mouse moves out of the button. The message handler can use WM_MOUSEMOVE to call TrackMouseEvent() to trigger WM_MOUSELEAVE and WM_MOUSEHOVER messages. It can then use WM_MOUSEHOVER to set a flag and invalidate the button to trigger a repaint, and it can use WM_MOUSELEAVE to clear the flag and invalidate the button to trigger a repaint.

Inside the button's normal draw handler (which DOES NOT need to be moved to the subclass procedure. BTW), if the flag is set then draw the button as hovered, otherwise draw it as non-hovered.

The only gotcha with this approach is if you have multiple buttons then you will need multiple flags, one per button. But you can use (Get|Set)WindowLongPtr(GWL_USERDATA) or (Get|Set)Prop() to store/retrieve each button's flag inside/from the button's HWND directly Or, you can just maintain your own std::map (or other lookup table) of HWND-to-flag associations.

Another option would be to have the subclass procedure use the WM_MOUSE(MOVE|HOVER|LEAVE) messages to send simulated WM_DRAWITEM messages to the button, specifying/omitting the ODS_HOTLIGHT style as needed so the drawing code can look for that style. You might need the subclass procedure to intercept WM_PAINT messages to make that work, though. Not sure about that.

Otherwise, you could simply have your WM_DRAWITEM handler grab the mouse's current coordinates and see if they fall within the button's client area, and then draw accordingly.

  • Related