首页 > 其他分享 >Neat Stuff to Do in List Controls Using Custom Draw

Neat Stuff to Do in List Controls Using Custom Draw

时间:2023-01-27 22:34:14浏览次数:47  
标签:control Do Draw Windows List draw item CUSTOMDRAW stage


Introduction

Version 4.70 of the common controls introduced a feature called custom draw. The name gives a vague hint about what it's about and the MSDN docs give some long-winded explanations and examples, but there's no one place that tells you what you really want to know. That is, namely, what's it good for. Custom draw can be thought of as a light-weight, easier-to-use version of owner draw. The ease of use comes from the fact that there is only one message to handle (​​NM_CUSTOMDRAW​​) and you can get Windows to do some of the work for you, so you don't have to go through all the grunt work that's part of owner-drawing.

This article will focus on using custom draw with a list view control, partly because I've done a few custom-drawn list controls in my own work and I'm familiar with the process; but also because it's possible to do some neat effects with very little code. Custom draw code can even replace several of the older list view articles on The Code Project!

The code for this article was written on Windows 98 with Microsoft Visual C++ 6 SP2, with version 5.0 of the common controls DLL. I have also tested it in Unicode on NT 4. The system running the code needs to have at least version 4.71 of the common controls installed. However, since this comes with IE 4 (which itself tags along with VC6), that fact shouldn't be a problem.

The Basics of Custom Draw

I will try my best to summarize the custom draw process here, without simply restating the documentation. For these examples, assume that you have a list control in a dialog and the list is in report view mode with multiple columns.

Hooking Up a Custom Draw Message Map Entry

Custom draw is a process similar to using callbacks. Windows notifies your program, via a notification message, at certain points in the process of drawing the list control. You can choose to ignore the notifications altogether (in which case you'll see the standard list control), process some part of the drawing yourself (for simple effects) or even draw the control yourself (just as when owner-drawing a control). The real selling point is that you can choose to respond to only some of the notifications if you so wish. That way, you only have to paint the parts you need to and Windows will do the rest.

Let's say that you want to add custom draw to the existing list control to give it some flare. Assuming you have the right common controls DLL on the system, Windows is already sending ​​NM_CUSTOMDRAW​​ messages your way; you just have to add a handler for the message to start using custom draw. The handler will look like this:


ON_NOTIFY ( NM_CUSTOMDRAW, IDC_MY_LIST, OnCustomdrawMyList )


...and the prototype looks like this:


afx_msg void OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult );


This tells MFC that you want to handle a ​​WM_NOTIFY​​​ message sent from your list control (whose ID is ​​IDC_MY_LIST​​​) when the notification code is ​​NM_CUSTOMDRAW​​​. The handler is a function called ​​OnCustomdrawMyList​​​. If you have a CListCtrl-derived class that you want to add custom draw to, you can use ​​ON_NOTIFY_REFLECT​​ instead:


ON_NOTIFY_REFLECT ( NM_CUSTOMDRAW, OnCustomdraw )


The message handler has the same prototype as above, but goes in your derived class instead.

Custom Draw Stages

Custom draw breaks the drawing process into two parts: erasing and painting. Windows may send ​​NM_CUSTOMDRAW​​ messages at the beginning and end of each part. So in total that's four messages. However, your application may actually receive as few as one or many more than four messages, depending on what you tell Windows you want. The times when a notification may be sent are called "draw stages." You need to have a good grasp of this concept, since it's used throughout custom draw. So, to summarize that rather dry paragraph, you may be notified at the following times (or stages):

  • Before an item is painted
  • After an item is painted
  • Before an item is erased
  • After an item is erased

Not all of those are equally useful and, in practice, I have not yet needed to handle more than one stage. In fact, while doing some experimenting in the process of writing this article, I was unable to get Windows to send either the pre-erase or post-erase messages! So don't let this section intimidate you.

Responding to NM_CUSTOMDRAW Messages

