原文链接
第1步: 使用自绘制列表控件
自定义的列表控件必须是自绘制的,因此需要在资源编辑器中设置LVS_OWNERDRAWFIXED标志,而且还必须在自定义的控件类中实现DrawItem函数。
第2步: 增加成员变量
当每一项都需要重绘的时候去重新加载位图或重新创建逻辑调色板的话,效率不高。因此我们增加成员变量来保存位图,逻辑调色板以及位图的尺寸信息。
protected
:
CPalette m_pal;
CBitmap m_bitmap;
int
m_cxBitmap, b_cyBitmap;
第3步: 增加成员函数来设置背景图片
SetBkImage函数首先做的是,如果位图和调色板已经被创建的话,就删除位图和调色板GDI对象(说明不是第一次调用)。
if
( m_bitmap.m_hObject
!=
NULL )
m_bitmap.DeleteObject();
if
( m_pal.m_hObject
!=
NULL )
m_pal.DeleteObject();
然后加载位图并且将位图连接到CBitmap对象上。在这里我们使用全局的::LoadImage()而不是CBitmap::LoadBitmap(),原因是我们需要访问到位图的DIBSECTION,而之所以需要DIBSECTION是因为我们要创建一个与位图使用的颜色相匹配的逻辑调色板。为什么要调色板?呵呵,如果你不建立并使用一个逻辑调色板的话,那么图片在256色的显示器上就会显得很阴暗。此外,为了后面的使用,我们也保存了位图的尺寸信息。
HBITMAP hBmp
=
(HBITMAP)::LoadImage( AfxGetInstanceHandle(),
lpszResourceName, IMAGE_BITMAP,
0
,
0
, LR_CREATEDIBSECTION );
if
( hBmp
==
NULL )
return
FALSE;
m_bitmap.Attach( hBmp );
BITMAP bm;
m_bitmap.GetBitmap(
&
bm );
m_cxBitmap
=
bm.bmWidth;
m_cyBitmap
=
bm.bmHeight;
一旦我们有了位图集,我们就开始创建逻辑调色板。我们通过调用CBitmap::GetObject()函数来获得对DIBSECTION的访问,从而得到位图所使用的颜色数目。
//
Create a logical palette for the bitmap
DIBSECTION ds;
BITMAPINFOHEADER
&
bmInfo
=
ds.dsBmih;
m_bitmap.GetObject(
sizeof
(ds),
&
ds );
有的时候DIBSECTION 中的BITMAPINFOHEADER字段并没有指明它所使用的颜色数目,在这种情况下我们可以从它每个像素使用的位数来推断它的颜色数目,比如说,8位可以代表256个不同值,因此也就代表了256种颜色。类似的,16位就表明有64K种颜色.
颜色数目多于256的位图并没有颜色表。这种情况下我们简单地创建一个与设备环境兼容的halftone 调色板。一个halftone调色板就是一个包含各种不同颜色的一个样本集。这并不是最好的,但却是最简单的方法。
如果位图的颜色数目少于或等于256,我们就创建调色板。我们为位图分配足够的空间来保存它的颜色表,并且调用函数:: GetDIBColorTable来从位图中获得这个颜色表。我们还分配足够的内存来创建逻辑调色板,并且从位图的颜色表中拷贝所有的颜色实体。
在创建完调色板对象后,我们提前释放掉分配的内存块,并且让窗口无效,从而窗口可以使用新的图片来重画自己。
//
Create a halftone palette if colors > 256.
CClientDC dc(NULL);
//
Desktop DC
if
( nColors
>
256
)
m_pal.CreateHalftonePalette(
&
dc );
else
{
//
Create the palette
RGBQUAD
*
pRGB
=
new
RGBQUAD[nColors];
CDC memDC;
memDC.CreateCompatibleDC(
&
dc);
memDC.SelectObject(
&
m_bitmap );
::GetDIBColorTable( memDC,
0
, nColors, pRGB );
UINT nSize
=
sizeof
(LOGPALETTE)
+
(
sizeof
(PALETTEENTRY)
*
nColors);
LOGPALETTE
*
pLP
=
(LOGPALETTE
*
)
new
BYTE[nSize];
pLP
->
palVersion
=
0x300
;
pLP
->
palNumEntries
=
nColors;
for
(
int
i
=
0
; i
<
nColors; i
++
)
{
pLP
->
palPalEntry[i].peRed
=
pRGB[i].rgbRed;
pLP
->
palPalEntry[i].peGreen
=
pRGB[i].rgbGreen;
pLP
->
palPalEntry[i].peBlue
=
pRGB[i].rgbBlue;
pLP
->
palPalEntry[i].peFlags
=
0
;
}
m_pal.CreatePalette( pLP );
delete[] pLP;
delete[] pRGB;
}
Invalidate();
第4步: 修改DrawItem() 来处理图片
DrawItem()函数是用于绘制列表中的每一项的。我们在这个函数中绘制背景图片。由于背景图片应该完全地覆盖掉整个客户区,因此我们对剪切区域进行调整,使得它延展到客户区的右边缘。同样地,如果画列表的最后一项,剪切区域就应该扩展到客户区的下边缘。
然后是选择调色板(这只在设备支持调色板时有意义)。
背景图片是以一种平铺地方式进行绘制的,绘制图片时总是使用列表的第一项的左上角来作为参照。这就使得图片产生这样的效果:随着列表控件的内容的变化而滚动。
对原来的DrawItem()函数的另一个改变是只有当背景图片没有绘制时才绘制未被选中的项的背景
//
Draw bitmap in the background if one has been set
if
( m_bitmap.m_hObject
!=
NULL )
{
CDC tempDC;
tempDC.CreateCompatibleDC(pDC);
tempDC.SelectObject(
&
m_bitmap );
GetClientRect(
&
rcClient);
CRgn rgnBitmap;
CRect rcTmpBmp( rcItem );
rcTmpBmp.right
=
rcClient.right;
//
We also need to check whether it is the last item
//
The update region has to be extended to the bottom if it is
if
( nItem
==
GetItemCount()
-
1
)
rcTmpBmp.bottom
=
rcClient.bottom;
rgnBitmap.CreateRectRgnIndirect(
&
rcTmpBmp);
pDC
->
SelectClipRgn(
&
rgnBitmap);
rgnBitmap.DeleteObject();
if
( pDC
->
GetDeviceCaps(RASTERCAPS)
&
RC_PALETTE
&&
m_pal.m_hObject
!=
NULL )
{
pDC
->
SelectPalette(
&
m_pal, FALSE );
pDC
->
RealizePalette();
}
CRect rcFirstItem;
GetItemRect(
0
, rcFirstItem, LVIR_BOUNDS);
for
(
int
i
=
rcFirstItem.left; i
<
rcClient.right; i
+=
m_cxBitmap )
for
(
int
j
=
rcFirstItem.top; j
<
rcClient.bottom; j
+=
m_cyBitmap )
pDC
->
BitBlt( i, j, m_cxBitmap, m_cyBitmap,
&
tempDC,
0
,
0
, SRCCOPY );
}
//
Draw the background color
if
( bHighlight )
{
pDC
->
SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
pDC
->
SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
pDC
->
FillRect(rcHighlight,
&
CBrush(::GetSysColor(COLOR_HIGHLIGHT)));
}
else
if
( m_bitmap.m_hObject
==
NULL )
pDC
->
FillRect(rcHighlight,
&
CBrush(::GetSysColor(COLOR_WINDOW)));
第5步: 为WM_ERASEBKGND增加处理
当一张图片用作背景时,由于图片会画在背景上面,所以擦除背景并没有什么意义。擦除背景只会导致屏幕的闪烁,我们对WM_ERASEBKGND处理时,当位图对象合法时就返回true;
BOOL CMyListCtrl::OnEraseBkgnd(CDC
*
pDC)
{
if
( m_bitmap.m_hObject
!=
NULL )
return
TRUE;
return
CListCtrl::OnEraseBkgnd(pDC);
}
第6步: 重写 OnNotify() 并处理列宽变化
当一列被resize后,只有新扩展出来的地方需要重新绘制,这就在背景图片上产生一个不好的效果。因此,当任何一列被resize后,我们应该在OnNotify函数中使控件的右边无效。
BOOL CMyListCtrl::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT
*
pResult)
{
HD_NOTIFY
*
pHDN
=
(HD_NOTIFY
*
)lParam;
//
This code is for using bitmap in the background
//
Invalidate the right side of the control when a column is resized
if
(pHDN
->
hdr.code
==
HDN_ITEMCHANGINGW
||
pHDN
->
hdr.code
==
HDN_ITEMCHANGINGA)
{
if
( m_bitmap.m_hObject
!=
NULL )
{
CRect rcClient;
GetClientRect(
&
rcClient );
DWORD dwPos
=
GetMessagePos();
CPoint pt( LOWORD(dwPos), HIWORD(dwPos) );
ScreenToClient(
&
pt );
rcClient.left
=
pt.x;
InvalidateRect(
&
rcClient );
}
}
return
CListCtrl::OnNotify(wParam, lParam, pResult);
}
第7步:处理WM_QUERYNEWPALETTE & WM_PALETTECHANGED
当一个窗口准备开始接收输入焦点的时候,WM_QUERYNEWPALETTE消息就发送给它。这给窗口一个实现其逻辑调色板的机会,这样它就可以以最佳的形式展现自己。当系统调色板被改变时WM_PALETTECHANGED就被发送给窗口。如果我们不处理这些消息,而恰好另一个应用程序改变了系统调色板,那么我们的背景图片就会变得很难看。不幸地是,这两个消息都是发给顶层的窗口的。
OnQueryNewPalette首先检查它是否需要重新选择调色板。一旦它实现了逻辑调色板,它就让窗口无效。如果列表控件自己负责处理消息,那么就返回,不做进一步处理,否则OnPaletteChanged函数调用OnQueryNewPalette来实现调色板。
第8步:从顶层窗口转发调色板消息
void
CListViewDlg::OnPaletteChanged(CWnd
*
pFocusWnd)
{
CDialog::OnPaletteChanged(pFocusWnd);
m_listctrl.SendMessage( WM_PALETTECHANGED, (WPARAM)pFocusWnd
->
m_hWnd );
}
BOOL CListViewDlg::OnQueryNewPalette()
{
CDialog::OnQueryNewPalette();
return
m_listctrl.SendMessage( WM_QUERYNEWPALETTE );
}