3

テーマが有効になっているときに Win32 のみを使用して、背景が透明なラジオ ボタン コントロールを作成しようとしています。これを行う理由は、ラジオ ボタンを画像の上に配置して、(灰色のデフォルト コントロールの背景ではなく) 画像を表示できるようにするためです。

箱から出してすぐに起こることは、コントロールが灰色のデフォルト コントロールの背景を持つことであり、以下のいずれWM_CTLCOLORSTATICWM_CTLCOLORBTNを処理することによってこれを変更する標準的な方法は機能しません。

case WM_CTLCOLORSTATIC:
    hdcStatic = (HDC)wParam;

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

    return (LRESULT)GetStockObject(NULL_BRUSH);
    break;  

これまでの私の調査では、これを達成する唯一の方法はオーナー ドローであることが示されています。私は所有者描画ラジオ ボタンでほとんどの方法を得ることができました - 以下のコードでは、ラジオ ボタンと透明な背景があります (背景は で設定されています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;

}                           

だから私の質問は、私が推測する2つの部分です:

  1. 目的の結果を達成するための他の方法を見逃していませんか?
  2. 私のコードでクリップされたボタンのコーナーの問題を修正し、透明な背景を維持することは可能ですか?
4

5 に答える 5

3

これをオンとオフをほぼ 3 か月間見た後、最終的に満足のいく解決策を見つけました。私が最終的に見つけたのは、何らかの理由でラジオ ボタンのエッジが WM_DRAWITEM 内のルーチンによって描画されていないことでしたが、コントロールの周囲の四角形でラジオ ボタン コントロールの親を無効にすると、それらが表示されました。

これの良い例が1つも見つからなかったので、完全なコードを提供しています(私自身のソリューションでは、所有者が描画したコントロールを独自のクラスにカプセル化したので、ボタンがチェックされているかどうかなどの詳細を提供する必要がありますか否か)

これは、ラジオボタンの作成 (親ウィンドウに追加) であり、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 メッセージを処理する必要があります。

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);
    }           
}

次は、ラジオ ボタン コントロールをサブクラス化するために使用されるウィンドウ プロシージャです。これは、すべてのウィンドウ メッセージで呼び出され、いくつかのメッセージを処理してから、未処理のメッセージをデフォルト プロシージャに渡します。

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);                
}

シンプルであるべき何かのための多くのコードと努力 - 背景画像の上にテーマラジオボタンを描く. うまくいけば、答えが他の誰かの痛みを救うでしょう!

*これに関する 1 つの大きな注意点は、背景 (塗りつぶしの四角形や画像など) の上にある所有者コントロールに対してのみ 100% 正しく機能することです。ただし、背景の上にラジコンを描画する場合にのみ必要なので、問題ありません。

于 2011-11-24T16:47:00.437 に答える
1

私もこれを少し前にやったことがあります。重要なのは、いつものように(ラジオ)ボタンを作成することだったことを覚えています。親は、タブコントロールではなく、ダイアログまたはウィンドウである必要があります。別の方法で行うこともできますが、ダイアログ用のメモリDC(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に似たいくつかの社内クラスと関数を使用していますが、あなたはその考えを理解する必要があると思います。ご覧のとおり、これらのコントロールの背景はメモリDCから描画されます。これが重要です。

これを試してみて、機能するかどうかを確認してください。

編集:タブコントロールをダイアログに追加し、コントロールをタブに配置する場合(私のアプリの場合)、その背景をキャプチャして、ダイアログのメモリDCにコピーする必要があります。これは少し醜いハックですが、グラデーションタブの背景を使用する贅沢なテーマをマシンが実行している場合でも機能します。

    // 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);

もう1つの興味深い点は、現在忙しいときに、すべてをちらつかないようにするために、WS_CLIPCHILDRENおよびWS_CLIPSIBLINGSスタイルで親と子(ボタン、静的、タブなど)を作成します。作成の順序は重要です。最初にタブに配置するコントロールを作成し、次にタブコントロールを作成します。逆ではありません(より直感的に感じますが)。これは、タブコントロールが、その上のコントロールによって隠されている領域をクリップする必要があるためです:)

于 2011-09-07T12:49:37.150 に答える
1

すぐに試すことはできませんが、私が思い出す限りでは、オーナー ドローは必要ありません。これを行う必要があります:

  1. から 1 を返しWM_ERASEBKGNDます。
  2. そこに背景を描画するためにDrawThemeParentBackgroundfromを呼び出します。WM_CTLCOLORSTATIC
  3. GetStockObject(NULL_BRUSH)から戻るWM_CTLCOLORSTATIC
于 2011-09-07T13:20:28.360 に答える
1
  1. サイズと座標のラジオ ボタンを知っているので、画像をそれらにコピーして閉じます。
  2. 次に、BS_PATTERN スタイルの CreateBrushIndirect を使用してブラシを作成します。
  3. さらに、通常のスキームに従って、COLOR メッセージ (WM_CTLCOLORSTATIC) に応答して、このブラシにハンドルを返します。
于 2013-10-14T11:37:25.950 に答える
0

なぜそんなに難しいのかわかりません。これはCustomDrawingで解決するのが一番です。これはCTabCtrlコントロールでノートブックを描画するための私のMFCハンドラーです。長方形を膨らませる必要がある理由はよくわかりません。そうしないと、黒い境界線が描画されるためです。

また、MSが作成したもう1つの概念上のバグは、PostEraseではなくPreErase描画フェーズを上書きする必要があるというIMHOです。しかし、後で行うと、チェックボックスが消えます。

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;    
} 
于 2012-01-26T06:23:00.720 に答える