Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Another WTL Grid

, 1 Jun 2003
A WTL Grid mostly aimed for use against databases.
wtlgrid2_demo.zip
CodeProject
codeproject_template
Northwind Demo
Debug
Release
Northwind.exe
res
Northwind.exe.manifest
Northwind.ico
Toolbar.bmp
Simple Demo
Debug
Release
Simple Demo.exe
res
Simple Demo.exe.manifest
Simple Demo.ico
toolbar.bmp
wtlgrid2_src.zip
/////////////////////////////////////////////////////////////////////////////
// WTL Grid Control version 1.0 (June 1st, 2003)
//
// Written by Bj�rnar Henden (bh@bhenden.org)
// Copyright (c) 2003 Bj�rnar Henden.
// Thanks to Bjarke Viksoe for atlgdix.h
// The CGridCtrl::CGridDC class uses some routines by Noel Frankinet.

// This code may be used in compiled form in any way you desire. This
// file may be redistributed by any means PROVIDING it is 
// not sold for profit without the authors written consent, and 
// providing that this notice and the authors name is included. 
//
// This file is provided "as is" with no expressed or implied warranty.
// The author accepts no liability if it causes any damage to you or your
// computer whatsoever.
//
// HISTORY
// 
// Version 1.0 (June 1st, 2003)
//  - Initial release

#pragma once

#ifndef __BHWTLGRID_H__
#define __BHWTLGRID_H__

#ifndef __ATLGDIX_H__
#include "atlgdix.h"
#endif

#ifndef _INC_COMUTIL
#include <comutil.h>
#endif

#ifndef _INC_COMDEF
#include <comdef.h>
#endif

#ifndef __ATLSTR_H__
#error You must include atlstr.h before including atlctrls.h
#endif

#ifndef __ATLCTRLS_H__
#include <atlctrls.h>
#endif

#ifndef __ATLSCRL_H__
#include <atlscrl.h>
#endif

#ifndef __ATLCOLL_H__
#include <atlcoll.h>
#endif

#ifndef __ATLMISC_H__
#define _ATL_TMP_NO_CSTRING
#include <atlmisc.h>
#endif

#define COL_MINWIDTH		20		// Minimum column width
#define COL_NUMWIDTH		32		// Width of row number column

#define GS_EX_LINENUMBERS	0x01	// Show lines numbers
#define GS_EX_CONTEXTMENU	0x02	// Allow context menu
#define GS_EX_READONLY		0x04	// Grid is read only
#define GS_EX_VGRIDLINES	0x08	// Vertical lines
#define GS_EX_HGRIDLINES	0x10	// Horizontal lines

class CGridCtrl : public CWindowImpl<CGridCtrl> {
	friend class CGrid;
	friend class CRow;
	friend class CColumn;
	friend class CCell;
	friend class CEditText;
public:
	DECLARE_WND_CLASS_EX("BHWTLGRID",CS_DBLCLKS,COLOR_BTNFACE)

	BEGIN_MSG_MAP(CGridCtrl)
		MESSAGE_HANDLER(WM_SETREDRAW, OnSetRedraw)
		MESSAGE_HANDLER(WM_CREATE,OnCreate)
		MESSAGE_HANDLER(WM_PAINT, OnPaint)
		MESSAGE_HANDLER(WM_SIZE, OnSize)
		MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
		MESSAGE_HANDLER(WM_MOUSEMOVE, OnMouseMove)
		MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
		MESSAGE_HANDLER(WM_LBUTTONUP, OnLButtonUp)
		MESSAGE_HANDLER(WM_CONTEXTMENU, OnContextMenu)
		COMMAND_ID_HANDLER(ID_GRID_ADDROW, OnAddRow)
		COMMAND_ID_HANDLER(ID_GRID_EDITROW, OnEditRow)
		COMMAND_ID_HANDLER(ID_GRID_DELETEROW, OnDeleteRow)
	END_MSG_MAP()

	// Forward class definitions
public:
	class CListener;
protected:
	class CEditText;
	class CEditDropdownlist;
	class CColumn;
	class CRow;
	class CGrid;
	class CGridDC;
	class CCellNone;
	class CCellText;
	class CCellDropdownlist;
	class CCellDropdown;
	class CCellDate;


// Handler prototypes (uncomment arguments if needed):
//	LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
//	LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
//	LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)

	LRESULT OnSetRedraw(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) {
		//DefWindowProc(uMsg,wParam,lParam);
		if(m_bRedraw = wParam!=FALSE) {
			UpdateScrollSize();
		}
		return 0;
	}

	LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) {
		CFontHandle hFont = AtlGetDefaultGuiFont();
		if(hFont) {
			LOGFONT lf;
			hFont.GetLogFont(lf);

			m_grid.m_font.CreateFontIndirect(&lf);
			lf.lfWeight = FW_BOLD;
			m_grid.m_font_bold.CreateFontIndirect(&lf);
		} else {
			m_grid.m_font.CreateFont(8,0,0,0,FW_NORMAL,0,0,0,0,0,0,0,0,"MS Sans Serif");
			m_grid.m_font_bold.CreateFont(8,0,0,0,FW_BOLD,0,0,0,0,0,0,0,0,"MS Sans Serif");
		}

		m_grid.Create(m_hWnd,rcDefault,NULL,WS_CHILD|WS_VISIBLE|WS_TABSTOP,0);
		return 0;
	}
	
	LRESULT OnSetFocus(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) {
		m_grid.SetFocus();
		return 0;
	}

	LRESULT OnMouseMove(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) {
		CPoint pt((DWORD)lParam);

		if(m_nDragCol>=0) {
			long x = pt.x;
			if(x-COL_MINWIDTH<m_nDragMin) x = m_nDragMin + COL_MINWIDTH;
			m_grid.DoDrag(x,true);
			return 0;
		}

		CPoint ptScroll;
		m_grid.GetScrollOffset(ptScroll);
		pt += ptScroll;

		if(pt.y - ptScroll.y<GetHeaderHeight()) {
			long x = 0;
			if(ShowLineNumbers()) x += COL_NUMWIDTH;
			for(long i=0;i<GetColumnCount();i++) {
				if(m_grid.m_columns[i]->OnEdge(x,pt.x)) {
					SetCursor(LoadCursor(NULL,IDC_SIZEWE));
					return 0;
				}
				x += m_grid.m_columns[i]->m_nWidth;
			}
		}

		SetCursor(LoadCursor(NULL,IDC_ARROW));

		return 0;
	}

	LRESULT OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) {
		CPoint pt((DWORD)lParam);
		if(pt.y<GetHeaderHeight()) {
			CPoint ptScroll;
			m_grid.GetScrollOffset(ptScroll);
			pt += ptScroll;

			long x = 0;
			if(ShowLineNumbers()) x += COL_NUMWIDTH; 
			for(long i=0;i<GetColumnCount();i++) {
				if(m_grid.m_columns[i]->OnEdge(x,pt.x)) {
					SetCursor(LoadCursor(NULL,IDC_SIZEWE));
					m_grid.DoDrag(pt.x - ptScroll.x,false);
					SetCapture();
					m_nDragCol = i;
					m_nDragMin = x - ptScroll.x;
					return 0;
				}
				x += m_grid.m_columns[i]->m_nWidth;
			}
		}
		return 0;
	}
	
	LRESULT OnLButtonUp(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) {
		CPoint pt((DWORD)lParam);
		if(m_nDragCol>=0) {
			long x = pt.x;
			if(x-COL_MINWIDTH<m_nDragMin) x = m_nDragMin + COL_MINWIDTH;

			CPoint ptScroll;
			m_grid.GetScrollOffset(ptScroll);
			pt += ptScroll;

			SetCursor(LoadCursor(NULL,IDC_ARROW));
			m_grid.m_columns[m_nDragCol]->m_nWidth = x - m_nDragMin;
			ReleaseCapture();
			m_nDragCol = -1;
			m_grid.SizeControls();
			Invalidate();
			m_grid.Invalidate();
			
			UpdateScrollSize();
			m_grid.SetScrollOffset(ptScroll);
		}
		return 0;
	}
	
	LRESULT OnPaint(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) {
		if(wParam!=NULL)
			DoPaint((HDC)wParam);
		else {
			CPaintDC dc(m_hWnd);
			DoPaint(dc.m_hDC);
		}

		return 0;
	}
	
	LRESULT OnSize(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) {
		ATLASSERT(IsWindow());
			
		Invalidate();
		long top = GetHeaderHeight();
		m_grid.SetWindowPos(NULL,0,top,GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam)-top,0);
		UpdateScrollSize();

		return 0;
	}
	
	LRESULT OnContextMenu(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& bHandled) {
		if(!(m_grid.m_dwStyle & GS_EX_CONTEXTMENU)) {
			bHandled = FALSE;
			return 0;
		}

		CPoint pt((DWORD)lParam);
		
		if(pt.x<0 && pt.y<0) {
			pt.x = 10;
			pt.y = 10;
			MapWindowPoints(NULL,&pt,1);
		}

		CMenu menu;
		menu.CreatePopupMenu();
		UINT uFlags = 0;
		menu.AppendMenu(MF_STRING|uFlags,ID_GRID_ADDROW,"Add Row\tIns");
		uFlags = m_grid.m_nSelectedRow>=0 ? 0 : MF_GRAYED;
		menu.AppendMenu(MF_STRING|uFlags,ID_GRID_DELETEROW,"Delete Row\tDel");
		menu.AppendMenu(MF_STRING|uFlags,ID_GRID_EDITROW,"Edit Row\tEnter");
		menu.TrackPopupMenu(TPM_LEFTALIGN,pt.x,pt.y,m_hWnd);

		return 0;
	}
	
	LRESULT OnAddRow(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) {
		long row = AddRow();
		if(m_grid.m_pListener) m_grid.m_pListener->OnNewRow(GetWindowLong(GWL_ID),row);
		m_grid.SelectRow(row);
		EnsureVisible(row);
		m_grid.EditRow();
		return 0;
	}

	LRESULT OnEditRow(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) {
		ATLASSERT(m_grid.m_nSelectedRow>=0);
		m_grid.EditRow();
		return 0;
	}

	LRESULT OnDeleteRow(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) {
		if(m_grid.m_nSelectedRow>=0)
			DeleteItem(m_grid.m_nSelectedRow);
		return 0;
	}

	void DoPaint(CDCHandle dc2) {
		if(!m_bRedraw) return;
		CMemDC dc(dc2);
		CGridDC mdc(dc);
		HFONT hSaveFont = dc.SelectFont(m_grid.m_font_bold);
		long x = 0;

		CPoint ptScroll;
		m_grid.GetScrollOffset(ptScroll);
		x -= ptScroll.x;

		CRect rcClient;
		GetClientRect(rcClient);
		dc.FillRect(&rcClient,(HBRUSH)(COLOR_BTNFACE+1));

		if(ShowLineNumbers()) {
			CRect rc(x,0,x+COL_NUMWIDTH,GetHeaderHeight());
			mdc.DrawButton(rc,false);
			x += COL_NUMWIDTH;
		}
		for(long i=0;i<m_grid.m_columns.GetSize();i++) {
			CRect rc(x,0,x+m_grid.m_columns[i]->m_nWidth,GetHeaderHeight());
			mdc.DrawButton(rc,false);
			rc.DeflateRect(2,1);
			rc.bottom = rc.top + m_grid.m_nControlHeight;
			mdc.DrawText(rc,m_grid.m_columns[i]->m_strTitle,m_grid.m_columns[i]->m_nAlign);
			x += m_grid.m_columns[i]->m_nWidth;
		}

		dc.SelectFont(hSaveFont);
	}

	void UpdateScrollSize() {
		ATLASSERT(IsWindow() && m_grid.IsWindow());
		CPoint ptScroll;
		CRect rcClient;
		SIZE sz;

		m_grid.GetClientRect(rcClient);
		m_grid.GetScrollOffset(ptScroll);
		m_grid.SetScrollSize(m_grid.GetClientWidth(true),m_grid.GetClientHeight(true),TRUE);
		m_grid.SetScrollLine(100,GetRowHeight());
		m_grid.SetScrollPage(rcClient.Width(),rcClient.Height() / GetRowHeight() * GetRowHeight());

		m_grid.GetScrollSize(sz);
		if(ptScroll.y + rcClient.Height() >= sz.cy && sz.cy>rcClient.Height())
			ptScroll.y = sz.cy - rcClient.Height();
		m_grid.SetScrollOffset(ptScroll);
	}

	long GetRowHeight() const {
		return m_grid.GetRowHeight();
	}

	long GetHeaderHeight() const {
		return m_grid.GetRowHeight();;
	}

	long GetColumnIndex(LPCTSTR pszName) const {
		long count = m_grid.m_columns.GetSize();
		while(count--) {
			if(!m_grid.m_columns[count]->m_strName.CompareNoCase(pszName))
				return count;
		}
		return -1;
	}
	
	bool ShowLineNumbers() const {
		return m_grid.m_dwStyle & GS_EX_LINENUMBERS;
	}
	
	static bool ChangeType(_variant_t& vt,VARTYPE type) {
		try {
			vt.ChangeType(type);
			return true;
		}
#ifdef _DEBUG
		catch(_com_error& e) {
			CString err;
			err.Format("%s\n%s",(LPCTSTR)e.Description(),(LPCTSTR)e.ErrorMessage());
			AtlMessageBox(GetActiveWindow(),(LPCTSTR)err);
			MessageBeep(MB_ICONEXCLAMATION);
			return false;
		}
#else
		catch(_com_error&) {
			MessageBeep(MB_ICONEXCLAMATION);
			return false;
		}
#endif
	}
	
	static void FormatNum(CString& str,_variant_t vtNum,long nDec) {
		str.Format("%f",(double)vtNum);

		CHAR buf[128], sDummy[16], sDecSep[16], sThowSep[16];

		LCID loc = LOCALE_USER_DEFAULT;
		//LCID loc = MAKELCID(MAKELANGID(LANG_ENGLISH,SUBLANG_ENGLISH_US),SORT_DEFAULT);

		NUMBERFMT nf;
		nf.NumDigits = nDec/*nDecimals*/;
		GetLocaleInfo( loc, LOCALE_ILZERO, sDummy, 16 );
		nf.LeadingZero = atol(sDummy);
		GetLocaleInfo( loc, LOCALE_SGROUPING, sDummy, 16 );
		nf.Grouping = atol(sDummy);
		GetLocaleInfo( loc, LOCALE_SDECIMAL, sDecSep, 16 );
		nf.lpDecimalSep = sDecSep;
		GetLocaleInfo( loc, LOCALE_STHOUSAND, sThowSep, 16 );
		nf.lpThousandSep = sThowSep;
		GetLocaleInfo( loc, LOCALE_INEGNUMBER, sDummy, 16 );
		nf.NegativeOrder = atol(sDummy);

		GetNumberFormat(loc,0,str,&nf,buf,sizeof buf);
		//GetNumberFormat(MAKELCID(MAKELANGID(LANG_ENGLISH,SUBLANG_ENGLISH_US),SORT_DEFAULT),0,tmp,NULL,buf,32);
		str = buf;
	}
	
public:
	enum CONTROLTYPE {
		EDIT_NONE,
		EDIT_TEXT,
		EDIT_DROPDOWNLIST,
		EDIT_DROPDOWN,
		EDIT_DATE,
		EDIT_TIME,
#if 0
		EDIT_CHECK
#endif
	};

	enum ALIGN {
		LEFT,
		CENTER,
		RIGHT
	};

	CGridCtrl() : m_grid(*this) {
		m_nDragCol = -1;
		m_grid.m_pListener = NULL;
		m_bRedraw = true;
	}

	virtual BOOL PreTranslateMessage(MSG* pMsg) {
		return m_grid.PreTranslateMessage(pMsg);
	}
	
	void AddColumn(LPCTSTR pszTitle,long nWidth,CONTROLTYPE nType=EDIT_NONE,ALIGN nAlign=LEFT,VARTYPE vt=VT_BSTR,LPCTSTR pszName=NULL) {
		ATLASSERT(m_grid.m_rows.GetCount()==0 && "Can't add columns after rows have been added.");
		if(!pszName) pszName = pszTitle;
		
		// Force VT_DATE for date formats
		if(nType==EDIT_DATE || nType==EDIT_TIME)
			vt = VT_DATE;
		
		CColumn* col = new CColumn(m_grid);
		col->m_strTitle = pszTitle;
		col->m_nWidth = nWidth;
		col->m_nType = nType;
		col->m_strName = pszName;
		col->m_vt = vt;
		col->m_nAlign = nAlign;
		m_grid.m_columns.Add(col);
		UpdateScrollSize();
		Invalidate();

		col->CreateEdit();
	}
	
	long AddRow() {
		ATLASSERT(m_grid.m_columns.GetSize()>0);
		CRow* row = new CRow(m_grid,m_grid.m_columns.GetSize());
		long nItem = (long)m_grid.m_rows.Add(row);
		if(m_bRedraw) UpdateScrollSize();
		return nItem;
	}
	
	long GetSelectedRow() const {
		return m_grid.m_nSelectedRow;
	}

	void SetItem(long nItem,long nColumn,_variant_t vtValue) {
		ATLASSERT(nItem<GetRowCount());
		ATLASSERT(nColumn<GetColumnCount());
		m_grid.m_rows[nItem]->SetItem(nColumn,vtValue);
		if(m_bRedraw) m_grid.DrawCell(nItem,nColumn);
	}

	void SetItem(long nItem,int nColumn,_variant_t vtValue) {
		SetItem(nItem,(long)nColumn,vtValue);
	}

	void SetItem(long nItem,LPCTSTR pszColumn,_variant_t vtValue) {
		long nColumn = this->GetColumnIndex(pszColumn);
		ATLASSERT(nColumn>=0);
		SetItem(nItem,nColumn,vtValue);
	}

	_variant_t GetItem(long nItem,long nColumn) {
		ATLASSERT(nItem<GetRowCount());
		ATLASSERT(nColumn<GetColumnCount());
		_variant_t vt = m_grid.m_rows[nItem]->GetItem(nColumn);
		ATLASSERT(IsNull(vt) || vt.vt==m_grid.m_columns[nColumn]->m_vt);
		return vt;
	}

	_variant_t GetItem(long nItem,int nColumn) {
		return GetItem(nItem,(long)nColumn);
	}

	_variant_t GetItem(long nItem,LPCTSTR pszColumn) {
		long nColumn = this->GetColumnIndex(pszColumn);
		ATLASSERT(nColumn>=0);
		return GetItem(nItem,nColumn);
	}

	_variant_t GetEditItem(long nColumn) {
		ATLASSERT(nColumn<GetColumnCount());
		ATLASSERT(m_grid.m_bEditing);
		return m_grid.m_columns[nColumn]->GetValue();
	}

	_variant_t GetEditItem(LPCTSTR pszColumn) {
		long nColumn = this->GetColumnIndex(pszColumn);
		ATLASSERT(nColumn>=0);
		return GetEditItem(nColumn);
	}

	void SetColumnFocus(LPCTSTR pszColumn) {
		ATLASSERT(m_grid.m_bEditing);
		long nColumn = this->GetColumnIndex(pszColumn);
		ATLASSERT(nColumn>=0);
		m_grid.m_columns[nColumn]->SetFocus();
	}

	void AddColumnLookup(long nColumn,_variant_t vtID,_variant_t vtDisplay) {
		m_grid.m_columns[nColumn]->AddLookup(vtID,vtDisplay);
	}
	
	void AddColumnLookup(LPCTSTR pszColumn,_variant_t vtID,_variant_t vtDisplay) {
		long nColumn = GetColumnIndex(pszColumn);
		ATLASSERT(nColumn>=0);
		AddColumnLookup(nColumn,vtID,vtDisplay);
	}
	
	void SetListener(CListener* pListener) {
		m_grid.m_pListener = pListener;
	}

	void DeleteAllItems() {
		m_grid.DeleteAllItems();
	}

	void DeleteAllColumns() {
		m_grid.DeleteAllColumns();
	}

	void SetItemData(long nItem,_variant_t vtItemData) {
		ATLASSERT(nItem>=0 && nItem<(long)m_grid.m_rows.GetCount());
		m_grid.m_rows[nItem]->m_vtItemData = vtItemData;
	}

	_variant_t GetItemData(long nItem) const {
		ATLASSERT(nItem>=0 && nItem<(long)m_grid.m_rows.GetCount());
		return m_grid.m_rows[nItem]->m_vtItemData;
	}

	void DeleteItem(long nItem) {
		ATLASSERT(nItem<(long)m_grid.m_rows.GetCount());
		
		if(m_grid.m_pListener && !m_grid.m_pListener->OnDeleteRow(GetWindowLong(GWL_ID),nItem))
			return;

		m_grid.SelectRow(-1);
		delete m_grid.m_rows[nItem];
		m_grid.m_rows.RemoveAt(nItem);
		UpdateScrollSize();
	}

	void EnsureVisible(long nRow) {
		CPoint ptScroll;
		CRect rcClient;
		m_grid.GetScrollOffset(ptScroll);
		m_grid.GetClientRect(rcClient);
		long nRowOffset = nRow * GetRowHeight();
		if(nRowOffset<ptScroll.y)
			ptScroll.y = nRowOffset;
		else if(nRowOffset>ptScroll.y - GetRowHeight() + rcClient.Height())
			ptScroll.y = nRowOffset - rcClient.Height() + GetRowHeight();
		else
			return;
		m_grid.SetScrollOffset(ptScroll);
	}
	
	static bool IsNull(const _variant_t& vt) {
		return vt.vt == VT_EMPTY || vt.vt == VT_NULL;
	}
	
	static void CGridCtrl::SetNull(_variant_t& vt) {
		ChangeType(vt,VT_NULL);
	}
	
	void SetExtendedGridStyle(DWORD dwStyle,DWORD dwMask=0) {
		m_grid.m_dwStyle &= ~dwMask;
		m_grid.m_dwStyle |= dwStyle;
		UpdateScrollSize();
	}
	
	bool GetModified(long nRow=-1) {
		ATLASSERT(nRow<(long)m_grid.m_rows.GetCount());

		if((nRow==m_grid.m_nSelectedRow || nRow<0) && m_grid.GetEditChanges())
			return true;

		if(nRow==-1) {
			long count = (long)m_grid.m_rows.GetCount();
			while(count--) {
				if(m_grid.m_rows[count]->m_bModified)
					return true;
			}
			return false;
		} else {
			return m_grid.m_rows[nRow]->m_bModified;
		}
	}
	
	void ClearModified(long nRow=-1) {
		if(nRow>=0) {
			m_grid.m_rows[nRow]->m_bModified = false;
		} else {
			long count = (long)m_grid.m_rows.GetCount();
			while(count--) m_grid.m_rows[count]->m_bModified = false;
		}
		m_grid.Invalidate();
	}
	
	long GetRowCount() const {
		return (long)m_grid.m_rows.GetCount();
	}

	long GetColumnCount() const {
		return m_grid.m_columns.GetSize();
	}

	// Classes
public:
	class CListener {
	public:
		virtual bool OnRowChanging(UINT uID,long nRow) {
			// Return false to abort row change
			return true;
		}
		
		virtual void OnRowChanged(UINT uID,long nRow) {}
		
		virtual void OnEdit(UINT uID,long nRow) {}
		
		virtual bool OnDeleteRow(UINT uID,long nRow) {
			// Return false to abort delete
			return true;
		}
		
		virtual void OnNewRow(UINT uID,long nRow) {}
		
		virtual void OnModified(UINT uID,LPCTSTR pszColumn,_variant_t vtValue) {}
		
		virtual void OnRowActivate(UINT uID,long nRow) {}
		
		virtual COLORREF GetCellColor(UINT uID,long nRow,LPCTSTR pszColumn) {
			// Return (COLORREF)-1 for default colors
			return (COLORREF)-1;
		}

		virtual bool OnValidate(UINT uID) {
			// Return false if not valid
			return true;
		}
	};
protected:
	class CEditText : public CWindowImpl<CEditText,CEdit> {
	public:
		DECLARE_WND_CLASS_EX(NULL,0,-1)

		BEGIN_MSG_MAP(CEditText)
			MESSAGE_HANDLER(WM_CHAR,OnChar)
			MESSAGE_HANDLER(WM_KILLFOCUS,OnKillFocus)
		END_MSG_MAP()

		CEditText(CColumn& column) : m_column(column) {}

		LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			bHandled = FALSE;
			if(wParam==VK_RETURN || wParam==VK_ESCAPE) {
				m_column.m_grid.EndEdit(wParam==VK_ESCAPE);
				bHandled = TRUE;
			}
			return 0;
		}

		LRESULT OnKillFocus(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			m_column.ValueModified();
			bHandled = FALSE;
			return 0;
		}
	protected:
		CColumn&	m_column;
	}; 

#if 0
	class CEditCheck : public CWindowImpl<CEditCheck,CButton> {
	public:
		DECLARE_WND_CLASS_EX(NULL,0,-1)

		BEGIN_MSG_MAP(CEditCheck)
			MESSAGE_HANDLER(WM_CHAR,OnChar)
			MESSAGE_HANDLER(WM_KILLFOCUS,OnKillFocus)
			MESSAGE_HANDLER(WM_ERASEBKGND,OnEraseBkgnd)
		END_MSG_MAP()

		CEditCheck(CColumn& column) : m_column(column) {}

		LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			bHandled = FALSE;
			if(wParam==VK_RETURN || wParam==VK_ESCAPE) {
				m_column.m_grid.EndEdit(wParam==VK_ESCAPE);
				bHandled = TRUE;
			}
			return 0;
		}

		LRESULT OnKillFocus(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			m_column.ValueModified();
			bHandled = FALSE;
			return 0;
		}

		LRESULT OnEraseBkgnd(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			return 1;
		}
	protected:
		CColumn&	m_column;
	}; 
#endif

	class CEditDropdownlist : public CWindowImpl<CEditDropdownlist,CComboBox> {
	public :
		DECLARE_WND_CLASS_EX(NULL,0,-1)

		BEGIN_MSG_MAP(CEditDropdownlist)
			MESSAGE_HANDLER(WM_KEYDOWN,OnKeyDown)
			MESSAGE_HANDLER(WM_DESTROY,OnDestroy)
			REFLECTED_COMMAND_CODE_HANDLER(CBN_SELCHANGE,OnSelChange)
			DEFAULT_REFLECTION_HANDLER()
		END_MSG_MAP()

		CEditDropdownlist(CColumn& column) : m_column(column) {}
		
		void SetValue(_variant_t vtValue) {
			long count = GetCount();
			while(count--) {
				_variant_t vt = (_variant_t*)GetItemData(count);
				if(vt==vtValue) {
					SetCurSel(count);
					return;
				}
			}
			SetCurSel(-1);
		}

		void ResetContent() {
			long count = GetCount();
			while(count--) {
				_variant_t* pValue = (_variant_t*)GetItemData(count);
				delete pValue;
			}
			CWindowImpl<CEditDropdownlist,CComboBox>::ResetContent();
		}

		LRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			bHandled = FALSE;
			if(!GetDroppedState() && (wParam==VK_RETURN || wParam==VK_ESCAPE)) {
				m_column.m_grid.EndEdit(wParam==VK_ESCAPE);
				bHandled = TRUE;
			}
			return 0;
		}

		LRESULT OnDestroy(UINT,WPARAM,LPARAM,BOOL& bHandled) {
			bHandled = FALSE;
			ResetContent();
			return 0;
		}

		LRESULT OnSelChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled) {
			m_column.ValueModified();
			return 0;
		}
	protected:
		CColumn&	m_column;
	};

	class CEditDropdown : public CWindowImpl<CEditDropdown,CComboBox> {
	public :
		DECLARE_WND_CLASS_EX(NULL,0,-1)

		BEGIN_MSG_MAP(CEditDropdown)
			MESSAGE_HANDLER(WM_CREATE,OnCreate)
			MESSAGE_HANDLER(WM_KEYDOWN,OnKeyDown)
			MESSAGE_HANDLER(WM_DESTROY,OnDestroy)
			REFLECTED_COMMAND_CODE_HANDLER(CBN_SELCHANGE,OnSelChange)
			DEFAULT_REFLECTION_HANDLER()
		END_MSG_MAP()

		CEditDropdown(CColumn& column) : m_column(column), m_wndEdit(*this) {}
		
		void SetValue(_variant_t vtValue) {
			long count = GetCount();
			while(count--) {
				_variant_t vt = (_variant_t*)GetItemData(count);
				if(vt==vtValue) {
					SetCurSel(count);
					return;
				}
			}
			SetCurSel(-1);
		}

		void ResetContent() {
			long count = GetCount();
			while(count--) {
				_variant_t* pValue = (_variant_t*)GetItemData(count);
				delete pValue;
			}
			CWindowImpl<CEditDropdown,CComboBox>::ResetContent();
		}

		LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			DefWindowProc(uMsg,wParam,lParam);
			m_wndEdit.SubclassWindow(GetDlgItem(1001));
			return 0;
		}
		
		LRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
			bHandled = FALSE;
			if(!GetDroppedState() && (wParam==VK_RETURN || wParam==VK_ESCAPE)) {
				m_column.m_grid.EndEdit(wParam==VK_ESCAPE);
				bHandled = TRUE;
			}
			return 0;
		}
		
		LRESULT OnDestroy(UINT,WPARAM,LPARAM,BOOL& bHandled) {
			bHandled = FALSE;
			ResetContent();
			return 0;
		}
		
		LRESULT OnSelChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled) {
			m_column.ValueModified();
			return 0;
		}
	protected:
		class CDropDownEdit : public CWindowImpl<CEditText,CEdit> {
		public:
			DECLARE_WND_CLASS_EX(NULL,0,-1)

			BEGIN_MSG_MAP(CDropDownEdit)
				MESSAGE_HANDLER(WM_CHAR,OnChar)
				MESSAGE_HANDLER(WM_KEYUP,OnKeyUp)
			END_MSG_MAP()

			CDropDownEdit(CEditDropdown& dropdown) : m_dropdown(dropdown) {}
			
			LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
				bHandled = FALSE;
				if(!m_dropdown.GetDroppedState() && (wParam==VK_RETURN || wParam==VK_ESCAPE)) {
					m_dropdown.m_column.m_grid.EndEdit(wParam==VK_ESCAPE);
					bHandled = TRUE;
				}
				return 0;
			}

			LRESULT OnKeyUp(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
				if(wParam==VK_UP || wParam==VK_DOWN || wParam==VK_LEFT || wParam==VK_RIGHT)
					return 0;

				if(wParam==VK_DELETE || wParam==VK_BACK || wParam==VK_HOME || wParam==VK_END || wParam==VK_TAB)
					return 0;

				CString str;
				GetWindowText(str);
				if(str.IsEmpty())
					return 0;

				long i = m_dropdown.FindString(0,str);
				if(i>=0) {
					m_dropdown.SetCurSel(i);
					CString strFull;
					m_dropdown.GetLBText(i,strFull);
					SetWindowText(str + strFull.Mid(str.GetLength()));
					SetSel(str.GetLength(),strFull.GetLength());
				}
				return 0;
			}
		protected:
			CEditDropdown&	m_dropdown;
		}; 

		CColumn&	m_column;
		CDropDownEdit	m_wndEdit;
	};

	class CEditDate : public CWindowImpl<CEditDate,CDateTimePickerCtrl> {
	public :
		DECLARE_WND_CLASS_EX(NULL,0,-1)

		BEGIN_MSG_MAP(CEditDate)
		//	MESSAGE_HANDLER(WM_KEYDOWN,OnKeyDown)
			REFLECTED_NOTIFY_CODE_HANDLER(DTN_DATETIMECHANGE,OnDateTimeChange)
			DEFAULT_REFLECTION_HANDLER()
		END_MSG_MAP()

		CEditDate(CColumn& column) : m_column(column) {}
		
		//void SetValue(long value);

		//LRESULT OnKeyDown(UINT	/*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled);

		LRESULT OnDateTimeChange(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) {
			m_column.ValueModified();
			return 0;
		}
	protected:
		CColumn&	m_column;
	};

	class CCell {
	public:
		CCell(CONTROLTYPE type,CColumn& column) : m_type(type), m_column(column) {}

		virtual void Create(CFontHandle hFont) {
			m_hWnd = NULL;
		}

		virtual _bstr_t GetDisplayText(const _variant_t vtValue) {
			if(IsNull(vtValue))
				return _bstr_t();

			long count = m_column.m_lookup.GetSize();
			if(count>0) {
				while(count--) {
					if(m_column.m_lookup[count]->m_vtID == vtValue)
						return m_column.m_lookup[count]->m_vtDisplay;
				}
				CString str;
				str.Format("[%s]",(LPCTSTR)(_bstr_t)vtValue);
				return (_bstr_t)str;
			} else if(m_column.m_vt == VT_R8) {
				CString str;
				FormatNum(str,vtValue,2);
				return (_bstr_t)str;
			}

			return (_bstr_t)vtValue;
		}

		virtual void SetWindowPos(RECT& rc,DWORD dwSWPFlags) {
			if(!m_hWnd) return;
			CWindow wnd(m_hWnd);
			wnd.SetWindowPos(NULL,&rc,dwSWPFlags);
		}

		virtual void SetValue(_variant_t vtValue) {
			m_column.m_vtValue = vtValue;	// FIX
		}

		virtual void SetFocus() {
		}

		virtual _variant_t GetValue() {
			return m_column.m_vtValue;
		}

		virtual void Draw(CDCHandle dc,CRect& rc,const _variant_t& vtValue) {
			CString str = (LPCTSTR)(_bstr_t)GetDisplayText(vtValue);

			CGridDC mdc(dc);
			mdc.DrawText(rc,str,m_column.m_nAlign);
		}
	
		CONTROLTYPE	m_type;
		HWND		m_hWnd;
		CColumn&	m_column;
	};

	class CCellNone : public CGridCtrl::CCell {
	public:
		CCellNone(CColumn& column) : CCell(EDIT_NONE,column) {
		}
	};

	class CCellText : public CGridCtrl::CCell {
	public:
		CCellText(CColumn& column,ALIGN nAlign) : CCell(EDIT_TEXT,column), m_wndEdit(column) {
		}

		virtual void Create(CFontHandle hFont) {
			DWORD dwFlags = 0;
			if(m_column.m_nAlign==RIGHT)
				dwFlags |= ES_RIGHT;
			else if(m_column.m_nAlign==CENTER)
				dwFlags |= ES_CENTER;

			m_hWnd = m_wndEdit.Create(m_column.m_grid,rcDefault,NULL,WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL|dwFlags,WS_EX_CLIENTEDGE);
			m_wndEdit.SetFont(hFont);
		}

		virtual void SetValue(_variant_t vtValue) {
			CCell::SetValue(vtValue);
			if(IsNull(vtValue))
				m_wndEdit.SetWindowText("");
			else
				m_wndEdit.SetWindowText((_bstr_t)vtValue);
		}

		virtual void SetFocus() {
			m_wndEdit.SetFocus();
			m_wndEdit.SetSel(0,-1);
		}

		virtual _variant_t GetValue() {
			_variant_t vt;
			SetNull(vt);
			CString str;
			m_wndEdit.GetWindowText(str);
			if(!str.IsEmpty()) {
				vt = (LPCTSTR)str;
				if(!ChangeType(vt,m_column.m_vt))
					vt = m_column.m_vtValue;
			}
			return vt;
		}
	protected:
		CEditText	m_wndEdit;
	};

#if 0
	class CCellCheck : public CGridCtrl::CCell {
	public:
		CCellCheck(CColumn& column) : CCell(EDIT_CHECK,column), m_wndEdit(column) {
		}

		virtual void Create(CFontHandle hFont) {
			DWORD dwFlags = 0;

			m_hWnd = m_wndEdit.Create(m_column.m_grid,rcDefault,NULL,WS_CHILD|WS_TABSTOP|BS_AUTOCHECKBOX|dwFlags,WS_EX_TRANSPARENT);
			m_wndEdit.SetFont(hFont);
			m_wndEdit.SetWindowText(m_column.m_strTitle);
		}

		virtual void Draw(CDCHandle dc,CRect& rc,const _variant_t& vtValue) {
			if(!IsNull(vtValue)) {
				DWORD dwFlag = 0;
				if((long)vtValue)
					dwFlag |= DFCS_CHECKED;
				dc.DrawFrameControl(&rc,DFC_BUTTON,DFCS_BUTTONCHECK|dwFlag);		
			}
		}

		virtual void SetValue(_variant_t vtValue) {
			CCell::SetValue(vtValue);
			if(IsNull(vtValue))
				m_wndEdit.SetCheck(FALSE);
			else
				m_wndEdit.SetCheck((long)vtValue!=0);
		}

		virtual void SetFocus() {
			m_wndEdit.SetFocus();
		}

		virtual _variant_t GetValue() {
#if 1
			_variant_t vt = (long)m_wndEdit.GetCheck();
			return vt;
#else
			_variant_t vt;
			SetNull(vt);
			CString str;
			m_wndEdit.GetWindowText(str);
			if(!str.IsEmpty()) {
				vt = (LPCTSTR)str;
				if(!ChangeType(vt,m_column.m_vt))
					vt = m_column.m_vtValue;
			}
			return vt;
#endif
		}
	protected:
		CEditCheck	m_wndEdit;
	};
#endif

	class CCellDropdownlist : public CGridCtrl::CCell {
	public:
		CCellDropdownlist(CColumn& column) : CCell(EDIT_DROPDOWNLIST,column), m_wndEdit(column) {
		}

		virtual void Create(CFontHandle hFont) {
			m_hWnd = m_wndEdit.Create(m_column.m_grid,rcDefault,NULL,WS_CHILD|WS_TABSTOP|WS_VSCROLL|CBS_AUTOHSCROLL|CBS_DROPDOWNLIST,0);
			m_wndEdit.SetFont(hFont);
		}

		virtual void SetWindowPos(RECT& rc,DWORD dwSWPFlags) {
			if(dwSWPFlags & SWP_SHOWWINDOW) {
				m_wndEdit.ResetContent();
				for(long i=0;i<m_column.m_lookup.GetSize();i++) {
					long iItem = m_wndEdit.AddString((_bstr_t)m_column.m_lookup[i]->m_vtDisplay);
					_variant_t* pValue = new _variant_t(m_column.m_lookup[i]->m_vtID);
					m_wndEdit.SetItemData(iItem,(DWORD_PTR)pValue);
				}
			}
			rc.bottom += 100;
			CCell::SetWindowPos(rc,dwSWPFlags);
		}

		virtual void SetValue(_variant_t vtValue) {
			CCell::SetValue(vtValue);
			m_wndEdit.SetValue(vtValue);
		}

		virtual void SetFocus() {
			m_wndEdit.SetFocus();
		}

		virtual _variant_t GetValue() {
			_variant_t vt;
			SetNull(vt);
			long nItem = m_wndEdit.GetCurSel();
			if(nItem>=0) {
				vt = (_variant_t*)m_wndEdit.GetItemData(nItem);
				if(!ChangeType(vt,m_column.m_vt))
					vt = m_column.m_vtValue;
			}
			return vt;
		}
	protected:
		CEditDropdownlist	m_wndEdit;
	};

	class CCellDropdown : public CGridCtrl::CCell {
	public:
		CCellDropdown(CColumn& column) : CCell(EDIT_DROPDOWN,column), m_wndEdit(column) {
		}

		virtual void Create(CFontHandle hFont) {
			m_hWnd = m_wndEdit.Create(m_column.m_grid,rcDefault,NULL,WS_CHILD|WS_TABSTOP|WS_VSCROLL|CBS_AUTOHSCROLL|CBS_DROPDOWN,0);
			m_wndEdit.SetFont(hFont);
		}

		virtual void AddLookup(long nValue,_variant_t vtDisplay) {
			long nItem = m_wndEdit.AddString((_bstr_t)vtDisplay);
			m_wndEdit.SetItemData(nItem,nValue);
		}

		virtual void SetWindowPos(RECT& rc,DWORD dwSWPFlags) {
			if(dwSWPFlags & SWP_SHOWWINDOW) {
				m_wndEdit.ResetContent();
				for(long i=0;i<m_column.m_lookup.GetSize();i++) {
					long iItem = m_wndEdit.AddString((_bstr_t)m_column.m_lookup[i]->m_vtDisplay);
					_variant_t* pValue = new _variant_t(m_column.m_lookup[i]->m_vtID);
					m_wndEdit.SetItemData(iItem,(DWORD_PTR)pValue);
				}
			} else if(dwSWPFlags & SWP_HIDEWINDOW) {
				m_wndEdit.ShowDropDown(FALSE);
			}
			rc.bottom += 100;
			CCell::SetWindowPos(rc,dwSWPFlags);
		}

		virtual void SetValue(_variant_t vtValue) {
			CCell::SetValue(vtValue);
			m_wndEdit.SetValue(vtValue);
		}

		virtual void SetFocus() {
			m_wndEdit.SetFocus();
		}

		virtual _variant_t GetValue() {
			_variant_t vt;
			SetNull(vt);
			long nItem = m_wndEdit.GetCurSel();
			if(nItem>=0) {
				vt = (_variant_t*)m_wndEdit.GetItemData(nItem);
				if(!ChangeType(vt,m_column.m_vt))
					vt = m_column.m_vtValue;
			}
			return vt;
		}
	protected:
		CEditDropdown	m_wndEdit;
	};

	class CCellDate : public CGridCtrl::CCell {
	public:
		CCellDate(CColumn& column,bool bTime) : CCell(EDIT_DATE,column), m_wndEdit(column), m_bTime(bTime) {
		}

		virtual void Create(CFontHandle hFont) {
			m_wndEdit.m_hWnd = NULL;
			DWORD dwStyle = DTS_SHOWNONE;
			if(m_bTime)
				dwStyle |= DTS_TIMEFORMAT;
			else
				dwStyle |= DTS_SHORTDATEFORMAT;
			m_hWnd = m_wndEdit.Create(m_column.m_grid,rcDefault,NULL,WS_CHILD|WS_TABSTOP|dwStyle,0);
			m_wndEdit.SetFont(hFont);
		}

		virtual _bstr_t GetDisplayText(const _variant_t vtValue) {
			SYSTEMTIME st;
			if(!IsNull(vtValue) && VariantTimeToSystemTime(vtValue,&st)) {
				LCID lcid = LOCALE_SYSTEM_DEFAULT;
				//lcid = MAKELCID(MAKELANGID(LANG_NORWEGIAN,SUBLANG_NORWEGIAN_BOKMAL),SORT_DEFAULT);

				CString str;
				if(m_bTime) {
					int nLen = GetTimeFormat(lcid,/*TIME_NOSECONDS|TIME_FORCE24HOURFORMAT*/0,&st,NULL,str.GetBuffer(20),20);
					if(nLen>1) str.ReleaseBufferSetLength(nLen-1);
				} else {
					int nLen = GetDateFormat(lcid,DATE_SHORTDATE,&st,NULL,str.GetBuffer(32),32);
					if(nLen>1) str.ReleaseBufferSetLength(nLen-1);
				}
				return bstr_t(str);
			}
			return CCell::GetDisplayText(vtValue);
		}

		virtual void SetValue(_variant_t vtValue) {
			CCell::SetValue(vtValue);
			SYSTEMTIME st;
			if(IsNull(vtValue) || !VariantTimeToSystemTime(vtValue.dblVal,&st))
				m_wndEdit.SendMessage(DTM_SETSYSTEMTIME,GDT_NONE);
			else
				m_wndEdit.SetSystemTime(0,&st);
		}

		virtual void SetFocus() {
			m_wndEdit.SetFocus();
		}

		virtual _variant_t GetValue() {
			_variant_t vtDate;
			SYSTEMTIME st;
			SetNull(vtDate);

			if(m_wndEdit.GetSystemTime(&st)!=GDT_NONE) {
				if(!m_bTime) {
					st.wHour = 0;
					st.wMinute = 0;
					st.wSecond = 0;
					st.wMilliseconds = 0;
				}
				if(::SystemTimeToVariantTime(&st,&vtDate.dblVal)) {
					vtDate.vt = VT_R8;
					if(!ChangeType(vtDate,VT_DATE))
						vtDate = m_column.m_vtValue;
				}
			}
			return vtDate;
		}
	protected:
		CEditDate	m_wndEdit;
		const bool	m_bTime;
	};


	class CColumn {
	public:
		CColumn(CGrid& grid) : m_grid(grid) {
			m_pEdit = NULL;
		}

		virtual ~CColumn() {
			long count = m_lookup.GetSize();
			while(count--) delete m_lookup[count];
			m_lookup.RemoveAll();
			if(m_pEdit) {
				delete m_pEdit;
				m_pEdit = NULL;
			}
		}
		
		bool OnEdge(long x,long cx) {
			return cx > x + m_nWidth - 2 && cx < x + m_nWidth + 2;
		}

		bool GetReadOnly() {
			return m_nType == EDIT_NONE || (m_grid.m_dwStyle & GS_EX_READONLY);
		}

		void CreateEdit() {
			switch(m_nType) {
				case EDIT_NONE:
					m_pEdit = new CCellNone(*this);
					break;
				case EDIT_TEXT:
					m_pEdit = new CCellText(*this,m_nAlign);
					break;
				case EDIT_DROPDOWN:
					m_pEdit = new CCellDropdown(*this);
					break;
				case EDIT_DROPDOWNLIST:
					m_pEdit = new CCellDropdownlist(*this);
					break;
				case EDIT_DATE:
				case EDIT_TIME:
					m_pEdit = new CCellDate(*this,m_nType==EDIT_TIME);
					break;
#if 0
				case EDIT_CHECK:
					m_pEdit = new CCellCheck(*this);
					break;
#endif
			}
			if(m_pEdit) {
				m_pEdit->Create((HFONT)m_grid.m_font);
			}
		}

		_bstr_t GetDisplayText(_variant_t vtValue) {
			if(m_pEdit)
				return m_pEdit->GetDisplayText(vtValue);
			else if(IsNull(vtValue))
				return _bstr_t();
			else
				return vtValue;
		}
		
		void SetWindowPos(RECT& rc,DWORD dwSWPFlags) {
			if(m_pEdit)
				m_pEdit->SetWindowPos(rc,dwSWPFlags);
		}
		
		void SetValue(_variant_t vtValue) {
			if(m_pEdit)
				m_pEdit->SetValue(vtValue);
		}
		
		void SetFocus() {
			if(m_pEdit)
				m_pEdit->SetFocus();
		}
		
		_variant_t GetValue() {
			if(m_pEdit)
				return m_pEdit->GetValue();
			else
				return m_vtValue;
		}
		
		void AddLookup(_variant_t vtID,_variant_t vtDisplay) {
			vtID.ChangeType(m_vt);
			CData* p = new CData;
			p->m_vtID = vtID;
			p->m_vtDisplay = vtDisplay;
			m_lookup.Add(p);
		}
		
		void ValueModified() {
			if(!m_pEdit || !m_grid.m_pListener)
				return;

			_variant_t vtValue = GetValue();
			
			if(vtValue != m_vtValue) {
				if(IsNull(vtValue))
					m_grid.m_pListener->OnModified(m_grid.m_wrapper.GetWindowLong(GWL_ID),m_strName,"");
				else
					m_grid.m_pListener->OnModified(m_grid.m_wrapper.GetWindowLong(GWL_ID),m_strName,vtValue);
			}
		}

		void Draw(CDCHandle dc,CRect& rc,const _variant_t& vtValue) {
			if(m_pEdit) m_pEdit->Draw(dc,rc,vtValue);
		}
		typedef struct CData {
			_variant_t	m_vtID;
			_variant_t	m_vtDisplay;
		} CData;

		CString					m_strName, m_strTitle;
		long					m_nWidth;
		long					m_nType;
		CGrid&					m_grid;
		CCell*					m_pEdit;
		CSimpleArray<CData*>	m_lookup;
		VARTYPE					m_vt;
		ALIGN					m_nAlign;
		_variant_t				m_vtValue;
	};

	class CRow {
	public:
		CRow(CGrid& grid,long nItems) : m_grid(grid) {
			CColumn& col = GetColumn(0);
			m_items = new _variant_t[nItems];

			m_vtItemData = 0;
			m_bModified = false;
		}

		virtual ~CRow() {
			delete []m_items;
		}
		
		_variant_t GetItem(long nItem) {
			CColumn& col = GetColumn(nItem);
			_variant_t vt = m_items[nItem];
			if(!CGridCtrl::IsNull(vt))
				if(!ChangeType(vt,col.m_vt))
					SetNull(vt);
			ATLASSERT(CGridCtrl::IsNull(vt) || vt.vt==col.m_vt);
			return vt;
		}
		
		_bstr_t GetItemDisplay(long nItem) {
			CColumn& col = GetColumn(nItem);
			return col.GetDisplayText(m_items[nItem]);
		}
		
		void SetItem(long nItem,_variant_t vtValue) {
			CColumn& col = GetColumn(nItem);
			if(IsNull(vtValue))
				SetNull(vtValue);
			else {
				if(!ChangeType(vtValue,col.m_vt))
					vtValue = col.m_vtValue;
			}

			m_items[nItem] = vtValue;
		}

		CColumn& GetColumn(long nColumn) {
			ATLASSERT(nColumn>=0 && nColumn<m_grid.m_columns.GetSize());
			return *m_grid.m_columns[nColumn];
		}

		_variant_t*		m_items;
		CGrid&			m_grid;
		_variant_t		m_vtItemData;
		bool			m_bModified;
	};

	class CGridDC {
	public:
		CGridDC(HDC hdc) : m_dc(hdc) {}
		
		void DrawText(CRect& rc,CString& str,ALIGN xalign) {
			int old = m_dc.SetBkMode(TRANSPARENT);
			int len = str.GetLength();

			long x = rc.left;
			long y = rc.top;

			SIZE sz;
			m_dc.GetTextExtent(str,str.GetLength(),&sz);

			y += rc.Height()/2 - sz.cy/2;
			
			if(sz.cx > rc.Width()) {
				CString tmp;
				long len = str.GetLength();
				do {
					tmp = str.Left(--len) + "...";
					m_dc.GetTextExtent(tmp,tmp.GetLength(),&sz);
				} while(sz.cx > rc.Width() && len>3);

				m_dc.TextOut(x,y,tmp,tmp.GetLength());
			} else {
				switch(xalign) {
					case CENTER: 
						x += rc.Width()/2 - sz.cx/2;    
						break;
					case RIGHT: 
						x += rc.Width() - sz.cx;   
						break;
				}
				m_dc.TextOut(x,y,str,len);
			}
			m_dc.SetBkMode(old);
		}

		void DrawButton(const CRect& rc,bool bSunken) {
			m_dc.FillRect(&rc, ::GetSysColorBrush(COLOR_3DFACE));

			if(bSunken)
				m_dc.Draw3dRect(rc.left,rc.top,rc.Width(),rc.Height(),
								::GetSysColor(COLOR_3DSHADOW),
								::GetSysColor(COLOR_3DHILIGHT));
			else
				m_dc.Draw3dRect(rc.left,rc.top,rc.Width(),rc.Height(),
								::GetSysColor(COLOR_3DHILIGHT),
								::GetSysColor(COLOR_3DSHADOW));
		}
	protected:
		CDCHandle	m_dc;
	};


	class CGrid : public CScrollWindowImpl<CGrid, CFSBWindow> {
		typedef CScrollWindowImpl<CGrid, CFSBWindow> baseClass;
		friend class CRow;
		friend class CColumn;
		friend class CGridCtrl;
		friend class CCell;
		friend class CEditText;
		friend class CEditDropdown;
		friend class CEditDropdownlist;
		friend class CEditDate;
	public:
		DECLARE_WND_CLASS_EX(NULL,CS_DBLCLKS,-1)

		BEGIN_MSG_MAP(CGrid)
			MESSAGE_HANDLER(WM_CREATE,OnCreate)
			MESSAGE_HANDLER(WM_LBUTTONDOWN,OnLButtonDown)
			MESSAGE_HANDLER(WM_RBUTTONDOWN,OnLButtonDown)
			MESSAGE_HANDLER(WM_LBUTTONDBLCLK,OnLButtonDblclk)
			MESSAGE_HANDLER(WM_KEYDOWN,OnKeyDown)
			CHAIN_MSG_MAP(baseClass)
			if(uMsg==WM_COMMAND || uMsg==WM_NOTIFY) {
				REFLECT_NOTIFICATIONS()
			}
		END_MSG_MAP()

		LRESULT OnCreate(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) {
			bHandled = FALSE;
			// Find minimum height for combo box
			CComboBox box;
			box.Create(m_hWnd,rcDefault,NULL,WS_CHILD|CBS_DROPDOWNLIST,0);
			box.SetFont(m_font);
			CRect rc;
			box.GetClientRect(rc);
			m_nControlHeight = rc.Height();
			box.DestroyWindow();

			return 0;
		}

		LRESULT OnLButtonDown(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) {
			CPoint pt((DWORD)lParam), ptScroll;
			GetScrollOffset(ptScroll);
			pt += ptScroll;

			long row = pt.y / GetRowHeight();
			if(row!=m_nSelectedRow) {
				SelectRow(row);
				SetFocus();
			}

			return 0;
		}
		
		LRESULT OnLButtonDblclk(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) {
			CPoint pt;
			GetScrollOffset(pt);
			pt.x += GET_X_LPARAM(lParam);
			pt.y += GET_Y_LPARAM(lParam);

			EditRow(GetPointRow(pt),GetPointColumn(pt));
			return 0;
		}
		
		LRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/) {
			switch(wParam) {
				case VK_RETURN:
					EditRow();
					break;
				case VK_UP:
					if(m_nSelectedRow>0)
						SelectRow(m_nSelectedRow-1);
					else if(m_nSelectedRow<0)
						SelectRow((long)m_rows.GetCount()-1);
					break;
				case VK_DOWN:
					if(m_nSelectedRow<(long)m_rows.GetCount()-1)
						SelectRow(m_nSelectedRow+1);
					break;
				case VK_INSERT:
					if(m_dwStyle & GS_EX_CONTEXTMENU)
						m_wrapper.SendMessage(WM_COMMAND,ID_GRID_ADDROW);
					break;
				case VK_DELETE:
					if(m_dwStyle & GS_EX_CONTEXTMENU)
						m_wrapper.SendMessage(WM_COMMAND,ID_GRID_DELETEROW);
					break;
				case VK_HOME:
					this->ScrollTop();
					if(m_rows.GetCount()>0) SelectRow(0);
					break;
				case VK_END:
					this->ScrollBottom();
					if(m_rows.GetCount()>0) SelectRow((long)m_rows.GetCount()-1);
					break;
				case VK_PRIOR:
					this->ScrollPageUp();
					SelectTopRow();
					break;
				case VK_NEXT:
					this->ScrollPageDown();
					SelectTopRow();
					break;
			}

			return 0;
		}

		CGrid(CGridCtrl& wrapper) : m_wrapper(wrapper) {
			//SetScrollExtendedStyle(0,SCRL_ERASEBACKGROUND);
			m_dwStyle = GS_EX_LINENUMBERS | GS_EX_VGRIDLINES | GS_EX_HGRIDLINES;
			m_nSelectedRow = -1;
			m_bEditing = false;
		}
		
		virtual ~CGrid() {
			FreeAll();
		}

		void SelectTopRow() {
			CPoint pt;
			GetScrollOffset(pt);
			long nRow = pt.y / GetRowHeight();
			if(nRow<(long)m_rows.GetCount())
				SelectRow(nRow);
		}

		virtual BOOL PreTranslateMessage(MSG* pMsg) {
			if(pMsg->message==WM_KEYDOWN && pMsg->wParam==VK_TAB)
				return IsDialogMessage(pMsg);
			else
				return FALSE;
		}
		
		void DrawCell(long nRow,long nColumn) {
			CWindowDC dcWindow(m_hWnd);
			
			CPoint ptScroll;
			GetScrollOffset(ptScroll);
			dcWindow.SetViewportOrg(-ptScroll.x,-ptScroll.y);

			CRect rc;
			rc.left = 0;
			if(m_wrapper.ShowLineNumbers()) rc.left += COL_NUMWIDTH;

			for(long i=0;i<nColumn;i++) rc.left += m_columns[i]->m_nWidth;
			rc.right = rc.left + m_columns[nColumn]->m_nWidth;
			rc.top = nRow * GetRowHeight();
			rc.bottom = rc.top + GetRowHeight();

			// Faster solution would be to only paint the actual cell, but 
			// painting all cells and only redrawing the actual one is easier.
			DoPaint((HDC)dcWindow,rc);
		}

		void DoPaint(CDCHandle dc2,LPRECT lpRect=NULL) {
			if(!m_wrapper.m_bRedraw) return;
			CRect rcClient;
			GetClientRect(&rcClient);

			CPoint ptView;
			dc2.GetViewportOrg(&ptView);

			CRect rcPaint;
			if(!lpRect) {
				rcPaint.left = rcClient.left - ptView.x;
				rcPaint.top = rcClient.top - ptView.y;
				rcPaint.right = rcPaint.left + rcClient.Width();
				rcPaint.bottom = rcPaint.top + rcClient.Height();
				lpRect = &rcPaint;
			}
			CMemDC dc(dc2,lpRect);
			HFONT hOldFont = dc.SelectFont(m_font_bold);
			const long nRows = m_wrapper.GetRowCount();

			{
				RECT rc = rcClient;
				rc.right = GetClientWidth();
				rc.bottom = GetClientHeight();
				dc.FillRect(&rc,COLOR_WINDOW);
			}

			CGridDC mdc(dc);
			const long nRowHeight = GetRowHeight();
			for(long i=0;i<nRows;i++) {
				RECT rc = rcClient;
				rc.top += i*nRowHeight;
				rc.bottom = rc.top + nRowHeight;

				// No point painting outside visible area
				if(rc.top + ptView.y + nRowHeight < 0 || rc.top + ptView.y > rcClient.Height())
					continue;

				long x = 0;
				if(m_wrapper.ShowLineNumbers()) {
					CString str;
					str.Format("%d",i+1);
					dc.SetTextColor(GetSysColor(COLOR_WINDOWTEXT));
					dc.SelectFont(m_font);

					CRect rcb(rc);
					rcb.right = rc.left + COL_NUMWIDTH;
					rcb.bottom = rc.top + nRowHeight;
					mdc.DrawButton(rcb,i==m_nSelectedRow);
					rcb.DeflateRect(4,1);
					rcb.bottom = rcb.top + m_nControlHeight;
					mdc.DrawText(rcb,str,RIGHT);
					
					x += COL_NUMWIDTH;
				}

				rc.left = x;
				rc.right = GetClientWidth(true);
				if(i==m_nSelectedRow) dc.FillRect(&rc,COLOR_HIGHLIGHT);

				COLORREF crColor = GetSysColor(i==m_nSelectedRow ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT);
				for(long col=0;col<m_wrapper.GetColumnCount();col++) {
					CColumn& column = *m_columns[col];
					if(m_pListener && i!=m_nSelectedRow) {
						COLORREF crBack = m_pListener->GetCellColor(m_wrapper.GetWindowLong(GWL_ID),i,m_columns[col]->m_strName);
						if(crBack!=(COLORREF)-1)
							dc.FillSolidRect(x,rc.top,m_columns[col]->m_nWidth,nRowHeight,crBack);
					}

					dc.SetTextColor(crColor);
					dc.SelectFont(m_font);
#if 1
					CRect rcCell;
					GetCellRect(rcCell,i,col,true,&ptView);
					m_columns[col]->Draw((HDC)dc,rcCell,m_rows[i]->m_items[col]);
#else				
					CString str = (LPCTSTR)(_bstr_t)m_rows[i]->GetItemDisplay(col);
					mdc.TextDraw(x,rc.top,m_columns[col]->m_nWidth,nRowHeight,str,column.m_nAlign);
#endif
					x += m_columns[col]->m_nWidth;
				}
			}

			if(m_dwStyle & GS_EX_VGRIDLINES) {
				long x = -1;
				if(m_wrapper.ShowLineNumbers()) x += COL_NUMWIDTH; 
				RECT rc = rcClient;
				rc.bottom = GetClientHeight(true);
				for(long i=0;i<m_wrapper.GetColumnCount();i++) {
					rc.left = x + m_columns[i]->m_nWidth;
					rc.right = rc.left + 1;
					dc.FillRect(&rc,COLOR_3DSHADOW);

					x += m_columns[i]->m_nWidth;
				}
			} else {
				CRect rc;
				rc.left = GetClientWidth(true) - 1;
				rc.right = rc.left + 1;
				rc.top = 0;
				rc.bottom = GetClientHeight(true);
				dc.FillRect(&rc,COLOR_3DSHADOW);
			}

			if(m_dwStyle & GS_EX_HGRIDLINES) {
				RECT rc = rcClient;
				rc.left--;
				if(m_wrapper.ShowLineNumbers()) rc.left += COL_NUMWIDTH;
				rc.right = GetClientWidth(true);
				for(long i=0;i<m_wrapper.GetRowCount();i++) {
					rc.top = (i+1) * nRowHeight - 1;
					rc.bottom = rc.top + 1;
					dc.FillRect(&rc,COLOR_3DSHADOW);
				}
			} else {
				CRect rc;
				rc.left = 0;
				if(m_wrapper.ShowLineNumbers()) rc.left += COL_NUMWIDTH;
				rc.right = GetClientWidth(true);
				rc.top = (long)m_rows.GetCount() * GetRowHeight() -1;
				rc.bottom = rc.top + 1;
				dc.FillRect(&rc,COLOR_3DSHADOW);
			}

			dc.SelectFont(hOldFont);
		}
		
		void DoScroll(int nType, int nScrollCode, int& cxyOffset, int cxySizeAll, int cxySizePage, int cxySizeLine) {
			baseClass::DoScroll(nType,nScrollCode,cxyOffset,cxySizeAll,cxySizePage,cxySizeLine);
			if(nType!=SB_VERT)
				m_wrapper.Invalidate();
		}

		long GetRowHeight() const {
			ATLASSERT(m_nControlHeight>15);
			return m_nControlHeight + 3;
		}
		
		long GetClientWidth(bool bGridOnly=false) const {
			CRect rc;
			GetClientRect(&rc);

			long nWidth = 0;
			if(m_wrapper.ShowLineNumbers()) nWidth += COL_NUMWIDTH; 
			for(long i=0;i<m_columns.GetSize();i++)
				nWidth += m_columns[i]->m_nWidth;

			if(!bGridOnly && nWidth < rc.Width())
				nWidth = rc.Width();
			if(nWidth<1)
				nWidth = 1;
			return nWidth;
		}
		
		long GetClientHeight(bool bGridOnly=false) const {
			CRect rc;
			GetClientRect(&rc);

			long nHeight = GetRowHeight() * m_wrapper.GetRowCount();
			if(!bGridOnly && nHeight < rc.Height())
				nHeight = rc.Height();
			if(nHeight<1)
				nHeight = 1;
			return nHeight;
		}

		bool SelectRow(long row) {
			ATLASSERT(IsWindow() && m_wrapper.IsWindow());
			if(m_pListener && !m_pListener->OnRowChanging(m_wrapper.GetWindowLong(GWL_ID),row))
				return false;

			if(!EndEdit(false))
				return false;

			if(row<m_wrapper.GetRowCount())
				m_nSelectedRow = row;
			else
				m_nSelectedRow = -1;

			if(m_nSelectedRow>=0)
				m_wrapper.EnsureVisible(m_nSelectedRow);

			Invalidate();

			if(m_pListener)
				m_pListener->OnRowChanged(m_wrapper.GetWindowLong(GWL_ID),m_nSelectedRow);

			return true;
		}

		void SizeControls(DWORD dwSWPFlags=0) {
			dwSWPFlags |= SWP_NOZORDER;
			long count = m_columns.GetSize();
			for(long i=0;i<count;i++) {
				CRect rc;
				GetCellRect(rc,m_nSelectedRow,i,true);
				m_columns[i]->SetWindowPos(rc,dwSWPFlags);
			}
		}

		void EditRow(long row=-1,long col=-1) {
			if(m_bEditing) return;
			if(col<0) col = 0;
			if(row<0) row = m_nSelectedRow;
			if(row<0) return;

			if(m_pListener) m_pListener->OnRowActivate(m_wrapper.GetWindowLong(GWL_ID),row);
			if(m_dwStyle & GS_EX_READONLY) return;

			SizeControls(SWP_SHOWWINDOW);
			for(long i=0;i<m_wrapper.GetColumnCount();i++) {
				CGridCtrl::CColumn* pCol = m_columns[i];
				if(!pCol->GetReadOnly()) {
					pCol->SetValue(m_rows[row]->GetItem(i));
					if(col>=0 && i>=col) {
						pCol->SetFocus();
						col = -1;
					}
				}
			}

			m_bEditing = true;
			if(m_pListener)
				m_pListener->OnEdit(m_wrapper.GetWindowLong(GWL_ID),m_nSelectedRow);
		}

		bool EndEdit(bool bAbort) {
			ATLASSERT(IsWindow());
			if(!m_bEditing) return true;

			if(!bAbort && m_pListener && !m_pListener->OnValidate(m_wrapper.GetWindowLong(GWL_ID)))
				return false;

			if(!bAbort) GetEditChanges();
			for(long i=0;i<m_wrapper.GetColumnCount();i++)
				m_columns[i]->SetWindowPos(rcDefault,SWP_HIDEWINDOW|SWP_NOMOVE|SWP_NOZORDER);
			m_bEditing = false;
			SetFocus();
			return true;
		}

		long GetPointRow(POINT pt) const {
			long row = pt.y / GetRowHeight();
			if(row<0 || row>=m_wrapper.GetRowCount())
				return -1;
			return row;
		}

		long GetPointColumn(POINT pt) const {
			if(m_wrapper.ShowLineNumbers()) pt.x -= COL_NUMWIDTH;
			if(pt.x<0) return -1;
			long x = 0;
			for(long i=0;i<m_wrapper.GetColumnCount();i++) {
				x += m_columns[i]->m_nWidth;
				if(pt.x < x)
					return i;
			}
			return -1;
		}
		
		void GetCellRect(CRect& rc,long row,long col,bool bInner=false,LPPOINT lpPoint=NULL) const {
			CPoint pt;
			GetScrollOffset(pt);
			rc.top = row * GetRowHeight() - pt.y;
			rc.bottom = rc.top + GetRowHeight() - 1;
			rc.left = 0;
			if(m_wrapper.ShowLineNumbers()) rc.left += COL_NUMWIDTH;

			for(long i=0;i<col;i++)
				rc.left += m_columns[i]->m_nWidth;

			rc.left -= pt.x;
			rc.right = rc.left + m_columns[col]->m_nWidth - 1;

			if(bInner) {
				rc.DeflateRect(2,1);
				rc.bottom = rc.top + m_nControlHeight;
			}
			if(lpPoint) {
				rc.left -= lpPoint->x;
				rc.right -= lpPoint->x;
				rc.top -= lpPoint->y;
				rc.bottom -= lpPoint->y;
			}
		}
		
		void DoDrag(long mx,bool bDoDrag) {
			CDCHandle dc = GetDC();
			RECT rc;
			GetClientRect(&rc);

			int old = dc.SetROP2(R2_NOT);
			if(bDoDrag) {
				dc.MoveTo(m_old_x,0);
				dc.LineTo(m_old_x,rc.bottom);
			}
			m_old_x = mx;

			dc.MoveTo(mx,0);
			dc.LineTo(mx,rc.bottom);
			dc.SetROP2(R2_COPYPEN);
			ReleaseDC(dc);
		}

		void DeleteAllItems() {
			EndEdit(true);
			long count = (long)m_rows.GetCount();
			while(count--) delete m_rows[count];
			m_rows.RemoveAll();
			this->SetScrollOffset(0,0,FALSE);
			SelectRow(-1);
		}

		void DeleteAllColumns() {
			long count = m_columns.GetSize();
			while(count--) delete m_columns[count];
			m_columns.RemoveAll();
		}

		bool GetEditChanges() {
			ATLASSERT(IsWindow());
			if(!m_bEditing)
				return false;

			bool bChanged = false;
			for(long i=0;i<m_wrapper.GetColumnCount();i++) {
				CGridCtrl::CColumn* pCol = m_columns[i];
				if(!pCol->GetReadOnly()) {
					_variant_t vtValue = pCol->GetValue();
					_variant_t vt2 = m_columns[i]->m_vtValue;
					if(IsNull(vtValue)) SetNull(vtValue);
					if(IsNull(vt2)) SetNull(vt2);
					if(vtValue!=vt2) {
						m_wrapper.SetItem(m_nSelectedRow,i,vtValue);
						m_rows[m_nSelectedRow]->m_bModified = true;
						bChanged = true;
					}
				}
			}
			return bChanged;
		}

		void FreeAll() {
			long count = (long)m_rows.GetCount();
			while(count--) delete m_rows[count];
			m_rows.RemoveAll();
			
			count = m_columns.GetSize();
			while(count--) delete m_columns[count];
			m_columns.RemoveAll();
		}

	protected:
		CGridCtrl&	m_wrapper;
		long		m_nSelectedRow;
		bool		m_bEditing;
		long		m_nControlHeight;
		long		m_old_x;
		// Fra CGridCtrl
		CSimpleArray<CColumn*>	m_columns;
		CAtlArray<CRow*>		m_rows;
		CFont						m_font;
		CFont						m_font_bold;
		CListener*					m_pListener;
		DWORD						m_dwStyle;
	};

	// Internal enums
protected:
	enum COMMAND {
		ID_GRID_ADDROW = 0x9F01,
		ID_GRID_EDITROW,
		ID_GRID_DELETEROW
	};

protected:
	CGrid	m_grid;
	long	m_nDragCol;
	long	m_nDragMin;
	bool	m_bRedraw;
};

#endif // __BHWTLGRID_H__

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Bjornar Henden
Web Developer
Norway Norway
No Biography provided

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 2 Jun 2003
Article Copyright 2003 by Bjornar Henden
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid