◆三角ボタンの作成(VC++ 2005 MFC)

[ ホーム ] [ 上へ ]

・概要

プッシュボタンと言えば長方形と相場は決まっていて、Visual Studioの リソースエディタでボタンを貼り付けると長方形のボタンが親ウィンドウ(ダイアログ)に 表示されます。あとは作成されたボタンのプロパティウィンドウから通知メッセージに対する ハンドラを作成し、必要な処理を実装するだけです。

さらにCButtonクラスを派生させてサブクラスにするとより高度な処理が 可能になります。この場合はコンストラクタを呼び出したあと、Create関数を呼び出して ボタンオブジェクト(ウィンドウオブジェクト)をつくりCButtonの派生クラスに結び付けます。
CButton::Create関数の宣言は

virtual BOOL Create(
   LPCTSTR      lpszCaption,
   DWORD        dwStyle,
   const RECT& rect,
   CWnd*        pParentWnd,
   UINT         nID
);

となっています。ここで注目する点は第三引数の const RECT& rectで、ここで 指定した位置と大きさでボタンを作成します。型がRECTなので当然長方形ということになります。

いずれにしてもボタンは長方形で、 今回のお題の三角のボタンはできません。ではどうするかというとCButtonクラスを使うのは 当然ですが大まかには以下のような手順になるかと思います。

  1. CButtonの派生クラスのインスタンスを作成。(親ウィンドウのメンバ変数に加える)
  2. 上記Create関数でボタンオブジェクトを作成。
  3. Create実行の際、第二引数にBS_OWNERDRAWを設定して自分で描画する
  4. CRgnを使って三角形のリージョンを作成。
  5. SetWindowRgnでウィンドウリージョンを設定
  6. 2bでBS_OWNERDRAWを設定したので CButton::DrawItemをオーバーライドして ボタンを描画する。
  7. ボタンクリックなど必要なメッセージハンドラを作成して実装する。

SetWindowRgnでウィンドウリージョンを設定するとその外側をシステムは 描画しないので三角形のボタンが描画されますが、これだけではボタンクリック時の 凹凸がうまく表現できません。そこでオーナードローで自分で描画します。

これが面倒で、ボタンが押された状態、押されていない状態、使用不可の状態、 フォーカスがある状態などすべてをDrawItem内で自分で描画しないといけないです。今回は ボタンの凹凸とフォーカスがあたった状態のみを対象にします。

・準備

実際にプロジェクトで三角ボタンのダイアログをつくってみます。

  1. VSのファイル(F)→新規作成(N)→プロジェクト(P)でアプリケーションウィザードを表示。
  2. プロジェクトの種類(P)ペイン : Visual C++
    テンプレートペイン(T)       : MFCアプリケーション
    プロジェクト名              : Test(任意)
    
    でOKを押す。
  3. 3.アプリケーションウィザードの各ダイアログで以下のように設定。
    <アプリケーションの種類ダイアログ>
            アプリケーションの種類      :ダイアログベース(D)
    
    <ユーザインターフェイス機能ダイアログ>
            メインフレームのスタイル    :すべてのチェックボックスをはずす
    
    <高度な機能>
            高度な機能                  :すべてのチェックをはずす
    
    CTestAppクラスとCTestDlgクラスが生成されるので完了をクリック。
  4. 生成されたスケルトンはそのままにして、VSのプロジェクト(P)→クラスの追加(C)で
    クラスウィザードを起動。
    カテゴリ(C)   : MFC
    テンプレート : MFCクラス
    で 追加(A)をクリック。
    
    基本クラス(B)   : CButton
    クラス名        : CTriangleButton
    
    でCButtonの派生クラスを作成してください。

・CTriangleButton実装

本題の三角ボタンを表すCButtonクラスの派生クラス、 CTriangleButtonの実装を行います。実装内容の要点は

  1. リージョンを作成するための三角形の各頂点の座標を保持するメンバ変数の作成。
  2. ボタンオブジェクトの作成 & リージョン設定を行うオーバーロードするCreate関数の実装
  3. ボタン描画のためのDrawItemのオーバーライド

です。

ヘッダファイルを以下のように記述してください。

//TriangleButton.hヘッダファイル

#pragma once

// CTriangleButton
class CTriangleButton : public CButton
{
    DECLARE_DYNAMIC(CTriangleButton)

public:
    CTriangleButton();          //コンストラクタ
    virtual ~CTriangleButton(); //デストラクタ

protected:
    CPoint   m_point[3];        //三角形の各頂点

public:
    BOOL Create(LPCTSTR lpszCaption,                          //ボタン作成
                CRect rect, 
                CWnd *pParent, 
                UINT nID); 
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); //オーナードロー
    afx_msg void OnBnClicked();                               //ボタンクリック

protected:
    DECLARE_MESSAGE_MAP()
};

実装の中心はオーバーロードしたCreate関数と オーバーライドしたDrawItem関数です。二つの関数の実装を以下のように行ってください。

// TriangleButton.cpp : 実装ファイル
//
#include "stdafx.h"
#include "Test.h"
#include "TriangleButton.h"

// CTriangleButton
IMPLEMENT_DYNAMIC(CTriangleButton, CButton)

//コンストラクタ
CTriangleButton::CTriangleButton()
{
}

//デストラクタ
CTriangleButton::~CTriangleButton()
{
}

BEGIN_MESSAGE_MAP(CTriangleButton, CButton)
    ON_CONTROL_REFLECT(BN_CLICKED, &CTriangleButton::OnBnClicked)  
END_MESSAGE_MAP()

//ボタン構築 & リージョン作成
BOOL CTriangleButton::Create(LPCTSTR lpszCaption, CRect rect, CWnd *pParent, UINT nID)  
{
    CRgn rgn;  

    m_point[0].SetPoint(rect.Width() / 2, 0);  
    m_point[1].SetPoint(0, rect.Height());  
    m_point[2].SetPoint(rect.Width(), rect.Height());  

    while (TRUE)  
    {
        if (! rgn.CreatePolygonRgn(m_point, 3, WINDING))  
            break;

        if (! CButton::Create(lpszCaption,  
                              WS_VISIBLE | WS_TABSTOP | WS_CHILD
                           | BS_PUSHBUTTON | BS_OWNERDRAW,  
                              rect,
                              pParent,
                              nID))
            break;

        if (! SetWindowRgn(rgn, TRUE))  
            break;

        return TRUE;  
    }

    if (rgn.GetSafeHandle() != NULL) rgn.DeleteObject();  
    if (GetSafeHwnd() != NULL) DestroyWindow();  

    return FALSE;  
}

//オーナードロー
void CTriangleButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)  
{
    CDC    *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);  
    CRect  rect = lpDrawItemStruct->rcItem;  
    BOOL   bSelect = lpDrawItemStruct->itemState & ODS_SELECTED;
    BOOL   bFocus = lpDrawItemStruct->itemState & ODS_FOCUS;

    //ボタンの描画
    CBrush   *pOrigBrush = NULL;  
    CBrush   brush;  

    brush.CreateSolidBrush(bSelect ? RGB(0, 0, 240) : RGB(0, 0, 255));  
    pOrigBrush = pDC->SelectObject(&brush);  

    pDC->Polygon(m_point, 3);  

    pDC->SelectObject(pOrigBrush);
    brush.DeleteObject();

    //境界の描画
    CPen    *pOrigPen = NULL;  
    CPen    lpen(PS_SOLID, 2, ::GetSysColor(COLOR_BTNFACE));  
    CPen    dpen(PS_SOLID, 2, RGB(0, 0, 100)); 

    pDC->MoveTo(m_point[0]);
    pOrigPen = pDC->SelectObject(bSelect ? &lpen : &dpen);  
    pDC->LineTo(m_point[1]);

    pDC->SelectObject(bSelect ? &dpen : &lpen);  
    pDC->LineTo(m_point[2]);

    pDC->SelectObject(bSelect ? &lpen : &dpen);  
    pDC->LineTo(m_point[0]);

    pDC->SelectObject(pOrigPen);
    lpen.DeleteObject();
    dpen.DeleteObject();

    //ボタン表題描画
    CFont   *pOrigFont = NULL;  
    CFont   font;  
    font.CreatePointFont(80, _T("MS Gothic"));
    pOrigFont = pDC->SelectObject(&font);  

    CString   str;  
    GetWindowText(str);

    CSize     size(pDC->GetTextExtent(str));  
    CPoint    point = rect.CenterPoint();  
    point.Offset(-1 * size.cx / 2, bSelect ? size.cy + 10 : size.cy + 9);  
    CRect txtrect(point, size);  

    pDC->SetBkMode(TRANSPARENT);
    pDC->DrawText(str, &txtrect, DT_CENTER);  

    pDC->SelectObject(pOrigFont);
    font.DeleteObject();

    //フォーカス枠描画
    if (bFocus)  
    {
        CPoint   point[3];
        point[0] = m_point[0] + CPoint(0, bSelect ? 8 : 7);  
        point[1] = m_point[1] + CPoint(bSelect ? 8 : 7, -5);  
        point[2] = m_point[2] + CPoint(bSelect ? -8 : -7, -5);  

        CPen   pen(PS_DOT, 1, RGB(0, 0, 0));  
        pOrigPen = pDC->SelectObject(&pen);  
        pOrigBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);  

        pDC->Polygon(point, 3);  

        pDC->SelectObject(pOrigPen);
        pDC->SelectObject(pOrigBrush);
        pen.DeleteObject();
    }
}

