忘掉代码 之 TGraphic ( 积分: 0 )

  • 主题发起人 小雨哥
  • 开始时间

小雨哥

Unregistered / Unconfirmed
GUEST, unregistred user!
这是一个抽象类。顾名思义,抽象类是用来抽象一般事物,它本身应该是泛意的,而基于它
产生的真正的对象,才是 Graphic 的。Delphi 原代码中直接定义为抽象类的类并不多,比
如 : TStream 也是一个抽象类,仔细通读这些类的定义方法,可以给自己带来很多收获。
现在,让我们一起走进 TGraphic ,我首先请大家要做的事情是忘掉代码,只需要用欣赏的
心情走入图形的世界。
怎样来定义一个抽象类?哇塞,这样的问题不要来问我,那是高智商问题,容易导致脑部抽
筋。TGraphic 就是一个抽象类,它是怎么定义的?任何事物的抽象,都是有其真实的参照
物来的,TGraphic 的参照物就是世界上的所有图形图像。那么图形图象这么多,那些是它
们共有的可以被抽象的?很容易回答这个问题,无非就是宽度、长度,除了这二个东西外,
我们都不能真正地抽象图形,这一点可以从 TGraphic 类中的 Equals 方法中领悟到。
Equals 方法是这样判断二个图像是否相等的:首先,它检查这二个图像是否具有相同的类
形,假如类型相同,那么就仔细比较他们的每一个部分,直到得到的比对结果完全一样,这
个函数才会回答这二个图像是否真的相等。
看看,也就是说,实际上并没有一个更简单的办法可以抽象图形,如果有的话,这个函数大
概就不会这么写了,当然,写这个函数的程序员并没有自大,他觉得在他目前的AI状态下,
这个函数大概也就到此为止了,但他并不排除 High AI 的人士存在,所以他在这个函数的
尾部定义了一个关键词:virtual ,目的无非是让高智商的人士有用武之地。
[请待下文,本篇没分,因为我也快穷得叮当响了]
 
感谢这位定义 Equals 为虚方法的程序员给高智商人士带来人文关怀的同时,我们或许会
思考,一个图形,难道真的只有长和宽吗?不是还有 RGB 颜色和透明与否等等内容吗?
还有,同一个图形,从不同角度不是还可以看到不同的变形吗?
是的,我完全同意这样的思考,只是这些思考中的部分内容,实际上在 TGraphic 中有更好
的思考。
前面我们讲到,TGraphic 只关心长和宽,也就是说,它只关心图形的维度。那么,也就意
味着这个类中的大多数代码将与维度有关。然而,即便是这么简单的维度问题,在具体的类
型未知的情况下,我们也无法深入,怎么办呢?最好的办法是交给具体类型的实现者去向我
们提供长和宽,甚至,我们在 TGraphic 看到的 GetHeight 和 GetWidth 直接就是纯虚方
法。纯虚方法是一个用来定义规则的占位符,实现者必须依照这个规则向 TGraphic 提供具
体的实现。
既然我们知道了图形的维度,那么图形的最终目的是用来显示给人看的,我们并没有在这个
类里看到每个图形都有的 RGB 要素,那么如何显示呢?这个问题很简单,TGraphic 也不知
道每个具体的图形应该如何显示,怎么办呢?老方法:留给实现者去做。于是就有了一个
Draw 的纯虚方法,这个方法的意思是:请按你的图形情况在合适的位置画出图形。
哇塞,什么都不用做啊,全推给了后面实现者去完成,这样的活我喜欢,最好还能工资照拿
就更妙了。说笑归说笑,事实上抽象类就是只定规则不干活的。这犹如真实世界里的企业管
理,好的规则不仅清晰容易明白,而且可以包容并且最大限度地发挥人才优势。
到这里,一个 TGraphic 的抽象实际上已经完成,但是,我们是写程序的,程序的要素是数
据和行为,我们还需要真正的图像数据,这些数据怎么来?呵呵,没错,TGraphic 又定义
了二个不干活的规则: LoadFromStream 和 SaveToStream 。说到这里,这个类是怎么来的
大概已经说得很清楚了,接下来我们来看看这个程序员百密一疏的地方。
[请待下文]
 
