一直为combo box不能弹出有复选框的CTreeCtrl而烦恼,因为有的记录项需要用户多项选择录入或查询,如果直接在界面上放上所有复选项,那么将浪费有限的空间。所以我下决心搞定combo box.
经过一番跟踪和试验,终于找到了解决办法,这是我成功后的界面:

现在将我研究的过程和结果和大家分享一下:
终于发现combo box下的list box并不是其子控件,原来是桌面的子控件,且有WS_EX_TOOLWINDOW扩展属性(防止在任务栏中出来标题框,嘿嘿)。
第一步:模仿之:
新建一个基于CTreeCtrl的子类CTreeList,并做为CComboBox继承类CComboList的成员变量m_tree
BOOL CTreeList::Create(CWnd* pBuddyWnd, UINT nID, DWORD dwStyle)
{
m_pBuddyWnd = pBuddyWnd;
dwStyle |= WS_CHILD | WS_BORDER | WS_TABSTOP;
dwStyle &= ~WS_POPUP;
return CWnd::CreateEx(WS_EX_TOOLWINDOW, WC_TREEVIEW, NULL, dwStyle, CRect(0,0,0,0), GetDesktopWindow(), nID, NULL);
}
void CComboList::PreSubclassWindow()
{
m_tree.Create(this, IDC_TREELIST);
CComboBox::PreSubclassWindow();
}
{
m_pBuddyWnd = pBuddyWnd;
dwStyle |= WS_CHILD | WS_BORDER | WS_TABSTOP;
dwStyle &= ~WS_POPUP;
return CWnd::CreateEx(WS_EX_TOOLWINDOW, WC_TREEVIEW, NULL, dwStyle, CRect(0,0,0,0), GetDesktopWindow(), nID, NULL);
}
void CComboList::PreSubclassWindow()
{
m_tree.Create(this, IDC_TREELIST);
CComboBox::PreSubclassWindow();
}
运行,没报错,有希望。
第二步,弹出它:
void CComboList::OnDropdown()
{
m_tree.ShowWindow(TRUE, m_cyTree);
}
BOOL CTreeList::ShowWindow(BOOL bShow, int cy)
{
if (bShow && m_pBuddyWnd)
{
if (!m_bInitialize)
{
NMHDR nmhdr = {m_hWnd, GetDlgCtrlID(), TVN_INITIALUPDATE};
if (!::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_INITIALUPDATE, (LPARAM)&nmhdr))//向同伴发初始化通知
return FALSE;
m_bInitialize = TRUE;
}
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_WILLBESHOW}, 0, {0, GetDropHilightItem()}, {0}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_WILLBESHOW, (LPARAM)&nmtv);//向同伴发将要显示通知
RECT rc;
m_pBuddyWnd->GetWindowRect(&rc);
SetWindowPos(&wndTop, rc.left, rc.bottom, rc.right - rc.left, cy, SWP_SHOWWINDOW);
}
else
{
CTreeCtrl::ShowWindow(FALSE);
}
return TRUE;
}
{
m_tree.ShowWindow(TRUE, m_cyTree);
}
BOOL CTreeList::ShowWindow(BOOL bShow, int cy)
{
if (bShow && m_pBuddyWnd)
{
if (!m_bInitialize)
{
NMHDR nmhdr = {m_hWnd, GetDlgCtrlID(), TVN_INITIALUPDATE};
if (!::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_INITIALUPDATE, (LPARAM)&nmhdr))//向同伴发初始化通知
return FALSE;
m_bInitialize = TRUE;
}
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_WILLBESHOW}, 0, {0, GetDropHilightItem()}, {0}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_WILLBESHOW, (LPARAM)&nmtv);//向同伴发将要显示通知
RECT rc;
m_pBuddyWnd->GetWindowRect(&rc);
SetWindowPos(&wndTop, rc.left, rc.bottom, rc.right - rc.left, cy, SWP_SHOWWINDOW);
}
else
{
CTreeCtrl::ShowWindow(FALSE);
}
return TRUE;
}
它是弹出了,但原有的listbox也弹出了,再努力。幸亏微软提供了的一个函数:
BOOL GetComboBoxInfo(
HWND hwndCombo, // handle to combo box
PCOMBOBOXINFO pcbi // combo box information
);
可以让我们找到list box的HWND,有了这个窗口句柄,啥事都好办了,我改变它的消息处理过程。
void CComboList::PreSubclassWindow()
{
m_tree.Create(this, IDC_TREELIST);
/////////////////////////////////////////////
COMBOBOXINFO cbi = {sizeof(cbi)};
::GetComboBoxInfo(m_hWnd, &cbi);
::SendMessage(cbi.hwndItem, EM_SETREADONLY, TRUE, 0);//不让用户乱输入
::SetParent(cbi.hwndList, m_hWnd);//改变其父窗口
::SetWindowLong(cbi.hwndList, GWL_WNDPROC, (LONG)ListBoxProc);//变为我自己的消息处理过程
RECT rc;
GetDroppedControlRect(&rc);//主要是得到其高度,还有用
m_cyTree = rc.bottom - rc.top;
GetWindowRect(&rc);
m_cyTree -= rc.bottom - rc.top;
m_cyTree = max(100, m_cyTree);
CComboBox::PreSubclassWindow();
}
LRESULT CALLBACK CComboList::ListBoxProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return 0;//自己的消息处理过程,直接返回0,这样它的WM_WINDOWPOSCHANGED等默认的消息处理就被我禁掉了。
}
{
m_tree.Create(this, IDC_TREELIST);
/////////////////////////////////////////////
COMBOBOXINFO cbi = {sizeof(cbi)};
::GetComboBoxInfo(m_hWnd, &cbi);
::SendMessage(cbi.hwndItem, EM_SETREADONLY, TRUE, 0);//不让用户乱输入
::SetParent(cbi.hwndList, m_hWnd);//改变其父窗口
::SetWindowLong(cbi.hwndList, GWL_WNDPROC, (LONG)ListBoxProc);//变为我自己的消息处理过程
RECT rc;
GetDroppedControlRect(&rc);//主要是得到其高度,还有用
m_cyTree = rc.bottom - rc.top;
GetWindowRect(&rc);
m_cyTree -= rc.bottom - rc.top;
m_cyTree = max(100, m_cyTree);
CComboBox::PreSubclassWindow();
}
LRESULT CALLBACK CComboList::ListBoxProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return 0;//自己的消息处理过程,直接返回0,这样它的WM_WINDOWPOSCHANGED等默认的消息处理就被我禁掉了。
}
测试,果然如我所愿,list box老实了,再不弹出了,感觉是在肉鸡,哈哈。
第三步:通过测试发现,控件一但基于桌面,就无法获得焦点了.(可能是非自己顶层窗口下的子窗口的缘故吧)所以为了让自己的treelist响应WM_MOUSEWHEEL,WM_KEYDOWN等需要有焦点才能有效的消息.
模拟tool tip类,中继消息:
BOOL CComboList::PreTranslateMessage(MSG* pMsg)
{
m_tree.RelayEvent(pMsg);
return CComboBox::PreTranslateMessage(pMsg);
}
void CTreeList::RelayEvent(LPMSG lpMsg)
{
if (lpMsg->message == WM_MOUSEWHEEL || lpMsg->message == WM_KEYDOWN)
{
::SendMessage(m_hWnd, lpMsg->message, lpMsg->wParam, lpMsg->lParam);
}
}
{
m_tree.RelayEvent(pMsg);
return CComboBox::PreTranslateMessage(pMsg);
}
void CTreeList::RelayEvent(LPMSG lpMsg)
{
if (lpMsg->message == WM_MOUSEWHEEL || lpMsg->message == WM_KEYDOWN)
{
::SendMessage(m_hWnd, lpMsg->message, lpMsg->wParam, lpMsg->lParam);
}
}
经过这一番处理,tree list可以处理这两个消息了,但失去焦点下的选择项是灰背景,有过编程经验的都知道这个现象.但幸亏tree list提供了拖拽到目标项就高亮的功能,偷梁换柱吧。
void CTreeList::OnLButtonDown(UINT nFlags, CPoint point)
{
UINT uFlag;//接收有关点击测试的信息的整数
HTREEITEM hItemNew = HitTest(point, &uFlag);//返回与CtreeCtrl关联的光标的当前位置和句柄
if (hItemNew && ((TVHT_ONITEMLABEL | TVHT_ONITEMSTATEICON) & uFlag))//点中标签文字或复选框
{
HTREEITEM hItemOld = GetDropHilightItem();
if (hItemOld != hItemNew)//发选择项变化通知
{
if (hItemOld)
Select(hItemOld, NULL);//消息原先的目标项高亮显示
Select(hItemNew, TVGN_DROPHILITE);//让新选择项有拖拽到目标项就高亮显示
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_SELCHANGED}, 0, {0, hItemOld}, {0, hItemNew}, {point.x, point.y}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_SELCHANGED, (LPARAM)&nmtv);
}
}
CTreeCtrl::OnLButtonDown(nFlags, point);
if (hItemNew && (TVHT_ONITEMSTATEICON & uFlag))//发复选框状态变化通知
{
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_CHECKCHANGED}, 0, {0, hItemNew}, {0, hItemNew}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_CHECKCHANGED, (LPARAM)&nmtv);
}
}
{
UINT uFlag;//接收有关点击测试的信息的整数
HTREEITEM hItemNew = HitTest(point, &uFlag);//返回与CtreeCtrl关联的光标的当前位置和句柄
if (hItemNew && ((TVHT_ONITEMLABEL | TVHT_ONITEMSTATEICON) & uFlag))//点中标签文字或复选框
{
HTREEITEM hItemOld = GetDropHilightItem();
if (hItemOld != hItemNew)//发选择项变化通知
{
if (hItemOld)
Select(hItemOld, NULL);//消息原先的目标项高亮显示
Select(hItemNew, TVGN_DROPHILITE);//让新选择项有拖拽到目标项就高亮显示
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_SELCHANGED}, 0, {0, hItemOld}, {0, hItemNew}, {point.x, point.y}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_SELCHANGED, (LPARAM)&nmtv);
}
}
CTreeCtrl::OnLButtonDown(nFlags, point);
if (hItemNew && (TVHT_ONITEMSTATEICON & uFlag))//发复选框状态变化通知
{
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_CHECKCHANGED}, 0, {0, hItemNew}, {0, hItemNew}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_CHECKCHANGED, (LPARAM)&nmtv);
}
}
WM_KEYDOWN处理过程可以模仿之,上下左右,调用默认的消息处理,只不过让选择项高亮就行了,复选只不过处理空格就行了:
void CTreeList::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
HTREEITEM hItemOld = GetDropHilightItem();
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
HTREEITEM hItemNew = GetSelectedItem();
if (hItemOld != hItemNew)//发选择项变化通知
{
if (hItemOld)
Select(hItemOld, NULL);
Select(hItemNew, TVGN_DROPHILITE);
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_SELCHANGED}, 0, {0, hItemOld}, {0, hItemNew}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_SELCHANGED, (LPARAM)&nmtv);
}
if (nChar == VK_SPACE)
{
HTREEITEM hItemNew = GetDropHilightItem();
if (hItemNew)//发复选框状态变化通知
{
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_CHECKCHANGED}, 0, {0, hItemNew}, {0, hItemNew}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_CHECKCHANGED, (LPARAM)&nmtv);
}
}
}
{
HTREEITEM hItemOld = GetDropHilightItem();
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
HTREEITEM hItemNew = GetSelectedItem();
if (hItemOld != hItemNew)//发选择项变化通知
{
if (hItemOld)
Select(hItemOld, NULL);
Select(hItemNew, TVGN_DROPHILITE);
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_SELCHANGED}, 0, {0, hItemOld}, {0, hItemNew}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_SELCHANGED, (LPARAM)&nmtv);
}
if (nChar == VK_SPACE)
{
HTREEITEM hItemNew = GetDropHilightItem();
if (hItemNew)//发复选框状态变化通知
{
NMTREEVIEW nmtv = {{m_hWnd, GetDlgCtrlID(), TVN_CHECKCHANGED}, 0, {0, hItemNew}, {0, hItemNew}, {0, 0}};
::SendMessage(m_pBuddyWnd->m_hWnd, WM_NOTIFY, TVN_CHECKCHANGED, (LPARAM)&nmtv);
}
}
}
再测试,发现窗口可以高亮显示选择项,这个类就基本成功了,但还要完善最后一步。
第四步:让同伴类处理4个通知,为了让继承类能重载,先什么都不做:
#define TVS_NOHSCROLL 0x8000//微软没有公开的宏
#define TVN_CHECKCHANGED (TVN_FIRST-16)//用户操作了一个复选框,发此通知
#define TVN_INITIALUPDATE (TVN_FIRST-17)//第一次弹出窗口时,发此通知
#define TVN_WILLBESHOW (TVN_FIRST-18)//每次弹出窗口前,发此通知
注意TVN_INITIALUPDATE通知的*pResult
*pResult = 1;//1初始化成功,0初始失败
BEGIN_MESSAGE_MAP(CComboList, CComboBox)
//{{AFX_MSG_MAP(CComboList)
ON_CONTROL_REFLECT(CBN_DROPDOWN, OnDropdown)
ON_CONTROL_REFLECT(CBN_CLOSEUP, OnCloseup)
ON_WM_CTLCOLOR()
ON_NOTIFY(TVN_INITIALUPDATE, IDC_TREELIST, OnTreeInitialUpdate)
ON_NOTIFY(TVN_WILLBESHOW, IDC_TREELIST, OnTreeWillbeShow)
ON_NOTIFY(TVN_SELCHANGED, IDC_TREELIST, OnTreeSelchanged)
ON_NOTIFY(TVN_CHECKCHANGED, IDC_TREELIST, OnTreeCheckChanged)
ON_WM_PAINT()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
void CComboList::OnTreeInitialUpdate(NMHDR* pNMHDR, LRESULT* pResult)
{
*pResult = 1;
}
void CComboList::OnTreeWillbeShow(NMHDR* pNMHDR, LRESULT* pResult)
{
*pResult = 0;
}
void CComboList::OnTreeSelchanged(NMTREEVIEW* pNMTV, LRESULT* pResult)
{
*pResult = 0;
}
void CComboList::OnTreeCheckChanged(NMTREEVIEW* pNMTV, LRESULT* pResult)
{
*pResult = 0;
}
#define TVN_CHECKCHANGED (TVN_FIRST-16)//用户操作了一个复选框,发此通知
#define TVN_INITIALUPDATE (TVN_FIRST-17)//第一次弹出窗口时,发此通知
#define TVN_WILLBESHOW (TVN_FIRST-18)//每次弹出窗口前,发此通知
注意TVN_INITIALUPDATE通知的*pResult
*pResult = 1;//1初始化成功,0初始失败
BEGIN_MESSAGE_MAP(CComboList, CComboBox)
//{{AFX_MSG_MAP(CComboList)
ON_CONTROL_REFLECT(CBN_DROPDOWN, OnDropdown)
ON_CONTROL_REFLECT(CBN_CLOSEUP, OnCloseup)
ON_WM_CTLCOLOR()
ON_NOTIFY(TVN_INITIALUPDATE, IDC_TREELIST, OnTreeInitialUpdate)
ON_NOTIFY(TVN_WILLBESHOW, IDC_TREELIST, OnTreeWillbeShow)
ON_NOTIFY(TVN_SELCHANGED, IDC_TREELIST, OnTreeSelchanged)
ON_NOTIFY(TVN_CHECKCHANGED, IDC_TREELIST, OnTreeCheckChanged)
ON_WM_PAINT()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
void CComboList::OnTreeInitialUpdate(NMHDR* pNMHDR, LRESULT* pResult)
{
*pResult = 1;
}
void CComboList::OnTreeWillbeShow(NMHDR* pNMHDR, LRESULT* pResult)
{
*pResult = 0;
}
void CComboList::OnTreeSelchanged(NMTREEVIEW* pNMTV, LRESULT* pResult)
{
*pResult = 0;
}
void CComboList::OnTreeCheckChanged(NMTREEVIEW* pNMTV, LRESULT* pResult)
{
*pResult = 0;
}
有了这几个通知,用户可以方便的重载添加自己的初始化代码,点击复选后保存复选项,让其与输入数据同步等功能。
一切大功告成,打包上来供大家使用。
能弹出复选tree列表的combo box