关于GDI+的几何变换类Matrix的理论和应用有很多书籍和文章介绍,本文只是谈一点自己的应用心得。
使用GDI+的Matrix类,可以很方便的进行提供了Rotate(旋转)、Scale( 缩放)、Shear(切变)等线性变换和Translate(平移),还可以通过这几个基本的变换组成更复杂的复合变换。通过变换后的图形及原点坐标变化很大,如下面的语句:
g.DrawEllipse(Pens.Blue, 0, 0, 100, 50);
g.ScaleTransform(1.0, 0.5);
g.TranslateTransform(50.0, 0.0, moAppend);
g.RotateTransform(30, moAppend);
g.DrawEllipse(Pens.Blue, 0, 0, 100, 50);
将产生下面的图像:
但是,我们有时需要知道变换后的图形尺寸及坐标,如果通过编程计算求得,可能很麻烦,特别是一些复杂的组合变换。利用Matrix提供的变换后的矩阵数据,可以很简单的得到变换后的图形矩形。
Matrix提供的是一个3 X 2 仿射矩阵,上面的2 X 2 矩阵是Rotate、Scale和Shear等线性变换后的数据,分别用m11,m21,m12,m22表示,可以用x = m11 * xi+ m21 * yi,y = m21 * xi + m22 * yi求得线性变换后图形的任意一点的相对坐标,我们只要计算四个角的坐标就行了;下面一行则是平移数据,用dx,dy表示,前面计算出的坐标点分别加上dx和dy就是实际的坐标点。下面是笔者写的一个计算函数和测试代码:
function GetTransPoint(x, y: Single): TGpPointF;
begin
Result.X : = e.m11 * x + e.m21 * y;
Result.Y : = e.m12 * x + e.m22 * y;
end;
var
R: TGpRectF;
pf: array[ 0 .. 3 ] of TGpPointF;
I: Integer;
begin
R : = Origin;
// 分别计算四个角的相对坐标
pf[ 0 ] : = GetTransPoint(R.X, R.Y);
pf[ 1 ] : = GetTransPoint(R.Width + R.X, R.Y);
pf[ 2 ] : = GetTransPoint(R.X, R.Height + R.Y);
pf[ 3 ] : = GetTransPoint(R.Width + R.X, R.Height + R.Y);
// 取得左上角和右下角的坐标点
R : = GpRect(pf[ 0 ].X, pf[ 0 ].Y, 0.0 , 0.0 );
for I : = 0 to 3 do
begin
if R.X > pf[I].X then R.X : = pf[I].X
else if R.Width < pf[I].X then R.Width : = pf[I].X;
if R.Y > pf[I].Y then R.Y : = pf[I].Y
else if R.Height < pf[I].Y then R.Height : = pf[I].Y;
end;
// 求出矩形尺寸
R.Width : = R.Width - R.X;
R.Height : = R.Height - R.Y;
// 求得左上角实际坐标
R.X : = R.X + e.dx;
R.Y : = R.Y + e.dy;
Result : = R;
end;
procedure TMainForm.Button1Click(Sender: TObject);
var
Image: TGpImage;
g: TGpGraphics;
m: TGpMatrix;
r: TGpRectF;
begin
Image : = TGpImage.Create( ' ....Mediamsn.jpg ' );
g : = TGpGraphics.Create(Handle, False);
m : = TGpMatrix.Create;
try
r : = GpRect( 20.0 , 20.0 , Image.Width, Image.Height);
g.Clear(ARGBFromTColor(Color));
g.DrawImage(Image, r);
m.Rotate( 30 );
m.Scale( 1.8 , 0.8 );
m.Shear( 0.3 , 0.2 );
g.SetTransform(m);
g.DrawImage(Image, r);
r : = CalcTransRect(r, m.Elements);
g.ResetTransform;
g.DrawRectangle(Pens.Red, r);
finally
Image.Free;
g.Free;
m.Free;
end;
end;
运行结果如下图,红色的矩形是计算出的变换后的坐标,与变换后的图像完全吻合:
还有的时候,我们需要精确控制图形的变换,比如,以图形左上角为原点缩放、以图形中心点进行旋转等。由于GDI+的几何变换是以Graphics画布原点,而不是以图形左上角为原点进行的,所以必须使用平移进行坐标变换,然后采用相对坐标画图。比如,我们希望上面的例子以画布20,20为原点(即无论怎样变换,左上角坐标不变)进行线性变换:
m.Translate( 20 , 20 ); // 坐标转换
m.Rotate( 30 );
m.Scale( 1.8 , 0.8 );
m.Shear( 0.3 , 0.2 );
g.SetTransform(m);
g.DrawImage(Image, 0 , 0 , Image.Width, Image.Height); //用相对坐标画图
其结果为下图,完全符合要求:
这个问题似乎很简单,但是我们忽略了一个变换顺序问题,前面所有的变换操作都是以默认顺序moPrepend进行的,如果改为moAppend是否符合我们的要求呢?答案是否!因为moPrepend是在上一个变换操作完成前进行的,也就是说无论是图形形状还是坐标位置,都是和上一变换操作同步进行的;而moAppend则是在上一变换操作完成后进行的,这时,图形形状和坐标点都已经变了,在已经改变了形状和坐标位置的基础上继续当前变换,肯定的不到我们预想的效果。下面的图是改变上面三个变换操作的顺序为moAppend后的运行结果:
为此,我们不能再用坐标变换后的相对坐标进行操作,而是需要在每个变换操作前预置坐标位置和变换操作后还原坐标位置,以便当前和后续变换都是正确的。Matrix已经提供了一个RotateAt,我们只要再写ScaleAt和ShearAt就行了,这样一来,无论操作顺序如何,变换操作位置都在我们的预期范围内。下面是这2个过程和测试代码:
begin
if order = moAppend then
begin
x : = - x;
y : = - y;
end;
m.Translate(x, y, Order);
m.Scale(ScaleX, ScaleY, Order);
m.Translate( - x, - y, Order);
end;
procedure ShearAt(m: TGpMatrix; ShearX, ShearY, x, y: Single; Order: TMatrixOrder = moPrepend);
begin
if order = moAppend then
begin
x : = - x;
y : = - y;
end;
m.Translate(x, y, Order);
m.Shear(ShearX, ShearY, Order);
m.Translate( - x, - y, Order);
end;
procedure TMainForm.Button2Click(Sender: TObject);
var
Image: TGpImage;
g: TGpGraphics;
m: TGpMatrix;
begin
Image : = TGpImage.Create( ' ....Mediamsn.jpg ' );
g : = TGpGraphics.Create(Handle, False);
m : = TGpMatrix.Create;
try
g.DrawImage(Image, 20 , 20 , Image.Width, Image.Height);
m.RotateAt( 30 , GpPoint( 20.0 , 20.0 ), moAppend);
ScaleAt(m, 1.8 , 0.8 , 20 , 20 , moAppend);
ShearAt(m, 0.3 , 0.2 , 20 , 20 , moAppend);
g.SetTransform(m);
g.DrawImage(Image, 20 , 20 , Image.Width, Image.Height);
finally
Image.Free;
g.Free;
m.Free;
end;
end;
运行结果如下图,如果改为默认操作顺序,则和前面平移坐标后的运行结果完全一样:
说明,本文所用GDI+代码与网上流通的不兼容,如上面例子中的TMatrixElements,定义为:
TMatrixElements = packed record
case Integer of
0: (Elements: array[0..5] of Single);
1: (m11, m12, m21, m22, dx, dy: Single);
end;
而网上流通的则定义为一个数组:
TMatrixArray = array[0..5] of Single;
如有错误,请来信指正:maozefa@hotmail.com