The value you return from your custom draw handler is a vitally important piece of information, as it tells Windows how much of the drawing process you have done and, indirectly, how much you want Windows itself to do. There are five responses you can send from your custom draw handler:

  1. I don't want to do anything now; Windows should paint the control or item itself as if there were no custom draw handler.
  2. I changed the font being used by the control; Windows must recalculate the ​​RECT​​ of the item being drawn.
  3. I painted the entire control or item; Windows should not do anything more with the control or item.
  4. I want to receive additional ​​NM_CUSTOMDRAW​​ messages during the draw stages of each item in the list.
  5. I want to receive additional ​​NM_CUSTOMDRAW​​ messages during the draw stages of each sub-item in the row currently being drawn.

Notice that the phrase "control or item" appears often. Remember that I said you may receive more than four ​​NM_CUSTOMDRAW​​​ messages? This is where that happens. The first ​​NM_CUSTOMDRAW​​ you receive applies to the entire control. If you return response 4 above (requesting notifications on a per-item basis), then you will receive messages as each item (row) goes through its drawing stages. And if you then return response 5, you'll get even more messages as each sub-item (column) goes through its drawing stages.

In report-mode list controls, you can use any of those responses, depending on what kind of effect you want to achieve. I'll present some examples of how to respond to ​​NM_CUSTOMDRAW​​ messages a little later.

Information Provided by NM_CUSTOMDRAW Messages

The ​​NM_CUSTOMDRAW​​​ message passes your handler a pointer to an ​​NMLVCUSTOMDRAW​​ struct, which contains the following information:

  • The window handle of the control
  • The ID of the control
  • The draw stage the control is currently in
  • The handle of a device context you should use if you perform any drawing
  • The ​​RECT​​ of the control, item, or sub-item being drawn
  • The item number (index) of the item being drawn
  • The sub-item number (index) of the sub-item being drawn
  • Flags indicating the state of the item being drawn (selected, grayed, etc.)
  • The ​​LPARAM​​​ data of the item being drawn, as set by ​​CListCtrl::SetItemData​

Any of those items may be important depending on the effect you're going for, but you'll always use the draw stage and usually the device context. The item indexes and ​​LPARAM​​ are often very useful as well.

A Simple Example

OK, time to actually see some code after all the boring details. The first example will be pretty simple, and will just change the color of the text in the control, rotating between red, green, and blue. This involves four steps:

  1. Handling ​​NM_CUSTOMDRAW​​ during the pre-paint stage for the control
  2. Telling Windows we want to get ​​NM_CUSTOMDRAW​​ messages for each item
  3. Handling the subsequent ​​NM_CUSTOMDRAW​​ messages sent for each item.
  4. Setting the text color for each item.

Here's the handler:


Neat Stuff to Do in List Controls Using Custom Draw_windows Collapse


void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );

// Take the default processing unless we
// set this to something else below.
*pResult = CDRF_DODEFAULT;

// First thing - check the draw stage. If it's the control's prepaint
// stage, then tell Windows we want messages for every item.

if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
// This is the prepaint stage for an item. Here's where we set the
// item's text color. Our return value will tell Windows to draw the
// item itself, but it will use the new color we set here.
// We'll cycle the colors through red, green, and light blue.

COLORREF crText;

if ( (pLVCD->nmcd.dwItemSpec % 3) == 0 )
crText = RGB(255,0,0);
else if ( (pLVCD->nmcd.dwItemSpec % 3) == 1 )
crText = RGB(0,255,0);
else
crText = RGB(128,128,255);

// Store the color back in the NMLVCUSTOMDRAW struct.
pLVCD->clrText = crText;

// Tell Windows to paint the control itself.
*pResult = CDRF_DODEFAULT;
}
}


The result of this code is shown below. See how each row has the color we told Windows to use? Cool, and all that with just a couple of if statements!


One thing to keep in mind is you must always check the draw stage before doing anything else, because your handler will receive many messages, and the draw stage determines what actions your code takes.

A Little Less Simple Example

The next sample shows how to handle custom draw for sub-items (that is, the columns). Our handler will set the text and cell background colors, but it won't be much more complex than the previous one; there's just one additional if block. The steps involved when dealing with sub-items are:

  1. Handling ​​NM_CUSTOMDRAW​​ during the pre-paint stage for the control
  2. Telling Windows we want to get ​​NM_CUSTOMDRAW​​ messages for each item
  3. When one of those messages comes in, tell Windows we want to get ​​NM_CUSTOMDRAW​​ messages during each sub-item's pre-paint stage.
  4. Set the text and background colors each time a subsequent message arrives for a sub-item.

Note that we will get an ​​NM_CUSTOMDRAW​​ message for each item as a whole, and another one for sub-item 0 (the first column). Here's the code:


Neat Stuff to Do in List Controls Using Custom Draw_windows Collapse


void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );

// Take the default processing unless
// we set this to something else below.
*pResult = CDRF_DODEFAULT;

// First thing - check the draw stage. If it's the control's prepaint
// stage, then tell Windows we want messages for every item.

if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
// This is the notification message for an item. We'll request
// notifications before each subitem's prepaint stage.

*pResult = CDRF_NOTIFYSUBITEMDRAW;
}
else if ( (CDDS_ITEMPREPAINT | CDDS_SUBITEM) == pLVCD->nmcd.dwDrawStage )
{
// This is the prepaint stage for a subitem. Here's where we set the
// item's text and background colors. Our return value will tell
// Windows to draw the subitem itself, but it will use the new colors
// we set here.
// The text color will cycle through red, green, and light blue.
// The background color will be light blue for column 0, red for
// column 1, and black for column 2.

COLORREF crText, crBkgnd;

if ( 0 == pLVCD->iSubItem )
{
crText = RGB(255,0,0);
crBkgnd = RGB(128,128,255);
}
else if ( 1 == pLVCD->iSubItem )
{
crText = RGB(0,255,0);
crBkgnd = RGB(255,0,0);
}
else
{
crText = RGB(128,128,255);
crBkgnd = RGB(0,0,0);
}

// Store the colors back in the NMLVCUSTOMDRAW struct.
pLVCD->clrText = crText;
pLVCD->clrTextBk = crBkgnd;

// Tell Windows to paint the control itself.
*pResult = CDRF_DODEFAULT;
}
}


The resulting list is shown below:


A couple of things to note here:

  • The ​​clrTextBk​​ color is painted in just the column. The area to the right of the last column and below the last row still gets the control's background color.
  • While I was reviewing the docs, I read the page titled "​​NM_CUSTOMDRAW​​​ (list view)" and it says that you can return ​​CDRF_NOTIFYSUBITEMDRAW​​​ from the very first custom draw message, without having to handle the ​​CDDS_ITEMPREPAINT​​​ draw stage. I tested this, however, and it did not work. You do in fact need to handle the ​​CDDS_ITEMPREPAINT​​ stage.

Handling the Post-paint Draw Stage

So far, the examples have handled the pre-print stage, to change the appearance of the list items when Windows draws them. However, during the pre-paint stage, your options are limited to just changing the color or appearance of the text. If you want to change how the icon is drawn, you can either draw the entire item during the pre-paint stage (overkill), or do custom drawing during the post-paint stage. When you do custom drawing during the post-paint stage, your custom draw handler is called after Windows has drawn the entire item or sub-item, and you can do any additional drawing you want to.

In this example, I will create a list control where the icons of the selected items do not change color. The steps involved are:

  1. Handling ​​NM_CUSTOMDRAW​​ during the pre-paint stage for the control
  2. Telling Windows we want to get ​​NM_CUSTOMDRAW​​ messages for each item
  3. When one of those messages comes in, tell Windows we want to get an ​​NM_CUSTOMDRAW​​ message during the item's post-paint stage.
  4. Redraw the icons when necessary each time a subsequent message arrives for an item.

