65.9K
CodeProject 正在变化。 阅读更多。
Home

CWinListBox - CWin 派生的自定义列表框控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (21投票s)

2004 年 8 月 17 日

11分钟阅读

viewsIcon

99626

downloadIcon

3667

逐步创建自定义列表框控件,从通用的 CWin 开始。

引言

您之所以在这里,是因为您认为自己是新手,并且希望获得一些关于如何从通用的 CWin 创建自定义控件的技巧。如果您已经从头开始构建了一个控件,那么这里将没有什么值得您关注的。本文的内容是入门级的,纯粹是教学性质的。唯一的目标是解决“这到底该如何开始做?”这个问题。

话虽如此,现在有一条警告。用自己制作的控件替换现有控件通常很难找到理由。例如,CListBox 包含了大量的功能,我建议任何有兴趣进行定制的人都对其进行子类化,根据需要添加和/或删除功能。Code Project 有许多文章,可以让您添加图标、支持多行文本、提供复选框和工具提示,应有尽有。几乎任何您能梦想到的功能都已在这里。也许缺少的是在背景中绘制或放置位图的技巧,以及在修改标准滚动条外观的同时保留它们的方法。如果您知道这些,请发帖。

必须直接感谢 Chris Maunder 的文章 "Creating Custom Controls",因为从头开始创建控件时必须经历相同的初步步骤,我在这里遵循了他的方法,因为这些步骤解释得非常清楚。我还从 norm.net 的文章 "Extended Use of CStatic Class - CLabel 1.6" 中借用并修改了用于渐变背景的代码。随着时间的推移,我阅读并从 The Code Project 上发布的许多贡献中学到了很多东西,因此我借此机会在此感谢大家,谢谢你们。

要完成什么

目标是一个具有单选功能的水平列表框,允许为每个项目设置默认和单独的颜色。边框颜色由用户定义,背景则使用颜色渐变绘制。滚动条将有四个按钮式的分区和一个中心显示:移至最左边的列,向左移动一列,显示列表框中的元素数量,向右移动一列,并移至最右边的列。

列表中只会绘制可见的项目,因此可以添加无限数量的项目(内存允许的情况下)。这将减少大量数据的显示时间(CListbox 在项目很多时可能会显得停滞),并且在此过程中将绕过 CListbox 在可添加项目数量方面的限制(尽管这个限制确实很大)。

开始编码。

分步过程

步骤 1 到 8 执行了使自定义控件启动并运行的基本步骤,仅此而已。编译后,应用程序将运行,但在自定义控件区域将看不到任何内容。

第 1 步。创建一个派生自 CWnd 的新类

  • 类类型:MFC 类
  • 类信息/名称:CCWinListBox
  • 类信息/基类:通用 CWnd

(所有其他保留为默认值。)

第 2 步。现在我们将注册我们打算创建的新窗口类。首先,在 *“CWinListBox.h”* 中添加以下行

#define C_CUSTOMLISTBOX_CLASSNAME _T("MFC_CCWinListBox")

第 3 步。然后,在 *“CWinListBox.h”* 中添加以下受保护方法的声明

BOOL RegisterWindowClass();

第 4 步。最后,在 *“CWinListBox.cpp”* 中添加 RegisterWindowClass 的实现(如下所示),并从构造函数中调用它。请注意,在此示例项目中,光标 IDC_CURSOR1 - 即“手”光标 - 必须作为资源添加。您可以将其替换为您喜欢的任何内容。或者,您可以将字段 wndcls.hCursor 设置为 NULL,然后将使用标准光标。

BOOL CCWinListBox::RegisterWindowClass()
{
    WNDCLASS  wndcls;
    HINSTANCE hInst = AfxGetInstanceHandle();

    if( !( ::GetClassInfo( hInst, C_CWINLISTBOX_CLASSNAME, 
                      &wndcls ) ) ) // Already registered?
    {
        wndcls.style            = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
        wndcls.lpfnWndProc      = ::DefWindowProc;
        wndcls.cbClsExtra       = wndcls.cbWndExtra = 0;
        wndcls.hInstance        = hInst;
        wndcls.hIcon            = NULL;
        wndcls.hCursor          = AfxGetApp()->LoadCursor( IDC_CURSOR1 );
        wndcls.hbrBackground    = (HBRUSH) (COLOR_3DFACE + 1);
        wndcls.lpszMenuName     = NULL;
        wndcls.lpszClassName    = C_CWINLISTBOX_CLASSNAME;

        if( !AfxRegisterClass( &wndcls ) )
        {
            AfxThrowResourceException();
            return FALSE;
        }
    }

    return TRUE;
}

...

CCWinListBox::CCWinListBox()
{
    RegisterWindowClass();    // Register custom control
}

第 5 步。完成此操作后,我们现在可以在将使用自定义控件的对话框类(即 *“CustomListBoxDlg.h”*)中包含我们的 CWin 派生类的头文件(*“CWinListBox.h”*)

#include "CWinListBox.h"

第 6 步。在 *“CustomListBoxDlg.h”*(或将托管控件的对话框)中添加一个受保护的成员变量

CCWinListBox m_CWindListBox;

第 7 步。我们已具备所有必要条件,可以通过资源编辑器将自定义控件放置到对话框中。因此,将其放置在对话框上,根据需要调整大小,然后右键单击灰色方块以选择“属性”。按如下方式修改字段

  • ID:IDC_CUSTOM1
  • 标题:(删除标题)
  • 类:MFC_CCWinListBox

(所有其他保留为默认值。)

第 8 步。在 *“CustomListBoxDlg.cpp”*(或相应的托管对话框)的 DoDataExchange 中添加以下行。如果您在之前的步骤中更改了 ID,请确保它与下面的 ID 一致

DDX_Control(pDX, IDC_CUSTOM1, m_CWindListBox);

第 9 步。代码已经可以编译并运行了。当然,由于还没有为 OnPaint 方法编写任何实现,所以自定义控件区域将不会显示任何内容。这只是一个到目前为止一切顺利的检查点。

步骤 10 到 13 将介绍绘制边框和背景的代码,并在此过程中实现允许托管对话框设置所用字体和颜色的方法。在这五步之后,自定义控件将在运行应用程序时可见。

第 10 步。添加公共方法以允许托管对话框设置控件将使用的颜色和字体(这些方法的实现主要涉及填充成员变量)。声明如下

virtual CCWinListBox& SetTextFont ( LONG nHeight, bool bBold, 
                          bool bItalic, CString csFaceName );

virtual CCWinListBox& SetBorderColor ( COLORREF crBorder1, 
                                        COLORREF crBorder2 );

virtual CCWinListBox& SetBkColor ( COLORREF crBkFrom, COLORREF crBkTo );

virtual CCWinListBox& SetDefaultTextColor ( COLORREF crText );

virtual CCWinListBox& SetSelectedColors ( COLORREF crSelText, 
                  COLORREF crSelTextBk,, COLORREF crSelBorder1, 
                  COLORREF crSelBorder2 );

第 11 步。要为自己选择一个最适合您尊贵肤色的颜色组合 - 一种无疑是由我们内心永不沉寂的疯子所构思的调色板 - 可能需要 Cray 超级计算机的高性能计算。无论内在的时尚恶魔如何运作,但是,为了初始化自定义列表框的外观,您只需从 *“CustomListBoxDlg.cpp”*(或相应的托管对话框)的 OnInitDialog 中传递首选的颜色和字体信息。我忘了我自己,我提议

m_CWindListBox
    .SetTextFont        ( 8, TRUE, FALSE, _T("Verdana") );
    .SetBorderColor     ( RGB(0,77,113), RGB(255,255,255 ) )
    .SetBkColor         ( RGB(183,235, 255 ), RGB(255,255,11 ) )
    .SetDefaultTextColor( RGB(0,77,113) )
    .SetSelectedColors  ( RGB(255,255,255 ), RGB(70,136,136), 
                               RGB(199,226,226), RGB(23,47,47) )

第 12 步。狂热的疯狂已经结束,我们准备开始绘图了。我们将离屏绘制以减少闪烁。首先,通过 ClassWizard 添加一个 WM_PAINT 的消息处理程序。完成之后,包含下面的实现;此时,只会绘制背景(此处为渐变)和边框。编译并运行以查看这个动物在生长过程中的样子。

void CCWinListBox::OnPaint() 
{
    CPaintDC dc(this); // device context for painting
    
    // Paint off-screen
    CDC*     pDCMem      = new CDC;
    CBitmap* pPrevBitmap = NULL;
    CBitmap  bmpCanvas;
    CRect    rArea;
    
    GetClientRect( rArea );
    
    pDCMem->CreateCompatibleDC( &dc );
    
    bmpCanvas.CreateCompatibleBitmap( &dc, rArea.Width(), rArea.Height() );
    
    pPrevBitmap = pDCMem->SelectObject( &bmpCanvas );
    
    // DRAWING BEGINS --------------------------------

    CRect rClient( rArea );

    // Leave room for the border
    rClient.DeflateRect( 1, 1, 1, 1 );
    
    // Draw gradient background
    DrawGradientBackground( pDCMem, rClient, m_crBkFrom, m_crBkTo );
    
    // Draw the border
    pDCMem->Draw3dRect( rArea, m_crBorder1, m_crBorder2 );

    // DRAWING ENDS --------------------------------

    // Copy from memory to the screen
    dc.BitBlt( 0, 0, rArea.Width(), rArea.Height(), pDCMem, 0, 0, SRCCOPY );

    pDCMem->SelectObject( pPrevBitmap );

    delete pDCMem;
}

第 13 步。您可能仍然会注意到一些闪烁。通过 ClassWizard 添加一个 WM_ERASEBKGND 的消息处理程序,并简单地返回 TRUE,正式通知盖茨先生,这个自定义控件不需要擦除背景。编译并运行,闪烁应该就消失了。

BOOL CCWinListBox::OnEraseBkgnd(CDC* pDC) 
{
    return TRUE;
}

步骤 14 到 20 将介绍绘制列表框项目代码,并在此过程中实现允许托管对话框添加、更改、插入和删除项目的方法。在这七步之后,如果从托管对话框添加项目,就可以在自定义控件区域内看到项目了。

第 14 步。确实,让我们把控件变成一个列表框。一个 CTypedPtrArray 将收集每个列表框条目的信息。在 CCWinListBox 类中创建受保护的类 CListBoxItem,并声明一个关于它的类型化指针数组成员变量。不要忘记包含头文件 *“afxtempl.h”* 以便能够使用数组模板

#include <afxtempl.h>

// Item class
class CListBoxItem : public CObject
{
public:
    CListBoxItem()
    {
        csLabel.Empty();
        rItem.SetRectEmpty();
        bSelected = FALSE;
    }
    
    virtual ~CListBoxItem()
    {
        csLabel.Empty();
    }

    CString  csLabel;
    COLORREF crFace;
    CRect    rItem;
    BOOL     bSelected;
};

// Listbox item storage
CTypedPtrArray < CObArray, CListBoxItem* > m_tpaItems;
int                                        m_iTotalItems;

第 15 步。在构造函数中将项目数初始化为零,并在析构函数中移除所有项目。

m_iTotalItems = 0;      // in the constructor

RemoveAll( FALSE );     // in the destructor

第 16 步。当然,我们需要声明并实现一个 RemoveAll 方法。它应该是公共的,以便也可以从托管对话框调用(在这种情况下,应该重新绘制控件)。它的实现是在移除数组中的所有指针之前删除每个 CListBoxItem 对象。不要忘记将总数 - m_iTotalItems - 重置为 0

void CCWinListBox::RemoveAll( BOOL bInvalidate /* = TRUE  */ )
{
    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
        delete m_tpaItems[ iIndex ];

    m_tpaItems.RemoveAll();
    m_iTotalItems = 0;

    if( bInvalidate )
        Invalidate();
}

第 17 步。既然我们有了一个用于存储列表框项目的结构,它已得到妥善初始化和销毁,我们就可以编写公共方法来添加、更改、插入和删除项目了。它们的实现应该是不言自明的

void CCWinListBox::AddString( CString csLabel )
{
    AddStringWithColor( csLabel, m_crText );
}

