How to see if two shapes overlap - delphi

I'm trying to write a simple firemonkey test app.
I have a form, with a panel (align:= alClient).
On the form are 2 TCircle's.
I have set TCircle.Dragmode:= dmAutomatic.
I would like to drag the circles around and have something happen when the circles overlap.
The question is: I don't see any method in TCircle called overlap, nor do I see an event called on overlap. I've tried all the xxxxDrag events, but that does not help me with the hittesting.
How can I see when a shape being dragged overlaps with another shape ?
I was expecting one of the DragOver, DragEnter events to detect this for me, but that does not seem to be the case.
Surely there must be some standard method for this in Firemonkey?
For now the pas file just looks like:
implementation
{$R *.fmx}
procedure TForm8.Circle1DragEnter(Sender: TObject; const Data: TDragObject;
const Point: TPointF);
begin
if Data.Source = Circle1 then Button1.Text:= 'DragEnter';
end;
procedure TForm8.Circle1DragOver(Sender: TObject; const Data: TDragObject;
const Point: TPointF; var Accept: Boolean);
begin
if (Data.Source = Circle2) then Button1.Text:= 'Circle2 drag';
end;
procedure TForm8.Circle2DragEnd(Sender: TObject);
begin
Button1.Text:= 'DragEnd';
end;
procedure TForm8.Circle2DragEnter(Sender: TObject; const Data: TDragObject;
const Point: TPointF);
begin
Button1.Text:= 'DragEnter';
end;
procedure TForm8.Circle2DragLeave(Sender: TObject);
begin
Button1.Text:= 'DragLeave';
end;
procedure TForm8.Circle2DragOver(Sender: TObject; const Data: TDragObject;
const Point: TPointF; var Accept: Boolean);
begin
if Data.Source = Circle2 then begin
Button1.Text:= 'DragOver';
Accept:= true;
end;
end;
The dfm looks something like this:
object Form8: TForm8
Left = 0
Top = 0
BiDiMode = bdLeftToRight
Caption = 'Form8'
ClientHeight = 603
ClientWidth = 821
Transparency = False
Visible = False
StyleLookup = 'backgroundstyle'
object Panel1: TPanel
Align = alClient
Width = 821.000000000000000000
Height = 603.000000000000000000
TabOrder = 1
object Button1: TButton
Position.Point = '(16,16)'
Width = 80.000000000000000000
Height = 22.000000000000000000
TabOrder = 1
StaysPressed = False
IsPressed = False
Text = 'Button1'
end
object Circle1: TCircle
DragMode = dmAutomatic
Position.Point = '(248,120)'
Width = 97.000000000000000000
Height = 105.000000000000000000
OnDragEnter = Circle1DragEnter
OnDragOver = Circle1DragOver
end
object Circle2: TCircle
DragMode = dmAutomatic
Position.Point = '(168,280)'
Width = 81.000000000000000000
Height = 65.000000000000000000
OnDragEnter = Circle2DragEnter
OnDragLeave = Circle2DragLeave
OnDragOver = Circle2DragOver
OnDragEnd = Circle2DragEnd
end
end
end

The general problem is difficult and known as collision detection - you can google the term to find the related algorithms.
The particular case of circles collision detection is easy - just calculate a distance between the centers of the circles. If the distance obtained is less than the sum of the circle's radii, the circles overlap.