Neat Stuff to Do in List Controls Using Custom Draw_windows Collapse


void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );

*pResult = 0;

// If this is the beginning of the control's paint cycle, request
// notifications for each item.

if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
// This is the pre-paint stage for an item. We need to make another
// request to be notified during the post-paint stage.

*pResult = CDRF_NOTIFYPOSTPAINT;
}
else if ( CDDS_ITEMPOSTPAINT == pLVCD->nmcd.dwDrawStage )
{
// If this item is selected, re-draw the icon in its normal
// color (not blended with the highlight color).

LVITEM rItem;
int nItem = static_cast<int>( pLVCD->nmcd.dwItemSpec );

// Get the image index and state of this item. Note that we need to
// check the selected state manually. The docs _say_ that the
// item's state is in pLVCD->nmcd.uItemState, but during my testing
// it was always equal to 0x0201, which doesn't make sense, since
// the max CDIS_* constant in commctrl.h is 0x0100.

ZeroMemory ( &rItem, sizeof(LVITEM) );
rItem.mask = LVIF_IMAGE | LVIF_STATE;
rItem.iItem = nItem;
rItem.stateMask = LVIS_SELECTED;
m_list.GetItem ( &rItem );

// If this item is selected, redraw the icon with its normal colors.
if ( rItem.state & LVIS_SELECTED )
{
CDC* pDC = CDC::FromHandle ( pLVCD->nmcd.hdc );
CRect rcIcon;

// Get the rect that holds the item's icon.
m_list.GetItemRect ( nItem, &rcIcon, LVIR_ICON );

// Draw the icon.
m_imglist.Draw ( pDC, rItem.iImage, rcIcon.TopLeft(),
ILD_TRANSPARENT );

*pResult = CDRF_SKIPDEFAULT;
}
}
}


Again, custom draw lets us get away with doing as little work as possible. What this example does is let Windows do all the painting for us, then overwrites the icon for each item that is selected. That way, all the user sees is the icon that we draw. The resulting list is pictured below. Notice that the icon of Stan looks the same as those of the unselected items.


The only drawback to drawing in this way is that sometimes you can see a bit of flicker, since the icons are drawn twice in quick succession.

Using Custom Draw as a Replacement for Owner Draw

Another neat thing you can do with custom draw is use it to do the same thing you would do with owner draw. The difference is that IMO the code is much easier to write and understand when using custom draw. The other advantage is that if you only need to owner-draw certain rows, you can do that and have Windows draw the others. When doing a real owner-drawn control, you have to do everything, even if you don't need any "special effects" for some rows.

When you owner-draw using custom draw, you handle the ​​NM_CUSTOMDRAW​​​ message sent during an item's pre-paint stage, do all the drawing, and return ​​CDRF_SKIPDEFAULT​​​ from your handler. This is different than what we've done so far. ​​CDRF_SKIPDEFAULT​​ tells Windows to not do any painting in the row because you've already done it all.

I won't include the code for this example here in the article, since it's a bit long, but you can step through the code in the debugger and see what's happening. If you arrange your windows so you can see both the demo app's dialog and the code at the same time, you'll see the painting happening step-by-step. The list control is pretty simple, with just one column and no header control. The list looks like this:


Other Things You Can (Supposedly) Do

With a little imagination, you can create some rather neat effects using custom draw. In a recent project I did at work, I wrote a list control that looks like this:


I won't mention the product by name, since I don't want this to turn into an advertisement, but you can probably figure it out. :) Notice how the list control items look normal if the text fits on one line, but the text wraps to two lines if necessary. This way, all of the text is visible, and the user doesn't have to keep scrolling back and forth to read all of the text. I achieved this effect by handling the pre-paint stage and doing all the drawing myself.

As for why the title of this section says "supposedly": As I mentioned earlier, the docs state that you can do custom draw processing during the pre-erase and post-erase draw stages. I had never written code that acted during those stages before, so in the process of writing this article, I planned on doing an example that painted a pattern behind the list items after they were erased. However, I was unable to get Windows to send ​​NM_CUSTOMDRAW​​​ messages during either the pre-erase or post-erase stages! I tried several things in my custom draw handler, and experimented a bit, but in the end I had to give up. I am suspicious of the documentation on this one, since on the page titled "​​NM_CUSTOMDRAW​​​ (list view)", it lists a return code (​​CDRF_NOTIFYITEMERASE​​) which does not even exist in the commctrl.h header file! When such a vital piece of information is wrong, I tend to view the surrounding documentation as being of questionable accuracy.

In any event, if you did handle the pre-erase or post-erase stage, you would also need to handle the pre-paint stage. Otherwise, the default Windows behavior would wipe out the drawing you did during the erase stage. Given that fact, I couldn't come up with anything that would require you to handle one of the erase stages; any special effects could be accomplished in the paint stages just as easily.

Demo Project

标签:control,Do,Draw,Windows,List,draw,item,CUSTOMDRAW,stage
From: https://blog.51cto.com/u_15942605/6024350

相关文章

  • GetWindowRect与GetClientRect
    先调用GetWindowRect后再调用ScreenToClient,这个时候得到的rect和直接使用GetClientRect得到的值是相等的。有时候需要获得窗口矩形的大小和客户区矩......
  • 跨平台Docker篇
    前言总结自老张的哲学b站视频【10分钟学会Blog.Core(ASP.NETCORE)基本完结】https://www.bilibili.com/video/BV1vC4y1p7Za?vd_source=3995da6daa798de472bb2ad50abb0a97[......
  • [转] python-docx
    https://python-docx.readthedocs.io/en/latest/ https://gitcode.net/mirrors/python-openxml/python-docx/-/tree/master/docx python-docxReleasev0.8.11(Inst......
  • Windows CE 编程的十点忠告
    [文章导读] 本文是根据作者在WindowsCE上编写Raima数据管理器的经验总结出来的,值得CE开发人员借鉴[正文]最近两周我们花了大部分时间将已有的应用程序移植到Microsoft......
  • Dotnet程序的内存与垃圾回收
    都知道程序运行时的变量是放在内存里的,从我们学c++就知道有析构函数 malloc,还有学c#的时候天天什么垃圾回收GC像背八股文样的,但是这个内存究竟能分配多少没有人想过......
  • Dotnet程序的内存与垃圾回收
    都知道程序运行时的变量是放在内存里的,从我们学c++就知道有析构函数 malloc,还有学c#的时候天天什么垃圾回收GC像背八股文样的,但是这个内存究竟能分配多少没有人想过......
  • Mongo集群安装-Docker环境
    安装简介本文主要介绍了在Docker环境下如何安装Mongo集群的过程,Mongo集群采用单分片三副本模式进行部署。服务器:bigdata1、bigdata2、bigdata3部署服务:config-server、shard......
  • docker运行不同版本activemq相关问题
    WARNING:Therequestedimage'splatform(linux/amd64)doesnotmatchthedetectedhostplatform(linux/arm64/v8)andnospecificplatformwasrequestedda6992c0......
  • 005 python 打开windows下的软件,python打开记事本 subprocess
    如果要用python代码打开windos上的记事本,代码如下:importsubprocessnotePro=subprocess.Popen('notepad.exe')print(notePro)如果要打开别的软件,则把notepad.exe......
  • [Oracle19C 数据库管理] 管理回滚表空间(UNDO Tablespace)
    当对数据进行修改时,Oracle数据库会将旧的数据存储到UNDO表空间(回滚表空间)。回滚表空间让用户可以rollback到修改前的数据,提供了读一致性,并支持闪回查询过去的数据。Undo......