Click here to Skip to main content
15,881,139 members
Articles / Desktop Programming / WTL

ListCtrl - A WTL list control with Windows Vista style item selection

Rate me:
Please Sign up or sign in to vote.
4.91/5 (107 votes)
18 Apr 2006CPOL9 min read 515.3K   11.7K   259  
A flexible WTL list control that supports Windows Vista style selection and cell editing.
#pragma once

#include <atltheme.h>
#include "ListTypes.h"

class CListCombo : public CWindowImpl< CListCombo, CComboBox >
{
public:
	CListCombo() : m_wndEditCtrl( this, 1 )
	{
		m_nItem = NULL_ITEM;
		m_nSubItem = NULL_SUBITEM;
		m_nFlags = ITEM_FLAGS_NONE;
		m_nExitChar = 0;
		m_bMouseOver = FALSE;
		m_bActivate = FALSE;
		m_bShowThemed = TRUE;
	}
	
	~CListCombo()
	{
	}

protected:
	int m_nItem;
	int m_nSubItem;
	UINT m_nFlags;
	TCHAR m_nExitChar;
	BOOL m_bShowThemed;
	BOOL m_bMouseOver;
	BOOL m_bActivate;
	BOOL m_bSwappedButtons;
	CRect m_rcStatic;
	CRect m_rcButton;
	COLORREF m_rgbStaticBackground;
	COLORREF m_rgbStaticText;
	
	CTheme m_thmCombo;
	CFont m_fntComboFont;
	CContainedWindowT< CEdit > m_wndEditCtrl;
	
public:
	BOOL Create( HWND hWndParent, int nItem, int nSubItem, CRect& rcRect, UINT nFlags, LPCTSTR lpszItemText, BOOL bShowThemed, CListArray < CString >& aComboList )
	{
		m_nItem = nItem;
		m_nSubItem = nSubItem;
		m_nFlags = nFlags;
		m_nExitChar = 0;
		m_bActivate = FALSE;
		m_bShowThemed = bShowThemed;
		
		m_bSwappedButtons = GetSystemMetrics( SM_SWAPBUTTON );
		m_rgbStaticBackground = GetSysColor( COLOR_HIGHLIGHT );
		m_rgbStaticText = GetSysColor( COLOR_HIGHLIGHTTEXT );
		
		// destroy old combo control...
		if ( IsWindow() )
			DestroyWindow();
			
		DWORD dwStyle = WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL | CBS_AUTOHSCROLL | CBS_SORT;
		
		if ( nFlags & ITEM_FLAGS_COMBO_EDIT )
			dwStyle |= CBS_DROPDOWN;
		else
			dwStyle |= CBS_DROPDOWNLIST;
			
		if ( nFlags & ITEM_FLAGS_EDIT_UPPER )
			dwStyle |= CBS_UPPERCASE;
		
		// create combo control
		if ( CWindowImpl< CListCombo, CComboBox >::Create( hWndParent, CRect( ( ( dwStyle & CBS_DROPDOWNLIST ) == CBS_DROPDOWNLIST ) ? rcRect.left + 3 : rcRect.left + 1, rcRect.top, rcRect.right, rcRect.bottom + ( 6 * rcRect.Height() ) ), NULL, dwStyle ) == NULL )
			return FALSE;
		
		// open combo theme
		if ( m_thmCombo.IsThemeNull() )
			m_thmCombo.OpenThemeData( m_hWnd, L"COMBOBOX" );
		
		// get system message font
		CLogFont logFont;
		logFont.SetMessageBoxFont();
		if ( !m_fntComboFont.IsNull() )
			m_fntComboFont.DeleteObject();
		if ( m_fntComboFont.CreateFontIndirect( &logFont ) == NULL )
			return FALSE;
		SetFont( m_fntComboFont, FALSE );
		
		// subclass edit control to capture keyboard input
		HWND hEditControl = GetWindow( GW_CHILD );
		if ( hEditControl != NULL )
			m_wndEditCtrl.SubclassWindow( hEditControl );
				
		for ( int nComboItem = 0; nComboItem < aComboList.GetSize(); nComboItem++ )
			AddString( aComboList[ nComboItem ] );
		
		if ( ( dwStyle & CBS_DROPDOWNLIST ) == CBS_DROPDOWNLIST )
		{
			int nIndex = FindStringExact( -1, lpszItemText );
			if ( nIndex != CB_ERR )
				SetCurSel( nIndex );
		}
		else
		{
			SetWindowText( lpszItemText );
			SetEditSel( 0, -1 );
		}
		
		// set static edit height
		SetItemHeight( -1, rcRect.Height() - 6 );
		
		COMBOBOXINFO infoComboBox = { sizeof( COMBOBOXINFO ) };
		if ( !::GetComboBoxInfo( m_hWnd, &infoComboBox ) )
			return FALSE;
		
		// store combobox details for painting		
		m_rcStatic = infoComboBox.rcItem;
		m_rcButton = infoComboBox.rcButton;
		m_rcButton.DeflateRect( 0, 1 );
		m_rcButton.OffsetRect( -2, 0 );
		
		// show combo control
		ShowWindow( SW_SHOW );

		SetFocus();
		
		// force redraw now
		RedrawWindow();
		
		return TRUE;
	}
	
	BOOL IsValid( TCHAR nChar )
	{
		// validate number and float input
		if ( !( m_nFlags & ( ITEM_FLAGS_EDIT_NUMBER | ITEM_FLAGS_EDIT_FLOAT ) ) || nChar == VK_BACK )
			return TRUE;
		
		CString strValue;
		int nValueLength = GetWindowTextLength() + 1;
		GetWindowText( strValue.GetBuffer( nValueLength ), nValueLength );
		strValue.ReleaseBuffer();
		
		// get selected positions
		DWORD dwSelection = GetEditSel();		
		int nStartChar = LOWORD( dwSelection );
		int nEndChar = HIWORD( dwSelection );
		
		// are we changing the sign?
		if ( ( m_nFlags & ITEM_FLAGS_EDIT_NEGATIVE ) && nChar == _T( '-' ) )
		{
			BOOL bNegative = FALSE;
			if ( m_nFlags & ITEM_FLAGS_EDIT_FLOAT )
			{
				double dblValue = _tstof( strValue );
				bNegative = ( dblValue < 0 );
				strValue.Format( _T( "%lf" ), -dblValue );
			}
			else
			{
				long lValue = _ttol( strValue );
				bNegative = ( lValue < 0 );
				strValue.Format( _T( "%ld" ), -lValue );
			}
			
			SetWindowText( strValue );
			
			// restore select position
			SetEditSel( bNegative ? nStartChar - 1 : nStartChar + 1, bNegative ? nEndChar - 1 : nEndChar + 1 );
			return FALSE;
		}
		
		// construct new value string using entered character
		CString strNewValue = strValue.Left( nStartChar ) + nChar + strValue.Right( strValue.GetLength() - nEndChar );
		
		int nGreaterThan = 0;
		int nLessThan = 0;
		int nEquals = 0;
		int nDecimalPoint = 0;
		
		int nNegativeIndex = -1;
		int nGreaterIndex = -1;
		int nLessIndex = -1;
		int nEqualIndex = -1;
		int nDecimalIndex = -1;
		int nDigitIndex = -1;
		
		for ( int nCharIndex = 0; nCharIndex < strNewValue.GetLength(); nCharIndex++ )
		{
			TCHAR nCharValue = strNewValue[ nCharIndex ];
			switch ( nCharValue )
			{
				case _T( '-' ):	nNegativeIndex = nCharIndex;
								break;
				case _T( '>' ):	if ( !( m_nFlags & ITEM_FLAGS_EDIT_OPERATOR ) )
									return FALSE;
								nGreaterIndex = nCharIndex;
								nGreaterThan++;
								break;
				case _T( '<' ):	if ( !( m_nFlags & ITEM_FLAGS_EDIT_OPERATOR ) )
									return FALSE;
								nLessIndex = nCharIndex;
								nLessThan++;
								break;
				case _T( '=' ):	if ( !( m_nFlags & ITEM_FLAGS_EDIT_OPERATOR ) )
									return FALSE;
								nEqualIndex = nCharIndex;
								nEquals++;
								break;
				case _T( '.' ):	if ( !( m_nFlags & ITEM_FLAGS_EDIT_FLOAT ) )
									return FALSE;
								nDecimalIndex = nCharIndex;
								nDecimalPoint++;
								break;
				default:		if ( !_istdigit( nCharValue ) )
									return FALSE;
								if ( nDigitIndex < 0 )
									nDigitIndex = nCharIndex;
								break;
			}

			// invalid if text contains more than one '>', '<', '=' or '.'
			if ( nGreaterThan > 1 || nLessThan > 1 || nEquals > 1 || nDecimalPoint > 1 )
				return FALSE;
		}

		// invalid if text contains '=>' or '=<'
		if ( nGreaterIndex != -1 && nEqualIndex != -1 && nGreaterIndex > nEqualIndex )
			return FALSE;
		if ( nLessIndex != -1 && nEqualIndex != -1 && nLessIndex > nEqualIndex )
			return FALSE;

		// invalid if digits exist before operator
		if ( nDigitIndex != -1 && nGreaterIndex != -1 && nGreaterIndex > nDigitIndex )
			return FALSE;
		if ( nDigitIndex != -1 && nLessIndex != -1 && nLessIndex > nDigitIndex )
			return FALSE;
		if ( nDigitIndex != -1 && nEqualIndex != -1 && nEqualIndex > nDigitIndex )
			return FALSE;
		if ( nDigitIndex != -1 && nNegativeIndex != -1 && nNegativeIndex > nDigitIndex )
			return FALSE;
		
		return TRUE;
	}
	
	BEGIN_MSG_MAP_EX(CListCombo)
		MESSAGE_RANGE_HANDLER_EX(WM_MOUSEFIRST,WM_MOUSELAST,OnMouseRange)
		MSG_WM_MOUSELEAVE(OnMouseLeave)
		MSG_WM_ERASEBKGND(OnEraseBkgnd)
		MSG_WM_PAINT(OnPaint)
		REFLECTED_COMMAND_CODE_HANDLER_EX(CBN_KILLFOCUS, OnKillFocus)
		MSG_WM_GETDLGCODE(OnGetDlgCode)
		MSG_WM_CHAR(OnChar)
		DEFAULT_REFLECTION_HANDLER()
	ALT_MSG_MAP(1)
		MSG_WM_GETDLGCODE(OnGetDlgCode)
		MSG_WM_CHAR(OnChar)
	END_MSG_MAP_EX()
	
	LRESULT OnMouseRange( UINT nMessage, WPARAM wParam, LPARAM lParam )
	{
		SetMsgHandled( FALSE );
		
		if ( nMessage == WM_MOUSEMOVE && !m_bMouseOver )
		{
			m_bMouseOver = TRUE;
			
			TRACKMOUSEEVENT trkMouse;
			trkMouse.cbSize = sizeof( TRACKMOUSEEVENT );
			trkMouse.dwFlags = TME_LEAVE;
			trkMouse.hwndTrack = m_hWnd;
			
			// notify when the mouse leaves button
			_TrackMouseEvent( &trkMouse );
		}
		
		// do not show button as pressed when first created
		m_bActivate = TRUE;
		
		InvalidateRect( m_rcButton );
		
		return 0;
	}
	
	void OnMouseLeave()
	{
		m_bMouseOver = FALSE;
		InvalidateRect( m_rcButton );
	}
	
	BOOL OnEraseBkgnd( CDCHandle dcErase ) 
	{
		return TRUE;
	}
	
	void OnPaint( HDC )
	{
		CPaintDC dcPaint( m_hWnd );
		
		CRect rcClient;
		GetClientRect( rcClient );
		
		CMemoryDC dcMemory( dcPaint.m_hDC, rcClient );
		
		CRect rcClip;
		if ( dcPaint.GetClipBox( rcClip ) == ERROR )
			return;
			
		int nContextState = dcMemory.SaveDC();		
		
		// do not repaint background if drawing button only
		if ( !rcClip.EqualRect( m_rcButton ) )
		{
			CWindow wndParent( GetParent() );
			if ( wndParent.IsWindow() )
			{
				// draw background from parent
				CPoint ptOrigin( 0 );
				MapWindowPoints( wndParent, &ptOrigin, 1 );
				dcMemory.OffsetWindowOrg( ptOrigin.x, ptOrigin.y, &ptOrigin );
				wndParent.SendMessage( WM_PAINT, (WPARAM)dcMemory.m_hDC );
				dcMemory.SetWindowOrg( ptOrigin );
			}
		}
		
		DWORD dwPoint = GetMessagePos();
		CPoint ptMouse( GET_X_LPARAM( dwPoint ), GET_Y_LPARAM( dwPoint ) );
		ScreenToClient( &ptMouse );
		
		DWORD dwStyle = GetStyle();	
		BOOL bHotButton = m_bActivate && ( ( ( dwStyle & CBS_DROPDOWNLIST ) == CBS_DROPDOWNLIST ) ? rcClient.PtInRect( ptMouse ) : m_rcButton.PtInRect( ptMouse ) );
		BOOL bPressed = bHotButton && ( GetAsyncKeyState( m_bSwappedButtons ? VK_RBUTTON : VK_LBUTTON ) < 0 );
		
		if ( ( dwStyle & CBS_DROPDOWNLIST ) == CBS_DROPDOWNLIST && !rcClip.EqualRect( m_rcButton ) )
		{
			dcMemory.SetBkColor( m_rgbStaticBackground );
			dcMemory.ExtTextOut( m_rcStatic.left, m_rcStatic.top, ETO_OPAQUE, m_rcStatic, _T( "" ), 0, NULL );
			
			// draw static text
			int nIndex = GetCurSel();
			if ( nIndex != CB_ERR )
			{
				CString strText;
				GetLBText( nIndex, strText );
			
				if ( !strText.IsEmpty() )
				{
					CRect rcText( m_rcStatic );
					rcText.OffsetRect( 1, 1 );					
					dcMemory.SelectFont( m_fntComboFont );
					dcMemory.SetTextColor( m_rgbStaticText );
					dcMemory.SetBkMode( TRANSPARENT );
					dcMemory.DrawText( strText, strText.GetLength(), rcText, DT_LEFT | DT_SINGLELINE | DT_NOPREFIX | DT_VCENTER );
				}
			}
		}
		
		// draw drop down button
		if ( m_bShowThemed && !m_thmCombo.IsThemeNull() )
			m_thmCombo.DrawThemeBackground( dcMemory, CP_DROPDOWNBUTTON, bPressed ? CBXS_PRESSED : ( bHotButton ? CBXS_HOT : CBXS_NORMAL ), m_rcButton );
		else
			dcMemory.DrawFrameControl( m_rcButton, DFC_SCROLL, DFCS_SCROLLDOWN | ( bPressed ? DFCS_FLAT | DFCS_PUSHED : 0 ) );

		dcMemory.RestoreDC( nContextState );
	}
	
	void OnKillFocus( UINT uCode, int nCtrlID, HWND hwndCtrl )
	{
		SetMsgHandled( FALSE );
		
		CWindow wndParent( GetParent() );
		if ( wndParent.IsWindow() )
		{
			CString strValue;
			
			if ( ( GetStyle() & CBS_DROPDOWNLIST ) == CBS_DROPDOWNLIST )
			{
				int nIndex = GetCurSel();
				if ( nIndex != CB_ERR )
					GetLBText( nIndex, strValue );
			}
			else
			{
				int nValueLength = GetWindowTextLength() + 1;
				GetWindowText( strValue.GetBuffer( nValueLength ), nValueLength );
				strValue.ReleaseBuffer();
			}
			
			CListNotify listNotify;
			listNotify.m_hdrNotify.hwndFrom = m_hWnd;
			listNotify.m_hdrNotify.idFrom = GetDlgCtrlID();
			listNotify.m_hdrNotify.code = LCN_ENDEDIT;
			listNotify.m_nItem = m_nItem;
			listNotify.m_nSubItem = m_nSubItem;
			listNotify.m_nExitChar = m_nExitChar;
			listNotify.m_lpszItemText = strValue;
			listNotify.m_lpItemDate = NULL;

			// forward notification to parent
			FORWARD_WM_NOTIFY( wndParent, listNotify.m_hdrNotify.idFrom, &listNotify.m_hdrNotify, ::SendMessage );
		}
		
		ShowWindow( SW_HIDE );
	}
	
	UINT OnGetDlgCode( LPMSG lpMessage )
	{
		return DLGC_WANTALLKEYS;
	}
	
	void OnChar( TCHAR nChar, UINT nRepCnt, UINT nFlags )
	{
		switch ( nChar )
		{
			case VK_TAB:
			case VK_RETURN:
			case VK_ESCAPE:	{
								m_nExitChar = nChar;
								CWindow wndParent( GetParent() );
								if ( wndParent.IsWindow() )
									wndParent.SetFocus();
							}
							break;
			default:		SetMsgHandled( !IsValid( nChar ) );
							break;
		}
	}
};

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
United Kingdom United Kingdom
Alan has been developing applications for a very long time (~10 years), but he's not bitter about this at all. My main area of expertise is C++. He lives in Sweden with his beautiful wife, daughter and son and enjoys climbing mountains and kayaking in his spare time (which isn't much).

Comments and Discussions