Introduction
This code shows how to place multiple controls, in particular, CListCtrl
controls, into the same view. Or, if you prefer, place two views into one composite view. The story behind this article is that I have been asked to add a feature to an MFC program: show a sum of numbers in columns. The first thing to remember is splitters, but there are already four there, so I do not want to touch that mess. Instead, I want to substitute a view. Since the easiest way to store your code so that you can later find it is to publish it on the Internet...
Step 1: Two List Controls in the Same View
Use the wizard to create a new project; use CView
as the view base class. Add two members to the view:
CListCtrl m_lista;
CListCtrl m_listb;
Modify the view's OnCreate()
function:
DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP |
LVS_REPORT;
BOOL bResultA = m_lista.Create(dwStyle, CRect(0,0,0,0), this, IDC_LIST1);
BOOL bResultB = m_listb.Create(dwStyle,
CRect(0,0,0,0), this, IDC_LIST2);
COLORREF cr = 0xe0f0f0;
m_listb.SetTextBkColor(cr);
m_listb.SetBkColor(cr);
Add the layout logic to the OnSize()
function:
void CMysplit2View::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
if (::IsWindow(m_lista.m_hWnd))
m_lista.MoveWindow(0, 0, cx, cy/2, TRUE);
if (::IsWindow(m_listb.m_hWnd))
m_listb.MoveWindow(0, cy/2, cx, cy, TRUE);
}
The identifiers IDC_LIST1
and IDC_LIST2
are added manually to Resource.h, do not forget to update _APS_NEXT_CONTROL_VALUE
.
#define IDC_LIST1 1001
#define IDC_LIST2 1002
....
#define _APS_NEXT_CONTROL_VALUE 1003
Try it: it works! Probably, this already is what you need and you don't need to read further.
An important question: can we put there a view instead of a control? Yes, we can. (But we will have to allocate the view in the heap separately.)
Step 2: Synchronous Row Resizing
Once more, manually add two constants, the header control IDs IDC_HDR1
and IDC_HDR2
, to Resource.h and update _APS_NEXT_CONTROL_VALUE
.
#define IDC_HDR1 1003
#define IDC_HDR2 1004
....
#define _APS_NEXT_CONTROL_VALUE 1005
Modify CMysplit2View::OnCreate()
to make header controls use these header IDs instead of the default 0:
m_lista.GetHeaderCtrl()->SetDlgCtrlID(IDC_HDR1);
m_listb.GetHeaderCtrl()->SetDlgCtrlID(IDC_HDR2);
Manually add message map entries (outside of the Class Wizard's AFX_MSG_MAP
block):
BEGIN_MESSAGE_MAP(CMysplit2View, CView)
ON_WM_CREATE()
ON_WM_SIZE()
.....
ON_NOTIFY_EX( HDN_ENDTRACK, IDC_HDR1, myOnNotifyHdnEndtrackA)
ON_NOTIFY_EX( HDN_ENDTRACK, IDC_HDR2, myOnNotifyHdnEndtrackB)
END_MESSAGE_MAP()
Add the notification handling functions:
protected:
afx_msg BOOL myOnNotifyHdnEndtrackA
( UINT id, NMHDR * pNotifyStruct, LRESULT * result );
afx_msg BOOL myOnNotifyHdnEndtrackB
( UINT id, NMHDR * pNotifyStruct, LRESULT * result );
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize(UINT nType, int cx, int cy);
DECLARE_MESSAGE_MAP()
afx_msg BOOL CMysplit2View::myOnNotifyHdnEndtrackA
( UINT id, NMHDR * pNotifyStruct, LRESULT * result )
{
NMHEADER* pnmh = (NMHEADER*)pNotifyStruct;
int n = pnmh->iItem;
m_listb.SetColumnWidth(n,m_lista.GetColumnWidth(n));
return 0;
}
afx_msg BOOL CMysplit2View::myOnNotifyHdnEndtrackB
( UINT id, NMHDR * pNotifyStruct, LRESULT * result )
{
return 0;
}
Try the code: It resizes the second list control after you resize the first one. I have not done the same functionality for the second header control because I am going to hide it. The first header will be enough. Taking that into account, we could live with 0 as the header control ID...
If It Does Not Work
There are two kinds of HDN_ENDTRACK
messages: HDN_ENDTRACKA
and HDN_ENDTRACKW
. They correspond to different numbers. It is very likely that you are trying to intercept the wrong message. So you may have to replace
ON_NOTIFY_REFLECT(HDN_ENDTRACK, OnEndtrack)
with:
ON_NOTIFY(HDN_ENDTRACKA, 0, OnEndtrack)
ON_NOTIFY(HDN_ENDTRACKW, 0, OnEndtrack)
in the message map (of course, if you are sure that the same code will do for both versions).
Step 3: Set the 2nd Control Height to be Just Enough for a Single Row
First of all, we remove the header from the second control:
int CMysplit2View::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP |
LVS_REPORT;
BOOL bResultA = m_lista.Create(dwStyle, CRect(0,0,0,0), this, IDC_LIST1);
BOOL bResultB = m_listb.Create
(dwStyle|LVS_NOCOLUMNHEADER, CRect(0,0,0,0), this, IDC_LIST2);
m_lista.GetHeaderCtrl()->SetDlgCtrlID(IDC_HDR1);
m_listb.GetHeaderCtrl()->SetDlgCtrlID(IDC_HDR2);
COLORREF cr = RGB(255,170,85);
m_listb.SetTextBkColor(cr);
m_listb.SetBkColor(cr);
...
}
Secondly, we modify the OnSize()
function so that the height is equal to that of one row:
void CMysplit2View::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
RECT r;
bool b=m_lista.GetItemRect(m_lista.GetTopIndex(),&r,LVIR_BOUNDS);
ASSERT(b);
int height=r.bottom-r.top;
if (::IsWindow(m_lista.m_hWnd))
m_lista.MoveWindow(0, 0, cx, cy-height, TRUE);
if (::IsWindow(m_listb.m_hWnd))
m_listb.MoveWindow(0, cy-height, cx, cy, TRUE);
}
Step 4: Showing and Hiding the Second Control
Use the Class Wizard to create a button and add a function for it:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnSwitch2ndview();
DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMysplit2View, CView)
...
ON_COMMAND(ID_SWITCH2NDVIEW, OnSwitch2ndview)
...
END_MESSAGE_MAP()
void CMysplit2View::OnSwitch2ndview()
{
ShowSeconList(!IsSecondListVisible());
}
(The logic inside this function will be explained below.)
Now, we need to add a few member variables and methods:
public:
bool b_listb_enabled;
bool IsSecondListVisible() {return b_listb_enabled;}
void ShowSeconList(bool Enable=true);
protected:
int m_height;
int m_cx;
int m_cy;
void RepositionControls();
CMysplit2View::CMysplit2View() : b_listb_enabled(false)
{
}
void CMysplit2View::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
RECT r;
bool b=m_lista.GetItemRect(m_lista.GetTopIndex(),&r,LVIR_BOUNDS);
ASSERT(b);
m_height = r.bottom-r.top;
m_cx = cx;
m_cy = cy;
RepositionControls();
}
void CMysplit2View::RepositionControls()
{
int height= IsSecondListVisible() ? m_height : 0;
if (::IsWindow(m_lista.m_hWnd))
m_lista.MoveWindow(0, 0, m_cx, m_cy-height, TRUE);
if (::IsWindow(m_listb.m_hWnd))
m_listb.MoveWindow(0, m_cy-height, m_cx, m_cy, TRUE);
}
void CMysplit2View::ShowSeconList(bool Enable)
{
b_listb_enabled = Enable;
RepositionControls();
}
The changes are transparent; I can only note that I refactored the positioning code into a new function, RepositionControls()<code>
.
Step 4: Synchronized Scrolling
Use the wizard in the workspace view to add a new class derived from CListCtrl
. (In principle, you could add the class manually.) Use Class Wizard to add a handler for WM_HSCROLL
:
CListCtrlEx::CListCtrlEx() {}
CListCtrlEx::~CListCtrlEx() {}
BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
void CListCtrlEx::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
CListCtrl::OnHScroll(nSBCode, nPos, pScrollBar);
m_view->ListA_OnHScroll(nSBCode,nPos,pScrollBar);
}
Change the class of list controls, and make CListCtrlEx
a friend of CMySplit2View
(a friend can access private
and protected
members):
...
#include "ListCtrlEx.h"
...
class CMysplit2View : public CView
{
friend class CListCtrlEx;
...
CListCtrlEx m_lista;
CListCtrlEx m_listb;
...
virtual void ListA_OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
CMysplit2View::CMysplit2View() :b_listb_enabled(false)
{
m_lista.m_view=this;
m_listb.m_view=this;
}
void CMysplit2View::ListA_OnHScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar )
{
int dx = m_lista.GetScrollPos(SB_HORZ) - m_listb.GetScrollPos(SB_HORZ);
if(dx)
{
m_listb.Scroll(dx);
}
}
Step 5: Pointing to a Row
To be continued...
History
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.