void CCWinListBox::AddStringWithColor( CString csLabel, COLORREF crFace )
{
    m_tpaItems.SetAtGrow( m_iTotalItems, new CListBoxItem() );

    m_tpaItems[ m_iTotalItems ]->csLabel = csLabel;
    m_tpaItems[ m_iTotalItems ]->crFace  = crFace;

    m_iTotalItems++;

    Invalidate();
}

void CCWinListBox::ChangeStringAt( int iIndex, CString csLabel )
{
    ChangeStringAndColorAt( iIndex, csLabel, m_tpaItems[ iIndex ]->crFace );
}

void CCWinListBox::ChangeColorAt( int iIndex, COLORREF crFace )
{
    ChangeStringAndColorAt( iIndex, m_tpaItems[ iIndex ]->csLabel, crFace );
}

void CCWinListBox::ChangeStringAndColorAt( int iIndex, 
                        CString csLabel, COLORREF crFace )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
    {
        m_tpaItems[ iIndex ]->csLabel   = csLabel;
        m_tpaItems[ iIndex ]->crFace    = crFace;

        Invalidate();
    }
}

void CCWinListBox::InsertString( int iIndex, CString csLabel )
{
    InsertStringWithColor( iIndex, csLabel, m_crText );
}

void CCWinListBox::InsertStringWithColor( int iIndex, 
                            CString csLabel, COLORREF crFace )
{
    // iIndex == m_ITotalItems is ok, will be appended in last position
    if( iIndex >= 0 && iIndex <= m_iTotalItems )
    {
       m_tpaItems.InsertAt( iIndex, new CListBoxItem() );

        m_tpaItems[ iIndex ]->csLabel = csLabel;
        m_tpaItems[ iIndex ]->crFace  = crFace;

        m_iTotalItems++;

        Invalidate();
    }
}

void CCWinListBox::RemoveAt( int iIndex )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
    {
        m_tpaItems.RemoveAt( iIndex );

        m_iTotalItems--;

        Invalidate();
    }
}

第 18 步。我们还可以提供公共方法来检索当前列表中项目的总数以及给定项目(通过其索引位置)的标签和颜色。

int CCWinListBox::GetCount()
{
    return m_iTotalItems;
}

CString CCWinListBox::GetStringAt( int iIndex )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
        return m_tpaItems[ iIndex ]->csLabel;
    else
        return "";
}

COLORREF CCWinListBox::GetColorAt( int iIndex )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
        return m_tpaItems[ iIndex ]->crFace;
    else
        return m_crText;    // m_crText acts as default
}

第 19 步。由于这将是一个水平列表框,我们需要决定列的宽度。因此,声明并实现一个合适的公共方法 SetColumnWidth。请注意,列宽可以很容易地变得可变,例如,每列的宽度取决于其中最大项目的大小。

// declaration
virtual CCWinListBox& SetColumnWidth( int iNumberOfCharacters, 
                                    BOOL bInvalidate = TRUE );

// implementation
CCWinListBox& CCWinListBox::SetColumnWidth( int iNumberOfCharacters, 
                                     BOOL bInvalidate /* = TRUE  */)
{
    CDC*  pDC        = GetDC();
    CFont *pPrevFont = pDC->SelectObject( &m_Font );

    TCHAR strWidestText[100];
    
    memset( strWidestText, 0, 100 );

    // Let's use a character likely to be wide
    memset( strWidestText, 'X', 
      ( iNumberOfCharacters < 100 )? iNumberOfCharacters:100 );
    
    // Calculate the lenght of the string under
    // this font plus 4 pixel padding
    m_iColumnWidth = ( pDC->GetTextExtent( strWidestText ) ).cx + 4;

    pDC->SelectObject( pPrevFont );
    ReleaseDC( pDC );

    if( bInvalidate )
        Invalidate();

    return *this;
}

第 20 步。我们现在准备绘制列表框内容。首先,有项目吗?如果有,则在确定要绘制的项目的开始位置时循环遍历数组。一旦项目的矩形超出视野,就停止绘制。下面的代码放在 OnPaint 方法的实现中,并放在 DrawGradientBackground 调用之后,在绘制边框之前。

