MSDN上说,在MFC中树控件有三种创建方法:
1. 在对话框模版中添加一个TreeBox控件(假设其ID 为:IDR_TREE), 则自动生成一个树控件对象。通过在对话框类中的OnInitDlg()函数中, 使用CTreeCtrl *pTree = (CTreeCtrl *) GetDlgItem(IDR_TREE); 将得到CTreeCtrl对象的指针, 使用此指针就可以对树控件进行操作。
另外, 对树控件的消息以及消息处理函数都是在对话框类中添加。
2. 使用CTreeView。 这样AppWzard将自动创建一个相关的CTreeCtrl 对象。
3. 使用CTreeCtrl::Create()函数创建。
void CTreeCtrl::Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID )
其中dwStyle使用以下树控件专用的风格:
TVS_HASLINES 在父/子结点之间绘制连线
TVS_LINESATROOT 在根/子结点之间绘制连线
TVS_HASBUTTONS 在每一个结点前添加一个按钮,用于表示当前结点是否已被展开
TVS_EDITLABELS 结点的显示字符可以被编辑
TVS_SHOWSELALWAYS 在失去焦点时也显示当前选中的结点
TVS_DISABLEDRAGDROP 不允许Drag/Drop
TVS_NOTOOLTIPS 不使用ToolTip显示结点的显示字符
TVS_CHECKBOXES 在每项旁边添加一个选择框
此外, dwStyle在树控件创建时一定要加上WS_VISIBLE , 否则树控件将会被隐藏!!当然,如果没有加上这个WS_VISIBLE也可以在创建之后立即调用ShowWindow(SW_SHOWNORMAL),控件便显示出来了,若调用ShowWindow(SW_HIDE)控件便隐藏了。同时,一般情况下,树控件是作为子窗口使用,那么在创建时要加上WS_CHILD,表示控件是子窗口(那么在创建时当然要指定父窗口了)。
nID可以随便定义一个和其他ID值不冲突的数字便于标识(当然也可以直接用数字,这里用的是IDD_TREE)。 而pParentWnd则指向树控件的父窗口, 在对话框中是对话框类对象的指针, 在视图中, 是视图类对象的指针(这里使用的是this指针)。 我在视图类CScrollView中的OninitialUpdate()函数中加入一个树控件:
m_tree.Create(WS_VISIBLE | WS_TABSTOP | WS_CHILD | WS_BORDER
| TVS_HASBUTTONS | TVS_LINESATROOT | TVS_HASLINES
| TVS_DISABLEDRAGDROP, CRect(0,0,100,100),this,IDD_TREE);
HTREEITEM hItem,hSubItem;
hItem = m_tree.InsertItem(_T("Parent1"),TVI_ROOT);
//在根结点上添加Parent1
hSubItem = m_tree.InsertItem(_T("Child1_1"),hItem);
//在Parent1上添加一个子结点
hSubItem = m_tree.InsertItem(_T("Child1_2"),hItem,hSubItem);
//在Parent1上添加一个子结点,排在Child1_1后面
hSubItem = m_tree.InsertItem(_T("Child1_3"),hItem,hSubItem);
hItem = m_tree.InsertItem(_T("Parent2"),TVI_ROOT,hItem);
hItem = m_tree.InsertItem(_T("Parent3"),TVI_ROOT,hItem);
其中的m_tree在CMyScrollView头文件中声明为:
Private:
CTreeCtrl m_tree;
注意: 一定要在头文件中声明一个树控件类的对象, 不能在函数中声明(表示对象是临时的), 如果在函数中声明了类对象, 那么在函数返回后该对象会被消除, 这样就无法显示树控件了!!!
父窗口指定为this指针,即CMyScrollView指针,表示树控件将会向它发送命令消息(WM_COMMAND)和通知消息(WM_NOTIFY)。那么所有的树控件处理消息都将在这个父窗口进行处理。(在对话框中的情形相同)
例如:为了能对树控件进行操作, 必须添加消息和消息处理函数。 此时由于是用Create()函数创建的树控件, 无法用AppWzard 为它产生消息以及消息处理函数, 必须手动添加。
例如:在CMyScrollView的头文件中声明: (用于处理双击树控件项事件)
afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);
在CMyScrollView的实现文件中添加:
BEGIN_MESSAGE_MAP(CMyScrollView, CScrollView)
…。
…。
ON_NOTIFY(NM_DBLCLK, IDD_TREE, OnTreeDblClk)
…。
…。
END_MESSAGE_MAP()
void CMyScrollView::OnTreeDblClk(NMHDR* pNMHDR, LRESULT *pResult)
{
HTREEITEM hSelected = m_tree.GetSelectedItem();
if (hSelected != NULL) {
if(m_tree.GetItemText(hSelected) == "Child1_1"){
AfxMessageBox(_T("This Child1_1 is Clicked!"));
}
}
*pResult = 0;
}
2007年5月26日土曜日
树控件的结点插入
树控件结点的插入使用CTreeCtrl::InsertItem()函数。
HTREEITEM InsertItem( LPTVINSERTSTRUCT lpInsertStruct );
HTREEITEM InsertItem(UINT nMask, LPCTSTR lpszItem, int nImage, int nSelectedImage, UINT nState, UINT nStateMask, LPARAM lParam, HTREEITEM hParent, HTREEITEM hInsertAfter );
HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST );
HTREEITEM InsertItem( LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST);
一般情况下使用最后两种形式,一种是用于插入结点,另一种增加了对图标的插入操作。
树控件可以用于树结构,其中有一个根结点(Root)然后下面有许多子结点,而每个子结点上允许有一个或多个或没有子结点。在树控件中每一个结点都有一个句柄(HTREEITEM),同时添加结点时必须提供的参数是该结点的父结点句柄,(其中根Root结点只有一个,既不可以添加也不可以删除)利用
HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST );可以添加一个结点,pszItem为显示的字符,hParent代表父结点的句柄,当前添加的结点会排在hInsertAfter表示的结点的后面,返回值为当前创建的结点的句柄。如例子中,在调用了Create()函数之后进行了结点的插入操作它生成了如下形式的树结构:
+--- Parent1
+--- Child1_1
+--- Child1_2
+--- Child1_3
+--- Parent2
+--- Parent3
如果你希望在每个结点前添加一个小图像,就必需先调用CTreeCtrl:: SetImageList( CImageList * pImageList, int nImageListType );指明当前所使用的ImageList,nImageListType为TVSIL_NORMAL。在调用完成后控件中使用图片以设置的ImageList中图片为准。然后调用
HTREEITEM InsertItem( LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST);添加结点,nImage为结点没被选中时所使用图片序号,nSelectedImage为结点被选中时所使用图片序号。下面的代码演示了ImageList的设置。
/*m_list 为CImageList对象IDB_TREE 为16*(16*4)的位图,每个图片为16*16共4个图标*/
m_list.Create(IDB_TREE,16, 4, RGB(0,0,0));
m_tree.SetImageList(&m_list,TVSIL_NORMAL);
m_tree.InsertItem("Parent1" , 0, 1);//添加,选中时显示图像1,未选中时显示图像0
添加结点最复杂的函数是前两个,它们使用了各种结构以及字段值来标识结点所处的状态和属性。
第一个函数:
HTREEITEM InsertItem( LPTVINSERTSTRUCT lpInsertStruct );
要求传入一个TVINSERTSTRUCT结构的指针。它的定义为:(包括两个结点句柄,一个描述树控件结点的结构)
//TVINSERTSTRUCT
typedef struct tagTVINSERTSTRUCT {
HTREEITEM hParent; //父结点句柄
HTREEITEM hInsertAfter; //插入其后的结点句柄
#if (_WIN32_IE >= 0x0400)
union
{
TVITEMEX itemex;
TVITEM item;
} DUMMYUNIONNAME;
#else
TVITEM item; //描述树控件结点的结构
#endif
} TVINSERTSTRUCT, FAR *LPTVINSERTSTRUCT;
而TVITEM定义为:
//TVITEM
typedef struct tagTVITEM{
UINT mask;
HTREEITEM hItem;
UINT state;
UINT stateMask;
LPTSTR pszText;
int cchTextMax;
int iImage;
int iSelectedImage;
int cChildren;
LPARAM lParam;
} TVITEM, FAR *LPTVITEM;
mask
一组标志位,表明哪个成员数据是有效的。当此结构用于TVM_GETITEM消息时,mask成员表示获得的项属性。它可以是以下值中的一个或多个组合:
TVIF_CHILDREN :cChildren 成员有效;
TVIF_HANDLE :hItem 成员有效;
TVIF_IMAGE :iImage 成员有效;
TVIF_PARAM :lParam成员有效;
TVIF_SELECTEDIMAGE :iSelectedImage成员有效;
TVIF_STATE :state 和 stateMask成员有效;
TVIF_TEXT :pszText 和 cchTextMax成员有效。
hItem
表示相关项的句柄。
state
一组标志位以及图像列表索引,表示项状态。当设置项状态时,stateMask表示此成员的哪些位是有效的;当获取项状态时,此成员返回由stateMask指示的当前有效的项状态位。此成员的0到7位包含项状态标志。见附录。8到11位代表从1开始的覆盖图像索引。覆盖图像用来叠加在项图像上。如果这些位是0,那么项就没有覆盖图像。为了隔离这些位,使用TVIS_OVERLAYMASK掩码(mask)。要设置项覆盖图像索引,应该使用INDEXTOOVERLAYMASK宏。图像列表的覆盖图像使用ImageList::SetOverlayImage()函数设置。因为使用的从 1 开始的索引是 4 位的,所以覆盖图像必须在图像列表的前 15 位。12到15位代表项状态图像索引。状态图像显示在项图标的旁边,表示应用程序定义的状态。也就是说指定了状态图像后,树控件就在每项的图标左边为状态图像保留空间。应用程序可以用状态图像(如选定和未选定的复选框)指示应用程序定义的项状态。如果这些位是0,那么就没有状态图像。为了隔离这些位,使用TVIS_STATEIMAGEMASK掩码(mask)。要设置状态图像,应该使用INDEXTOSTATEIMAGEMASK宏。状态图像索引表示要画的图像列表的图像索引。状态图像列表由TVM_SETIMAGELIST消息指定。
HTREEITEM InsertItem( LPTVINSERTSTRUCT lpInsertStruct );
HTREEITEM InsertItem(UINT nMask, LPCTSTR lpszItem, int nImage, int nSelectedImage, UINT nState, UINT nStateMask, LPARAM lParam, HTREEITEM hParent, HTREEITEM hInsertAfter );
HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST );
HTREEITEM InsertItem( LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST);
一般情况下使用最后两种形式,一种是用于插入结点,另一种增加了对图标的插入操作。
树控件可以用于树结构,其中有一个根结点(Root)然后下面有许多子结点,而每个子结点上允许有一个或多个或没有子结点。在树控件中每一个结点都有一个句柄(HTREEITEM),同时添加结点时必须提供的参数是该结点的父结点句柄,(其中根Root结点只有一个,既不可以添加也不可以删除)利用
HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST );可以添加一个结点,pszItem为显示的字符,hParent代表父结点的句柄,当前添加的结点会排在hInsertAfter表示的结点的后面,返回值为当前创建的结点的句柄。如例子中,在调用了Create()函数之后进行了结点的插入操作它生成了如下形式的树结构:
+--- Parent1
+--- Child1_1
+--- Child1_2
+--- Child1_3
+--- Parent2
+--- Parent3
如果你希望在每个结点前添加一个小图像,就必需先调用CTreeCtrl:: SetImageList( CImageList * pImageList, int nImageListType );指明当前所使用的ImageList,nImageListType为TVSIL_NORMAL。在调用完成后控件中使用图片以设置的ImageList中图片为准。然后调用
HTREEITEM InsertItem( LPCTSTR lpszItem, int nImage, int nSelectedImage, HTREEITEM hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST);添加结点,nImage为结点没被选中时所使用图片序号,nSelectedImage为结点被选中时所使用图片序号。下面的代码演示了ImageList的设置。
/*m_list 为CImageList对象IDB_TREE 为16*(16*4)的位图,每个图片为16*16共4个图标*/
m_list.Create(IDB_TREE,16, 4, RGB(0,0,0));
m_tree.SetImageList(&m_list,TVSIL_NORMAL);
m_tree.InsertItem("Parent1" , 0, 1);//添加,选中时显示图像1,未选中时显示图像0
添加结点最复杂的函数是前两个,它们使用了各种结构以及字段值来标识结点所处的状态和属性。
第一个函数:
HTREEITEM InsertItem( LPTVINSERTSTRUCT lpInsertStruct );
要求传入一个TVINSERTSTRUCT结构的指针。它的定义为:(包括两个结点句柄,一个描述树控件结点的结构)
//TVINSERTSTRUCT
typedef struct tagTVINSERTSTRUCT {
HTREEITEM hParent; //父结点句柄
HTREEITEM hInsertAfter; //插入其后的结点句柄
#if (_WIN32_IE >= 0x0400)
union
{
TVITEMEX itemex;
TVITEM item;
} DUMMYUNIONNAME;
#else
TVITEM item; //描述树控件结点的结构
#endif
} TVINSERTSTRUCT, FAR *LPTVINSERTSTRUCT;
而TVITEM定义为:
//TVITEM
typedef struct tagTVITEM{
UINT mask;
HTREEITEM hItem;
UINT state;
UINT stateMask;
LPTSTR pszText;
int cchTextMax;
int iImage;
int iSelectedImage;
int cChildren;
LPARAM lParam;
} TVITEM, FAR *LPTVITEM;
mask
一组标志位,表明哪个成员数据是有效的。当此结构用于TVM_GETITEM消息时,mask成员表示获得的项属性。它可以是以下值中的一个或多个组合:
TVIF_CHILDREN :cChildren 成员有效;
TVIF_HANDLE :hItem 成员有效;
TVIF_IMAGE :iImage 成员有效;
TVIF_PARAM :lParam成员有效;
TVIF_SELECTEDIMAGE :iSelectedImage成员有效;
TVIF_STATE :state 和 stateMask成员有效;
TVIF_TEXT :pszText 和 cchTextMax成员有效。
hItem
表示相关项的句柄。
state
一组标志位以及图像列表索引,表示项状态。当设置项状态时,stateMask表示此成员的哪些位是有效的;当获取项状态时,此成员返回由stateMask指示的当前有效的项状态位。此成员的0到7位包含项状态标志。见附录。8到11位代表从1开始的覆盖图像索引。覆盖图像用来叠加在项图像上。如果这些位是0,那么项就没有覆盖图像。为了隔离这些位,使用TVIS_OVERLAYMASK掩码(mask)。要设置项覆盖图像索引,应该使用INDEXTOOVERLAYMASK宏。图像列表的覆盖图像使用ImageList::SetOverlayImage()函数设置。因为使用的从 1 开始的索引是 4 位的,所以覆盖图像必须在图像列表的前 15 位。12到15位代表项状态图像索引。状态图像显示在项图标的旁边,表示应用程序定义的状态。也就是说指定了状态图像后,树控件就在每项的图标左边为状态图像保留空间。应用程序可以用状态图像(如选定和未选定的复选框)指示应用程序定义的项状态。如果这些位是0,那么就没有状态图像。为了隔离这些位,使用TVIS_STATEIMAGEMASK掩码(mask)。要设置状态图像,应该使用INDEXTOSTATEIMAGEMASK宏。状态图像索引表示要画的图像列表的图像索引。状态图像列表由TVM_SETIMAGELIST消息指定。
附:树视图控件的状态标志
TVIS_BOLD 项标签文本(ItemText)是粗体
TVIS_CUT 所选择的项作为粘贴复制操作
TVIS_DROPHILITED 所选择的项作为拖拽标志
TVIS_EXPANDED 子项列表当前是展开的。也就是子项是可见的。此值仅用于父项。
TVIS_EXPANDEDONCE子项列表已经至少展开了一次。如果父项有此状态,并对TV_EXPAND消息作出了响应,就不会为该父项产生 TVN_ITEMEXPANDING和TVN_ITEMEXPANDED 通知消息。把 TVE_COLLAPSE 和 TVE_COLLAPSERESET同 TVM_EXPAND一起使用就会使此状态重置。此值仅用于父项。
TVIS_EXPANDPARTIAL 部分展开树视图项。在此状态中,只是部分并不是全部的子项是可见的,而且会显示父项的加号。
TVIS_SELECTED 此项被选中了。项的外观取决于是否有焦点。会使用系统颜色画出所选择的项。
要设置或获取项的覆盖图像索引或者状态图像索引,必须在stateMask成员中指定以下标志:(这些值也可以用于去掉不感兴趣的状态位)
TVIS_OVERLAYMASK 用来指定项覆盖图像索引的位掩码。
TVIS_STATEIMAGEMASK 用来指定项状态图像的位掩码。
TVIS_USERMASK 同TVIS_STATEIMAGEMASK相同。
stateMask
表示state成员的有效位。如果要获取项状态,则在stateMask成员中设置相应位来指示state成员所返回的值;如果想要设置项状态,则在stateMask成员中设置相应位来指示想在sate成员中设置的位。如果要设置或获取项覆盖图像索引,则设置TVIS_OVERLAYMASK位;如果要设置或获取项状态索引,则设置TVIS_STATEIMAGEMASK位。
pszText
以NULL结尾的字符串地址,如果TVITEM结构要求项属性,那么它代表项标签文本(Item Text)。如果此成员的值是LPSTR_TEXTCALLBACK,那么父窗口负责存储项标签文本的名称。此种情况下,当树控件需要显示项标签、对项标签排序、编辑项标签或者当项标签改变时需要发送TVN_SETDISPINFO通知消息时,它会给父窗口发送TVN_GETDISPINFO通知消息。
如果TVITEM正在接收项属性时,此成员表示接收到的项标签文本的缓冲区地址。
cchTextMax
由pszText指向的缓冲区地址的字节大小。如果用TVITEM设置项属性,此成员被忽略。
说明:通常在向树控件添加项时指定项标签文本。InsertItem 成员函数能传递 TVITEM 结构。该结构定义了项的属性,项属性中有一个包含标签文本的字符串。树控件分配存储各项的内存,其中大部分内存都被项标签文本占用。如果应用程序保存了树控件中字符串的副本,就可以减少控件所需的内存空间,方法是在 TV_ITEM 的 pszText 成员中或者在 lpszItem 参数中指定 LPSTR_TEXTCALLBACK 值,而不是将实际字符串传递给树控件。每当需要重绘某项时,LPSTR_TEXTCALLBACK 使树控件从应用程序中检索该项的标签文本。为了检索标签文本,树控件发送 TVN_GETDISPINFO 通知消息,该消息包括 NMTVDISPINFO 结构的地址。必须通过设置所含结构的适当成员来响应该通知消息。
树控件使用从创建树控件的进程堆分配的内存。树控件最多可以包含的项数取决于堆中可用的内存量。每个项占用 64 字节。
iImage
当树控件的项结点是非选中状态时所使用的图标在图像列表中的索引值。如果此成员的值是I_IMAGECALLBACK,则父窗口负责存储该索引值。在此情况下,当树控件需要显示项图像时它向父窗口发送TVN_GETDISPINFO通知消息来获得索引值。
iSelectedImage
当树控件的项结点是选中状态时所使用的图标在图像列表中的索引值。如果此成员的值是I_IMAGECALLBACK,则父窗口负责存储该索引值。在此情况下,当树控件需要显示项图像时它向父窗口发送VN_GETDISPINFO通知消息来获得索引值。
cChildren
表示树控件项是否有相关的子结点的标志位。此成员可以是以下值之一:
0 —— 表示此项没有子结点。
1 —— 表示此此项有一个或多个子结点。
I_CHILDRENCALLBACK —— 父窗口会始终跟踪确定此项是否有子结点。此时,如果树控件需要显示项时,它会给父窗口发送TVN_GETDISPINFO通知消息,决定此项是否有子结点。如果树控件含有TVS_HASBUTTONS风格,则它使用此成员来决定是否显示按钮以表明存在子结点。也可以使用此成员来强制树控件显示按钮,尽管没有在此项之下插入任何子结点。这样可以最小化由于在此项之下插入了子结点所占用的控件内存。
lParam
同控件相关的32位值。
下面这段代码显示了如何用此结构插入树控件项,同时也演示了如何为项插入图像。
HICON hIcon[8];
int n;
m_imageList.Create(16, 16, 0, 8, 8); // 32, 32 for large icons
hIcon[0] = AfxGetApp()->LoadIcon(IDI_WHITE);
hIcon[1] = AfxGetApp()->LoadIcon(IDI_BLACK);
hIcon[2] = AfxGetApp()->LoadIcon(IDI_RED);
hIcon[3] = AfxGetApp()->LoadIcon(IDI_BLUE);
hIcon[4] = AfxGetApp()->LoadIcon(IDI_YELLOW);
hIcon[5] = AfxGetApp()->LoadIcon(IDI_CYAN);
hIcon[6] = AfxGetApp()->LoadIcon(IDI_PURPLE);
hIcon[7] = AfxGetApp()->LoadIcon(IDI_GREEN);
for (n = 0; n < 8; n++) {
m_imageList.Add(hIcon[n]);
}
CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
pTree->SetImageList(&m_imageList, TVSIL_NORMAL);
TV_INSERTSTRUCT tvinsert;
tvinsert.hParent = NULL;
tvinsert.hInsertAfter = TVI_LAST;
tvinsert.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE |
TVIF_TEXT;// | TVIF_STATE;
tvinsert.item.hItem = NULL;
tvinsert.item.state = 0;
//tvinsert.item.state = TVIS_EXPANDED;
tvinsert.item.stateMask = 0;
//tvinsert.item.stateMask = TVIS_OVERLAYMASK;
tvinsert.item.cchTextMax = 6;
tvinsert.item.iSelectedImage = 1;
tvinsert.item.cChildren = 0;
tvinsert.item.lParam = 0;
// top level
tvinsert.item.pszText = "Parent1";
tvinsert.item.iImage = 2;
HTREEITEM hDad = pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Parent2";
HTREEITEM hMom = pTree->InsertItem(&tvinsert);
// second level
tvinsert.hParent = hDad;
tvinsert.item.pszText = "Children1-1";
tvinsert.item.iImage = 3;
pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Children1-2";
pTree->InsertItem(&tvinsert);
// second level
tvinsert.hParent = hMom;
tvinsert.item.pszText = "Children2-1";
tvinsert.item.iImage = 4;
pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Children2-2";
pTree->InsertItem(&tvinsert);
tvinser.item.pszText = "Children2-3";
HTREEITEM hOther = pTree->InsertItem(&tvinsert);
// third level
tvinsert.hParent = hOther;
tvinsert.item.pszText = "Children2-3-1";
tvinsert.item.iImage = 7;
pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Children2-3-2";
pTree->InsertItem(&tvinsert);
―――――――――――――――――――――――――――――――――――
即:
+Parent1
--Children1-1
--Children1-2
+Parent2
--Children2-1
--Children2-2
+Children2-3
--Children2-3-1
--Children2-3-2
TVIS_CUT 所选择的项作为粘贴复制操作
TVIS_DROPHILITED 所选择的项作为拖拽标志
TVIS_EXPANDED 子项列表当前是展开的。也就是子项是可见的。此值仅用于父项。
TVIS_EXPANDEDONCE子项列表已经至少展开了一次。如果父项有此状态,并对TV_EXPAND消息作出了响应,就不会为该父项产生 TVN_ITEMEXPANDING和TVN_ITEMEXPANDED 通知消息。把 TVE_COLLAPSE 和 TVE_COLLAPSERESET同 TVM_EXPAND一起使用就会使此状态重置。此值仅用于父项。
TVIS_EXPANDPARTIAL 部分展开树视图项。在此状态中,只是部分并不是全部的子项是可见的,而且会显示父项的加号。
TVIS_SELECTED 此项被选中了。项的外观取决于是否有焦点。会使用系统颜色画出所选择的项。
要设置或获取项的覆盖图像索引或者状态图像索引,必须在stateMask成员中指定以下标志:(这些值也可以用于去掉不感兴趣的状态位)
TVIS_OVERLAYMASK 用来指定项覆盖图像索引的位掩码。
TVIS_STATEIMAGEMASK 用来指定项状态图像的位掩码。
TVIS_USERMASK 同TVIS_STATEIMAGEMASK相同。
stateMask
表示state成员的有效位。如果要获取项状态,则在stateMask成员中设置相应位来指示state成员所返回的值;如果想要设置项状态,则在stateMask成员中设置相应位来指示想在sate成员中设置的位。如果要设置或获取项覆盖图像索引,则设置TVIS_OVERLAYMASK位;如果要设置或获取项状态索引,则设置TVIS_STATEIMAGEMASK位。
pszText
以NULL结尾的字符串地址,如果TVITEM结构要求项属性,那么它代表项标签文本(Item Text)。如果此成员的值是LPSTR_TEXTCALLBACK,那么父窗口负责存储项标签文本的名称。此种情况下,当树控件需要显示项标签、对项标签排序、编辑项标签或者当项标签改变时需要发送TVN_SETDISPINFO通知消息时,它会给父窗口发送TVN_GETDISPINFO通知消息。
如果TVITEM正在接收项属性时,此成员表示接收到的项标签文本的缓冲区地址。
cchTextMax
由pszText指向的缓冲区地址的字节大小。如果用TVITEM设置项属性,此成员被忽略。
说明:通常在向树控件添加项时指定项标签文本。InsertItem 成员函数能传递 TVITEM 结构。该结构定义了项的属性,项属性中有一个包含标签文本的字符串。树控件分配存储各项的内存,其中大部分内存都被项标签文本占用。如果应用程序保存了树控件中字符串的副本,就可以减少控件所需的内存空间,方法是在 TV_ITEM 的 pszText 成员中或者在 lpszItem 参数中指定 LPSTR_TEXTCALLBACK 值,而不是将实际字符串传递给树控件。每当需要重绘某项时,LPSTR_TEXTCALLBACK 使树控件从应用程序中检索该项的标签文本。为了检索标签文本,树控件发送 TVN_GETDISPINFO 通知消息,该消息包括 NMTVDISPINFO 结构的地址。必须通过设置所含结构的适当成员来响应该通知消息。
树控件使用从创建树控件的进程堆分配的内存。树控件最多可以包含的项数取决于堆中可用的内存量。每个项占用 64 字节。
iImage
当树控件的项结点是非选中状态时所使用的图标在图像列表中的索引值。如果此成员的值是I_IMAGECALLBACK,则父窗口负责存储该索引值。在此情况下,当树控件需要显示项图像时它向父窗口发送TVN_GETDISPINFO通知消息来获得索引值。
iSelectedImage
当树控件的项结点是选中状态时所使用的图标在图像列表中的索引值。如果此成员的值是I_IMAGECALLBACK,则父窗口负责存储该索引值。在此情况下,当树控件需要显示项图像时它向父窗口发送VN_GETDISPINFO通知消息来获得索引值。
cChildren
表示树控件项是否有相关的子结点的标志位。此成员可以是以下值之一:
0 —— 表示此项没有子结点。
1 —— 表示此此项有一个或多个子结点。
I_CHILDRENCALLBACK —— 父窗口会始终跟踪确定此项是否有子结点。此时,如果树控件需要显示项时,它会给父窗口发送TVN_GETDISPINFO通知消息,决定此项是否有子结点。如果树控件含有TVS_HASBUTTONS风格,则它使用此成员来决定是否显示按钮以表明存在子结点。也可以使用此成员来强制树控件显示按钮,尽管没有在此项之下插入任何子结点。这样可以最小化由于在此项之下插入了子结点所占用的控件内存。
lParam
同控件相关的32位值。
下面这段代码显示了如何用此结构插入树控件项,同时也演示了如何为项插入图像。
HICON hIcon[8];
int n;
m_imageList.Create(16, 16, 0, 8, 8); // 32, 32 for large icons
hIcon[0] = AfxGetApp()->LoadIcon(IDI_WHITE);
hIcon[1] = AfxGetApp()->LoadIcon(IDI_BLACK);
hIcon[2] = AfxGetApp()->LoadIcon(IDI_RED);
hIcon[3] = AfxGetApp()->LoadIcon(IDI_BLUE);
hIcon[4] = AfxGetApp()->LoadIcon(IDI_YELLOW);
hIcon[5] = AfxGetApp()->LoadIcon(IDI_CYAN);
hIcon[6] = AfxGetApp()->LoadIcon(IDI_PURPLE);
hIcon[7] = AfxGetApp()->LoadIcon(IDI_GREEN);
for (n = 0; n < 8; n++) {
m_imageList.Add(hIcon[n]);
}
CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
pTree->SetImageList(&m_imageList, TVSIL_NORMAL);
TV_INSERTSTRUCT tvinsert;
tvinsert.hParent = NULL;
tvinsert.hInsertAfter = TVI_LAST;
tvinsert.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE |
TVIF_TEXT;// | TVIF_STATE;
tvinsert.item.hItem = NULL;
tvinsert.item.state = 0;
//tvinsert.item.state = TVIS_EXPANDED;
tvinsert.item.stateMask = 0;
//tvinsert.item.stateMask = TVIS_OVERLAYMASK;
tvinsert.item.cchTextMax = 6;
tvinsert.item.iSelectedImage = 1;
tvinsert.item.cChildren = 0;
tvinsert.item.lParam = 0;
// top level
tvinsert.item.pszText = "Parent1";
tvinsert.item.iImage = 2;
HTREEITEM hDad = pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Parent2";
HTREEITEM hMom = pTree->InsertItem(&tvinsert);
// second level
tvinsert.hParent = hDad;
tvinsert.item.pszText = "Children1-1";
tvinsert.item.iImage = 3;
pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Children1-2";
pTree->InsertItem(&tvinsert);
// second level
tvinsert.hParent = hMom;
tvinsert.item.pszText = "Children2-1";
tvinsert.item.iImage = 4;
pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Children2-2";
pTree->InsertItem(&tvinsert);
tvinser.item.pszText = "Children2-3";
HTREEITEM hOther = pTree->InsertItem(&tvinsert);
// third level
tvinsert.hParent = hOther;
tvinsert.item.pszText = "Children2-3-1";
tvinsert.item.iImage = 7;
pTree->InsertItem(&tvinsert);
tvinsert.item.pszText = "Children2-3-2";
pTree->InsertItem(&tvinsert);
―――――――――――――――――――――――――――――――――――
即:
+Parent1
--Children1-1
--Children1-2
+Parent2
--Children2-1
--Children2-2
+Children2-3
--Children2-3-1
--Children2-3-2
Win32怎么做?
对Win32程序来说,插入树控件项也不难,只需要向树控件发送TVM_INSERTITEM消息即可。下面是用Win32写出的等价代码(为了简明起见,去掉了对控件项图像的支持)在主窗口过程中加入如下代码:
HWND hwndTree;
TVINSERTSTRUCT tvinsert;
switch (message)
{
case WM_CREATE:
{ hwndTree = CreateWindow(WC_TREEVIEW, _T("TreeView"),
WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT,
0,0,200,100, hWnd,(HMENU)1, hInst,NULL);
tvinsert.hParent = NULL;
tvinsert.hInsertAfter = TVI_LAST;
tvinsert.item.mask = TVIF_TEXT;
tvinsert.item.hItem = NULL;
tvinsert.item.state = 0;
tvinsert.item.stateMask = 0;
tvinsert.item.cchTextMax = 6;
tvinsert.item.cChildren = 0;
tvinsert.item.lParam = 0;
// top level
tvinsert.item.pszText = _T("Parent1");
HTREEITEM hDad = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Parent2");
HTREEITEM hMom = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
// second level
tvinsert.hParent = hDad;
tvinsert.item.pszText = _T("Children1-1");
SendMessage(hwndTree,TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children1-2");
SendMessage(hwndTree,TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
// second level
tvinsert.hParent = hMom;
tvinsert.item.pszText = _T("Children2-1");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children2-2");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children2-3");
HTREEITEM hOther = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
// third level
tvinsert.hParent = hOther;
tvinsert.item.pszText = _T("Children2-3-1");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children2-3-2");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
break;
}
case: WM_COMMAND:
…
…
break;
other case …
… …
return 0;
}
此外还要添加 InitCommonControls();函数,它的头文件为:commctrl.h,在工程设置链接选项卡中的“对象/库模块”中添加comctrl32.lib。这样便可顺利通过编译和链接。
HWND hwndTree;
TVINSERTSTRUCT tvinsert;
switch (message)
{
case WM_CREATE:
{ hwndTree = CreateWindow(WC_TREEVIEW, _T("TreeView"),
WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT,
0,0,200,100, hWnd,(HMENU)1, hInst,NULL);
tvinsert.hParent = NULL;
tvinsert.hInsertAfter = TVI_LAST;
tvinsert.item.mask = TVIF_TEXT;
tvinsert.item.hItem = NULL;
tvinsert.item.state = 0;
tvinsert.item.stateMask = 0;
tvinsert.item.cchTextMax = 6;
tvinsert.item.cChildren = 0;
tvinsert.item.lParam = 0;
// top level
tvinsert.item.pszText = _T("Parent1");
HTREEITEM hDad = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Parent2");
HTREEITEM hMom = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
// second level
tvinsert.hParent = hDad;
tvinsert.item.pszText = _T("Children1-1");
SendMessage(hwndTree,TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children1-2");
SendMessage(hwndTree,TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
// second level
tvinsert.hParent = hMom;
tvinsert.item.pszText = _T("Children2-1");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children2-2");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children2-3");
HTREEITEM hOther = (HTREEITEM)SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
// third level
tvinsert.hParent = hOther;
tvinsert.item.pszText = _T("Children2-3-1");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Children2-3-2");
SendMessage(hwndTree, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
break;
}
case: WM_COMMAND:
…
…
break;
other case …
… …
return 0;
}
此外还要添加 InitCommonControls();函数,它的头文件为:commctrl.h,在工程设置链接选项卡中的“对象/库模块”中添加comctrl32.lib。这样便可顺利通过编译和链接。
树控件消息
树控件一共有43个控制消息(TVM_XXX)和23个通知消息(TVN_XXX)下面按操作的不同进行简单的说明。
一、 一、拖拽消息
拖拽(drag-drop)操作包含这样几步:
1. 1。选中一个节点后,按住鼠标左键(此时是左键拖拽)开始拖动鼠标时,树控件向其父窗口发送TVN_BEGINDRAG通知消息(通过此消息可以得到被拖拽节点的句柄)。此时,需要准备在拖拽操作时所显示的拖拽图标。那么需要向树控件发送TVM_CREATEDRAGIMAGE消息,它创建一个Image List对象,并把被选中节点的图标复制到这个Image List中,并返回Image List句柄。然后使用ImageList_BeginDrag以及ImageList_DragEnter宏对这个Image List做一些设置,比如拖拽时鼠标所处的作标点。
case TVN_BEGINDRAG:
{
POINT pt;
pt.x = 0;
pt.y = 0;
ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &pt);
NMTREEVIEW *lpnmtv = (NMTREEVIEW*) lParam;
hitemDrag = lpnmtv->itemNew.hItem;
hitemDrop = NULL;
himge = (HIMAGELIST)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_CREATEDRAGIMAGE,0,(LPARAM)(HTREEITEM)lpnmtv->itemNew.hItem);
if(!himge)
break;
bdragging = TRUE;
ImageList_BeginDrag(himge, 0, pt.x ,pt.y );
POINT point = lpnmtv->ptDrag;
ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &point);
ImageList_DragEnter(GetDlgItem(hWnd, ID_TREEVIEW),point.x ,point.y );
SetCapture(hWnd);
break;
}
2.2。 拖拽过程中会产生一系列的WM_MOUSEMOVE消息,因此要在这个消息的处理例程中要确定拖拽图标的位置(ImageList_DragMove())以及拖拽目标的位置。对于拖拽目标的确定,一般使用TVM_HITTEST消息来获得拖拽目标节点的句柄。并把这个节点加亮显示(使用带TVGN_DROPHILITE标记的TVM_SELECTITEM消息可以加亮显示拖拽节点)。
case WM_MOUSEMOVE:
{
TVHITTESTINFO tvht;
UINT flags;
flags = (UINT)wParam;
if(bdragging){
pos.x = LOWORD(lParam);
pos.y = HIWORD(lParam);
ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &pos);
ImageList_DragMove(pos.x, pos.y);
ImageList_DragShowNolock(FALSE);
ScreenToClient(GetDlgItem(hWnd, ID_TREEVIEW), &pos);
tvht.pt.x = pos.x;
tvht.pt.y = pos.y;
if(hitemDrop = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW),
TVM_HITTEST, (WPARAM)&flags ,(LPARAM)&tvht))
{
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,
TVGN_DROPHILITE,(LPARAM)hitemDrop);
}
ImageList_DragShowNolock(TRUE);
}
break;
}
3.3。 最后选定拖拽目标节点后,释放鼠标键产生WM_LBUTTONUP消息。在这里,Image List已完成它的任务,应该释放它(ImageList_DragLeave 、ImageList_EndDrag)。然后将源节点插入到目标节点处,并删除源节点。
case WM_LBUTTONUP:
{
if (bdragging){
bdragging = FALSE;
ImageList_DragLeave(GetDlgItem(hWnd, ID_TREEVIEW));
ImageList_EndDrag();
ReleaseCapture();
if( hitemDrag == hitemDrop )
break;
HTREEITEM htiParent = hitemDrop;
while( (htiParent = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW),
TVM_GETNEXTITEM,
(WPARAM)(UINT)TVGN_PARENT,
(LPARAM)htiParent )) != NULL )
{
if( htiParent == hitemDrag )
break;
}
DeleteObject(himge);
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_EXPAND,
(WPARAM) (UINT)TVE_EXPAND, (LPARAM)hitemDrop);
HTREEITEM htiNew = CopyBranch(GetDlgItem(hWnd, ID_TREEVIEW),
hitemDrag, hitemDrop, TVI_LAST );
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_DELETEITEM,
0, (LPARAM)hitemDrag);
/*检查最后高亮显示的节点,并选中它。还必须使得其不再高亮显示,否则其它的节点被选中时就不能高亮显示了。*/
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,
TVGN_DROPHILITE,(LPARAM)0);
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,
TVGN_CARET, (LPARAM)htiNew);
}
break;
}
为了将源节点插入到目标节点处,还需要进行插入操作。为此需要两个函数如下:
char Text[128];
HTREEITEM CopyItem(HWND htreewnd, HTREEITEM hItem, HTREEITEM htiNewParent, HTREEITEM htiAfter /*= TVI_LAST*/ )
{
TV_INSERTSTRUCT tvstruct;
HTREEITEM hNewItem;
// 获得源节点信息
tvstruct.item.hItem = hItem;
tvstruct.item.mask = TVIF_CHILDREN | TVIF_HANDLE |
TVIF_IMAGE | TVIF_SELECTEDIMAGE ;
SendMessage(htreewnd, TVM_GETITEM, 0 ,(LPARAM)&tvstruct.item);
// 在适当的位置上插入节点
tvstruct.hParent = htiNewParent;
tvstruct.hInsertAfter = htiAfter;
tvstruct.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT;
tvstruct.item.pszText = Text;
tvstruct.item.cchTextMax = 128;
SendMessage(htreewnd, TVM_GETITEM, 0 ,(LPARAM)&tvstruct.item);
hNewItem = (HTREEITEM)SendMessage(htreewnd, TVM_INSERTITEM, 0, (LPARAM)&tvstruct);
// 复制节点数据和状态
TVITEM item;
item.mask = TVIF_PARAM | TVIF_HANDLE | TVIF_STATE;
item.hItem = hItem;
item.stateMask = TVIS_STATEIMAGEMASK;
item.state = 0;
SendMessage(htreewnd, TVM_GETITEM, 0, (LPARAM)&item);
item.hItem = hNewItem;
SendMessage(htreewnd, TVM_SETITEM, 0, (LPARAM)&item);
return hNewItem;
}
HTREEITEM CopyBranch(HWND htreewnd, HTREEITEM htiBranch, HTREEITEM htiNewParent, HTREEITEM htiAfter )
{
HTREEITEM hChild;
HTREEITEM hNewItem = CopyItem(htreewnd, htiBranch, htiNewParent, htiAfter);
hChild = (HTREEITEM)SendMessage(htreewnd, TVM_GETNEXTITEM, TVGN_CHILD,
(LPARAM) htiBranch);
while( hChild != NULL)
{
// 递归地移动所有节点
CopyBranch(htreewnd, hChild, hNewItem, htiAfter);
hChild = (HTREEITEM)SendMessage(htreewnd, TVM_GETNEXTITEM, TVGN_CHILD, (LPARAM)hChild);
}
return hNewItem;
}
一、 一、拖拽消息
拖拽(drag-drop)操作包含这样几步:
1. 1。选中一个节点后,按住鼠标左键(此时是左键拖拽)开始拖动鼠标时,树控件向其父窗口发送TVN_BEGINDRAG通知消息(通过此消息可以得到被拖拽节点的句柄)。此时,需要准备在拖拽操作时所显示的拖拽图标。那么需要向树控件发送TVM_CREATEDRAGIMAGE消息,它创建一个Image List对象,并把被选中节点的图标复制到这个Image List中,并返回Image List句柄。然后使用ImageList_BeginDrag以及ImageList_DragEnter宏对这个Image List做一些设置,比如拖拽时鼠标所处的作标点。
case TVN_BEGINDRAG:
{
POINT pt;
pt.x = 0;
pt.y = 0;
ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &pt);
NMTREEVIEW *lpnmtv = (NMTREEVIEW*) lParam;
hitemDrag = lpnmtv->itemNew.hItem;
hitemDrop = NULL;
himge = (HIMAGELIST)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_CREATEDRAGIMAGE,0,(LPARAM)(HTREEITEM)lpnmtv->itemNew.hItem);
if(!himge)
break;
bdragging = TRUE;
ImageList_BeginDrag(himge, 0, pt.x ,pt.y );
POINT point = lpnmtv->ptDrag;
ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &point);
ImageList_DragEnter(GetDlgItem(hWnd, ID_TREEVIEW),point.x ,point.y );
SetCapture(hWnd);
break;
}
2.2。 拖拽过程中会产生一系列的WM_MOUSEMOVE消息,因此要在这个消息的处理例程中要确定拖拽图标的位置(ImageList_DragMove())以及拖拽目标的位置。对于拖拽目标的确定,一般使用TVM_HITTEST消息来获得拖拽目标节点的句柄。并把这个节点加亮显示(使用带TVGN_DROPHILITE标记的TVM_SELECTITEM消息可以加亮显示拖拽节点)。
case WM_MOUSEMOVE:
{
TVHITTESTINFO tvht;
UINT flags;
flags = (UINT)wParam;
if(bdragging){
pos.x = LOWORD(lParam);
pos.y = HIWORD(lParam);
ClientToScreen(GetDlgItem(hWnd, ID_TREEVIEW), &pos);
ImageList_DragMove(pos.x, pos.y);
ImageList_DragShowNolock(FALSE);
ScreenToClient(GetDlgItem(hWnd, ID_TREEVIEW), &pos);
tvht.pt.x = pos.x;
tvht.pt.y = pos.y;
if(hitemDrop = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW),
TVM_HITTEST, (WPARAM)&flags ,(LPARAM)&tvht))
{
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,
TVGN_DROPHILITE,(LPARAM)hitemDrop);
}
ImageList_DragShowNolock(TRUE);
}
break;
}
3.3。 最后选定拖拽目标节点后,释放鼠标键产生WM_LBUTTONUP消息。在这里,Image List已完成它的任务,应该释放它(ImageList_DragLeave 、ImageList_EndDrag)。然后将源节点插入到目标节点处,并删除源节点。
case WM_LBUTTONUP:
{
if (bdragging){
bdragging = FALSE;
ImageList_DragLeave(GetDlgItem(hWnd, ID_TREEVIEW));
ImageList_EndDrag();
ReleaseCapture();
if( hitemDrag == hitemDrop )
break;
HTREEITEM htiParent = hitemDrop;
while( (htiParent = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREEVIEW),
TVM_GETNEXTITEM,
(WPARAM)(UINT)TVGN_PARENT,
(LPARAM)htiParent )) != NULL )
{
if( htiParent == hitemDrag )
break;
}
DeleteObject(himge);
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_EXPAND,
(WPARAM) (UINT)TVE_EXPAND, (LPARAM)hitemDrop);
HTREEITEM htiNew = CopyBranch(GetDlgItem(hWnd, ID_TREEVIEW),
hitemDrag, hitemDrop, TVI_LAST );
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_DELETEITEM,
0, (LPARAM)hitemDrag);
/*检查最后高亮显示的节点,并选中它。还必须使得其不再高亮显示,否则其它的节点被选中时就不能高亮显示了。*/
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,
TVGN_DROPHILITE,(LPARAM)0);
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SELECTITEM,
TVGN_CARET, (LPARAM)htiNew);
}
break;
}
为了将源节点插入到目标节点处,还需要进行插入操作。为此需要两个函数如下:
char Text[128];
HTREEITEM CopyItem(HWND htreewnd, HTREEITEM hItem, HTREEITEM htiNewParent, HTREEITEM htiAfter /*= TVI_LAST*/ )
{
TV_INSERTSTRUCT tvstruct;
HTREEITEM hNewItem;
// 获得源节点信息
tvstruct.item.hItem = hItem;
tvstruct.item.mask = TVIF_CHILDREN | TVIF_HANDLE |
TVIF_IMAGE | TVIF_SELECTEDIMAGE ;
SendMessage(htreewnd, TVM_GETITEM, 0 ,(LPARAM)&tvstruct.item);
// 在适当的位置上插入节点
tvstruct.hParent = htiNewParent;
tvstruct.hInsertAfter = htiAfter;
tvstruct.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT;
tvstruct.item.pszText = Text;
tvstruct.item.cchTextMax = 128;
SendMessage(htreewnd, TVM_GETITEM, 0 ,(LPARAM)&tvstruct.item);
hNewItem = (HTREEITEM)SendMessage(htreewnd, TVM_INSERTITEM, 0, (LPARAM)&tvstruct);
// 复制节点数据和状态
TVITEM item;
item.mask = TVIF_PARAM | TVIF_HANDLE | TVIF_STATE;
item.hItem = hItem;
item.stateMask = TVIS_STATEIMAGEMASK;
item.state = 0;
SendMessage(htreewnd, TVM_GETITEM, 0, (LPARAM)&item);
item.hItem = hNewItem;
SendMessage(htreewnd, TVM_SETITEM, 0, (LPARAM)&item);
return hNewItem;
}
HTREEITEM CopyBranch(HWND htreewnd, HTREEITEM htiBranch, HTREEITEM htiNewParent, HTREEITEM htiAfter )
{
HTREEITEM hChild;
HTREEITEM hNewItem = CopyItem(htreewnd, htiBranch, htiNewParent, htiAfter);
hChild = (HTREEITEM)SendMessage(htreewnd, TVM_GETNEXTITEM, TVGN_CHILD,
(LPARAM) htiBranch);
while( hChild != NULL)
{
// 递归地移动所有节点
CopyBranch(htreewnd, hChild, hNewItem, htiAfter);
hChild = (HTREEITEM)SendMessage(htreewnd, TVM_GETNEXTITEM, TVGN_CHILD, (LPARAM)hChild);
}
return hNewItem;
}
二、编辑标签消息
可以对树控件节点标签(节点文本)进行编辑,此时控件会在节点文本上产生一个编辑控件(Edit Control)。想要对文本进行编辑并使其生效,必须获得该编辑控件的窗口句柄(发送TVM_GETEDITCONTROL消息)。之后,通过这个窗口句柄获得用户在控件上所编辑修改后的节点文本。最后再把这个节点文本设置进该节点属性中才能使编辑生效(若不然编辑修改后,在节点上显示的文本仍将是原来的文本)。
编辑节点文本只需要处理TVN_BEGINLABELEDIT和TVN_ENDLABELEDIT两个通知消息。用户已选中的节点上再次单击鼠标左键时树控件就会向其父窗口发送TVN_BEGINLABELEDIT通知消息,这时在节点上产生一个编辑控件,那么在处理这个通知消息时通常是获取编辑控件的窗口句柄:
case TVN_BEGINLABELEDIT:
{
//TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)lParam;
hedit =(HWND) SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_GETEDITCONTROL ,0, 0);
break;
}
当用户在节点的编辑控件上编辑完文本后在树控件空白处单击鼠标左键或按Enter键后,树控件向其父窗口发送TVN_ENDLABELEDIT通知消息结束编辑操作。在这里要更新节点文本并使其生效。此外,为了更新节点文本以及其他属性,在选中一个节点时要设法保存被选中节点的节点句柄(HTREEITEM),把它存储在一个全局变量hSelected中。
case TVN_ENDLABELEDIT:
{
char Text[256]="";
tvitem.mask = TVIF_HANDLE | TVIF_TEXT;
tvitem.hItem = hSelected;
SendMessage(GetDlgItem(hWnd,ID_TREECHECK), TVM_GETITEM,
0, (LPARAM)&tvitem);
GetWindowText(hedit, Text, sizeof(Text));
tvitem.pszText = Text;
SendMessage(GetDlgItem(hWnd,ID_TREECHECK), TVM_SETITEM,
0, (LPARAM)&tvitem);
break;
}
此时编辑节点标签操作全部完成。至于获得那个节点句柄的操作可以在NM_CLICK通知消息中发送TVM_HITTEST消息进行处理处理(当然也可以有其他的处理办法):
HTREEITEM hSelected;
case NM_CLICK:
{
UINT uFlags = 0;
POINT pt;
GetCursorPos(&pt);
ScreenToClient(GetDlgItem(hWnd, ID_TREECHECK),&pt);
TVHITTESTINFO hti;
hti.pt = pt;
//Selected必须是全局变量,然后到处理TVN_ENDLABELEDIT消息这个
//变量才有效!!
hSelected = (HTREEITEM)SendMessage(GetDlgItem(hWnd,
ID_TREECHECK),TVM_HITTEST, 0, (LPARAM)&hti);
//……
//……
//其他处理代码
break;
}
编辑节点文本只需要处理TVN_BEGINLABELEDIT和TVN_ENDLABELEDIT两个通知消息。用户已选中的节点上再次单击鼠标左键时树控件就会向其父窗口发送TVN_BEGINLABELEDIT通知消息,这时在节点上产生一个编辑控件,那么在处理这个通知消息时通常是获取编辑控件的窗口句柄:
case TVN_BEGINLABELEDIT:
{
//TV_DISPINFO* pTVDispInfo = (TV_DISPINFO*)lParam;
hedit =(HWND) SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_GETEDITCONTROL ,0, 0);
break;
}
当用户在节点的编辑控件上编辑完文本后在树控件空白处单击鼠标左键或按Enter键后,树控件向其父窗口发送TVN_ENDLABELEDIT通知消息结束编辑操作。在这里要更新节点文本并使其生效。此外,为了更新节点文本以及其他属性,在选中一个节点时要设法保存被选中节点的节点句柄(HTREEITEM),把它存储在一个全局变量hSelected中。
case TVN_ENDLABELEDIT:
{
char Text[256]="";
tvitem.mask = TVIF_HANDLE | TVIF_TEXT;
tvitem.hItem = hSelected;
SendMessage(GetDlgItem(hWnd,ID_TREECHECK), TVM_GETITEM,
0, (LPARAM)&tvitem);
GetWindowText(hedit, Text, sizeof(Text));
tvitem.pszText = Text;
SendMessage(GetDlgItem(hWnd,ID_TREECHECK), TVM_SETITEM,
0, (LPARAM)&tvitem);
break;
}
此时编辑节点标签操作全部完成。至于获得那个节点句柄的操作可以在NM_CLICK通知消息中发送TVM_HITTEST消息进行处理处理(当然也可以有其他的处理办法):
HTREEITEM hSelected;
case NM_CLICK:
{
UINT uFlags = 0;
POINT pt;
GetCursorPos(&pt);
ScreenToClient(GetDlgItem(hWnd, ID_TREECHECK),&pt);
TVHITTESTINFO hti;
hti.pt = pt;
//Selected必须是全局变量,然后到处理TVN_ENDLABELEDIT消息这个
//变量才有效!!
hSelected = (HTREEITEM)SendMessage(GetDlgItem(hWnd,
ID_TREECHECK),TVM_HITTEST, 0, (LPARAM)&hti);
//……
//……
//其他处理代码
break;
}
节点展开/折叠消息
当点击节点左边的加号(+)或减号(-)时,带有子节点的项目就相应展开或折叠起来。这时树控件向其父窗口发送TVN_ITEMEXPANDING通知消息。这个消息的lParam参数是NMTREEVIEW结构指针。它含有两个TVITEM成员itemOld和itemNew,itemOld成员代表刚刚失去焦点的节点而itemNew成员表示正在获得焦点的节点。其中itemNew成员的hItem字段即为正在获得焦点的节点句柄。在处理TVN_ITEMEXPANDING通知消息时就是通过该句柄来对此节点的状态属性进行设置或获取操作的。而NMTREEVIEW结构的action成员用于进一步确定节点是展开还是折叠:当action == TVE_EXPAND节点展开;当action == TVE_COLLAPSE节点折叠。通常节点展开时希望该节点的图标换成展开状态的图标,比如一个表示已打开文件样式的图标等等而结点折叠则用另外的图标表示之,下面的代码就是处理这样的情况:
case TVN_ITEMEXPANDING:
{
lpnmtv = (LPNMTREEVIEW)lParam;
HTREEITEM hSelected = lpnmtv->itemNew.hItem;
if(lpnmtv->action == TVE_EXPAND){
tvitem.mask = TVIF_HANDLE | TVIF_IMAGE |
TVIF_SELECTEDIMAGE | TVIF_STATE;
tvitem.hItem = hSelected;
tvitem.state = 0;
tvitem.stateMask = TVIS_SELECTED;
tvitem.iImage = 3;
tvitem.iSelectedImage = 3;
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SETITEM,
(WPARAM)0, (LPARAM)(const LPTVITEM)&tvitem);
}
else if(lpnmtv->action == TVE_COLLAPSE){
tvitem.mask = TVIF_HANDLE | TVIF_IMAGE |
TVIF_SELECTEDIMAGE | TVIF_STATE;
tvitem.hItem = hSelected;
tvitem.state = 0;
tvitem.stateMask = TVIS_SELECTED;
tvitem.iSelectedImage = 2;
tvitem.iImage = 2;
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SETITEM,
(WPARAM)0, (LPARAM)(const LPTVITEM)&tvitem);
}
break;
}
四、 节点状态图标消息
可以使用TVS_CHECKBOXES样式的树控件使每个节点旁都有一个checkbox,也可以自己手动添加checkbox样式的图标来表示下面的代码负责手动添加checkbox图标:
先在WM_CREATE消息处理中插入节点:
hbitmapcheck = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP2));
himlc = ImageList_Create(13, 13, ILC_COLOR16 , 8, 0);
ImageList_Add(himlc, hbitmapcheck, 0);
DeleteObject(hbitmapcheck);
tvinsert.hParent = NULL;
tvinsert.hInsertAfter = TVI_LAST;
tvinsert.item.mask = TVIF_TEXT | TVIF_HANDLE |TVIF_STATE;
tvinsert.item.hItem = NULL;
tvinsert.item.state = INDEXTOSTATEIMAGEMASK(1);
tvinsert.item.stateMask = TVIS_STATEIMAGEMASK;
tvinsert.item.cchTextMax = 6;
SendMessage(hwndTreeCheck, TVM_SETIMAGELIST,(WPARAM)TVSIL_STATE,
(LPARAM)(HIMAGELIST)himlc);
tvinsert.item.pszText = _T("Item01");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item02");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item03");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item04");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item05");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
然后在NM_CLICK消息中处理当鼠标单击节点的checkbox图标时变化checkbox的状态用于确认节点被选中:
case NM_CLICK:
{
UINT uFlags = 0;
POINT pt;
GetCursorPos(&pt);
ScreenToClient(GetDlgItem(hWnd, ID_TREECHECK),&pt);
TVHITTESTINFO hti;
hti.pt = pt;
TVITEM item;
//Selected必须是全局变量,然后到处理TVN_ENDLABELEDIT消息这个变量才//有效!!
Selected = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREECHECK),
TVM_HITTEST, 0, (LPARAM)&hti);
uFlags = hti.flags;
if( uFlags & TVHT_ONITEMSTATEICON){
item.mask = TVIF_HANDLE | TVIF_STATE;
item.hItem = Selected;
item.stateMask = TVIS_STATEIMAGEMASK;
item.state = 0;
SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_GETITEM,
(WPARAM)0, (LPARAM)&item);
item.state = INDEXTOSTATEIMAGEMASK((item.state>>12) == 1 ? 2 : 1);
item.stateMask = TVIS_STATEIMAGEMASK;
SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_SETITEM,
(WPARAM)0, (LPARAM)&item);
break;
}
case TVN_ITEMEXPANDING:
{
lpnmtv = (LPNMTREEVIEW)lParam;
HTREEITEM hSelected = lpnmtv->itemNew.hItem;
if(lpnmtv->action == TVE_EXPAND){
tvitem.mask = TVIF_HANDLE | TVIF_IMAGE |
TVIF_SELECTEDIMAGE | TVIF_STATE;
tvitem.hItem = hSelected;
tvitem.state = 0;
tvitem.stateMask = TVIS_SELECTED;
tvitem.iImage = 3;
tvitem.iSelectedImage = 3;
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SETITEM,
(WPARAM)0, (LPARAM)(const LPTVITEM)&tvitem);
}
else if(lpnmtv->action == TVE_COLLAPSE){
tvitem.mask = TVIF_HANDLE | TVIF_IMAGE |
TVIF_SELECTEDIMAGE | TVIF_STATE;
tvitem.hItem = hSelected;
tvitem.state = 0;
tvitem.stateMask = TVIS_SELECTED;
tvitem.iSelectedImage = 2;
tvitem.iImage = 2;
SendMessage(GetDlgItem(hWnd, ID_TREEVIEW), TVM_SETITEM,
(WPARAM)0, (LPARAM)(const LPTVITEM)&tvitem);
}
break;
}
四、 节点状态图标消息
可以使用TVS_CHECKBOXES样式的树控件使每个节点旁都有一个checkbox,也可以自己手动添加checkbox样式的图标来表示下面的代码负责手动添加checkbox图标:
先在WM_CREATE消息处理中插入节点:
hbitmapcheck = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP2));
himlc = ImageList_Create(13, 13, ILC_COLOR16 , 8, 0);
ImageList_Add(himlc, hbitmapcheck, 0);
DeleteObject(hbitmapcheck);
tvinsert.hParent = NULL;
tvinsert.hInsertAfter = TVI_LAST;
tvinsert.item.mask = TVIF_TEXT | TVIF_HANDLE |TVIF_STATE;
tvinsert.item.hItem = NULL;
tvinsert.item.state = INDEXTOSTATEIMAGEMASK(1);
tvinsert.item.stateMask = TVIS_STATEIMAGEMASK;
tvinsert.item.cchTextMax = 6;
SendMessage(hwndTreeCheck, TVM_SETIMAGELIST,(WPARAM)TVSIL_STATE,
(LPARAM)(HIMAGELIST)himlc);
tvinsert.item.pszText = _T("Item01");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item02");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item03");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item04");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
tvinsert.item.pszText = _T("Item05");
SendMessage(hwndTreeCheck, TVM_INSERTITEM , 0 ,(LPARAM)&tvinsert);
然后在NM_CLICK消息中处理当鼠标单击节点的checkbox图标时变化checkbox的状态用于确认节点被选中:
case NM_CLICK:
{
UINT uFlags = 0;
POINT pt;
GetCursorPos(&pt);
ScreenToClient(GetDlgItem(hWnd, ID_TREECHECK),&pt);
TVHITTESTINFO hti;
hti.pt = pt;
TVITEM item;
//Selected必须是全局变量,然后到处理TVN_ENDLABELEDIT消息这个变量才//有效!!
Selected = (HTREEITEM)SendMessage(GetDlgItem(hWnd, ID_TREECHECK),
TVM_HITTEST, 0, (LPARAM)&hti);
uFlags = hti.flags;
if( uFlags & TVHT_ONITEMSTATEICON){
item.mask = TVIF_HANDLE | TVIF_STATE;
item.hItem = Selected;
item.stateMask = TVIS_STATEIMAGEMASK;
item.state = 0;
SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_GETITEM,
(WPARAM)0, (LPARAM)&item);
item.state = INDEXTOSTATEIMAGEMASK((item.state>>12) == 1 ? 2 : 1);
item.stateMask = TVIS_STATEIMAGEMASK;
SendMessage(GetDlgItem(hWnd, ID_TREECHECK), TVM_SETITEM,
(WPARAM)0, (LPARAM)&item);
break;
}
用于树控件消息处理的几个结构
众所周知,Windows程序是消息驱动模式,各种消息由Windows操作系统侦测得到,并由用户创建的窗口所取得。这时,操作系统将消息的具体信息包含在两个典型的消息参数里:LPARAM和WPARAM,它们都是32位无符号整型参数UINT类型。其实,奥妙就在这两个参数当中。它们依据不同的消息,包含不同的参数格式。比如,当用户在窗口中点击鼠标左键时,就会发送WM_LBUTTIONDOWN消息。想想看,点击消息一定要包含点击的鼠标位置,那么自然地,这个消息中的LPARAM中包含了POINT数据类型。其中,低字节是X轴坐标,高字节是Y轴坐标,可以通过LOWORD和HIWORD宏取得:
POINT pt;
pt.x = LOWORD(lParam);
pt.y = HIWORD(lParam);
同时,消息参数WPARAM包含一些标志位flags,用于确定在点击鼠标的同时是否还按下了键盘上的功能键等等。
而有时候,有些消息需要传递一些比较大的结构struct,这时32位的无符号整数UINT肯定是装不下的。那么,就把传递的结构指针(地址)放在消息参数当中。当窗口接收到这种消息时,用户负责把消息参数转换成适当的指针类型即可。比如下面将要说明的NMHDR结构。当控件向其父窗口发送通知消息WM_NOTIFY时,消息参数LPARAM就是一个结构的地址指针。那么使用时,可以这样处理:
NMHDR *pnmhdr = (NMHDR *)lParam;
此外,对于控件来说,当控件上发生什么事件时(产生了相应的消息),那么控件会向其父窗口发送通知消息WM_NOTIFY,其中的lParam是NMHDR结构地址。给出这个结构还要进一步确定究竟是什么通知消息,这时code字段包含具体的消息类型。它可以是通用控件的通知消息,以NM_XXX形式命名,也可以是具体控件的通知消息。对于树控件来说它们以TVN_XXX形式命名。
另外,还要对控件进行控制,这些控制操作是通过向控件发送消息来实现。这些消息以TVM_XXX形式命名。
好了,可以简单地说,树控件上发生了某种事件时,树控件会向其父窗口发送通知消息WM_NOTIFY, 它们是TVN_XXX;当需要控制树控件时,通过SendMessage向它发送通知消息,它们是TVM_XXX。
1.NMHDR结构
typedef struct tagNMHDR {
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
该结构包含有关通知消息的信息。
hwndFrom
发送消息的控件窗口句柄。
idFrom
发送消息的控件标识符(ID)。
code
通知码。此成员可以是特殊控件的通知码,例如,TVN_ITEMEXPANDING.。也可以是通用控件的通知码之一。
通用通知码列表如下:
通知码
发送原因
NM_CLICK
用户在控件上单击鼠标左键
NM_DBLCLK
用户在控件上双击鼠标左键
NM_RCLICK
用户在控件上单击鼠标右键
NM_RDBLCLK
用户在控件上双击鼠标右键
NM_RETURN
当控件具有输入焦点时,用户按下回车键
NM_SETFOCUS
控件已经具有输入焦点
NM_KILLFOCUS
控件已经失去焦点
NM_OUTOFMEMORY
因为没有足够的内存可用,控件不能完成指定的操作
此结构一般作为控件通知消息处理函数的参数,例如上面代码中的通知消息处理函数:
afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);
――――――――――――――――――――――――――――――――――――――
2.NM_TREEVIEW结构
typedef struct _NM_TREEVIEW {
NMHDR hdr;
UINT action;
TV_ITEM itemOld;
TV_ITEM itemNew;
POINT ptDrag; }
NM_TREEVIEW;
typedef NM_TREEVIEW FAR *LPNM_TREEVIEW;
此结构包含树控件消息的信息。
此结构中action成员是特定的通知消息位。它表明是由鼠标动作引起的选择操作的改变,还是由键盘动作引起的。比如,当在树控件的小加号/减号(+/-)上点击鼠标左键时,带有子节点的项就要展开或者合拢。那么究竟是展开还是合拢,就要通过这个action字段来区分。当action == TVE_EXPAND时,就展开;当action == TVE_COLLAPSE时就合拢。 而itemOld和itemNew分别是旧结点的状态和新结点的状态。ptDrag是引起消息发送的事件发生时的鼠标在客户区上的坐标。
在通知消息处理函数中一般使用HMHDR指针,例如:
afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);
而有时,甚至是大多数时候,在消息处理函数需要使用NM_TREEVIEW对象,这时可以将消息处理函数传进来的NMHDR指针强制转换为NM_TREEVIEW指针。例如:
void CComDlg::OnSelchangedTreeview(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
HTREEITEM hSelected = pNMTreeView->itemNew.hItem;
if (hSelected != NULL) {
char text[31];
TV_ITEM item;
item.mask = TVIF_HANDLE | TVIF_TEXT;
item.hItem = hSelected;
item.pszText = text;
item.cchTextMax = 30;
VERIFY(pTree->GetItem(&item));
SetDlgItemText(IDC_STATIC_TREEVIEW1,
pTree->GetItemText(hSelected));
}
*pResult = 0;
}
――――――――――――――――――――――――――――――――――――
此函数处理了NM_SELCHANGED消息,在树控件中某项的选择状态被改变后,在一个静态文本框中显示改变后新项的项标签文本。其实这段代码也可以用如下代码代替:
CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
HTREEITEM hSelected = pTree->GetSelectedItem();
if (hSelected != NULL) {
SetDlgItemText(IDC_STATIC_TREEVIEW1,
pTree->GetItemText(hSelected));
}
pResult = 0;
――――――――――――――――――――――――――――――――――――
区别是前一段代码中选择在程序栈中为项标签文本缓冲区分配内存,而后一段代码中选择在程序堆中为项标签文本分配内存。
POINT pt;
pt.x = LOWORD(lParam);
pt.y = HIWORD(lParam);
同时,消息参数WPARAM包含一些标志位flags,用于确定在点击鼠标的同时是否还按下了键盘上的功能键等等。
而有时候,有些消息需要传递一些比较大的结构struct,这时32位的无符号整数UINT肯定是装不下的。那么,就把传递的结构指针(地址)放在消息参数当中。当窗口接收到这种消息时,用户负责把消息参数转换成适当的指针类型即可。比如下面将要说明的NMHDR结构。当控件向其父窗口发送通知消息WM_NOTIFY时,消息参数LPARAM就是一个结构的地址指针。那么使用时,可以这样处理:
NMHDR *pnmhdr = (NMHDR *)lParam;
此外,对于控件来说,当控件上发生什么事件时(产生了相应的消息),那么控件会向其父窗口发送通知消息WM_NOTIFY,其中的lParam是NMHDR结构地址。给出这个结构还要进一步确定究竟是什么通知消息,这时code字段包含具体的消息类型。它可以是通用控件的通知消息,以NM_XXX形式命名,也可以是具体控件的通知消息。对于树控件来说它们以TVN_XXX形式命名。
另外,还要对控件进行控制,这些控制操作是通过向控件发送消息来实现。这些消息以TVM_XXX形式命名。
好了,可以简单地说,树控件上发生了某种事件时,树控件会向其父窗口发送通知消息WM_NOTIFY, 它们是TVN_XXX;当需要控制树控件时,通过SendMessage向它发送通知消息,它们是TVM_XXX。
1.NMHDR结构
typedef struct tagNMHDR {
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
该结构包含有关通知消息的信息。
hwndFrom
发送消息的控件窗口句柄。
idFrom
发送消息的控件标识符(ID)。
code
通知码。此成员可以是特殊控件的通知码,例如,TVN_ITEMEXPANDING.。也可以是通用控件的通知码之一。
通用通知码列表如下:
通知码
发送原因
NM_CLICK
用户在控件上单击鼠标左键
NM_DBLCLK
用户在控件上双击鼠标左键
NM_RCLICK
用户在控件上单击鼠标右键
NM_RDBLCLK
用户在控件上双击鼠标右键
NM_RETURN
当控件具有输入焦点时,用户按下回车键
NM_SETFOCUS
控件已经具有输入焦点
NM_KILLFOCUS
控件已经失去焦点
NM_OUTOFMEMORY
因为没有足够的内存可用,控件不能完成指定的操作
此结构一般作为控件通知消息处理函数的参数,例如上面代码中的通知消息处理函数:
afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);
――――――――――――――――――――――――――――――――――――――
2.NM_TREEVIEW结构
typedef struct _NM_TREEVIEW {
NMHDR hdr;
UINT action;
TV_ITEM itemOld;
TV_ITEM itemNew;
POINT ptDrag; }
NM_TREEVIEW;
typedef NM_TREEVIEW FAR *LPNM_TREEVIEW;
此结构包含树控件消息的信息。
此结构中action成员是特定的通知消息位。它表明是由鼠标动作引起的选择操作的改变,还是由键盘动作引起的。比如,当在树控件的小加号/减号(+/-)上点击鼠标左键时,带有子节点的项就要展开或者合拢。那么究竟是展开还是合拢,就要通过这个action字段来区分。当action == TVE_EXPAND时,就展开;当action == TVE_COLLAPSE时就合拢。 而itemOld和itemNew分别是旧结点的状态和新结点的状态。ptDrag是引起消息发送的事件发生时的鼠标在客户区上的坐标。
在通知消息处理函数中一般使用HMHDR指针,例如:
afx_msg void OnTreeDblClk(NMHDR * pNMHDR, LRESULT *pResult);
而有时,甚至是大多数时候,在消息处理函数需要使用NM_TREEVIEW对象,这时可以将消息处理函数传进来的NMHDR指针强制转换为NM_TREEVIEW指针。例如:
void CComDlg::OnSelchangedTreeview(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
HTREEITEM hSelected = pNMTreeView->itemNew.hItem;
if (hSelected != NULL) {
char text[31];
TV_ITEM item;
item.mask = TVIF_HANDLE | TVIF_TEXT;
item.hItem = hSelected;
item.pszText = text;
item.cchTextMax = 30;
VERIFY(pTree->GetItem(&item));
SetDlgItemText(IDC_STATIC_TREEVIEW1,
pTree->GetItemText(hSelected));
}
*pResult = 0;
}
――――――――――――――――――――――――――――――――――――
此函数处理了NM_SELCHANGED消息,在树控件中某项的选择状态被改变后,在一个静态文本框中显示改变后新项的项标签文本。其实这段代码也可以用如下代码代替:
CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
HTREEITEM hSelected = pTree->GetSelectedItem();
if (hSelected != NULL) {
SetDlgItemText(IDC_STATIC_TREEVIEW1,
pTree->GetItemText(hSelected));
}
pResult = 0;
――――――――――――――――――――――――――――――――――――
区别是前一段代码中选择在程序栈中为项标签文本缓冲区分配内存,而后一段代码中选择在程序堆中为项标签文本分配内存。
基于TCP/IP的多线程通信及其在远程监控系统中的应用
传统的应用程序都是单线程的,即在程序运行期间,由单个线程独占CPU的控制权,负责执行所有任务。在这种情况下,程序在执行一些比较费时的任务时,就无法及时响应用户的操作,影响了应用程序的实时性能。在监控系统,特别是远程监控系统中,应用程序往往不但要及时把监控对象的最新信息反馈给监视客户(通过图形显示),还要处理本地机与远程机之间的通信以及对控制对象的实时控制等任务,这时,仅仅由单个线程来完成所有任务,显然无法满足监控系统的实时性要求。在DOS系统下,这些工作可以由中断来完成。而在Windows NT下,中断机制对用户是不透明的。为此,可引进多线程机制,主线程专门负责消息的响应,使程序能够响应命令和其他事件。辅助线程可以用于完成其他比较费时的工作,如通信、图形显示和后台打印等,这样就不至于影响主线程的运行。
1 Windows NT 多线程概述
Windows NT是一个真正的抢占式多任务操作系统。在 Windows NT中,启动一个应用程序就是启动该应用程序的一个实例,即进程。进程由一个或多个线程构成,拥有内存和资源,但自己不能执行自己,而是进程中的线程被调度执行。进程至少要有一个线程,当创建一个进程时,就创建了一个线程,即主线程。主线程可以创建其他辅助线程,由主线程创建的线程又可创建线程。每个线程都可指定优先级,操作系统根据线程的优先级调度线程的执行。
Windows NT中使用多线程的方法有三种:
· 使用C多线程库函数;
· 使用CreateThread() 等Win32函数;
· 使用MFC类。
本文采用第三种方法。在Visual C++5.0 中,MFC应用程序用CWinThread 对象表示线程。基本操作如下:
· 创建新线程:调用MFC全局函数AfxBeginThread ()创建新线程。AfxBeginThread()启动新线程并返回控制,然后,新线程和调用AfxBeginThread()的线程同时运行。它的返回值为指向CWinThread对象的指针;
· 暂停/恢复线程:调用CWinThread类成员函数SuspendThread()暂停线程的运行,调用ResumeThread()成员函数恢复线程的运行;
· 终止线程:在线程内部可调用全局函数AfxBeginThread()终止线程的运行,否则,线程执行结束后,线程自动从线程函数返回并释放线程占有的资源。
2 基于TCP/IP的多线程编程
TCP/IP是lnternet上广泛使用的一种协议,可用于异种机之间的互联。TCP/IP协议本身是非常复杂的,然而在网络编程中,程序员不必考虑 TCP/IP的实现细节,只需利用协议的网络编程接口Socket(亦称套接字)即可。在 Windows 中,网络编程接口是 Windows Socket它包含标准的Berkley Sockets的功能调用的集合,以及为 Windows 所做的一些扩展。TCP/IP协议的应用一般采用客户/服务器模式,面向连接的应用调用如图1所示。
根据上述顺序调用函数建立连接后,通信双方便可交换数据。然而,在调用带*号的函数时,操作常会阻塞,特别是当套接字工作在同步阻塞模式(Blocking Mode)时。这时,程序无法响应任何消息。为了避免出现这种情况,本文引进辅助线程。在执行含有可能阻塞的函数的任务时,动态创建新的线程,专门处理该任务。主线程把任务交给辅助线程后,不再对辅助线程加以控制与调度。本文分别针对connect()、accept()、receive()、send ()等可能阻塞的函数创建了相应的线程,如表1所示。
多线程编程常常还要考虑线程间的通信。线程间的通信可以采用全局变量、指针参数和文件映射等方式。本文采用指针参数方式。在调用AfxBeginThread()函数时,通过传递指针参数的方式在主线程与辅助线程间通信。
AfxBeginThread()函数的用法如下:
CWinThread*AfxBeginThread (AFX_THREADPROC pfnThreadproc,
LPVOID pParam,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSixe=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES pSecurityAttrs=NULL);
参数pfnThreadProc指定线程函数必须如下定义:
UINT MyControllingFunction(LPVOID pParam);
参数pParam 是调用线程传递给线程函数pfThreadProc的参数;
其他参数一般只需采用缺省值。
指针参数通信方式就是通过参数pParam在线程间通信的,它可为指向任何数据类型的指针。本文中,定义了一个名叫EXCHANGE_INFO的结构如下:
typedef struct
{ SOCKET sServerSocket;
SOCKET *pcCoientSocket;
SOCKADDR_IN *pClientAddr;
BOOL *pbConnected;
unsigned char *pucBuffer;
int *pnMessageLen;
} EXCHANGE_INFO;
在需要通信时,先声明一个结构变量,再把变量的指针作为pParam参数,调用AfxBeginThread((AFX_THREADPROC) CSocketThread::WaitFor ConnectThread,(LPVOID)& m_Exchangeinfo)函数即可。
为了利用面向对象技术编程所具有的模块性强、便于修改、可移植性好等优点,本文还把表1中的线程封装为父类为CWinThread的自定义类 CSocketThread中。还自定义了一个叫CSocketComm的新类,封装了一些函数,如CreateSocket、 ConnectToServer、WaitForClient、ReadMessage、SendMessage等,这些函数屏蔽了面向连接的通信程序的实现细节,如创建、连接、发送和接收等,在这些函数里,动态创建辅助线程。
下面以CSocketComm类中的等待客户连接请求的函数WaitForClient()为例,注释说明多线程编程的具体细节。
BOOL CSocketComm::WaitForClient
{
if(m_bConnected)return( TRUE );
//配置bind函数的参数即服务器的套接字地址结构
SOCKADDR_IN Addr;
memset(&Addr,0,sizeof(SOCKADDR_IN));
Addr.sin_family=AF_INET;
ADDR.SIN_port= htonl(m_nPort);
Addr.sin_addr.s_addrr = htonl(INADDR_ANY);
//将套接字地址结构赋予套接字(绑定),以指定本地半相关
int nReturnValue;
nReturnValue =::bind( m_sSserverSocket,( LPSOCKADDR)&Addr,sizeof (SOCKADDR_IN ));
if(nReturnValue == SOCKET_ERROR) returu( FALSE );
//配置传给WaitForConnectThread线程函数的参数m_Exchangeinfo
m_Exchangeinfo.sServerSocket = m_serverSocket;
m_Exchangeinfo.psClientSocket = &m_sClientSocket;
m_Exchangeinfo.pClientAddr = &m_lientAddr;
m_Exchangeinfo.pbConnected = &m_bConnected;
//以m_Exchangeinfo的指针为参数调用WaitforConnectThread线程等待客户端连接
AfxBeginThread((AFX_THREADPROC)CSocketThread::
WaitForConnectThread,(LPVOID) &m_Exchanginfo);
returi( TRUE )
}
//等待连接线程
UINT CSocketThread::WaitForConnectThread(LPVOIDpParam)
{
EXCHANGE_INFO*pExchangelnfo=(EXCHANGE_INFO*) pParam;
int nReturnValue, nClientAddrSize= Sizeof( SOCKADDR_IN);
//侦听连接
nReturnValue=:: listen(pExchangelnfo ->sServerSocket, 1);
if( nReturnValue == SOCKET_ERROR )return(0);
//阻塞调用accept,直至有客户连接请求
*pExchangelnfo->psClitentSocket=:: accept(pExchangelnfo->sServerSocket, (LPSOCKADDR) pEchangelnfo ->pClientAddr,&nClientAddrSize);
if(( *pExchangelnfo->psClitentSocket)!= INVALID_SOCKET)
//通过pExchangelnfo的指针在线程间通信
* pExchangelnfo->pbConnected TRUE;
return( 0 );
3 应用实例-高层协议的设计
在电厂和电站中,为了保证安全工作,保护系统必不可少。保护系统的电源供应通常使用两种方式。一般情况下,使用交流电系统对保护系统进行供电;当交流电系统出现故障时立即使用后备的蓄电池系统对保护系统进行供电。为了对蓄电池系统进行监控和管理,以保证蓄电池在关键时刻能正常工作,设计了在 Windows NT环境下具有远程通讯功能和动态人机界面的智能蓄电池远程监控系统。该系统由蓄电池智能管理、充电机控制、母线绝缘在线检测、声光报警、系统组态、远程通信等子系统组成,实现对蓄电池/充电机智能化远程管理和控制,对整个系统的运行状态进行实时监控,具有多媒体报警、事件处理、动态数据库、趋势画面和动态画面显示、操作提前提醒等功能。系统框图如图2所示。在远程通信模块中,远程监控机需把监控客户的操作命令及时传给本地机,本地机根据命令控制充电机,使之按照一定的方式工作,而本地机需定时向远程监控机反馈实时的充电机状态信息。它们之间的通信是基于TCP/IP的广域网通信,而且,我们引进了多线程机制以保证系统具有良好的实时性。
下面以其中的充电机控制系统为例谈谈如何使用CSocketComm类进行远程通信。为简单起见,假定本地机与远程监控机之间通信的信息仅有下面三种类型:
·本地机接收到该命令后,控制充电机按照稳压模式运行,输出电压为电压给定值;
·本地机接收到该命令后,控制充电机按照稳流定时模式运行,输出电流为电流给定值;
·本地机向远程监控机发送充电机的实时状态数据(包括输出电压、输出电流、状态指示和故障类型指示)。
在基于TCP/IP的面向连接的网络通信中,客户与服务器之间传送的是有序可靠的字节流(Byte Stream),所以程序员有必要在传输层TCP上定义自己的高层协议,设计帧结构,将字节流变成有意义的信息。在CSocketComm类中由 AssembleMessage()函数把数据组合成一定的帧结构。帧结构为:
其中@为帧起始标志,#为帧终结标志
对应的结构定义如下:
typedef struct
{ int MessageType; //信息类型
int ChargerNo; //充电机编号
int DataNo; //数据类型
float Data; //数据
} MessageStruct;
需要通信时,先声明一个 MessageStruct变量,根据信息内容对各成员变量赋值,传给 AssembleMessage()函数组合成帧,再调用SendMessage()函数发送给接受方。接受方接到数据后,对数据内容的解释,是由 CsocketComm类中的AnalyzeMessage()函数完成的。AnalyzeMessage()函数返回一个 MessageStruct变量。应用程序就可根据它的各成员变量控制充电机或动态显示充电机的状态。
总之,把多线程机制引进通信,有利于提高应用程序的实时性,充分利用系统资源。对于大型的工程应用来说,不同的线程完成不同的任务,也有利于提高程序的模块化,便于维护和扩展。本文给出了一种在Windows NT下基于TCP/IP协议的多线程通信的基本方法,根据该方法进行修改和扩充,便可设计出符合具体应用的高质量的多线程通信程序。
1 Windows NT 多线程概述
Windows NT是一个真正的抢占式多任务操作系统。在 Windows NT中,启动一个应用程序就是启动该应用程序的一个实例,即进程。进程由一个或多个线程构成,拥有内存和资源,但自己不能执行自己,而是进程中的线程被调度执行。进程至少要有一个线程,当创建一个进程时,就创建了一个线程,即主线程。主线程可以创建其他辅助线程,由主线程创建的线程又可创建线程。每个线程都可指定优先级,操作系统根据线程的优先级调度线程的执行。
Windows NT中使用多线程的方法有三种:
· 使用C多线程库函数;
· 使用CreateThread() 等Win32函数;
· 使用MFC类。
本文采用第三种方法。在Visual C++5.0 中,MFC应用程序用CWinThread 对象表示线程。基本操作如下:
· 创建新线程:调用MFC全局函数AfxBeginThread ()创建新线程。AfxBeginThread()启动新线程并返回控制,然后,新线程和调用AfxBeginThread()的线程同时运行。它的返回值为指向CWinThread对象的指针;
· 暂停/恢复线程:调用CWinThread类成员函数SuspendThread()暂停线程的运行,调用ResumeThread()成员函数恢复线程的运行;
· 终止线程:在线程内部可调用全局函数AfxBeginThread()终止线程的运行,否则,线程执行结束后,线程自动从线程函数返回并释放线程占有的资源。
2 基于TCP/IP的多线程编程
TCP/IP是lnternet上广泛使用的一种协议,可用于异种机之间的互联。TCP/IP协议本身是非常复杂的,然而在网络编程中,程序员不必考虑 TCP/IP的实现细节,只需利用协议的网络编程接口Socket(亦称套接字)即可。在 Windows 中,网络编程接口是 Windows Socket它包含标准的Berkley Sockets的功能调用的集合,以及为 Windows 所做的一些扩展。TCP/IP协议的应用一般采用客户/服务器模式,面向连接的应用调用如图1所示。
根据上述顺序调用函数建立连接后,通信双方便可交换数据。然而,在调用带*号的函数时,操作常会阻塞,特别是当套接字工作在同步阻塞模式(Blocking Mode)时。这时,程序无法响应任何消息。为了避免出现这种情况,本文引进辅助线程。在执行含有可能阻塞的函数的任务时,动态创建新的线程,专门处理该任务。主线程把任务交给辅助线程后,不再对辅助线程加以控制与调度。本文分别针对connect()、accept()、receive()、send ()等可能阻塞的函数创建了相应的线程,如表1所示。
多线程编程常常还要考虑线程间的通信。线程间的通信可以采用全局变量、指针参数和文件映射等方式。本文采用指针参数方式。在调用AfxBeginThread()函数时,通过传递指针参数的方式在主线程与辅助线程间通信。
AfxBeginThread()函数的用法如下:
CWinThread*AfxBeginThread (AFX_THREADPROC pfnThreadproc,
LPVOID pParam,
int nPriority=THREAD_PRIORITY_NORMAL,
UINT nStackSixe=0,
DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES pSecurityAttrs=NULL);
参数pfnThreadProc指定线程函数必须如下定义:
UINT MyControllingFunction(LPVOID pParam);
参数pParam 是调用线程传递给线程函数pfThreadProc的参数;
其他参数一般只需采用缺省值。
指针参数通信方式就是通过参数pParam在线程间通信的,它可为指向任何数据类型的指针。本文中,定义了一个名叫EXCHANGE_INFO的结构如下:
typedef struct
{ SOCKET sServerSocket;
SOCKET *pcCoientSocket;
SOCKADDR_IN *pClientAddr;
BOOL *pbConnected;
unsigned char *pucBuffer;
int *pnMessageLen;
} EXCHANGE_INFO;
在需要通信时,先声明一个结构变量,再把变量的指针作为pParam参数,调用AfxBeginThread((AFX_THREADPROC) CSocketThread::WaitFor ConnectThread,(LPVOID)& m_Exchangeinfo)函数即可。
为了利用面向对象技术编程所具有的模块性强、便于修改、可移植性好等优点,本文还把表1中的线程封装为父类为CWinThread的自定义类 CSocketThread中。还自定义了一个叫CSocketComm的新类,封装了一些函数,如CreateSocket、 ConnectToServer、WaitForClient、ReadMessage、SendMessage等,这些函数屏蔽了面向连接的通信程序的实现细节,如创建、连接、发送和接收等,在这些函数里,动态创建辅助线程。
下面以CSocketComm类中的等待客户连接请求的函数WaitForClient()为例,注释说明多线程编程的具体细节。
BOOL CSocketComm::WaitForClient
{
if(m_bConnected)return( TRUE );
//配置bind函数的参数即服务器的套接字地址结构
SOCKADDR_IN Addr;
memset(&Addr,0,sizeof(SOCKADDR_IN));
Addr.sin_family=AF_INET;
ADDR.SIN_port= htonl(m_nPort);
Addr.sin_addr.s_addrr = htonl(INADDR_ANY);
//将套接字地址结构赋予套接字(绑定),以指定本地半相关
int nReturnValue;
nReturnValue =::bind( m_sSserverSocket,( LPSOCKADDR)&Addr,sizeof (SOCKADDR_IN ));
if(nReturnValue == SOCKET_ERROR) returu( FALSE );
//配置传给WaitForConnectThread线程函数的参数m_Exchangeinfo
m_Exchangeinfo.sServerSocket = m_serverSocket;
m_Exchangeinfo.psClientSocket = &m_sClientSocket;
m_Exchangeinfo.pClientAddr = &m_lientAddr;
m_Exchangeinfo.pbConnected = &m_bConnected;
//以m_Exchangeinfo的指针为参数调用WaitforConnectThread线程等待客户端连接
AfxBeginThread((AFX_THREADPROC)CSocketThread::
WaitForConnectThread,(LPVOID) &m_Exchanginfo);
returi( TRUE )
}
//等待连接线程
UINT CSocketThread::WaitForConnectThread(LPVOIDpParam)
{
EXCHANGE_INFO*pExchangelnfo=(EXCHANGE_INFO*) pParam;
int nReturnValue, nClientAddrSize= Sizeof( SOCKADDR_IN);
//侦听连接
nReturnValue=:: listen(pExchangelnfo ->sServerSocket, 1);
if( nReturnValue == SOCKET_ERROR )return(0);
//阻塞调用accept,直至有客户连接请求
*pExchangelnfo->psClitentSocket=:: accept(pExchangelnfo->sServerSocket, (LPSOCKADDR) pEchangelnfo ->pClientAddr,&nClientAddrSize);
if(( *pExchangelnfo->psClitentSocket)!= INVALID_SOCKET)
//通过pExchangelnfo的指针在线程间通信
* pExchangelnfo->pbConnected TRUE;
return( 0 );
3 应用实例-高层协议的设计
在电厂和电站中,为了保证安全工作,保护系统必不可少。保护系统的电源供应通常使用两种方式。一般情况下,使用交流电系统对保护系统进行供电;当交流电系统出现故障时立即使用后备的蓄电池系统对保护系统进行供电。为了对蓄电池系统进行监控和管理,以保证蓄电池在关键时刻能正常工作,设计了在 Windows NT环境下具有远程通讯功能和动态人机界面的智能蓄电池远程监控系统。该系统由蓄电池智能管理、充电机控制、母线绝缘在线检测、声光报警、系统组态、远程通信等子系统组成,实现对蓄电池/充电机智能化远程管理和控制,对整个系统的运行状态进行实时监控,具有多媒体报警、事件处理、动态数据库、趋势画面和动态画面显示、操作提前提醒等功能。系统框图如图2所示。在远程通信模块中,远程监控机需把监控客户的操作命令及时传给本地机,本地机根据命令控制充电机,使之按照一定的方式工作,而本地机需定时向远程监控机反馈实时的充电机状态信息。它们之间的通信是基于TCP/IP的广域网通信,而且,我们引进了多线程机制以保证系统具有良好的实时性。
下面以其中的充电机控制系统为例谈谈如何使用CSocketComm类进行远程通信。为简单起见,假定本地机与远程监控机之间通信的信息仅有下面三种类型:
·本地机接收到该命令后,控制充电机按照稳压模式运行,输出电压为电压给定值;
·本地机接收到该命令后,控制充电机按照稳流定时模式运行,输出电流为电流给定值;
·本地机向远程监控机发送充电机的实时状态数据(包括输出电压、输出电流、状态指示和故障类型指示)。
在基于TCP/IP的面向连接的网络通信中,客户与服务器之间传送的是有序可靠的字节流(Byte Stream),所以程序员有必要在传输层TCP上定义自己的高层协议,设计帧结构,将字节流变成有意义的信息。在CSocketComm类中由 AssembleMessage()函数把数据组合成一定的帧结构。帧结构为:
其中@为帧起始标志,#为帧终结标志
对应的结构定义如下:
typedef struct
{ int MessageType; //信息类型
int ChargerNo; //充电机编号
int DataNo; //数据类型
float Data; //数据
} MessageStruct;
需要通信时,先声明一个 MessageStruct变量,根据信息内容对各成员变量赋值,传给 AssembleMessage()函数组合成帧,再调用SendMessage()函数发送给接受方。接受方接到数据后,对数据内容的解释,是由 CsocketComm类中的AnalyzeMessage()函数完成的。AnalyzeMessage()函数返回一个 MessageStruct变量。应用程序就可根据它的各成员变量控制充电机或动态显示充电机的状态。
总之,把多线程机制引进通信,有利于提高应用程序的实时性,充分利用系统资源。对于大型的工程应用来说,不同的线程完成不同的任务,也有利于提高程序的模块化,便于维护和扩展。本文给出了一种在Windows NT下基于TCP/IP协议的多线程通信的基本方法,根据该方法进行修改和扩充,便可设计出符合具体应用的高质量的多线程通信程序。
VC中利用多线程技术实现线程之间的通信
本文章地址:http://www.jztop.com/dev/vc/20060206/11217.html [点此复制地址]
当前流行的Windows操作系统能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线 程,线程提供了多任务处理的能力。用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义。现在 的大型应用软件无一不是多线程多任务处理,单线程的软件是不可想象的。因此掌握多线程多任务设计方法对每个程序员都是必需要掌握的。本实例针对多线程技术 在应用中经常遇到的问题,如线程间的通信、同步等,分别进行探讨,并利用多线程技术进行线程之间的通信,实现了数字的简单排序。
一、 实现方法
1、理解线程
要讲解线程,不得不说一下进程,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成。进程在运行时创建的资源随 着进程的终止而死亡。线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应于Visual C++中的CwinThread类对象。单独一个执行程序运行时,缺省地包含的一个主线程,主线程以函数地址的形式出现,提供程序的启动点,如main ()或WinMain()函数等。当主线程终止时,进程也随之终止。根据实际需要,应用程序可以分解成许多独立执行的线程,每个线程并行的运行在同一进程 中。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间 片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程 在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。
线程 被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程 CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进程终止。工作线程用来执行程序的后台处 理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CWinThread类派生来创建,对它来说最重要的是如何实现工作线程 任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同 时每个线程可以拥有自己的变量。
2、线程的管理和操作
(一)线程的启动
创建一个用户界 面线程,首先要从类CwinThread产生一个派生类,同时必须使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE来声 明和实现这个CwinThread派生类。第二步是根据需要重载该派生类的一些成员函数如:ExitInstance()、InitInstance ()、OnIdle()、PreTranslateMessage()等函数。最后调用AfxBeginThread()函数的一个版本: CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ) 启动该用户界面线程,其中第一个参数为指向定义的用户界面线程类指针变量,第二个参数为线程的优先级,第三个参数为线程所对应的堆栈大小,第四个参数为线 程创建时的附加标志,缺省为正常状态,如为CREATE_SUSPENDED则线程启动后为挂起状态。
对于工作线程来说,启动一个线 程,首先需要编写一个希望与应用程序的其余部分并行运行的函数如Fun1(),接着定义一个指向CwinThread对象的指针变量*pThread,调 用AfxBeginThread(Fun1,param,priority)函数,返回值赋给pThread变量的同时一并启动该线程来执行上面的 Fun1()函数,其中Fun1是线程要运行的函数的名字,也既是上面所说的控制函数的名字,param是准备传送给线程函数Fun1的任意32位值, priority则是定义该线程的优先级别,它是预定义的常数,读者可参考MSDN。
(二)线程的优先级
以下的CwinThread类的成员函数用于线程优先级的操作:
上述的二个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次而言的,处于同一优先权层次的线程,优先级高的线程先运 行;处于不同优先权层次上的线程,谁的优先权层次高,谁先运行。至于优先级设置所需的常数,自己参考MSDN就可以了,要注意的是要想设置线程的优先级, 这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。对于线程的优先权层次的设置,CwinThread类没有提供相应的函 数,但是可以通过Win32 SDK函数GetPriorityClass()和SetPriorityClass()来实现。
(三)线程的悬挂和恢复
CWinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数,其中SuspendThread()用来悬挂线程,暂停线程的执行; ResumeThread()用来恢复线程的执行。如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的 ResumeThread()来恢复线程的运行。
(四)结束线程
终止线程有三种途径,线程可以在自身内部调用 AfxEndThread()来终止自身的运行;可以在线程的外部调用BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )来强行终止一个线程的运行,然后调用CloseHandle()函数释放线程所占用的堆栈;第三种方法是改变全局变量,使线程的执行函数返回,则该线程 终止。下面以第三种方法为例,给出部分代码:
3、线程之间的通信
通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种 方法实现这种通信任务:使用全局变量(上一节的例子其实使用的就是这种方法)、使用事件对象、使用消息。这里我们主要介绍后两种方法。
(一) 利用用户定义的消息通信
在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的非 常简单。首先用户要定义一个用户消息,如下所示:#define WM_USERMSG WMUSER+100;在需要的时候,在一个线程中调用::PostMessage((HWND)param,WM_USERMSG,0,0)或 CwinThread::PostThradMessage()来向另外一个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的目的窗口的句 柄、要发送的消息标志符、消息的参数WPARAM和LPARAM。下面的代码是对上节代码的修改,修改后的结果是在线程结束时显示一个对话框,提示线程结 束:
上面的例子是工作者线程向用户界面线程发送消息,对于工作者线程,如果它的设计模式也是消息驱动的,那么调用者可以向它发送初始化、退出、执行某种特定 的处理等消息,让它在后台完成。在控制函数中可以直接使用::GetMessage()这个SDK函数进行消息分检和处理,自己实现一个消息循环。 GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的时间片让给其它线程,不无效的占用CPU的时间,如果消息队列不为空, 就获取这个消息,判断这个消息的内容并进行相应的处理。
(二)用事件对象实现通信
在线程之间传递信号进行通信比较复杂的方法是使用事件对象,用MFC的Cevent类的对象来表示。事件对象处于两种状态之一:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。上述例子代码修改如下:
运行这个程序,当关闭程序时,才显示提示框,显示"Thread ended"。
4、线程之间的同步
前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏 数据的完整性。保证各个线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC支持的七个多线程的同步类可以分成两大类:同步对象 (CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象 (CmultiLock和CsingleLock)。本节主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调工作,程序运行起来更安全。
(一) 临界区
临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都 可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数 据。临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成 之后,调用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。同时启动两个线程,它 们对应的函数分别为WriteThread()和ReadThread(),用以对公共数组组array[]操作,下面的代码说明了如何使用临界区对象:
上述代码运行的结果应该是Destarray数组中的元素分别为1-9,而不是杂乱无章的数,如果不使用同步,则不是这个结果,有兴趣的读者可以实验一下。
(二)互斥
互斥与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互 斥与Cmutex类的对象相对应,使用互斥对象时,必须创建一个CSingleLock或CMultiLock对象,用于实际的访问控制,因为这里的例子 只处理单个互斥,所以我们可以使用CSingleLock对象,该对象的Lock()函数用于占有互斥,Unlock()用于释放互斥。实现代码如下:
(三)信号量
信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,创建一个信号量需要用Csemaphore类声明一个对象,一旦 创建了一个信号量对象,就可以用它来对资源的访问技术。要实现计数处理,先创建一个CsingleLock或CmltiLock对象,然后用该对象的 Lock()函数减少这个信号量的计数值,Unlock()反之。下面的代码分别启动三个线程,执行时同时显示二个消息框,然后10秒后第三个消息框才得 以显示。
四、 小结
对复杂的应用程序来说,线程的应用给应用程序提供了高效、快速、安全的数据处理能力。本实例讲述了线程处理中经常遇到的问题,希望对读者朋友有一定的帮助,起到抛砖引玉的作用。
一、 实现方法
1、理解线程
要讲解线程,不得不说一下进程,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成。进程在运行时创建的资源随 着进程的终止而死亡。线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应于Visual C++中的CwinThread类对象。单独一个执行程序运行时,缺省地包含的一个主线程,主线程以函数地址的形式出现,提供程序的启动点,如main ()或WinMain()函数等。当主线程终止时,进程也随之终止。根据实际需要,应用程序可以分解成许多独立执行的线程,每个线程并行的运行在同一进程 中。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间 片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程 在计算机中是并行处理的。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。
线程 被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程 CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进程终止。工作线程用来执行程序的后台处 理任务,比如计算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CWinThread类派生来创建,对它来说最重要的是如何实现工作线程 任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同 时每个线程可以拥有自己的变量。
2、线程的管理和操作
(一)线程的启动
创建一个用户界 面线程,首先要从类CwinThread产生一个派生类,同时必须使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE来声 明和实现这个CwinThread派生类。第二步是根据需要重载该派生类的一些成员函数如:ExitInstance()、InitInstance ()、OnIdle()、PreTranslateMessage()等函数。最后调用AfxBeginThread()函数的一个版本: CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ) 启动该用户界面线程,其中第一个参数为指向定义的用户界面线程类指针变量,第二个参数为线程的优先级,第三个参数为线程所对应的堆栈大小,第四个参数为线 程创建时的附加标志,缺省为正常状态,如为CREATE_SUSPENDED则线程启动后为挂起状态。
对于工作线程来说,启动一个线 程,首先需要编写一个希望与应用程序的其余部分并行运行的函数如Fun1(),接着定义一个指向CwinThread对象的指针变量*pThread,调 用AfxBeginThread(Fun1,param,priority)函数,返回值赋给pThread变量的同时一并启动该线程来执行上面的 Fun1()函数,其中Fun1是线程要运行的函数的名字,也既是上面所说的控制函数的名字,param是准备传送给线程函数Fun1的任意32位值, priority则是定义该线程的优先级别,它是预定义的常数,读者可参考MSDN。
(二)线程的优先级
以下的CwinThread类的成员函数用于线程优先级的操作:
| int GetThreadPriority(); BOOL SetThradPriority()(int nPriority); |
上述的二个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次而言的,处于同一优先权层次的线程,优先级高的线程先运 行;处于不同优先权层次上的线程,谁的优先权层次高,谁先运行。至于优先级设置所需的常数,自己参考MSDN就可以了,要注意的是要想设置线程的优先级, 这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。对于线程的优先权层次的设置,CwinThread类没有提供相应的函 数,但是可以通过Win32 SDK函数GetPriorityClass()和SetPriorityClass()来实现。
(三)线程的悬挂和恢复
CWinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数,其中SuspendThread()用来悬挂线程,暂停线程的执行; ResumeThread()用来恢复线程的执行。如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的 ResumeThread()来恢复线程的运行。
(四)结束线程
终止线程有三种途径,线程可以在自身内部调用 AfxEndThread()来终止自身的运行;可以在线程的外部调用BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )来强行终止一个线程的运行,然后调用CloseHandle()函数释放线程所占用的堆栈;第三种方法是改变全局变量,使线程的执行函数返回,则该线程 终止。下面以第三种方法为例,给出部分代码:
| //////////////////////////////////////////////////////////////// //////CtestView message handlers /////Set to True to end thread Bool bend=FALSE;//定义的全局变量,用于控制线程的运行; //The Thread Function; UINT ThreadFunction(LPVOID pParam)//线程函数 { while(!bend) { Beep(100,100); Sleep(1000); } return 0; } ///////////////////////////////////////////////////////////// CwinThread *pThread; HWND hWnd; Void CtestView::OninitialUpdate() { hWnd=GetSafeHwnd(); pThread=AfxBeginThread(ThradFunction,hWnd);//启动线程 pThread->m_bAutoDelete=FALSE;//线程为手动删除 Cview::OnInitialUpdate(); } //////////////////////////////////////////////////////////////// Void CtestView::OnDestroy() { bend=TRUE;//改变变量,线程结束 WaitForSingleObject(pThread->m_hThread,INFINITE);//等待线程结束 delete pThread;//删除线程 Cview::OnDestroy(); } |
3、线程之间的通信
通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种 方法实现这种通信任务:使用全局变量(上一节的例子其实使用的就是这种方法)、使用事件对象、使用消息。这里我们主要介绍后两种方法。
(一) 利用用户定义的消息通信
在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不例外,这样一来,就使得线程之间利用消息来传递信息就变的非 常简单。首先用户要定义一个用户消息,如下所示:#define WM_USERMSG WMUSER+100;在需要的时候,在一个线程中调用::PostMessage((HWND)param,WM_USERMSG,0,0)或 CwinThread::PostThradMessage()来向另外一个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的目的窗口的句 柄、要发送的消息标志符、消息的参数WPARAM和LPARAM。下面的代码是对上节代码的修改,修改后的结果是在线程结束时显示一个对话框,提示线程结 束:
| UINT ThreadFunction(LPVOID pParam) { while(!bend) { Beep(100,100); Sleep(1000); } ::PostMessage(hWnd,WM_USERMSG,0,0); return 0; } ////////WM_USERMSG消息的响应函数为OnThreadended(WPARAM wParam, LPARAM lParam) LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam) { AfxMessageBox("Thread ended."); Retrun 0; } |
上面的例子是工作者线程向用户界面线程发送消息,对于工作者线程,如果它的设计模式也是消息驱动的,那么调用者可以向它发送初始化、退出、执行某种特定 的处理等消息,让它在后台完成。在控制函数中可以直接使用::GetMessage()这个SDK函数进行消息分检和处理,自己实现一个消息循环。 GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的时间片让给其它线程,不无效的占用CPU的时间,如果消息队列不为空, 就获取这个消息,判断这个消息的内容并进行相应的处理。
(二)用事件对象实现通信
在线程之间传递信号进行通信比较复杂的方法是使用事件对象,用MFC的Cevent类的对象来表示。事件对象处于两种状态之一:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。上述例子代码修改如下:
| //////////////////////////////////////////////////////////////////// Cevent threadStart ,threadEnd; UINT ThreadFunction(LPVOID pParam) { ::WaitForSingleObject(threadStart.m_hObject,INFINITE); AfxMessageBox("Thread start."); while(!bend) { Beep(100,100); Sleep(1000); Int result=::WaitforSingleObject(threadEnd.m_hObject,0); //等待threadEnd事件有信号,无信号时线程在这里悬停 If(result==Wait_OBJECT_0) Bend=TRUE; } ::PostMessage(hWnd,WM_USERMSG,0,0); return 0; } ///////////////////////////////////////////////////////////// Void CtestView::OninitialUpdate() { hWnd=GetSafeHwnd(); threadStart.SetEvent();//threadStart事件有信号 pThread=AfxBeginThread(ThreadFunction,hWnd);//启动线程 pThread->m_bAutoDelete=FALSE; Cview::OnInitialUpdate(); } //////////////////////////////////////////////////////////////// Void CtestView::OnDestroy() { threadEnd.SetEvent(); WaitForSingleObject(pThread->m_hThread,INFINITE); delete pThread; Cview::OnDestroy(); } |
运行这个程序,当关闭程序时,才显示提示框,显示"Thread ended"。
4、线程之间的同步
前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏 数据的完整性。保证各个线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC支持的七个多线程的同步类可以分成两大类:同步对象 (CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象 (CmultiLock和CsingleLock)。本节主要介绍临界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线程协调工作,程序运行起来更安全。
(一) 临界区
临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都 可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数 据。临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成 之后,调用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。同时启动两个线程,它 们对应的函数分别为WriteThread()和ReadThread(),用以对公共数组组array[]操作,下面的代码说明了如何使用临界区对象:
| #include "afxmt.h" int array[10],destarray[10]; CCriticalSection Section; UINT WriteThread(LPVOID param) { Section.Lock(); for(int x=0;x<10;x++) array[x]=x; Section.Unlock(); } UINT ReadThread(LPVOID param) { Section.Lock(); For(int x=0;x<10;x++) Destarray[x]=array[x]; Section.Unlock(); } |
上述代码运行的结果应该是Destarray数组中的元素分别为1-9,而不是杂乱无章的数,如果不使用同步,则不是这个结果,有兴趣的读者可以实验一下。
(二)互斥
互斥与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互 斥与Cmutex类的对象相对应,使用互斥对象时,必须创建一个CSingleLock或CMultiLock对象,用于实际的访问控制,因为这里的例子 只处理单个互斥,所以我们可以使用CSingleLock对象,该对象的Lock()函数用于占有互斥,Unlock()用于释放互斥。实现代码如下:
| #include "afxmt.h" int array[10],destarray[10]; CMutex Section; UINT WriteThread(LPVOID param) { CsingleLock singlelock; singlelock (&Section); singlelock.Lock(); for(int x=0;x<10;x++) array[x]=x; singlelock.Unlock(); } UINT ReadThread(LPVOID param) { CsingleLock singlelock; singlelock (&Section); singlelock.Lock(); For(int x=0;x<10;x++) Destarray[x]=array[x]; singlelock.Unlock(); } |
(三)信号量
信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,创建一个信号量需要用Csemaphore类声明一个对象,一旦 创建了一个信号量对象,就可以用它来对资源的访问技术。要实现计数处理,先创建一个CsingleLock或CmltiLock对象,然后用该对象的 Lock()函数减少这个信号量的计数值,Unlock()反之。下面的代码分别启动三个线程,执行时同时显示二个消息框,然后10秒后第三个消息框才得 以显示。
| ///////////////////////////////////////////////////////////////////////// Csemaphore *semaphore; Semaphore=new Csemaphore(2,2); HWND hWnd=GetSafeHwnd(); AfxBeginThread(threadProc1,hWnd); AfxBeginThread(threadProc2,hWnd); AfxBeginThread(threadProc3,hWnd); UINT ThreadProc1(LPVOID param) { CsingleLock singelLock(semaphore); singleLock.Lock(); Sleep(10000); ::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK); return 0; } UINT ThreadProc2(LPVOID param) { CSingleLock singelLock(semaphore); singleLock.Lock(); Sleep(10000); ::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK); return 0; } UINT ThreadProc3(LPVOID param) { CsingleLock singelLock(semaphore); singleLock.Lock(); Sleep(10000); ::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK); return 0; } |
二、 编程步骤
1、 启动Visual C++6.0,生成一个32位的控制台程序,将该程序命名为"sequence"
2、 输入要排续的数字,声明四个子线程;
3、 输入代码,编译运行程序。
三、 程序代码
| ////////////////////////////////////////////////////////////////////////////////////// // sequence.cpp : Defines the entry point for the console application. /* 主要用到的WINAPI线程控制函数,有关详细说明请查看MSDN; 线程建立函数: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性结构指针,可为NULL; DWORD dwStackSize, // 线程栈大小,若为0表示使用默认值; LPTHREAD_START_ROUTINE lpStartAddress, // 指向线程函数的指针; LPVOID lpParameter, // 传递给线程函数的参数,可以保存一个指针值; DWORD dwCreationFlags, // 线程建立是的初始标记,运行或挂起; LPDWORD lpThreadId // 指向接收线程号的DWORD变量; ); 对临界资源控制的多线程控制的信号函数: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性结构指针,可为NULL; BOOL bManualReset, // 手动清除信号标记,TRUE在WaitForSingleObject后必须手动//调用RetEvent清除信号。若为 FALSE则在WaitForSingleObject //后,系统自动清除事件信号; BOOL bInitialState, // 初始状态,TRUE有信号,FALSE无信号; LPCTSTR lpName // 信号量的名称,字符数不可多于MAX_PATH; //如果遇到同名的其他信号量函数就会失败,如果遇 //到同类信号同名也要注意变化; ); HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性结构指针,可为NULL BOOL bInitialOwner, // 当前建立互斥量是否占有该互斥量TRUE表示占有, //这样其他线程就不能获得此互斥量也就无法进入由 //该互斥量控制的临界区。FALSE表示不占有该互斥量 LPCTSTR lpName // 信号量的名称,字符数不可多于MAX_PATH如果 //遇到同名的其他信号量函数就会失败, //如果遇到同类信号同名也要注意变化; ); //初始化临界区信号,使用前必须先初始化 VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection // 临界区变量指针 ); //阻塞函数 //如果等待的信号量不可用,那么线程就会挂起,直到信号可用 //线程才会被唤醒,该函数会自动修改信号,如Event,线程被唤醒之后 //Event信号会变得无信号,Mutex、Semaphore等也会变。 DWORD WaitForSingleObject( HANDLE hHandle, // 等待对象的句柄 DWORD dwMilliseconds // 等待毫秒数,INFINITE表示无限等待 ); //如果要等待多个信号可以使用WaitForMutipleObject函数 */ #include "stdafx.h" #include "stdlib.h" #include "memory.h" HANDLE evtTerminate; //事件信号,标记是否所有子线程都执行完 /* 下面使用了三种控制方法,你可以注释其中两种,使用其中一种。 注意修改时要连带修改临界区PrintResult里的相应控制语句 */ HANDLE evtPrint; //事件信号,标记事件是否已发生 //CRITICAL_SECTION csPrint; //临界区 //HANDLE mtxPrint; //互斥信号,如有信号表明已经有线程进入临界区并拥有此信号 static long ThreadCompleted = 0; /* 用来标记四个子线程中已完成线程的个数,当一个子线程完成时就对ThreadCompleted进行加一操作, 要使用InterlockedIncrement(long* lpAddend)和InterlockedDecrement(long* lpAddend)进行加减操作*/ //下面的结构是用于传送排序的数据给各个排序子线程 struct MySafeArray { long* data; int iLength; }; //打印每一个线程的排序结果 void PrintResult(long* Array, int iLength, const char* HeadStr = "sort"); //排序函数 unsigned long __stdcall BubbleSort(void* theArray); //冒泡排序 unsigned long __stdcall SelectSort(void* theArray); //选择排序 unsigned long __stdcall HeapSort(void* theArray); //堆排序 unsigned long __stdcall InsertSort(void* theArray); //插入排序 /*以上四个函数的声明必须适合作为一个线程函数的必要条件才可以使用CreateThread 建立一个线程。 (1)调用方法必须是__stdcall,即函数参数压栈顺序由右到左,而且由函数本身负责 栈的恢复, C和C++默认是__cdecl, 所以要显式声明是__stdcall (2)返回值必须是unsigned long (3)参数必须是一个32位值,如一个指针值或long类型 (4) 如果函数是类成员函数,必须声明为static函数,在CreateThread时函数指针有特殊的写法。如下(函数是类CThreadTest的成员函数中): static unsigned long _stdcall MyThreadFun(void* pParam); handleRet = CreateThread(NULL, 0, &CThreadTestDlg::MyThreadFun, NULL, 0, &ThreadID); 之所以要声明为static是由于,该函数必须要独立于对象实例来使用,即使没有声明实例也可以使用。*/ int QuickSort(long* Array, int iLow, int iHigh); //快速排序 int main(int argc, char* argv[]) { long data[] = {123,34,546,754,34,74,3,56}; int iDataLen = 8; //为了对各个子线程分别对原始数据进行排序和保存排序结果 //分别分配内存对data数组的数据进行复制 long *data1, *data2, *data3, *data4, *data5; MySafeArray StructData1, StructData2, StructData3, StructData4; data1 = new long[iDataLen]; memcpy(data1, data, iDataLen << 2); //把data中的数据复制到data1中 //内存复制 memcpy(目标内存指针, 源内存指针, 复制字节数), 因为long的长度 //为4字节,所以复制的字节数为iDataLen << 2, 即等于iDataLen*4 StructData1.data = data1; StructData1.iLength = iDataLen; data2 = new long[iDataLen]; memcpy(data2, data, iDataLen << 2); StructData2.data = data2; StructData2.iLength = iDataLen; data3 = new long[iDataLen]; memcpy(data3, data, iDataLen << 2); StructData3.data = data3; StructData3.iLength = iDataLen; data4 = new long[iDataLen]; memcpy(data4, data, iDataLen << 2); StructData4.data = data4; StructData4.iLength = iDataLen; data5 = new long[iDataLen]; memcpy(data5, data, iDataLen << 2); unsigned long TID1, TID2, TID3, TID4; //对信号量进行初始化 evtTerminate = CreateEvent(NULL, FALSE, FALSE, "Terminate"); evtPrint = CreateEvent(NULL, FALSE, TRUE, "PrintResult"); //分别建立各个子线程 CreateThread(NULL, 0, &BubbleSort, &StructData1, NULL, &TID1); CreateThread(NULL, 0, &SelectSort, &StructData2, NULL, &TID2); CreateThread(NULL, 0, &HeapSort, &StructData3, NULL, &TID3); CreateThread(NULL, 0, &InsertSort, &StructData4, NULL, &TID4); //在主线程中执行行快速排序,其他排序在子线程中执行 QuickSort(data5, 0, iDataLen - 1); PrintResult(data5, iDataLen, "Quick Sort"); WaitForSingleObject(evtTerminate, INFINITE); //等待所有的子线程结束 //所有的子线程结束后,主线程才可以结束 delete[] data1; delete[] data2; delete[] data3; delete[] data4; CloseHandle(evtPrint); return 0; } /* 冒泡排序思想(升序,降序同理,后面的算法一样都是升序):从头到尾对数据进行两两比较进行交换,小的放前大的放后。这样一次下来,最大的元素就会被交换的最后,然后下一次 循环就不用对最后一个元素进行比较交换了,所以呢每一次比较交换的次数都比上一次循环的次数少一,这样N次之后数据就变得升序排列了*/ unsigned long __stdcall BubbleSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; int i, j=0; long swap; for (i = iLength-1; i > 0; i--) { for(j = 0; j < i; j++) { if(Array[j] > Array[j+1]) //前比后大,交换 { swap = Array[j]; Array[j] = Array[j+1]; Array[j+1] = swap; } } } PrintResult(Array, iLength, "Bubble Sort"); //向控制台打印排序结果 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1 if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完 //若都执行完则设置程序结束信号量 return 0; } /* 选择排序思想:每一次都从无序的数据中找出最小的元素,然后和前面已经有序的元素序列的后一个元素进行交换,这样整个源序列就会分成两部分,前面一部分是 已经排好序的有序序列,后面一部分是无序的,用于选出最小的元素。循环N次之后,前面的有序序列加长到跟源序列一样长,后面的无序部分长度变为0,排序就 完成了。*/ unsigned long __stdcall SelectSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; long lMin, lSwap; int i, j, iMinPos; for(i=0; i < iLength-1; i++) { lMin = Array[i]; iMinPos = i; for(j=i + 1; j <= iLength-1; j++) //从无序的元素中找出最小的元素 { if(Array[j] < lMin) { iMinPos = j; lMin = Array[j]; } } //把选出的元素交换拼接到有序序列的最后 lSwap = Array[i]; Array[i] = Array[iMinPos]; Array[iMinPos] = lSwap; } PrintResult(Array, iLength, "Select Sort"); //向控制台打印排序结果 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1 if(ThreadCompleted == 4) SetEvent(evtTerminate);//检查是否其他线程都已执行完 //若都执行完则设置程序结束信号量 return 0; } /* 堆排序思想:堆:数据元素从1到N排列成一棵二叉树,而且这棵树的每一个子树的根都是该树中的元素的最小或最大的元素这样如果一个无序数据集合是一个堆那 么,根元素就是最小或最大的元素堆排序就是不断对剩下的数据建堆,把最小或最大的元素析透出来。下面的算法,就是从最后一个元素开始,依据一个节点比父节 点数值大的原则对所有元素进行调整,这样调整一次就形成一个堆,第一个元素就是最小的元素。然后再对剩下的无序数据再进行建堆,注意这时后面的无序数据元 素的序数都要改变,如第一次建堆后,第二个元素就会变成堆的第一个元素。*/ unsigned long __stdcall HeapSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; int i, j, p; long swap; for(i=0; i<iLength-1; i++) { for(j = iLength - 1; j>i; j--) //从最后倒数上去比较字节点和父节点 { p = (j - i - 1)/2 + i; //计算父节点数组下标 //注意到树节点序数跟数组下标不是等同的,因为建堆的元素个数逐个递减 if(Array[j] < Array[p]) //如果父节点数值大则交换父节点和字节点 { swap = Array[j]; Array[j] = Array[p]; Array[p] = swap; } } } PrintResult(Array, iLength, "Heap Sort"); //向控制台打印排序结果 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1 if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完 //若都执行完则设置程序结束信号量 return 0; } /*插入排序思想:把源数据序列看成两半,前面一半是有序的,后面一半是无序的,把无序的数据从头到尾逐个逐个的插入到前面的有序数据中,使得有序的数据的个数不断增大,同时无序的数据个数就越来越少,最后所有元素都会变得有序。*/ unsigned long __stdcall InsertSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; int i=1, j=0; long temp; for(i=1; i<iLength; i++) { temp = Array[i]; //取出序列后面无序数据的第一个元素值 for(j=i; j>0; j--) //和前面的有序数据逐个进行比较找出合适的插入位置 { if(Array[j - 1] > temp) //如果该元素比插入值大则后移 Array[j] = Array[j - 1]; else //如果该元素比插入值小,那么该位置的后一位就是插入元素的位置 break; } Array[j] = temp; } PrintResult(Array, iLength, "Insert Sort"); //向控制台打印排序结果 InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1 if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完 //若都执行完则设置程序结束信号量 return 0; } /*快速排序思想:快速排序是分治思想的一种应用,它先选取一个支点,然后把小于支点的元素交换到支点的前边,把大于支点的元素交换到支点的右边。然后再对支点左边部分和右 边部分进行同样的处理,这样若干次之后,数据就会变得有序。下面的实现使用了递归 建 立两个游标:iLow,iHigh;iLow指向序列的第一个元素,iHigh指向最后一个先选第一个元素作为支点,并把它的值存贮在一个辅助变量里。那 么第一个位置就变为空并可以放置其他的元素。 这样从iHigh指向的元素开始向前移动游标,iHigh查找比支点小的元素,如果找到,则把它放置到空置了的位置(现在是第一个位置),然后iHigh 游标停止移动,这时iHigh指向的位置被空置,然后移动iLow游标寻找比支点大的元素放置到iHigh指向的空置的位置,如此往复直到iLow与 iHigh相等。最后使用递归对左右两部分进行同样处理*/ int QuickSort(long* Array, int iLow, int iHigh) { if(iLow >= iHigh) return 1; //递归结束条件 long pivot = Array[iLow]; int iLowSaved = iLow, iHighSaved = iHigh; //保未改变的iLow,iHigh值保存起来 while (iLow < iHigh) { while (Array[iHigh] >= pivot && iHigh > iLow) //寻找比支点大的元素 iHigh -- ; Array[iLow] = Array[iHigh]; //把找到的元素放置到空置的位置 while (Array[iLow] < pivot && iLow < iHigh) //寻找比支点小的元素 iLow ++ ; Array[iHigh] = Array[iLow]; //把找到的元素放置到空置的位置 } Array[iLow] = pivot; //把支点值放置到支点位置,这时支点位置是空置的 //对左右部分分别进行递归处理 QuickSort(Array, iLowSaved, iHigh-1); QuickSort(Array, iLow+1, iHighSaved); return 0; } //每一个线程都要使用这个函数进行输出,而且只有一个显示器,产生多个线程 //竞争对控制台的使用权。 void PrintResult(long* Array, int iLength, const char* HeadStr) { WaitForSingleObject(evtPrint, INFINITE); //等待事件有信号 //EnterCriticalSection(&csPrint); //标记有线程进入临界区 //WaitForSingleObject(mtxPrint, INFINITE); //等待互斥量空置(没有线程拥有它) int i; printf("%s: ", HeadStr); for (i=0; i<iLength-1; i++) { printf("%d,", Array[i]); Sleep(100); //延时(可以去掉) /*只是使得多线程对临界区访问的问题比较容易看得到 如果你把临界控制的语句注释掉,输出就会变得很凌乱,各个排序的结果会 分插间隔着输出,如果不延时就不容易看到这种不对临界区控制的结果 */ } printf("%dn", Array[i]); SetEvent(evtPrint); //把事件信号量恢复,变为有信号 } |
四、 小结
对复杂的应用程序来说,线程的应用给应用程序提供了高效、快速、安全的数据处理能力。本实例讲述了线程处理中经常遇到的问题,希望对读者朋友有一定的帮助,起到抛砖引玉的作用。
2007年5月21日月曜日
CComboBox控件详解
CComboBox控件又称作组合框控件,其有三种形态可供选择,1.简单组合框(Simple)2.下拉组合框(Drop-down)3.下拉列表式组合框(Drop-down list).
CComboBox控件的常用设置属性说明:
type属性:里面一共有三个选项.就是其三种形式, 我们常用的是后两种形态,其区别就是Dropdown的编辑区为可编辑控件,而droplist为静态控件.
Data属性:当程序初始化的时候,下拉列表将显示其属性里面的内容,内容用分号分隔.其属性只支持后2中形式,
Sort 属性:对添加到下拉列表框中的数据自动进行排序,如果你不想他改变你的显示序列,把他设置成False.
控件是一个窗口.其基类是为CWnd.所以CComboBox也可以使用CWnd的一些函数,它自己的函数有30几种.我写了一个示范程序,演示了一些常用的函数,你可以参考其代码看到其功能.
注:m_cbox 为CComboBox控件的关联变量,m_end为EDIT控件的关联变量
1.GetCount() 函数:用以获得列表框中的选项数目. int GetCount() const;(此乃函数原型,我会列在每条的后面) 下面的例子是程序中各个按钮里面的代码
int i;
CString str;
i=m_cbox.GetCount();
str.Format ("%d",i);
m_end="一共有"+str+"个数据";
UpdateData(FALSE);
2.GetCurSel() 函数:用以得到用户选中下拉列表框中数据的索引值.返回的值是重0开始的,如果没有选择任何选项将会返回-1 Int GetCurSel() const
int i;
CString str;
i=m_cbox.GetCurSel ();
str.Format ("%d",i+1);
if (i==-1) m_end="你什么都没有选";
else m_end="你选的是第"+str+"项";
UpdateData(FALSE);
3.SetCurSel函数:选取列表框中的一个选项,索引从0开始,-1为什么都不选.当你需要把列表框中已经选取的内容清空时就可以使用这个函数;
if(m_i<3) m_cbox.SetCurSel (m_i++);//m_i为int型变量 用以计数
else {
m_cbox.SetCurSel (-1);
m_i=0;
}
4.SetEditSel函数:设置编辑区中蓝色选中字段的长度, BOOL SetEditSel( intnStartChar,
int nEndChar ); nStartChar是起始的位置,当设置为-1时为不选择任何字段,nEndChar是结束位置.还有一个对应的函数GetEditSel是取得其位置,在此不详解.
m_cbox.SetEditSel (3,5);//在编辑框中打入一串数字在按此按钮就能看到效果
//其作用就是选中第3到第5个字符作为热点
5.Clear() Cut() Paste() Copy() 这四个函数我放在一起说,因为他们都是针对编辑框中选定的文字进行编辑的函数,很多初学者很不明白这4个函数,使用这些函数没有作用,其实不然,这些函数对于在编辑框中的内容不进行处理,他真正处理的是你用鼠标选中的内容,对于Clear()函数很多书中解释都是有误的,很多书中对他的解释是清除当前选中的文本,我认为这样的解释是有歧异的,真正的解释是把你刚选中的文本释放掉,也就是不选,而不是把选中的删除,真正的删除选中的文本的函数是Cut(), Copy()是复制选中的文本,Paste()是粘贴到光标处.其演示就不写在程序中,有兴趣的朋友可以自己试试.
6.LimitText()函数.其作用是限制在编辑框中键入的文本长度,BOOL LimitText(int nMaxChars); 当nMaxChars为0时,不是不能输入,而是长度最大化为65535字节
7.GetLBText()函数.其原型为void GetLBText( int nIndex, CString&rString ).nIndex为列表框中数据的索引值,rString 为字符串,其作用就是把索引号为nIndex的数据放到rString变量中.与其对应的GetLBTextLen(intnIndex)函数就是得到索引号为nIndex中数据的长度
m_cbox.GetLBText (2,m_end); // GetLBText按钮
UpdateData(0);
CString str; //GetLBTextLen按钮
int i=m_cbox.GetLBTextLen (2);
str.Format ("%d",i);
m_end=str;
UpdateData(FALSE);
8.AddString() DeleteString() InsertString() ResetContent() 四个函数放在一起,他们都是针对下拉列表框的处理函数, AddString(LPCTSTR lpszString) 是在列表框的尾巴添加一条数据,参数为字符串. DeleteString(UINT nIndex)为删除指定索引为nIndex的数据. InsertString(int nIndex, LPCTSTR lpszString)在索引nIndex处插入IpszString的内容. ResetContent()是清除列表框中所有的内容.
9.SetDroppedWidth(UINT nWidth) 函数.用于设定下拉列表框的最小宽度,当下拉列表框中的数据很长不能完全显示时,我们可以使用这个函数把列表框的宽度设置的大些.同样GetDroppedWidth()是返回列表框的宽度.
以下为总结了各大论坛中对CCombBox控件使用的问题.并给出了正解.
1.关于CComboBox在对话框中没有下拉项目的问题
答: 资源编辑器中对话框中CComboBox组件的垂直范围拉大了下,就看见字体了,看到那个向下的黑箭头了吗,点他下拉,嘿嘿就这么简单.
2.请问怎样能让CCombobox记住用户的输入.下次使用时还有
答: 要记住用户的输入必须用到文件读写,或者把内容添加到注册表中,我会在以后给出解决办法.
3. CCOMBOBOX如何使它只读,不能编辑
答: 看了本文的介绍也许你现在知道了, CCOMBOBOX有三种类型的,选择你需要的吧!
4. 我要让界面一显示,就能在Combo框中显示默认的数据
答: 你要在OnInitDialog里面加上我上面所提到的SetCurSel()函数就行了.
5. 如何把选好的内容赋给变量
答: 先给CCOMBOBOX设置一个CString关联变量,再添加事件处理函数CBN_ Selchange.使用UpdateData(FALSE)就行了.
6. 在CComboBox组合框中,条目的对齐方式是左对齐,能设置为对中或者右对齐吗?
答: 没有直接的方法,你必须自己对齐, 比如最大长度是10
int i;
String.Format("%10d", i);
CComboBox->AddString
CComboBox控件的常用设置属性说明:
type属性:里面一共有三个选项.就是其三种形式, 我们常用的是后两种形态,其区别就是Dropdown的编辑区为可编辑控件,而droplist为静态控件.
Data属性:当程序初始化的时候,下拉列表将显示其属性里面的内容,内容用分号分隔.其属性只支持后2中形式,
Sort 属性:对添加到下拉列表框中的数据自动进行排序,如果你不想他改变你的显示序列,把他设置成False.
控件是一个窗口.其基类是为CWnd.所以CComboBox也可以使用CWnd的一些函数,它自己的函数有30几种.我写了一个示范程序,演示了一些常用的函数,你可以参考其代码看到其功能.
注:m_cbox 为CComboBox控件的关联变量,m_end为EDIT控件的关联变量
1.GetCount() 函数:用以获得列表框中的选项数目. int GetCount() const;(此乃函数原型,我会列在每条的后面) 下面的例子是程序中各个按钮里面的代码
int i;
CString str;
i=m_cbox.GetCount();
str.Format ("%d",i);
m_end="一共有"+str+"个数据";
UpdateData(FALSE);
2.GetCurSel() 函数:用以得到用户选中下拉列表框中数据的索引值.返回的值是重0开始的,如果没有选择任何选项将会返回-1 Int GetCurSel() const
int i;
CString str;
i=m_cbox.GetCurSel ();
str.Format ("%d",i+1);
if (i==-1) m_end="你什么都没有选";
else m_end="你选的是第"+str+"项";
UpdateData(FALSE);
3.SetCurSel函数:选取列表框中的一个选项,索引从0开始,-1为什么都不选.当你需要把列表框中已经选取的内容清空时就可以使用这个函数;
if(m_i<3) m_cbox.SetCurSel (m_i++);//m_i为int型变量 用以计数
else {
m_cbox.SetCurSel (-1);
m_i=0;
}
4.SetEditSel函数:设置编辑区中蓝色选中字段的长度, BOOL SetEditSel( intnStartChar,
int nEndChar ); nStartChar是起始的位置,当设置为-1时为不选择任何字段,nEndChar是结束位置.还有一个对应的函数GetEditSel是取得其位置,在此不详解.
m_cbox.SetEditSel (3,5);//在编辑框中打入一串数字在按此按钮就能看到效果
//其作用就是选中第3到第5个字符作为热点
5.Clear() Cut() Paste() Copy() 这四个函数我放在一起说,因为他们都是针对编辑框中选定的文字进行编辑的函数,很多初学者很不明白这4个函数,使用这些函数没有作用,其实不然,这些函数对于在编辑框中的内容不进行处理,他真正处理的是你用鼠标选中的内容,对于Clear()函数很多书中解释都是有误的,很多书中对他的解释是清除当前选中的文本,我认为这样的解释是有歧异的,真正的解释是把你刚选中的文本释放掉,也就是不选,而不是把选中的删除,真正的删除选中的文本的函数是Cut(), Copy()是复制选中的文本,Paste()是粘贴到光标处.其演示就不写在程序中,有兴趣的朋友可以自己试试.
6.LimitText()函数.其作用是限制在编辑框中键入的文本长度,BOOL LimitText(int nMaxChars); 当nMaxChars为0时,不是不能输入,而是长度最大化为65535字节
7.GetLBText()函数.其原型为void GetLBText( int nIndex, CString&rString ).nIndex为列表框中数据的索引值,rString 为字符串,其作用就是把索引号为nIndex的数据放到rString变量中.与其对应的GetLBTextLen(intnIndex)函数就是得到索引号为nIndex中数据的长度
m_cbox.GetLBText (2,m_end); // GetLBText按钮
UpdateData(0);
CString str; //GetLBTextLen按钮
int i=m_cbox.GetLBTextLen (2);
str.Format ("%d",i);
m_end=str;
UpdateData(FALSE);
8.AddString() DeleteString() InsertString() ResetContent() 四个函数放在一起,他们都是针对下拉列表框的处理函数, AddString(LPCTSTR lpszString) 是在列表框的尾巴添加一条数据,参数为字符串. DeleteString(UINT nIndex)为删除指定索引为nIndex的数据. InsertString(int nIndex, LPCTSTR lpszString)在索引nIndex处插入IpszString的内容. ResetContent()是清除列表框中所有的内容.
9.SetDroppedWidth(UINT nWidth) 函数.用于设定下拉列表框的最小宽度,当下拉列表框中的数据很长不能完全显示时,我们可以使用这个函数把列表框的宽度设置的大些.同样GetDroppedWidth()是返回列表框的宽度.
以下为总结了各大论坛中对CCombBox控件使用的问题.并给出了正解.
1.关于CComboBox在对话框中没有下拉项目的问题
答: 资源编辑器中对话框中CComboBox组件的垂直范围拉大了下,就看见字体了,看到那个向下的黑箭头了吗,点他下拉,嘿嘿就这么简单.
2.请问怎样能让CCombobox记住用户的输入.下次使用时还有
答: 要记住用户的输入必须用到文件读写,或者把内容添加到注册表中,我会在以后给出解决办法.
3. CCOMBOBOX如何使它只读,不能编辑
答: 看了本文的介绍也许你现在知道了, CCOMBOBOX有三种类型的,选择你需要的吧!
4. 我要让界面一显示,就能在Combo框中显示默认的数据
答: 你要在OnInitDialog里面加上我上面所提到的SetCurSel()函数就行了.
5. 如何把选好的内容赋给变量
答: 先给CCOMBOBOX设置一个CString关联变量,再添加事件处理函数CBN_ Selchange.使用UpdateData(FALSE)就行了.
6. 在CComboBox组合框中,条目的对齐方式是左对齐,能设置为对中或者右对齐吗?
答: 没有直接的方法,你必须自己对齐, 比如最大长度是10
int i;
String.Format("%10d", i);
CComboBox->AddString
登録:
投稿 (Atom)