wabc库程序设计-button

在这一篇文章里,主要介绍如何编写一个button。读者看完这篇文章后,能掌握编写自绘控件的一些关键知识点。

按钮(Button)是windows上一个常用的控件,很多应用程序里都有按钮的存在,同时,它的实现也是简单的,若没有自绘控件的经验,编写一个自定义的按钮是一个很好的练手项目。

先看看标准的windows按钮。

winmain.h

#pragma once
#include "wabc.h"

class main_wnd :public wabc::wndbase
{
    WABC_DECLARE_MSG_MAP()
    wabc::button m_button;
public:
    typedef main_wnd self;

    main_wnd();
    virtual ~main_wnd(){}

    bool on_create(wabc::msg_create &msg);

    bool on_destroy(wabc::msg_destroy &msg)
    {
        ::PostQuitMessage(0);
        return false;

    }

    bool on_paint(wabc::dcclass &dc, const rect &rtClip, wabc::msg_paint &msg)
    {
        dc.fill(rtClip, dc.black_brush());
        return true;
    }

    bool on_ok(wabc::msg_command &msg)
    {
        msgbox.information.ok(_T("You have clicked the 'OK' button!"));
        return true;
    }
};

winmain.cpp

#include "winmain.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int iCmdShow)
{
    wabc::application app(hInstance);
    main_wnd wnd;
    wnd.create(_T("BUTTON"));

    MSG  msg;
    BOOL bRet;

    while ((bRet = ::GetMessage(&msg, NULL, 0, 0)) != 0)
    {
        if (bRet == -1)
            return 0;

        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }
    return int(msg.wParam);
}

main_wnd::main_wnd()
{
    WABC_BEGIN_MSG_MAP(self)
        WABC_ON_CREATE(&self::on_create)
        WABC_ON_DESTROY(&self::on_destroy)
        WABC_ON_PAINT(&self::on_paint)
        BN_ON_CLICKED(IDOK, &self::on_ok)
    WABC_END_MSG_MAP()
}

bool main_wnd::on_create(wabc::msg_create &msg)
{
    const RECT rt = { 10, 10, 10 + 73, 10 + 26 };
    m_button.create(m_hWnd, WS_CHILD | WS_VISIBLE, 0, _T("确定"), IDOK, &rt);
    return false;
}

wabc::button创建的是标准的windows按钮控件,它从wabc::scwnd派生,因为超类化了windows的"BUTTON"类,所以wabc::button能拦截所有的windows消息。若需要更多的了解超类化,请参阅“win32消息映射13-子类化和超类化”一文。

wabc::button单击后,会往其parent发送一个BN_CLICKED的WM_COMMAND消息,当前程序里,main_wnd收到这消息后,会弹出一个message box:

bool main_wnd::on_ok(wabc::msg_command &msg)
{
    msgbox.information.ok(_T("You have clicked the 'OK' button!"));
    return true;
}

msgbox是main_wnd的一个属性,msgbox.information.ok(...)实际上是调用api MessageBox:

::MessageBox(m_hWnd,_T("You have clicked the 'OK' button!"), _T("BUTTON"),MB_OK|MB_ICONINFORMATION);

若追踪msgbox的源码,会发现,编译器能将里面的代码inline化,从而有直接调用api的性能,而使用上,却比直接调用MessageBox友好。

编译运行上面代码,会出现如下界面:

windows button

界面上按钮的“确定”两字好丑,原因在于没有给按钮设定字体。所以,我们要给button设定一种字体,首先创建字体:

class main_wnd
{
    // ...
    wabc::font m_font;
}

main_wnd::main_wnd()
{
    // ...
    LOGFONT lf = { 0 };

    lf.lfHeight = -14;
    lf.lfWeight = FW_NORMAL;
    lf.lfCharSet = GB2312_CHARSET;
    lf.lfOutPrecision = 3;
    lf.lfClipPrecision = 2;
    lf.lfQuality = 1;
    lf.lfPitchAndFamily = 49;

    ::wcscpy_s(lf.lfFaceName, _T("新宋体"));

    m_font.create(lf);
}

然后赋值给m_button

bool main_wnd::on_create(wabc::msg_create &msg)
{
    const RECT rt = { 10, 10, 10 + 73, 10 + 26 };
    m_button.create(m_hWnd, WS_CHILD | WS_VISIBLE, 0, _T("确定"), IDOK, &rt);
    m_button.font= m_font;
    return false;
}

注意这句:m_button.font= m_font,这里实际上是给m_button.m_hWnd发送一个WM_SETFONT的消息。

再一次编译运行,会看到,这次按钮上的字体好看多了。

windows button

将鼠标移动到“确定”按钮,左键按下,再松开,这时候才会弹出Message box。由此可以判定,BN_CLICK消息是在WM_LBUTTONUP中触发。
将鼠标移动到“确定”按钮,左键按下,不松开,移动鼠标,离开按钮的区域,会发现,按钮会自动弹起,这里能判定,里面肯定对WM_MOUSELEAVE消息有处理。

我们这里自定义一种按钮,鼠标移动到上面时候,是一种样式,离开时,是一种样式,鼠标左键按下时,又是另外一种样式。鼠标左键按下,没离开按钮区域再松开,这时候需要发送BN_CLICK消息。由此,可以定义mybutton了:

class mybutton : public wabc::wndbase
{
    WABC_DECLARE_MSG_MAP()

    // m_flag是按位做标记的,若m_flag & flag_lbutton_down,表明鼠标左键按下,
    // 若m_flag & flag_track,表明鼠标正移动到窗口上。
    enum{ flag_lbutton_down = 1, flag_track = flag_lbutton_down * 2 };
    size_t m_flag;