其实,在定义规则的时候,并不仅是定义一个方法,更多的时候我们会为这个方法应该带上
怎么样的参数费心思。GetHeight 和 GetWidth 这样的方法很明确,他们将返回一个具体的
维度数据,不需要带任何参数,就象是发布命令一样就可以了。而 Draw 方法就不怎么好处
理了,通常的观点是:你提供一个画布,并规定画到画布的什么位置,我来完成绘画。于是
Draw 的方法可以定义成:Draw(DC:HDC,R:TRect)或者Draw(X,Y,DC:HDC)或者Draw(C:TCanvas;R:TRect)
等等。这时候我们看到,要么是一个Delphi专用的TCanvas,要么是一个Windows通用的HDC。
TCanvas 是 Delphi 用来包装 Windows 系统中的 DC 的一个对象,所以,这二个东西本质
上是一个东西。考虑到 Delphi 中更通用的是 TCanvas ,所以 TGraphic 中就选择了 TCanvas。
但是,我们既然在说抽象,一个 Draw 方法就把我们的抽象带回到了具体的操作系统依赖
上,这似乎是出人意料的简单。为什么我们不能更抽象一些呢?为什么我们的 Draw 方法
不是这个样子呢: Draw( Dest:TGraphic;
DR:TRect ) ?
 
写得太好了,这才叫深入浅出.没有真正的理解是写不出来的...
 
不知道你想说啥哦?
 
说明楼上功力还不够。哈哈。支持小雨哥。TGraphic是VCL里面一个非常有意思的类,之所以各种Image图片格式都可以让VCL支持,就在TGraphic的设计上的灵活哦!
 
哈哈,小雨哥,顶一下!!
 
我看不明白上面写什么? TGraphic 是一个待扩充的框架,没有装修的毛胚房,没那么神吧
 
深入浅出
 
突然想起某人说过的一句话“抽象程度越高的使用越复杂,越低的越易使用”
 
Draw( Dest:TGraphic;
DR:TRect ) ?
我另外想到一个办法,首先转换一下观念,这里的TGraphic是一个主动者,所以它要表现自己(Draw)就需要一个画布一样的东西,假如能表现这个TGraphic不是个画布,而是其他的呢,所以这时候我们可以选择是TGraphic成为一个被调用者,即主动者变调用者,定义一个接口 IDrawInterFace ,它接受的参数为TGraphic,则Draw可以这样写
Draw(Drawer : IDrawInterFace) ;
begin
Drawer.draw(self) ;
//draw 是 IDrawInterFace 接口必须实现的方法
end;

这样可以将把自己的画的任务交给专门实现了 IDrawInterFace的类了~~
 
搞java的整天就研究这类问题,搞delphi的研究这类问题的相对少得多,建议对类库设计感兴趣的朋友多看看java相关的东东,会有所收获的
 
to 小雨哥:
好厉害的小雨哥,高手啊!
但是,borland的程序员那样定义自然有他的好处,就是这个类不仅仅可以画在自己知道的那个东西上,还可以画在别处。难以两全!我想他当时应该也是矛盾良久的吧。
有时候为了理论上的完美,要做很多枯燥、冗余、费解的工作,绕了很多很多,实现了理论上的完美;另一方面,朴素、使用、简洁!两者结合不仅仅需要智慧,而且需要经验和眼光。
正如“所有飞着的终究要着地”一样,所有的变化和灵活都是基于某些确定不变的东西,所有的抽象都是为了更好的表述现实。
抽象,一种手段罢了!
 
yeskert1 说的极是。从应用角度说, TGraphic 已经定义得很完美了。
ztf86781163 则从另一个角度提出了规则的转换法则,也是相当有见地的。
实际上,如果 TGraphic 没有用 TCanvas 作为一个基本成员的话,VCL 架构就未必好看。
反正这样对应用已经够用了,这应该是设计者一个理性的择中。甚至,由于这样的设计,我
们现在已经可以直接把呈现从 TGraphic 中剥离出来,比如,我们可以直接创建一个 VCL
形式的 DirectX 呈现,几乎花不了几行代码。
 
为了把帖子玩个够,索性我再继续上面已经扯远的关于呈现的话题吧。
下面我将定义一个继承自 TCanvas 的特殊的画布对象,这个对象可以呈现所有继承自 TGraphic
的对象,像 TBitmap 、TTJPEGImage 、TPngObject 等等,都可以通过它来呈现。而这个
TCanvas 类的特殊点是:它不再是一个 Windows GDI 对象,而是一个 DirectX 对象,让
我们一起来见识见识在 Delphi 的 VCL 架构下,封装一个绘图引擎需要几行代码。
首先声明这个类:
type
TDirectCanvas = class(TCanvas)
private
DDS : IDirectDrawSurface7;
protected
procedure Changed;
override;
procedure CreateHandle;
override;
public
constructor Create(pDD: IDirectDraw7;
pddsd: TDDSurfaceDesc2);
destructor Destroy;
override;
property Surface: IDirectDrawSurface7 read DDS;
end;

如果以上代码也算是代码的话,这大概只能算 6 行代码,甚至更少。
再来看看实现:
procedure TDirectCanvas.Changed;
begin
if Handle <> 0 then
DDS.ReleaseDC(Handle);
Handle := 0;
inherited Changed;
end;

constructor TDirectCanvas.Create(pDD: IDirectDraw7;
pddsd: TDDSurfaceDesc2);
begin
if pDD <> nil then
begin
if pDD.CreateSurface(pddsd, DDS, nil) <> DD_OK then
raise EInvalidOperation.Create('CreateSurface Faile');
end
else
raise EInvalidOperation.Create('Invalid IDirectDraw7');
inherited Create;
end;

procedure TDirectCanvas.CreateHandle;
var
DC:HDC;
begin
inherited CreateHandle;
Handle := 0;
if DDS.GetDC(DC) = S_OK then
Handle := DC;
end;

destructor TDirectCanvas.Destroy;
begin
DDS := nil;
inherited Destroy;
end;

全部实现代码大概约 12 行,即便加上刚才用来声明类的代码也算的话,也没有超过 20 行。
至此,一个绘图引擎在 VCL 支撑下就算完成了。呵呵,非常简单啊。
 
使用上面的绘图引擎与使用普通的 VCL TCanvas 没有什么区别,因此,可以使用我们已经
非常熟悉的 Canvas 上全部的绘图方法,下面的演示将把一个 TBitmap 图片绘制到窗体上。
首先把主要的初始化代码贴出来,演示中,我创建了二块 TDirectCanvas 画布,分别称为
PrimaCanvas 和 BackCanvas ,顾名思义就是前台画布和后台画布。所有的绘图工作都在
后台画布上操作,绘制完成后再由前台画布显示出来。这个初始化代码如下:
procedure TForm1.Initialize;
var
DDX : IDirectDraw7;
Clipper : IDirectDrawClipper;
ddsd : TDDSurfaceDesc2;
begin
DirectDrawCreateEx(nil, DDX, IID_IDirectDraw7, nil);
DDX.SetCooperativeLevel(Handle, DDSCL_NORMAL);
DDX.CreateClipper(0, Clipper, nil);
Clipper.SetHWnd(0, Handle);
FillChar(ddsd, sizeof(ddsd),0);
ddsd.dwSize := sizeof(ddsd);
ddsd.dwFlags := DDSD_CAPS;
ddsd.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE;
PrimaCanvas := TDirectCanvas.Create(DDX,ddsd);
PrimaCanvas.Surface.SetClipper(Clipper);
ddsd.dwFlags := DDSD_CAPS or DDSD_WIDTH or DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps := DDSCAPS_SYSTEMMEMORY;
ddsd.dwWidth := Bmp.Width;
ddsd.dwHeight := Bmp.Height;
BackCanvas := TDirectCanvas.Create(DDX,ddsd);
BackCanvas.Draw(0,0,Bmp);
Clipper := nil;
DDX := nil;
end;

