I use Delphi 6.0, and I have TPanel, that contains TVirtualStringTree, that contains many nodes (more, than 1 screen).
I need to save this panel (with all nodes) to the BMP as a picture.
I use this method to save panel:
procedure TfrmDidTreeTime.cmSaveGantClick(Sender: TObject);
var
bmp : tBitmap;
cnt : Integer;
Dc : HDC;
R : TRect;
begin
inherited;
DC := GetDC ( pnlChart.handle);
R := pnlChart.boundsRect;
bmp := tBitmap.create;
bmp.width := R.Right-R.Left;
bmp.Height := R.Bottom - R.Top;
Bitblt(bmp.canvas.handle,0,0,bmp.Width,bmp.height,dc,r.left,r.top,srccopy);
spdSaveGraph.DefaultExt := 'bmp';
spdSaveGraph.FileName := 'Gant.bmp';
spdSaveGraph.Filter := 'Bitmap Picture|*.bmp';
if spdSaveGraph.Execute then
bmp.saveToFile (spdSaveGraph.FileName);
bmp.free;
end;
But this method saves only nodes that shows on the screen, but I need to save all of nodes.
procedure TfrmDidTreeTime.cmSaveDidTreeTimeClick(Sender: TObject);
var
bmp_total: tBitmap;
bmp_current : tBitmap;
r: TRect;
dc: HDC;
date_start: TDateTime;
h: integer;
bmp_current_position: integer;
scroll_position_current: integer;
scroll_position_total: integer;
screen_count: integer;
begin
inherited;
date_start := dtpDateStart.Value;
dc := GetDC (tvDidTimeTree.handle);
r := tvDidTimeTree.boundsRect;
bmp_total := tBitmap.create;
bmp_total.width := r.Right - r.Left - sbxVertical.Width - 3;
h := r.Bottom - r.Top - tvDidTimeTree.Header.Height;
if h < 0 then
h := 0;
SendMessage(tvDidTimeTree.Handle, WM_VSCROLL, SB_BOTTOM, 0);
scroll_position_total := GetScrollPos(tvDidTimeTree.Handle, sb_Vert);
screen_count := round(scroll_position_total / h);
bmp_total.Height := h * (screen_count + 1);
bmp_current_position := 0;
SendMessage(tvDidTimeTree.Handle, WM_VSCROLL, SB_TOP, 0);
tvDidTimeTree.Repaint;
while (scroll_position_current < scroll_position_total) do
begin
r := tvDidTimeTree.boundsRect;
dc := GetDC (tvDidTimeTree.handle);
bmp_current := tBitmap.create;
bmp_current.width := r.Right - r.Left - sbxVertical.Width - 3;
bmp_current.Height := h - 5;
Bitblt(bmp_total.canvas.handle, 0, bmp_current_position, bmp_current.Width, bmp_current.height, dc, r.left, r.top, srccopy);
scroll_position_current := GetScrollPos(tvDidTimeTree.Handle, sb_Vert);
SendMessage(tvDidTimeTree.Handle, WM_VSCROLL, SB_PAGEDOWN, 0);
tvDidTimeTree.Repaint;
bmp_current_position := bmp_current_position + bmp_current.Height;
end;
spdSaveGraph.DefaultExt := 'bmp';
spdSaveGraph.FileName := DateToStr(date_start) + '—' + DateToStr(IncMonth(date_start, 1)) + '.bmp';
spdSaveGraph.Filter := 'Bitmap Picture|*.bmp';
if spdSaveGraph.Execute then
bmp_total.saveToFile (spdSaveGraph.FileName);
bmp_total.free;
end;
In Firemonkey we can use a TShadowEffect to draw a nice looking shadow.
This shadow also adjusts its opacity and translucency so it displays the correct component beneath it if a control is overlapping.
Without TShadowEffect:
With TShadowEffect:
Is there a way to draw the same shadow effect in VCL forms without embedding a FMX form?
My idea was to create a TGraphicControl and place it underneath the target control. The shadow control will stick to the target control. The steps of drawing the shadow are as follow:
We create an off screen Bitmap and draw a RoundRect
Then apply Gaussian Blur convolution kernel:
see http://www.concepto.ch/delphi/uddf/pages/graphics.htm#graphics9 (unit GBlur2). (EDIT: Link is dead)
Finally we make it 32 bit alpha semi transparent gray scale. depending on the amount of darkness:
And draw it via AlphaBlend on the TGraphicControl canvas.
GBlur2.pas (Author unknown)
unit GBlur2;
interface
uses
Windows, Graphics;
type
PRGBTriple = ^TRGBTriple;
TRGBTriple = packed record
b: byte; {easier to type than rgbtBlue}
g: byte;
r: byte;
end;
PRow = ^TRow;
TRow = array[0..1000000] of TRGBTriple;
PPRows = ^TPRows;
TPRows = array[0..1000000] of PRow;
const
MaxKernelSize = 100;
type
TKernelSize = 1..MaxKernelSize;
TKernel = record
Size: TKernelSize;
Weights: array[-MaxKernelSize..MaxKernelSize] of single;
end;
{the idea is that when using a TKernel you ignore the Weights except
for Weights in the range -Size..Size.}
procedure GBlur(theBitmap: TBitmap; radius: double);
implementation
uses
SysUtils;
procedure MakeGaussianKernel(var K: TKernel; radius: double; MaxData, DataGranularity: double);
{makes K into a gaussian kernel with standard deviation = radius. For the current application
you set MaxData = 255 and DataGranularity = 1. Now the procedure sets the value of K.Size so
that when we use K we will ignore the Weights that are so small they can't possibly matter. (Small
Size is good because the execution time is going to be propertional to K.Size.)}
var
j: integer;
temp, delta: double;
KernelSize: TKernelSize;
begin
for j := Low(K.Weights) to High(K.Weights) do
begin
temp := j / radius;
K.Weights[j] := exp(-temp * temp / 2);
end;
{now divide by constant so sum(Weights) = 1:}
temp := 0;
for j := Low(K.Weights) to High(K.Weights) do
temp := temp + K.Weights[j];
for j := Low(K.Weights) to High(K.Weights) do
K.Weights[j] := K.Weights[j] / temp;
{now discard (or rather mark as ignorable by setting Size) the entries that are too small to matter.
This is important, otherwise a blur with a small radius will take as long as with a large radius...}
KernelSize := MaxKernelSize;
delta := DataGranularity / (2 * MaxData);
temp := 0;
while (temp < delta) and (KernelSize > 1) do
begin
temp := temp + 2 * K.Weights[KernelSize];
dec(KernelSize);
end;
K.Size := KernelSize;
{now just to be correct go back and jiggle again so the sum of the entries we'll be using is exactly 1}
temp := 0;
for j := -K.Size to K.Size do
temp := temp + K.Weights[j];
for j := -K.Size to K.Size do
K.Weights[j] := K.Weights[j] / temp;
end;
function TrimInt(Lower, Upper, theInteger: integer): integer;
begin
if (theInteger <= Upper) and (theInteger >= Lower) then
result := theInteger
else if theInteger > Upper then
result := Upper
else
result := Lower;
end;
function TrimReal(Lower, Upper: integer; x: double): integer;
begin
if (x < upper) and (x >= lower) then
result := trunc(x)
else if x > Upper then
result := Upper
else
result := Lower;
end;
procedure BlurRow(var theRow: array of TRGBTriple; K: TKernel; P: PRow);
var
j, n: integer;
tr, tg, tb: double; {tempRed, etc}
w: double;
begin
for j := 0 to High(theRow) do
begin
tb := 0;
tg := 0;
tr := 0;
for n := -K.Size to K.Size do
begin
w := K.Weights[n];
{the TrimInt keeps us from running off the edge of the row...}
with theRow[TrimInt(0, High(theRow), j - n)] do
begin
tb := tb + w * b;
tg := tg + w * g;
tr := tr + w * r;
end;
end;
with P[j] do
begin
b := TrimReal(0, 255, tb);
g := TrimReal(0, 255, tg);
r := TrimReal(0, 255, tr);
end;
end;
Move(P[0], theRow[0], (High(theRow) + 1) * Sizeof(TRGBTriple));
end;
procedure GBlur(theBitmap: TBitmap; radius: double);
var
Row, Col: integer;
theRows: PPRows;
K: TKernel;
ACol: PRow;
P: PRow;
begin
if (theBitmap.HandleType <> bmDIB) or (theBitmap.PixelFormat <> pf24Bit) then
raise exception.Create('GBlur only works for 24-bit bitmaps');
MakeGaussianKernel(K, radius, 255, 1);
GetMem(theRows, theBitmap.Height * SizeOf(PRow));
GetMem(ACol, theBitmap.Height * SizeOf(TRGBTriple));
{record the location of the bitmap data:}
for Row := 0 to theBitmap.Height - 1 do
theRows[Row] := theBitmap.Scanline[Row];
{blur each row:}
P := AllocMem(theBitmap.Width * SizeOf(TRGBTriple));
for Row := 0 to theBitmap.Height - 1 do
BlurRow(Slice(theRows[Row]^, theBitmap.Width), K, P);
{now blur each column}
ReAllocMem(P, theBitmap.Height * SizeOf(TRGBTriple));
for Col := 0 to theBitmap.Width - 1 do
begin
{first read the column into a TRow:}
for Row := 0 to theBitmap.Height - 1 do
ACol[Row] := theRows[Row][Col];
BlurRow(Slice(ACol^, theBitmap.Height), K, P);
{now put that row, um, column back into the data:}
for Row := 0 to theBitmap.Height - 1 do
theRows[Row][Col] := ACol[Row];
end;
FreeMem(theRows);
FreeMem(ACol);
ReAllocMem(P, 0);
end;
end.
ShadowBox.pas
unit ShadowBox;
interface
uses Messages, Windows, SysUtils, Classes, Controls, Graphics, StdCtrls;
type
TShadowBox = class(TGraphicControl)
private
FControl: TControl;
FControlWndProc: TWndMethod;
procedure SetControl(AControl: TControl);
procedure ControlWndProc(var Message: TMessage);
procedure AdjustBounds;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
procedure Paint; override;
public
destructor Destroy; override;
published
property Control: TControl read FControl write SetControl;
end;
implementation
uses GBlur2;
destructor TShadowBox.Destroy;
begin
SetControl(nil);
inherited;
end;
procedure TShadowBox.SetControl(AControl: TControl);
begin
if AControl = Self then Exit;
if FControl <> AControl then
begin
if FControl <> nil then
begin
FControl.WindowProc := FControlWndProc;
FControl.RemoveFreeNotification(Self);
end;
FControl := AControl;
if FControl <> nil then
begin
FControlWndProc := FControl.WindowProc;
FControl.WindowProc := ControlWndProc;
FControl.FreeNotification(Self);
end else
FControlWndProc := nil;
if FControl <> nil then
begin
Parent := FControl.Parent;
AdjustBounds;
end;
end;
end;
procedure TShadowBox.ControlWndProc(var Message: TMessage);
begin
if Assigned(FControlWndProc) then
FControlWndProc(Message);
case Message.Msg of
CM_VISIBLECHANGED:
Visible := FControl.Visible;
WM_WINDOWPOSCHANGED:
begin
if Parent <> FControl.Parent then
Parent := FControl.Parent;
AdjustBounds;
end;
end;
end;
procedure TShadowBox.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FControl) then
begin
FControl := nil;
FControlWndProc := nil;
end;
end;
procedure TShadowBox.AdjustBounds;
begin
if FControl <> nil then
begin
SetBounds(FControl.Left - 8, FControl.Top - 8, FControl.Width + 16, FControl.Height + 16);
if FControl is TWinControl then
BringToFront
else
SendToBack;
end;
end;
procedure PrepareBitmap32Shadow(Bitmap: TBitmap; Darkness: Byte=100);
var
I, J: Integer;
Pixels: PRGBQuad;
Color: COLORREF;
begin
for I := 0 to Bitmap.Height - 1 do
begin
Pixels := PRGBQuad(Bitmap.ScanLine[I]);
for J := 0 to Bitmap.Width - 1 do
begin
with Pixels^ do
begin
Color := RGB(rgbRed, rgbGreen, rgbBlue);
case Color of
$FFFFFF: rgbReserved := 0; // white = transparent
$000000: rgbReserved := 255; // black = opaque
else
rgbReserved := 255 - ((rgbRed + rgbGreen + rgbBlue) div 3); // intensity of semi transparent
end;
rgbRed := Darkness; rgbGreen := Darkness; rgbBlue := Darkness; // darkness
// pre-multiply the pixel with its alpha channel
rgbRed := (rgbRed * rgbReserved) div $FF;
rgbGreen := (rgbGreen * rgbReserved) div $FF;
rgbBlue := (rgbBlue * rgbReserved) div $FF;
end;
Inc(Pixels);
end;
end;
end;
{$IFDEF VER130} // D5
const
AC_SRC_ALPHA = $01;
{$ENDIF}
procedure TShadowBox.Paint;
var
Bitmap: TBitmap;
BlendFunction: TBlendFunction;
begin
Bitmap := TBitmap.Create;
try
Bitmap.PixelFormat := pf24bit;
Bitmap.Width := Width;
Bitmap.Height := Height;
Bitmap.Canvas.Pen.Color := clBlack;
Bitmap.Canvas.Brush.Color := clBlack;
Bitmap.Canvas.RoundRect(5, 5, Width - 5, Height - 5, 10, 10);
GBlur(Bitmap, 3); // Radius
Bitmap.PixelFormat := pf32bit;
Bitmap.IgnorePalette := True;
Bitmap.HandleType := bmDIB;
PrepareBitmap32Shadow(Bitmap, 150); // Darkness
BlendFunction.BlendOp := AC_SRC_OVER;
BlendFunction.BlendFlags := 0;
BlendFunction.SourceConstantAlpha := 255;
BlendFunction.AlphaFormat := AC_SRC_ALPHA;
Windows.AlphaBlend(
Canvas.Handle, // HDC hdcDest
0, // int xoriginDest
0, // int yoriginDest
Bitmap.Width, // int wDest
Bitmap.Height, // int hDest
Bitmap.Canvas.Handle, // HDC hdcSrc
0, // int xoriginSrc
0, // int yoriginSrc
Bitmap.Width, // int wSrc
Bitmap.Height, // int hSrc
BlendFunction); // BLENDFUNCTION
finally
Bitmap.Free;
end;
end;
end.
Usage:
uses ShadowBox;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
with TShadowBox.Create(Self) do
Control := Edit1;
with TShadowBox.Create(Self) do
Control := Shape1;
with TShadowBox.Create(Self) do
Control := Panel1;
end;
How can I replace color on TCanvas on Delphi XE2? The following code works incredibly slow:
for y := ARect.Top to ARect.Top + ARect.Height - 1 do
for x := ARect.Left to ARect.Left + ARect.Width - 1 do
if Canvas.Pixels[x, y] = FixedColor then
Canvas.Pixels[x, y] := Canvas.Pixels[ARect.Left, ARect.Top];
Here are two function (with and without tolerance) to replace the color:
Bonus:
Code to test the functions also provided. Load your image in a TImage control, then use the MouseUp event to change the color under mouse.
procedure ReplaceColor(BMP: TBitmap; OldColor, NewColor: TColor);
VAR
x, y: Integer;
R,G,B: Byte;
R_,G_,B_: Byte;
aPixel: PRGBTriple;
begin
R:= GetRValue(OldColor);
G:= GetGValue(OldColor);
B:= GetBValue(OldColor);
R_:= GetRValue(NewColor);
G_:= GetGValue(NewColor);
B_:= GetBValue(NewColor);
BMP.PixelFormat := pf24bit;
for y := 0 to BMP.Height - 1 do
for x := 0 to BMP.Width - 1 do
begin
aPixel := BMP.ScanLine[y];
Inc(aPixel, x);
if (aPixel^.rgbtRed = R)
AND (aPixel^.rgbtGreen = G)
AND (aPixel^.rgbtBlue = B) then
begin
aPixel^.rgbtRed := R_;
aPixel^.rgbtGreen := G_;
aPixel^.rgbtBlue := B_;
end;
end;
end;
procedure ReplaceColor(BMP: TBitmap; OldColor, NewColor: TColor; ToleranceR, ToleranceG, ToleranceB: Byte);
VAR
x, y: Integer;
R,G,B: Byte;
R_,G_,B_: Byte;
aPixel: PRGBTriple;
begin
R:= GetRValue(OldColor);
G:= GetGValue(OldColor);
B:= GetBValue(OldColor);
R_:= GetRValue(NewColor);
G_:= GetGValue(NewColor);
B_:= GetBValue(NewColor);
BMP.PixelFormat := pf24bit;
for y := 0 to BMP.Height - 1 do
for x := 0 to BMP.Width - 1 do
begin
aPixel := BMP.ScanLine[y];
Inc(aPixel, x);
if (abs(aPixel^.rgbtRed - R)< ToleranceR)
AND (abs(aPixel^.rgbtGreen- G)< ToleranceG)
AND (abs(aPixel^.rgbtBlue - B)< ToleranceB) then
begin
aPixel^.rgbtRed := R_;
aPixel^.rgbtGreen := G_;
aPixel^.rgbtBlue := B_;
end;
end;
end;
procedure TfrmTester.imgOnMouseUp(Sender: TObject; Shift: TShiftState; X, Y: Integer);
VAR
PixelClr: TColor;
BMP: TBitmap;
begin
// Collect the new color, under mouse pointer
PixelClr:= imgReplaceOrig.Picture.Bitmap.Canvas.Pixels[x, y];
if PixelClr< 0 then EXIT;
Label1.Caption := 'x'+IntToStr(X)+':y='
+ IntToStr(Y)
+' r'+ IntToStr(GetRValue(Pixel))
+', g'+ IntToStr(GetGValue(Pixel))
+', b'+ IntToStr(GetBValue(Pixel));
BMP:= TBitmap.Create;
BMP.Assign(imgReplaceOrig.Picture.Bitmap);
// Replace the color
cGraphUtil.ReplaceColor(BMP, PixelClr, clBlue, 44, 44, 44);
imgReplace.Picture.Assign(BMP);
FreeAndNil(BMP);
end;
var
aBitmap: TBitmap;
x, y: Integer;
aPixel: PRGBTriple;
...
aBitmap := TBitmap.Create;
try
aBitmap.PixelFormat := pf24bit;
aBitmap.Height := ARect.Height;
aBitmap.Width := ARect.Width;
aBitmap.Canvas.CopyRect(TRect.Create(0, 0, aBitmap.Width, aBitmap.Height), Canvas, ARect);
for y := 0 to aBitmap.Height - 1 do
for x := 0 to aBitmap.Width - 1 do
begin
aPixel := aBitmap.ScanLine[y];
Inc(aPixel, x);
if (aPixel^.rgbtRed = GetRValue(FixedColor)) and (aPixel^.rgbtGreen = GetGValue(FixedColor)) and (aPixel^.rgbtBlue = GetBValue(FixedColor)) then
aPixel^ := PRGBTriple(aBitmap.ScanLine[y])^;
end;
Canvas.Draw(ARect.Left, ARect.Top, aBitmap);
finally
aBitmap.Free;
end;
Once upon a time, reading this question, I wondered how to rotate a bitmap by any degree without fiddling around with all the bits myself. Recently, someone else had obvious difficulties with it too.
There are already many questions dealing with rotation at 90° intervals, most notabaly this one, but I want to rotate by a real angle. Preferably with the possibility to adjust the image size due to the rotation, and with setting a custom (transparent) background color for the parts that will be added to image surface. I then suppose the signature of the routine would look something like:
procedure RotateBitmap(Bmp: TBitmap; Angle: Single; AdjustSize: Boolean;
BackColor: TColor);
These answers mention the following candidates for constructing this routine: SetWorldTransform, PlgBlt, GDI+, but I would like to see an (efficient) implementation.
tl;dr; Use GDI+
SetWorldTransform
With WinAPI's SetWorldTransform you can transform the space of device context: rotate, shear, offset, and scale. This is done by setting the members of a transform matrix of type XFORM. Fill its members according the documentation.
procedure RotateBitmap(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
BkColor: TColor = clNone);
var
C: Single;
S: Single;
XForm: tagXFORM;
Tmp: TBitmap;
begin
C := Cos(Rads);
S := Sin(Rads);
XForm.eM11 := C;
XForm.eM12 := S;
XForm.eM21 := -S;
XForm.eM22 := C;
Tmp := TBitmap.Create;
try
Tmp.TransparentColor := Bmp.TransparentColor;
Tmp.TransparentMode := Bmp.TransparentMode;
Tmp.Transparent := Bmp.Transparent;
Tmp.Canvas.Brush.Color := BkColor;
if AdjustSize then
begin
Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
XForm.eDx := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
XForm.eDy := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end
else
begin
Tmp.Width := Bmp.Width;
Tmp.Height := Bmp.Height;
XForm.eDx := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
XForm.eDy := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end;
SetGraphicsMode(Tmp.Canvas.Handle, GM_ADVANCED);
SetWorldTransform(Tmp.Canvas.Handle, XForm);
BitBlt(Tmp.Canvas.Handle, 0, 0, Tmp.Width, Tmp.Height, Bmp.Canvas.Handle,
0, 0, SRCCOPY);
Bmp.Assign(Tmp);
finally
Tmp.Free;
end;
end;
PlgBlt
The PlgBlt function performs a bit-block transfer from the specified rectangle in the source device context to the specified parallelogram in the destination device context. Map the corner points of the source image via the lpPoint parameter.
procedure RotateBitmap(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
BkColor: TColor = clNone);
var
C: Single;
S: Single;
Tmp: TBitmap;
OffsetX: Single;
OffsetY: Single;
Points: array[0..2] of TPoint;
begin
C := Cos(Rads);
S := Sin(Rads);
Tmp := TBitmap.Create;
try
Tmp.TransparentColor := Bmp.TransparentColor;
Tmp.TransparentMode := Bmp.TransparentMode;
Tmp.Transparent := Bmp.Transparent;
Tmp.Canvas.Brush.Color := BkColor;
if AdjustSize then
begin
Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
OffsetX := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
OffsetY := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end
else
begin
Tmp.Width := Bmp.Width;
Tmp.Height := Bmp.Height;
OffsetX := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
OffsetY := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end;
Points[0].X := Round(OffsetX);
Points[0].Y := Round(OffsetY);
Points[1].X := Round(OffsetX + Bmp.Width * C);
Points[1].Y := Round(OffsetY + Bmp.Width * S);
Points[2].X := Round(OffsetX - Bmp.Height * S);
Points[2].Y := Round(OffsetY + Bmp.Height * C);
PlgBlt(Tmp.Canvas.Handle, Points, Bmp.Canvas.Handle, 0, 0, Bmp.Width,
Bmp.Height, 0, 0, 0);
Bmp.Assign(Tmp);
finally
Tmp.Free;
end;
end;
Graphics32
Graphics32 is a library especially designed for fast bitmap handling. It requires some experience to grasp its full potential, but the documentation as well as the provided examples should get you started.
A rotation of a TBitmap32 image is done by transforming it by one of the many available transformation classes. The TAffineTransformation class is needed here. First, shift the image half its size to the upper left, then rotate, and shift the result back to the lower right, possibly using the new image dimensions.
uses
GR32, GR32_Transforms;
procedure RotateBitmap(Bmp: TBitmap32; Degs: Integer; AdjustSize: Boolean;
BkColor: TColor = clNone; Transparent: Boolean = False); overload;
var
Tmp: TBitmap32;
Transformation: TAffineTransformation;
begin
Tmp := TBitmap32.Create;
Transformation := TAffineTransformation.Create;
try
Transformation.BeginUpdate;
Transformation.SrcRect := FloatRect(0, 0, Bmp.Width, Bmp.Height);
Transformation.Translate(-0.5 * Bmp.Width, -0.5 * Bmp.Height);
Transformation.Rotate(0, 0, -Degs);
if AdjustSize then
with Transformation.GetTransformedBounds do
Tmp.SetSize(Round(Right - Left), Round(Bottom - Top))
else
Tmp.SetSize(Bmp.Width, Bmp.Height);
Transformation.Translate(0.5 * Tmp.Width, 0.5 * Tmp.Height);
Transformation.EndUpdate;
Tmp.Clear(Color32(BkColor));
if not Transparent then
Bmp.DrawMode := dmTransparent;
Transform(Tmp, Bmp, Transformation);
Bmp.Assign(Tmp);
Bmp.OuterColor := Color32(BkColor);
if Transparent then
Bmp.DrawMode := dmTransparent;
finally
Transformation.Free;
Tmp.Free;
end;
end;
procedure RotateBitmap(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
BkColor: TColor = clNone); overload;
var
Tmp: TBitmap32;
Transparent: Boolean;
begin
Tmp := TBitmap32.Create;
try
Transparent := Bmp.Transparent;
Tmp.Assign(Bmp);
RotateBitmapGR32(Tmp, Degs, AdjustSize, BkColor, Transparent);
Bmp.Assign(Tmp);
if Transparent then
Bmp.Transparent := True;
finally
Tmp.Free;
end;
end;
GDI+
Introduced in Windows XP, Microsoft's GDI+ API is more efficient then the default GDI API. For Delphi 2009 and up, the library is available from here. For older Delphi versions, the library is available from here.
In GDI+ the rotation is also done by a transformation matrix. Drawing works quite differently though. Create a TGPGraphics object and attach it to a device context with its constructor. Subsequently, drawing operations on the object are translated by the API and will be output to the destination context.
uses
GDIPOBJ, GDIPAPI; // < D2009
GdiPlus; // >= D2009
procedure RotateBitmap(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
BkColor: TColor = clNone);
var
Tmp: TGPBitmap;
Matrix: TGPMatrix;
C: Single;
S: Single;
NewSize: TSize;
Graphs: TGPGraphics;
P: TGPPointF;
begin
Tmp := TGPBitmap.Create(Bmp.Handle, Bmp.Palette);
Matrix := TGPMatrix.Create;
try
Matrix.RotateAt(Degs, MakePoint(0.5 * Bmp.Width, 0.5 * Bmp.Height));
if AdjustSize then
begin
C := Cos(DegToRad(Degs));
S := Sin(DegToRad(Degs));
NewSize.cx := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
NewSize.cy := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
Bmp.Width := NewSize.cx;
Bmp.Height := NewSize.cy;
end;
Graphs := TGPGraphics.Create(Bmp.Canvas.Handle);
try
Graphs.Clear(ColorRefToARGB(ColorToRGB(BkColor)));
Graphs.SetTransform(Matrix);
Graphs.DrawImage(Tmp, (Cardinal(Bmp.Width) - Tmp.GetWidth) div 2,
(Cardinal(Bmp.Height) - Tmp.GetHeight) div 2);
finally
Graphs.Free;
end;
finally
Matrix.Free;
Tmp.Free;
end;
end;
Handling transparency
The routines above preserve the transparent settings of the fead bitmap, with the exception of the Graphics32 solution which requires an additional Transparent parameter.
Performance and image quality
I wrote a test application (see full code below) to tune the performance of the various methods and to compare the resulting image quality.
The first and most important conclusion is that GDI+ uses anti-aliasing where the others do not, resulting in the best image quality. (I unsuccessfully tried to prevent anti-aliasing by setting CompositingQuality, InterpolationMode, SmoothingMode, and PixelOffsetMode, so when anti-aliasing is not preferred, do not use GDI+.)
Furthermore, the GDI+ solution is also the fastest method, by far.
unit RotateTestForm;
interface
uses
Windows, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls,
JPEG, Math, GR32, GR32_Transforms, GDIPOBJ, GDIPAPI {, GdiPlus};
type
TTestForm = class(TForm)
private
FImage: TImage;
FOpenDialog: TOpenDialog;
procedure FormPaint(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
end;
var
TestForm: TTestForm;
implementation
{$R *.dfm}
procedure RotateBitmapSWT(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
BkColor: TColor = clNone);
var
C: Single;
S: Single;
XForm: TXForm;
Tmp: TBitmap;
begin
C := Cos(Rads);
S := Sin(Rads);
XForm.eM11 := C;
XForm.eM12 := S;
XForm.eM21 := -S;
XForm.eM22 := C;
Tmp := TBitmap.Create;
try
Tmp.TransparentColor := Bmp.TransparentColor;
Tmp.TransparentMode := Bmp.TransparentMode;
Tmp.Transparent := Bmp.Transparent;
Tmp.Canvas.Brush.Color := BkColor;
if AdjustSize then
begin
Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
XForm.eDx := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
XForm.eDy := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end
else
begin
Tmp.Width := Bmp.Width;
Tmp.Height := Bmp.Height;
XForm.eDx := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
XForm.eDy := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end;
SetGraphicsMode(Tmp.Canvas.Handle, GM_ADVANCED);
SetWorldTransform(Tmp.Canvas.Handle, XForm);
BitBlt(Tmp.Canvas.Handle, 0, 0, Tmp.Width, Tmp.Height, Bmp.Canvas.Handle,
0, 0, SRCCOPY);
Bmp.Assign(Tmp);
finally
Tmp.Free;
end;
end;
procedure RotateBitmapPLG(Bmp: TBitmap; Rads: Single; AdjustSize: Boolean;
BkColor: TColor = clNone);
var
C: Single;
S: Single;
Tmp: TBitmap;
OffsetX: Single;
OffsetY: Single;
Points: array[0..2] of TPoint;
begin
C := Cos(Rads);
S := Sin(Rads);
Tmp := TBitmap.Create;
try
Tmp.TransparentColor := Bmp.TransparentColor;
Tmp.TransparentMode := Bmp.TransparentMode;
Tmp.Transparent := Bmp.Transparent;
Tmp.Canvas.Brush.Color := BkColor;
if AdjustSize then
begin
Tmp.Width := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
Tmp.Height := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
OffsetX := (Tmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
OffsetY := (Tmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end
else
begin
Tmp.Width := Bmp.Width;
Tmp.Height := Bmp.Height;
OffsetX := (Bmp.Width - Bmp.Width * C + Bmp.Height * S) / 2;
OffsetY := (Bmp.Height - Bmp.Width * S - Bmp.Height * C) / 2;
end;
Points[0].X := Round(OffsetX);
Points[0].Y := Round(OffsetY);
Points[1].X := Round(OffsetX + Bmp.Width * C);
Points[1].Y := Round(OffsetY + Bmp.Width * S);
Points[2].X := Round(OffsetX - Bmp.Height * S);
Points[2].Y := Round(OffsetY + Bmp.Height * C);
PlgBlt(Tmp.Canvas.Handle, Points, Bmp.Canvas.Handle, 0, 0, Bmp.Width,
Bmp.Height, 0, 0, 0);
Bmp.Assign(Tmp);
finally
Tmp.Free;
end;
end;
procedure RotateBitmapGR32(Bmp: TBitmap32; Degs: Integer; AdjustSize: Boolean;
BkColor: TColor = clNone; Transparent: Boolean = False); overload;
var
Tmp: TBitmap32;
Transformation: TAffineTransformation;
begin
Tmp := TBitmap32.Create;
Transformation := TAffineTransformation.Create;
try
Transformation.BeginUpdate;
Transformation.SrcRect := FloatRect(0, 0, Bmp.Width, Bmp.Height);
Transformation.Translate(-0.5 * Bmp.Width, -0.5 * Bmp.Height);
Transformation.Rotate(0, 0, -Degs);
if AdjustSize then
with Transformation.GetTransformedBounds do
Tmp.SetSize(Round(Right - Left), Round(Bottom - Top))
else
Tmp.SetSize(Bmp.Width, Bmp.Height);
Transformation.Translate(0.5 * Tmp.Width, 0.5 * Tmp.Height);
Transformation.EndUpdate;
Tmp.Clear(Color32(BkColor));
if not Transparent then
Bmp.DrawMode := dmTransparent;
Transform(Tmp, Bmp, Transformation);
Bmp.Assign(Tmp);
Bmp.OuterColor := Color32(BkColor);
if Transparent then
Bmp.DrawMode := dmTransparent;
finally
Transformation.Free;
Tmp.Free;
end;
end;
procedure RotateBitmapGR32(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
BkColor: TColor = clNone); overload;
var
Tmp: TBitmap32;
Transparent: Boolean;
begin
Tmp := TBitmap32.Create;
try
Transparent := Bmp.Transparent;
Tmp.Assign(Bmp);
RotateBitmapGR32(Tmp, Degs, AdjustSize, BkColor, Transparent);
Bmp.Assign(Tmp);
if Transparent then
Bmp.Transparent := True;
finally
Tmp.Free;
end;
end;
procedure RotateBitmapGDIP(Bmp: TBitmap; Degs: Integer; AdjustSize: Boolean;
BkColor: TColor = clNone);
var
Tmp: TGPBitmap;
Matrix: TGPMatrix;
C: Single;
S: Single;
NewSize: TSize;
Graphs: TGPGraphics;
P: TGPPointF;
begin
Tmp := TGPBitmap.Create(Bmp.Handle, Bmp.Palette);
Matrix := TGPMatrix.Create;
try
Matrix.RotateAt(Degs, MakePoint(0.5 * Bmp.Width, 0.5 * Bmp.Height));
if AdjustSize then
begin
C := Cos(DegToRad(Degs));
S := Sin(DegToRad(Degs));
NewSize.cx := Round(Bmp.Width * Abs(C) + Bmp.Height * Abs(S));
NewSize.cy := Round(Bmp.Width * Abs(S) + Bmp.Height * Abs(C));
Bmp.Width := NewSize.cx;
Bmp.Height := NewSize.cy;
end;
Graphs := TGPGraphics.Create(Bmp.Canvas.Handle);
try
Graphs.Clear(ColorRefToARGB(ColorToRGB(BkColor)));
Graphs.SetTransform(Matrix);
Graphs.DrawImage(Tmp, (Cardinal(Bmp.Width) - Tmp.GetWidth) div 2,
(Cardinal(Bmp.Height) - Tmp.GetHeight) div 2);
finally
Graphs.Free;
end;
finally
Matrix.Free;
Tmp.Free;
end;
end;
{ TTestForm }
constructor TTestForm.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Font.Name := 'Tahoma';
Top := 0;
ClientWidth := 560;
ClientHeight := 915;
Show;
FImage := TImage.Create(Self);
FOpenDialog := TOpenDialog.Create(Self);
FOpenDialog.Title := 'Select an small sized image (min. 100 x 100)';
FOpenDialog.Options := FOpenDialog.Options + [ofFileMustExist];
FOpenDialog.Filter := 'JPEG|*.JPG|BMP|*.BMP';
if FOpenDialog.Execute then
begin
FImage.Picture.LoadFromFile(FOpenDialog.FileName);
OnPaint := FormPaint;
Invalidate;
end
else
Application.Terminate;
end;
procedure TTestForm.FormPaint(Sender: TObject);
var
Img: TBitmap;
Bmp: TBitmap;
Bmp32: TBitmap32;
BkColor: TColor;
AdjustSize: Boolean;
Degs: Integer;
Rads: Single;
RotCount: Integer;
I: Integer;
Tick: Cardinal;
begin
Img := TBitmap.Create;
Bmp := TBitmap.Create;
Bmp32 := TBitmap32.Create;
try
BkColor := clBtnFace;
Img.Canvas.Brush.Color := BkColor;
Img.Width := 100;
Img.Height := 100;
Img.Canvas.Draw(0, 0, FImage.Picture.Graphic);
AdjustSize := False;
Degs := 45;
Rads := DegToRad(Degs);
RotCount := 1000;
Canvas.TextOut(10, 10, 'Original:');
Canvas.Draw(10, 30, Img);
Canvas.TextOut(10, 140, Format('Size = %d x %d', [Img.Width, Img.Height]));
Canvas.TextOut(10, 160, Format('Angle = %d°', [Degs]));
Canvas.TextOut(10, 250, Format('%d rotations:', [RotCount]));
Canvas.TextOut(120, 10, 'SetWorldTransform:');
Bmp.Assign(Img);
RotateBitmapSWT(Bmp, Rads, AdjustSize, BkColor);
Canvas.Draw(120, 30, Bmp);
if not AdjustSize then
begin
Tick := GetTickCount;
for I := 0 to RotCount - 2 do
RotateBitmapSWT(Bmp, Rads, AdjustSize, BkColor);
Canvas.TextOut(120, 250, Format('%d msec', [GetTickCount - Tick]));
Canvas.Draw(120, 140, Bmp);
end;
Canvas.TextOut(230, 10, 'PlgBlt:');
Bmp.Assign(Img);
RotateBitmapPLG(Bmp, Rads, AdjustSize, BkColor);
Canvas.Draw(230, 30, Bmp);
if not AdjustSize then
begin
Tick := GetTickCount;
for I := 0 to RotCount - 2 do
RotateBitmapPLG(Bmp, Rads, AdjustSize, BkColor);
Canvas.TextOut(230, 250, Format('%d msec', [GetTickCount - Tick]));
Canvas.Draw(230, 140, Bmp);
end;
Canvas.TextOut(340, 10, 'Graphics32:');
Bmp.Assign(Img);
RotateBitmapGR32(Bmp, Degs, AdjustSize, BkColor);
Canvas.Draw(340, 30, Bmp);
if not AdjustSize then
begin
Tick := GetTickCount;
for I := 0 to RotCount - 2 do
RotateBitmapGR32(Bmp, Degs, AdjustSize, BkColor);
Canvas.TextOut(340, 250, Format('%d msec', [GetTickCount - Tick]));
Canvas.Draw(340, 140, Bmp);
// Without in between conversion to TBitmap:
Bmp32.Assign(Img);
Tick := GetTickCount;
for I := 0 to RotCount - 1 do
RotateBitmapGR32(Bmp32, Degs, AdjustSize, BkColor, False);
Canvas.TextOut(340, 270, Format('%d msec (optimized)',
[GetTickCount - Tick]));
end;
Canvas.TextOut(450, 10, 'GDI+ :');
Bmp.Assign(Img);
RotateBitmapGDIP(Bmp, Degs, AdjustSize, BkColor);
Canvas.Draw(450, 30, Bmp);
if not AdjustSize then
begin
Tick := GetTickCount;
for I := 0 to RotCount - 2 do
RotateBitmapGDIP(Bmp, Degs, AdjustSize, BkColor);
Canvas.TextOut(450, 250, Format('%d msec', [GetTickCount - Tick]));
Canvas.Draw(450, 140, Bmp);
end;
finally
Bmp32.Free;
Bmp.Free;
Img.Free;
OnPaint := nil;
end;
end;
end.
If someone is looking into image rotation, they might also take a look at the Mitov video library (free for non-commercial use: link). VCL and FireMonkey. It takes care of all the low-level details, which lets us avoid the kind of detailed coding that NGLN's excellent answer explores.
We've been using it for the past two years and have been very happy with it in our commercial app.
It has a rotate component that works with static images and video streams. Their library is fully-multi-tasking, optionally using all the cores and low level primitives available, on Intel chipsets with Intel's own performance library (http://software.intel.com/en-us/articles/intel-ipp)
On moderate hardware we can run multiple video or bmp streams which we rotate, clip, scale, and process at the pixel level, in real-time.
I'm using the code posted in this link TSplitter enhanced with grab bar , to draw a grab bar in a splitter control,
procedure TSplitter.Paint;
var
R: TRect;
X, Y: integer;
DX, DY: integer;
i: integer;
Brush: TBitmap;
begin
R := ClientRect;
Canvas.Brush.Color := Color;
Canvas.FillRect(ClientRect);
X := (R.Left+R.Right) div 2;
Y := (R.Top+R.Bottom) div 2;
if (Align in [alLeft, alRight]) then
begin
DX := 0;
DY := 3;
end else
begin
DX := 3;
DY := 0;
end;
dec(X, DX*2);
dec(Y, DY*2);
Brush := TBitmap.Create;
try
Brush.SetSize(2, 2);
Brush.Canvas.Brush.Color := clBtnHighlight;
Brush.Canvas.FillRect(Rect(0,0,1,1));
Brush.Canvas.Pixels[0, 0] := clBtnShadow;
for i := 0 to 4 do
begin
Canvas.Draw(X, Y, Brush);
inc(X, DX);
inc(Y, DY);
end;
finally
Brush.Free;
end;
end;
the code works nicely but when I enabled the vcl styles, the colors used to draw the the splitter and the grab bar doesn´t fit the used by the vcl styles.
How I can draw the TSplitter using the Vcl style colors of the current theme?
The system color constants which uses the code (clBtnFace, clBtnHighlight, clBtnShadow) not store the vcl styles colors, you must use the StyleServices.GetSystemColor function to translate these to the vcl styles colors.
procedure TSplitter.Paint;
var
R: TRect;
X, Y: integer;
DX, DY: integer;
i: integer;
Brush: TBitmap;
begin
R := ClientRect;
if TStyleManager.IsCustomStyleActive then
Canvas.Brush.Color := StyleServices.GetSystemColor(clBtnFace)
else
Canvas.Brush.Color := Color;
Canvas.FillRect(ClientRect);
X := (R.Left+R.Right) div 2;
Y := (R.Top+R.Bottom) div 2;
if (Align in [alLeft, alRight]) then
begin
DX := 0;
DY := 3;
end else
begin
DX := 3;
DY := 0;
end;
dec(X, DX*2);
dec(Y, DY*2);
Brush := TBitmap.Create;
try
Brush.SetSize(2, 2);
if TStyleManager.IsCustomStyleActive then
Brush.Canvas.Brush.Color := StyleServices.GetSystemColor(clBtnHighlight)
else
Brush.Canvas.Brush.Color := clBtnHighlight;
Brush.Canvas.FillRect(Rect(0, 0, Brush.Height, Brush.Width));
if TStyleManager.IsCustomStyleActive then
Brush.Canvas.Pixels[0, 0] := StyleServices.GetSystemColor(clBtnShadow)
else
Brush.Canvas.Pixels[0, 0] := clBtnShadow;
for i := 0 to 4 do
begin
Canvas.Draw(X, Y, Brush);
inc(X, DX);
inc(Y, DY);
end;
finally
Brush.Free;
end;
end;