    string m_text;    // 用作按钮的标题
    HFONT m_hFont;    // 用作接收父窗口的WM_SETFONT消息

public:
    typedef mybutton self;
    mybutton();
    virtual ~mybutton(){}

public:
    bool on_create(wabc::msg_create &msg);
    bool on_paint(wabc::dcclass &dc, const rect &rtClip, wabc::msg_paint &msg);

    bool on_set_font(wabc::msg_setfont &msg)
    {
        m_hFont = msg.hFont;
        return true;
    }

    bool on_mouse_move(wabc::msg_mouse &msg);
    bool on_lbutton_down(wabc::msg_mouse &msg);
    bool on_lbutton_up(wabc::msg_mouse &msg);
    bool on_mouse_leave(wabc::msg_struct &msg);
};

mybutton::mybutton() : m_hFont(0)
{
    WABC_BEGIN_MSG_MAP(self)
        WABC_ON_CREATE(&self::on_create)
        WABC_ON_PAINT(&self::on_paint)
        WABC_ON_SETFONT(&self::on_set_font)

        WABC_ON_MOUSEMOVE(&self::on_mouse_move)
        WABC_ON_LBUTTONDOWN(&self::on_lbutton_down)
        WABC_ON_LBUTTONUP(&self::on_lbutton_up)
        WABC_ON_MOUSELEAVE(&self::on_mouse_leave)
    WABC_END_MSG_MAP()
}

要想接收鼠标离开消息,必须调用_TrackMouseEvent api。调用的时机在鼠标移动消息里,

bool mybutton::on_mouse_move(wabc::msg_mouse &msg)
{
    if ((m_flag & flag_track) == 0)
    {
        TRACKMOUSEEVENT tme;
        tme.cbSize = sizeof(tme);
        tme.hwndTrack = m_hWnd;
        tme.dwFlags = TME_LEAVE;
        if (::_TrackMouseEvent(&tme) != 0)
            m_flag |= flag_track;

        this->invalidate();
    }
    return true;
}

鼠标离开后,一切复位:

bool mybutton::on_mouse_leave(wabc::msg_struct &msg)
{
    m_flag = 0;
    this->invalidate();
    return true;
}

鼠标按下后,置一个标记位,表明鼠标按下:

bool mybutton::on_lbutton_down(wabc::msg_mouse &msg)
{
    this->set_focus();
    m_flag |= flag_lbutton_down;
    this->invalidate();
    return true;
}

鼠标松开后,发送BN_CLICK消息:

bool mybutton::on_lbutton_up(wabc::msg_mouse &msg)
{
    if (m_flag & flag_lbutton_down)
    {
        m_flag &= ~flag_lbutton_down;
        this->invalidate();

        HWND hParent = ::GetParent(m_hWnd);
        const size_t idFrom = ::GetWindowLong(m_hWnd, GWL_ID);
        const LRESULT ret = ::SendMessage(hParent, WM_COMMAND, MAKEWPARAM(idFrom, BN_CLICKED), LPARAM(m_hWnd));
    }
    return true;
}

这里,可以看到WM_COMMAND消息的实质。它实际上是一种规范,规范比实现更为重要,它是一种无形的公共接口。任何按照规范而做的,都能获得预期的结果。

对鼠标消息的处理,都没有涉及到绘图。只有在状态改变的时候,发送一个需要绘图的通知。也就是说,需要将状态保存起来,供绘图使用。绘图时,是根据状态而自绘,至于状态什么时候改变,不是绘图时所关心的。编写自绘控件,这一点要切记。

bool mybutton::on_paint(wabc::dcclass &dc, const rect &rtClip, wabc::msg_paint &msg)
{
    DWORD clrFill;

    if (m_flag & flag_lbutton_down)
        clrFill = RGB(190, 230, 253);
    else if (m_flag & flag_track)
        clrFill = RGB(229, 241, 251);
    else
        clrFill = RGB(225, 225, 225);

    dc.bkmode = TRANSPARENT;

    dc.fill(rtClip, clrFill);
    dc.use_font(m_hFont);
    dc.center_text(this->client_rect(), m_text);
    dc.discard();
    return true;
}

这里注意dc.use_font这句,里面实际上是调用SelectObject api,使用过SelectObject的都体会过临时变量的苦恼。这里,将SelectObject的返回值保存在dcclass,调用其discard时恢复。

至此,mybutton的代码基本已经完成,将wabc::button换成mybutton,重新编译运行,界面如下:

mybutton

将鼠标移上去,移开,或按下弹起,看看效果。

button的标题应该是可以改变,这可以通过WM_SETTEXT实现:

class mybutton : public wabc::wndbase
{
    //...
public:
    //...
    bool on_set_text(wabc::msg_settext &msg)
    {
        m_text = msg.text;
        this->invalidate();
        return true;
    }
};

mybutton::mybutton()
{
    WABC_BEGIN_MSG_MAP(self)
        WABC_ON_CREATE(&self::on_create)
        WABC_ON_SETTEXT(&self::on_set_text)
        WABC_ON_PAINT(&self::on_paint)
        // ...
    WABC_END_MSG_MAP()
}

现在可以将m_button的标题改成“取消”:

m_button.text= _T("取消");

对于图像button,所需要做的改变就是on_paint里面的代码,还有对应的成员变量,m_text和m_hFont可能都不需要了。很多按钮的图片都有4态的:正常、鼠标移动、鼠标按下和按钮Disable,这时候可以根据m_flag的标记位选择对应图片。至于如何把图片画到DC上去,这是另外一个话题,网上有很多答案可供参考。

上面的按钮,并没有对按键消息进行响应。一个好的控件应该是支持键盘的,这作为一个拓展留给读者练习,关键是映射WM_KEYDOWN和WM_KEYUP消息。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值