在创建后台画布的时候,我把它的宽和高与将要显示的 Bitmap 设计得一样,无非是偷懒而
已,实际可以根据需要进行处理。也因为这个原因,我为了在这个函数调用时可以使用位图,
所以,这个位图要先于这个函数创建好,我把它放在了窗体的创建过程中完成:
procedure TForm1.FormCreate(Sender: TObject);
begin
strBmp := '1280X720.bmp';
Bmp:=TBitmap.Create;
if FileExists(strBmp) then
Bmp.LoadFromFile(strBmp)
else
begin
Bmp.Width :=Width;
Bmp.Height:=Height;
end;
Width := Bmp.Width;
Height:= Bmp.Height;
Initialize;
end;

好了,接下来只要在窗体重绘事件里把后台画布上的内容画到前台画布就可以了:
procedure TForm1.FormPaint(Sender: TObject);
var
pt:TPoint;
R :TRect;
begin
if PrimaCanvas <> nil then
begin
pt := ClientOrigin;
SetRect(R,pt.x,pt.y,pt.x + Width,pt.y + Height);
PrimaCanvas.Surface.Blt(@R, BackCanvas.Surface,nil,DDBLT_WAIT,nil);
end;
end;

因为窗口是可以随意缩放的,为让效果看起来好一点,在窗口尺寸变化的时候,最好做个画
面刷新动作如下:
procedure TForm1.FormResize(Sender: TObject);
begin
Invalidate;
end;

所有的画图程序,如果直接画在 Windows 管理的地方,都要注意小心不要让 Windows 来刷
新背景,所以我截断了背景刷新的消息,禁止 Windows 刷新背景:
// 函数声明:procedure WMEraseBkgnd(var Msg: TWmEraseBkgnd);
message WM_ERASEBKGND;
procedure TForm1.WMEraseBkgnd(var Msg: TWmEraseBkgnd);
begin
Msg.Result:=1;
end;

最后,当程序结束的时候,也要像普通程序那样,把内存清干净:
procedure TForm1.FormDestroy(Sender: TObject);
begin
Bmp.Free;
PrimaCanvas.Free;
BackCanvas.Free;
end;
 
因为上面的代码涉及到了 DirectX 的内容,所以一定不要忘了添加 DirectDraw.pas 单元
的引用。
上面的代码会显示你提供的位图,假如我们要写几个文字,也相当简单:
procedure TForm1.TextOut(x,y:integer;Text: WideString);
begin
BackCanvas.Font.Name:='隶书';
BackCanvas.Font.Size:=46;
BackCanvas.Font.Style:=[fsBold];
BackCanvas.Font.Color:=clRed;
SetBkMode(BackCanvas.Handle,TRANSPARENT);
BackCanvas.TextOut(x,y,Text);
end;

把这个函数嵌到合适的位置即可:
procedure TForm1.FormPaint(Sender: TObject);
var
pt:TPoint;
R :TRect;
begin
if PrimaCanvas <> nil then
begin
pt := ClientOrigin;
SetRect(R,pt.x,pt.y,pt.x + Width,pt.y + Height);
TextOut(0,0,'写点文字看看');
// <--- 我把它嵌到了这里
PrimaCanvas.Surface.Blt(@R, BackCanvas.Surface,nil,DDBLT_WAIT,nil);
end;
end;

呵呵,本来说好了要忘掉代码的,没想到结果又贴了这许多代码,罪过,罪过。
 
学习了:)
小雨哥也是夜猫子啊,呵呵。
 
另外,禁止WINDOWS刷新背景是不是应该 Msg.Result:=0 才对啊 :)
 
很有深度的贴,复制下来慢慢学研究
 
顶部