I want to make a kind of multi-color bar in my software. A kind of progressbar, but with two current values.
That's why I need it.
I have some "budget parts", and each one of them has its own limit (100$, 1000$ etc.)
I also have an editing form for adding new bills (and linking bills to budget parts).
In this editor I want to visually represent how full is a budget part, and how much price of current bill affects this budget part.
For example, the whole bar is 100$.
Green part means sum of prices across saved bills, for example 60$.
Yellow part means price of the current bill, which is not saved yet, for example 5$.
Like this:
Of course, values should be set dynamically.
Can you recommend me any components for drawing this (maybe some advanced progressbar, that can display more than one current value?)
As David suggests, just paint it yourself. Just about the same amount of trouble. Drop a TImage where you want your gauge and use something like this:
procedure PaintTwoColorGauge(const BackgroundColor, BorderColor, FirstGaugeColor, SecondGaugeColor: TColor; FirstGaugeValue, SecondGaugeValue, TotalValue: Integer; const Img: TImage);
var B: TBitmap;
ImgWidth, G1Width, G2Width: Integer;
begin
B := TBitmap.Create;
try
B.Width := Img.Width;
B.Height := Img.Height;
B.Canvas.Brush.Color := BackgroundColor;
B.Canvas.Brush.Style := bsSolid;
B.Canvas.Pen.Style := psClear;
B.Canvas.Pen.Width := 1;
B.Canvas.FillRect(Rect(0, 0, B.Width, B.Height));
if TotalValue <> 0 then
begin
ImgWidth := B.Width - 2; // Don't account the width of the borders.
G1Width := (FirstGaugeValue * ImgWidth) div TotalValue;
G2Width := (SecondGaugeValue * ImgWidth) div TotalValue;
if G1Width > ImgWidth then G1Width := ImgWidth; // Just in case
if G2Width > ImgWidth then G2Width := ImgWidth;
if G2Width > G1Width then
begin
B.Canvas.Brush.Color := SecondGaugeColor;
B.Canvas.FillRect(Rect(0, 0, G2Width, B.Height));
B.Canvas.Brush.Color := FirstGaugeColor;
B.Canvas.FillRect(Rect(0, 0, G1Width, B.Height));
end
else
begin
B.Canvas.Brush.Color := FirstGaugeColor;
B.Canvas.FillRect(Rect(0, 0, G1Width, B.Height));
B.Canvas.Brush.Color := SecondGaugeColor;
B.Canvas.FillRect(Rect(0, 0, G2Width, B.Height));
end;
end;
B.Canvas.Pen.Color := BorderColor;
B.Canvas.Pen.Style := psSolid;
B.Canvas.Brush.Style := bsClear;
B.Canvas.Rectangle(0, 0, B.Width, B.Height);
Img.Picture.Assign(B);
finally B.Free;
end;
end;
For example, here's what this code does to my 3 TImages (my images are intentionally shpaed as you see them):
procedure TForm1.FormCreate(Sender: TObject);
begin
PaintTwoColorGauge(clWhite, clBlack, clGreen, clYellow, 50, 55, 100, Image1);
PaintTwoColorGauge(clWhite, clBlack, clGreen, clYellow, 50, 60, 100, Image2);
PaintTwoColorGauge(clWhite, clBlack, clGreen, clYellow, 20, 60, 100, Image3);
end;
Write your own, it's fun! But while not really thát difficult, writing an own component could look like a daunting task. Especially for novice uses or without experience doing so.
Next in line of options is to draw it yourself, and the therefore intended component should "always" be the TPaintBox control. Implement the OnPaint event handler and it redraws itself when needed. Here an example implementation of how to transform such a paint box into a double gauge component:
type
TDoubleGauge = record
BackgroundColor: TColor;
BorderColor: TColor;
Color1: TColor;
Color2: TColor;
Value1: Integer;
Value2: Integer;
MaxValue: Integer;
end;
TForm1 = class(TForm)
PaintBox1: TPaintBox;
procedure FormCreate(Sender: TObject);
procedure PaintBox1Paint(Sender: TObject);
private
FDoubleGauge: TDoubleGauge;
end;
...
procedure TForm1.PaintBox1Paint(Sender: TObject);
var
Box: TPaintBox absolute Sender;
MaxWidth: Integer;
Width1: Integer;
Width2: Integer;
begin
with FDoubleGauge do
begin
Box.Canvas.Brush.Color := BackgroundColor;
Box.Canvas.Pen.Color := BorderColor;
Box.Canvas.Rectangle(0, 0, Box.Width, Box.Height);
if MaxValue <> 0 then
begin
MaxWidth := Box.Width - 2;
Width1 := (MaxWidth * Value1) div MaxValue;
Width2 := (MaxWidth * Value2) div MaxValue;
Box.Canvas.Brush.Color := Color2;
if Abs(Value2) < Abs(MaxValue) then
Box.Canvas.FillRect(Rect(1, 1, Width2, Box.Height - 1));
Box.Canvas.Brush.Color := Color1;
if Abs(Value1) < Abs(Value2) then
Box.Canvas.FillRect(Rect(1, 1, Width1, Box.Height - 1));
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FDoubleGauge.BackgroundColor := clWhite;
FDoubleGauge.BorderColor := clBlack;
FDoubleGauge.Color1 := clGreen;
FDoubleGauge.Color2 := clYellow;
FDoubleGauge.Value1 := 50;
FDoubleGauge.Value2 := 60;
FDoubleGauge.MaxValue := 100;
PaintBox1.Invalidate;
end;
Well, that looks like quite an effort. Especially when there are more of such doudble gauges needed on a single form. Therefore I like Cosmin Prund's answer, because he uses TImage components which are capable of "memorizing" what has to be redrawn when needed. Just as a bonus, here an alternative version of his code (with slightly different behaviour on invalid input):
procedure DrawDoubleGauge(BackgroundColor, BorderColor, Color1, Color2: TColor;
Value1, Value2, MaxValue: Integer; Img: TImage);
var
Width: Integer;
Width1: Integer;
Width2: Integer;
begin
Img.Canvas.Brush.Color := BackgroundColor;
Img.Canvas.Pen.Color := BorderColor;
Img.Canvas.Rectangle(0, 0, Img.Width, Img.Height);
if MaxValue <> 0 then
begin
Width := Img.Width - 2;
Width1 := (Width * Value1) div MaxValue;
Width2 := (Width * Value2) div MaxValue;
Img.Canvas.Brush.Color := Color2;
if Abs(Value2) < Abs(MaxValue) then
Img.Canvas.FillRect(Rect(1, 1, Width2, Img.Height - 1));
Img.Canvas.Brush.Color := Color1;
if Abs(Value1) < Abs(Value2) then
Img.Canvas.FillRect(Rect(1, 1, Width1, Img.Height - 1));
end;
end;
I was also looking for this exactly, as I don't want to spend any money on this I will follow the proposed solution, nevertheless if anyone would like an advanced component I found one that's not too expensive and look pretty decent in my opinion, here's the link in case it could be useful for someone else:
http://www.tmssoftware.com/site/advprogr.asp?s=
Thank's to all.
Related
I want the currency values in the TStringGrid table to have different color decimals. How can do that?
You need to draw the cells yourself by implementing an OnDrawCell handler.
Something like this:
procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
var
Grid: TStringGrid;
S: string;
Val: Double;
FracVal, IntVal: Integer;
FracStr, IntStr: string;
IntW, FracW, W, H: Integer;
Padding: Integer;
const
PowersOfTen: array[0..8] of Integer =
(
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000
);
Decimals = 2;
BgColor = clWhite;
IntColor = clBlack;
FracColor = clRed;
begin
Grid := Sender as TStringGrid;
if (ACol < Grid.FixedCols) or (ARow < Grid.FixedRows) then
Exit;
Grid.Canvas.Brush.Color := BgColor;
Grid.Canvas.FillRect(Rect);
S := Grid.Cells[ACol, ARow];
Padding := Grid.Canvas.TextWidth('0') div 2;
if not TryStrToFloat(S, Val) or not InRange(Val, Integer.MinValue, Integer.MaxValue) then
begin
Grid.Canvas.TextRect(Rect, S, [tfSingleLine, tfVerticalCenter, tfLeft]);
Exit;
end;
IntVal := Trunc(Val);
IntStr := IntVal.ToString;
if Decimals > 0 then
IntStr := IntStr + FormatSettings.DecimalSeparator;
IntW := Grid.Canvas.TextWidth(IntStr);
FracVal := Round(Frac(Abs(Val)) * PowersOfTen[Decimals]);
FracStr := FracVal.ToString.PadRight(Decimals, '0');
if Decimals = 0 then
FracStr := '';
FracW := Grid.Canvas.TextWidth(FracStr);
W := IntW + FracW;
H := Grid.Canvas.TextHeight(IntStr);
if W >= Grid.ColWidths[ACol] - 2*Padding then
begin
S := '###';
Grid.Canvas.TextRect(Rect, S, [tfSingleLine, tfVerticalCenter, tfRight]);
end
else
begin
Grid.Canvas.Font.Color := IntColor;
Grid.Canvas.TextOut(Rect.Right - Padding - W,
Rect.Top + Rect.Height div 2 - H div 2, IntStr);
Grid.Canvas.Font.Color := FracColor;
Grid.Canvas.TextOut(Rect.Right - Padding - FracW,
Rect.Top + Rect.Height div 2 - H div 2, FracStr);
end;
end;
This code will write non-numeric data left-aligned as is. For numeric data, it will draw the values with a fixed number of decimals. You can choose the decimals (0..8), as well as the colours of the integral and fractional parts. If the number doesn't fit in its cell, ### will be displayed instead.
I haven't fully tested the code. I'll leave that to you as an exercise.
Update: Sorry, I forgot you are using Delphi 7. This means that you need to replace IntVal.ToString with IntToStr(IntVal) and so on.
I have an image list containing several bitmaps which I would like to save together as one single bitmap, but I need it saving just like how a spritesheet or tilesheet is drawn in 2d and rpg games etc.
Typically the tilesheet is drawn with several images across (in a row), so for example if I wanted a maximum of 6 images per row, it will only draw 6, with further images been drawn underneath in a new row.
I can save it in one single row like so:
var
CurrentFrame: Integer;
StripWidth: Integer;
Strip: TBitmap;
Bmp: TBitmap;
I: Integer;
begin
if SaveDialog.Execute then
begin
StripWidth := ImageList1.Width * ImageList1.Count - ImageList1.Width;
CurrentFrame := - ImageList1.Width;
Strip := TBitmap.Create;
try
Strip.SetSize(StripWidth, ImageList1.Height);
Bmp := TBitmap.Create;
try
for I := 0 to ImageList1.Count - 1 do
begin
CurrentFrame := CurrentFrame + ImageList1.Width;
ImageList1.GetImage(I, Bmp);
Strip.Canvas.Draw(CurrentFrame, 0, Bmp);
end;
finally
Bmp.Free;
end;
Strip.SaveToFile(SaveDialog.FileName);
finally
Strip.Free;
end;
end;
end;
So imagine the result for the above is:
The result I want is something like:
So the above would have considered in the procedure/ function a parameter to allow only 3 images per row as an example.
How do I export all images from an imagelist into one single bitmap, allowing only x amount if images to be drawn horizontally before creating a new row?
Thanks.
EDIT
Thanks to David's answer, I put together these procedures:
procedure DrawImageOnSheet(Images: TImageList; Sheet: TBitmap;
ImageIndex, X, Y: Integer);
var
Bitmap: TBitmap;
begin
Bitmap := TBitmap.Create;
try
Images.GetBitmap(ImageIndex, Bitmap);
Sheet.Canvas.Draw(X, Y, Bitmap);
finally
Bitmap.Free;
end;
end;
procedure SaveImageListAsSheet(Images: TImageList; FileName: string;
NumberOfColumns: Integer);
var
Sheet: TBitmap;
nImage: Integer;
nCol: Integer;
nRow: Integer;
nToDraw: Integer;
nRemaining: Integer;
ImageIndex: Integer;
X, Y: Integer;
I: Integer;
begin
Sheet := TBitmap.Create;
try
nImage := Images.Count;
nCol := NumberOfColumns;
nRow := (nImage + nCol - 1) div nCol;
Sheet.Height := nRow * Images.Height;
Sheet.Width := nCol * Images.Width;
nRemaining := nImage;
ImageIndex := 0;
Y := 0;
while nRemaining > 0 do
begin
nToDraw := Math.Min(nRemaining, nCol);
X := 0;
for I := 0 to nToDraw - 1 do
begin
DrawImageOnSheet(Images, Sheet, ImageIndex, X, Y);
Inc(ImageIndex);
Inc(X, Images.Width);
end;
Inc(Y, Images.Height);
Dec(nRemaining, nToDraw);
end;
Sheet.SaveToFile(FileName);
finally
Sheet.Free;
end;
end;
According to clarification from the comments, you are struggling with the counting of the images, the organisation of the rows/columns and so on. So, let's assume you already have this function which draws image ImageIndex to the output bitmap at a position of X, Y.
procedure Draw(ImageIndex, X, Y: Integer);
Let's also assume that the images have dimensions given by ImageWidth and ImageHeight. Finally, there are nImage images and you want to have nCol images per column.
So, first of all, how many rows do you need?
nRow := (nImage + nCol - 1) div nCol;
Now you can set the size of the output bitmap. Its width is nCol * ImageWidth and its height is nRow * ImageHeight.
Now to draw the images.
nRemaining := nImage;
ImageIndex := 0;
Y := 0;
while nRemaining > 0 do
begin
nToDraw := Math.Min(nRemaining, nCol);
X := 0;
for i := 0 to nToDraw - 1 do
begin
Draw(ImageIndex, X, Y);
inc(ImageIndex);
inc(X, ImageWidth);
end;
inc(Y, ImageHeight);
dec(nRemaining, nToDraw);
end;
I have ImageList sized 72x72, handle to the icon (HICON), obtained by SHGetFileInfo (for example a large icon sized 32x32). How to add it to this ImageList keeping transparency, but without stretching? Now I draw the icon in the middle of a temporary bitmap desired size, then add it to the ImageList.
SHGetFileInfo(PChar(Path + sr.Name), sr.FindData.dwFileAttributes, fi, SizeOf(fi), SHGFI_ICON or SHGFI_LARGEICON or SHGFI_USEFILEATTRIBUTES);
Bmp:=TBitmap.Create;
Bmp.PixelFormat:=pf32bit;
Bmp.SetSize(72, 72);
DrawIcon(Bmp.Canvas.Handle, 20, 20, fi.hIcon);
iIcon:=ImageList_AddMasked(ilThumbs.Handle, Bmp.Handle, 0);
Bmp.Free;
But I think a way faster exists (without drawing on temporary bitmap). Also image in ImageList loses transparency and when I set index of this Image in ImageList for ListView item.ImageIndex it looks not pretty (when this item is selected, white background around is present). Is any way to solve this problem?
Thanks.
This is the code that I use to perform this task.
Note that I am assuming that the original icon uses 32 bit colour, with alpha channel. That's reasonable in the settings that I use this code, but I can't be sure whether or not it's reasonable for you.
uses
Windows, Graphics;
function CreateIconFromSmallerIcon(IconSize: Integer; SmallerIcon: HICON): HICON;
procedure GetDIBheaderAndBits(bmp: HBITMAP; out bih: BITMAPINFOHEADER; out bits: Pointer);
var
pbih: ^BITMAPINFOHEADER;
bihSize, bitsSize: DWORD;
begin
bits := nil;
GetDIBSizes(bmp, bihSize, bitsSize);
pbih := AllocMem(bihSize);
Try
bits := AllocMem(bitsSize);
GetDIB(bmp, 0, pbih^, bits^);
if pbih.biSize<SizeOf(bih) then begin
FreeMem(bits);
bits := nil;
exit;
end;
bih := pbih^;
Finally
FreeMem(pbih);
End;
end;
procedure InitialiseBitmapInfoHeader(var bih: BITMAPINFOHEADER);
begin
bih.biSize := SizeOf(BITMAPINFOHEADER);
bih.biWidth := IconSize;
bih.biHeight := 2*IconSize;//height of xor bitmap plus height of and bitmap
bih.biPlanes := 1;
bih.biBitCount := 32;
bih.biCompression := BI_RGB;
end;
procedure CreateXORbitmap(const sbih, dbih: BITMAPINFOHEADER; sptr, dptr: PDWORD);
var
line, xOffset, yOffset: Integer;
begin
xOffset := (IconSize-sbih.biWidth) div 2;
yOffset := (IconSize-sbih.biHeight) div 2;
inc(dptr, xOffset + IconSize*yOffset);
for line := 0 to sbih.biHeight-1 do begin
Move(sptr^, dptr^, sbih.biWidth*SizeOf(DWORD));
inc(dptr, IconSize);//relies on the fact that no padding is needed for RGBA scanlines
inc(sptr, sbih.biWidth);//likewise
end;
end;
var
SmallerIconInfo: TIconInfo;
sBits, xorBits: PDWORD;
xorScanSize, andScanSize: Integer;
xorBitsSize, andBitsSize: Integer;
sbih: BITMAPINFOHEADER;
dbih: ^BITMAPINFOHEADER;
resbitsSize: DWORD;
resbits: Pointer;
begin
Result := 0;
Try
if not GetIconInfo(SmallerIcon, SmallerIconInfo) then begin
exit;
end;
Try
GetDIBheaderAndBits(SmallerIconInfo.hbmColor, sbih, Pointer(sBits));
if Assigned(sBits) then begin
Try
if (sbih.biWidth>IconSize) or (sbih.biHeight>IconSize) or (sbih.biPlanes<>1) or (sbih.biBitCount<>32) then begin
exit;
end;
xorScanSize := BytesPerScanline(IconSize, 32, 32);
Assert(xorScanSize=SizeOf(DWORD)*IconSize);
andScanSize := BytesPerScanline(IconSize, 1, 32);
xorBitsSize := IconSize*xorScanSize;
andBitsSize := IconSize*andScanSize;
resbitsSize := SizeOf(BITMAPINFOHEADER) + xorBitsSize + andBitsSize;
resbits := AllocMem(resbitsSize);//AllocMem zeroises the memory
Try
dbih := resbits;
InitialiseBitmapInfoHeader(dbih^);
xorBits := resbits;
inc(PByte(xorBits), SizeOf(BITMAPINFOHEADER));
CreateXORbitmap(sbih, dbih^, sBits, xorBits);
//don't need to fill in the mask bitmap when using RGBA
Result := CreateIconFromResourceEx(resbits, resbitsSize, True, $00030000, IconSize, IconSize, LR_DEFAULTCOLOR);
Finally
FreeMem(resbits);
End;
Finally
FreeMem(sBits);
End;
end;
Finally
if SmallerIconInfo.hbmMask<>0 then begin
DeleteObject(SmallerIconInfo.hbmMask);
end;
if SmallerIconInfo.hbmColor<>0 then begin
DeleteObject(SmallerIconInfo.hbmColor);
end;
End;
Finally
DestroyIcon(SmallerIcon);
End;
end;
Ok, my solution below:
procedure SetAlpha(Bitmap: TBitmap);
type
PPixelRec = ^TPixelRec;
TPixelRec = packed record
B, G, R, Alpha: Byte;
end;
var
X, Y: Integer;
Pixel: PPixelRec;
begin
for Y := 0 to (Bitmap.Height - 1) do
begin
Pixel := Bitmap.ScanLine[Y];
for X := 0 to (Bitmap.Width - 1) do
begin
Pixel.Alpha:=255;
Inc(Pixel);
end;
end;
end;
//skipped
var Bmp: TBitmap;
fi: TSHFileInfo;
ImageList1: TImageList;
begin
ImageList1:=TImageList.CreateSize(72, 72);
ImageList1.DrawingStyle:=dsTransparent;
ImageList1.ColorDepth:=cd32Bit;
SHGetFileInfo('c:\Windows\notepad.exe', FILE_ATTRIBUTE_NORMAL, fi, SizeOf(fi), SHGFI_ICON or SHGFI_LARGEICON or SHGFI_USEFILEATTRIBUTES);
Bmp:=TBitmap.Create;
Bmp.SetSize(72, 72);
SetAlpha(Bmp);
Bmp.Canvas.Brush.Color:=clWhite;
Bmp.Canvas.FillRect(Rect(0, 0, 72, 72));
DrawIcon(Bmp.Canvas.Handle, 20, 20, fi.hIcon);
fi.iIcon:=ImageList1.Add(Bmp, nil);
ImageList1.Draw(Canvas, 0, 0, fi.iIcon); //just to see that's alright
end;
When designing a form in the IDE, non-visual components (eg TMainMenus, TDatamodules) can be freely placed and positioned. The position is persisted, so that on reloading the form these components appear in the correct place.
But, TComponent does not have Top or Left properties!
So, how can my code access the 'designed position' of non visual components?
This can be accessed at runtime, but it's sort of a hack. (Mostly because it's implemented as sort of a hack.)
The Left and Top properties are set up as Word-size values, and the two of them are packed together into a Longint called TComponent.FDesignInfo. You can obtain its value with the DesignInfo property. Have a look at TComponent.DefineProperties to get a look into how it's used.
And also:
How to set DesignInfo to a point like (-100,-100)?
Objective: Put the icon out of visual area, hide it on design-time.
Note: It is very usefull when for example creating simple visual components derived directly from TComponent, i have in mind a very simple label (taht is allways aligned to top, has allways left=0, top is auto-calculated, bla bla bla) that only stores it's caption property into the .dfm file; and also any localizer will only see that caption property.
SOLUTION is to Override ReadState with code like this:
procedure TMyComponent.ReadState(Reader:TReader);
var
NewDesignInfo:LongRec;
begin
inherited ReadState(Reader);
NewDesignInfo.Hi:=Word(-100); // Hide design-time icon (top position = -100)
NewDesignInfo.Lo:=Word(-100); // Hide design-time icon (left position = -100)
DesignInfo:=Longint(NewDesignInfo); // Set the design-icon position out of visual area
end;
Hope help others!
This worked for me. Source: CnPack CnAlignSizeWizard.pas.
procedure SetNonVisualPos(Form: TCustomForm; Component: TComponent; X, Y: Integer);
const
NonvisualClassNamePattern = 'TContainer';
csNonVisualSize = 28;
csNonVisualCaptionSize = 14;
csNonVisualCaptionV = 30;
var
P: TSmallPoint;
H1, H2: HWND;
Offset: TPoint;
function HWndIsNonvisualComponent(hWnd: hWnd): Boolean;
var
AClassName: array[0..256] of Char;
begin
AClassName[GetClassName(hWnd, #AClassName, SizeOf(AClassName) - 1)] := #0;
Result := string(AClassName) = NonvisualClassNamePattern;
end;
procedure GetComponentContainerHandle(AForm: TCustomForm; L, T: Integer; var H1, H2: hWnd; var Offset: TPoint);
var
R1, R2: TRect;
P: TPoint;
ParentHandle: hWnd;
AControl: TWinControl;
I: Integer;
begin
ParentHandle := AForm.Handle;
AControl := AForm;
if AForm.ClassNameIs('TDataModuleForm') then // ÊÇ DataModule
begin
for I := 0 to AForm.ControlCount - 1 do
if AForm.Controls[I].ClassNameIs('TComponentContainer')
and (AForm.Controls[I] is TWinControl) then
begin
AControl := AForm.Controls[I] as TWinControl;
ParentHandle := AControl.Handle;
Break;
end;
end;
H2 := 0;
H1 := GetWindow(ParentHandle, GW_CHILD);
H1 := GetWindow(H1, GW_HWNDLAST);
while H1 <> 0 do
begin
if HWndIsNonvisualComponent(H1) and GetWindowRect(H1, R1) then
begin
P.x := R1.Left;
P.y := R1.Top;
P := AControl.ScreenToClient(P);
if (P.x = L) and (P.y = T) and (R1.Right - R1.Left = csNonVisualSize)
and (R1.Bottom - R1.Top = csNonVisualSize) then
begin
H2 := GetWindow(ParentHandle, GW_CHILD);
H2 := GetWindow(H2, GW_HWNDLAST);
while H2 <> 0 do
begin
if HWndIsNonvisualComponent(H2) and GetWindowRect(H2, R2) then
begin
if (R2.Top - R1.Top = csNonVisualCaptionV) and (Abs(R2.Left + R2.Right - R1.Left - R1.Right) <= 1)
and (R2.Bottom - R2.Top = csNonVisualCaptionSize) then
begin
Offset.x := R2.Left - R1.Left;
Offset.y := R2.Top - R1.Top;
Break;
end;
end;
H2 := GetWindow(H2, GW_HWNDPREV);
end;
Exit;
end;
end;
H1 := GetWindow(H1, GW_HWNDPREV);
end;
end;
begin
P := TSmallPoint(Component.DesignInfo);
GetComponentContainerHandle(Form, P.x, P.y, H1, H2, Offset);
Component.DesignInfo := Integer(PointToSmallPoint(Point(X, Y)));
if H1 <> 0 then
SetWindowPos(H1, 0, X, Y, 0, 0, SWP_NOSIZE or SWP_NOZORDER);
if H2 <> 0 then
SetWindowPos(H2, 0, X + Offset.x, Y + Offset.y, 0, 0, SWP_NOSIZE or SWP_NOZORDER);
end;
Use sample:
SetNonVisualPos(TCustomForm(Designer.Root),MyComponent,10,10);
How to merge two PNG's together? I know that you can't use PNGObject.Draw because it doesn't copy alpha transaperncy (I am not sure but it doesn't work anyway) so custom procedure/function is needed. I didn't come with empty hands, I have this procedure:
procedure MergePNGLayer(Layer1,Layer2: TPNGObject; Const aLeft,aTop:Integer);
var
x, y: Integer;
SL1, SL2, SLBlended : pRGBLine;
aSL1, aSL2, aSLBlended: PByteArray;
blendCoeff: single;
blendedPNG, Lay2buff: TPNGObject;
begin
blendedPNG:=TPNGObject.Create;
blendedPNG.Assign(Layer1);
Lay2buff:=TPNGObject.Create;
Lay2buff.Assign(Layer2);
SetPNGCanvasSize(Layer2,Layer1.Width,Layer1.Height,aLeft,aTop);
for y := 0 to Layer1.Height-1 do
begin
SL1 := Layer1.Scanline[y];
SL2 := Layer2.Scanline[y];
aSL1 := Layer1.AlphaScanline[y];
aSL2 := Layer2.AlphaScanline[y];
SLBlended := blendedPNG.Scanline[y];
aSLBlended := blendedPNG.AlphaScanline[y];
for x := 0 to Layer1.Width-1 do
begin
blendCoeff:=aSL1[x] * 100/255/100;
aSLBlended[x] := round(aSL2[x] + (aSL1[x]-aSL2[x]) * blendCoeff);
SLBlended[x].rgbtRed := round(SL2[x].rgbtRed + (SL1[x].rgbtRed-SL2[x].rgbtRed) * blendCoeff);
SLBlended[x].rgbtGreen := round(SL2[x].rgbtGreen + (SL1[x].rgbtGreen-SL2[x].rgbtGreen) * blendCoeff);
SLBlended[x].rgbtBlue := round(SL2[x].rgbtBlue + (SL1[x].rgbtBlue-SL2[x].rgbtBlue) * blendCoeff);
end;
end;
Layer1.Assign(blendedPNG);
Layer2.Assign(Lay2buff);
blendedPNG.Free;
Lay2buff.Free;
end;
But sadly it doesn't work how it should, it does the job but not right. When it merges empty image with loaded image, it works fine, but when both images aren't empty, it makes them loss transparancy.
procedure TForm1.FormClick(Sender: TObject);
var
PNG1, PNG2, PNG3, Dest: TPNGObject;
begin
PNG1 := TPNGObject.Create;
PNG2 := TPNGObject.Create;
PNG3 := TPNGObject.Create;
PNG1.LoadFromFile('Aero\TopLeft.png');//Width 10
PNG2.LoadFromFile('Aero\Top.png');//Width 200
PNG3.LoadFromFile('Aero\TopRight.png');//Width 10
Dest := TPNGObject.CreateBlank(COLOR_RGBALPHA, 16, 220, 10);
MergePNGLayer(Dest, PNG1, 0, 0);
MergePNGLayer(Dest, PNG2, 10, 0);
MergePNGLayer(Dest, PNG3, 210, 0);
Dest.SaveToFile('C:\OUT.PNG');
end;
Wanted result:
Actual result:
I am not sure if you can see differences between these imgaes, but you should open these in PNG editor software and you will see the difference. So I need other procedure to merge PNGs. I am using newest version of PNGImage by the way.
Thanks and have a good day!
This seems to work just fine:
procedure DrawPngWithAlpha(Src, Dest: TPNGObject; const R: TRect);
var
X, Y: Integer;
Alpha: PByte;
begin
Src.Draw(Dest.Canvas, R);
// I have no idea why standard implementation of TPNGObject.Draw doesn't apply transparency.
for Y := R.Top to R.Bottom - 1 do
for X := R.Left to R.Right - 1 do
begin
Alpha := #Dest.AlphaScanline[Y]^[X];
Alpha^ := Min(255, Alpha^ + Src.AlphaScanline[Y - R.Top]^[X - R.Left]);
end;
end;