Although this question is over a year old, i was facing a similar problem recently. Thanks to a bit of research into TRectF (used by FMX and FM2 Primitives), i came up with the following very simple function;
var
aRect1, aRect2 : TRectF;
begin
aRect1 := Selection1.AbsoluteRect;
aRect2 := Selection2.AbsoluteRect;
if System.Types.IntersectRect(aRect1,aRect2) then Result := True else Result := False;
end;
Self-explanatory, but if the 2 rectangles/objects intersect or overlap, then the result is true.
Alternative - Same routine, but code refined
var
aRect1, aRect2 : TRectF;
begin
aRect1 := Selection1.AbsoluteRect;
aRect2 := Selection2.AbsoluteRect;
result := System.Types.IntersectRect(aRect1,aRect2);
end;
You'll need to work on it to accept some input objects (in my case, i used TSelection's known as Selection1 and Selection2) and perhaps find a way to add an offset (take a look at TControl.GetAbsoluteRect in FMX.Types), but theoretically it should work with just about any primitive or any control.
Just as an additional note, there are numerous TRectF's in use for objects like this;
AbsoluteRect
BoundsRect
LocalRect
UpdateRect (May not apply to this situation, investigation needed)
ParentedRect
ClipRect
ChildrenRect
It's important to use the one most appropriate to your situation (as results will vary wildly in each case). In my example, the TSelection's were children of the form so using AbsoluteRect was very much the best choice (as LocalRect didn't return the correct values).
Realistically, you could loop through each child component of your parent to be able to figure out if there's collision between any and potentially, you could build a function that tells you exactly which ones are colliding (though to do so would likely require a recursive function).
If you ever need to deal with "basic physics" under which Collision Detection would be considered one (at least in this case, it's at the basic level) in Firemonkey, then dealing with TRectF is where you need to look. There's a lot of routines built into System.Types (XE3 and likely XE2) to deal with this stuff automatically and as such you can avoid a lot of math commonly associated with this problem.
Further Notes
Something i noted was that the routine above wasn't very precise and was several pixels out. One solution is to put your shape inside a parent container with alClient alignment, and then 5 pixel padding to all sides. Then, instead of measuring on the TSelection.AbsoluteRect, measure on the child object's AbsoluteRect.
For example, i put a TCircle inside each TSelection, set the circles alignments to alClient, padding to 5 on each side, and the modified the routine to work with Circle1 and Circle2 as opposed to Selection1 and Selection2. This turned out to be precise to the point that if the circles themselves didn't overlap (or rather, their area didn't overlap), then they'd not be seen as colliding until the edges actually touched. Obviously, the corners of the circles themselves are a problem, but you could perhaps add another child component inside each circle with it's visibility set to false, and it being slightly smaller in dimensions so as to imitate the old "Bounding Box" method of collision detection.
Example Application
I've added an example application with source showing the above. 1 tab provides a usable example, while a second tab provides a brief explanation of how TRectF works (and shows some of the limitations through the use of a radar-like visual interface. There's a third tab that demonstrates use of TBitmapListAnimation to create animated images.
FMX Collision Detection - Example and Source

It seems to me that there are far too many possible permutations to easily solve this problem generically and efficiently. Some special cases may have a simple and efficient solution: E.g. mouse cursor intersection is simplified by only considering a single point on the cursor; a very good technique for circles has been provided; many regular shapes may also benefit from custom formulae to detect collision.
However, irregular shapes make the problem much more difficult.
One option would be to enclose each shape in an imaginary circle. If those circles overlap, you can then imagine smaller tighter circles in the vicinity of the original intersection. Repeat the calculations with smaller and smaller circles as often as desired. This approach will allow you to choose a trade-off between processing requirements and accuracy of the detection.
A simpler and very generic - though somewhat less efficient approach would be to draw each shape to an off-screen canvas using solid colours and an xor mask. After drawing, if any pixels of the xor colour are found, this would indicate a collision.

Hereby a begin/setup for collision-detection between TCircle, TRectangle and TRoundRect:
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Objects, Generics.Collections, Math;
type
TForm1 = class(TForm)
Panel1: TPanel;
Circle1: TCircle;
Circle2: TCircle;
Rectangle1: TRectangle;
Rectangle2: TRectangle;
RoundRect1: TRoundRect;
RoundRect2: TRoundRect;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Panel1DragOver(Sender: TObject; const Data: TDragObject;
const Point: TPointF; var Accept: Boolean);
procedure Panel1DragDrop(Sender: TObject; const Data: TDragObject;
const Point: TPointF);
private
FShapes: TList<TShape>;
function CollidesWith(Source: TShape; const SourceCenter: TPointF;
out Target: TShape): Boolean;
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
function Radius(AShape: TShape): Single;
begin
Result := Min(AShape.ShapeRect.Width, AShape.ShapeRect.Height) / 2;
end;
function TForm1.CollidesWith(Source: TShape; const SourceCenter: TPointF;
out Target: TShape): Boolean;
var
Shape: TShape;
TargetCenter: TPointF;
function CollidesCircleCircle: Boolean;
begin
Result :=
TargetCenter.Distance(SourceCenter) <= (Radius(Source) + Radius(Target));
end;
function CollidesCircleRectangle: Boolean;
var
Dist: TSizeF;
RHorz: TRectF;
RVert: TRectF;
begin
Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
RHorz := Target.ShapeRect;
RHorz.Offset(Target.ParentedRect.TopLeft);
RVert := RHorz;
RHorz.Inflate(Radius(Source), 0);
RVert.Inflate(0, Radius(Source));
Result := RHorz.Contains(SourceCenter) or RVert.Contains(SourceCenter) or
(Sqr(RVert.Width / 2 - Dist.cx) + Sqr(RHorz.Height / 2 - Dist.cy) <=
Sqr(Radius(Source)));
end;
function CollidesRectangleCircle: Boolean;
var
Dist: TSizeF;
RHorz: TRectF;
RVert: TRectF;
begin
Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
RHorz := Source.ShapeRect;
RHorz.Offset(Source.ParentedRect.TopLeft);
RHorz.Offset(SourceCenter.Subtract(Source.ParentedRect.CenterPoint));
RVert := RHorz;
RHorz.Inflate(Radius(Target), 0);
RVert.Inflate(0, Radius(Target));
Result := RHorz.Contains(TargetCenter) or RVert.Contains(TargetCenter) or
(Sqr(RVert.Width / 2 - Dist.cx) + Sqr(RHorz.Height / 2 - Dist.cy) <=
Sqr(Radius(Target)));
end;
function CollidesRectangleRectangle: Boolean;
var
Dist: TSizeF;
begin
Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
Result :=
(Dist.cx <= (Source.ShapeRect.Width + Target.ShapeRect.Width) / 2) and
(Dist.cy <= (Source.ShapeRect.Height + Target.ShapeRect.Height) / 2);
end;
function CollidesCircleRoundRect: Boolean;
var
Dist: TSizeF;
R: TRectF;
begin
Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
R := Target.ShapeRect;
R.Offset(Target.ParentedRect.TopLeft);
if R.Width > R.Height then
begin
Dist.cx := Dist.cx - (R.Width - R.Height) / 2;
R.Inflate(-Radius(Target), Radius(Source));
end
else
begin
Dist.cy := Dist.cy - (R.Height - R.Width) / 2;
R.Inflate(Radius(Source), -Radius(Target));
end;
Result := R.Contains(SourceCenter) or
(Sqrt(Sqr(Dist.cx) + Sqr(Dist.cy)) <= (Radius(Source) + Radius(Target)));
end;
function CollidesRoundRectCircle: Boolean;
var
Dist: TSizeF;
R: TRectF;
begin
Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
R := Source.ShapeRect;
R.Offset(Source.ParentedRect.TopLeft);
R.Offset(SourceCenter.Subtract(Source.ParentedRect.CenterPoint));
if R.Width > R.Height then
begin
Dist.cx := Dist.cx - (R.Width - R.Height) / 2;
R.Inflate(-Radius(Source), Radius(Target));
end
else
begin
Dist.cy := Dist.cy - (R.Height - R.Width) / 2;
R.Inflate(Radius(Target), -Radius(Source));
end;
Result := R.Contains(TargetCenter) or
(Sqrt(Sqr(Dist.cx) + Sqr(Dist.cy)) <= (Radius(Source) + Radius(Target)));
end;
function CollidesRectangleRoundRect: Boolean;
begin
Result := False;
end;
function CollidesRoundRectRectangle: Boolean;
begin
Result := False;
end;
function CollidesRoundRectRoundRect: Boolean;
begin
Result := False;
end;
function Collides: Boolean;
begin
if (Source is TCircle) and (Target is TCircle) then
Result := CollidesCircleCircle
else if (Source is TCircle) and (Target is TRectangle) then
Result := CollidesCircleRectangle
else if (Source is TRectangle) and (Target is TCircle) then
Result := CollidesRectangleCircle
else if (Source is TRectangle) and (Target is TRectangle) then
Result := CollidesRectangleRectangle
else if (Source is TCircle) and (Target is TRoundRect) then
Result := CollidesCircleRoundRect
else if (Source is TRoundRect) and (Target is TCircle) then
Result := CollidesRoundRectCircle
else if (Source is TRectangle) and (Target is TRoundRect) then
Result := CollidesRectangleRoundRect
else if (Source is TRoundRect) and (Target is TRectangle) then
Result := CollidesRoundRectRectangle
else if (Source is TRoundRect) and (Target is TRoundRect) then
Result := CollidesRoundRectRoundRect
else
Result := False;
end;
begin
Result := False;
for Shape in FShapes do
begin
Target := Shape;
TargetCenter := Target.ParentedRect.CenterPoint;
Result := (Target <> Source) and Collides;
if Result then
Break;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FShapes := TList<TShape>.Create;
FShapes.AddRange([Circle1, Circle2, Rectangle1, Rectangle2, RoundRect1,
RoundRect2]);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FShapes.Free;
end;
procedure TForm1.Panel1DragDrop(Sender: TObject; const Data: TDragObject;
const Point: TPointF);
var
Source: TShape;
begin
Source := TShape(Data.Source);
Source.Position.Point := PointF(Point.X - Source.Width / 2,
Point.Y - Source.Height / 2);
end;
procedure TForm1.Panel1DragOver(Sender: TObject; const Data: TDragObject;
const Point: TPointF; var Accept: Boolean);
var
Source: TShape;
Target: TShape;
begin
Source := TShape(Data.Source);
if CollidesWith(Source, Point, Target) then
Caption := Format('Kisses between %s and %s', [Source.Name, Target.Name])
else
Caption := 'No love';
Accept := True;
end;
end.

Guess we have to roll our own.
One option for this is a 2D implementation of the Gilbert-Johnson-Keerthi distance algorithm.
A D implementation can be found here: http://code.google.com/p/gjkd/source/browse/

Related

How to get the number of displayed lines in TMemo?

I need to get the number of displayed lines in TMemo (this include the lines that was wrapped because WordWrap is set to true). I need this to auto adjust the height of the Tmemo to it's content.
lines.count of course don't care about wrapped lines so i can't use it. strangely TextPosToPos also don't care about wrapped lines so i can't use it too ...
I m under firemonkey and delphi Berlin
Edit after I learned about ContentsBounds. My original (and obsolete) answer is still visible in the revisions.
Why do you need the number of displayed lines to adjust the height of the TMemo? This resizes the TMemo to its content:
Memo1.Height := Memo1.ContentBounds.Height + 5; // Small const to allow for rendering margins
It also takes word-wrap into consideration.
I don't know why using ContentBounds is "not really ideal". Here's how I do it:
uses
FMX.TextLayout, FMX.Graphics;
function MeasureTextHeight(const AFont: TFont; const AText: string): Single;
var
LLayout: TTextLayout;
begin
LLayout := TTextLayoutManager.DefaultTextLayout.Create;
try
LLayout.BeginUpdate;
try
LLayout.WordWrap := False;
LLayout.Font.Assign(AFont);
LLayout.Text := AText;
finally
LLayout.EndUpdate;
end;
Result := LLayout.TextHeight;
finally
LLayout.Free;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
LTextHeight: Single;
LLines: Integer;
begin
LTextHeight := MeasureTextHeight(Memo1.TextSettings.Font, Memo1.Text);
LLines := Round(Memo1.ContentBounds.Height / LTextHeight);
end;
This is the rough way i use to calculate the number of lines an TMemo (styled) under firemonkey :
type
_TStyledMemoProtectedAccess = class(TStyledMemo);
procedure TALStyledMemo.OnApplyStyleLookupImpl(sender: Tobject);
Var I, j, k, l: integer;
begin
// TALStyledMemo
// TStyledMemo
// TLayout
// TActiveStyleObject
// TLayout
// TScrollBar
// TScrollBar
// TLayout
// TSmallScrollBar
// TSmallScrollBar
// TScrollContent
for I := 0 to controls.Count - 1 do begin
if (controls[i] is TStyledMemo) then begin // << TStyledMemo
fStyledMemo := TStyledMemo(controls[i]);
end;
end;
end;
function TALStyledMemo.getLineHeight: Single;
begin
if fStyledMemo <> nil then result := _TStyledMemoProtectedAccess(fStyledMemo).GetLineHeight
else result := 0;
end;
function TALStyledMemo.getLineCount: integer;
var aLineHeight: Single;
begin
aLineHeight := getLineHeight;
if compareValue(aLineHeight, 0, Tepsilon.Position) > 0 then result := round(ContentBounds.Height / aLineHeight)
else result := 0;
end;

TListView columns order bug after windows theme change

TListView's column contents become incorrect after windows theme change. I've narrowed it down to CM_RECREATE message, that's when VCL recreates TListView's window handle in response to system theme change. Below are some screenshots illustrating the problem.
Original list view state
Last column has been moved moved to the first position. Everything is fine.
After Windows theme was changed, the columns positions are preserved, however, the contents are no longer correct.
Currently I overcome the issue by simply recreating the columns manually in my custom CM_RECREATEWND handler. Is it a bug? It it a good solution to recreate columns or is there a better way?
I'm using Delphi10 but the same behavior was observed in the previous versions as well.
I'll post my workaround in case anyone needs a quick fix for this bug. Just include this unit as a last used unit in a Form's uses list.
unit LVFix;
interface
uses
Winapi.Windows, Winapi.Messages, System.Classes, System.UITypes,
Vcl.Controls, Vcl.ComCtrls;
type
TListView = class(Vcl.ComCtrls.TListView)
strict private
type
TColumnRec = record
Alignment: TAlignment;
AutoSize: Boolean;
Caption: String;
ImageIndex: TImageIndex;
MaxWidth, MinWidth, Width: TWidth;
Tag: Integer;
ID: Integer;
end;
var
FSavedCols: TArray<TColumnRec>;
FSavedColOrder: TArray<Integer>;
private
procedure SaveColumnState;
procedure RestoreColumnState;
protected
procedure CMRecreate(var M: TMessage); message CM_RECREATEWND;
end;
implementation
uses
Winapi.CommCtrl;
{ TListView }
procedure TListView.CMRecreate(var M: TMessage);
begin
SaveColumnState;
inherited;
RestoreColumnState;
end;
procedure TListView.RestoreColumnState;
var
I: Integer;
begin
Items.BeginUpdate; //lock to prevent unnecessary events firing
try
//recreate columns
Columns.Clear;
for I := 0 to High(FSavedCols) do
begin
with Columns.Add do
begin
Alignment := FSavedCols[I].Alignment;
AutoSize := FSavedCols[I].AutoSize;
Caption := FSavedCols[I].Caption;
ImageIndex := FSavedCols[I].ImageIndex;
MinWidth := FSavedCols[I].MinWidth;
MaxWidth := FSavedCols[I].MaxWidth;
Width := FSavedCols[I].Width;
Tag := FSavedCols[I].Tag;
end;
end;
//restore column order
if Length(FSavedColOrder) <> 0 then
ListView_SetColumnOrderArray(Handle, Columns.Count, PInteger(FSavedColOrder));
finally
Items.EndUpdate;
end;
end;
procedure TListView.SaveColumnState;
var
R: LongBool;
I: Integer;
J: Integer;
T: TColumnRec;
begin
//save column order
SetLength(FSavedColOrder, Columns.Count);
R := ListView_GetColumnOrderArray(Handle, Columns.Count, PInteger(FSavedColOrder));
if not R then
SetLength(FSavedColOrder, 0);
//save original columns in original order
SetLength(FSavedCols, Columns.Count);
for I := 0 to Columns.Count - 1 do
begin
FSavedCols[I].Alignment := Columns[I].Alignment;
FSavedCols[I].AutoSize := Columns[I].AutoSize;
FSavedCols[I].Caption := Columns[I].Caption;
FSavedCols[I].ImageIndex := Columns[I].ImageIndex;
FSavedCols[I].MinWidth := Columns[I].MinWidth;
FSavedCols[I].MaxWidth := Columns[I].MaxWidth;
FSavedCols[I].Width := Columns[I].Width;
FSavedCols[I].Tag := Columns[I].Tag;
FSavedCols[I].ID := Columns[I].ID;
end;
for I := 0 to High(FSavedCols) - 1 do
for J := I + 1 to High(FSavedCols) do
if FSavedCols[J].ID < FSavedCols[I].ID then
begin
T := FSavedCols[I];
FSavedCols[I] := FSavedCols[J];
FSavedCols[J] := T;
end;
end;
end.

Bouncing Ball Game

Working on a school project and need some help please... I have built a game which purpose it to avoid your primary ball from crashing into the random balls that generate every 30 seconds. My problem is that the random balls are not staying within the frame and I am not sure what I am doing wrong. Any help would be much appreciated. Thank you
procedure TFrmGamePage.EnemyBall(shpEnemy: TShape);
VAR
bOutside, bAbove, bBelow, bFarLeft, bFarRight : Boolean;
ixMove, iyMove, iyDirec{Negative = increase, Positive = decrease}, ixDirec{positive = increase, negative = decrease} : integer;
begin
bAbove := pnlArena.Height-shpEnemy.Top > pnlArena.Height;
bBelow := pnlArena.Height < shpEnemy.Top;
bFarLeft := pnlArena.Width-shpEnemy.Left > pnlArena.Width;
bFarRight := pnlArena.Width < shpEnemy.Left;
ixMove:=random(3)+1;
iyMove:=random(3)+1;
ixDirec:=1;
iyDirec:=1;
//Check if the shape is outside.
if bAbove=true or bBelow=true or bFarLeft=true or bFarRight=true then
Begin
bOutside:=true;
End
Else
begin
bOutside:=False;
end;
// if shape is outside swop relavent direction
if bOutside=true then
Begin
Begin
if bAbove=true then
begin
iyDirec:=1;
end;
if bBelow=true then
begin
iyDirec:=-1;
end;
if bFarRight then
begin
ixDirec:=-1;
end;
if bFarLeft then
begin
ixDirec:=1;
end;
End;
End;
shpEnemy.Top := shpEnemy.Top + iyMove * iyDirec;
shpEnemy.Left := shpEnemy.Left + ixMove * ixDirec; // Change pos of enemy shapes
end;
You've made two main mistakes:
You are not calculating correctly the conditions (bAbove, bBelow etc.) responsible for the change of ball direction.
The important thing to know here is that the ball position is relative to its parent (pnlArena in this case). To explain it in different words: ball doesn't know anything about the outside world, pnlArena is the whole world for ball. So if your window coordinate system origin is top left then the far most left of the pnlArena equals to 0 (pnlArena.Left = 0) and the far most top is also 0 (pnlArena.Top = 0).
Knowing this you could probably guess now that ball will cross the left border of its world when shpEnemy.Left < 0. I will not go onto details about other directions, try to understand the code which I've provided.
The second mistake is more subtle. Currently your shpEnemy will not bounce back correctly from walls of your arena. The thing is that shpEnemy does not remember its last direction. You need to change the direction only when it is necessery, not each time.
Here is a fully working example:
unit Unit144;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls;
// Our shape needs to remeber its direction, otherwise it won't move correctly.
type
TBallShape = class(TShape)
public
xDirec, yDirec: Integer;
constructor Create(AOwner: TComponent; AParent: TWinControl); reintroduce;
end;
type
TForm144 = class(TForm)
pnlArena: TPanel;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
BallShape: TBallShape;
public
procedure EnemyBall(shpEnemy: TBallShape);
end;
var
Form144: TForm144;
implementation
{$R *.dfm}
{ TForm144 }
procedure TForm144.Button1Click(Sender: TObject);
begin
EnemyBall(BallShape);
end;
procedure TForm144.EnemyBall(shpEnemy: TBallShape);
VAR
bOutside, bAbove, bBelow, bFarLeft, bFarRight : Boolean;
ixMove, iyMove : integer;
begin
ixMove:=random(3)+1;
iyMove:=random(3)+1;
bAbove := shpEnemy.Top < 0;
bBelow := shpEnemy.Top + shpEnemy.Height > pnlArena.Height;
bFarLeft := shpEnemy.Left < 0;
bFarRight := shpEnemy.Left + shpEnemy.Width > pnlArena.Width;
//Check if the shape is outside.
if bAbove or bBelow or bFarLeft or bFarRight then
Begin
bOutside:=true;
End
Else
begin
bOutside:=False;
end;
// if shape is outside swop relavent direction
if bOutside=true then
Begin
Begin
if bAbove=true then
begin
shpEnemy.yDirec:=1;
end;
if bBelow=true then
begin
shpEnemy.yDirec:=-1;
end;
if bFarRight then
begin
shpEnemy.xDirec:=-1;
end;
if bFarLeft then
begin
shpEnemy.xDirec:=1;
end;
End;
End;
shpEnemy.Top := shpEnemy.Top + iyMove * shpEnemy.yDirec;
shpEnemy.Left := shpEnemy.Left + ixMove * shpEnemy.xDirec; // Change pos of enemy shapes
end;
procedure TForm144.FormCreate(Sender: TObject);
begin
Randomize;
BallShape := TBallShape.Create(Self, pnlArena);
BallShape.Shape := stCircle;
end;
{ TBallShape }
constructor TBallShape.Create(AOwner: TComponent; AParent: TWinControl);
var
LDirection: Integer;
begin
inherited Create(AOwner);
Width := 20;
Height := 20;
// We chose random direction of our ball.
Self.Parent := AParent;
LDirection := Random(1);
if LDirection = 0 then
xDirec := - 1
else
xDirec := 1;
LDirection := Random(1);
if LDirection = 0 then
yDirec := - 1
else
yDirec := 1;
// We must place our ball somewhere on the parent.
Left := Random(AParent.Width) - Self.Width;
if Left < 0 then
Left := 0;
Top := Random(AParent.Height) - Self.Height;
if Top < 0 then
Top := 0;
end;
end.
Hope this helps.

TEdit not redrawing correctly with Invalidate in Delphi 5

There's a problem with the TScrollBox in Delphi 5 when using Cirtix, on some systems, when a user scrolls by clicking the button at the top or bottom of the end of scrollbar the whole application freezes. We had the issue in QucikReports previews initially and got round it by implementing our own scrollbars in the TScrollBox.
We now have a piece of bespoke work that uses a TScrollBox and the client is reporting a similar problem so I'm working round it in the same way. I hide the TScrollBox scrollbars and add in my own. When those are clicked I call the following.
Note, this test code is not currently running in Citrix, I've tested on XP and Window 7.
I am turning off redrawing of the control, moving all the child controls, then turning drawing back on and calling Invalidate. I would expect invalidate to fully redraw the control but that's not happening.
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
begin
if (x = 0) and (y = 0) then
Exit;
// Stop the control from repaining while we're updating it
try
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if (FScrollBox.Controls[I] = FVScrollBar) or (FScrollBox.Controls[I] = FHScrollBar) then
Continue;
FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
end;
finally
// Turn on painting again
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
end;
// Redraw everything
InvalidateEverything(FScrollBox);
end;
Code to redraw controls
procedure TScrollBoxScrollReplacement.InvalidateEverything(AControl: TControl);
var
I: Integer;
begin
AControl.Invalidate();
if (AControl is TWinControl) then
for I := 0 to TWinControl(AControl).ControlCount - 1 do
InvalidateEverything(TWinControl(AControl).Controls[I]);
end;
I added in the Invalidate, Refresh and Reapint and loop through all child controls in an effort to get it working, but still no luck. The edit boxes look as follows:
If I set Visible to false and back to true then they'll redraw correctly, but there is obviously a horrible flicker. They also redraw correctly if I minimise the maximise the window, or drag it off and on the screen.
Any help would be much appreciated.
edit : Some info about the answers.
Users looking for a solution, I'd recommend you try both. David's and Sertac's. David's looks like it is the correct solution according to Microsoft's documentation. However, with the Delphi scrollbox, labels placed directly in the scrollbox flicker, where are labels placed in groupboxes in the scrollbox are perfectly smooth. I think this might be an issue with all components that don't descend from TWinControl. Scrolling itself is smoother with David's solution, but there's less flicking using WM_SETREDRAW and RedrawWindow. I would have liked to accept both as answers as both have their advantages and disadvantages.
edit : Code for the whole class below
To test just add a scrollbox with some controls to your form and call
TScrollBoxScrollReplacement.Create(ScrollBox1);
.
unit ScrollBoxScrollReplacement;
interface
uses extctrls, stdctrls, SpScrollBox, forms, Controls, classes, Messages, Windows, Sysutils, Math;
type
TScrollBoxScrollReplacement = class(TComponent)
private
FLastVScrollPos: Integer;
FLastHScrollPos: Integer;
FScrollBox: TScrollBox;
FVScrollBar: TScrollBar;
FHScrollBar: TScrollBar;
FVScrollBarVisible: Boolean;
FHScrollBarVisible: Boolean;
FCornerPanel: TPanel;
FMaxRight: Integer;
FMaxBottom: Integer;
FOriginalResizeEvent: TNotifyEvent;
FOriginalCanResizeEvent: TCanResizeEvent;
FInScroll: Boolean;
function GetHScrollHeight: Integer;
function GetVScrollWidth: Integer;
procedure ReplaceScrollBars;
function SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
procedure ScrollBoxResize(Sender: TObject);
procedure ScrollBarEnter(Sender: TObject);
procedure PositionScrollBars;
procedure Scroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
procedure ScrollControls(x, y: Integer);
procedure CalculateControlExtremes();
procedure ResetVScrollBarRange;
procedure ResetHScrollBarRange;
function IsReplacementControl(AControl: TControl): Boolean;
property HScrollHeight: Integer read GetHScrollHeight;
property VScrollWidth: Integer read GetVScrollWidth;
procedure ScrollBoxCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean);
public
constructor Create(AScrollBox: TScrollBox); reintroduce; overload;
destructor Destroy(); override;
procedure ResetScrollBarRange();
procedure BringReplacementControlsToFront();
end;
implementation
{ TScrollBoxScrollReplacement }
constructor TScrollBoxScrollReplacement.Create(AScrollBox: TScrollBox);
begin
// Set up the scrollbox as our owner so we're destroyed when the scrollbox is
inherited Create(AScrollBox);
FScrollBox := AScrollBox;
ReplaceScrollBars();
// We make a note of any existing resize and can resize events so we can call them to make sure we don't break anything
FOriginalResizeEvent := FScrollBox.OnResize;
FScrollBox.OnResize := ScrollBoxResize;
FOriginalCanResizeEvent := FScrollBox.OnCanResize;
FScrollBox.OnCanResize := ScrollBoxCanResize;
end;
// This is called (unintuitively) when controls are moved within the scrollbox. We can use this to reset our scrollbar ranges
procedure TScrollBoxScrollReplacement.ScrollBoxCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean);
begin
if (not FInScroll) then
begin
ResetScrollBarRange();
BringReplacementControlsToFront();
end;
if (Assigned(FOriginalCanResizeEvent)) then
FOriginalCanResizeEvent(Sender, NewWidth, NewHeight, Resize);
end;
procedure TScrollBoxScrollReplacement.ScrollBoxResize(Sender: TObject);
begin
if (Assigned(FOriginalResizeEvent)) then
FOriginalResizeEvent(Sender);
ResetScrollBarRange();
end;
// Hides the original scrollbars and adds in ours
procedure TScrollBoxScrollReplacement.ReplaceScrollBars();
begin
FVScrollBar := SetUpScrollBar(FScrollBox.VertScrollBar, sbVertical);
FVScrollBarVisible := FVScrollBar.Visible;
FHScrollBar := SetUpScrollBar(FScrollBox.HorzScrollBar, sbHorizontal);
FHScrollBarVisible := FHScrollBar.Visible;
FCornerPanel := TPanel.Create(FScrollBox);
FCornerPanel.Parent := FScrollBox;
ResetScrollBarRange();
end;
procedure TScrollBoxScrollReplacement.PositionScrollBars();
begin
// Align our scrollbars correctly
FVScrollBar.Top := 0;
FVScrollBar.Left := FScrollBox.ClientWidth - FVScrollBar.Width;
FVScrollBar.Height := FScrollBox.ClientHeight - HScrollHeight;
// FVScrollBar.BringToFront();
FHScrollBar.Left := 0;
FHScrollBar.Top := FScrollBox.ClientHeight - FHScrollBar.Height;
FHScrollBar.Width := FScrollBox.ClientWidth - VScrollWidth;
// FHScrollBar.BringToFront();
// If both scrollbars are visible we'll put a panel in the corner so we can't see components through it
if (FVScrollBar.Visible) and (FHScrollBar.Visible) then
begin
FCornerPanel.Left := FHScrollBar.Width;
FCornerPanel.Top := FVScrollBar.Height;
FCornerPanel.Width := FVScrollBar.Width;
FCornerPanel.Height := FHScrollBar.Height;
FCornerPanel.Visible := True;
// FCornerPanel.BringToFront();
end
else
FCornerPanel.Visible := False;
end;
procedure TScrollBoxScrollReplacement.ResetScrollBarRange();
begin
CalculateControlExtremes();
ResetVScrollBarRange();
ResetHScrollBarRange();
PositionScrollBars();
end;
procedure TScrollBoxScrollReplacement.ResetVScrollBarRange();
var
ScrollMax: Integer;
ScrollAmount: Integer;
begin
// If all the controls fit to the right of the screen, but there are controls off the left then we'll scroll right.
ScrollMax := FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height;
if (ScrollMax < 0) and (FLastVScrollPos > 0) then
begin
ScrollAmount := Min(Abs(ScrollMax), FLastVScrollPos);
ScrollControls(0, ScrollAmount);
FLastVScrollPos := FLastVScrollPos - ScrollAmount;
CalculateControlExtremes();
end;
FVScrollBar.Max := Max(FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height + FLastVScrollPos, 0);
FVScrollBar.Visible := (FVScrollBar.Max > 0) and FVScrollBarVisible;
end;
procedure TScrollBoxScrollReplacement.ResetHScrollBarRange();
var
ScrollMax: Integer;
ScrollAmount: Integer;
begin
// If all the controls fit to the bottom of the screen, but there are controls off the top then we'll scroll up.
ScrollMax := FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width;
if (ScrollMax < 0) and (FLastHScrollPos > 0) then
begin
ScrollAmount := Min(Abs(ScrollMax), FLastHScrollPos);
ScrollControls(ScrollAmount, 0);
FLastHScrollPos := FLastHScrollPos - ScrollAmount;
CalculateControlExtremes();
end;
FHScrollBar.Max := Max(FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width + FLastHScrollPos, 0);
FHScrollBar.Visible := (FHScrollBar.Max > 0) and FHScrollBarVisible;
end;
function TScrollBoxScrollReplacement.SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
begin
Result := TScrollBar.Create(FScrollBox);
Result.Visible := AControlScrollBar.Visible;
Result.Parent := FScrollBox;
Result.Kind := AKind;
Result.Ctl3D := False;
Result.Max := AControlScrollBar.Range;
Result.OnEnter := ScrollBarEnter;
Result.OnScroll := Scroll;
Result.SmallChange := 5;
Result.LargeChange := 20;
AControlScrollBar.Visible := False;
end;
destructor TScrollBoxScrollReplacement.Destroy;
begin
inherited;
end;
procedure TScrollBoxScrollReplacement.ScrollBarEnter(Sender: TObject);
begin
// We just call this here to make sure our ranges are set correctly - a backup in case things go wrong
ResetScrollBarRange();
end;
procedure TScrollBoxScrollReplacement.Scroll(Sender: TObject;
ScrollCode: TScrollCode; var ScrollPos: Integer);
var
Change: Integer;
begin
ResetScrollBarRange();
if (Sender = FVScrollBar) then
begin
Change := FLastVScrollPos - ScrollPos;
ScrollControls(0, Change);
FLastVScrollPos := ScrollPos;
end
else if (Sender = FHScrollBar) then
begin
Change := FLastHScrollPos - ScrollPos;
ScrollControls(Change, 0);
FLastHScrollPos := ScrollPos;
end;
end;
// Moves all the controls in the scrollbox except for the scrollbars we've added
{procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
begin
if (x = 0) and (y = 0) then
Exit;
// Stop the control from repaining while we're updating it
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
FInScroll := True;
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if IsReplacementControl(FScrollBox.Controls[I]) then
Continue;
FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
end;
finally
// Turn on painting again
FInScroll := False;
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
end;
// Redraw everything
RedrawWindow(FSCrollBox.Handle, nil, 0, RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
end; }
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
Control: TControl;
WinControl: TWinControl;
hWinPosInfo: HDWP;
begin
if (x = 0) and (y = 0) then
Exit;
hWinPosInfo := BeginDeferWindowPos(0);
Win32Check(hWinPosInfo<>0);
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
Control := FScrollBox.Controls[I];
if (Control = FVScrollBar) or (Control = FHScrollBar) then
Continue;
if Control is TWinControl then
begin
WinControl := FScrollBox.Controls[I] as TWinControl;
hWinPosInfo := DeferWindowPos(
hWinPosInfo,
WinControl.Handle,
0,
WinControl.Left + x,
WinControl.Top + y,
WinControl.Width,
WinControl.Height,
SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
);
Win32Check(hWinPosInfo<>0);
end
else
Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
end;
finally
EndDeferWindowPos(hWinPosInfo);
end;
end;
// works out where our right most and bottom most controls are so we can set the scrollbars correctly
procedure TScrollBoxScrollReplacement.CalculateControlExtremes();
var
I: Integer;
Right: Integer;
Bottom: Integer;
begin
FMaxRight := 0;
FMaxBottom := 0;
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if IsReplacementControl(FScrollBox.Controls[I]) then
Continue;
Right := FScrollBox.Controls[I].Left + FScrollBox.Controls[I].Width;
Bottom := FScrollBox.Controls[I].Top + FScrollBox.Controls[I].Height;
FMaxRight := Max(FMaxRight, Right);
FMaxBottom := Max(FMaxBottom, Bottom);
end;
end;
function TScrollBoxScrollReplacement.GetHScrollHeight: Integer;
begin
if (FHScrollBar.Visible) then
Result := FHScrollBar.Height
else
Result := 0;
end;
function TScrollBoxScrollReplacement.GetVScrollWidth: Integer;
begin
if (FVScrollBar.Visible) then
Result := FVScrollBar.Width
else
Result := 0;
end;
// Returns true if the passed control is one of the controls we've added
function TScrollBoxScrollReplacement.IsReplacementControl(
AControl: TControl): Boolean;
begin
Result := (AControl = FVScrollBar) or (AControl = FHScrollBar) or (AControl = FCornerPanel);
end;
procedure TScrollBoxScrollReplacement.BringReplacementControlsToFront;
begin
FVScrollBar.BringToFront();
FHScrollBar.BringToFront();
FCornerPanel.BringToFront();
end;
end.
I found that your code started working once I remove the two WM_SETREDRAW messages. That's your fundamental problem. You will need to remove the WM_SETREDRAW messages.
That will no doubt mean you still need to solve your problem with flickering, but that's a different problem. My quick experiments suggest that DeferWindowPos could solve that problem. For example:
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
Control: TControl;
WinControl: TWinControl;
hWinPosInfo: HDWP;
begin
if (x = 0) and (y = 0) then
Exit;
hWinPosInfo := BeginDeferWindowPos(0);
Win32Check(hWinPosInfo<>0);
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
Control := FScrollBox.Controls[I];
if (Control = FVScrollBar) or (Control = FHScrollBar) then
Continue;
if Control is TWinControl then
begin
WinControl := FScrollBox.Controls[I] as TWinControl;
hWinPosInfo := DeferWindowPos(
hWinPosInfo,
WinControl.Handle,
0,
WinControl.Left + x,
WinControl.Top + y,
WinControl.Width,
WinControl.Height,
SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
);
Win32Check(hWinPosInfo<>0);
end
else
Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
end;
finally
EndDeferWindowPos(hWinPosInfo);
end;
end;
Your non-windowed controls will still flicker, but you can make them windowed, or indeed put the whole content of the scroll box inside a windowed control. Heck, if you just did that, it would be enough to solve the problem!
For what it is worth, my trials indicate that DeferWindowPos gives smoother scrolling than WM_SETREDRAW and RedrawWindow. But these tests were hardly exhaustive and you might find different outcomes in your app.
Some asides regarding your code:
Your use of try/finally is incorrect. The pattern must be:
BeginSomething;
try
Foo;
finally
EndSomething;
end;
You get that wrong with your calls to SendMessage.
And you use an incorrect cast in InvalidateEverything. You cannot blindly cast a TControl to TWinControl. That said, that function does no good. You can remove it altogether. What it is attempting to do can be performed with a single call to Invalidate of the parent control.
You can replace your
FScrollBox.Invalidate();
with
RedrawWindow(FSCrollBox.Handle, nil, 0,
RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
to have all controls invalidated and updated properly. RDW_ERASE is for erasing the previous positions of controls and RDW_ALLCHILDREN is for taking care of windowed controls inside. Non-win controls like labels should already be repainted because of RDW_INVALIDATE.
Although this approach may help avoiding the flicker that you observe, it may also cause some loss of smoothness of scrolling while thumb tracking. That's because the scroll position might need to be updated more often than a paint cycle is processed. To circumvent this, instead of invalidating you can update the control positions immediately:
RedrawWindow(FSCrollBox.Handle, nil, 0,
RDW_ERASE or RDW_INVALIDATE or RDW_UPDATENOW or RDW_ALLCHILDREN);

Rotate TImage in Delphi

I am doing just for fun a virtual desktop to play Magic The Gathering with friends. I am using Delphi 2010. The cards are represented in the application by TImage components (loading PNG files of the cards loaded from a database). The point here is that in MTG a very common thing to do is to tap a card (rotating it 90ยบ degrees to right). There is a simple way to do this? I really don't need the "animation", just the card rotated once is clicked (animation would be nice though). The game should work simultaneously with many cards and they can be moved anywhere in the form. I am thinking in having the image of the card tapped and untapped in the database but this may be an overkill if there is a nice and efficient way to rotate the cards.
Any ideas?
The old-skool way of doing this is with PlgBlt.
procedure RotateBitmap90CW(b1,b2:TBitmap);
var
x,y:integer;
p:array[0..2] of TPoint;
begin
x:=b1.Width;
y:=b1.Height;
b2.Width:=y;
b2.Height:=x;
p[0].X:=y;
p[0].Y:=0;
p[1].X:=y;
p[1].Y:=x;
p[2].X:=0;
p[2].Y:=0;
PlgBlt(b2.Canvas.Handle,p,b1.Canvas.Handle,0,0,x,y,0,0,0);
end;
Or you can leave the TImage and use e.g. TPaintBox and GDI+ library. GDI+ has the RotateFlip method directly for doing this. Using the GDI+ Library for Delphi it would look like:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, ActiveX, GDIPOBJ, GDIPAPI;
type
TForm1 = class(TForm)
Button1: TButton;
PaintBox1: TPaintBox;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure PaintBox1Paint(Sender: TObject);
private
FImage: TGPImage;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
var
Stream: IStream;
BlobStream: TMemoryStream;
begin
BlobStream := TMemoryStream.Create;
try
// assuming the BlobStream here has a valid image loaded from a database
Stream := TStreamAdapter.Create(BlobStream);
FImage := TGPImage.Create(Stream);
finally
BlobStream.Free;
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FImage.Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FImage.RotateFlip(Rotate90FlipNone);
PaintBox1.Invalidate;
end;
procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
with TGPGraphics.Create(PaintBox1.Canvas.Handle) do
try
DrawImage(FImage, 0, 0);
finally
Free;
end;
end;
end.
Such an overkill, doesn't it :-?
You could use a TPaintBox instead of a TImage and use SetWorldTransform with a rotation matrix to draw the tapped card. I use StretchDrawRotated for this:
procedure XForm_SetRotation(out AXForm: TXForm; AAngle: Extended; ACenter: TPoint);
var
SinA, CosA: Extended;
begin
SinCos(AAngle, SinA, CosA);
AXForm.eM11 := CosA;
AXForm.eM12 := SinA;
AXForm.eM21 := -SinA;
AXForm.eM22 := CosA;
AXForm.eDx := (ACenter.X - (CosA * ACenter.X)) + ((SinA * ACenter.Y));
AXForm.eDy := (ACenter.Y - (SinA * ACenter.X)) - ((CosA * ACenter.Y));
end;
procedure StretchDrawRotated(ACanvas: TCanvas; const ARect: TRect; AAngle: Extended; ACenter: TPoint; AGraphic: TGraphic);
var
XForm, XFormOld: TXForm;
GMode: Integer;
begin
GMode := SetGraphicsMode(ACanvas.Handle, GM_ADVANCED);
try
if GetWorldTransform(ACanvas.Handle, XFormOld) then
try
XForm_SetRotation(XForm, AAngle, ACenter);
SetWorldTransform(ACanvas.Handle, XForm);
ACanvas.StretchDraw(ARect, AGraphic);
finally
SetWorldTransform(ACanvas.Handle, XFormOld);
end;
finally
SetGraphicsMode(ACanvas.Handle, GMode);
end;
end;
You can also use the Graphics32 library or just this function I grabbed some time ago from CodeCentral:
{by Gustavo Daud (Submited on 21 May 2006 )
Use this method to rotate RGB and RGB Alpha 'Portable Network Graphics' Images using a smooth antialiased algorithm in order to get much better results.
Note: Part of this code was based on JansFreeware code [http://jansfreeware.com/]
This is only possible when using the 1.56 library version.}
{Smooth rotate a png object}
procedure SmoothRotate(var aPng: TPNGImage; Angle: Extended);
{Supporting functions}
function TrimInt(i, Min, Max: Integer): Integer;
begin
if i>Max then Result:=Max
else if i<Min then Result:=Min
else Result:=i;
end;
function IntToByte(i:Integer):Byte;
begin
if i>255 then Result:=255
else if i<0 then Result:=0
else Result:=i;
end;
function Min(A, B: Double): Double;
begin
if A < B then Result := A else Result := B;
end;
function Max(A, B: Double): Double;
begin
if A > B then Result := A else Result := B;
end;
function Ceil(A: Double): Integer;
begin
Result := Integer(Trunc(A));
if Frac(A) > 0 then
Inc(Result);
end;
{Calculates the png new size}
function newsize: tsize;
var
fRadians: Extended;
fCosine, fSine: Double;
fPoint1x, fPoint1y, fPoint2x, fPoint2y, fPoint3x, fPoint3y: Double;
fMinx, fMiny, fMaxx, fMaxy: Double;
begin
{Convert degrees to radians}
fRadians := (2 * PI * Angle) / 360;
fCosine := abs(cos(fRadians));
fSine := abs(sin(fRadians));
fPoint1x := (-apng.Height * fSine);
fPoint1y := (apng.Height * fCosine);
fPoint2x := (apng.Width * fCosine - apng.Height * fSine);
fPoint2y := (apng.Height * fCosine + apng.Width * fSine);
fPoint3x := (apng.Width * fCosine);
fPoint3y := (apng.Width * fSine);
fMinx := min(0,min(fPoint1x,min(fPoint2x,fPoint3x)));
fMiny := min(0,min(fPoint1y,min(fPoint2y,fPoint3y)));
fMaxx := max(fPoint1x,max(fPoint2x,fPoint3x));
fMaxy := max(fPoint1y,max(fPoint2y,fPoint3y));
Result.cx := ceil(fMaxx-fMinx);
Result.cy := ceil(fMaxy-fMiny);
end;
type
TFColor = record b,g,r:Byte end;
var
Top, Bottom, Left, Right, eww,nsw, fx,fy, wx,wy: Extended;
cAngle, sAngle: Double;
xDiff, yDiff, ifx,ify, px,py, ix,iy, x,y, cx, cy: Integer;
nw,ne, sw,se: TFColor;
anw,ane, asw,ase: Byte;
P1,P2,P3:Pbytearray;
A1,A2,A3: pbytearray;
dst: TPNGImage;
IsAlpha: Boolean;
new_colortype: Integer;
begin
{Only allows RGB and RGBALPHA images}
if not (apng.Header.ColorType in [COLOR_RGBALPHA, COLOR_RGB]) then
raise Exception.Create('Only COLOR_RGBALPHA and COLOR_RGB formats' +
' are supported');
IsAlpha := apng.Header.ColorType in [COLOR_RGBALPHA];
if IsAlpha then new_colortype := COLOR_RGBALPHA else
new_colortype := COLOR_RGB;
{Creates a copy}
dst := tpngobject.Create;
with newsize do
dst.createblank(new_colortype, 8, cx, cy);
cx := dst.width div 2; cy := dst.height div 2;
{Gather some variables}
Angle:=angle;
Angle:=-Angle*Pi/180;
sAngle:=Sin(Angle);
cAngle:=Cos(Angle);
xDiff:=(Dst.Width-apng.Width)div 2;
yDiff:=(Dst.Height-apng.Height)div 2;
{Iterates over each line}
for y:=0 to Dst.Height-1 do
begin
P3:=Dst.scanline[y];
if IsAlpha then A3 := Dst.AlphaScanline[y];
py:=2*(y-cy)+1;
{Iterates over each column}
for x:=0 to Dst.Width-1 do
begin
px:=2*(x-cx)+1;
fx:=(((px*cAngle-py*sAngle)-1)/ 2+cx)-xDiff;
fy:=(((px*sAngle+py*cAngle)-1)/ 2+cy)-yDiff;
ifx:=Round(fx);
ify:=Round(fy);
{Only continues if it does not exceed image boundaries}
if(ifx>-1)and(ifx<apng.Width)and(ify>-1)and(ify<apng.Height)then
begin
{Obtains data to paint the new pixel}
eww:=fx-ifx;
nsw:=fy-ify;
iy:=TrimInt(ify+1,0,apng.Height-1);
ix:=TrimInt(ifx+1,0,apng.Width-1);
P1:=apng.scanline[ify];
P2:=apng.scanline[iy];
if IsAlpha then A1 := apng.alphascanline[ify];
if IsAlpha then A2 := apng.alphascanline[iy];
nw.r:=P1[ifx*3];
nw.g:=P1[ifx*3+1];
nw.b:=P1[ifx*3+2];
if IsAlpha then anw:=A1[ifx];
ne.r:=P1[ix*3];
ne.g:=P1[ix*3+1];
ne.b:=P1[ix*3+2];
if IsAlpha then ane:=A1[ix];
sw.r:=P2[ifx*3];
sw.g:=P2[ifx*3+1];
sw.b:=P2[ifx*3+2];
if IsAlpha then asw:=A2[ifx];
se.r:=P2[ix*3];
se.g:=P2[ix*3+1];
se.b:=P2[ix*3+2];
if IsAlpha then ase:=A2[ix];
{Defines the new pixel}
Top:=nw.b+eww*(ne.b-nw.b);
Bottom:=sw.b+eww*(se.b-sw.b);
P3[x*3+2]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
Top:=nw.g+eww*(ne.g-nw.g);
Bottom:=sw.g+eww*(se.g-sw.g);
P3[x*3+1]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
Top:=nw.r+eww*(ne.r-nw.r);
Bottom:=sw.r+eww*(se.r-sw.r);
P3[x*3]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
{Only for alpha}
if IsAlpha then
begin
Top:=anw+eww*(ane-anw);
Bottom:=asw+eww*(ase-asw);
A3[x]:=IntToByte(Round(Top+nsw*(Bottom-Top)));
end;
end;
end;
end;
apng.assign(dst);
dst.free;
end;
Graphics32 library was already mentioned there above.
http://graphics32.org/documentation/Docs/Units/GR32_Transforms/Classes/TAffineTransformation/Methods/Rotate.htm
http://graphics32.org
I'd like to mention one more good library, Vampyre Imaging
http://galfar.vevb.net/imaging/doc/imaging.html
http://ImagingLib.sf.net/

Resources