Прозрачный переключатель с темами с использованием Win32 - PullRequest
3 голосов
/ 07 сентября 2011

Я пытаюсь сделать элемент управления переключателем с прозрачным фоном, используя только Win32, когда темы включены.Причина для этого заключается в том, чтобы позволить радиокнопке быть размещенной над изображением и показывать изображение (а не серый фон управления по умолчанию).

Что происходит из коробки, так это то, что элемент управления будет иметьсерый фон управления по умолчанию и стандартный метод изменения этого значения с помощью WM_CTLCOLORSTATIC или WM_CTLCOLORBTN, как показано ниже, не работают:

case WM_CTLCOLORSTATIC:
    hdcStatic = (HDC)wParam;

    SetTextColor(hdcStatic, RGB(0,0,0)); 
    SetBkMode(hdcStatic,TRANSPARENT);

    return (LRESULT)GetStockObject(NULL_BRUSH);
    break;  

Мои исследования до сих пор показывают, что Owner Draw является единственным способомдля достижения этой цели.Мне удалось пройти большую часть пути с переключателем Owner Draw - с кодом ниже у меня есть переключатель и прозрачный фон (фон установлен в WM_CTLCOLORBTN).Тем не менее, края радио-проверки обрезаются с помощью этого метода - я могу получить их обратно, раскомментировав вызов функции DrawThemeParentBackgroundEx, но это нарушает прозрачность.

void DrawRadioControl(HWND hwnd, HTHEME hTheme, HDC dc, bool checked, RECT rcItem)
{
    if (hTheme)
    {
      static const int cb_size = 13;

      RECT bgRect, textRect;
      HFONT font = (HFONT)SendMessageW(hwnd, WM_GETFONT, 0, 0);
      WCHAR *text = L"Experiment";

      DWORD state = ((checked) ? RBS_CHECKEDNORMAL : RBS_UNCHECKEDNORMAL) | ((bMouseOverButton) ? RBS_HOT : 0); 

      GetClientRect(hwnd, &bgRect);
      GetThemeBackgroundContentRect(hTheme, dc, BP_RADIOBUTTON, state, &bgRect, &textRect);

      DWORD dtFlags = DT_VCENTER | DT_SINGLELINE;

      if (dtFlags & DT_SINGLELINE) /* Center the checkbox / radio button to the text. */
         bgRect.top = bgRect.top + (textRect.bottom - textRect.top - cb_size) / 2;

      /* adjust for the check/radio marker */
      bgRect.bottom = bgRect.top + cb_size;
      bgRect.right = bgRect.left + cb_size;
      textRect.left = bgRect.right + 6;

      //Uncommenting this line will fix the button corners but breaks transparency
      //DrawThemeParentBackgroundEx(hwnd, dc, DTPB_USECTLCOLORSTATIC, NULL);

      DrawThemeBackground(hTheme, dc, BP_RADIOBUTTON, state, &bgRect, NULL);
      if (text)
      {
          DrawThemeText(hTheme, dc, BP_RADIOBUTTON, state, text, lstrlenW(text), dtFlags, 0, &textRect);

      }

   }
   else
   {
       // Code for rendering the radio when themes are not present
   }

}

Вызывается метод вышеиз WM_DRAWITEM, как показано ниже:

case WM_DRAWITEM:
{
    LPDRAWITEMSTRUCT pDIS = (LPDRAWITEMSTRUCT)lParam;
    hTheme = OpenThemeData(hDlg, L"BUTTON");    

    HDC dc = pDIS->hDC;

    wchar_t sCaption[100];
    GetWindowText(GetDlgItem(hDlg, pDIS->CtlID), sCaption, 100);
    std::wstring staticText(sCaption);

    DrawRadioControl(pDIS->hwndItem, hTheme, dc, radio_group.IsButtonChecked(pDIS->CtlID), pDIS->rcItem, staticText);                               

    SetBkMode(dc, TRANSPARENT);
    SetTextColor(hdcStatic, RGB(0,0,0));                                
    return TRUE;

}                           

