Introduction
This HitTester demo application was written just to illustrate the basic drawing application framework using GDI/GDI+. It also features the useful curve/line hit test technique together with the implementation of CObject
class and CObArray
collection class which can be extended for serialization in future. I also added extra features for storing the drawing into *.png file format.
Background
Hit test is very important for developing good drawing kind of applications to determine whether the object is currently selected or not. There are various methods that can be used to perform curve or line hit test. For diagonal (straight) line, performing hit test can be done by comparing the two slopes of the object line and the test line. Test line is a line that is virtually constructed starting from the first point of the line shape to the test point. If both slopes match, the object line is hit.
For the case of curved-line, it is a bit difficult to perform hit test. Normally, flatten technique will be used to convert the curved line to a series of connected straight lines before performing slope comparison for each line segment. Phew! Thanks to GDI+ for the GraphicsPath
class and its IsOutlineVisible()
member function, this hit test task can be done very easily.
You can refer to this article from MSDN regarding hit test using Win32. And this one as well for creating cables and connectors...
Using the code
Below are the steps for implementing the object class and GDI+ initialization.
Step 1:
Add the lines below to StdAfx.h file:
#include <gdiplus.h>
#include <Gdiplusinit.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
Step 2:
Add the lines below to HitTester.h file:
GdiplusStartupInput m_gdiplusStartupInput;
ULONG_PTR m_gdiplusToken;
Step 3:
Add the line below to HitTester.cpp file, under the CHitTesterApp::InitInstance()
function, before the return
statement:
GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);
Step 4:
Add the line below to HitTester.cpp file, under the CHitTesterApp::Exitnstance()
function:
GdiplusShutdown(m_gdiplusToken);
Step 5:
Add the line below to HitTesterView.h file:
#include "HCNObject.h"
Step 6:
Add the lines below to HitTesterView.cpp:
void CHitTesterView::OnLButtonDown(UINT nFlags, CPoint point)
{
CRect rect;
GetClientRect(rect);
if(rect.PtInRect(point))
{
if(m_bSelectMode)
{
CDC* pDC = this->GetDC();
VERIFY(pDC != NULL);
if(pDC != NULL)
{
int nR2 = pDC->SetROP2(R2_NOTXORPEN);
CPen* newPen = new CPen(PS_SOLID|PS_GEOMETRIC, 1, RGB(255,0,255));
CPen* pOldPen = pDC->SelectObject(newPen);
for(int i=0; i<m_obArray->GetSize(); i++)
{
CHCN_Object* pBase = m_obArray->GetObjectBaseClass(i);
if(pBase->GetObjectType() == pBase->HCN_Line)
{
CHCN_ObjectLine* pLine = (CHCN_ObjectLine*)pBase;
if(m_bSelectSegment)
{
if(pLine->IsSegmentHit(point))
{
if(pLine->m_bSelected)
{
pLine->m_bSelected = FALSE;
pLine->m_bShowNode = FALSE;
}
else
{
pLine->m_bSelected = TRUE;
pLine->m_bShowNode = TRUE;
}
}
}
if(m_bSelectNode)
{
if(pLine->IsNodeHit(point))
{
if(pLine->m_bSelected)
{
pLine->m_bSelected = FALSE;
pLine->m_bShowNode = FALSE;
}
else
{
pLine->m_bSelected = TRUE;
pLine->m_bShowNode = TRUE;
}
}
}
}
Invalidate();
}
VERIFY(nR2 >= 0);
pDC->SetROP2(nR2);
pDC->SelectObject(pOldPen);
pDC->Detach();
pDC = NULL;
}
}
if(m_bDrawMode)
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
if(m_bDrawLine || m_bDrawCurve)
{
if(!m_bDrawBegin)
{
m_bDrawBegin = TRUE;
m_ptArray = new CHCN_ObjectArray();
}
AddPointToArray(point);
}
}
}
CView::OnLButtonDown(nFlags, point);
}
void CHitTesterView::OnMouseMove(UINT nFlags, CPoint point)
{
CRect rect;
GetClientRect(rect);
if( rect.PtInRect(point))
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW));
if(m_bDrawMode)
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
if(m_bDrawBegin && (nFlags != MK_LBUTTON))
{
CDC* pDC = this->GetDC();
VERIFY(pDC != NULL);
if(pDC != NULL)
{
int nR2 = pDC->SetROP2(R2_NOTXORPEN);
if(m_bDrawLine || m_bDrawCurve)
{
pDC->DPtoLP(&m_pt1);
pDC->DPtoLP(&m_pt2);
pDC->MoveTo(m_pt1);
pDC->LineTo(m_pt2);
m_pt2 = point;
pDC->DPtoLP(&m_pt2);
pDC->MoveTo(m_pt1);
pDC->LineTo(m_pt2);
}
VERIFY(nR2 >= 0);
pDC->SetROP2(nR2);
pDC->Detach();
pDC = NULL;
}
}
}
}
else
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW));
}
CView::OnMouseMove(nFlags, point);
}
void CHitTesterView::OnDraw(CDC* pDC)
{
CHitTesterDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
if(m_obArray->GetSize() > 0)
{
CRect rect;
GetClientRect(rect);
Bitmap bmp(rect.Width(),rect.Height());
Graphics* graph = Graphics::FromImage(&bmp);
for(int i=0; i<m_obArray->GetSize(); i++)
{
CHCN_Object* pBase =
reinterpret_cast<CHCN_Object*>(m_obArray->GetAt(i));
if(pBase->GetObjectType() == pBase->HCN_Line)
{
CHCN_ObjectLine* pLine = (CHCN_ObjectLine*)m_obArray->GetAt(i);
pLine->DrawObject(*graph);
}
}
Rect rc(rect.left,rect.top,rect.Width(),rect.Height());
m_Bitmap = bmp.Clone(rc, PixelFormatDontCare);
Graphics graphics(pDC->m_hDC);
graphics.DrawImage(m_Bitmap, rc);
}
}
void CHitTesterView::OnFileSave()
{
const char szFilter[] = "Image Files (*.png)|*.png|All Files (*.*)|*.*||";
const char szExt[] = "png";
CFileDialog* dlg = new CFileDialog(FALSE, szExt, NULL,
OFN_CREATEPROMPT|OFN_OVERWRITEPROMPT, szFilter, this);
if(dlg->DoModal() == IDOK)
{
CLSID pngClsid;
GetEncoderClsid(L"image/png", &pngClsid);
m_Bitmap->Save(dlg->GetPathName().AllocSysString(), &pngClsid, NULL);
}
}
int CHitTesterView::GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num = 0;
UINT size = 0;
ImageCodecInfo* pImageCodecInfo = NULL;
GetImageEncodersSize(&num, &size);
if(size == 0)
return -1;
pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
if(pImageCodecInfo == NULL)
return -1;
GetImageEncoders(num, size, pImageCodecInfo);
for(UINT j = 0; j < num; ++j)
{
if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )
{
*pClsid = pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j;
}
}
free(pImageCodecInfo);
return -1;
}