//ボタンクリック
void CTriangleButton::OnBnClicked()
{
    TRACE(_T("CTriangleButton::OnBnClicked\n"));
}

ちょっと長いので簡単に説明しておきます。

1.Create関数

※SetWindowRgnの解説を読むと「必要がなくなると、システムが自動的に リージョンハンドルを削除します」との記述があるので、このAPIが成功したら、最後に後始末を する必要がないように思われます(rgn.DeleteObject()を実行しなくてもよい)。
ということで、CTriangleButtonのメンバ変数に持たずにローカル変数で処理してもGDIリソースの メモリリークは起こらないかと思います。(間違いでしたらご連絡いただくとありがたいです)

2.DrawItem関数

こちらの関数はちょっと長いので難しそうですが、 細かく?ボタンを描いているだけで実はそれほど難解ではないです。 注目してほしい点は1点で、最初の4行だけです。

CDC    *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);     //(A)
CRect  rect = lpDrawItemStruct->rcItem;                      //(B)
BOOL   bSelect = lpDrawItemStruct->itemState & ODS_SELECTED; //(C)
BOOL   bFocus = lpDrawItemStruct->itemState & ODS_FOCUS;     //(D)

ボタンの描画が必要になるとフレームワークからこの関数が呼び出されます。その際、引数に DRAWITEMSTRUCT構造体へのポインタ(lpDrawItemStructを受け取っています。詳しくは MSDNをご覧いただきたいですが、この構造体の各メンバから必要な情報を(A)から(D)に受け取って います。

(A)はデバイスコンテキストのハンドルからCDCの一時オブジェクトへのポインタを得て、 これで以下すべてのボタン描画を行います。

(B)は描画する四角形を取得します。(実際にはリージョンを設定しているので、 その外側は描画されません。

(C)はボタンが押されているか押されていないかを判断します。

(D)はボタンがフォーカスを持っているかどうかを判断します。

ここから以下は上記を使ってボタンが押された状態、押されてない状態とフォーカスを 持った状態、持っていない状態ごとにボタン、境界の影、表題を描いているだけです。 ただできるだけ簡略化したつもりなので実際をみてみると非常に雑です (私のセンスがないだけです)。なのでボタンらしく振舞うには影のつけ方などを 工夫する必要があるでしょう。

最後に上記サンプルではプロパティシートからボタンクリックのメッセージハンドラ (OnBnClicked関数)を実装しています。実装といってもクリックしたことをデバッガに 表示しているだけです。

・CTriangleButtonの使用

では実際にCTriagleButtonを使ってみます。
アプリケーションウィザードでメインウィンドウ(CTestDlgダイアログ)のスケルトンができていますので、 そこに三角ボタンを作成します。CTestDlgのヘッダに以下のように記述して、CTriangleButtonの メンバ変数を加えます。

// TestDlg.h : ヘッダーファイル
#pragma once

#include "TriangleButton.h" //三角ボタンのヘッダをインクルード

// CTestDlg ダイアログ
class CTestDlg : public CDialog 
{
// ・
// ・

protected:
    CTriangleButton     m_TrigButton; //三角ボタン 

// ・
// ・
};

これだけではダイアログにボタンはつくられないので、CTestDlg::OnInitDialogで Create関数を呼び出すと三角ボタンが構築されます。

//TestDlg.cpp : 実装ファイル

BOOL CTestDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    SetIcon(m_hIcon, TRUE);  // 大きいアイコンの設定
    SetIcon(m_hIcon, FALSE); // 小さいアイコンの設定

    CRect   rect;
    GetWindowRect(&rect);

    CPoint  point = rect.CenterPoint();
    point.Offset(-50, -50);

    m_TrigButton.Create(_T("三角ボタン"),
                        CRect(point, CSize(100, 100)),
                        this,
                        1);

    return TRUE; // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

上記はダイアログのクライアント領域の中心に縦、横それぞれ100の四角形に 収まる三角形のボタンを作成しています。実行すると以下のようになります。