Итак, мой вопрос состоит из двух частей:

  1. Я пропустил какой-то другой способ достичь желаемого результата?
  2. Можно ли исправить проблему с обрезанными углами кнопок в моем коде и при этом иметь прозрачный фон

Ответы [ 5 ]

3 голосов
/ 24 ноября 2011

Посмотрев на это в течение почти трех месяцев, я наконец нашел решение, которым я доволен.В конечном итоге я обнаружил, что края переключателей по какой-то причине не прорисовывались процедурой в WM_DRAWITEM, но если я аннулировал родительский элемент переключателя в прямоугольнике вокруг элемента управления, они появлялись.

Поскольку яне смог найти ни одного хорошего примера этого, я предоставляю полный код (в своем собственном решении я инкапсулировал элементы управления, нарисованные владельцем, в их собственный класс, поэтому вам нужно будет предоставить некоторые детали, например, проверена кнопка или нет)

Это создание радиокнопки (добавление ее в родительское окно), также установка GWL_UserData и создание подкласса радиокнопки:

HWND hWndControl = CreateWindow( _T("BUTTON"), caption, WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, 
    xPos, yPos, width, height, parentHwnd, (HMENU) id, NULL, NULL);

// Using SetWindowLong and GWL_USERDATA I pass in the this reference, allowing my 
// window proc toknow about the control state such as if it is selected
SetWindowLong( hWndControl, GWL_USERDATA, (LONG)this);

// And subclass the control - the WndProc is shown later
SetWindowSubclass(hWndControl, OwnerDrawControl::WndProc, 0, 0);

Поскольку это рисование владельца, нам нужно обработать WM_DRAWITEMсообщение в родительском окне proc.

case WM_DRAWITEM:      
{      
    LPDRAWITEMSTRUCT pDIS = (LPDRAWITEMSTRUCT)lParam;      
    hTheme = OpenThemeData(hDlg, L"BUTTON");          

    HDC dc = pDIS->hDC;      

    wchar_t sCaption[100];      
    GetWindowText(GetDlgItem(hDlg, pDIS->CtlID), sCaption, 100);      
    std::wstring staticText(sCaption);      

    // Controller here passes to a class that holds a map of all controls 
    // which then passes on to the correct instance of my owner draw class
    // which has the drawing code I show below
    controller->DrawControl(pDIS->hwndItem, hTheme, dc, pDIS->rcItem, 
        staticText, pDIS->CtlID, pDIS->itemState, pDIS->itemAction);    

    SetBkMode(dc, TRANSPARENT);      
    SetTextColor(hdcStatic, RGB(0,0,0));     

    CloseThemeData(hTheme);                                 
    return TRUE;      

}    

Вот метод DrawControl - он имеет доступ к переменным уровня класса, позволяющим управлять состоянием, поскольку при обращении владельца это не обрабатывается автоматически.

