概述
上一篇讲述了如何在wince上实现自绘按钮,这一篇将继续wince上的自绘控件--列表。效果如下图,主要是依据当时的项目需要进行定制,无法像之前的自绘按钮那样具有通用性,不过如果了解了基本流程,修改起来应该没问题的!
- 仅包含一列;
- 在每列的前后显示图标,并且图标支持透明色;
- 对于选中的行,加载并显示设定的底图;
- 隐藏列表原有的滚动条,并重新自绘;
- 未显示列表的头。
本自绘列表参考了codeproject上的文章"How to skin CListCtrl including scrollbars and column headers"(基于pc的实现)
主要步骤
- 从CListCtrl派生子类,如CImageListCtrl;
- 定制列表的样式,使之自绘,无列表头等;
- 列表的自绘
- 隐藏滚动条
- 自绘滚动条
定制列表样式
重载虚函数virtual void PreSubclassWindow(),实现如下:
void CImageListCtrl::PreSubclassWindow()
{
VERIFY(ModifyStyle(LVS_TYPEMASK | // this styles are removed
LVS_SHOWSELALWAYS |
LVS_EDITLABELS,
LVS_REPORT | // this styles are added
LVS_OWNERDRAWFIXED |
LVS_NOCOLUMNHEADER |
LVS_SINGLESEL,
LVS_NOSCROLL));
// Insert at least one column to the list control
VERIFY(InsertColumn(0, _T("Column"), LVCFMT_LEFT) == 0);
//SetColumnWidth(0, LVSCW_AUTOSIZE_USEHEADER);
CRect clientRect;
GetClientRect(&clientRect);
SetColumnWidth(0, clientRect.Width());
CListCtrl::PreSubclassWindow();
}
由于定制的列表只有一列,所以这当中调用函数
SetColumnWidth
设置列的宽度为客户区的宽度。
列表的自绘
列表的自绘直接明了,重载virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct),其中参数lpDrawItemStruct,包含了当前所要绘制的列表子项(即列表的一行)的相关信息,包括绘制句柄
lpDrawItemStruct->hDC,列表子项的客户区
lpDrawItemStruct->rcItem。
而列表子项的状态参数可以通过函数
GetItem(
LV_ITEM
& lvi)获得,一旦获取了上面这些参数,那么绘制当前的列表子项就是调用相关绘制函数了,以及如何调整图片,文字的显示位置。详细实现如下:
void CImageListCtrl::DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct )
{
ASSERT(::IsWindow(m_hWnd));
ASSERT(lpDrawItemStruct != 0);
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
CRect rcItem(lpDrawItemStruct->rcItem);
int nItem = lpDrawItemStruct->itemID;
LV_ITEM lvi;
lvi.mask = LVIF_STATE;
lvi.iItem = nItem;
lvi.iSubItem = 0;
lvi.stateMask = 0xFFFF; // get all state flags
GetItem(&lvi);
BOOL isSelected = lvi.state & LVIS_SELECTED;
CRect rcBounds;
GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
CString sLabel = GetItemText(nItem, 0);
SetBkColor(RGB(192,192,192));
PaintBk(pDC, rcItem);
HBITMAP hbmOldBmp = NULL;
CDC bitmapDC;
bitmapDC.CreateCompatibleDC(pDC);
if (isSelected && m_selImg != NULL)
{
hbmOldBmp = (HBITMAP)bitmapDC.SelectObject(m_selImg);
pDC->BitBlt(rcItem.left, rcItem.top, rcItem.Width(), rcItem.Height(), &bitmapDC,0,0,SRCCOPY);
bitmapDC.SelectObject(hbmOldBmp);
}
else
{
pDC->FillRect(rcItem,&CBrush(GetBkColor()));
}
if (m_ItemIcon != NULL)
{
hbmOldBmp = (HBITMAP)bitmapDC.SelectObject(m_ItemIcon);
TransparentImage(pDC->m_hDC,rcItem.left+29, rcItem.top+12, 32, 32,
bitmapDC.m_hDC,0, 0, 32, 32,RGB(0,0,0));
bitmapDC.SelectObject(hbmOldBmp);
}
CFont font;
font.CreatePointFont(200, _T("Times New Roman"));
pDC->SelectObject(&font);
pDC->SetTextColor(m_textColor);
pDC->DrawText(sLabel,rcItem, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
font.DeleteObject();
HBITMAP hbmNewBmp = NULL;
if (isSelected)
{
if (m_checkedImg != NULL)
{
hbmNewBmp = m_checkedImg;
}
}
else if(m_uncheckedImg)
{
hbmNewBmp = m_uncheckedImg;
}
if (hbmNewBmp != NULL)
{
hbmOldBmp = (HBITMAP)bitmapDC.SelectObject(hbmNewBmp);
TransparentImage(pDC->m_hDC,rcItem.right-50, rcItem.top+12, 32, 32,
bitmapDC.m_hDC,0, 0, 32, 32,RGB(0,0,0));
bitmapDC.SelectObject(hbmOldBmp);
}
}
此外,如果想绘制列表子项的边框,需要边界大小,可以通过如下方式得到:
CRect rcBounds;
GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
隐藏滚动条
wince平台上想要隐藏(禁用)滚动条,没有直接的办法。因为列表的窗口包括显示列表子项的区域和显示滚动条的部分(我的猜测),所以间接的办法是:
- 设置列表的窗口大小 = 原来的窗口大小 + 滚动条的大小
- 设置列表的裁剪区域 = 原来的窗口大小
通过如上的方式,滚动条就因为被裁剪区域裁剪掉,而不会显示了。具体实现如下:
void CImageListCtrl::HideScrollBars()
{
RECT ierect;
int cxvs, cyvs;
GetClientRect(&ierect); //Get client width and height
cxvs = GetSystemMetrics (SM_CXVSCROLL); //Get the system metrics - VERT
cyvs = GetSystemMetrics (SM_CYVSCROLL); //Get the system metrics - HORZ
//if(Which==SB_HORZ) cxvs=0; //Set VERT to zero when choosen HORZ
//if(Which==SB_VERT) cyvs=0; //Set HORZ to zero when choosen VERT
//Here we set the position of the window to the clientrect + the size of the scrollbars
SetWindowPos(NULL, ierect.left, ierect.top, ierect.right+cxvs, ierect.bottom+cyvs, SWP_NOMOVE | SWP_NOZORDER);
//Her we modify the rect so the right part is subbed from the rect.
ierect.bottom -= ierect.top;
ierect.right -= ierect.left;
//Just to be safe that the left/top corner is 0...
ierect.top = 0;
ierect.left = 0;
HRGN iehrgn = NULL; //This range is created base on which scrollbar that is going to be removed!
//The -2 is probably a border of some kind that we also need to remove. I could not find any good
//metrics that gave me an 2 as an answer. So insted we makes it static with -2.
iehrgn=CreateRectRgn (ierect.left, ierect.top, ierect.right-2, ierect.bottom-2);
//After the range has been made we add it...
SetWindowRgn (iehrgn, TRUE);
}
自绘滚动条
剩下的事情,就是添加自绘的滚动条,响应滚动事件并更新滚动条的位置。其中比较麻烦的就是给滚动条设置合适的位置,具体方法为:
- 得到列表的窗口位置并转换为屏幕坐标
- 考虑到滚动条的宽度、标题栏的宽度等参数,用上一步的坐标计算出滚动条的位置并设置。
这当中第二步的位置计算不是绝对的,根据绘制的需要和经验可以进行相应的调整。具体实现参见函数CImageListCtrl::PositionScrollBars()。
至于滚动条的绘制和滚动都封装在类CSkinHorizontalScrollbar和CSkinVerticleScrollbar中,并不难理解。
滚动条自动滚动的问题
前面的文章--
CListCtrl自动滚动的问题
曾经提到列表会自动滚动,你可以选择按照文章中的办法禁用掉,或者在函数void CImageListCtrl::OnTimer(UINT nIDEvent)中处理对应的nIDEvent事件,然后调用类CSkinHorizontalScrollbar和CSkinVerticleScrollbar的函数UpdateThumbPosition()来更新滚动条的位置。
用例
在对话框类中定义成员变量CImageListCtrl m_imageListCtrl,并通过DDX_Control(pDX, IDC_LIST2, m_imageListCtrl)将控件与变量绑定,然后在对话框类的初始化函数中
m_imageListCtrl.LoadVScrollImages(IDB_VERTICLE_SCROLLBAR_BOTTOM,IDB_VERTICLE_SCROLLBAR_DOWNARROW,IDB_VERTICLE_SCROLLBAR_SPAN,
IDB_VERTICLE_SCROLLBAR_THUMB,IDB_VERTICLE_SCROLLBAR_TOP,IDB_VERTICLE_SCROLLBAR_UPARROW);
m_imageListCtrl.LoadHScrollImages(IDB_HORIZONTAL_SCROLLBAR_LEFTARROW,IDB_HORIZONTAL_SCROLLBAR_RIGHTARROW,
IDB_HORIZONTAL_SCROLLBAR_SPAN,IDB_HORIZONTAL_SCROLLBAR_THUMB);
m_imageListCtrl.InitCtrl();
//m_imageListCtrl.SetImageList( &m_imageList, LVSIL_SMALL );
m_imageListCtrl.SetItemCheckedImg(IDB_LISTCTRL_CHECKED_BITMAP);
m_imageListCtrl.SetItemSelectedImg(IDB_LISTCTRL_ITEM_SEL_BITMAP);
m_imageListCtrl.SetItemIcon(IDB_LISTCTRL_ICON_BITMAP);
//m_imageListCtrl.SetItemUnCheckedImg(IDB_LISTCTRL_UNCHECK_BITMAP);
for (int n = 0; n < 8; n++)
{
CString str;
str.Format(_T("Item %d"), n);
m_imageListCtrl.InsertItem(n, str);
}