荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: jek (好好学习天天向上), 信区: Program
标  题: 创世纪的 C++builder(3)
发信站: BBS 荔园晨风站 (Sun Mar 12 02:36:44 2000), 转信

第XX章 具体而微的绘图程式



在本章中我将为你示范如何在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_NCRBUT?

鼠的按键,或是需要利用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 滑鼠的处理的加强



前面的程式对於滑鼠的移动处理有部份考虑的不够周详,因为

它在滑鼠移动时不分青红皂白就将线画在萤慕上,造成萤幕上

的线条混乱,这并不是正规的处理方猎悖⒒惶跤稍阒聊壳八诘愕南摺M?

时更新原点位置至目前所在之点。

    滑鼠放开时,将记录滑鼠按下的旗标设为False。



以下就是关於三个滑鼠事件的处理程式码。



// 滑鼠按下的事件处理函式

// 1. 将旗标设为True

// 2. 记录原点位置

void __fastcall TForm1::FormMouseDown(TObject *Sender,

TMouseButton Button,

TShiftState Shift, int X, int Y)

{

m_bDraw = TRUE;

m_nOrgX=X;

m_nOrgY=Y;

}



// 滑鼠移动的事件处理函式

// 1. 判断旗标是否为True。若是则进行以下动作。

// 2. 移动至原点。

// 3. 画一条由原点至目前所在点的线条。

// 4. 更改原点位置。



void __fastcall TForm1::FormMouseMove(TObject *Sender,

TShiftState Shift,

int X, int Y)

{

if (m_bDraw)

{

Canvas->MoveTo(m_nOrgX,m_nOrgY);

Canvas->LineTo(X,Y);

m_nOrgX = X;

m_nOrgY = Y;

}

}



// 滑鼠放开的事件处理函式

// 1. 判断旗标是否为True。若是则进行以下动作。

// 1. 将旗标设为 False。

// 2. 画线并记录原点位置(非必要)。

void __fastcall TForm1::FormMouseUp(TObject *Sender,

TMouseButton Button,

TShiftState Shift, int X, int Y)

{

if (m_bDraw)

{

m_bDraw=FALSE;

Canvas->MoveTo(m_nOrgX,m_nOrgY);

Canvas->LineTo(X,Y);

m_nOrgX = X;

m_nOrgY = Y;

}

}



将滑鼠事件处理函式做以上的修改之後,我们就完成了一个基

本的涂鸦程式的雏形了。喂?







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的类别都必须改写此三个成员

函式。

(关於物件导向的关念请叁阅 <必要的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 = pmXor;

m_pCanvas-

>Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMo

ve.y);

m_ptMove.x = x;

m_ptMove.y = y;

m_pCanvas-

>Rectangle(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMo

ve.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_ptMo

ve.y,4,4);

m_ptMove.x = x;

m_ptMove.y = y;

m_pCanvas-

>RoundRect(m_ptOrigin.x,m_ptOrigin.y,m_ptMove.x,m_ptMo

ve.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关闭时引发。 标设为 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元件。

    晌颐窍Mぞ吡兄渺侗砀裆戏剑越?

定为浮贴於表格的上方。如此一来当表格大小改变时,工具列

的宽度为跟着改变,而高度则维持原先的高度。

    将TSpeedButton加入TPanel原件上。



你可以在表格中加入多个上述的工具列,它们会依序自动排列

於表格的上方,因此你不需费心去处理这些额外的动作。



XX-04-01 TSpeedButton 元件解析及设定



ToolBar的几个基本要素是:

(1) 代表该功能的图形。

(2) 可依状况切换其状态。

    当使用者点取该功能时,必须执行该功能。



我们来看看TSpeedButton如何达到以上的要求。



首先,TSpeedButton具备Glyph性质,可以指定其图形,所以

第一个要求不成问题,再来TSpeedButton具备以下三种状态:



    一般按钮的功能。

    可以除能/致能。

    具备群组特性。(也就是说同一群组的TSpeedButton会互

相影响,因此可轻易做出互斥的功能,以绘图程式为例,一次

只能使用一种工具,因此当使用者选取工具时,除了被选取的

工具之外,其他的工具应该都呈浮起状态)



由上可知,TSpeedButton确实是实作ToolBar的适当人选。



XX-04-02将SpeedButton加入ToolBar



依照我们先前的需求,我们需要一个可以切换绘图工具的工具

列,因此我们就照前面所说的方式来完成它。





图XX-07工?

    为SpeedButton命名。取一个有意义的名字。

    依需要设定其高度及位置。

    设定图形。

    设定SpeedButton状态初值。

    设定群组特性。

◎ 设定事件处理函式。



SpeedButton的命名原则和一般变数的命名原则相同,简单明

了就好。以本程式为例::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具备绘图工具列的绘图程式范例。



--
※ 来源:·BBS 荔园晨风站 bbs.szu.edu.cn·[FROM: 192.168.1.118]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店