void OwnerDrawControl::DrawControl(HWND hwnd, HTHEME hTheme, HDC dc, bool checked, RECT rcItem, std::wstring caption, int ctrlId, UINT item_state, UINT item_action)
{   
    // Check if we need to draw themed data    
    if (hTheme)
    {   
        HWND parent = GetParent(hwnd);      

        static const int cb_size = 13;                      

        RECT bgRect, textRect;
        HFONT font = (HFONT)SendMessageW(hwnd, WM_GETFONT, 0, 0);

        DWORD state;

        // This method handles both radio buttons and checkboxes - the enums here
        // are part of my own code, not Windows enums.
        // We also have hot tracking - this is shown in the window subclass later
        if (Type() == RADIO_BUTTON) 
            state = ((checked) ? RBS_CHECKEDNORMAL : RBS_UNCHECKEDNORMAL) | ((is_hot_) ? RBS_HOT : 0);      
        else if (Type() == CHECK_BOX)
            state = ((checked) ? CBS_CHECKEDNORMAL : CBS_UNCHECKEDNORMAL) | ((is_hot_) ? RBS_HOT : 0);      

        GetClientRect(hwnd, &bgRect);

        // the theme type is either BP_RADIOBUTTON or BP_CHECKBOX where these are Windows enums
        DWORD theme_type = ThemeType(); 

        GetThemeBackgroundContentRect(hTheme, dc, theme_type, state, &bgRect, &textRect);

        DWORD dtFlags = DT_VCENTER | DT_SINGLELINE;

        if (dtFlags & DT_SINGLELINE) /* Center the checkbox / radio button to the text. */
            bgRect.top = bgRect.top + (textRect.bottom - textRect.top - cb_size) / 2;

        /* adjust for the check/radio marker */
        // The +3 and +6 are a slight fudge to allow the focus rectangle to show correctly
        bgRect.bottom = bgRect.top + cb_size;
        bgRect.left += 3;
        bgRect.right = bgRect.left + cb_size;       

        textRect.left = bgRect.right + 6;       

        DrawThemeBackground(hTheme, dc, theme_type, state, &bgRect, NULL);          
        DrawThemeText(hTheme, dc, theme_type, state, caption.c_str(), lstrlenW(caption.c_str()), dtFlags, 0, &textRect);                    

        // Draw Focus Rectangle - I still don't really like this, it draw on the parent
        // mainly to work around the way DrawFocus toggles the focus rect on and off.
        // That coupled with some of my other drawing meant this was the only way I found
        // to get a reliable focus effect.
        BOOL bODAEntire = (item_action & ODA_DRAWENTIRE);
        BOOL bIsFocused  = (item_state & ODS_FOCUS);        
        BOOL bDrawFocusRect = !(item_state & ODS_NOFOCUSRECT);

        if (bIsFocused && bDrawFocusRect)
        {
            if ((!bODAEntire))
            {               
                HDC pdc = GetDC(parent);
                RECT prc = GetMappedRectanglePos(hwnd, parent);
                DrawFocus(pdc, prc);                
            }
        }   

    }
      // This handles drawing when we don't have themes
    else
    {
          TEXTMETRIC tm;
          GetTextMetrics(dc, &tm);      

          RECT rect = { rcItem.left , 
              rcItem.top , 
              rcItem.left + tm.tmHeight - 1, 
              rcItem.top + tm.tmHeight - 1};    

          DWORD state = ((checked) ? DFCS_CHECKED : 0 ); 

          if (Type() == RADIO_BUTTON) 
              DrawFrameControl(dc, &rect, DFC_BUTTON, DFCS_BUTTONRADIO | state);
          else if (Type() == CHECK_BOX)
              DrawFrameControl(dc, &rect, DFC_BUTTON, DFCS_BUTTONCHECK | state);

          RECT textRect = rcItem;
          textRect.left = rcItem.left + 19;

          SetTextColor(dc, ::GetSysColor(COLOR_BTNTEXT));
          SetBkColor(dc, ::GetSysColor(COLOR_BTNFACE));
          DrawText(dc, caption.c_str(), -1, &textRect, DT_WORDBREAK | DT_TOP);
    }           
}

Далее идет процесс окна, который используется для переходаss переключатель радио-кнопки - он вызывается со всеми сообщениями Windows и обрабатывает несколько, а затем передает необработанные сообщения в процедуру по умолчанию.

LRESULT OwnerDrawControl::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
                               LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    // Get the button parent window
    HWND parent = GetParent(hWnd);  

    // The page controller and the OwnerDrawControl hold some information we need to draw
    // correctly, such as if the control is already set hot.
    st_mini::IPageController * controller = GetWinLong<st_mini::IPageController *> (parent);

    // Get the control
    OwnerDrawControl *ctrl = (OwnerDrawControl*)GetWindowLong(hWnd, GWL_USERDATA);

    switch (uMsg)
    {       
        case WM_LBUTTONDOWN:
        if (controller)
        {
            int ctrlId = GetDlgCtrlID(hWnd);

            // OnCommand is where the logic for things like selecting a radiobutton
            // and deselecting the rest of the group lives.
            // We also call our Invalidate method there, which redraws the radio when
            // it is selected. The Invalidate method will be shown last.
            controller->OnCommand(parent, ctrlId, 0);       

            return (0);
        }
        break;
        case WM_LBUTTONDBLCLK:
            // We just treat doubleclicks as clicks
            PostMessage(hWnd, WM_LBUTTONDOWN, wParam, lParam);
            break;
        case WM_MOUSEMOVE:
        {
            if (controller)                 
            {
                // This is our hot tracking allowing us to paint the control
                // correctly when the mouse is over it - it sets flags that get
                // used by the above DrawControl method
                if(!ctrl->IsHot())
                {
                    ctrl->SetHot(true);
                    // We invalidate to repaint
                    ctrl->InvalidateControl();

                    // Track the mouse event - without this the mouse leave message is not sent
                    TRACKMOUSEEVENT tme;
                    tme.cbSize = sizeof(TRACKMOUSEEVENT);
                    tme.dwFlags = TME_LEAVE;
                    tme.hwndTrack = hWnd;

                    TrackMouseEvent(&tme);
                }
            }    
            return (0);
        }
        break;
    case WM_MOUSELEAVE:
    {
        if (controller)
        {
            // Turn off the hot display on the radio
            if(ctrl->IsHot())
            {
                ctrl->SetHot(false);        
                ctrl->InvalidateControl();
            }
        }

        return (0);
    }
    case WM_SETFOCUS:
    {
        ctrl->InvalidateControl();
    }
    case WM_KILLFOCUS:
    {
        RECT rcItem;
        GetClientRect(hWnd, &rcItem);
        HDC dc = GetDC(parent);
        RECT prc = GetMappedRectanglePos(hWnd, parent);
        DrawFocus(dc, prc);

        return (0);
    }
    case WM_ERASEBKGND:
        return 1;
    }
    // Any messages we don't process must be passed onto the original window function
    return DefSubclassProc(hWnd, uMsg, wParam, lParam); 

}

Наконец, последний маленький кусочек головоломки состоит в том, что вам нужно сделать недействительнымиконтроль (перерисовать его) в нужное время.В конце концов я обнаружил, что признание родителя недействительным позволило рисованию работать на 100% правильно.Это вызывало мерцание до тех пор, пока я не осознал, что могу уйти, только сделав недействительным прямоугольник размером с проверку радио, а не такой большой, как весь элемент управления, включая текст.

void InvalidateControl()
{
    // GetMappedRectanglePos is my own helper that uses MapWindowPoints 
    // to take a child control and map it to its parent
    RECT rc = GetMappedRectanglePos(ctrl_, parent_);

    // This was my first go, that caused flicker
    // InvalidateRect(parent_, &rc_, FALSE);    

    // Now I invalidate a smaller rectangle
    rc.right = rc.left + 13;
    InvalidateRect(parent_, &rc, FALSE);                
}

Многокода и усилий для чего-то, что должно быть простым - рисование тематического переключателя поверх фонового изображения.Надеемся, что ответ спасет кого-то еще от боли!

* Одно большое предостережение, заключающееся в том, что он работает на 100% корректно только для элементов управления владельца над фоном (таких как прямоугольник заливки иликартинка).Это нормально, так как это необходимо только при рисовании радиоуправления на фоне.

1 голос
/ 14 октября 2013
  1. Зная радиокнопки размеров и координат, мы скопируем Образ для них закрыт.
  2. Затем создаем кисть с помощью Стиль BS_PATTERN CreateBrushIndirect
  3. дальше по обычная схема - мы возвращаем дескриптор этой кисти в ответ на COLOR - сообщение (WM_CTLCOLORSTATIC).
1 голос
/ 07 сентября 2011

Я не могу сразу попробовать это, но, насколько я помню, вам не нужен розыгрыш владельца.Вам нужно сделать следующее:

  1. Возврат 1 из WM_ERASEBKGND.
  2. Вызов DrawThemeParentBackground из WM_CTLCOLORSTATIC, чтобы нарисовать фон там.1011 * из WM_CTLCOLORSTATIC.
1 голос
/ 07 сентября 2011

Я тоже это делал некоторое время назад.Я помню, что ключом было просто создать (радио) кнопки, как обычно.Родителем должен быть диалог или окно, а не элемент управления вкладкой.Вы могли бы сделать это по-другому, но я создал память постоянного тока (m_mdc) для диалога и нарисовал фон для этого.Затем добавьте OnCtlColorStatic и OnCtlColorBtn для вашего диалога:

virtual HBRUSH OnCtlColorStatic(HDC hDC, HWND hWnd)
{
    RECT rc;
    GetRelativeClientRect(hWnd, m_hWnd, &rc);
    BitBlt(hDC, 0, 0, rc.right - rc.left, rc.bottom - rc.top, m_mdc, rc.left, rc.top, SRCCOPY);
    SetBkColor(hDC, GetSysColor(COLOR_BTNFACE));
    if (IsAppThemed())
        SetBkMode(hDC, TRANSPARENT);
    return (HBRUSH)GetStockObject(NULL_BRUSH);
}

virtual HBRUSH OnCtlColorBtn(HDC hDC, HWND hWnd)
{
    return OnCtlColorStatic(hDC, hWnd);
}

В коде используются некоторые собственные классы и функции, аналогичные MFC, но я думаю, вы должны понять.Как вы можете видеть, он рисует фон этих элементов управления из памяти постоянного тока, это ключ.

Попробуйте и посмотрите, работает ли он!

РЕДАКТИРОВАТЬ: Если вы добавляете элемент управления вкладки вВ диалоговом окне и поместите элементы управления на вкладку (как это было в моем приложении), вы должны захватить его фон и скопировать его в память постоянного тока диалога.Это немного уродливый хак, но он работает, даже если на машине запущена какая-то экстравагантная тема, которая использует градиентный фон вкладки:

    // calculate tab dispay area

    RECT rc;
    GetClientRect(m_tabControl, &rc);
    m_tabControl.AdjustRect(false, &rc);
    RECT rc2;
    GetRelativeClientRect(m_tabControl, m_hWnd, &rc2);
    rc.left += rc2.left;
    rc.right += rc2.left;
    rc.top += rc2.top;
    rc.bottom += rc2.top;

    // copy that area to background

    HRGN hRgn = CreateRectRgnIndirect(&rc);
    GetRelativeClientRect(m_hWnd, m_tabControl, &rc);
    SetWindowOrgEx(m_mdc, rc.left, rc.top, NULL);
    SelectClipRgn(m_mdc, hRgn);
    SendMessage(m_tabControl, WM_PRINTCLIENT, (WPARAM)(HDC)m_mdc, PRF_CLIENT);
    SelectClipRgn(m_mdc, NULL);
    SetWindowOrgEx(m_mdc, 0, 0, NULL);
    DeleteObject(hRgn);

Еще один интересный момент, пока мы заняты, чтобы получить еговсе не мерцающие создают родительские и дочерние элементы (кнопки, статика, вкладки и т. д.) в стиле WS_CLIPCHILDREN и WS_CLIPSIBLINGS.Порядок создания важен: сначала создайте элементы управления, которые вы помещаете на вкладки, а затем создайте элемент управления вкладками.Не наоборот (хотя это кажется более интуитивным).Это потому, что элемент управления tab должен обрезать область, скрытую элементами управления на нем:)

0 голосов
/ 26 января 2012

Понятия не имею, почему вы делаете это так сложно, это лучше всего решить с помощью CustomDrawing Это мой обработчик MFC для рисования блокнота на элементе управления CTabCtrl. Я не совсем уверен, почему мне нужно надуть прямоугольник, потому что, если я не сделаю этого, будет нарисована черная граница.

И еще одна концептуальная ошибка, допущенная MS, это ИМХО, что мне нужно перезаписать фазу рисования PreErase вместо PostErase. Но если я сделаю это позже, флажок исчезнет.

afx_msg void AguiRadioButton::OnCustomDraw(NMHDR* notify, LRESULT* res) {
    NMCUSTOMDRAW* cd  = (NMCUSTOMDRAW*)notify;            
    if (cd->dwDrawStage == CDDS_PREERASE) {
        HTHEME theme = OpenThemeData(m_hWnd, L"Button");
        CRect r = cd->rc; r.InflateRect(1,1,1,1);
        DrawThemeBackground(theme, cd->hdc, TABP_BODY, 0, &r,NULL);
        CloseThemeData(theme);
        *res = 0;
    }
    *res = 0;    
} 
...