void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { Canvas-%26gt;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-%26gt;LineTo(X,Y);
}
此程式的意义即在於将滑鼠所经过的每个点,以线条连接起来,在加上OnMouseMove 事件处理函式之後,它的执行结果会变成图XX-02
图XX-02
XX-02-03 滑鼠的处理的加强
前面的程式对於滑鼠的移动处理有部份考虑的不够周详,因为它在滑鼠移动时不分青红皂白就将线画在萤慕上,造成萤幕上的线条混乱,这并不是正规的处理方法,正确的处理方法应该如下
(1) 滑鼠键按下时,将记录滑鼠按下的旗标设为True.同时将该点记录下来,谓之原点。
-
滑鼠移动时,判定滑鼠按下的旗标是否设为 True,若为 True,则移动至原点,并画一条由原点至目前所在点的线。同时更新原点位置至目前所在之点。
- 滑鼠放开时,将记录滑鼠按下的旗标设为False。
XX-03 绘图物件的定义
至目前为止,我们已经完成了一个简单的涂鸦程式,接下来,我希望将程式扩充为一个一般的绘图程式,它必须具备基本的画线、画圆、画方等功能。为了要实作出这些功能,我们必须先定义我们的绘图物件。
XX-03-01 绘图物件之始CShape
class CShape
{ 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的类别都必须改写此叁个成员函式。 (关於物件导向的关念请参阅 %26lt;必要的C++ 基础章节%26gt; 或是相关书籍,在此尽作简短的解释)。这叁个函式名称称如下 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-%26gt;MoveTo(x,y); } // 滑鼠移动事件处理函式 // 1.将画笔模式设为XOR模式,以便擦去上一条线。 // 2.擦去原来的线(以XOR模式再画一次就会擦去了) // 3.在目前的位置画出一条新线。 // 4.更新坐标并改变画笔模式。 void CLine::OnMouseMove(int x,int y) { m_pCanvas-%26gt;Pen-%26gt;Mode = pmXor; m_pCanvas-%26gt;MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas-%26gt;LineTo(m_ptMove.x,m_ptMove.y); m_pCanvas-%26gt;MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas-%26gt;LineTo(x,y); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas-%26gt;Pen-%26gt;Mode = pmCopy; } // 滑鼠放开事件处理函式 // 1.画出原点至目前点的直线。 void CLine::OnMouseUp(int x,int y) {
m_pCanvas-%26gt;MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas-%26gt;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-%26gt;MoveTo(x,y); } void CPolyline::OnMouseMove(int x,int y) { m_pCanvas-%26gt;LineTo(x,y); } void CPolyline::OnMouseUp(int x,int y) { m_pCanvas-%26gt;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-%26gt;MoveTo(m_ptOrigin.x,m_ptOrigin.y); m_pCanvas-%26gt;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-%26gt;Pen-%26gt;Mode = pmXor; m_pCanvas-%26gt;Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y); m_ptMove.x = x; m_ptMove.y = y; m_pCanvas-%26gt;Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y); m_pCanvas-%26gt;Pen-%26gt;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-%26gt;RoundRect来画出图形的。 void CRoundRect::OnMouseMove(int x,int y)
{ m_pCanvas-%26gt;Pen-%26gt;Mode = pmXor; m_pCanvas-%26gt;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-%26gt;RoundRect(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMove.y,4,4); m_pCanvas-%26gt;Pen-%26gt;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-%26gt;Pen-%26gt;Color = clRed; Canvas-%26gt;Pen-%26gt;Width = 2; m_pObj = new CLine(Canvas); } // Form的OnClose事件处理函式。Form关闭时引发。 // 1.杀掉绘图物件。 void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction %26amp;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-%26gt;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-%26gt;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-%26gt;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原件上。
-
当使用者点取该功能时,必须执行该功能。
◎ 设定事件处理函式。 SpeedButton的命名原则和一般变数的命名原则相同,简单明了就好。以本程式为例,我们就可以LineButton、RectangleButton等名字命名之。命名时只要改变SpeedButton的Name性质即可。 至於设定图形,只要先选取该SpeedButton,然後至物件检视器点取Glyph性质,然後将欲选取的点阵图Load进来,即可完成设定图形的程序了。
图XX-08 Glyph图形之选取。
设定状态初值由於我们希望本程式执行的初始值是使用CLine元件,因此我将LineButton的Down属性为True,其馀则为 False。
在完成了以上的设定之後,此程式就具备了利用绘图工具列来切换绘图工具的功能。
图XX-10具备绘图工具列的绘图程式范例。