Click here to Skip to main content
15,891,184 members
Articles / Desktop Programming / MFC

CListCtrl Which Can Show and Hide Columns

Rate me:
Please Sign up or sign in to vote.
4.94/5 (18 votes)
4 Sep 2008CPOL4 min read 109.3K   3.5K   85  
An example of how to implement a column picker in the MFC list control
<ul class="download">
	<li>
		<a href="CListCtrl_column_picker/ListCtrl_Column_Picker.zip">Download source code - 
			72.95 KB</a></li>
</ul>
<h2>Introduction</h2>
<p>Microsoft's <code>CListCtrl</code> has support for displaying data in a grid, 
	but requires a helping hand for handling column selection.</p>
<p>This article will demonstrate the following:</p>
<ul>
	<li>
	How to make a column hidden without deleting it.
	<li>
		How to restore the width and position of the column when showing the column 
		again.</li>
</ul>
<p>The demo application allows us to show / hide columns when right-clicking the 
	column headers.</p>
<p><img height="276" alt="screenshot.png" hspace="0" src="CListCtrl_column_picker/screenshot.png"
		width="493" border="0"></p>
<h2>Background</h2>
<p>There are lots of advanced grid controls that extend the <code>CListCtrl</code>, 
	so it is possible to change the column configuration at runtime. But, because 
	these grid controls can be very complex, it can be difficult to see how they do 
	it.</p>
<P>This article is part of a series, where the final article <a href="CGridListCtrlEx.aspx">
		CGridListCtrlEx</a> combines the details of all the articles.
</P>
<h2>How To Implement a Column Picker in CListCtrl</h2>
<p>Usually, there are two approaches for implementing the ability to dynamically 
	hide and show columns in a <code>CListCtrl</code>:</p>
<ul>
	<li>
	Maintaining two lists of columns. A list of columns that are displayed, and a 
	list of all available columns. When having to show a column, we will have to 
	provide all the necessary details to insert the column in the displayed list.
	<li>
		Change the width of hidden columns to zero pixels, so they are not shown. When 
		having to show a column, we just have to resize it back to its original size.</li>
</ul>
<p>The second approach is the one described in this article, and it introduces some 
	issues we have to take care of:</p>
<ul>
	<li>
	Show and hide columns while preserving the original width and position.
	<li>
	Prevent the user from being able to resize columns that are hidden.
	<li>
	Prevent the user from being able to drag columns that are hidden.
	<li>
	When columns are inserted or deleted, we have to update the visible state of 
	the columns.
	<li>
		Persistence of the column configuration has to include the show / hide state of 
		each column (not part of this article).</li>
</ul>
<h3>Show and Hide Columns</h3>
<p>The solution of hiding a column by changing the width to zero seems easy, but 
	there is also some extra work:</p>
<ul>
	<li>
	Have to maintain a secondary list which tells what columns are visible, which 
	is used to prevent the user from messing with the hidden columns.
	<li>
		When hiding a column, it must be moved to the beginning of the <code>CHeaderCtrl</code>
	display order list, so the hidden columns will not interfere with the visible 
	columns (E.g. prevent resize of visible columns, because hidden columns are in 
	between).
	<li>
		When showing a column, we must restore its position in the <code>CHeaderCtrl</code>
		display order list, and at the same time restore its width.</li>
</ul>
<pre lang="C++">BOOL CListCtrl_Column_Picker::ShowColumn(int nCol, bool bShow)
{
    SetRedraw(FALSE);

    ColumnState&amp; columnState = GetColumnState(nCol);

    int nColCount = GetHeaderCtrl()-&gt;GetItemCount();
    int* pOrderArray = new int[nColCount];
    VERIFY( GetColumnOrderArray(pOrderArray, nColCount) );
    if (bShow)
    {
        // Restore the position of the column
        int nCurIndex = -1;
        for(int i = 0; i &lt; nColCount ; ++i)
        {
            if (pOrderArray[i]==nCol)
                nCurIndex = i;
            else
            if (nCurIndex!=-1)
            {
                // We want to move it to the original position,
                // and after the last hidden column
                if ( (i &lt;= columnState.m_OrgPosition)
                  || !IsColumnVisible(pOrderArray[i])
                   )
                {
                    pOrderArray[nCurIndex] = pOrderArray[i];
                    pOrderArray[i] = nCol;
                    nCurIndex = i;
                }
            }
        }
    }
    else
    {
        // Move the column to the front of the display order list
        int nCurIndex(-1);
        for(int i = nColCount-1; i &gt;=0 ; --i)
        {
            if (pOrderArray[i]==nCol)
            {
                // Backup the current position of the column
                columnState.m_OrgPosition = i;
                nCurIndex = i;
            }
            else
            if (nCurIndex!=-1)
            {
                pOrderArray[nCurIndex] = pOrderArray[i];
                pOrderArray[i] = nCol;
                nCurIndex = i;
            }
        }
    }

    VERIFY( SetColumnOrderArray(nColCount, pOrderArray) );
    delete [] pOrderArray;

    if (bShow)
    {
        // Restore the column width
        columnState.m_Visible = true;
        VERIFY( SetColumnWidth(nCol, columnState.m_OrgWidth) );
    }
    else
    {
        // Backup the column width
        int orgWidth = GetColumnWidth(nCol);
        VERIFY( SetColumnWidth(nCol, 0) );
        columnState.m_Visible = false;
        columnState.m_OrgWidth = orgWidth;
    }
    SetRedraw(TRUE);
    Invalidate(FALSE);
    return TRUE;
}</pre>
<h3>Prevent Resizing of Hidden Columns</h3>
<p>We have to block the resize events for the hidden columns. This is done by 
	intercepting the resize event (<code>HDN_BEGINTRACK</code>) for the <code>CHeaderCtrl</code>. 
	We also want to block any mistaken resizing of the hidden columns (<code>LVM_SETCOLUMNWIDTH</code>).</p>