// Are there any items to draw?
if( m_iTotalItems > 0 )
{
    CFont* pPrevFont = pDCMem->SelectObject( &m_Font );

    if( m_iItemHeight == - 1 )
        m_iItemHeight = 
        pDCMem->GetTextExtent( m_tpaItems[ 0 ]->csLabel ).cy + 4;

    // A default value of 10 characters
    // in case no column width was set
    if( m_iColumnWidth == -1 )
        SetColumnWidth( 10, FALSE );

    CRect rItem;

    rItem.left   = rClient.left + 2; // left padding
    rItem.top    = rClient.top;
    rItem.right  = rItem.left + m_iColumnWidth;
    rItem.bottom = rItem.top + m_iItemHeight;

    int       iPrevMode  = pDCMem->SetBkMode( TRANSPARENT );
    COLORREF  crPrevText = pDCMem->SetTextColor( m_crText );

    BOOL bStopDrawing = FALSE;

    // Loop through items and draw them
    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
    {
        // next column?
        if( rItem.bottom > rClient.bottom )
        {
            rItem.left   += m_iColumnWidth + 2;
            rItem.top    = rClient.top;
            rItem.right  = rItem.left + m_iColumnWidth;
            rItem.bottom = rItem.top + m_iItemHeight;

            if( rItem.right > rClient.right )      // Just prettier
                rItem.right = rClient.right - 3;

            // out of sight?
            if( rItem.left > rClient.right )
                bStopDrawing = TRUE;
        }

        if( !bStopDrawing )
        {
            // Is the item selected?
            if( m_tpaItems[ iIndex ]->bSelected )
            {
                pDCMem->FillSolidRect( rItem.left, rItem.top + 2,
                                       rItem.Width(), rItem.Height() - 4,
                                       m_crSelTextBk );
                pDCMem->Draw3dRect ( rItem.left, rItem.top + 2, 
                        rItem.Width(), rItem.Height() - 4,
                        m_crSelBorder1, m_crSelBorder2 );
                           
                pDCMem->SetTextColor( m_crSelText );
            }
            else
                pDCMem->SetTextColor( m_tpaItems[ iIndex ]->crFace );

            // Now the text
            pDCMem->DrawText( m_tpaItems[ iIndex ]->csLabel,
                              CRect( rItem.left + 2, rItem.top, 
                              rItem.right, rItem.bottom ),
                              DT_LEFT | DT_SINGLELINE | DT_VCENTER );

            // Record item rectangle
            m_tpaItems[ iIndex ]->rItem = rItem;
        }
        else
            m_tpaItems[ iIndex ]->rItem.SetRectEmpty();

        // Next item rectangle
        rItem.top    = rItem.bottom;
        rItem.bottom = rItem.top + m_iItemHeight;
    }
    
    pDCMem->SelectObject( pPrevFont );
    pDCMem->SetBkMode( iPrevMode );
    pDCMem->SetTextColor( crPrevText );
}

步骤 21 和 22 将介绍检测控件区域鼠标点击的代码。在这两步之后,将可以选中一个项目并看到其背景切换。

第 21 步。通过 ClassWizard 添加一个 WM_LBUTTONDOWN 消息处理程序将使我们能够检测用户是否通过单击鼠标选中了一个项目。该实现循环遍历项目数组,找到被单击的项目,并在过程中取消选中所有其他项目。

void CCWinListBox::OnLButtonDown(UINT nFlags, CPoint point) 
{
    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
      if( m_tpaItems[ iIndex ]->rItem.PtInRect( point ) )
        m_tpaItems[ iIndex ]->bSelected = !m_tpaItems[ iIndex ]->bSelected;
      else
        m_tpaItems[ iIndex ]->bSelected = FALSE;

    Invalidate();
    
    CWnd::OnLButtonDown(nFlags, point);
}

第 22 步。允许托管对话框了解当前选中了哪个项目(如果有)将是非常友善的。下面是一个公共方法的实现,该方法返回选中项目的索引,如果未选中任何项目,则返回 -1

int CCWinListBox::GetSelectedItem()
{
    int iSelectedIndex = -1;

    for( int iIndex = 0; iIndex < m_iTotalItems 
                         && iSelectedIndex == -1; iIndex++ )
        if( m_tpaItems[ iIndex ]->bSelected )
            iSelectedIndex = iIndex;

    return iSelectedIndex;
}

步骤 23 到 27 将介绍实现滚动条的代码。将对 OnPaintOnLButtonDown 方法的实现进行修改。在这五步之后,列表框的内容将响应鼠标点击而移动。

第 23 步。正如建议的,滚动条将有四个按钮式的分区和一个中心显示:移至最左边的列,向左移动一列,显示列表中当前项目的总数,向右移动一列,并移至最右边的列。

为了记住分区的确切位置以及它们是否被点击,声明了一个类型化指针数组,该数组基于 CScrollBarDiv 类(如下所示声明和实现)。该数组包含每个按钮的矩形位置及其相应的标签。如果被点击,相应的分区将在 OnPaint 方法中标记以进行绘制 - 并执行滚动操作。提供了标志 m_bCalculateSB_Div,以便每个分区的坐标只计算一次(在 OnPaint 方法中),并且在自定义控件由那些不那么好的可调整大小的对话框托管时也可能很有用(在这种情况下,将 m_bCalculateSB_Div 设置为 TRUE 将使滚动条分区动态重新计算和调整大小)。

// Scrollbar divisions
class CScrollBarDiv : public CObject
{
public:
    CScrollBarDiv()
    {
        csLabel.Empty();
        rItem.SetRectEmpty();
        bPressed = FALSE;
    }
    
    virtual ~CScrollBarDiv()
    {
        csLabel.Empty();
    }
    
    CString csLabel;
    CRect   rItem;
    BOOL    bPressed;
};

CTypedPtrArray<CObArray, CScrollBarDiv*> m_tpaSB;
BOOL                                     m_bCalculateSB_Div;

第 24 步OnLButtonDownOnLButtonUp 方法循环遍历滚动条数组,以选择和取消选择发生鼠标点击的分区(按钮)。中心区域(分区 #2)保留用于显示列表框中项目数量的计数,因此该分区不受鼠标点击的影响。下面显示了对 OnLButtonDown 的修改。对于 OnLButtonUp,通过 ClassWizard 为 WM_LBUTTONUP 添加一个消息处理程序,并用下面的代码填充它。此外,添加一个 WM_TIMER 的消息处理程序,以便在鼠标按钮保持按下状态时允许列表框继续滚动。

void CCWinListBox::OnLButtonDown(UINT nFlags, CPoint point) 
{
    if( point.y > m_tpaSB[ 0 ]->rItem.top )
    {
        for( int i = 0; i < 5; i++ )
            if( i != 2 && m_tpaSB[ i ]->rItem.PtInRect( point ) )
            {
                m_tpaSB[ i ]->bPressed = TRUE;

                if( i == 1 || i == 3 )
                    SetTimer( 1, 250, NULL );

                break;
           }
    }
    else
    {
        for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
            if( m_tpaItems[ iIndex ]->rItem.PtInRect( point ) )
                m_tpaItems[ iIndex ]->bSelected = 
                              !m_tpaItems[ iIndex ]->bSelected;
            else
                m_tpaItems[ iIndex ]->bSelected = FALSE;
    }

    Invalidate();
    
    CWnd::OnLButtonDown(nFlags, point);
}

void CCWinListBox::OnLButtonUp(UINT nFlags, CPoint point) 
{
    for( int i = 0; i < 5; i++ )
        if( m_tpaSB[ i ]->bPressed )
        {
            m_tpaSB[ i ]->bPressed = FALSE;

            Invalidate();

            if( i == 1 || i == 3 )
                KillTimer( 1 );

            break;
        }
    
    CWnd::OnLButtonUp(nFlags, point);
}

void CCWinListBox::OnTimer(UINT nIDEvent) 
{
    switch( nIDEvent )
    {
    case 1:
        Invalidate();
    break;
    }
    
    CWnd::OnTimer(nIDEvent);
}

第 25 步。为了简化 onPaint 方法中代码的外观和可读性,实现了两个辅助的受保护方法:CalculateSBDivisionsDrawSB_ShiftColumn。第一个简单地划分滚动条区域,计算并存储每个分区的坐标。第二个绘制每个分区的边框,切换颜色以显示点击/空闲切换。然而,最重要的是,它根据当前点击了哪个分区(如果有)来修改全局的 m_iDisplayColumn 属性。

void CCWinListBox::CalculateSBDivisions( CRect rSBRect )
{
    m_bCalculateSB_Div = FALSE;

    int iWidth = rSBRect.Width() / 5;
    int iAddum = rSBRect.Width() % 5; // extra pixels?

    for( int i = 0; i < 5; i++ )
    {
        m_tpaSB[ i ]->rItem = CRect( rSBRect.left + ( i * iWidth ), 
                              rSBRect.top, 1 + ( i + 1 ) * iWidth, 
                              rSBRect.bottom );

        switch ( i )
        {
            case 2:
                 // add extra pixels to the center division
                m_tpaSB[ 2 ]->rItem.right += iAddum;
                break;
            case 3:
                 // shift position to account for extra pixels
                m_tpaSB[ 3 ]->rItem.OffsetRect( iAddum, 0 );
                break;
            case 4:
                 // shift position to account for extra pixels
                m_tpaSB[ 4 ]->rItem.OffsetRect( iAddum, 0 );
                break;
        }
    }
}

void CCWinListBox::DrawSB_ShiftColumn( CDC *pDC, CRect rSBArea )
{
    pDC->FillSolidRect( rSBArea, m_crSB_Bk );

    for( int i = 0; i < 5; i++ ) // draw the scrollbar
    {
        CRect rItem( m_tpaSB[ i ]->rItem );

        // display or button section
        if( i == 2 )
        {
            pDC->Draw3dRect( rItem, m_crSB_Border1, m_crSB_Border1 );
            rItem.DeflateRect( 2, 2, 2, 2 );
            pDC->Draw3dRect( rItem, m_crSB_Border2, m_crSB_Border2 );
        }
        else
            pDC->Draw3dRect( rItem,
                 ( m_tpaSB[ i ]->bPressed )? m_crSB_Border1:m_crSB_Border2,
                 ( m_tpaSB[ i ]->bPressed )? m_crSB_Border2:m_crSB_Border1 );

        pDC->SetTextColor( m_crSB_Text );
        pDC->DrawText( m_tpaSB[ i ]->csLabel, rItem, 
                       DT_CENTER | DT_SINGLELINE | DT_VCENTER );

        // if a scrollbar division is currently clicked on,
        // modify the display column
        if( m_tpaSB[ i ]->bPressed )
            switch ( i )
            {
                case 0:
                    m_iDisplayColumn = 0;
                    break;
                case 1:
                    m_iDisplayColumn -= ( m_iDisplayColumn == 0 )? 0:1;

                    if( m_iDisplayColumn == 0 )
                        KillTimer( 1 ); // no need to continue repainting

                    break;
                case 3:
                    m_iDisplayColumn += 
                               ( m_iDisplayColumn == m_iMaxColumn )? 0:1;

                    if( m_iDisplayColumn == m_iMaxColumn )
                        KillTimer( 1 ); // no need to continue repainting

                    break;
                case 4:
                    m_iDisplayColumn = m_iMaxColumn;
                    break;
            }
    }
}

第 26 步。为了使其有用,滚动条的中心显示需要(实时)反映列表中项目数量的任何变化。可以在 DrawSB_ShiftColumn 中,或者在修改列表框中项目数量的方法中(即 AddStringWithColorInsertStringWithColorRemoveAtRemoveAll)包含代码。无论哪种方式,所需做的就是包含以下行

m_tpaSB[ 2 ]->csLabel.Format( _T("%d"), m_iTotalItems );

第 27 步。现在让我们修改 onPaint 方法中的代码。重要部分在 for 循环内:首先,字段 rItem.left 继续被重置为 rClient.left + 2(左边距),直到我们到达要显示的正确列;其次,在再次到达要显示的正确列之前,阻止绘制。

请注意,在绘制项目之前调用了 CalculateSBDivisionsDrawSB_ShiftColumn 方法。这很重要,因为 m_iDisplayColumn 属性在 DrawSB_ShiftColumn 中被修改 - 以适应鼠标点击引起的移动。现在的动物看起来是这样的。

// Are there any items to draw?
if( m_iTotalItems > 0 )
{
    CFont* pPrevFont = pDCMem->SelectObject( &m_Font );
    
    if( m_iItemHeight == - 1 )
        m_iItemHeight = 
           pDCMem->GetTextExtent( m_tpaItems[ 0 ]->csLabel ).cy + 4;

    // Deal with the scrollbar first
    CRect rSB( rClient );

    rSB.top        = rSB.bottom - m_iItemHeight;
    rClient.bottom -= m_iItemHeight + 2;
    // adjust client area so that items
    // are not drawn over the scrollbar area

    if( m_bCalculateSB_Div ) // need to calculate scrollbar divisions?
        CalculateSBDivisions( rSB );

    int      iPrevMode  = pDCMem->SetBkMode( TRANSPARENT );
    COLORREF crPrevText = pDCMem->SetTextColor( m_crText );

    DrawSB_ShiftColumn( pDCMem, rSB ); // adjust shift and draw scrollbar

    // A default value of 10 characters in case no column width was set
    if( m_iColumnWidth == -1 )
        SetColumnWidth( 10, FALSE );

    // Now draw the contents of the listbox
    CRect rItem;

    rItem.left   = rClient.left + 2; // left padding
    rItem.top    = rClient.top;
    rItem.right  = rItem.left + m_iColumnWidth;
    rItem.bottom = rItem.top + m_iItemHeight;

    int  iCurrentColumn = 0;
    BOOL bStopDrawing   = FALSE;

    // Loop through items and draw them
    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
    {
        // next column?
        if( rItem.bottom > rClient.bottom )
        {
            iCurrentColumn++;

            // Once the column to display has been reached
            // positions are advanced as usual
            // otherwise, reset the left position each time
            if( iCurrentColumn > m_iDisplayColumn )
                rItem.left += m_iColumnWidth + 2;
            else
                rItem.left = rClient.left + 2;
            
            rItem.top    = rClient.top;
            rItem.right  = rItem.left + m_iColumnWidth;
            rItem.bottom = rItem.top + m_iItemHeight;

            if( rItem.right > rClient.right ) // Just prettier
                rItem.right = rClient.right - 3;

            // out of sight?
            if( rItem.left > rClient.right )
                bStopDrawing = TRUE;
        }

        // Only draw the items that can be seen
        // otherwise set the items' rectangles to 0
        if( iCurrentColumn >= m_iDisplayColumn && !bStopDrawing )
        {
            // Is the item selected?
            if( m_tpaItems[ iIndex ]->bSelected )
            {
                pDCMem->FillSolidRect( rItem.left, rItem.top + 2,
                                       rItem.Width(), rItem.Height() - 4,
                                       m_crSelTextBk );
                pDCMem->Draw3dRect ( rItem.left, rItem.top + 2, 
                        rItem.Width(), rItem.Height() - 4,
                        m_crSelBorder1, m_crSelBorder2 );
                            
                pDCMem->SetTextColor( m_crSelText );
            }
            else
                pDCMem->SetTextColor( m_tpaItems[ iIndex ]->crFace );

            // Now the text
            pDCMem->DrawText( m_tpaItems[ iIndex ]->csLabel,
                              CRect( rItem.left + 2, rItem.top, 
                              rItem.right, rItem.bottom ),
                              DT_LEFT | DT_SINGLELINE | DT_VCENTER );

            // Record item rectangle
            m_tpaItems[ iIndex ]->rItem = rItem;
        }
        else
            m_tpaItems[ iIndex ]->rItem.SetRectEmpty();

        // Next item's rectangle
        rItem.top    = rItem.bottom;
        rItem.bottom = rItem.top + m_iItemHeight;
    }

    pDCMem->SelectObject( pPrevFont );
    pDCMem->SetBkMode( iPrevMode );
    pDCMem->SetTextColor( crPrevText );

    m_iMaxColumn = iCurrentColumn;
}

第 28 步。编译并运行应用程序。已经实现了目标功能,因此我们完成了。

这个自定义列表框比它有用看起来更漂亮。尽管如此,我希望您能从它的组装中有所收获,并能着手进行更有趣、更复杂的项目。

演变

我对这个很感兴趣。我唯一的目的是提供一个代码清晰、易于理解和遵循的示例。我相信有更好的方法来实现我在这里实现的功能。任何改进、简化或更好地解释代码的建议都欢迎。

© . All rights reserved.