有一位太太怀孕了。在第9个月的时候,先生终于忍不住了,强迫他太太跟他做了一些事情。
一个月后,小孩生出来了!是个男生,而且是个天才,一出生就会讲话!
只见他转头一看见医生就问:“你是不是我爸?”医生说:“不是,我是医生。”
然后他又看到了护士,也问说:“你是不是我爸?”护士说:“不是,我是护士。”
最后他看到了他的亲生父亲,问说:“你是不是我爸?”他父亲很高兴的说:“对呀!我就是你的爸爸!”
结果小孩就很生气地一边拿手指戳他老爸的头,一边骂:“这样戳你痛不痛?痛不痛?痛不痛???”
哈哈,本节课笨笨就来讲讲CChart的子类化。前面讲客户区自绘的时候,已经说过要讲的,今天笨笨就来履行承诺。
其实嘛,笨笨这里说的子类化,严格来说,是要打个引号的,因为这并不是通常说的子类化。Windows下通常说的子类化,是指把一个窗口的窗口函数替换成我们自定义的函数。嗯嗯,这个嘛,不就是CChartWnd中Attach函数干的事情吗?
本节课说的子类化要简单多了,实际上,就是从CChart类继承出一个子类,然后在子类中自定义绘图动作和消息响应动作。看明白了,笨笨说的子类化其实就是C++的子类继承,是不是有种上当受骗的感觉呢?
其实呢,这里虽然只是简单的子类继承,但实际上也是重新定义一些消息的响应函数,这和真正的子类化中通过设置窗口函数替换消息响应函数是一脉相承的。所以,笨笨也没有骗大家哈。
不废话了,下面开始!
本节课笨笨仍然计划在第一课的基础上进行修改。毕竟嘛,初恋是最甜蜜的,哈哈。
目标很简单,就是在程序已有的界面上,增加一个圆,这个圆可以用鼠标拖动。
先可以简单思考一下。在屏幕上画圆的话,可以采用高四第五课的客户区自绘函数;鼠标拖动的话,上一课刚刚讲了,可以自定义鼠标消息;但这两个任务是耦合的,相互关系需要用全局变量来联系。
所以呢,从原则上来说,本节课的任务,用以前讲过的知识已经可以完成了。
这里,本节课就采用一种全新的方法来实现这个任务。
首先看看CChart的那些函数可以重定义。很简单,前面带有virtual的函数才可以,大家都明白C++虚函数的多态性,所以笨笨这里也不多说了。先列出CChart类中所有的虚函数如下。
virtual void OnDraw();
virtual void OnDraw(HWND hWnd);
virtual void OnDraw(HDC hDC);
virtual void OnDraw(HWND hWnd, RECT destRect);
virtual void OnDraw(HDC hDC, RECT destRect);
virtual bool OnLButtonDown( HWND hWnd, POINT point, UINT ctrlKey );
virtual bool OnLButtonUp( HWND hWnd, POINT point, UINT ctrlKey );
virtual bool OnLButtonDblClk( HWND hWnd, POINT point, UINT ctrlKey );
virtual bool OnMouseMove( HWND hWnd, POINT point, UINT ctrlKey );
virtual bool OnMouseLeave( HWND hWnd, POINT point, UINT ctrlKey );
virtual bool OnMouseWheel( HWND hWnd, POINT point, UINT ctrlKey );
virtual bool OnContextMenu( HMENU hMenu, HWND hWnd, POINT point );
virtual bool OnKeyDown( HWND hWnd, UINT key );
virtual bool OnEvent(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
这里,前面5个是绘图函数,后面的全是消息响应函数。
为方便大家在屏幕自由绘图,笨笨还专门提供了如下几个底层函数。
// 底层函数,获取子图的绘图区域
RECT GetLastClientRect(int nPlotIndex);
// 底层函数,获取绘图区域
RECT GetLastClientRect();
// 底层函数,获取子图的数据区域
RECT GetLastPlotRect(int nPlotIndex);
// 底层函数,获取数据区域
RECT GetLastPlotRect();
// 底层函数,获取坐标轴的范围
void GetLastPlotRange(double xRange[2], double yRange[2], int nPlotIndex=0);
// 底层函数,获取数据的范围
void GetLastDataRange(double xRange[2], double yRange[2], int nPlotIndex=0);
// 底层函数,根据屏幕坐标计算数据坐标
void ClientToData(POINT *point, double *data, int nPlotIndex=0);
// 底层函数,根据数据坐标计算屏幕坐标
void DataToClient(double *data, POINT *point, int nPlotIndex=0);
当然这些函数在前面讲的屏幕自绘和消息自定义中,也可以使用。
下面开始。
第一步,采用和第一课同样的步骤,建立和第一课一模一样的程序LessonA19。
第二步,先简单测试一下子类化。
1) 从CChart继承一个子类CSubChart。
class CSubChart : public CChart
{
// 子类化键盘消息函数
bool OnKeyDown( HWND hWnd, UINT key )
{
MessageBox(NULL, _T("在子类里面哟!"), _T("提示"), MB_OK);
//子类化消息处理函数时,应调用默认函数,否则消息处理将会混乱
return CChart::OnKeyDown(hWnd, key);
}
//CChart里面有四个OnDraw函数,在CChartWnd中实际调用的是下面这个,故需要子类化它
void OnDraw(HWND hWnd)
{
//需要先调用默认函数,因为这个函数会清背景
CChart::OnDraw(hWnd);
RECT rtPlot = GetLastPlotRect();
HDC hDC = GetDC(hWnd);
TCHAR str[] = _T("我现在是在子类中画图哟");
TextOut(hDC, rtPlot.left+10, rtPlot.top+10, str, _tcslen(str));
ReleaseDC(hWnd, hDC);
}
};
2) 设置两个全局变量。
CSubChart *pSubChart = NULL;
CChart *pOldChart = NULL;
3) 在WM_CREATE消息里面,Attach的下面,添加代码。
pSubChart = new CSubChart;
pOldChart = chartWnd.SetChart(pSubChart);
这里利用SetChart把我们编写的子类对象替换掉原来的CChart类对象,原来的CChart类对象保存到pOldChart指针里。
4) 在WM_DESTROY消息里面,添加代码。
chartWnd.SetChart(pOldChart);
if(pSubChart)delete pSubChart;
chartWnd.Detach();
这里我们把保存的pOldChart又替换回去,并把我们建立的子类对象delete掉。
其实,CChartWnd析构时会自动删除内部的CChart类对象。这里为什么不delete pOldChart,让CChartWnd自动删除pSubChart对象呢?
实际上,本来这里是可以的,但我们现在用的是DLL,动态库的内存管理比较奇葩。DLL文件的内存空间和用户程序的内存空间是不同的,所以当我们删除pOldChart时,程序会立马崩溃,且CChartWnd析构时删除我们新建的pSubChart时,同样也会崩溃。笨笨在这里让两个指针各自归位,就不会有问题了。
程序运行结果如图。
随便按一个键,结果弹出对话框。
实际上,到这里为止,CChart子类化的方法已经介绍结束了,是不是非常简单呢?
不过,我们还是利用这种方法完成前面提出的任务。
第三步,正式开始表演,先画椭圆。
1)增加一个宏定义
#define SQR(x) ((x)*(x))
这个宏是用于计算平方的,有些表达式比较长,用这个宏可以省点事。
2)增加一个函数,用于判断鼠标是否在圆内部。
bool PointInCircle(POINT point, RECT plotRect)
{
return SQR( point.x-ptCenter.x-plotRect.left ) + SQR( point.y-ptCenter.y-plotRect.top )<=SQR( Radius );
}
3)在CSubChart的定义之前,设置如下的全局变量。
POINT ptCenter = {80, 60};
long Radius = 20;
bool bSelected = false;
bool bLighted = false;
COLORREF crNorm = RGB(192, 128, 128);
COLORREF crLighted = RGB(255, 128, 128);
COLORREF crSelected = RGB(0, 128, 128);
COLORREF crFill = RGB(192, 128, 128);
bool bDraged = false;
POINT ptOld, ptCenterOld;
因为我们的任务不是特别简单,所以增加了比较多的变量。
前两个变量就是圆心和半径,注意笨笨这里的圆心坐标是相对于绘图区左上角的坐标。
中间六个变量用于处理鼠标单击和移动。这里我们考虑鼠标单击圆后bSelected为true,鼠标掠过圆时bLighted为true;
后面三个变量用于处理圆的拖动。
4)把OnDraw修改为如下形式:
void OnDraw(HWND hWnd)
{
//需要先调用默认函数,因为这个函数会清背景
CChart::OnDraw(hWnd);
RECT rtPlot = GetLastPlotRect();
HDC hDC = GetDC(hWnd);
//TCHAR str[] = _T("我现在是在子类中画图哟");
//TextOut(hDC, rtPlot.left+10, rtPlot.top+10, str, _tcslen(str));
HPEN hPen, hOldPen;
HBRUSH hBrush, hOldBrush;
COLORREF crLine;
int linew = 2;;
crLine = bSelected?crSelected:(bLighted?crLighted:crNorm);
linew = bSelected?linew+2:(bLighted?linew+1:linew);
hPen = CreatePen(PS_SOLID, linew, crLine);
hOldPen = (HPEN)SelectObject(hDC, hPen);
hBrush = CreateSolidBrush(crFill);
hOldBrush = (HBRUSH)SelectObject(hDC, hBrush);
Ellipse(hDC, rtPlot.left+ptCenter.x-Radius, rtPlot.top+ptCenter.y-Radius, rtPlot.left+ptCenter.x+Radius, rtPlot.top+ptCenter.y+Radius);
SelectObject(hDC, hOldPen);
DeleteObject(hPen);
SelectObject(hDC, hOldBrush);
DeleteObject(hBrush);
ReleaseDC(hWnd, hDC);
}
注意到根据圆选择和高亮的不同情况选择颜色和线宽。
5)重载OnLButtonDown,OnLButtonUp,OnMouseMove三个处理鼠标消息的虚函数。
bool OnLButtonDown( HWND hWnd, POINT point, UINT ctrlKey )
{
static bool bSelected_Old = false;
bool needUpdate = CChart::OnLButtonDown(hWnd, point, ctrlKey);
if(PointInCircle(point, GetLastPlotRect()))
{
bSelected = true;
bDraged = true;
ptOld = point;
ptCenterOld = ptCenter;
RECT plotRect = GetLastPlotRect();
//SetCapture(hWnd);
ClientToScreen(hWnd, (LPPOINT)&plotRect);
ClientToScreen(hWnd, (LPPOINT)&plotRect+1);
ClipCursor(&plotRect);
needUpdate = true;
}
else
{
if(bSelected != bSelected_Old)needUpdate = true;
bSelected = false;
bSelected_Old = bSelected;
}
return needUpdate;
}
bool OnLButtonUp( HWND hWnd, POINT point, UINT ctrlKey )
{
bool needUpdate = false;
if(bDraged)
{
bDraged = false;
needUpdate = true;
}
return CChart::OnLButtonUp(hWnd, point, ctrlKey) | needUpdate;
}
bool OnMouseMove( HWND hWnd, POINT point, UINT ctrlKey )
{
static bool bLighted_Old = false;
bool needUpdate = false;
if(bDraged)
{
ptCenter.x = ptCenterOld.x + (point.x - ptOld.x);
ptCenter.y = ptCenterOld.y + (point.y - ptOld.y);
RECT plotRect = GetLastPlotRect();
if(ptCenter.x - Radius < 0)
ptCenter.x += (Radius - ptCenter.x);
if(ptCenter.x + Radius > plotRect.right - plotRect.left)
ptCenter.x -= (ptCenter.x + Radius - plotRect.right + plotRect.left);
if(ptCenter.y - Radius < 0)
ptCenter.y += (Radius - ptCenter.y);
if(ptCenter.y + Radius > plotRect.bottom - plotRect.top)
ptCenter.y -= (ptCenter.y + Radius - plotRect.bottom + plotRect.top);
needUpdate = true;
}
else
{
if(PointInCircle(point, GetLastPlotRect()))
{
bLighted = true;
}
else
{
bLighted = false;
}
if(bLighted!=bLighted_Old)needUpdate = true;
bLighted_Old = bLighted;
}
return CChart::OnMouseMove(hWnd, point, ctrlKey) | needUpdate;
}
在这三个重载函数里,我们都根据实际情况调用了CChart的默认版本。一般情况下,这是必须的,因为CChart的内部消息响应机制是比较复杂的,缺了一部分消息,那响应肯定会出问题。
注意到OnLButtonDown函数里面,笨笨把SetCapture注释掉了,原因是默认函数里面调用了SetCapture,这里就不需要了。
这几个函数比较完美地处理了圆的选择、高亮和拖动。基本上,CChart内部也是用同样的方式处理相关问题的。
最后我们得到的效果如图。
同学们可以试试,这个圆在鼠标掠过和鼠标单击时是有变化的,同时可以用鼠标拖动,拖动时不会超出绘图区的边界。
本节课老师拖堂了,哈哈,下课!