<pre lang="C++">BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
    ON_MESSAGE(LVM_SETCOLUMNWIDTH, OnSetColumnWidth)
    ON_NOTIFY_EX(HDN_BEGINTRACKA, 0, OnHeaderBeginResize)
    ON_NOTIFY_EX(HDN_BEGINTRACKW, 0, OnHeaderBeginResize)
END_MESSAGE_MAP()

BOOL CListCtrl_Column_Picker::OnHeaderBeginResize(UINT, NMHDR* pNMHDR, LRESULT* pResult)
{
    // Check that column is allowed to be resized
    NMHEADER* pNMH = (NMHEADER*)pNMHDR;
    int nCol = (int)pNMH-&gt;iItem;
    if (!IsColumnVisible(nCol))
    {
        *pResult = TRUE;    // Block resize
        return TRUE;        // Block event
    }
    return FALSE;
}

LRESULT CListCtrl_Column_Picker::OnSetColumnWidth(WPARAM wParam, LPARAM lParam)
{
    // Check that column is allowed to be resized
    int nCol = (int)wParam;
    if (!IsColumnVisible(nCol))
    {
        return FALSE;
    }

    // Let the CListCtrl handle the event
    return DefWindowProc(LVM_SETCOLUMNWIDTH, wParam, lParam);
}</pre>
<p>This doesn't handle the situation where one can double-click the divider area of 
	the column, which causes the column to resize according to its entire content. 
	Handling the resize event (LVM_SETCOLUMNWIDTH) doesn't prevent the resizing. 
	One have have to handle the messsage HDN_DIVIDERDBLCLICK to block this 
	resizing:
</p>
<PRE lang="cpp">
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
	ON_NOTIFY_EX(HDN_DIVIDERDBLCLICKA, 0, OnHeaderDividerDblClick)
	ON_NOTIFY_EX(HDN_DIVIDERDBLCLICKW, 0, OnHeaderDividerDblClick)
END_MESSAGE_MAP()

BOOL CListCtrl_Column_Picker::OnHeaderDividerDblClick(UINT, NMHDR* pNMHDR, LRESULT* pResult)
{
	NMHEADER* pNMH = (NMHEADER*)pNMHDR;
	SetColumnWidthAuto(pNMH-&gt;iItem);
	return TRUE;	// Don't let parent handle the event
}
</PRE>
<p>There is also a special shortcut key we can press in any <code>CListCtrl</code>, 
	which causes all columns to resize according to the widest <code>string </code>(CTRL+Numeric-plus). 
	One would think the handling of the resize event (<code>LVM_SETCOLUMNWIDTH</code>) 
	will take off this shortcut, but sadly enough, this shortcut has to be handled 
	explicitly.</p>
<pre lang="C++">BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
    ON_WM_KEYDOWN()        // OnKeydown
END_MESSAGE_MAP()

void CListCtrl_Column_Picker::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    switch(nChar)
    {
        case VK_ADD:    // CTRL + NumPlus (Auto size all columns)
        {
            if (GetKeyState(VK_CONTROL) &lt; 0)
            {
                // Special handling to avoid showing "hidden" columns
                SetColumnWidthAuto(-1);
                return;
            }
        } break;
    }
    CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}

BOOL CListCtrl_Column_Picker::SetColumnWidthAuto(int nCol, bool includeHeader)
{
    if (nCol == -1)
    {
        for(int i = 0; i &lt; GetHeaderCtrl()-&gt;GetItemCount() ; ++i)
        {
            SetColumnWidthAuto(i, includeHeader);
        }
        return TRUE;
    }
    else
    {
        if (includeHeader)
            return SetColumnWidth(nCol, LVSCW_AUTOSIZE_USEHEADER);
        else
            return SetColumnWidth(nCol, LVSCW_AUTOSIZE);
    }
}</pre>
<h3>Prevent the Drag of Hidden Columns
</h3>
<p>We cannot drag a column if the width is zero, but we still have to ensure that 
	when other columns are dragged, they are not dragged in between the hidden 
	ones. This is done by intercepting the end drag event (<code>HDN_ENDDRAG</code>) 
	for the <code>CHeaderCtrl</code>.</p>
<pre lang="C++">BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
    ON_NOTIFY_EX(HDN_ENDDRAG, 0, OnHeaderEndDrag)
END_MESSAGE_MAP()

BOOL CListCtrl_Column_Picker::OnHeaderEndDrag(UINT, NMHDR* pNMHDR, LRESULT* pResult)
{
    NMHEADER* pNMH = (NMHEADER*)pNMHDR;
    if (pNMH-&gt;pitem-&gt;mask &amp; HDI_ORDER)
    {
        // Correct iOrder so it is just after the last hidden column
        int nColCount = GetHeaderCtrl()-&gt;GetItemCount();
        int* pOrderArray = new int[nColCount];
        VERIFY( GetColumnOrderArray(pOrderArray, nColCount) );

        for(int i = 0; i &lt; nColCount ; ++i)
        {
            if (IsColumnVisible(pOrderArray[i]))
            {
                pNMH-&gt;pitem-&gt;iOrder = max(pNMH-&gt;pitem-&gt;iOrder,i);
                break;
            }
        }
        delete [] pOrderArray;
    }
    return FALSE;
}</pre>
<h3>Inserting and Deleting Columns</h3>
<p>We have to keep the list of column-visible-state synchronized with the actual 
	list of displayed columns. This could be done by providing a custom method for 
	inserting / deleting columns, which also updates the column state list. Another 
	approach is to monitor for the events issued when a column is inserted / 
	deleted.</p>
<pre lang="C++">BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
    ON_MESSAGE(LVM_DELETECOLUMN, OnDeleteColumn)
    ON_MESSAGE(LVM_INSERTCOLUMN, OnInsertColumn)
END_MESSAGE_MAP()

LRESULT CListCtrl_Column_Picker::OnDeleteColumn(WPARAM wParam, LPARAM lParam)
{
    // Let the CListCtrl handle the event
    LRESULT lRet = DefWindowProc(LVM_DELETECOLUMN, wParam, lParam);
    if (lRet == FALSE)
        return FALSE;

    // Book keeping of columns
    DeleteColumnState((int)wParam);
    return lRet;
}

LRESULT CListCtrl_Column_Picker::OnInsertColumn(WPARAM wParam, LPARAM lParam)
{
    // Let the CListCtrl handle the event
    LRESULT lRet = DefWindowProc(LVM_INSERTCOLUMN, wParam, lParam);
    if (lRet == -1)
        return -1;

    int nCol = (int)lRet;

    // Book keeping of columns
    if (GetColumnStateCount() &lt; GetHeaderCtrl()-&gt;GetItemCount())
        InsertColumnState((int)nCol, true);    // Insert as visible

    return lRet;
}</pre>
<h2>Using the Code</h2>
<p>The source code provided includes a simple implementation of a <code>CListCtrl</code>, 
	which implements the above solution for showing and hiding columns (<code>CListCtrl_Column_Picker</code>).</p>
<h2>Points of Interest</h2>
<p>The demo application demonstrates a little oddity in the <code>CListCtrl</code>. 
	When hiding a different column than the label column (first column), the 
	label-column will change its margins a little. This does not happen with other 
	columns, so once again, the label-column ruins the perfect picture. We could 
	consider creating the label column as hidden by default (and add a block for 
	enabling it) thus avoiding the small quirks of the label column.</p>
<p>The demo application also shows a little visual quirk introduced by using this 
	way of showing / hiding columns. If hiding a column, then move the mouse to the 
	start of the first column-header, so the mouse icon will change as we should be 
	able to resize the column.</p>
<h2>History</h2>
<ul>
	<li>
		<strong>2008-08-23</strong>
		<ul>
			<li>
				First release of the article</li>
		</ul>
	<li>
		<strong>2008-08-25</strong>
		<ul>
			<li>
			Fixed a bug in the source code where it wouldn't restore the original position 
			correctly (could place the visible column between hidden columns)
			<li>
				Updated the article about making sure to keep the hidden columns at the 
				beginning of the <code>CHeaderCtrl </code>display order list</li>
		</ul>
	<li>
		<strong>2008-09-04</strong>
		<ul>
			<li>
				Fixed a bug where a hidden column could be resized, when double clicking the 
				header divider.</li>
		</ul>
	</li>
</ul>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Denmark Denmark
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions