Click here to Skip to main content
12,758,605 members (34,972 online)
Click here to Skip to main content

Stats

689.9K views
35.3K downloads
384 bookmarked
Posted 20 Mar 2002

Scientific charting control

, 17 Jan 2005
Multi-purpose scientific charting control.
XGRAPH10.dll
XGraphTest.exe
Include
Release
res
bitmap1.bmp
bs1.bmp
bs2.bmp
bs3.bmp
bs4.bmp
bs5.bmp
Toolbar.bmp
XGraphTest.ico
XGraphTestDoc.ico
XGRAPH10.lib
XGRAPH10d.lib
XGraphTest.clw
XGraphTest.dsp
XGraphTest.dsw
XGRAPH10.dll
bitmap2.bmp
default1.bin
Logo.bmp
Screen1.bmp
xgraphtest
XGraph.clw
xgraph.def
XGRAPH.dsw
XGRAPH.dsp
bitmap1.bmp
bs1.bmp
bs2.bmp
bs3.bmp
bs4.bmp
bs5.bmp
lib
XGRAPH10.dll
XGRAPH10.exp
XGRAPH10.lib
// XGRAPH.cpp : Definiert den Einsprungpunkt f�r die DLL-Anwendung.
//

#include "stdafx.h"
#include "XGRAPH.h"
#include "XGraphLabel.h"
#ifndef _WIN32_WCE
#include "AxisDlg.h"
#include "ChartPage.h"
#include "CurveDlg.h"
#include "ChartDlg.h"
#endif
#include <afxpriv.h>

#include "float.h"
#include "GfxUtils.h"
#include "math.h"
#include "MemDC.h"

#include "WinGDI.h"


#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

// XGraph.cpp: Implementation
//

/////////////////////////////////////////////////////////////////////////////
// CXGraph

IMPLEMENT_SERIAL( CXGraph, CWnd, 1 )
  
#pragma warning (disable : 4244)
#pragma warning (disable : 4800)

CXGraph::NM_POINTMOVING CXGraph::m_nmCurrentPointMoving;
	
CXGraph::CXGraph()
{
	WNDCLASS wndcls;

	// setup drawing function pointers
	m_pDrawFn[0] = DrawRect;
	m_pDrawFn[1] = DrawCircle;
	m_pDrawFn[2] = DrawCross;
	m_pDrawFn[3] = DrawRhombus;
	m_pDrawFn[4] = DrawLeftTriangle;
	m_pDrawFn[5] = DrawUpTriangle;
	m_pDrawFn[6] = DrawRightTriangle;
	m_pDrawFn[7] = DrawDownTriangle;
	
    HINSTANCE hInst   = AfxGetInstanceHandle();
	m_crBackColor     = RGB(255,255,255);
	m_crGraphColor    = RGB(255,255,255);
	m_crInnerColor    = RGB(240,240,240);
	m_nLeftMargin     = 
	m_nTopMargin      = 
	m_nRightMargin    =
	m_nBottomMargin   = 5;
	m_LegendAlignment = right;
	m_bLButtonDown    = false;
	m_bRButtonDown    = false;
	m_bObjectSelected = false;
	m_bTmpDC		  = false;
	m_bDoubleBuffer   = true;
	m_bShowLegend     = true;
	m_bInteraction    = true;
	m_bDataPointMoving= false;
	m_nSelectedSerie  = -1;
	m_pTracker        = NULL;
	m_pCurrentObject  = NULL;
	m_pPrintDC        = NULL;
	m_pDrawDC         = NULL;
	m_nAxisSpace      = 5;
	m_nCursorFlags    = XGC_LEGEND | XGC_VERT | XGC_HORIZ;
	m_OldPoint        = CPoint(0,0);
	m_opOperation	  = opNone;
	m_bSnapCursor     = true;
	m_nSnapRange      = 10;
	m_cPrintHeader    = _T("");
	m_cPrintFooter    = _T("");
	m_pCurrentObject  = NULL;
	m_nForcedSnapCurve = -1;
	m_nSnappedCurve1   = 0;
	m_fCurrentEditValue = 0.0;
	
	m_OldCursorRect.SetRectEmpty();

	// Create one legend for the cursor
	m_CursorLabel.m_bVisible = false;
	m_CursorLabel.m_bCanEdit = false;
	m_CursorLabel.m_bBorder  = true;
	m_CursorLabel.m_clRect.SetRect(100,100,200,175);

	m_Objects.AddHead (&m_CursorLabel);

    // Register window class
    if (!(::GetClassInfo(hInst, _T("XGraph"), &wndcls)))
    {
        wndcls.style            = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
        wndcls.lpfnWndProc      = ::DefWindowProc;
        wndcls.cbClsExtra       = 0;
		wndcls.cbWndExtra		= 0;
        wndcls.hInstance        = hInst;
        wndcls.hIcon            = NULL;
        wndcls.hCursor          = AfxGetApp()->LoadStandardCursor(IDC_ARROW);
        wndcls.hbrBackground    = (HBRUSH) (COLOR_3DFACE + 1);
        wndcls.lpszMenuName     = NULL;
        wndcls.lpszClassName    = _T("XGraph");

        if (!AfxRegisterClass(&wndcls))
        {
            AfxThrowResourceException();
            return ;
        }
    }
}

CXGraph::~CXGraph()
{
	ResetAll();
}



BEGIN_MESSAGE_MAP(CXGraph, CWnd)
	//{{AFX_MSG_MAP(CXGraph)
	ON_WM_PAINT()
	ON_WM_SIZE()
	ON_WM_MOUSEMOVE()
	ON_WM_LBUTTONUP()
	ON_WM_LBUTTONDOWN()
	ON_WM_ERASEBKGND()
	ON_WM_LBUTTONDBLCLK()
	ON_WM_SETCURSOR()
#ifndef _WIN32_WCE
	ON_WM_RBUTTONUP()
	ON_WM_RBUTTONDOWN()
	ON_WM_MOUSEWHEEL()
#endif
	ON_WM_KEYUP()
	ON_COMMAND(IDM_ZOOM, Zoom )
	ON_COMMAND(IDM_PAN, Pan )
	ON_COMMAND(IDM_CURSOR, Cursor )
	ON_COMMAND(IDM_MEASURE, Measure)
	ON_COMMAND(IDM_SELECT, NoOp )
	ON_COMMAND(IDM_RESET, ResetZoom )
	ON_COMMAND(IDM_INSERTLABEL, InsertEmptyLabel)
	ON_COMMAND(IDM_PROPERTIES, OnProperties)
#ifndef _WIN32_WCE
	ON_COMMAND(IDM_PRINT, OnPrint)
#endif
	ON_COMMAND(IDM_LINEARTREND, LinearTrend)
	ON_COMMAND(IDM_CUBICTREND, CubicTrend)
	ON_COMMAND(IDM_PARENTCALLBACK, ParentCallback)
	ON_COMMAND(IDM_ZOOM_BACK, RestoreLastZoom)
	ON_COMMAND(IDM_EDITCURVE, Edit)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()


bool CXGraph::DeleteXAxis(int nAxis)
{
	if (nAxis < 0 || nAxis >= static_cast<int>(m_XAxis.size()))
		return false;

	m_XAxis.erase (&m_XAxis[nAxis]);
	
	return true;
}

bool CXGraph::DeleteYAxis(int nAxis)
{
	if (nAxis < 0 || nAxis >= static_cast<int>(m_YAxis.size()))
		return false;

	m_YAxis.erase (&m_YAxis[nAxis]);
	
	return true;
}

bool CXGraph::DeleteCurve(int nCurve)
{
	if (nCurve < 0 || nCurve >= static_cast<int>(m_Data.size()))
		return false;
	
	m_Data.erase (&m_Data[nCurve]);

	return true;
}

CXGraphDataSerie& CXGraph::SetData(TDataPoint* pValues, long nCount, int nCurve, int nXAxis, int nYAxis, bool bAutoDelete)
{
	ASSERT (nCurve <= static_cast<int>(m_Data.size()));
	ASSERT (nXAxis <= static_cast<int>(m_XAxis.size()));
	ASSERT (nYAxis <= static_cast<int>(m_YAxis.size()));

			
	if (pValues == NULL || nCount == 0)
		return (CXGraphDataSerie&)*((CXGraphDataSerie*)NULL);


	if (nCurve == m_Data.size ())
	{
		// New data serie

		CXGraphDataSerie serie;
		
		serie.m_bAutoDelete = bAutoDelete;
		serie.m_nXAxis		= nXAxis;
		serie.m_nYAxis		= nYAxis;
		serie.m_nCount		= nCount;
		serie.m_nIndex      = nCurve;

		if (bAutoDelete)
		{
			serie.m_pData = (TDataPoint*) new TDataPoint[nCount];
			memcpy(serie.m_pData, pValues, sizeof(TDataPoint) * nCount);
		}
		else
			serie.m_pData = pValues;
		
		serie.m_crColor = BASECOLORTABLE[m_Data.size () % 12];

		m_Data.push_back (serie);
	}
	else
	{
		// existing data serie, just update

		m_Data[nCurve].m_nXAxis = nXAxis;
		m_Data[nCurve].m_nYAxis = nYAxis;
		m_Data[nCurve].m_pData  = pValues;
		m_Data[nCurve].m_nCount = nCount;
		m_Data[nCurve].m_bAutoDelete = bAutoDelete;
	}

	// Need additional X-Axis
	if (nXAxis == m_XAxis.size())
	{
		CXGraphAxis axis;
		axis.m_pGraph   = this;
		axis.m_AxisKind = CXGraphAxis::xAxis;
		m_XAxis.push_back (axis);
	}
	
	// Need additional Y-Axis
	if (nYAxis == m_YAxis.size())
	{
		CXGraphAxis axis;
		axis.m_pGraph   = this;
		axis.m_AxisKind = CXGraphAxis::yAxis;
		m_YAxis.push_back (axis);
	}

	m_Data[nCurve].m_pGraph = this;
	
	Autoscale(nXAxis, nYAxis, nCurve);
	
	Invalidate(TRUE);
		
	return m_Data[nCurve];
}


void CXGraph::Autoscale(int nXAxis, int nYAxis, int nCurve)
{
	// Get min/max for data serie
	double fxMin = m_XAxis[nXAxis].m_fMin;
	double fxMax = m_XAxis[nXAxis].m_fMax;
	double fyMin = m_YAxis[nYAxis].m_fMin;
	double fyMax = m_YAxis[nYAxis].m_fMax;

	GetMinMaxForData(m_Data[nCurve], fxMin, 
									 fxMax,
									 fyMin, 
									 fyMax);

	// Set axis ranges
	VERIFY(m_XAxis[nXAxis].SetRange(fxMin, fxMax));
	VERIFY(m_YAxis[nYAxis].SetRange(fyMin, fyMax));
	
}


CXGraphAxis& CXGraph::GetXAxis(int nAxis)
{
	ASSERT(nAxis >= 0 && nAxis < static_cast<int>(m_XAxis.size()));
	return m_XAxis[nAxis]; 
}

CXGraphAxis& CXGraph::GetYAxis(int nAxis)
{
	ASSERT(nAxis >= 0 && nAxis < static_cast<int>(m_YAxis.size()));
	return m_YAxis[nAxis]; 
}

CXGraphDataSerie& CXGraph::GetCurve(int nCurve)
{
	ASSERT(nCurve >= 0 && nCurve < static_cast<int>(m_Data.size()));
	return m_Data[nCurve];
}

TDataPoint CXGraph::GetCursorAbsolute(int nCurve)
{
	TDataPoint vPoint = { 0.0, 0.0};
	long     nIndex;

	vPoint.fXVal = m_XAxis[m_Data[nCurve].m_nXAxis].GetValueForPos (m_CurrentPoint.x);

	double fSnappedXVal = m_XAxis[m_Data[nCurve].m_nXAxis].GetValueForPos (m_CurrentPoint.x);
	// Find index for this value
	m_XAxis[m_Data[nCurve].m_nXAxis].GetIndexByXVal(nIndex, fSnappedXVal, nCurve);
	// get yval for index
	vPoint.fYVal = m_Data[nCurve].m_pData[nIndex].fYVal;

	return vPoint;
}

void CXGraph::SetCursorAbsolute(int nCurve, TDataPoint vPoint, bool bForceVisible )
{
	long nIndex;

	m_CurrentPoint = m_XAxis[m_Data[nCurve].m_nXAxis].GetPointForValue (&vPoint);

	m_XAxis[m_Data[nCurve].m_nXAxis].GetIndexByXVal(nIndex, vPoint.fXVal, nCurve);
	vPoint.fYVal = m_Data[nCurve].m_pData[nIndex].fYVal;


	if (m_nCursorFlags & XGC_ADJUSTSMOOTH && bForceVisible)
	{
		double fRange = m_XAxis[m_Data[nCurve].m_nXAxis].m_fCurMax - m_XAxis[m_Data[nCurve].m_nXAxis].m_fCurMin;
		m_XAxis[m_Data[nCurve].m_nXAxis].SetCurrentRange(vPoint.fXVal -  fRange / 2, vPoint.fXVal + fRange / 2);
	}
	else
	if ( (vPoint.fXVal < m_XAxis[m_Data[nCurve].m_nXAxis].m_fCurMin || 
		  vPoint.fXVal > m_XAxis[m_Data[nCurve].m_nXAxis].m_fCurMax ) &&
		  bForceVisible)
	{
		double fRange = m_XAxis[m_Data[nCurve].m_nXAxis].m_fCurMax - m_XAxis[m_Data[nCurve].m_nXAxis].m_fCurMin;
		m_XAxis[m_Data[nCurve].m_nXAxis].SetCurrentRange(vPoint.fXVal - (fRange / 2.0), vPoint.fXVal + (fRange / 2.0));
	}

	if (m_nCursorFlags & XGC_ADJUSTSMOOTH && bForceVisible)
	{
		double fRange = m_YAxis[m_Data[nCurve].m_nYAxis].m_fCurMax - m_YAxis[m_Data[nCurve].m_nYAxis].m_fCurMin;
		m_YAxis[m_Data[nCurve].m_nYAxis].SetCurrentRange(vPoint.fYVal - (fRange / 2.0), vPoint.fYVal + (fRange / 2.0));
	}
	else
	if ( (vPoint.fYVal < m_YAxis[m_Data[nCurve].m_nYAxis].m_fCurMin || 
		  vPoint.fYVal > m_YAxis[m_Data[nCurve].m_nYAxis].m_fCurMax ) &&
		  bForceVisible)
	{
		double fRange = m_YAxis[m_Data[nCurve].m_nYAxis].m_fCurMax - m_YAxis[m_Data[nCurve].m_nYAxis].m_fCurMin;
		m_YAxis[m_Data[nCurve].m_nYAxis].SetCurrentRange(vPoint.fYVal - (fRange / 2.0), vPoint.fYVal + (fRange / 2.0));
	}

	Invalidate();

}

void CXGraph::GetMinMaxForData (CXGraphDataSerie& serie, double& fXMin, double& fXMax, double& fYMin, double& fYMax)
{	
	// check mins and maxs
	for (long i = 0; i < serie.m_nCount; i++)
	{
		if (serie.m_pData[i].fXVal > fXMax)
			fXMax = serie.m_pData[i].fXVal;

		if (serie.m_pData[i].fXVal < fXMin)
			fXMin = serie.m_pData[i].fXVal;

		if (serie.m_pData[i].fYVal > fYMax)
			fYMax = serie.m_pData[i].fYVal;

		if (serie.m_pData[i].fYVal < fYMin)
			fYMin = serie.m_pData[i].fYVal;
	}
}

// Returns the index for the given axis
int CXGraph::GetAxisIndex(CXGraphAxis* pAxis)
{
	for (int x = 0; x < static_cast<int>(m_XAxis.size()); x++)
		if (pAxis == &m_XAxis[x])
			return x;
	
	for (int y = 0; y < static_cast<int>(m_YAxis.size()); y++)
		if (pAxis == &m_YAxis[y])
			return y;

	return -1;
}

// Returns the index for the given curve
int CXGraph::GetCurveIndex(CXGraphDataSerie* pSerie)
{
	for (int i = 0; i < static_cast<int>(m_Data.size()); i++)
		if (pSerie == &m_Data[i])
			return i;

	return -1;
}

void CXGraph::SetGraphMargins(int nLeft, int nTop, int nRight, int nBottom)
{
	m_nLeftMargin   = nLeft;
	m_nTopMargin    = nTop;
	m_nRightMargin  = nRight;
	m_nBottomMargin = nBottom;
}

void CXGraph::DrawMeasures (CDCEx* pDC)
{
	for (UINT i = 0; i < m_Measures.size(); i++)
	{
		CRect measureRect;
						
		measureRect.left = m_XAxis[m_Data[m_Measures[i].nCurve1].m_nXAxis].GetPointForValue(&m_Data[m_Measures[i].nCurve1].m_pData[m_Measures[i].nIndex1]).x;
		measureRect.right= m_XAxis[m_Data[m_Measures[i].nCurve2].m_nXAxis].GetPointForValue(&m_Data[m_Measures[i].nCurve2].m_pData[m_Measures[i].nIndex2]).x;
		
		measureRect.top = m_YAxis[m_Data[m_Measures[i].nCurve1].m_nYAxis].GetPointForValue(&m_Data[m_Measures[i].nCurve1].m_pData[m_Measures[i].nIndex1]).y;
		measureRect.bottom = m_YAxis[m_Data[m_Measures[i].nCurve2].m_nYAxis].GetPointForValue(&m_Data[m_Measures[i].nCurve2].m_pData[m_Measures[i].nIndex2]).y;
		
		DrawMeasure(pDC, measureRect);
	}
}

void CXGraph::DrawMeasure (CDCEx* pDC, CRect measureRect)
{
	COLORREF crColor = RGB(255,0,0);
	COLORREF crColorGray = RGB(200, 200, 200);
	int      nArrowSize = 6;
	CString  cMarker;
	long	 nIndex1, nIndex2;

	UINT nOldBkColor = pDC->SetBkColor(m_crInnerColor);
	
	CQuickFont font("Arial", -11, FW_BOLD);
	CFontSelector fs(&font, pDC, false);

	if (measureRect.IsRectEmpty () && m_opOperation == opMeasure)
	{
		measureRect.SetRect(m_MouseDownPoint.x, m_MouseDownPoint.y, m_CurrentPoint.x, m_CurrentPoint.y);
	
		measureRect.left = max(measureRect.left, m_clInnerRect.left);
		measureRect.top = max(measureRect.top, m_clInnerRect.top);
		measureRect.right = min(measureRect.right, m_clInnerRect.right);
		measureRect.bottom = min(measureRect.bottom, m_clInnerRect.bottom);

		m_clCurrentMeasure = measureRect;
	}


	{
		CPenSelector ps(crColorGray, 1, pDC, PS_DOT);

		pDC->MoveTo (m_clInnerRect.left, measureRect.top);
		pDC->LineTo (m_clInnerRect.right, measureRect.top);

		pDC->MoveTo (measureRect.right, m_clInnerRect.top);
		pDC->LineTo (measureRect.right, m_clInnerRect.bottom);

	}	
	{
		CPenSelector ps(crColor, 1, pDC, PS_DOT);

		pDC->MoveTo (measureRect.left, measureRect.top);
		pDC->LineTo (measureRect.right, measureRect.top);
		pDC->LineTo (measureRect.right, measureRect.bottom);

		if (abs(measureRect.Width()) > nArrowSize )
			DrawLeftTriangle(CPoint(measureRect.left, measureRect.top - (nArrowSize/2)), nArrowSize, crColor, false, pDC);
		else
			DrawUpTriangle(CPoint(measureRect.right - (nArrowSize/2), measureRect.top + (nArrowSize/2)), nArrowSize, crColor, false, pDC);
		

		if (abs(measureRect.Height()) > nArrowSize )
			DrawDownTriangle(CPoint(measureRect.right - (nArrowSize/2), measureRect.bottom - nArrowSize), nArrowSize, crColor, false, pDC);
		else
			DrawRightTriangle(CPoint(measureRect.right - nArrowSize, measureRect.bottom - nArrowSize), nArrowSize, crColor, false, pDC);

	}
	

	double fSnappedX1 = m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].GetValueForPos (measureRect.left);
	double fSnappedX2 = m_XAxis[m_Data[m_nSnappedCurve1].m_nXAxis].GetValueForPos (measureRect.right);
	
	double fX = fSnappedX2 - fSnappedX1;

	if (m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].GetDateTime())
#ifndef _WIN32_WCE
		cMarker = COleDateTime(fX).Format(m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].GetDisplayFmt());
#else
		cMarker = COleDateTime(fX).Format();
#endif
	else
		cMarker.Format(m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].GetDisplayFmt(), fX);

		
	pDC->SetBkMode(TRANSPARENT);

	if (abs(measureRect.Width()) > pDC->GetTextExtent(cMarker).cx)
		pDC->DrawText(cMarker, measureRect, DT_CENTER | DT_SINGLELINE | DT_TOP | DT_NOCLIP);
		
	m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].GetIndexByXVal(nIndex1, fSnappedX1, m_nSnappedCurve);
	m_XAxis[m_Data[m_nSnappedCurve1].m_nXAxis].GetIndexByXVal(nIndex2, fSnappedX2, m_nSnappedCurve1);
		
	double fSnappedY1 = m_Data[m_nSnappedCurve].m_pData[nIndex1].fYVal;
	double fSnappedY2 = m_Data[m_nSnappedCurve1].m_pData[nIndex2].fYVal;

	double fY = fabs(fSnappedY2 - fSnappedY1);

	cMarker.Format(m_YAxis[m_Data[m_nSnappedCurve].m_nYAxis].GetDisplayFmt(), fY);
	cMarker += (" " + m_YAxis[m_Data[m_nSnappedCurve].m_nYAxis].GetLabel());

	if (abs(measureRect.Height()) > pDC->GetTextExtent(cMarker).cy)
		pDC->DrawText(cMarker, measureRect, DT_RIGHT | DT_SINGLELINE | DT_VCENTER | DT_NOCLIP);

	pDC->SetBkColor(nOldBkColor);
}

void CXGraph::DrawZoom (CDCEx* pDC)
{
	CRect zoomRect(m_MouseDownPoint.x, m_MouseDownPoint.y, m_CurrentPoint.x, m_CurrentPoint.y);

	zoomRect.NormalizeRect ();

	zoomRect.left = max(zoomRect.left, m_clInnerRect.left);
	zoomRect.top = max(zoomRect.top, m_clInnerRect.top);
	zoomRect.right = min(zoomRect.right, m_clInnerRect.right);
	zoomRect.bottom = min(zoomRect.bottom, m_clInnerRect.bottom);


	// If not in doublebuffer mode and zoomrect is not empty delete old zoomrect
	if (!m_bDoubleBuffer && !m_clCurrentZoom.IsRectEmpty ())
		pDC->DrawFocusRect (m_clCurrentZoom);

	m_clCurrentZoom = zoomRect;

	if (m_bDoubleBuffer)
	{
		// In db-mode we use a semi-transparent zoomrect ...
		CBrushSelector bs(RGB(150,150,255), pDC);
		COLORREF crOldBkColor = pDC->SetBkColor (m_crInnerColor); 
		int	nOldROP2 = pDC->SetROP2 (R2_NOTXORPEN);

		pDC->Rectangle(zoomRect);
		pDC->SetBkColor (crOldBkColor);
		pDC->SetROP2 (nOldROP2);
		
	}
	
	pDC->DrawFocusRect (m_clCurrentZoom);
	
}

CXGraphLabel& CXGraph::InsertLabel(CRect rect, CString cText)
{
	CXGraphLabel *pLabel = new CXGraphLabel;

	pLabel->m_clRect = rect;
	pLabel->m_cText  = cText;
	pLabel->m_pGraph = this;
	
	m_Objects.AddTail (pLabel);

	return *pLabel;
}

CXGraphLabel& CXGraph::InsertLabel(CString cText)
{
	CXGraphLabel *pLabel = new CXGraphLabel;

	CRect rect(m_clInnerRect.CenterPoint().x - 50,m_clInnerRect.CenterPoint().y - 50,
		       m_clInnerRect.CenterPoint().x + 50,m_clInnerRect.CenterPoint().y + 50);
	
	pLabel->m_clRect = rect;
	pLabel->m_cText  = cText;
	pLabel->m_pGraph = this;
	
	m_Objects.AddTail (pLabel);

	return *pLabel;
}

void CXGraph::Cursor()
{
	Pan();
	m_nSnappedCurve = m_nForcedSnapCurve;
	m_oldCursorPoint = CPoint(-1, -1);
	m_opOperation = opCursor;
	
	if (m_nCursorFlags & XGC_LEGEND)
		m_CursorLabel.m_clRect.SetRect(m_clInnerRect.left + 1, m_clInnerRect.top , m_clInnerRect.left + 150, m_clInnerRect.top + 50);
	

}

void CXGraph::Measure()
{
	m_opOperation = opMeasure;
	m_clCurrentMeasure.SetRectEmpty();
	Invalidate();
}

void CXGraph::Zoom()
{
	m_CursorLabel.m_bVisible = false;
	m_clCurrentZoom.SetRectEmpty ();
	m_opOperation = opZoom;
	Invalidate();
}

void CXGraph::Edit()
{
	m_opOperation = opEditCurve;
	Invalidate();
}

void CXGraph::ResetZoom()
{
	m_CursorLabel.m_bVisible = false;
	for (int y = 0; y < static_cast<int>(m_YAxis.size()); y++)
		m_YAxis[y].Reset();
	for (int x = 0; x < static_cast<int>(m_XAxis.size()); x++)
		m_XAxis[x].Reset();
	Invalidate();
}

void CXGraph::NoOp()
{
	m_CursorLabel.m_bVisible = false;
	m_opOperation = opNone;
	Invalidate();
}

void CXGraph::Pan()
{
	m_CursorLabel.m_bVisible = false;
	m_opOperation = opPan;

	for (int y = 0; y < static_cast<int>(m_YAxis.size()); y++)
		m_YAxis[y].m_bAutoScale = false;
	
	for (int x = 0; x < static_cast<int>(m_XAxis.size()); x++)
		m_XAxis[x].m_bAutoScale = false;

	Invalidate();
}

void CXGraph::DoPan(CPoint point)
{
	for (int y = 0; y < static_cast<int>(m_YAxis.size()); y++)
	{
		double fY1 = m_YAxis[y].GetValueForPos (point.y);
		double fY2 = m_YAxis[y].GetValueForPos (m_OldPoint.y);
		double fOffset = fY1 - fY2;
		VERIFY(m_YAxis[y].SetCurrentRange(m_YAxis[y].m_fCurMin - fOffset,m_YAxis[y].m_fCurMax - fOffset));
	}

	for (int x = 0; x < static_cast<int>(m_XAxis.size()); x++)
	{
		double fX1 = m_XAxis[x].GetValueForPos (point.x);
		double fX2 = m_XAxis[x].GetValueForPos (m_OldPoint.x);
		double fOffset = fX1 - fX2;
		VERIFY(m_XAxis[x].SetCurrentRange(m_XAxis[x].m_fCurMin - fOffset,m_XAxis[x].m_fCurMax - fOffset));
	}

	m_OldPoint = point;

	Invalidate();
}

void CXGraph::DoZoom()
{		
	if (m_clCurrentZoom.Width () < MIN_ZOOM_PIXELS ||
		m_clCurrentZoom.Height () < MIN_ZOOM_PIXELS)
		return;

	ZOOM zoom;
			
	for (UINT y = 0; y < m_YAxis.size(); y++)
	{
		double fMin, fMax;

		fMin = m_YAxis[y].GetValueForPos(min(m_clCurrentZoom.bottom, m_clInnerRect.bottom));
		fMax = m_YAxis[y].GetValueForPos(max(m_clCurrentZoom.top, m_clInnerRect.top));

		if ((fMax - fMin) != 0.0)
		{			
			zoom.fMin = m_YAxis[y].m_fCurMin;
			zoom.fMax = m_YAxis[y].m_fCurMax;
	
			m_YAxis[y].m_ZoomHistory.push_back (zoom);

			m_YAxis[y].m_fCurMin = fMin;
			m_YAxis[y].m_fCurMax = fMax;
			m_YAxis[y].SetBestStep ();

		}
	}

	for (UINT x = 0; x < m_XAxis.size(); x++)
	{
		double fMin, fMax;

		fMin = m_XAxis[x].GetValueForPos(max(m_clCurrentZoom.left, m_clInnerRect.left));
		fMax = m_XAxis[x].GetValueForPos(min(m_clCurrentZoom.right, m_clInnerRect.right));
		
		if ((fMax - fMin) != 0.0)
		{
			zoom.fMin = m_XAxis[x].m_fCurMin;
			zoom.fMax = m_XAxis[x].m_fCurMax;
	
			m_XAxis[x].m_ZoomHistory.push_back (zoom);
	
			m_XAxis[x].m_fCurMin = fMin;
			m_XAxis[x].m_fCurMax = fMax;
			m_XAxis[x].SetBestStep();
			
		}
	}

	::PostMessage(GetParent()->m_hWnd, XG_ZOOMCHANGE, 0, (long) this);
	
	m_clCurrentZoom.SetRectEmpty ();
}

int CXGraph::GetZoomDepth()
{
	if (m_XAxis.size() > 0)
		return m_XAxis[0].m_ZoomHistory.size();

	return 0;

}
void CXGraph::RestoreLastZoom()
{		
	for (UINT y = 0; y < m_YAxis.size(); y++)
	{
		if (m_YAxis[y].m_ZoomHistory.size() > 0)
		{
			m_YAxis[y].m_fCurMin = m_YAxis[y].m_ZoomHistory[m_YAxis[y].m_ZoomHistory.size() - 1].fMin;
			m_YAxis[y].m_fCurMax = m_YAxis[y].m_ZoomHistory[m_YAxis[y].m_ZoomHistory.size() - 1].fMax;
			m_YAxis[y].m_ZoomHistory.pop_back ();
			m_YAxis[y].SetBestStep ();

		}
	}

	for (UINT x = 0; x < m_XAxis.size(); x++)
	{
		if (m_XAxis[x].m_ZoomHistory.size() > 0)
		{
			m_XAxis[x].m_fCurMin = m_XAxis[x].m_ZoomHistory[m_XAxis[x].m_ZoomHistory.size() - 1].fMin;
			m_XAxis[x].m_fCurMax = m_XAxis[x].m_ZoomHistory[m_XAxis[x].m_ZoomHistory.size() - 1].fMax;
			m_XAxis[x].m_ZoomHistory.pop_back ();
			m_XAxis[x].SetBestStep ();

		}
	}
	
	::PostMessage(GetParent()->m_hWnd, XG_ZOOMCHANGE, 0, (long) this);
	
	m_clCurrentZoom.SetRectEmpty ();

	Invalidate();

}



/////////////////////////////////////////////////////////////////////////////
// Behandlungsroutinen f�r Nachrichten CXGraph 

void CXGraph::DrawLegend(CDCEx *pDC, CRect& ChartRect)
{
	int nLegendWidth = 0;
	int nLegendHeight = 0;
	int nItemHeight = 0;

	CQuickFont font("Arial", 13, FW_NORMAL);
	{
		CFontSelector fs(&font, pDC, false);

		for (UINT i = 0; i < m_Data.size(); i++)
		{
			if (!m_Data[i].m_bVisible)
				continue;

			CString cLegend;
			cLegend.Format(_T("%02d %s"), i + 1, m_Data[i].m_cLabel);
			nLegendWidth = __max(nLegendWidth, pDC->GetTextExtent (cLegend).cx);
			nLegendHeight += (nItemHeight = pDC->GetTextExtent (cLegend).cy);
		}
	}

	CFontSelector fs(&font, pDC);

	nLegendWidth += 16;

	CRect legendRect;
	
	if (m_LegendAlignment == left)
	{
		legendRect.SetRect (ChartRect.left + 10, ChartRect.top + 10, ChartRect.left + 10 + nLegendWidth, ChartRect.top + 12 + nLegendHeight);
		ChartRect.left += (nLegendWidth + 20);
	}
	
	if (m_LegendAlignment == right)
	{
		legendRect.SetRect (ChartRect.right - 10 - nLegendWidth, ChartRect.top + 10, ChartRect.right - 10, ChartRect.top + 12 + nLegendHeight);
		ChartRect.right -= (nLegendWidth + 20);
	}

	CPenSelector ps(0, 1, pDC);

	COLORREF brushColor = pDC->m_bMono ? RGB(255,255,255) : ::GetSysColor(COLOR_WINDOW);

	CBrushSelector bs(brushColor, pDC);
	
	pDC->Rectangle(legendRect);

	legendRect.DeflateRect (1,1,1,1);
	
	int nItem = 0;

	m_SelectByMarker.clear();

	for (UINT i = 0; i < m_Data.size(); i++)
	{
		SelectByMarker sbm;

		if (!m_Data[i].m_bVisible)
			continue;

		CString cLegend;
		cLegend.Format(_T("%02d %s"), i + 1, m_Data[i].m_cLabel);
		CRect itemRect(legendRect.left + 12, legendRect.top + nItemHeight * nItem, legendRect.right, legendRect.top + nItemHeight * nItem + nItemHeight);

		sbm.markerRect = itemRect;
		sbm.markerRect.left -= 12;
		sbm.pObject = &m_Data[i];

		m_SelectByMarker.push_back(sbm);
				
		if (pDC->m_bMono)
			pDC->SetBkColor(RGB(255,255,255));
		else
			pDC->SetBkColor(::GetSysColor(COLOR_WINDOW));

		pDC->DrawText(cLegend, itemRect, DT_LEFT);

		if (m_Data[i].m_bSelected)
			pDC->DrawFocusRect (sbm.markerRect);

		itemRect.left = legendRect.left + 2;
		itemRect.right = legendRect.left + 10;
		itemRect.DeflateRect (0,1,0,2);

		if (m_Data[i].m_nMarkerType == 0)
			pDC->FillSolidRect (itemRect, pDC->m_bMono ? 0L : m_Data[i].m_crColor);
		else
			m_YAxis[0].DrawMarker (pDC, CPoint(itemRect.left - 1, itemRect.top + 1), m_Data[i].m_nMarker, 8, pDC->m_bMono ? 0L : m_Data[i].m_crColor, true);
	
		nItem++;
	}
}


void CXGraph::OnDraw(CDC *pDC)
{
	CDCEx dc;
	
	dc.Attach (pDC->m_hDC);
	dc.Prepare (pDC->m_hAttribDC);
	
	m_pDrawDC = &dc;
	m_pDrawDC->m_bMono = false;
	m_pDrawDC->m_bPrinting = false;
	m_pDrawDC->m_bPrintPreview = false;
	
	m_clPrintRect.SetRect(0, 0, pDC->GetDeviceCaps(HORZRES), pDC->GetDeviceCaps(VERTRES));

	OnPaint();

	m_pDrawDC = NULL;

	dc.Detach();
}


	
void CXGraph::OnPaint() 
{
	CDCEx*    pDC   = new CDCEx;
	CMemDC*   pmdc  = NULL;
	CPaintDC* pdc   = new CPaintDC(this);
	CRect     clRect, clChartRect;
	int		  nSaveDC;

	m_oldCursorPoint = CPoint(-1, -1);

	if(m_pDrawDC != NULL)
	{
		pDC = (CDCEx*) m_pDrawDC;
	}
	else
	if (m_bDoubleBuffer)
	{
		pmdc = new CMemDC(pdc);
		pDC->Attach(pmdc->m_hDC);
		nSaveDC = pDC->SaveDC();
		pDC->m_bMono = false;
		pDC->m_bPrinting = false;
		pDC->m_bPrintPreview = false;
	}
	else
	{
		pDC->Attach(pdc->m_hDC);
		nSaveDC = pDC->SaveDC();
		pDC->m_bMono = false;
		pDC->m_bPrinting = false;
		pDC->m_bPrintPreview = false;
	}

	

	
	m_oldCursorPoint = CPoint(-1, -1);

	
	// Background
	if (pDC->m_bPrinting)
		clRect = m_clPrintRect;
	else
		GetClientRect(clRect);

	if (pDC->m_bMono)
		pDC->FillSolidRect(clRect, RGB(255,255,255));
	else
		pDC->FillSolidRect(clRect, m_crBackColor);
	
	// Chart
	clChartRect.left   = clRect.left + m_nLeftMargin;
	clChartRect.top    = clRect.top + m_nTopMargin;
	clChartRect.right  = clRect.right - m_nRightMargin;
	clChartRect.bottom = clRect.bottom - m_nBottomMargin;

	if (m_bShowLegend)
		DrawLegend(pDC, clChartRect);

	if (pDC->m_bMono)
		pDC->FillSolidRect(clChartRect, RGB(255,255,255));
	else
		pDC->FillSolidRect(clChartRect, m_crGraphColor);

	int nLeft   = clChartRect.left;
	int nRight  = clChartRect.right;
	int nBottom = clChartRect.bottom;

	CXGraphAxis::EAxisPlacement lastPlacement = CXGraphAxis::apAutomatic;

	// Calculate layout, prepare automatic axis
	for (UINT nYAxis = 0; nYAxis < m_YAxis.size (); nYAxis++)
	{
		if (m_YAxis[nYAxis].m_Placement == CXGraphAxis::apAutomatic)
		{
			if (lastPlacement == CXGraphAxis::apLeft)
			{
				lastPlacement = CXGraphAxis::apRight;
				m_YAxis[nYAxis].m_Placement = CXGraphAxis::apRight;
			}
			else
			{
				lastPlacement = CXGraphAxis::apLeft;
				m_YAxis[nYAxis].m_Placement = CXGraphAxis::apLeft;
			}
		}
	}

	// Y-Axis, left side
	for (nYAxis = 0; nYAxis < m_YAxis.size (); nYAxis++)
	{
		if (m_YAxis[nYAxis].m_Placement != CXGraphAxis::apLeft)
			continue;
	
		int nTickWidth  = m_YAxis[nYAxis].GetMaxLabelWidth (pDC); 
		int nLabelWidth = (m_YAxis[nYAxis].GetTitleSize (pDC).cx + m_YAxis[nYAxis].m_nArcSize);

		if (m_YAxis[nYAxis].m_bVisible)
			nLeft += (max (nTickWidth, nLabelWidth) + m_nAxisSpace );

		m_YAxis[nYAxis].m_nLeft   = nLeft;
		m_YAxis[nYAxis].m_clChart = clChartRect;
	}
	
	// Y-Axis, right side
	for (nYAxis = 0; nYAxis < m_YAxis.size (); nYAxis++)
	{
		if (m_YAxis[nYAxis].m_Placement != CXGraphAxis::apRight)
			continue;

		int nTickWidth  = m_YAxis[nYAxis].GetMaxLabelWidth (pDC); 
		int nLabelWidth = m_YAxis[nYAxis].GetTitleSize (pDC).cx;
	
		if (m_YAxis[nYAxis].m_bVisible)
			nRight -= (max (nTickWidth, nLabelWidth) + m_nAxisSpace );
		
		m_YAxis[nYAxis].m_nLeft   = nRight;
		m_YAxis[nYAxis].m_clChart = clChartRect;
	}

	// X-Axis
	for (UINT nXAxis = 0; nXAxis < m_XAxis.size (); nXAxis++)
	{
		if (m_XAxis[nXAxis].m_bVisible)
			nBottom -= (m_XAxis[nXAxis].GetMaxLabelHeight (pDC) + m_nAxisSpace);

		m_XAxis[nXAxis].m_nTop    = nBottom;
		m_XAxis[nXAxis].m_nLeft   = nLeft;
		m_XAxis[nXAxis].m_clChart = clChartRect;
	}

	// Draw Inner Rect
	// 

	m_clInnerRect.SetRect (nLeft + 1, clChartRect.top, nRight, nBottom);
	if (pDC->m_bMono)
		pDC->FillSolidRect(m_clInnerRect, RGB(255,255,255));
	else
		pDC->FillSolidRect(m_clInnerRect, m_crInnerColor);
	
	// Draw axis
	// Y-Axis
	for (nYAxis = 0; nYAxis < m_YAxis.size (); nYAxis++)
	{
		m_YAxis[nYAxis].m_nTop = nBottom;
		m_YAxis[nYAxis].Draw(pDC);
	}
	
	// X-Axis
	for (nXAxis = 0; nXAxis < m_XAxis.size (); nXAxis++)
		m_XAxis[nXAxis].Draw(pDC);
	
	// Setup clipping
	CRgn  clipRegion;
	CRect rectClip(m_clInnerRect.left, m_clInnerRect.top, m_clInnerRect.right, m_clInnerRect.bottom);
	
	// Should move inside CDCEx in future
	if (pDC->m_bPrinting && !pDC->m_bPrintPreview)
		pDC->AdjustRatio (rectClip);

	
	clipRegion.CreateRectRgn (rectClip.left, rectClip.top, rectClip.right, rectClip.bottom);


	// PRB: Clipping doesn't work in print preview, no idea why ...
	pDC->SelectClipRgn (&clipRegion);
		

	// Draw curves
	for (UINT nCurve = 0; nCurve < m_Data.size (); nCurve++)
		if (m_Data[nCurve].m_bVisible)
			m_Data[nCurve].Draw(pDC);

	// Draw curve markers
	for (nCurve = 0; nCurve < m_Data.size (); nCurve++)
		if (m_Data[nCurve].m_bVisible && m_Data[nCurve].m_bShowMarker)
			m_Data[nCurve].DrawMarker(pDC);

	DrawMeasures (pDC);

	// Draw zoom if active
	if (m_opOperation == opZoom && m_bLButtonDown)
		DrawZoom(pDC);
	
	if (m_opOperation == opMeasure && m_bLButtonDown)
	{
		CRect tmpRect;
		tmpRect.SetRectEmpty ();
		DrawMeasure (pDC, tmpRect);
	}

	// Draw cursor if active
	if (m_opOperation == opCursor)
		DrawCursor(pDC);

	// DrawObjects
	for (POSITION pos = m_Objects.GetHeadPosition (); pos != NULL; )
	{
		CXGraphObject *pObject = (CXGraphObject*) m_Objects.GetNext (pos);
		pObject->Draw (pDC);
	}

	if (m_bDataPointMoving)
	{
		CQuickFont font("Arial", 13, FW_NORMAL);
		CFontSelector fs(&font, pDC, false);
		pDC->SetBkMode (TRANSPARENT);
		CString cPoint;
		cPoint.Format ("%.2f", m_fCurrentEditValue);
		pDC->TextOut(m_CurrentPoint.x + 10, m_CurrentPoint.y, cPoint);
	}

	pDC->RestoreDC(nSaveDC);
	pDC->Detach();

	delete pDC;

	if (pmdc)
		delete pmdc;
	
	delete pdc;

	
}

void CXGraph::OnSize(UINT nType, int cx, int cy) 
{
	CWnd::OnSize(nType, cx, cy);
	
	Invalidate(TRUE);
}

#if _MFC_VER >= 0x0700
BOOL CXGraph::OnMouseWheel( UINT nFlags, short zDelta, CPoint pt )
#else
void CXGraph::OnMouseWheel( UINT nFlags, short zDelta, CPoint pt )
#endif
{

	if (nFlags & MK_SHIFT)
	{
		for (int y = 0; y < GetYAxisCount(); y++)
		{
			m_YAxis[y].SetAutoScale(false);
			double fStep = (m_YAxis[y].m_fCurMax - m_YAxis[y].m_fCurMin) / 10.0;
			if (zDelta < 0)
				m_YAxis[y].SetCurrentRange(m_YAxis[y].m_fCurMin - fStep,m_YAxis[y].m_fCurMax - fStep);
			else
				m_YAxis[y].SetCurrentRange(m_YAxis[y].m_fCurMin + fStep,m_YAxis[y].m_fCurMax + fStep);
		}
	}
	else
	{
		for (int x = 0; x < GetXAxisCount(); x++)
		{
			m_XAxis[x].SetAutoScale(false);
			double fStep = (m_XAxis[x].m_fCurMax - m_XAxis[x].m_fCurMin) / 10.0;
			if (zDelta < 0)
				m_XAxis[x].SetCurrentRange(m_XAxis[x].m_fCurMin - fStep,m_XAxis[x].m_fCurMax - fStep);
			else
				m_XAxis[x].SetCurrentRange(m_XAxis[x].m_fCurMin + fStep,m_XAxis[x].m_fCurMax + fStep);
		}
	}

	Invalidate(TRUE);

#if _MFC_VER >= 0x0700
	return TRUE;
#endif
}


void CXGraph::OnMouseMove(UINT nFlags, CPoint point) 
{
	m_bLButtonDown = (nFlags & MK_LBUTTON);

	if (m_opOperation == opMeasure && m_bLButtonDown)
		AdjustPointToData(point);

	m_CurrentPoint = point;
		
	if (m_opOperation == opCursor)
	{
		m_bTmpDC = true;
		CDCEx dc;
		dc.Attach(GetDC()->m_hDC);
		DrawCursor(&dc);
		ReleaseDC(&dc);
		dc.Detach ();
		m_bTmpDC = false;
		::PostMessage(GetParent()->m_hWnd, XG_CURSORMOVED, 0, (long) this);
	}	

	m_bObjectSelected = CheckObjectSelection(false, m_opOperation != opEditCurve);

	if (m_bLButtonDown && m_opOperation == opPan && m_clInnerRect.PtInRect (point))
	{
		if (m_OldPoint == CPoint(0,0))
			m_OldPoint = point;
		else
			DoPan(point);
	}

	if (m_bRButtonDown && m_opOperation == opCursor && m_clInnerRect.PtInRect (point))
	{
		if (m_OldPoint == CPoint(0,0))
			m_OldPoint = point;
		else
			DoPan(point);
	}

	if (m_bLButtonDown && m_opOperation == opZoom /*&& m_clInnerRect.PtInRect (point)*/)
	{
		if (m_bDoubleBuffer)
			Invalidate();
		else
		{
			CDCEx dc;
			dc.Attach(GetDC()->m_hDC);
			DrawZoom(&dc);
			ReleaseDC(&dc);
			dc.Detach ();
		}
	}
	
	if (m_bLButtonDown && m_opOperation == opMeasure /*&& m_clInnerRect.PtInRect (point)*/)
	{
		if (m_bDoubleBuffer)
			Invalidate();
		else
		{
			CDCEx dc;
			dc.Attach(GetDC()->m_hDC);
			CRect tmpRect;
			tmpRect.SetRectEmpty ();
			DrawMeasure(&dc, tmpRect);
			ReleaseDC(&dc);
			dc.Detach ();
		}
	}
	
	CWnd::OnMouseMove(nFlags, point);
}


void CXGraph::AdjustPointToData(CPoint& point)
{
	int  nYDistance = 10000000;
	int  nY = point.y;
	long nIndex;

	for (int i = 0; i < m_Data.size(); i++)
	{
		// Get xval for current position
		double fSnappedXVal = m_XAxis[m_Data[i].m_nXAxis].GetValueForPos (point.x);
		// Find index for this value
		m_XAxis[m_Data[i].m_nXAxis].GetIndexByXVal(nIndex, fSnappedXVal, i);
		
		int y = m_YAxis[m_Data[i].m_nYAxis].GetPointForValue(&m_Data[i].m_pData[nIndex]).y;
				
		if (abs(point.y - y) < nYDistance)
		{
			nYDistance = abs(point.y - y);
			nY = y;
			if (point == m_MouseDownPoint)
			{
				m_nSnappedCurve = i;
				m_MeasureDef.nIndex1 = nIndex;
				m_MeasureDef.nCurve1 = i;
			}
			else
			{
				m_nSnappedCurve1 = i;
				m_MeasureDef.nIndex2 = nIndex;
				m_MeasureDef.nCurve2 = i;
			}

		}
	}

	point.y = nY;
	
}



bool CXGraph::CheckObjectSelection(bool bEditAction, bool bCheckFocusOnly)
{
	UINT i;
	static int nXMarkerMoving = -1;
	static int nYMarkerMoving = -1;

	AFX_MANAGE_STATE(AfxGetModuleState( ));

	bool bHasNotified = false;

	if (!m_bInteraction)
		return false;

		
	// Check y axis selection
	for (i = 0; i < m_YAxis.size(); i++)
	{

		// Check for Marker selections
		if (m_opOperation == opNone)
		for (int nMarker = 0; nMarker < m_YAxis[i].m_AxisMarkers.size(); nMarker++)
		{
			CRect	   hitRect;
			TDataPoint dPoint;


			if (nYMarkerMoving == -1)
			{
			
				dPoint.fYVal = m_YAxis[i].m_AxisMarkers[nMarker].fValue;
				CPoint point = m_YAxis[i].GetPointForValue (&dPoint);

				hitRect.SetRect(m_clInnerRect.left, point.y - 10, m_clInnerRect.right, point.y + 10);
				
				if (bCheckFocusOnly && nYMarkerMoving == -1 && !m_bLButtonDown)
					if (hitRect.PtInRect (m_CurrentPoint))
						return true;
					else
						continue;
			}

			if (m_bRButtonDown && hitRect.PtInRect (m_CurrentPoint))
			{
				m_YAxis[i].DeleteAxisMarker(nMarker);
				Invalidate();
			}
						
			if (m_bLButtonDown && nYMarkerMoving == -1 && hitRect.PtInRect (m_CurrentPoint))
				nYMarkerMoving = nMarker;
			
			if (!m_bLButtonDown && nYMarkerMoving != -1)
				nYMarkerMoving = -1;

			if (nYMarkerMoving != -1 && m_bLButtonDown)
			{
				m_YAxis[i].m_AxisMarkers[nYMarkerMoving].fValue = m_YAxis[i].GetValueForPos (m_CurrentPoint.y);
				Invalidate();
			}

		}
		
		if (bCheckFocusOnly)
			if (m_YAxis[i].m_clRect.PtInRect (m_CurrentPoint))
				return true;
			else
				continue;
	
		if (m_opOperation == opNone)
		if (m_YAxis[i].m_bSelected && !m_YAxis[i].m_clRect.PtInRect (m_CurrentPoint))
		{
			double fValue = m_YAxis[i].GetValueForPos (m_CurrentPoint.y);
			m_YAxis[i].SetAxisMarker(m_YAxis[i].m_AxisMarkers.size(), fValue, 0L);
		}
		
		m_YAxis[i].m_bSelected = m_YAxis[i].m_clRect.PtInRect (m_CurrentPoint);

		
		if (m_YAxis[i].m_bSelected)
		{
			bHasNotified = true;
			::PostMessage(GetParent()->m_hWnd, bEditAction ? XG_YAXISDBLCLICK :XG_YAXISCLICK, i, (long) this);
#ifndef _WIN32_WCE
			if (bEditAction && m_bInteraction)
			{
				CChartPage dlg;
				dlg.m_pGraph = this;
				dlg.m_pGraphAxis = &m_YAxis[i];
				dlg.DoModal();		
				Invalidate();
			}
#endif
		}
	}
			 
	// Check x axis selection
	for (i = 0; i < m_XAxis.size(); i++)
	{

		// Check for Marker selections
		if (m_opOperation == opNone)
		for (int nMarker = 0; nMarker < m_XAxis[i].m_AxisMarkers.size(); nMarker++)
		{
			CRect	   hitRect;
			TDataPoint dPoint;

			if (nXMarkerMoving == -1)
			{
				dPoint.fXVal = m_XAxis[i].m_AxisMarkers[nMarker].fValue;
				CPoint point = m_XAxis[i].GetPointForValue (&dPoint);

				hitRect.SetRect(point.x - 10, m_clInnerRect.top, point.x + 10, m_clInnerRect.bottom);
				
				if (bCheckFocusOnly && nXMarkerMoving == -1 && !m_bLButtonDown)
					if (hitRect.PtInRect (m_CurrentPoint))
						return true;
					else
						continue;
			}

			if (m_bRButtonDown && hitRect.PtInRect (m_CurrentPoint))
			{
				m_XAxis[i].DeleteAxisMarker(nMarker);
				Invalidate();
			}


			if (m_bLButtonDown && nXMarkerMoving == -1 && hitRect.PtInRect (m_CurrentPoint))
				nXMarkerMoving = nMarker;

			if (!m_bLButtonDown && nXMarkerMoving != -1)
				nXMarkerMoving = -1;

			if (nXMarkerMoving != -1 && m_bLButtonDown)
			{
				m_XAxis[i].m_AxisMarkers[nXMarkerMoving].fValue = m_XAxis[i].GetValueForPos (m_CurrentPoint.x);
				Invalidate();
			}

		}
	
		if (bCheckFocusOnly)
			if (m_XAxis[i].m_clRect.PtInRect (m_CurrentPoint))
				return true;
			else
				continue;
		
		if (m_opOperation == opNone)
		if (m_XAxis[i].m_bSelected && !m_XAxis[i].m_clRect.PtInRect (m_CurrentPoint))
		{
			double fValue = m_XAxis[i].GetValueForPos (m_CurrentPoint.x);
			m_XAxis[i].SetAxisMarker(m_XAxis[i].m_AxisMarkers.size(), fValue, 0L);
		}
		
		m_XAxis[i].m_bSelected = m_XAxis[i].m_clRect.PtInRect (m_CurrentPoint);

		if (m_XAxis[i].m_bSelected && !bHasNotified)
		{
			bHasNotified = true;
			::PostMessage(GetParent()->m_hWnd, bEditAction ? XG_XAXISDBLCLICK : XG_XAXISCLICK, i, (long) this);
#ifndef _WIN32_WCE
			if (bEditAction && m_bInteraction)
			{
				CChartPage dlg;
				dlg.m_pGraph = this;
				dlg.m_pGraphAxis = &m_XAxis[i];
				dlg.DoModal();		
				Invalidate();
			}
#endif
		}
	}

	bool bCheckCurves = true;

	// Check object selection
	for (POSITION pos = m_Objects.GetHeadPosition (); pos != NULL; )
	{
		CXGraphObject *pObject = (CXGraphObject*) m_Objects.GetNext (pos);

		if (bCheckFocusOnly)
			if (pObject->m_clRect.PtInRect (m_CurrentPoint) && pObject->m_bVisible)
				return true;
			else
				continue;
		
		pObject->m_bSelected = (pObject->m_clRect.PtInRect (m_CurrentPoint) && pObject->m_bVisible);
		
		if (bEditAction && pObject->m_bSelected )
		{
			bHasNotified = true;
			bCheckCurves = false;
			pObject->Edit ();
		}
		
		if (pObject->m_bSelected && !pObject->m_bEditing && m_pTracker == NULL)
		{
			bHasNotified = true;
			pObject->BeginSize ();
			m_pTracker = &pObject->m_Tracker; 
			bCheckCurves = false;
		}

		if (!pObject->m_bSelected && pObject->m_bEditing)
			pObject->EndEdit ();

		if (!pObject->m_bSelected && pObject->m_bSizing)
		{
			m_pTracker = NULL;
			pObject->EndSize ();
		}
		
	}

	if (!bCheckFocusOnly)
		m_nSelectedSerie = -1;

	
	// Special check for moving datapoint in edit mode
	static int  nEditSerie = -1;
	static long nXIndex = -1;

	// Reset previous stored informations about last selected curve and index
	if (!m_bLButtonDown)
	{
		nXIndex = -1;
		nEditSerie = -1;
	}

	
	// check curve selection
	for (i = 0; i < m_Data.size(); i++)
	{
		CXGraphDataSerie& serie = m_Data[i];
		
		if (!serie.m_bVisible)
			continue;

		if (!bCheckFocusOnly)
			serie.m_bSelected = false;

		if (!bCheckCurves)
			continue;
		

		for (int j = 0; j < serie.m_CurveRegions.size(); j++)
		{
			CRect hitRect(serie.m_CurveRegions[j].p1.x, serie.m_CurveRegions[j].p1.y,serie.m_CurveRegions[j].p2.x, serie.m_CurveRegions[j].p2.y);
			hitRect.NormalizeRect ();
			hitRect.InflateRect (2,2,2,2);
			
			if (bCheckFocusOnly)
				if (hitRect.PtInRect (m_CurrentPoint) && serie.HitTestLine(serie.m_CurveRegions[j].p1, serie.m_CurveRegions[j].p2, m_CurrentPoint, 4))
					return true;
				else
					continue;
			
			if (hitRect.PtInRect (m_CurrentPoint) && serie.HitTestLine(serie.m_CurveRegions[j].p1, serie.m_CurveRegions[j].p2,m_CurrentPoint, 4) && !bHasNotified)
			{
				::PostMessage(GetParent()->m_hWnd, bEditAction ? XG_CURVEDBLCLICK : XG_CURVECLICK, i, (long) this);
				m_nSelectedSerie = i;

				// User is editing a datapoint
				if (nXIndex == -1)
				{
					nEditSerie = i;
					double fX = m_XAxis[serie.GetXAxis ()].GetValueForPos (m_CurrentPoint.x - serie.m_nMarkerSize / 2);
					// Remember index of moving point
					GetXAxis(serie.GetXAxis ()).GetIndexByXVal (nXIndex, fX, i);
				}
				
				serie.m_bSelected = true;
				bHasNotified = true;
#ifndef _WIN32_WCE
				if (bEditAction && m_bInteraction)
				{
					CChartPage dlg;
					dlg.m_pGraph = this;
					dlg.m_pGraphDataSerie = &serie;
					dlg.DoModal();			
					Invalidate();
				}
#endif
				break;
			}
		}
	}

	m_bDataPointMoving = false;

	if (m_bLButtonDown && nXIndex != -1 && m_opOperation == opEditCurve)
	{
		m_nmCurrentPointMoving.nCurve = nEditSerie;
		m_nmCurrentPointMoving.nIndex = nXIndex;
		m_nmCurrentPointMoving.dOldVal = m_Data[nEditSerie].m_pData[nXIndex].fYVal;
		m_nmCurrentPointMoving.dNewVal = GetYAxis(m_Data[nEditSerie].GetYAxis()).GetValueForPos (m_CurrentPoint.y);
		
		// Inform parent
		::PostMessage(GetParent()->m_hWnd, XG_POINTCHANGED, (long)&m_nmCurrentPointMoving, (long) this);

		m_Data[nEditSerie].m_pData[nXIndex].fYVal = GetYAxis(m_Data[nEditSerie].GetYAxis()).GetValueForPos (m_CurrentPoint.y);
		m_fCurrentEditValue = m_Data[nEditSerie].m_pData[nXIndex].fYVal;
		m_bDataPointMoving = true;
		
		Invalidate();
	}

	if (!bHasNotified)
	for (i = 0; i < m_SelectByMarker.size(); i++)
	{
		bool bSelected = m_SelectByMarker[i].markerRect.PtInRect (m_CurrentPoint);
		
		if (bCheckFocusOnly && bSelected)
			return true;
		
		m_SelectByMarker[i].pObject->m_bSelected = bSelected;

		if (bSelected)
		{
			bHasNotified = true;
#ifndef _WIN32_WCE
			if (bEditAction && m_bInteraction)
			{
				CXGraphDataSerie& serie = m_Data[i];
				m_nSelectedSerie = i;
				CChartPage dlg;
				dlg.m_pGraph = this;
				dlg.m_pGraphDataSerie = &serie;
				dlg.DoModal();			
				Invalidate();
			}
#endif
			break;
		}
	}

#ifndef _WIN32_WCE	
	if (!bHasNotified && bEditAction && m_bInteraction)
	{
		::PostMessage(GetParent()->m_hWnd, XG_GRAPHDBLCLICK, 0, (long) this);
		CChartPage dlg;
		dlg.m_pGraph = this;
		dlg.DoModal();
		Invalidate();
	}
#endif

		
	return false;
}

void CXGraph::InsertDataNotation(int nCurve, int nIndex)
{
	CString cText, cXFmt, cYFmt, cFmt;

	bool bXDT = m_XAxis[m_Data[nCurve].m_nXAxis].m_bDateTime;
	bool bYDT = m_YAxis[m_Data[nCurve].m_nYAxis].m_bDateTime;

	cYFmt = cXFmt = "%s";
	cYFmt = "%s";
	cFmt  = "[%d] : " + cXFmt + "%s," + cYFmt + "%s";

	if (bXDT)
#ifndef _WIN32_WCE
			cXFmt = COleDateTime(m_Data[nCurve].m_pData[nIndex].fXVal).Format(m_XAxis[m_Data[nCurve].m_nXAxis].m_cDisplayFmt);
#else
			cXFmt = COleDateTime(m_Data[nCurve].m_pData[nIndex].fXVal).Format();
#endif

	else
		cXFmt.Format(m_XAxis[m_Data[nCurve].m_nXAxis].m_cDisplayFmt, m_Data[nCurve].m_pData[nIndex].fXVal);

	if (bYDT)
#ifndef _WIN32_WCE
		cYFmt = COleDateTime(m_Data[nCurve].m_pData[nIndex].fYVal).Format(m_YAxis[m_Data[nCurve].m_nYAxis].m_cDisplayFmt);
#else
		cYFmt = COleDateTime(m_Data[nCurve].m_pData[nIndex].fYVal).Format();
#endif
	else
		cYFmt.Format(m_YAxis[m_Data[nCurve].m_nYAxis].m_cDisplayFmt, m_Data[nCurve].m_pData[nIndex].fYVal);

	cText.Format(cFmt, nIndex + 1, cXFmt, m_XAxis[m_Data[nCurve].m_nXAxis].m_cLabel, cYFmt, m_YAxis[m_Data[nCurve].m_nYAxis].m_cLabel);
	
	CXGraphDataNotation *pNotation = (CXGraphDataNotation*) new CXGraphDataNotation;
	
	pNotation->m_fXVal  = m_Data[nCurve].m_pData[nIndex].fXVal;
	pNotation->m_fYVal  = m_Data[nCurve].m_pData[nIndex].fYVal;
	pNotation->m_cText  = cText;
	pNotation->m_nCurve = nCurve;
	pNotation->m_nIndex = nIndex;
	pNotation->m_pGraph = this;

	m_Objects.AddTail (pNotation);

	Invalidate();
}

void CXGraph::OnLButtonUp(UINT nFlags, CPoint point) 
{
	if (!m_bLButtonDown)
		return;

	ReleaseCapture();

	m_OldPoint     = CPoint(0,0);
	m_MouseUpPoint = point;
	m_bLButtonDown = false;

	if (m_opOperation == opMeasure)
	{
		AdjustPointToData(point);
		m_Measures.push_back(m_MeasureDef);
	
	}

	if (m_opOperation == opCursor)
	{
		if (!CheckObjectSelection() && m_nSnappedCurve != -1)
		{
			long nIndex;
			m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].GetIndexByXVal(nIndex, m_fSnappedXVal, m_nSnappedCurve);
			InsertDataNotation(m_nSnappedCurve, nIndex);
		}
	}
	
	if (m_opOperation == opZoom)
		DoZoom();
	
	if (m_opOperation == opNone || m_opOperation == opEditCurve)
		CheckObjectSelection();

	Invalidate(TRUE);

	CWnd::OnLButtonUp(nFlags, point);
}

void CXGraph::OnLButtonDown(UINT nFlags, CPoint point) 
{

	SetFocus();
	SetCapture();

	m_MouseDownPoint = point;

	if (m_opOperation == opMeasure)
		AdjustPointToData(point);

	m_MouseDownPoint = point;

	
	m_bLButtonDown   = true;
	m_pCurrentObject = NULL;

			
	// Check objects
	for (POSITION pos = m_Objects.GetHeadPosition (); pos != NULL; )
	{
		CXGraphObject *pObject = (CXGraphObject*) m_Objects.GetNext (pos);

		if (pObject->m_clRect.PtInRect(point))
			m_pCurrentObject = pObject;
				
		if (pObject->m_bSizing && pObject->m_Tracker.HitTest(point) != CRectTracker::hitNothing)
		{
			ReleaseCapture();
			
			if (pObject->m_Tracker.Track(this, point) && pObject->m_bCanMove && pObject->m_bCanResize)
			{
				pObject->m_clRect = pObject->m_Tracker.m_rect;
				Invalidate(FALSE);
			}
		}
	}

	CWnd::OnLButtonDown(nFlags, point);
}

BOOL CXGraph::OnEraseBkgnd(CDC* pDC) 
{
	return TRUE;
}

void CXGraph::OnLButtonDblClk(UINT nFlags, CPoint point) 
{
	if (m_opOperation == opNone)
	{
		m_pTracker = NULL;
		CheckObjectSelection(true);
	}
	
	CWnd::OnLButtonDblClk(nFlags, point);
}

BOOL CXGraph::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 
{	
	if (m_pTracker && m_pTracker->SetCursor(this,  nHitTest))
        return TRUE;

	if (m_opOperation == opPan)
	{
		HCURSOR hCur = AfxGetApp()->LoadStandardCursor(IDC_SIZEALL);
		if (hCur)
		{
			::SetCursor(hCur);			 
			return TRUE;
		}
	}
	else
	if (m_opOperation == opZoom || m_opOperation == opCursor)
	{
		HCURSOR hCur = AfxGetApp()->LoadStandardCursor(IDC_CROSS);
		if (hCur)
		{
			::SetCursor(hCur);			 
			return TRUE;
		}
	}
	else
	if (m_bObjectSelected && m_opOperation == opNone)
	{
		HCURSOR hCur = AfxGetApp()->LoadStandardCursor(MAKEINTRESOURCE(32649));
		if (hCur)
		{
			::SetCursor(hCur);			 
			return TRUE;
		}
	}
	else
	if (m_opOperation == opEditCurve && m_nSelectedSerie != -1)
	{
		HCURSOR hCur = AfxGetApp()->LoadStandardCursor(IDC_SIZENS);
		if (hCur)
		{
			::SetCursor(hCur);			 
			return TRUE;
		}
	}

	
	return CWnd::OnSetCursor(pWnd, nHitTest, message);
}

BOOL CXGraph::PreTranslateMessage(MSG* pMsg) 
{
	if ((pMsg->message == WM_KEYDOWN || pMsg->message == WM_KEYUP) && pMsg->wParam == VK_DELETE)
	{
		if (m_pCurrentObject && m_pCurrentObject->m_bSizing)
		{
			if (m_Objects.GetHead() != m_pCurrentObject)
			{
				m_Objects.RemoveAt(m_Objects.Find(m_pCurrentObject));
				delete m_pCurrentObject;
				m_pCurrentObject = NULL;
				m_pTracker = NULL;
				Invalidate();
			}
		}
	}
		
	return CWnd::PreTranslateMessage(pMsg);
}

#ifndef _WIN32_WCE
void CXGraph::OnRButtonUp(UINT nFlags, CPoint point) 
{
	CheckObjectSelection();

	m_bRButtonDown = false;

	if (m_opOperation == opCursor)
	{
		m_oldCursorPoint = CPoint(-1,-1);
		Invalidate();
	}

	if (m_pCurrentObject && m_pCurrentObject->m_clRect.PtInRect (point))
		m_pCurrentObject->InvokeProperties();
	else
		::PostMessage(GetParent()->m_hWnd, XG_RBUTTONUP, 0, (long) this);
		

	CWnd::OnRButtonUp(nFlags, point);
}
#endif

void CXGraph::InsertEmptyLabel()
{
	InsertLabel();
	Invalidate();
}

void CXGraph::OnProperties()
{
	AFX_MANAGE_STATE(AfxGetModuleState( ));
	CheckObjectSelection(true);
}


#ifndef _WIN32_WCE
void CXGraph::OnPrint()
{
	AFX_MANAGE_STATE(AfxGetModuleState());

	CPrintDialog dlg(FALSE);

	if (dlg.DoModal () == IDOK)
	{
		CDCEx dc;
		
		dc.Attach (dlg.GetPrinterDC ());

		dc.Prepare (dc.m_hAttribDC);

		int nWidth  = dc.GetDeviceCaps (HORZRES);
		int nHeight = dc.GetDeviceCaps (VERTRES);

		m_clPrintRect.SetRect(0, 0, nWidth / dc.m_fScaleX, nHeight / dc.m_fScaleY);

		m_pPrintDC = &dc;
		m_pPrintDC->m_bPrinting = true;
		m_pPrintDC->StartDoc ("Graph"); 
		m_pPrintDC->StartPage ();
		OnPaint();
		m_pPrintDC->EndPage ();
		m_pPrintDC->EndDoc ();
		m_pPrintDC = NULL;
		dc.Detach ();
	}
	
	//Invalidate();
}

void CXGraph::PrintGraph(CDC *pDC)
{ 
	CDCEx dc;
	
	dc.Attach (pDC->m_hDC);

	dc.Prepare (pDC->m_hAttribDC);
	
	int nWidth, nHeight;

	if (pDC->m_hDC == pDC->m_hAttribDC)
	{
		m_pPrintDC = &dc;
		nWidth  = pDC->GetDeviceCaps (HORZRES);
		nHeight = pDC->GetDeviceCaps (VERTRES);
		dc.m_bPrintPreview = false;
		dc.m_bPrinting = true;
	    DOCINFO docinfo;
	    memset(&docinfo, 0, sizeof(docinfo));
	    docinfo.cbSize = sizeof(docinfo);
        docinfo.lpszDocName = _T("Graph");
		int nRet;//= dc.StartDoc(&docinfo);
		nRet = GetLastError();
		nRet = dc.StartPage();
		m_clPrintRect.SetRect(0, 0, nWidth / dc.m_fScaleX, nHeight / dc.m_fScaleY);
		OnPaint();
		dc.EndPage ();
		dc.EndDoc ();
		m_pPrintDC = NULL;

	}
	else
	{
		nWidth  = ::GetDeviceCaps (pDC->m_hAttribDC ,HORZRES);
		nHeight = ::GetDeviceCaps (pDC->m_hAttribDC ,VERTRES);
		dc.m_bPrintPreview = true;
		m_clPrintRect.SetRect(0, 0, nWidth / dc.m_fScaleX, nHeight / dc.m_fScaleY);
		m_pPrintDC = &dc;
		m_pPrintDC->m_bPrinting = true;
		OnPaint();
		m_pPrintDC = NULL;
	}
}

void CXGraph::PrintGraph(CDC *pDC, CRect printRect)
{ 
	CDCEx dc;
	
	dc.Attach (pDC->m_hDC);

	dc.Prepare (pDC->m_hAttribDC);

	int nWidth, nHeight;

	nWidth  = pDC->GetDeviceCaps (HORZRES);
	nHeight = pDC->GetDeviceCaps (VERTRES);
		
	if (pDC->m_hDC == pDC->m_hAttribDC)
	{
		m_pPrintDC = &dc;
		dc.m_bPrintPreview = false;
		dc.m_bPrinting = true;
	    DOCINFO docinfo;
	    memset(&docinfo, 0, sizeof(docinfo));
	    docinfo.cbSize = sizeof(docinfo);
        docinfo.lpszDocName = _T("Graph");
		int nRet;//= dc.StartDoc(&docinfo);
		nRet = GetLastError();
		nRet = dc.StartPage();
		m_clPrintRect.SetRect(printRect.left / dc.m_fScaleX, printRect.top / dc.m_fScaleX, printRect.right / dc.m_fScaleX, printRect.bottom / dc.m_fScaleX);
		OnPaint();
		dc.EndPage ();
		dc.EndDoc ();
		m_pPrintDC = NULL;

	}
	else
	{
		dc.m_bPrintPreview = true;
		m_clPrintRect.SetRect(printRect.left / dc.m_fScaleX, printRect.top / dc.m_fScaleX, printRect.right / dc.m_fScaleX, printRect.bottom / dc.m_fScaleX);
		m_pPrintDC = &dc;
		m_pPrintDC->m_bPrinting = true;
		OnPaint();
		m_pPrintDC = NULL;
	}

}
#endif

void CXGraph::CubicTrend()
{
	AddCubicTrend(m_Data[m_nSelectedSerie]);
}

void CXGraph::LinearTrend()
{
	AddLinearTrend(m_Data[m_nSelectedSerie]);
}

void CXGraph::AddLinearTrend(CXGraphDataSerie& serie)
{
	TDataPoint* pTrend = serie.GetLinearTrend ();
	int			nCurveCount = GetCurveCount ();
	CString cLabel = serie.m_cLabel;
	CXGraphDataSerie &ret = SetData (pTrend, serie.m_nCount, nCurveCount, serie.m_nXAxis, serie.m_nYAxis, true);
	ret.m_cLabel = cLabel + " linear trend";
	delete pTrend;
}

void CXGraph::AddCubicTrend(CXGraphDataSerie& serie)
{
	TDataPoint* pTrend = serie.GetCubicTrend ();
	int			nCurveCount = GetCurveCount ();
	CString cLabel = serie.m_cLabel;
	CXGraphDataSerie &ret = SetData (pTrend, serie.m_nCount, nCurveCount, serie.m_nXAxis, serie.m_nYAxis, true);
	ret.m_cLabel = cLabel + " cubic trend";
	delete pTrend;
}

void CXGraph::DrawCursor(CDCEx* pDC)
{
	CPenSelector ps(0L, 1, pDC);
	long		 nIndex;
	TDataPoint   point;
	int			 nOldROP2 = pDC->SetROP2 (R2_NOTXORPEN);

	m_nSnappedCurve = -1;

	if (m_nForcedSnapCurve != -1)
	{
		m_nSnappedCurve = m_nForcedSnapCurve;
		m_fSnappedXVal = m_XAxis[m_Data[m_nForcedSnapCurve].m_nXAxis].GetValueForPos (m_CurrentPoint.x);
		// Find index for this value
		m_XAxis[m_Data[m_nForcedSnapCurve].m_nXAxis].GetIndexByXVal(nIndex, m_fSnappedXVal, m_nForcedSnapCurve);
		// get yval for index
		m_fSnappedYVal = m_Data[m_nForcedSnapCurve].m_pData[nIndex].fYVal;
		point.fYVal = m_fSnappedYVal;
		m_CurrentPoint.y = m_YAxis[m_Data[m_nSnappedCurve].m_nYAxis].GetPointForValue(&point).y;
	}
	else
	if (m_bSnapCursor)
	// Snap cursor to nearest curve
	for (int i = 0; i < m_Data.size(); i++)
	{
		// Get xval for current position
		m_fSnappedXVal = m_XAxis[m_Data[i].m_nXAxis].GetValueForPos (m_CurrentPoint.x);
		// Find index for this value
		m_XAxis[m_Data[i].m_nXAxis].GetIndexByXVal(nIndex, m_fSnappedXVal, i);
		// get yval for index
		m_fSnappedYVal = m_Data[i].m_pData[nIndex].fYVal;
		point.fYVal = m_fSnappedYVal;
		// Check if cursor is in snap-range
		int y = m_YAxis[m_Data[i].m_nYAxis].GetPointForValue(&point).y;
		if (abs(m_CurrentPoint.y - y) < m_nSnapRange)
		{
			m_nSnappedCurve = i;
			m_CurrentPoint.y = y;
			break;
		}
	}
		
	if (m_oldCursorPoint != CPoint(-1,-1))
	{
		if (m_nCursorFlags & XGC_VERT)
		{
			pDC->MoveTo (m_oldCursorPoint.x, m_clInnerRect.top);
			pDC->LineTo (m_oldCursorPoint.x, m_clInnerRect.bottom);
		}

		if (m_nCursorFlags & XGC_HORIZ)
		{
			pDC->MoveTo (m_clInnerRect.left, m_oldCursorPoint.y);
			pDC->LineTo (m_clInnerRect.right, m_oldCursorPoint.y);
		}

		pDC->Rectangle(m_oldCursorPoint.x - (m_nSnapRange / 2), m_oldCursorPoint.y - (m_nSnapRange / 2), m_oldCursorPoint.x + (m_nSnapRange / 2) + 1, m_oldCursorPoint.y + (m_nSnapRange / 2) + 1);
	}

	if (m_nCursorFlags & XGC_VERT)
	{
		pDC->MoveTo (m_CurrentPoint.x, m_clInnerRect.top);
		pDC->LineTo (m_CurrentPoint.x, m_clInnerRect.bottom);
	}

	if (m_nCursorFlags & XGC_HORIZ)
	{
		pDC->MoveTo (m_clInnerRect.left, m_CurrentPoint.y);
		pDC->LineTo (m_clInnerRect.right, m_CurrentPoint.y);
	}
	
	pDC->Rectangle(m_CurrentPoint.x - (m_nSnapRange / 2), m_CurrentPoint.y - (m_nSnapRange / 2), m_CurrentPoint.x + (m_nSnapRange / 2) + 1, m_CurrentPoint.y + (m_nSnapRange / 2) + 1);

	m_oldCursorPoint = m_CurrentPoint;

	pDC->SetROP2 (nOldROP2);

	if (m_nCursorFlags & XGC_LEGEND)
		DrawCursorLegend(pDC);
}

void CXGraph::DrawCursorLegend (CDCEx* pDC)
{
	int    i;
    double fVal;

    CString cLabel = "", cFmt, cItem;

    m_CursorLabel.m_bVisible = true;
	   
    for (i = 0; i < m_XAxis.size(); i++)
    {
        //CHANGED: bugfix, correct handling of X axis in time mode
        fVal = m_XAxis[i].GetValueForPos (m_CurrentPoint.x);
        if(m_XAxis[i].m_bDateTime)
        {
			cFmt = m_XAxis[i].m_cDisplayFmt;
			cFmt.Replace (_T("\r"),_T(" "));
			cFmt.Replace (_T("\n"),_T(" "));
#ifndef _WIN32_WCE
            cFmt = _T("X[%02d] : ") + COleDateTime(fVal).Format(cFmt) + _T(" %s");
#else
			cFmt = _T("X[%02d] : ") + COleDateTime(fVal).Format() + _T(" %s");
#endif
            cItem.Format(cFmt, i+1, m_XAxis[i].m_cLabel);
            cLabel += (_T(" ") + cItem + _T("\r\n"));
        }
        else
        {
            cFmt = _T("X[%02d] : ") + m_XAxis[i].m_cDisplayFmt + _T(" %s");
            cItem.Format(cFmt, i+1, fVal, m_XAxis[i].m_cLabel);
            cLabel += (_T(" ") + cItem + _T("\r\n"));
        }
        //ENDCHANGES
    }
    
    for (i = 0; i < m_YAxis.size(); i++)
    {
        fVal = m_YAxis[i].GetValueForPos (m_CurrentPoint.y);
        cFmt = _T("Y[%02d] : ") + m_YAxis[i].m_cDisplayFmt + _T(" %s");
        cItem.Format(cFmt, i+1, fVal, m_YAxis[i].m_cLabel);
        cLabel += (_T(" ") + cItem + _T("\r\n"));
    }

    if (m_nSnappedCurve != -1)
    {
        //CHANGED: bugfix, correct handling of X axis in time mode
        CString cXFmt = m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].m_cDisplayFmt;
		cXFmt.Replace (_T("\r"),_T(" "));
		cXFmt.Replace (_T("\n"),_T(" "));
        CString cYFmt = m_YAxis[m_Data[m_nSnappedCurve].m_nYAxis].m_cDisplayFmt;
        if(m_XAxis[m_Data[m_nSnappedCurve].m_nXAxis].m_bDateTime)
        {			
#ifndef _WIN32_WCE
            cFmt = _T("%s[%02d] : ") + COleDateTime(m_fSnappedXVal).Format(cXFmt) + _T(", ") + cYFmt;
#else
			cFmt = _T("%s[%02d] : ") + COleDateTime(m_fSnappedXVal).Format() + _T(", ") + cYFmt;
#endif
            cItem.Format(cFmt, m_Data[m_nSnappedCurve].m_cLabel, m_nSnappedCurve + 1, m_fSnappedYVal);
            cLabel += (_T(" ") + cItem + _T("\r\n"));
        }
        else
        {
            cFmt = _T("%s[%02d] : ") + cXFmt + _T(", ") + cYFmt;
            cItem.Format(cFmt, m_Data[m_nSnappedCurve].m_cLabel, m_nSnappedCurve + 1, m_fSnappedXVal, m_fSnappedYVal);
            cLabel += (_T(" ") + cItem + _T("\r\n"));
        }
        //ENDCHANGES
    }

    m_CursorLabel.m_cText = cLabel;

    m_CursorLabel.Draw (pDC);
}

LRESULT CXGraph::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
	return CWnd::WindowProc(message, wParam, lParam);
}

bool CXGraph::SelectCurve(int nCurve)
{
	if (nCurve < 0 || nCurve >= m_Data.size())
		return false;

	for (int i = 0; i < m_Data.size(); i++)
		m_Data[i].m_bSelected = (i == nCurve);

	Invalidate();

	return true;
}

bool CXGraph::SelectXAxis(int nAxis)
{
	if (nAxis < 0 || nAxis >= m_XAxis.size())
		return false;

	for (int i = 0; i < m_XAxis.size(); i++)
		m_XAxis[i].m_bSelected = (i == nAxis);

	Invalidate();

	return true;
}

bool CXGraph::SelectYAxis(int nAxis)
{
	if (nAxis < 0 || nAxis >= m_YAxis.size())
		return false;

	for (int i = 0; i < m_YAxis.size(); i++)
		m_YAxis[i].m_bSelected = (i == nAxis);

	Invalidate();
	
	return true;
}

void CXGraph::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
	if (nChar == VK_HOME)
	{
		for (int i = 0; i < m_XAxis.size(); i++)
			m_XAxis[i].Reset ();
		
		for (i = 0; i < m_YAxis.size(); i++)
			m_YAxis[i].Reset();
		
		Invalidate();
	}

	if (nChar == VK_ADD)
	{
		for (int i = 0; i < m_XAxis.size(); i++)
		{
			double fStep = m_XAxis[i].m_fRange / 10.0;
			VERIFY(m_XAxis[i].SetCurrentRange (m_XAxis[i].m_fCurMin + fStep, m_XAxis[i].m_fCurMax - fStep));
		}
		for (i = 0; i < m_YAxis.size(); i++)
		{
			double fStep = m_YAxis[i].m_fRange / 10.0;
			VERIFY(m_YAxis[i].SetCurrentRange (m_YAxis[i].m_fCurMin + fStep, m_YAxis[i].m_fCurMax - fStep));
		}
		
		Invalidate();
	}

	if (nChar == VK_SUBTRACT)
	{
		for (int i = 0; i < m_XAxis.size(); i++)
		{
			double fStep = m_XAxis[i].m_fRange / 10.0;
			VERIFY(m_XAxis[i].SetCurrentRange (m_XAxis[i].m_fCurMin - fStep, m_XAxis[i].m_fCurMax + fStep));
		}
		for (i = 0; i < m_YAxis.size(); i++)
		{
			double fStep = m_YAxis[i].m_fRange / 10.0;
			VERIFY(m_YAxis[i].SetCurrentRange (m_YAxis[i].m_fCurMin - fStep, m_YAxis[i].m_fCurMax + fStep));
		}
		
		Invalidate();
	}

	if (nChar == VK_LEFT)
	{
		if (m_opOperation == opCursor)
			m_CurrentPoint.x --;
		else
		for (int i = 0; i < m_XAxis.size(); i++)
		{
			double fStep = m_XAxis[i].m_fRange / 10.0;
			VERIFY(m_XAxis[i].SetCurrentRange (m_XAxis[i].m_fCurMin + fStep, m_XAxis[i].m_fCurMax + fStep));
		}

		Invalidate();
	}
	
	if (nChar == VK_RIGHT)
	{
		if (m_opOperation == opCursor)
			m_CurrentPoint.x++;
		else
		for (int i = 0; i < m_XAxis.size(); i++)
		{
			double fStep = m_XAxis[i].m_fRange / 10.0;
			VERIFY(m_XAxis[i].SetCurrentRange (m_XAxis[i].m_fCurMin - fStep, m_XAxis[i].m_fCurMax - fStep));
		}

		Invalidate();
	}

	if (nChar == VK_UP)
	{
		if (m_opOperation == opCursor)
			m_CurrentPoint.y--;
		else
		for (int i = 0; i < m_YAxis.size(); i++)
		{
			double fStep = m_YAxis[i].m_fRange / 10.0;
			VERIFY(m_YAxis[i].SetCurrentRange (m_YAxis[i].m_fCurMin - fStep, m_YAxis[i].m_fCurMax - fStep));
		}

		Invalidate();
	}

	if (nChar == VK_DOWN)
	{
		if (m_opOperation == opCursor)
			m_CurrentPoint.y++;
		else
		for (int i = 0; i < m_YAxis.size(); i++)
		{
			double fStep = m_YAxis[i].m_fRange / 10.0;
			VERIFY(m_YAxis[i].SetCurrentRange (m_YAxis[i].m_fCurMin + fStep, m_YAxis[i].m_fCurMax + fStep));
		}

		Invalidate();
	}
	
	CWnd::OnKeyUp(nChar, nRepCnt, nFlags);
}

void CXGraph::ResetAll()
{
	m_XAxis.clear ();
	m_YAxis.clear ();
	m_Data.clear ();

	// Delete all objects
	// Spare 1st object (CursorLabel)
	while (m_Objects.GetCount () > 1)
		delete m_Objects.RemoveTail ();

}

#ifndef _WIN32_WCE
void CXGraph::OnRButtonDown(UINT nFlags, CPoint point) 
{
	m_bRButtonDown = true;
	
	CWnd::OnRButtonDown(nFlags, point);
}
#endif

void CXGraph::Serialize( CArchive& archive, UINT nFlags)
{	
	int nHelper;
	
	CWnd::Serialize (archive);

    if( archive.IsStoring() )
    {
		archive << nFlags;
		
		if (nFlags & PERSIST_PROPERTIES)
		{
			archive << m_crBackColor;
			archive << m_crGraphColor;
			archive << m_crInnerColor;
			archive << m_bDoubleBuffer;
			archive << m_bShowLegend;
			archive << (int) m_LegendAlignment;
			archive << m_bSnapCursor;
			archive << m_nSnapRange;
			archive << m_nLeftMargin;
			archive << m_nTopMargin;
			archive << m_nRightMargin;
			archive << m_nBottomMargin;
			archive << m_nAxisSpace;
			archive << m_nSelectedSerie;
		}

		if (nFlags & PERSIST_DATA)
		{
			archive << m_Data.size ();
			for (int i = 0; i < m_Data.size(); i++)
				m_Data[i].Serialize (archive);

			archive << m_XAxis.size ();
			for (i = 0; i < m_XAxis.size(); i++)
				m_XAxis[i].Serialize (archive);

			archive << m_YAxis.size ();
			for (i = 0; i < m_YAxis.size(); i++)
				m_YAxis[i].Serialize (archive);
		}

		if (nFlags & PERSIST_OBJECTS)
		{

			archive << m_Measures.size();
			for (int i = 0; i < m_Measures.size(); i++)
				archive.Write(&m_Measures[i], sizeof(MeasureDef));
		
			// Spare cursor label (1st object), created within constructor
			archive << m_Objects.GetCount () - 1;

			i = 0;

			for (POSITION pos = m_Objects.GetHeadPosition (); pos != NULL; i++ )
			{			
				CXGraphObject *pObject = (CXGraphObject*) m_Objects.GetNext (pos);
				if ( i > 0)
					archive << pObject;
		
			}
		}
	}
	else
    {
		archive >> nFlags;

		if (nFlags & PERSIST_PROPERTIES)
		{
			archive >> m_crBackColor;
			archive >> m_crGraphColor;
			archive >> m_crInnerColor;
			archive >> m_bDoubleBuffer;
			archive >> m_bShowLegend;
			archive >> nHelper;
			m_LegendAlignment = (EAlignment) nHelper;
			archive >> m_bSnapCursor;
			archive >> m_nSnapRange;
			archive >> m_nLeftMargin;
			archive >> m_nTopMargin;
			archive >> m_nRightMargin;
			archive >> m_nBottomMargin;
			archive >> m_nAxisSpace;
			archive >> m_nSelectedSerie;
		}

		if (nFlags & PERSIST_DATA)
		{
			archive >> nHelper;
			for (int i = 0; i < nHelper; i++)
			{
				CXGraphDataSerie serie;
				serie.Serialize (archive);
				serie.m_pGraph = this;
				m_Data.push_back (serie);
			}

			archive >> nHelper;
			for (i = 0; i < nHelper; i++)
			{
				CXGraphAxis axis;
				axis.Serialize (archive);
				axis.m_pGraph = this;
				m_XAxis.push_back (axis);
			}

			archive >> nHelper;
			for (i = 0; i < nHelper; i++)
			{
				CXGraphAxis axis;
				axis.Serialize (archive);
				axis.m_pGraph = this;
				m_YAxis.push_back (axis);
			}
		}

		if (nFlags & PERSIST_OBJECTS)
		{
			archive >> nHelper;

			for (int i = 0; i < nHelper; i++)
			{
				MeasureDef measure;
				archive.Read(&measure, sizeof(MeasureDef));
				m_Measures.push_back (measure);
			}
			
			archive >> nHelper;

			for (i = 0; i < nHelper; i++)
			{			
				CXGraphObject *pObject;
				archive >> pObject;
				pObject->m_pGraph = this;
				m_Objects.AddTail (pObject);
			}
		}

		Invalidate();
	}
}

bool CXGraph::Save(const CString cFile, UINT nFlags)
{
	CFile file;

	if (nFlags == 0)
		return false;

	m_pTracker = NULL;
	m_pCurrentObject = NULL;

	if (!file.Open (cFile, CFile::modeCreate | CFile::modeWrite ))
		return false;

#ifndef _WIN32_WCE
	try	
	{
#endif
		CArchive ar(&file, CArchive::store);
		Serialize(ar, nFlags);
#ifndef _WIN32_WCE
	} 
	catch (CException* e)	
	{
		e->Delete ();
		return false;
	}
#endif

	return true;
}

bool CXGraph::Load(const CString cFile)
{
	CFile file;
	
	if (!file.Open (cFile, CFile::modeRead ))
		return false;

	ResetAll();
#ifndef _WIN32_WCE
	try
#endif
	{

		CArchive ar(&file, CArchive::load);
		Serialize(ar, 0);
	} 
#ifndef _WIN32_WCE
	catch (CException* e)
	{
		e->Delete ();
		return false;
	}
#endif
	return true;
}

void CXGraph::ParentCallback()
{
	::PostMessage(GetParent()->m_hWnd, IDM_PARENTCALLBACK, 0, (long) this);		
}

#ifndef _WIN32_WCE
HANDLE CXGraph::DDBToDIB( CBitmap& bitmap, DWORD dwCompression, CPalette* pPal ) 
{
	BITMAP			bm;
	BITMAPINFOHEADER	bi;
	LPBITMAPINFOHEADER 	lpbi;
	DWORD			dwLen;
	HANDLE			hDIB;
	HANDLE			handle;
	HDC 			hDC;
	HPALETTE		hPal;


	ASSERT( bitmap.GetSafeHandle() );

	// The function has no arg for bitfields
	if( dwCompression == BI_BITFIELDS )
		return NULL;

	// If a palette has not been supplied use defaul palette
	hPal = (HPALETTE) pPal->GetSafeHandle();
	if (hPal==NULL)
		hPal = (HPALETTE) GetStockObject(DEFAULT_PALETTE);

	// Get bitmap information
	bitmap.GetObject(sizeof(bm),(LPSTR)&bm);

	// Initialize the bitmapinfoheader
	bi.biSize		= sizeof(BITMAPINFOHEADER);
	bi.biWidth		= bm.bmWidth;
	bi.biHeight 		= bm.bmHeight;
	bi.biPlanes 		= 1;
	bi.biBitCount		= bm.bmPlanes * bm.bmBitsPixel;
	bi.biCompression	= dwCompression;
	bi.biSizeImage		= 0;
	bi.biXPelsPerMeter	= 0;
	bi.biYPelsPerMeter	= 0;
	bi.biClrUsed		= 0;
	bi.biClrImportant	= 0;

	// Compute the size of the  infoheader and the color table
	int nColors = (1 << bi.biBitCount);
	if( nColors > 256 ) 
		nColors = 0;
	dwLen  = bi.biSize + nColors * sizeof(RGBQUAD);

	// We need a device context to get the DIB from
	hDC = ::GetDC(NULL);
	hPal = SelectPalette(hDC,hPal,FALSE);
	RealizePalette(hDC);

	// Allocate enough memory to hold bitmapinfoheader and color table
	hDIB = GlobalAlloc(GMEM_FIXED,dwLen);

	if (!hDIB){
		SelectPalette(hDC,hPal,FALSE);
		::ReleaseDC(NULL,hDC);
		return NULL;
	}

	lpbi = (LPBITMAPINFOHEADER)hDIB;

	*lpbi = bi;

	// Call GetDIBits with a NULL lpBits param, so the device driver 
	// will calculate the biSizeImage field 
	GetDIBits(hDC, (HBITMAP)bitmap.GetSafeHandle(), 0L, (DWORD)bi.biHeight,
			(LPBYTE)NULL, (LPBITMAPINFO)lpbi, (DWORD)DIB_RGB_COLORS);

	bi = *lpbi;

	// If the driver did not fill in the biSizeImage field, then compute it
	// Each scan line of the image is aligned on a DWORD (32bit) boundary
	if (bi.biSizeImage == 0){
		bi.biSizeImage = ((((bi.biWidth * bi.biBitCount) + 31) & ~31) / 8) 
						* bi.biHeight;

		// If a compression scheme is used the result may infact be larger
		// Increase the size to account for this.
		if (dwCompression != BI_RGB)
			bi.biSizeImage = (bi.biSizeImage * 3) / 2;
	}

	// Realloc the buffer so that it can hold all the bits
	dwLen += bi.biSizeImage;
	if (handle = GlobalReAlloc(hDIB, dwLen, GMEM_MOVEABLE))
		hDIB = handle;
	else{
		GlobalFree(hDIB);

		// Reselect the original palette
		SelectPalette(hDC,hPal,FALSE);
		::ReleaseDC(NULL,hDC);
		return NULL;
	}

	// Get the bitmap bits
	lpbi = (LPBITMAPINFOHEADER)hDIB;

	// FINALLY get the DIB
	BOOL bGotBits = GetDIBits( hDC, (HBITMAP)bitmap.GetSafeHandle(),
				0L,				// Start scan line
				(DWORD)bi.biHeight,		// # of scan lines
				(LPBYTE)lpbi 			// address for bitmap bits
				+ (bi.biSize + nColors * sizeof(RGBQUAD)),
				(LPBITMAPINFO)lpbi,		// address of bitmapinfo
				(DWORD)DIB_RGB_COLORS);		// Use RGB for color table

	if( !bGotBits )
	{
		GlobalFree(hDIB);
		
		SelectPalette(hDC,hPal,FALSE);
		::ReleaseDC(NULL,hDC);
		return NULL;
	}

	SelectPalette(hDC,hPal,FALSE);
	::ReleaseDC(NULL,hDC);
	return hDIB;
}


BOOL CXGraph::WriteDIB( LPTSTR szFile, HANDLE hDIB)
{
	BITMAPFILEHEADER	hdr;
	LPBITMAPINFOHEADER	lpbi;

	if (!hDIB)
		return FALSE;

	CFile file;
	if( !file.Open( szFile, CFile::modeWrite|CFile::modeCreate) )
		return FALSE;

	lpbi = (LPBITMAPINFOHEADER)hDIB;

	int nColors = 1 << lpbi->biBitCount;

	// Fill in the fields of the file header 
	hdr.bfType		= ((WORD) ('M' << 8) | 'B');	// is always "BM"
	hdr.bfSize		= GlobalSize (hDIB) + sizeof( hdr );
	hdr.bfReserved1 	= 0;
	hdr.bfReserved2 	= 0;
	hdr.bfOffBits		= (DWORD) (sizeof( hdr ) + lpbi->biSize +
						nColors * sizeof(RGBQUAD));

	// Write the file header 
	file.Write( &hdr, sizeof(hdr) );

	// Write the DIB header and the bits 
	file.Write( lpbi, GlobalSize(hDIB) );

	return TRUE;
}

BOOL CXGraph::SaveBitmap( const CString cFile )
{
	CWnd*       pWnd = this;
	CBitmap 	bitmap;
	CWindowDC	dc(pWnd);
	CDC 		memDC;
	CRect		rect;

	
	memDC.CreateCompatibleDC(&dc); 

	pWnd->GetWindowRect(rect);

	bitmap.CreateCompatibleBitmap(&dc, rect.Width(),rect.Height() );
	
	CBitmap* pOldBitmap = memDC.SelectObject(&bitmap);
	memDC.BitBlt(0, 0, rect.Width(),rect.Height(), &dc, 0, 0, SRCCOPY); 

	// Create logical palette if device support a palette
	CPalette pal;
	if( dc.GetDeviceCaps(RASTERCAPS) & RC_PALETTE )
	{
		UINT nSize = sizeof(LOGPALETTE) + (sizeof(PALETTEENTRY) * 256);
		LOGPALETTE *pLP = (LOGPALETTE *) new BYTE[nSize];
		pLP->palVersion = 0x300;

		pLP->palNumEntries = 
			GetSystemPaletteEntries( dc, 0, 255, pLP->palPalEntry );

		// Create the palette
		pal.CreatePalette( pLP );

		delete[] pLP;
	}

	memDC.SelectObject(pOldBitmap);

	// Convert the bitmap to a DIB
	HANDLE hDIB = DDBToDIB( bitmap, BI_RGB, &pal );

	if( hDIB == NULL )
		return FALSE;

	// Write it to file
	WriteDIB( const_cast <char*>( (LPCTSTR) cFile ), hDIB );

	// Free the memory allocated by DDBToDIB for the DIB
	GlobalFree( hDIB );
	return TRUE;
}
#endif

#pragma warning (default : 4244)
#pragma warning (default : 4800)


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

Gunnar Bolle
Web Developer
Germany Germany
No Biography provided

You may also be interested in...

Pro
Pro
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170217.1 | Last Updated 17 Jan 2005
Article Copyright 2002 by Gunnar Bolle
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid