具体而微的绘图程式-c++ Borland 入门
2008-03-08 21:40:51 来源:WEB开发网核心提示:在本章中我将为你示范如何在C++Builder中撰写一个完整的绘图程式,藉由这个程式的撰写,具体而微的绘图程式-c++ Borland 入门,你会更加了解C++Builder的 Canvas 绘图精神,而在撰写这个程式的同时,//-void __fastcall TGraphEx::LineButtonClick(TO
在本章中我将为你示范如何在C++Builder中撰写一个完整的绘图程式。藉由这个程式的撰写,你会更加了解C++Builder的 Canvas 绘图精神,而在撰写这个程式的同时,我们也可将相关的技术做一个整体的检阅。此绘图程式的执行结果如下:
点击查看大图
在此程式中我会以循序渐进的方式一步一步地带领你完成整个程式,基本上这个程式和C++Builder内附的范例程式有几分类似,但我必须要说明的是:在 C++Builder中所附的范例程式是直接由原先在Delphi内以 Object Pascal 所撰写的范例程式修改而成,所以有部份程式的写法大为违反C++ 式物件导向精神,在迈入C++Builder 的新世纪之後,我们当然希望写出的程式是『系出名门,血统纯正』的C++ 式的物件导向程式。而这就是我在本章中希望带领你完成的程式。
XX-01 关於滑鼠事件(Mouse Event)
撰写绘图程式,首先要了解滑鼠事件,在Windows中定义了许多的滑鼠讯息(Message),而这些滑鼠讯息在BCB中就成为滑鼠事件了,为了要处理滑鼠事件,我们必须要选写滑鼠事件处理程式:
在Windows中定义的滑鼠讯息列表
WM_CAPTURECHANGED
WM_LBUTTONDBLCLK
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_MBUTTONDBLCLK
WM_MBUTTONDOWN
WM_MBUTTONUP
WM_MOUSEACTIVATE
WM_MOUSEMOVE
WM_NCHITTEST
WM_NCLBUTTONDBLCLK
WM_NCLBUTTONDOWN
WM_NCLBUTTONUP
WM_NCMBUTTONDBLCLK
WM_NCMBUTTONDOWN
WM_NCMBUTTONUP
WM_NCMOUSEMOVE
WM_NCRBUTTONDBLCLK
WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
WM_RBUTTONDBLCLK
WM_RBUTTONDOWN
WM_RBUTTONUP
表XX-01 Windows内滑鼠相关 Message。
虽然在Windows作业系统中定义了非常多的讯息,但是在C++Builder 中已经把庞大的讯息系统作适度的简化了,并且不再以讯息的方式存在,而改以事件 (Event)的处理方式,在本章的绘图程式中,我们只要处理以下的几个事件即可:
OnMouseDown 滑鼠键按下事件
OnMouseMove 滑鼠移动事件
OnMouseUp 滑鼠键放开事件
OnClick 任何滑鼠的点取
在此,你可以很明显地发现,在C++Builder的事件中并未将左右滑鼠键分别定义,而是以合并处理的方式,因此在收到以上滑鼠事件时,若你要分辨左右滑鼠事件时,必须在事件处理程式中判定左右键。
具备了基本的滑鼠事件认知後,我们开始进行後续的程式探索吧!
为了让你实际了解程式的细节,我希望将程式撰写的步骤细节交代楚,在往下进行之前,我们先建立一个新的专案档,并将其命名为 DrawMain,同时将Form的Color性质设为黑色(clBlack),以便直接在上面画图。
XX-02滑鼠事件的处理
当C++ Builder应用程式侦测到物件滑鼠事件时,它会检查你是否定义该物件相对应的滑鼠事件处理程式,然後呼叫该函数,将相关参数传给它。以OnMouseDown事件为例,它的事件处理程式模版如下:
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
}
它总共接收了以下几个参数:
Sender 引发该事件的软体元件。
Button 表示滑鼠的按键。它的值可为mbLeft(左键),mbRight(右键),mbMiddle(中间键)。
Shift 用以表示事件发生的同时Alt,Shift及Ctrl叁键的状态。
X,Y 用以表示事件发生时之座标位置。
在大多数的情况下,滑鼠事件的(X,Y)座标值是我们最为感爱好的项目,不过,有时候我们也需要靠Button键来判定滑鼠的按键,或是需要利用Shift来取得非凡键的状态,而做一些额外的程式处理。
XX-02-01 OnMouseDown事件的处理
首先我们先以一个最基本的画线程式来说明OnMouseDown事件的处理,当使用者按下滑鼠时,我们希望将笔移至事件发生时的坐标,因此我们可将程式写成如下:
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
Canvas->MoveTo(X,Y);
}
XX-02-03 OnMouseUp事件的处理
同样地,我们可以再为这个Form加上OnMouseUp的事件处理函式,在收到OnMouseUp事件时,由滑鼠点下的坐标,画一条直线至现在的坐标。
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { Canvas->LineTo(X,Y); } 在写完了以上两个事件处理函式之後,我们就可以在Form上面作画了,你可以用滑鼠在Form上面拖戈出一条条直线。其执行结果大致如图XX-01: 图XX-01 XX-02-02 OnMouseMove事件的处理 在加上了OnMouseDown及OnMouseUp处理函式之後,我们只能画出一条条直线,若是我们想要以滑鼠画出不规则线段时,就必须再处理OnMouseMove事件,利用OnMouseMove事件,我们可以追纵到滑鼠移动的位置,简单的OnMouseMove事件处理函式如下: void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { Canvas->LineTo(X,Y); } 此程式的意义即在於将滑鼠所经过的每个点,以线条连接起来,在加上OnMouseMove 事件处理函式之後,它的执行结果会变成图XX-02: 图XX-02 XX-02-03 滑鼠的处理的加强 前面的程式对於滑鼠的移动处理有部份考虑的不够周详,因为它在滑鼠移动时不分青红皂白就将线画在萤慕上,造成萤幕上的线条混乱,这并不是正规的处理方法,正确的处理方法应该如下: (1) 滑鼠键按下时,将记录滑鼠按下的旗标设为True.同时将该点记录下来,谓之原点。
{ PRotected: TCanvas* m_pCanvas; TColor m_Color; int m_nWidth; public: CShape(TCanvas* pCanvas) {m_pCanvas = pCanvas;} virtual ~CShape() {} virtual void OnMouseMove(int,int)=0; virtual void OnMouseDown(int,int)=0; virtual void OnMouseUp(int,int)=0; }; 我们首先定义一个CShape类别,它是所有绘图物件之始,也因此它定义了一个绘图物件的基本行为。在此绘图程式中我希望它可以处理叁个不同的滑鼠事件并加以处理之,所以我在CShape中定义了叁个相对应的成员函式,而且它们都是纯虚拟函式,表示所有继续自CShape的类别都必须改写此叁个成员函式。 (关於物件导向的关念请参阅 <必要的C++ 基础章节> 或是相关书籍,在此尽作简短的解释)。这叁个函式名称称如下: virtual void OnMouseMove(int,int)=0; virtual void OnMouseDown(int,int)=0; virtual void OnMouseUp(int,int)=0; 另外我们再定义一般性的绘图物件都会用到的基本特性,如颜色及线条宽度,再加上绘图时所需要的 Canvas,如此就组成了CShape的类别定义: TCanvas* m_pCanvas; // 绘图所需的Canvas TColor m_Color; // 颜色 int m_nWidth; // 宽度 至於CShape的解构函式为何也设成virtual呢?这关系到继续物件的毁灭方法。若是基础类别的解构函式没有定义成虚拟函式时,会造成特定情况下,子类别的解构函式没有被呼叫到的情形: 如: CLine *pLine = new Line; CShape* pShape=pLIne; delete pShape; 上述的例子因为CLine为CShape的子类别,因此可以直接将pShape指标指向pLine,然而在後面delete pShape时,若是pShape的解构函式不为虚拟函式,会造成pLine的解构函式不被呼叫到。这是一般C++ 程式设计时很轻易犯的错误。 我们可以将以上的经验法则归纳成一个原则,即是:只要该类别有可能被继续,就必须将其解构函式设为虚拟函式。如此就有了以下的定义了: CShape(TCanvas* pCanvas) {m_pCanvas = pCanvas;} virtual ~CShape() {} CShape的建构函式必须传入Canvas以便绘图,而解构函式则不做任何事,只将其定义为虚拟函式。 XX-03-02 CLine类别定义及实作 画直线的类别 class CLine : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CLine(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CLine() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; 我们将CLine定义为一个画直线的类别,而我们希望在画直线时可以在拖弋滑鼠时将原先的线条擦去,并画出新的线,因此我们必须宣告两个点来记载滑鼠按下的点及上次的点以便擦去原来的线条。 以下就是CLine对於叁个滑鼠事件的处理函式: // 滑鼠按下的事件处理函式 // 1. 设定原点及上个启始点为目前所在点。 // 2. 移动至目前所在点。 void CLine::OnMouseDown(int x,int y) { m_ptOrigin.x = m_ptMove.x = x; m_ptOrigin.y = m_ptMove.y = y; m_pCanvas->MoveTo(x,y); } // 滑鼠移动事件处理函式 // 1.将画笔模式设为XOR模式,以便擦去上一条线。 // 2.擦去原来的线(以XOR模式再画一次就会擦去了) // 3.在目前的位置画出一条新线。 // 4.更新坐标并改变画笔模式。 void CLine::OnMouseMove(int x,int y) { m_pCanvas->Pen->Mode = pmXor; m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(m_ptMove.x,m_ptMove.y); m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(x,y); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas->Pen->Mode = pmCopy; } // 滑鼠放开事件处理函式 // 1.画出原点至目前点的直线。 void CLine::OnMouseUp(int x,int y) {
m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(x,y); } 这就是画直线类别的定义及实作内容。 XX-03-03 CPolyline类别定义及实作 画随意线的类别 class CPolyline : public CShape { public: POINT m_ptOrigin; public: CPolyline(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CPolyline() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; CPolyline类别其实和我们前面所写的涂鸦程式的行为模式极为类似,所以我就简单带过好了。 void CPolyline::OnMouseDown(int x,int y) { m_ptOrigin.x = x; m_ptOrigin.y = y; m_pCanvas->MoveTo(x,y); } void CPolyline::OnMouseMove(int x,int y) { m_pCanvas->LineTo(x,y); } void CPolyline::OnMouseUp(int x,int y) { m_pCanvas->LineTo(x,y); } XX-03-04 CPolygon类别定义及实作 画多边形的类别 class CPolygon : public CPolyline { public: CPolygon(TCanvas* pCanvas):CPolyline(pCanvas){} virtual ~CPolygon() {} virtual void OnMouseUp(int,int); }; CPolygon是CPolyline的子类别,其差别仅在於它会将首尾两点连接,使其成为一个多边形,因此我们就直接由CPolyline继续而来,只改写其OnMouseUp成员函式即可。 void CPolygon::OnMouseUp(int x,int y) { m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(x,y); } XX-03-05 CRectangle类别定义及实作 画矩形的类别 class CRectangle : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CRectangle(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CRectangle() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; 画矩形类别其实和画线类别有些类似,它们同样必须记载上次滑鼠移动的点,并擦掉原来的图形画出新的图形,所以我只针对其相异的部份加以说明之: // 滑鼠移动事件处理函式 // 原理和CLine类似,只不过改成画矩形。 void CRectangle::OnMouseMove(int x,int y) { m_pCanvas->Pen->Mode = pmXor; m_pCanvas->Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas->Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y); m_pCanvas->Pen->Mode = pmCopy; } XX-03-06 CRoundRect类别定义及实作 画圆矩形的类别 class CRoundRect : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CRoundRect(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CRoundRect() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; CRoundRect的实作几乎和Crectangle相同,只不过它们呼叫不同的API罢了,CRoundRect是以Canvas->RoundRect来画出图形的。 void CRoundRect::OnMouseMove(int x,int y)
{ m_pCanvas->Pen->Mode = pmXor; m_pCanvas->RoundRect(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y,4,4); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas->RoundRect(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y,4,4); m_pCanvas->Pen->Mode = pmCopy; } XX-03-07 CEllipse 类别定义及实作 画圆形的类别 画圆形的处理和画矩形也大致相同,因为在Windows中是以包围矩形来定义一个圆形,因此和CRoundRect相同的,我们只要改写成画圆函式即可。其馀我就不多说了。 class CEllipse : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CEllipse(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CEllipse() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; XX-03-08小结 以上就是此绘图程式中所使用的各个物件的定义,此乃血统纯正的C++ 写法的程式,不像C++Builder官方的范例是由Delphi的范例修改而来,布满了Object Pascal的味道。 若你对C++ 尚不太熟悉的话,请你一定要细细领略以上的精神。因为它是C++ 式的物件导向程式最基本且精要的精神所在,当你了解了以上的精神,你就可谓把握了C++ 的封装、继续、及动态连结这叁把权仗的基本精神。 至於C++ 老手,以上的定义都是很自然就可以接受的。也许有人会质疑以上的物件定义并未考虑到物件的永续性 (Object Persistence)。没错,不过这并不是我疏忽了,而是在本章的程式中图形的存取是以Timage来存取,因此所有向量式的物件都已转化成点阵图了,自然不需考虑到物件的储存问题。 在後续章节,我还会再针对物件的永续性来做一讨论。现在我们先就TImage的点阵图存取方式为平台讨论之。 最後,在完成了物件的定义之後,我们再将程式根据物件导向的方式再加以改写之。因为我目前尚未加入选择物件的方法,所以我只能用预设物件型态的方式来展示程式的结果。 // 表格建构函式,设定m_bDraw旗标初值 __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { m_bDraw = FALSE; } // Form的OnCreate事件处理函式。Form建立时引发。 // 1.设定笔的颜色及宽度。 // 2.产生一个CLine绘图物件。 // 注:你可以自行修改CLine成CPolyline、CPolygon、CRect等值。 void __fastcall TForm1::FormCreate(TObject *Sender) { Canvas->Pen->Color = clRed; Canvas->Pen->Width = 2; m_pObj = new CLine(Canvas); } // Form的OnClose事件处理函式。Form关闭时引发。 // 1.杀掉绘图物件。 void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) { delete m_pObj; } // 更改後的OnMouseDown物件处理函式。 // 1.将m_bDraw旗标设为 TRUE。 // 2.呼叫绘图物件的OnMouseDown函式。 void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { m_bDraw = TRUE; m_pObj->OnMouseDown(X,Y); } // 更改後的OnMouseMove物件处理函式。 // 1.判定m_bDraw旗标是否为 TRUE。 // 2.若是则呼叫绘图物件的OnMouseMove函式。 void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { if (m_bDraw) m_pObj->OnMouseMove(X,Y); } // 更改後的OnMouseUp物件处理函式。 // 1.将m_bDraw旗标设为 FALSE。 // 2.若是则呼叫绘图物件的OnMouseUp函式。 void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y) { m_bDraw = FALSE; m_pObj->OnMouseUp(X,Y); } 瞧!这就是更改後的程式,是不是变得格外简洁呢?除此之外,它最大的优点在於,无论我们日後加入了多少绘图物件,你都不需再修改以上程式中关於绘图物件的处理部份,只要再自行增加一个物件宣告即可。和原先Borland公司产品内附由Object Pascal修改而来的范例,它的C++ 血统纯正多了。而且若是日後你想要将其修改成为一个物件式的绘图系统,也只需要很简单的修改而已。 好吧!让我们先检阅现在的成果。 图XX-04 CLine绘图物件范例。 图XX-05 CPolyline绘图物件范例(将程式改成new CPolyline) XX-04 工具列(ToolBar)的使用 到目前为止我们已经将所有绘图物件定义完成,因此理论上你的程式应该可以画出各种不同的绘图物件了。但是前面我提到,目前我们尚未将绘图物件的选择功能实作出来,因此我们是以直接修改程式的方式来绘制不同的图形。这是为了说明方便的权宜之计。 在一般的绘图程式中都是以工具列的方式来实作出绘图功能的切换功能,如Windows 95内的小画家即是一典型例子。因此接下来我就为你说明在C++ Builder中实作出工具列的方法。 图XX-06小画家使用的工具列 在C++ Builder中实作工具列的方式和其他的程式如Visual C++,Borland C++ 不同。後两者都是直接使用Windows 95内建的工具列型别来达到此功能。然而在C++ Builder中因为有一种更为简单且直接的方式来做到,因此就不采用上述作法 (当然C++ Builder也可以用Windows 95内建的ToolBar型别,只是用法较为复杂。)。 那麽在C++ Builder中是如何来实作出工具列呢?说穿了其实很简单:那就是利用TPanel和TSpeedButton。 CPanel是一个多功能的容器元件,因此我们可以用它来做为工具列的平台,使用CPanel是因为它是少数几个可做为容器元件的元件,所以它会自动调整置於其上的软体元件的位置,因此很适合做为放置工具列的平台。 注:在C++ Builder的程式模式中大量使用TPanel来做为容器元件。它除了可以做为ToolBar的平台外,另外如状态列 (StatusBar)也可以用它来完成,而且它也可以用来做为画面分割的工具,来达成在MFC中类似分割视窗(Splitter Window)效果。 TSpeedButton快速按钮元件在功能上本来就和工具列有几分类似,现在我们可以将相同属性的快速按钮元件整合在一个TPanel中即可完成我们所要的工具列了。 最後我再将工具列的作法按部就班详述之: (1) 在表格上加入TPanel元件。
◎ 设定事件处理函式。 SpeedButton的命名原则和一般变数的命名原则相同,简单明了就好。以本程式为例,我们就可以LineButton、RectangleButton等名字命名之。命名时只要改变SpeedButton的Name性质即可。 至於设定图形,只要先选取该SpeedButton,然後至物件检视器点取Glyph性质,然後将欲选取的点阵图Load进来,即可完成设定图形的程序了。 图XX-08 Glyph图形之选取。 设定状态初值:由於我们希望本程式执行的初始值是使用CLine元件,因此我将LineButton的Down属性为True,其馀则为 False。 图XX-09选取後的状态 设定群组特性:群组特性是SpeedButton用以实作出ToolBar的重要功能之一。我们可以将一群SpeedButton设为同一群组,如此一来在此一群组的SpeedButton就具备了互斥特性,也就是说在任何一个SpeedButton按下时,会导致其他的SpeedButton浮起。此为实作ToolBar的必备条件,而利用SpeedButton可以轻易达成此目的。 设定群组特性其实很简单,只要把该群组的SpeedButton的GroupIndex性质设为相同数字即可,在此例中我们希望将所有绘图工具列按钮设为同一群组,因此我把该相关工具的GroupIndex性质都设为1。 另外,相信你已经发现在上面的工具列中,除了前面所谈到的绘图工具之外,我还多加了两个额外的SpeedButton ,它们是用来设定笔及笔刷的工具。在此例中,我们是以它来叫出另外两个设定笔及笔刷的工具列,因此它必须具备所谓Toggle On/Off的开关功能。也就是类似CheckBox的功能。 SpeedButton也可轻易达到以上的要求,只要设定该SpeedButton的AllowAllUp属性为True即可以做到此功能。 最後我们必须将设定笔及笔刷的两个SpeedButton,PenButton及BrushButton的GroupIndex分别设为 2、3,如此才不会和绘图工具的SpeedButton的群组特性相干扰。 XX-05工具列的事件处理函式 完成的工具列的设定之後,接着我们要设定工具列的处理函式,由於此工具列是用来切换绘图工具的,因此我们只要处理SpeedButton的OnClick事件,再分别根据不同的事件做处理即可。 在此例中,OnClick的事件处理函式其很简单,只要删除原来使用的绘图工具物件,再重新启始一个新的绘图工具即可。 //--------------------------------------------------------------------- void __fastcall TGraphEx::LineButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CLine(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::PolylineButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CPolyline(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::PolygonButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CPolygon(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::RectangleButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CRectangle(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::EllipseButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CEllipse(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::RoundRectButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CRoundRect(Canvas); } //---------------------------------------------------------------------
在完成了以上的设定之後,此程式就具备了利用绘图工具列来切换绘图工具的功能。 图XX-10具备绘图工具列的绘图程式范例。
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { Canvas->LineTo(X,Y); } 在写完了以上两个事件处理函式之後,我们就可以在Form上面作画了,你可以用滑鼠在Form上面拖戈出一条条直线。其执行结果大致如图XX-01: 图XX-01 XX-02-02 OnMouseMove事件的处理 在加上了OnMouseDown及OnMouseUp处理函式之後,我们只能画出一条条直线,若是我们想要以滑鼠画出不规则线段时,就必须再处理OnMouseMove事件,利用OnMouseMove事件,我们可以追纵到滑鼠移动的位置,简单的OnMouseMove事件处理函式如下: void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { Canvas->LineTo(X,Y); } 此程式的意义即在於将滑鼠所经过的每个点,以线条连接起来,在加上OnMouseMove 事件处理函式之後,它的执行结果会变成图XX-02: 图XX-02 XX-02-03 滑鼠的处理的加强 前面的程式对於滑鼠的移动处理有部份考虑的不够周详,因为它在滑鼠移动时不分青红皂白就将线画在萤慕上,造成萤幕上的线条混乱,这并不是正规的处理方法,正确的处理方法应该如下: (1) 滑鼠键按下时,将记录滑鼠按下的旗标设为True.同时将该点记录下来,谓之原点。
- 滑鼠移动时,判定滑鼠按下的旗标是否设为 True,若为 True,则移动至原点,并画一条由原点至目前所在点的线。同时更新原点位置至目前所在之点。
- 滑鼠放开时,将记录滑鼠按下的旗标设为False。
{ PRotected: TCanvas* m_pCanvas; TColor m_Color; int m_nWidth; public: CShape(TCanvas* pCanvas) {m_pCanvas = pCanvas;} virtual ~CShape() {} virtual void OnMouseMove(int,int)=0; virtual void OnMouseDown(int,int)=0; virtual void OnMouseUp(int,int)=0; }; 我们首先定义一个CShape类别,它是所有绘图物件之始,也因此它定义了一个绘图物件的基本行为。在此绘图程式中我希望它可以处理叁个不同的滑鼠事件并加以处理之,所以我在CShape中定义了叁个相对应的成员函式,而且它们都是纯虚拟函式,表示所有继续自CShape的类别都必须改写此叁个成员函式。 (关於物件导向的关念请参阅 <必要的C++ 基础章节> 或是相关书籍,在此尽作简短的解释)。这叁个函式名称称如下: virtual void OnMouseMove(int,int)=0; virtual void OnMouseDown(int,int)=0; virtual void OnMouseUp(int,int)=0; 另外我们再定义一般性的绘图物件都会用到的基本特性,如颜色及线条宽度,再加上绘图时所需要的 Canvas,如此就组成了CShape的类别定义: TCanvas* m_pCanvas; // 绘图所需的Canvas TColor m_Color; // 颜色 int m_nWidth; // 宽度 至於CShape的解构函式为何也设成virtual呢?这关系到继续物件的毁灭方法。若是基础类别的解构函式没有定义成虚拟函式时,会造成特定情况下,子类别的解构函式没有被呼叫到的情形: 如: CLine *pLine = new Line; CShape* pShape=pLIne; delete pShape; 上述的例子因为CLine为CShape的子类别,因此可以直接将pShape指标指向pLine,然而在後面delete pShape时,若是pShape的解构函式不为虚拟函式,会造成pLine的解构函式不被呼叫到。这是一般C++ 程式设计时很轻易犯的错误。 我们可以将以上的经验法则归纳成一个原则,即是:只要该类别有可能被继续,就必须将其解构函式设为虚拟函式。如此就有了以下的定义了: CShape(TCanvas* pCanvas) {m_pCanvas = pCanvas;} virtual ~CShape() {} CShape的建构函式必须传入Canvas以便绘图,而解构函式则不做任何事,只将其定义为虚拟函式。 XX-03-02 CLine类别定义及实作 画直线的类别 class CLine : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CLine(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CLine() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; 我们将CLine定义为一个画直线的类别,而我们希望在画直线时可以在拖弋滑鼠时将原先的线条擦去,并画出新的线,因此我们必须宣告两个点来记载滑鼠按下的点及上次的点以便擦去原来的线条。 以下就是CLine对於叁个滑鼠事件的处理函式: // 滑鼠按下的事件处理函式 // 1. 设定原点及上个启始点为目前所在点。 // 2. 移动至目前所在点。 void CLine::OnMouseDown(int x,int y) { m_ptOrigin.x = m_ptMove.x = x; m_ptOrigin.y = m_ptMove.y = y; m_pCanvas->MoveTo(x,y); } // 滑鼠移动事件处理函式 // 1.将画笔模式设为XOR模式,以便擦去上一条线。 // 2.擦去原来的线(以XOR模式再画一次就会擦去了) // 3.在目前的位置画出一条新线。 // 4.更新坐标并改变画笔模式。 void CLine::OnMouseMove(int x,int y) { m_pCanvas->Pen->Mode = pmXor; m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(m_ptMove.x,m_ptMove.y); m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(x,y); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas->Pen->Mode = pmCopy; } // 滑鼠放开事件处理函式 // 1.画出原点至目前点的直线。 void CLine::OnMouseUp(int x,int y) {
m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(x,y); } 这就是画直线类别的定义及实作内容。 XX-03-03 CPolyline类别定义及实作 画随意线的类别 class CPolyline : public CShape { public: POINT m_ptOrigin; public: CPolyline(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CPolyline() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; CPolyline类别其实和我们前面所写的涂鸦程式的行为模式极为类似,所以我就简单带过好了。 void CPolyline::OnMouseDown(int x,int y) { m_ptOrigin.x = x; m_ptOrigin.y = y; m_pCanvas->MoveTo(x,y); } void CPolyline::OnMouseMove(int x,int y) { m_pCanvas->LineTo(x,y); } void CPolyline::OnMouseUp(int x,int y) { m_pCanvas->LineTo(x,y); } XX-03-04 CPolygon类别定义及实作 画多边形的类别 class CPolygon : public CPolyline { public: CPolygon(TCanvas* pCanvas):CPolyline(pCanvas){} virtual ~CPolygon() {} virtual void OnMouseUp(int,int); }; CPolygon是CPolyline的子类别,其差别仅在於它会将首尾两点连接,使其成为一个多边形,因此我们就直接由CPolyline继续而来,只改写其OnMouseUp成员函式即可。 void CPolygon::OnMouseUp(int x,int y) { m_pCanvas->MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas->LineTo(x,y); } XX-03-05 CRectangle类别定义及实作 画矩形的类别 class CRectangle : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CRectangle(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CRectangle() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; 画矩形类别其实和画线类别有些类似,它们同样必须记载上次滑鼠移动的点,并擦掉原来的图形画出新的图形,所以我只针对其相异的部份加以说明之: // 滑鼠移动事件处理函式 // 原理和CLine类似,只不过改成画矩形。 void CRectangle::OnMouseMove(int x,int y) { m_pCanvas->Pen->Mode = pmXor; m_pCanvas->Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas->Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y); m_pCanvas->Pen->Mode = pmCopy; } XX-03-06 CRoundRect类别定义及实作 画圆矩形的类别 class CRoundRect : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CRoundRect(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CRoundRect() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; CRoundRect的实作几乎和Crectangle相同,只不过它们呼叫不同的API罢了,CRoundRect是以Canvas->RoundRect来画出图形的。 void CRoundRect::OnMouseMove(int x,int y)
{ m_pCanvas->Pen->Mode = pmXor; m_pCanvas->RoundRect(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y,4,4); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas->RoundRect(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y,4,4); m_pCanvas->Pen->Mode = pmCopy; } XX-03-07 CEllipse 类别定义及实作 画圆形的类别 画圆形的处理和画矩形也大致相同,因为在Windows中是以包围矩形来定义一个圆形,因此和CRoundRect相同的,我们只要改写成画圆函式即可。其馀我就不多说了。 class CEllipse : public CShape { public: POINT m_ptMove; POINT m_ptOrigin; public: CEllipse(TCanvas* pCanvas):CShape(pCanvas) {} virtual ~CEllipse() {} virtual void OnMouseMove(int,int); virtual void OnMouseDown(int,int); virtual void OnMouseUp(int,int); }; XX-03-08小结 以上就是此绘图程式中所使用的各个物件的定义,此乃血统纯正的C++ 写法的程式,不像C++Builder官方的范例是由Delphi的范例修改而来,布满了Object Pascal的味道。 若你对C++ 尚不太熟悉的话,请你一定要细细领略以上的精神。因为它是C++ 式的物件导向程式最基本且精要的精神所在,当你了解了以上的精神,你就可谓把握了C++ 的封装、继续、及动态连结这叁把权仗的基本精神。 至於C++ 老手,以上的定义都是很自然就可以接受的。也许有人会质疑以上的物件定义并未考虑到物件的永续性 (Object Persistence)。没错,不过这并不是我疏忽了,而是在本章的程式中图形的存取是以Timage来存取,因此所有向量式的物件都已转化成点阵图了,自然不需考虑到物件的储存问题。 在後续章节,我还会再针对物件的永续性来做一讨论。现在我们先就TImage的点阵图存取方式为平台讨论之。 最後,在完成了物件的定义之後,我们再将程式根据物件导向的方式再加以改写之。因为我目前尚未加入选择物件的方法,所以我只能用预设物件型态的方式来展示程式的结果。 // 表格建构函式,设定m_bDraw旗标初值 __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { m_bDraw = FALSE; } // Form的OnCreate事件处理函式。Form建立时引发。 // 1.设定笔的颜色及宽度。 // 2.产生一个CLine绘图物件。 // 注:你可以自行修改CLine成CPolyline、CPolygon、CRect等值。 void __fastcall TForm1::FormCreate(TObject *Sender) { Canvas->Pen->Color = clRed; Canvas->Pen->Width = 2; m_pObj = new CLine(Canvas); } // Form的OnClose事件处理函式。Form关闭时引发。 // 1.杀掉绘图物件。 void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) { delete m_pObj; } // 更改後的OnMouseDown物件处理函式。 // 1.将m_bDraw旗标设为 TRUE。 // 2.呼叫绘图物件的OnMouseDown函式。 void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { m_bDraw = TRUE; m_pObj->OnMouseDown(X,Y); } // 更改後的OnMouseMove物件处理函式。 // 1.判定m_bDraw旗标是否为 TRUE。 // 2.若是则呼叫绘图物件的OnMouseMove函式。 void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y) { if (m_bDraw) m_pObj->OnMouseMove(X,Y); } // 更改後的OnMouseUp物件处理函式。 // 1.将m_bDraw旗标设为 FALSE。 // 2.若是则呼叫绘图物件的OnMouseUp函式。 void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y) { m_bDraw = FALSE; m_pObj->OnMouseUp(X,Y); } 瞧!这就是更改後的程式,是不是变得格外简洁呢?除此之外,它最大的优点在於,无论我们日後加入了多少绘图物件,你都不需再修改以上程式中关於绘图物件的处理部份,只要再自行增加一个物件宣告即可。和原先Borland公司产品内附由Object Pascal修改而来的范例,它的C++ 血统纯正多了。而且若是日後你想要将其修改成为一个物件式的绘图系统,也只需要很简单的修改而已。 好吧!让我们先检阅现在的成果。 图XX-04 CLine绘图物件范例。 图XX-05 CPolyline绘图物件范例(将程式改成new CPolyline) XX-04 工具列(ToolBar)的使用 到目前为止我们已经将所有绘图物件定义完成,因此理论上你的程式应该可以画出各种不同的绘图物件了。但是前面我提到,目前我们尚未将绘图物件的选择功能实作出来,因此我们是以直接修改程式的方式来绘制不同的图形。这是为了说明方便的权宜之计。 在一般的绘图程式中都是以工具列的方式来实作出绘图功能的切换功能,如Windows 95内的小画家即是一典型例子。因此接下来我就为你说明在C++ Builder中实作出工具列的方法。 图XX-06小画家使用的工具列 在C++ Builder中实作工具列的方式和其他的程式如Visual C++,Borland C++ 不同。後两者都是直接使用Windows 95内建的工具列型别来达到此功能。然而在C++ Builder中因为有一种更为简单且直接的方式来做到,因此就不采用上述作法 (当然C++ Builder也可以用Windows 95内建的ToolBar型别,只是用法较为复杂。)。 那麽在C++ Builder中是如何来实作出工具列呢?说穿了其实很简单:那就是利用TPanel和TSpeedButton。 CPanel是一个多功能的容器元件,因此我们可以用它来做为工具列的平台,使用CPanel是因为它是少数几个可做为容器元件的元件,所以它会自动调整置於其上的软体元件的位置,因此很适合做为放置工具列的平台。 注:在C++ Builder的程式模式中大量使用TPanel来做为容器元件。它除了可以做为ToolBar的平台外,另外如状态列 (StatusBar)也可以用它来完成,而且它也可以用来做为画面分割的工具,来达成在MFC中类似分割视窗(Splitter Window)效果。 TSpeedButton快速按钮元件在功能上本来就和工具列有几分类似,现在我们可以将相同属性的快速按钮元件整合在一个TPanel中即可完成我们所要的工具列了。 最後我再将工具列的作法按部就班详述之: (1) 在表格上加入TPanel元件。
- 设定TPanel的Align性质为 alTop。因为我们希望工具列置於表格上方,所以将它设定为浮贴於表格的上方。如此一来当表格大小改变时,工具列的宽度为跟着改变,而高度则维持原先的高度。
- 将TSpeedButton加入TPanel原件上。
- 当使用者点取该功能时,必须执行该功能。
- 一般按钮的功能。
- 可以除能/致能。
- 具备群组特性。(也就是说同一群组的TSpeedButton会互相影响,因此可轻易做出互斥的功能,以绘图程式为例,一次只能使用一种工具,因此当使用者选取工具时,除了被选取的工具之外,其他的工具应该都呈浮起状态)
- 为SpeedButton命名。取一个有意义的名字。
- 依需要设定其高度及位置。
- 设定图形。
- 设定SpeedButton状态初值。
- 设定群组特性。
◎ 设定事件处理函式。 SpeedButton的命名原则和一般变数的命名原则相同,简单明了就好。以本程式为例,我们就可以LineButton、RectangleButton等名字命名之。命名时只要改变SpeedButton的Name性质即可。 至於设定图形,只要先选取该SpeedButton,然後至物件检视器点取Glyph性质,然後将欲选取的点阵图Load进来,即可完成设定图形的程序了。 图XX-08 Glyph图形之选取。 设定状态初值:由於我们希望本程式执行的初始值是使用CLine元件,因此我将LineButton的Down属性为True,其馀则为 False。 图XX-09选取後的状态 设定群组特性:群组特性是SpeedButton用以实作出ToolBar的重要功能之一。我们可以将一群SpeedButton设为同一群组,如此一来在此一群组的SpeedButton就具备了互斥特性,也就是说在任何一个SpeedButton按下时,会导致其他的SpeedButton浮起。此为实作ToolBar的必备条件,而利用SpeedButton可以轻易达成此目的。 设定群组特性其实很简单,只要把该群组的SpeedButton的GroupIndex性质设为相同数字即可,在此例中我们希望将所有绘图工具列按钮设为同一群组,因此我把该相关工具的GroupIndex性质都设为1。 另外,相信你已经发现在上面的工具列中,除了前面所谈到的绘图工具之外,我还多加了两个额外的SpeedButton ,它们是用来设定笔及笔刷的工具。在此例中,我们是以它来叫出另外两个设定笔及笔刷的工具列,因此它必须具备所谓Toggle On/Off的开关功能。也就是类似CheckBox的功能。 SpeedButton也可轻易达到以上的要求,只要设定该SpeedButton的AllowAllUp属性为True即可以做到此功能。 最後我们必须将设定笔及笔刷的两个SpeedButton,PenButton及BrushButton的GroupIndex分别设为 2、3,如此才不会和绘图工具的SpeedButton的群组特性相干扰。 XX-05工具列的事件处理函式 完成的工具列的设定之後,接着我们要设定工具列的处理函式,由於此工具列是用来切换绘图工具的,因此我们只要处理SpeedButton的OnClick事件,再分别根据不同的事件做处理即可。 在此例中,OnClick的事件处理函式其很简单,只要删除原来使用的绘图工具物件,再重新启始一个新的绘图工具即可。 //--------------------------------------------------------------------- void __fastcall TGraphEx::LineButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CLine(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::PolylineButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CPolyline(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::PolygonButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CPolygon(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::RectangleButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CRectangle(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::EllipseButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CEllipse(Canvas); } //--------------------------------------------------------------------- void __fastcall TGraphEx::RoundRectButtonClick(TObject *Sender) { delete m_pObj; m_pObj = new CRoundRect(Canvas); } //---------------------------------------------------------------------
在完成了以上的设定之後,此程式就具备了利用绘图工具列来切换绘图工具的功能。 图XX-10具备绘图工具列的绘图程式范例。
[]
赞助商链接