Related
I have a ImgView32, that is anchored to all form margins. The form is maximized.
The bitmap of ImgView is not fixed (it can be of different sizes)
I am trying to draw a line on a transparent layer using ther code from this question:Drawing lines on layer
Now the problem is that, using that exact code, I can only draw in the top-left corner, like in this image:
As you can observe, the lines can be drawn only in the left top corner.
If I try to add some value to the Start and End Points, the whole thing goes crazy. So I must find a way to translate the points in such a fashion that, the user will be able to draw only inside of the center rect (visible in the image)
I am out of ideas.
Please help
Here is the whole unit:
unit MainU;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,GR32, GR32_Image, GR32_Layers, GR32_Backends, GR32_PNG, StdCtrls,
ExtCtrls;
type
TForm5 = class(TForm)
ImgView: TImgView32;
Button1: TButton;
Memo: TMemo;
Edit3: TEdit;
Button2: TButton;
RadioGroup1: TRadioGroup;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure ImgViewPaintStage(Sender: TObject; Buffer: TBitmap32;
StageNum: Cardinal);
procedure ImgViewResize(Sender: TObject);
private
{ Private declarations }
FStartPoint, FEndPoint: TPoint;
FDrawingLine: boolean;
bm32: TBitmap32;
BL : TBitmapLayer;
FSelection: TPositionedLayer;
public
{ Public declarations }
procedure AddLineToLayer;
procedure AddCircleToLayer;
procedure SwapBuffers32;
procedure LayerMouseDown(Sender: TObject; Buttons: TMouseButton;Shift: TShiftState; X, Y: Integer);
procedure LayerMouseUp(Sender: TObject; Buttons: TMouseButton;Shift: TShiftState; X, Y: Integer);
procedure LayerMouseMove(Sender: TObject; Shift: TShiftState;X, Y: Integer);
procedure LayerOnPaint(Sender: TObject; Buffer: TBitmap32);
procedure SetSelection(Value: TPositionedLayer);
property Selection: TPositionedLayer read FSelection write SetSelection;
Procedure SelectGraficLayer(idu:string);
procedure AddTransparentPNGlayer;
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
var
imwidth: integer;
imheight: integer;
OffsX, OffsY: Integer;
const
penwidth = 3;
pencolor = clBlue; // Needs to be a VCL color!
procedure TForm5.AddLineToLayer;
begin
bm32.Canvas.Pen.Color := pencolor;
bm32.Canvas.Pen.Width := penwidth;
bm32.Canvas.MoveTo(FStartPoint.X, FStartPoint.Y);
bm32.Canvas.LineTo(FEndPoint.X, FEndPoint.Y);
end;
procedure TForm5.FormCreate(Sender: TObject);
var
P: TPoint;
W, H: Single;
begin
imwidth := Form5.ImgView.Width;
imheight := Form5.ImgView.Height;
with ImgView.PaintStages[0]^ do
begin
if Stage = PST_CLEAR_BACKGND then Stage := PST_CUSTOM;
end;
bm32 := TBitmap32.Create;
bm32.DrawMode := dmTransparent;
bm32.SetSize(imwidth,imheight);
bm32.Canvas.Pen.Width := penwidth;
bm32.Canvas.Pen.Color := pencolor;
with ImgView do
begin
Selection := nil;
Layers.Clear;
Scale := 1;
Scaled := True;
Bitmap.DrawMode := dmTransparent;
Bitmap.SetSize(imwidth, imheight);
Bitmap.Canvas.Pen.Width := 4;//penwidth;
Bitmap.Canvas.Pen.Color := clBlue;
Bitmap.Canvas.FrameRect(Rect(20, 20, imwidth-20, imheight-20));
Bitmap.Canvas.TextOut(15, 32, 'ImgView');
end;
AddTransparentPNGLayer;
BL := TBitmapLayer.Create(ImgView.Layers);
try
BL.Bitmap.DrawMode := dmTransparent;
BL.Bitmap.SetSize(imwidth,imheight);
BL.Bitmap.Canvas.Pen.Width := penwidth;
BL.Bitmap.Canvas.Pen.Color := pencolor;
BL.Location := GR32.FloatRect(0, 0, imwidth, imheight);
BL.Scaled := False;
BL.OnMouseDown := LayerMouseDown;
BL.OnMouseUp := LayerMouseUp;
BL.OnMouseMove := LayerMouseMove;
BL.OnPaint := LayerOnPaint;
except
Edit3.Text:=IntToStr(BL.Index);
BL.Free;
raise;
end;
FDrawingLine := false;
SwapBuffers32;
end;
procedure TForm5.FormDestroy(Sender: TObject);
begin
bm32.Free;
BL.Free;
end;
procedure TForm5.ImgViewPaintStage(Sender: TObject; Buffer: TBitmap32;
StageNum: Cardinal);
const //0..1
Colors: array [Boolean] of TColor32 = ($FFFFFFFF, $FFB0B0B0);
var
R: TRect;
I, J: Integer;
OddY: Integer;
TilesHorz, TilesVert: Integer;
TileX, TileY: Integer;
TileHeight, TileWidth: Integer;
begin
TileHeight := 13;
TileWidth := 13;
TilesHorz := Buffer.Width div TileWidth;
TilesVert := Buffer.Height div TileHeight;
TileY := 0;
for J := 0 to TilesVert do
begin
TileX := 0;
OddY := J and $1;
for I := 0 to TilesHorz do
begin
R.Left := TileX;
R.Top := TileY;
R.Right := TileX + TileWidth;
R.Bottom := TileY + TileHeight;
Buffer.FillRectS(R, Colors[I and $1 = OddY]);
Inc(TileX, TileWidth);
end;
Inc(TileY, TileHeight);
end;
end;
procedure TForm5.ImgViewResize(Sender: TObject);
begin
OffsX := (ImgView.ClientWidth - imwidth) div 2;
OffsY := (ImgView.ClientHeight - imheight) div 2;
BL.Location := GR32.FloatRect(OffsX, OffsY, imwidth+OffsX, imheight+OffsY);
end;
procedure TForm5.LayerMouseDown(Sender: TObject; Buttons: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
FStartPoint := Point(X-OffsX, Y-OffsY);
FDrawingLine := true;
end;
procedure TForm5.LayerMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
if FDrawingLine then
begin
SwapBuffers32;
BL.Bitmap.Canvas.Pen.Color := pencolor;
BL.Bitmap.Canvas.MoveTo(FStartPoint.X-OffsX, FStartPoint.Y-OffsY);
BL.Bitmap.Canvas.LineTo(X-OffsX, Y-OffsY);
end;
end;
procedure TForm5.LayerMouseUp(Sender: TObject; Buttons: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
FDrawingLine := false;
FEndPoint := Point(X-OffsX, Y-OffsY);
AddLineToLayer;
SwapBuffers32;
end;
procedure TForm5.LayerOnPaint(Sender: TObject; Buffer: TBitmap32);
begin
SwapBuffers32;
end;
procedure TForm5.SetSelection(Value: TPositionedLayer);
begin
if Value <> FSelection then
begin
FSelection := Value;
end;
end;
procedure TForm5.SwapBuffers32;
begin
TransparentBlt(
BL.Bitmap.Canvas.Handle, 0, 0, BL.Bitmap.Width, BL.Bitmap.Height,
bm32.Canvas.Handle, 0, 0, bm32.Width, bm32.Height, clWhite);
end;
procedure TForm5.AddTransparentPNGlayer;
var
mypng:TPortableNetworkGraphic32;
B : TBitmapLayer;
P: TPoint;
W, H: Single;
begin
try
mypng := TPortableNetworkGraphic32.Create;
mypng.LoadFromFile('C:\Location\Of\ATransparentPNGFile.png');
B := TBitmapLayer.Create(ImgView.Layers);
with B do
try
mypng.AssignTo(B.Bitmap);
Bitmap.DrawMode := dmBlend;
with ImgView.GetViewportRect do
P := ImgView.ControlToBitmap(GR32.Point((Right + Left) div 2, (Top + Bottom) div 2));
W := Bitmap.Width * 0.5;
H := Bitmap.Height * 0.5;
Location := GR32.FloatRect(P.X - W, P.Y - H, P.X + W, P.Y + H);
Scaled := True;
OnMouseDown := LayerMouseDown;
except
Free;
raise;
end;
Selection := B;
Edit3.Text:=IntToStr(B.Index);
finally
mypng.Free;
end;
end;
end.
What am I doing wrong? Please test the unit above to see what I mean. Remember to add a ImgView and anchor it to all margins, then at runtime, maximize the form and try to draw the lines...
EDIT
In the green image above, there is a rect, more like a square in the middle of it (not very visible) but you can see it if you look closely.
Since my problem might be misunderstood, please take a look at the following image
I need to be able to draw ONLY in the white rectangle (Bitmap) in the middle of the ImgView. I do not know how to explain better.
It is not a solution for me to make the rectangle/Bitmap fit exactly the ImgView, because that is not the point of my project.
Take a look at Paint.net and imagine that my project kind of does the same (except it's not that complex). But the principle is the same: you decide the size of your document/image when you start a new project, then you add different images as layers, you scale and rotate them, and now I want to allow the users to draw lines inside of a special layer (the drawing layer)
But everything happens inside the boundaries of that document size. Like for example in the above image, the size of the document there is A5 (100dpi) scaled at 83%.
So my problem is that I cannot allow the users to draw the lines outside the white rectangle (middle of the screen). So their lines can start in those boundaries and end there.
I know my test unit is not perfectly clean. I pasted some functions used in the main project and quickly removed some parts from them that are not relevant to this example. The AddTransparentPng procedure is there only to allow the testing of adding a transparent image to the ImgView so I can test if the drawing layer is not covering another possible latyer.
(The Scaled property belongs to the layer (B) it's under the 'with B' statement. I removed the With 'ImgView.Bitmap... Location' statement so it would not bother you anymore :) )
Anyway, please do not pay attention to the code that does not affect the drawing of lines. That code is what needs attention.
EDIT
If I set the layer's scaled to true (Scaled:=true) then it messes everything up, like in the image bellow:
I still have to use offsets but a little differently
Thank you
Error one
In LayerMouseMove() you subtract OffsX and OffsY from FStartPoint in BL.Bitmap.Canvas.MoveTo(). FStartPoint was already adjusted in LayerMouseDown(). I told you to "In the three Mouse procs adjust the X and Y arguments only to become X-OffsX and Y-OffsY." Note arguments only Here's LayerMouseMove() corrected:
procedure TForm5.LayerMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
if FDrawingLine then
begin
SwapBuffers32;
BL.Bitmap.Canvas.Pen.Color := pencolor;
// BL.Bitmap.Canvas.MoveTo(FStartPoint.X-OffsX, FStartPoint.Y-OffsY);
BL.Bitmap.Canvas.MoveTo(FStartPoint.X, FStartPoint.Y);
BL.Bitmap.Canvas.LineTo(X-OffsX, Y-OffsY);
end;
end;
Error two
I also told you to add if FDrawingLine then ... condition to LayerMouseUp() to avoid spurious line when the mouse down happens outside of the layer, but mouse up occurs inside. The corrected LayerMouseUp():
procedure TForm5.LayerMouseUp(Sender: TObject; Buttons: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if FDrawingLine then
begin
FDrawingLine := false;
FEndPoint := Point(X-OffsX, Y-OffsY);
AddLineToLayer;
SwapBuffers32;
end;
end;
Error three
The posted code does not perform as your first image shows. The image looks like you would have outcommented the line BL.Location := ... in ImgViewResize(). Possibly you did this because of Error one. Anyway, with ImgViewResize as follows and the other corrections above I get the result as shown in the picture that follows.
procedure TForm5.ImgViewResize(Sender: TObject);
begin
// centering the drawing area
OffsX := (ImgView.ClientWidth - imwidth) div 2;
OffsY := (ImgView.ClientHeight - imheight) div 2;
BL.Location := GR32.FloatRect(OffsX, OffsY, imwidth+OffsX, imheight+OffsY);
end;
Variables imwidth and imheight defines the size of the drawing area. If you change these you need to recalculate OffsX and OffsY and you need to resize the backbuffer bm32 as well.
The lines in the corners indicate the extent of the drawing area (defined by imwidth and imheight) in the middle of the window. It stays the same also when the window is maximized.
Ok, I solved it. Here is the final (relevant) code:
procedure TForm5.ImgViewResize(Sender: TObject);
begin
OffsX := (ImgView.ClientWidth - imwidth) div 2;
OffsY := (ImgView.ClientHeight - imheight) div 2;
BL.Location := GR32.FloatRect(OffsX, OffsY, imwidth+OffsX, imheight+OffsY);
end;
procedure TForm5.SwapBuffers32;
begin
TransparentBlt(
BL.Bitmap.Canvas.Handle, 0, 0, BL.Bitmap.Width, BL.Bitmap.Height,
bm32.Canvas.Handle, 0, 0, bm32.Width, bm32.Height, clWhite);
end;
procedure TForm5.LayerMouseDown(Sender: TObject; Buttons: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
FStartPoint := Point(X-OffsX, Y-OffsY);
FDrawingLine := true;
end;
procedure TForm5.LayerMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
if FDrawingLine then
begin
SwapBuffers32;
BL.Bitmap.Canvas.Pen.Color := pencolor;
BL.Bitmap.Canvas.MoveTo(FStartPoint.X, FStartPoint.Y);
BL.Bitmap.Canvas.LineTo(X-OffsX, Y-OffsY);
end;
end;
procedure TForm5.LayerMouseUp(Sender: TObject; Buttons: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
FDrawingLine := false;
FEndPoint := Point(X-OffsX, Y-OffsY);
AddLineToLayer;
SwapBuffers32;
end;
procedure TForm5.LayerOnPaint(Sender: TObject; Buffer: TBitmap32);
begin
SwapBuffers32;
end;
procedure TForm5.AddLineToLayer;
begin
bm32.Canvas.Pen.Color := pencolor;
bm32.Canvas.Pen.Width := penwidth;
bm32.Canvas.MoveTo(FStartPoint.X, FStartPoint.Y);
bm32.Canvas.LineTo(FEndPoint.X, FEndPoint.Y);
end;
With this code, everything works as expected. The drawing of lines can only happen within the boundaries
Thank you
Well this is my goal.
Use left mouse button to scroll the image, right mouse button to
choose zoom rectangle and doubleclick to restore full zoom.
I have currently tired, so far found its NOT to do with the way i load the images or display the image but something with how it paints. The on-screen image always fills the control's client area regardless of the shape of the form or the source image, so the aspect ratio cannot possibly be preserved. I am not sure how to change this or keep the aspect ratio. Thus giving me a clean nice picture.
I am posting the whole code for my ZImage unit Though i think the problem is either in the Zimage.paint or Zimage.mouseup But figured if you needed to see a function inside one of those it would help to have it all posted.
unit ZImage;
interface
uses
Windows, Messages, SysUtils,jpeg, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls;
type
TZImage = class(TGraphicControl)
private
FBitmap : Tbitmap;
PicRect : TRect;
ShowRect : TRect;
FShowBorder : boolean;
FBorderWidth : integer;
FForceRepaint : boolean;
FMouse : (mNone, mDrag, mZoom);
FProportional : boolean;
FDblClkEnable : boolean;
FLeft :integer;
FRight :integer;
FTop :integer;
FBottom :integer;
startx, starty,
oldx, oldy : integer;
procedure SetShowBorder(s:boolean);
procedure SetBitmap(b:TBitmap);
procedure SetBorderWidth(w:integer);
procedure SetProportional(b:boolean);
protected
procedure Paint; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
public
constructor Create(AOwner:TComponent); override;
destructor Destroy; override;
procedure DblClick; override;
published
procedure zoom(Endleft,EndRight,EndTop,EndBottom:integer);
property ValueLeft : integer read FLeft write FLeft;
property ValueRight : Integer read FRight write FRight;
Property ValueTop : Integer read FTop write FTop;
Property ValueBottom : Integer read FBottom write FBottom;
property ShowBorder : boolean
read FShowBorder
write SetShowBorder default true;
property KeepAspect : boolean
read FProportional
write SetProportional default true;
property Bitmap : TBitmap
read FBitmap
write Setbitmap;
property BorderWidth : integer
read FBorderWidth
write SetBorderWidth default 7;
property ForceRepaint : boolean
read FForceRepaint
write FForceRepaint default true;
property DblClkEnable : boolean
read FDblClkEnable
write FDblClkEnable default False;
property Align;
property Width;
property Height;
property Top;
property Left;
property Visible;
property Hint;
property ShowHint;
end;
procedure Register;
implementation
//This is the basic create options.
constructor TZImage.Create(AOwner:TComponent);
begin
inherited;
FShowBorder:=True;
FBorderWidth:=7;
FMouse:=mNone;
FForceRepaint:=true; //was true
FDblClkEnable:=False;
FProportional:=true; //was true
Width:=100; Height:=100;
FBitmap:=Tbitmap.Create;
FBitmap.Width:=width;
FBitmap.height:=Height;
ControlStyle:=ControlStyle+[csOpaque];
autosize:= false;
//Scaled:=false;
end;
//basic destroy frees the FBitmap
destructor TZImage.Destroy;
begin
FBitmap.Free;
inherited;
end;
//This was a custom zoom i was using to give the automated zoom effect
procedure TZimage.zoom(Endleft,EndRight,EndTop,EndBottom:integer);
begin
while ((Endbottom <> picrect.bottom) or (Endtop <> picrect.top)) or ((endleft <> picrect.left) or (endright <> picrect.right)) do
begin
if picrect.left > endleft then
picrect.left := picrect.left -1;
if picrect.left < endleft then //starting
picrect.left := picrect.left +1;
if picrect.right > endright then //starting
picrect.right := picrect.right -1;
if picrect.right < endright then
picrect.right := picrect.right +1;
if picrect.top > endtop then
picrect.top := picrect.top -1;
if picrect.top < endtop then //starting
picrect.top := picrect.top +1;
if picrect.bottom > endbottom then //starting
picrect.bottom := picrect.bottom -1;
if picrect.bottom < endbottom then
picrect.bottom := picrect.bottom +1;
self.refresh;
end;
end;
//this is the custom paint I know if i put
//Canvas.Draw(0,0,FBitmap); as the methond it displays
//perfect but the zoom option is gone of course and
//i need the Zoom.
procedure TZImage.Paint;
var buf:TBitmap;
coef,asps,aspp:Double;
sz,a : integer;
begin
buf:=TBitmap.Create;
buf.Width:=Width;
buf.Height:=Height;
if not FShowBorder
then ShowRect:=ClientRect
else ShowRect:=Rect(ClientRect.Left,ClientRect.Top,
ClientRect.Right-FBorderWidth,
ClientRect.Bottom-FBorderWidth);
ShowRect:=ClientRect;
with PicRect do begin
if Right=0 then Right:=FBitmap.Width;
if Bottom=0 then Bottom:=FBitmap.Height;
end;
buf.Canvas.CopyMode:=cmSrcCopy;
buf.Canvas.CopyRect(ShowRect,FBitmap.Canvas,PicRect);
Canvas.CopyMode:=cmSrcCopy;
Canvas.Draw(0,0,buf);
buf.Free;
end;
procedure TZImage.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
begin
// if mbLeft<>Button then Exit;
if not PtInRect(ShowRect,Point(X,Y)) and
not PtInRect(Rect(ShowRect.Right,ShowRect.Bottom,
Width,Height),Point(X,Y)) then Exit;
if PtInRect(Rect(ShowRect.Right,ShowRect.Bottom,
Width,Height),Point(X,Y)) then begin
DblClick;
Exit;
end;
//here click is in the picture area only
startx:=x; oldx:=x;
starty:=y; oldy:=y;
if mbRight=Button then begin
MouseCapture:=True;
FMouse:=mZoom;
Canvas.Pen.Mode:=pmNot;
end else begin
FMouse:=mDrag;
Screen.Cursor:=crHandPoint;
end;
end;
function Min(a,b:integer):integer;
begin
if a<b then Result:=a else Result:=b;
end;
function Max(a,b:integer):integer;
begin
if a<b then Result:=b else Result:=a;
end;
procedure TZImage.MouseMove(Shift: TShiftState; X, Y: Integer);
var d,s:integer;
coef:Double;
begin
if FMouse=mNone then Exit;
if FMouse=mZoom then begin
Canvas.DrawFocusRect(Rect(Min(startx,oldx),Min(starty,oldy),Max(startx,oldx),Max(starty,oldy)));
oldx:=x; oldy:=y;
Canvas.DrawFocusRect(Rect(Min(startx,oldx),Min(starty,oldy),Max(startx,oldx),Max(starty,oldy)));
end;
if FMouse=mDrag then begin
//horizontal movement
coef:=(PicRect.Right-PicRect.Left)/(ShowRect.Right-ShowRect.Left);
d:=Round(coef*(x-oldx));
s:=PicRect.Right-PicRect.Left;
if d>0 then begin
if PicRect.Left>=d then begin
PicRect.Left:=PicRect.Left-d;
PicRect.Right:=PicRect.Right-d;
end else begin
PicRect.Left:=0;
PicRect.Right:=PicRect.Left+s;
end;
end;
if d<0 then begin
if PicRect.Right<FBitmap.Width+d then begin
PicRect.Left:=PicRect.Left-d;
PicRect.Right:=PicRect.Right-d;
end else begin
PicRect.Right:=FBitmap.Width;
PicRect.Left:=PicRect.Right-s;
end;
end;
//vertical movement
coef:=(PicRect.Bottom-PicRect.Top)/(ShowRect.Bottom-ShowRect.Top);
d:=Round(coef*(y-oldy));
s:=PicRect.Bottom-PicRect.Top;
if d>0 then begin
if PicRect.Top>=d then begin
PicRect.Top:=PicRect.Top-d;
PicRect.Bottom:=PicRect.Bottom-d;
end else begin
PicRect.Top:=0;
PicRect.Bottom:=PicRect.Top+s;
end;
end;
{There was a bug in the fragment below. Thanks to all, who reported this bug to me}
if d<0 then begin
if PicRect.Bottom<FBitmap.Height+d then begin
PicRect.Top:=PicRect.Top-d;
PicRect.Bottom:=PicRect.Bottom-d;
end else begin
PicRect.Bottom:=FBitmap.Height;
PicRect.Top:=PicRect.Bottom-s;
end;
end;
oldx:=x; oldy:=y;
if FForceRepaint then Repaint
else Invalidate;
end;
end;
procedure TZImage.MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var coef:Double;
t:integer;
left,right,top,bottom : integer;
begin
if FMouse=mNone then Exit;
if x>ShowRect.Right then x:=ShowRect.Right;
if y>ShowRect.Bottom then y:=ShowRect.Bottom;
if FMouse=mZoom then begin //calculate new PicRect
t:=startx;
startx:=Min(startx,x);
x:=Max(t,x);
t:=starty;
starty:=Min(starty,y);
y:=Max(t,y);
FMouse:=mNone;
MouseCapture:=False;
//enable the following if you want to zoom-out by dragging in the opposite direction}
{ if Startx>x then begin
DblClick;
Exit;
end;}
if Abs(x-startx)<5 then Exit;
//showmessage('picrect Left='+inttostr(picrect.Left)+' right='+inttostr(picrect.Right)+' top='+inttostr(picrect.Top)+' bottom='+inttostr(picrect.Bottom));
//startx and start y is teh starting x/y of the selected area
//x and y is the ending x/y of the selected area
if (x - startx < y - starty) then
begin
while (x - startx < y - starty) do
begin
x := x + 100;
startx := startx - 100;
end;
end
else if (x - startx > y - starty) then
begin
while (x - startx > y - starty) do
begin
y := y + 100;
starty := starty - 100;
end;
end;
//picrect is the size of whole area
//PicRect.top and left are 0,0
//IFs were added in v.1.2 to avoid zero-divide
if (PicRect.Right=PicRect.Left)
then
coef := 100000
else
coef:=ShowRect.Right/(PicRect.Right-PicRect.Left); //if new screen coef= 1
left:=Round(PicRect.Left+startx/coef);
Right:=Left+Round((x-startx)/coef);
if (PicRect.Bottom=PicRect.Top)
then
coef := 100000
else
coef:=ShowRect.Bottom/(PicRect.Bottom-PicRect.Top);
Top:=Round(PicRect.Top+starty/coef);
Bottom:=Top+Round((y-starty)/coef);
//showmessage(inttostr(left)+' '+inttostr(Right)+' '+inttostr(top)+' '+inttostr(bottom));
zoom(left,right,top,bottom);
ValueLeft := left;
ValueRight := Right;
ValueTop := top;
ValueBottom := bottom;
end;
if FMouse=mDrag then begin
FMouse:=mNone;
Canvas.Pen.Mode:=pmCopy;
Screen.Cursor:=crDefault;
end;
Invalidate;
end;
procedure TZImage.DblClick;
begin
zoom(0,FBitMap.Width,0,FBitMap.Height);
ValueLeft := 0;
ValueRight := FBitMap.Width;
ValueTop := 0;
ValueBottom := FBitMap.Height;
//PicRect:=Rect(0,0,FBitmap.Width,FBitmap.Height);
Invalidate;
end;
procedure TZImage.SetBitmap(b:TBitmap);
begin
FBitmap.Assign(b);
PicRect:=Rect(0,0,b.Width, b.Height);
Invalidate;
end;
procedure TZImage.SetBorderWidth(w:integer);
begin
FBorderWidth:=w;
Invalidate;
end;
procedure TZImage.SetShowBorder(s:boolean);
begin
FShowBorder:=s;
Invalidate;
end;
procedure TZImage.SetProportional(b:boolean);
begin
FProportional:=b;
Invalidate;
end;
procedure Register;
begin
RegisterComponents('Custom', [TZImage]);
end;
end.
With this code you can register the componet ZImage and see how it runs.. if needed
The question is clear, but I think the problem answering it is how not to rewrite the complete code to be understandable for you. And since I am better at coding then explaining, I did.
I think you are searching for something like the following:
unit ZImage2;
interface
uses
Windows, Messages, Classes, Controls, Graphics, StdCtrls, ExtCtrls, Math;
const
DefAnimDuration = 500;
type
TZImage = class(TGraphicControl)
private
FAlignment: TAlignment;
FAnimDuration: Cardinal;
FAnimRect: TRect;
FAnimStartTick: Cardinal;
FAnimTimer: TTimer;
FBuffer: TBitmap;
FCropRect: TRect;
FImgRect: TRect;
FLayout: TTextLayout;
FPicture: TPicture;
FPrevCropRect: TRect;
FProportional: Boolean;
FProportionalCrop: Boolean;
FScale: Single;
FSelColor: TColor;
FSelecting: Boolean;
FSelPoint: TPoint;
FSelRect: TRect;
procedure Animate(Sender: TObject);
function HasGraphic: Boolean;
procedure PictureChanged(Sender: TObject);
procedure RealignImage;
procedure SetAlignment(Value: TAlignment);
procedure SetLayout(Value: TTextLayout);
procedure SetPicture(Value: TPicture);
procedure SetProportional(Value: Boolean);
procedure UpdateBuffer;
protected
function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; override;
procedure ChangeScale(M: Integer; D: Integer); override;
procedure DblClick; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer); override;
procedure Paint; override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Reset;
function ScreenToGraphic(R: TRect): TRect;
procedure Zoom(const ACropRect: TRect);
procedure ZoomSelection(const ASelRect: TRect);
published
property Alignment: TAlignment read FAlignment write SetAlignment
default taLeftJustify;
property AnimDuration: Cardinal read FAnimDuration write FAnimDuration
default DefAnimDuration;
property Layout: TTextLayout read FLayout write SetLayout default tlTop;
property Picture: TPicture read FPicture write SetPicture;
property Proportional: Boolean read FProportional write SetProportional
default False;
property ProportionalCrop: Boolean read FProportionalCrop
write FProportionalCrop default True;
property SelColor: TColor read FSelColor write FSelColor default clWhite;
published
property Align;
property Anchors;
property AutoSize;
property Color;
end;
implementation
function FitRect(const Boundary: TRect; Width, Height: Integer;
CanGrow: Boolean; HorzAlign: TAlignment; VertAlign: TTextLayout): TRect;
var
W: Integer;
H: Integer;
Scale: Single;
Offset: TPoint;
begin
Width := Max(1, Width);
Height := Max(1, Height);
W := Boundary.Right - Boundary.Left;
H := Boundary.Bottom - Boundary.Top;
if CanGrow then
Scale := Min(W / Width, H / Height)
else
Scale := Min(1, Min(W / Width, H / Height));
Result := Rect(0, 0, Round(Width * Scale), Round(Height * Scale));
case HorzAlign of
taLeftJustify:
Offset.X := 0;
taCenter:
Offset.X := (W - Result.Right) div 2;
taRightJustify:
Offset.X := W - Result.Right;
end;
case VertAlign of
tlTop:
Offset.Y := 0;
tlCenter:
Offset.Y := (H - Result.Bottom) div 2;
tlBottom:
Offset.Y := H - Result.Bottom;
end;
OffsetRect(Result, Boundary.Left + Offset.X, Boundary.Top + Offset.Y);
end;
function NormalizeRect(const Point1, Point2: TPoint): TRect;
begin
Result.Left := Min(Point1.X, Point2.X);
Result.Top := Min(Point1.Y, Point2.Y);
Result.Right := Max(Point1.X, Point2.X);
Result.Bottom := Max(Point1.Y, Point2.Y);
end;
{ TZImage }
procedure TZImage.Animate(Sender: TObject);
var
Done: Single;
begin
Done := (GetTickCount - FAnimStartTick) / FAnimDuration;
if Done >= 1.0 then
begin
FAnimTimer.Enabled := False;
FAnimRect := FCropRect;
end
else
with FPrevCropRect do
FAnimRect := Rect(
Left + Round(Done * (FCropRect.Left - Left)),
Top + Round(Done * (FCropRect.Top - Top)),
Right + Round(Done * (FCropRect.Right - Right)),
Bottom + Round(Done * (FCropRect.Bottom - Bottom)));
UpdateBuffer;
RealignImage;
Invalidate;
end;
function TZImage.CanAutoSize(var NewWidth, NewHeight: Integer): Boolean;
begin
Result := True;
if not (csDesigning in ComponentState) or HasGraphic then
begin
if Align in [alNone, alLeft, alRight] then
NewWidth := Round(FScale * FPicture.Width);
if Align in [alNone, alTop, alBottom] then
NewHeight := Round(FScale * FPicture.Height);
end;
end;
procedure TZImage.ChangeScale(M, D: Integer);
var
SaveAnchors: TAnchors;
begin
SaveAnchors := Anchors;
Anchors := [akLeft, akTop];
FScale := FScale * M / D;
inherited ChangeScale(M, D);
Anchors := SaveAnchors;
end;
constructor TZImage.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csCaptureMouse, csClickEvents, csOpaque, csDoubleClicks];
FAnimTimer := TTimer.Create(Self);
FAnimTimer.Interval := 15;
FAnimTimer.OnTimer := Animate;
FAnimDuration := DefAnimDuration;
FBuffer := TBitmap.Create;
FPicture := TPicture.Create;
FPicture.OnChange := PictureChanged;
FProportionalCrop := True;
FScale := 1.0;
FSelColor := clWhite;
end;
procedure TZImage.DblClick;
begin
if not HasGraphic then
Reset
else
Zoom(Rect(0, 0, FPicture.Width, FPicture.Height));
inherited DblClick;
end;
destructor TZImage.Destroy;
begin
FPicture.Free;
FBuffer.Free;
inherited Destroy;
end;
function TZImage.HasGraphic: Boolean;
begin
Result := (Picture.Width > 0) and (Picture.Height > 0);
end;
procedure TZImage.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
if (Button = mbRight) and HasGraphic and PtInRect(FImgRect, Point(X, Y)) then
begin
FSelPoint.X := X;
FSelPoint.Y := Y;
FSelRect := Rect(X, Y, X, Y);
FSelecting := True;
Canvas.Brush.Color := FSelColor;
Canvas.DrawFocusRect(FSelRect);
end;
inherited MouseDown(Button, Shift, X, Y);
end;
procedure TZImage.MouseMove(Shift: TShiftState; X, Y: Integer);
const
HorzAlign: array[Boolean] of TAlignment = (taLeftJustify, taRightJustify);
VertAlign: array[Boolean] of TTextLayout = (tlTop, tlBottom);
begin
if FSelecting and PtInRect(FImgRect, Point(X, Y)) then
begin
Canvas.DrawFocusRect(FSelRect);
FSelRect := NormalizeRect(FSelPoint, Point(X, Y));
if (not FProportionalCrop) then
FSelRect := FitRect(FSelRect, FPicture.Graphic.Width,
FPicture.Graphic.Height, True, HorzAlign[X < FSelPoint.X],
VertAlign[Y < FSelPoint.Y]);
Canvas.DrawFocusRect(FSelRect);
end;
inherited MouseMove(Shift, X, Y);
end;
procedure TZImage.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
if FSelecting then
begin
FSelecting := False;
Canvas.DrawFocusRect(FSelRect);
if (Abs(X - FSelPoint.X) > Mouse.DragThreshold) or
(Abs(Y - FSelPoint.Y) > Mouse.DragThreshold) then
ZoomSelection(FSelRect);
end;
inherited MouseUp(Button, Shift, X, Y);
end;
procedure TZImage.Paint;
begin
Canvas.Brush.Color := Color;
if HasGraphic then
begin
Canvas.StretchDraw(FImgRect, FBuffer);
if FSelecting then
Canvas.DrawFocusRect(FSelRect);
with FImgRect do
ExcludeClipRect(Canvas.Handle, Left, Top, Right, Bottom);
end;
Canvas.FillRect(Canvas.ClipRect);
end;
procedure TZImage.PictureChanged(Sender: TObject);
begin
Reset;
end;
procedure TZImage.RealignImage;
begin
if not HasGraphic then
FImgRect := Rect(0, 0, 0, 0)
else if FProportional then
FImgRect := ClientRect
else
FImgRect := FitRect(ClientRect, FBuffer.Width, FBuffer.Height, True,
FAlignment, FLayout);
end;
procedure TZImage.Reset;
begin
FCropRect := Rect(0, 0, FPicture.Width, FPicture.Height);
FAnimRect := FCropRect;
UpdateBuffer;
RealignImage;
Invalidate;
end;
procedure TZImage.Resize;
begin
RealignImage;
inherited Resize;
end;
function TZImage.ScreenToGraphic(R: TRect): TRect;
var
CropWidth: Integer;
CropHeight: Integer;
ImgWidth: Integer;
ImgHeight: Integer;
begin
CropWidth := FCropRect.Right - FCropRect.Left;
CropHeight := FCropRect.Bottom - FCropRect.Top;
ImgWidth := FImgRect.Right - FImgRect.Left;
ImgHeight := FImgRect.Bottom - FImgRect.Top;
IntersectRect(R, R, FImgRect);
OffsetRect(R, -FImgRect.Left, -FImgRect.Top);
Result := Rect(
FCropRect.Left + Round(CropWidth * (R.Left / ImgWidth)),
FCropRect.Top + Round(CropHeight * (R.Top / ImgHeight)),
FCropRect.Left + Round(CropWidth * (R.Right / ImgWidth)),
FCropRect.Top + Round(CropHeight * (R.Bottom / ImgHeight)));
end;
procedure TZImage.SetAlignment(Value: TAlignment);
begin
if FAlignment <> Value then
begin
FAlignment := Value;
RealignImage;
Invalidate;
end;
end;
procedure TZImage.SetLayout(Value: TTextLayout);
begin
if FLayout <> Value then
begin
FLayout := Value;
RealignImage;
Invalidate;
end;
end;
procedure TZImage.SetPicture(Value: TPicture);
begin
FPicture.Assign(Value);
end;
procedure TZImage.SetProportional(Value: Boolean);
begin
if FProportional <> Value then
begin
FProportional := Value;
RealignImage;
Invalidate;
end;
end;
procedure TZImage.UpdateBuffer;
begin
if HasGraphic then
begin
FBuffer.Width := FAnimRect.Right - FAnimRect.Left;
FBuffer.Height := FAnimRect.Bottom - FAnimRect.Top;
FBuffer.Canvas.Draw(-FAnimRect.Left, -FAnimRect.Top, FPicture.Graphic);
end;
end;
procedure TZImage.Zoom(const ACropRect: TRect);
begin
if HasGraphic then
begin
FPrevCropRect := FAnimRect;
FCropRect := ACropRect;
if FAnimDuration = 0 then
begin
FAnimRect := FCropRect;
UpdateBuffer;
RealignImage;
Invalidate;
end
else
begin
FAnimStartTick := GetTickCount;
FAnimTimer.Enabled := True;
end;
end;
end;
procedure TZImage.ZoomSelection(const ASelRect: TRect);
begin
Zoom(ScreenToGraphic(ASelRect));
end;
end.
Sample code:
procedure TForm1.FormCreate(Sender: TObject);
begin
FImage := TZImage.Create(Self);
FImage.SetBounds(10, 10, 200, 300);
FImage.Picture.LoadFromFile('D:\Pictures\Mona_Lisa.jpg');
FImage.Alignment := taCenter;
FImage.Layout := tlCenter;
FImage.AutoSize := True;
FImage.Parent := Self;
end;
I created a Custom scrollbox derives from TScrollbox that works the same except that it will scrolls when dragging in the client area aside from its scrollbars.
My problem now is i cannot Drag To Scroll when mouse is on a button or panel inside my CustomScrollbox.
the MouseDown, MouseUp, MouseMove override will not trigger because it hovers into different controls.
How can I keep tracking the MouseDown, MouseUp, MouseMove and prevent Button/Panels events from firing(inside my CustomScrollbox) when i start dragging?
here's the video of my smooth CustomScrollbox
So you want to adjust the mouse down behaviour of all childs, in such way that when a dragging operation is being initiated, the mouse events of the clicked child should be ignored. But when no drag is performed, then it would be required to fire the child's mouse events as usual.
Not a bad question actually. Since most of the default control interaction is tight to the release of the mouse button (e.g. OnClick is handled in WM_LBUTTONUP), this still should be possible in an intuitive manner.
I tried the code below, and it feels quite nice indeed. It involves:
handling WM_PARENTNOTIFY to catch when a child control is clicked on,
bypassing Child.OnMouseMove and Child.OnMouseUp,
transfer control to the scrollbox when the move exceeds Mouse.DragThreshold,
resetting focus to the previous focussed control before the drag,
canceling all changes made to the child's mouse events after the drag.
unit Unit2;
interface
uses
Windows, Messages, Classes, Controls, Forms, StdCtrls, ExtCtrls;
type
TScrollBox = class(Forms.TScrollBox)
private
FChild: TControl;
FDragging: Boolean;
FPrevActiveControl: TWinControl;
FPrevScrollPos: TPoint;
FPrevTick: Cardinal;
FOldChildOnMouseMove: TMouseMoveEvent;
FOldChildOnMouseUp: TMouseEvent;
FSpeedX: Single;
FSpeedY: Single;
FStartPos: TPoint;
FTracker: TTimer;
function ActiveControl: TWinControl;
procedure ChildMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure ChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
function GetScrollPos: TPoint;
procedure SetScrollPos(const Value: TPoint);
procedure Track(Sender: TObject);
procedure WMParentNotify(var Message: TWMParentNotify);
message WM_PARENTNOTIFY;
protected
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
property ScrollPos: TPoint read GetScrollPos write SetScrollPos;
end;
TForm2 = class(TForm)
ScrollBox1: TScrollBox;
...
end;
implementation
{$R *.dfm}
{ TScrollBox }
type
TControlAccess = class(TControl);
function TScrollBox.ActiveControl: TWinControl;
var
Control: TWinControl;
begin
Result := Screen.ActiveControl;
Control := Result;
while (Control <> nil) do
begin
if Control = Self then
Exit;
Control := Control.Parent;
end;
Result := nil;
end;
procedure TScrollBox.ChildMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
if (Abs(FChild.Left + X - FStartPos.X) > Mouse.DragThreshold) or
(Abs(FChild.Top + Y - FStartPos.Y) > Mouse.DragThreshold) then
begin
MouseCapture := True;
TControlAccess(FChild).OnMouseMove := FOldChildOnMouseMove;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
MouseDown(mbLeft, Shift, FChild.Left + X, FChild.Top + Y);
FChild := nil;
if FPrevActiveControl <> nil then
FPrevActiveControl.SetFocus;
end
else
if Assigned(FOldChildOnMouseMove) then
FOldChildOnMouseMove(Sender, Shift, X, Y);
end;
procedure TScrollBox.ChildMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if FChild <> nil then
begin
if Assigned(FOldChildOnMouseUp) then
FOldChildOnMouseUp(Sender, Button, Shift, X, Y);
TControlAccess(FChild).OnMouseMove := FOldChildOnMouseMove;
TControlAccess(FChild).OnMouseUp := FOldChildOnMouseUp;
FChild := nil;
end;
end;
constructor TScrollBox.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FTracker := TTimer.Create(Self);
FTracker.Enabled := False;
FTracker.Interval := 15;
FTracker.OnTimer := Track;
end;
function TScrollBox.GetScrollPos: TPoint;
begin
Result := Point(HorzScrollBar.Position, VertScrollBar.Position);
end;
procedure TScrollBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
FDragging := True;
FPrevTick := GetTickCount;
FPrevScrollPos := ScrollPos;
FTracker.Enabled := True;
FStartPos := Point(ScrollPos.X + X, ScrollPos.Y + Y);
Screen.Cursor := crHandPoint;
inherited MouseDown(Button, Shift, X, Y);
end;
procedure TScrollBox.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
if FDragging then
ScrollPos := Point(FStartPos.X - X, FStartPos.Y - Y);
inherited MouseMove(Shift, X, Y);
end;
procedure TScrollBox.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
FDragging := False;
Screen.Cursor := crDefault;
inherited MouseUp(Button, Shift, X, Y);
end;
procedure TScrollBox.SetScrollPos(const Value: TPoint);
begin
HorzScrollBar.Position := Value.X;
VertScrollBar.Position := Value.Y;
end;
procedure TScrollBox.Track(Sender: TObject);
var
Delay: Cardinal;
begin
Delay := GetTickCount - FPrevTick;
if FDragging then
begin
if Delay = 0 then
Delay := 1;
FSpeedX := (ScrollPos.X - FPrevScrollPos.X) / Delay;
FSpeedY := (ScrollPos.Y - FPrevScrollPos.Y) / Delay;
end
else
begin
if (Abs(FSpeedX) < 0.005) and (Abs(FSpeedY) < 0.005) then
FTracker.Enabled := False
else
begin
ScrollPos := Point(FPrevScrollPos.X + Round(Delay * FSpeedX),
FPrevScrollPos.Y + Round(Delay * FSpeedY));
FSpeedX := 0.83 * FSpeedX;
FSpeedY := 0.83 * FSpeedY;
end;
end;
FPrevScrollPos := ScrollPos;
FPrevTick := GetTickCount;
end;
procedure TScrollBox.WMParentNotify(var Message: TWMParentNotify);
begin
inherited;
if Message.Event = WM_LBUTTONDOWN then
begin
FChild := ControlAtPos(Point(Message.XPos, Message.YPos), False, True);
if FChild <> nil then
begin
FPrevActiveControl := ActiveControl;
FOldChildOnMouseMove := TControlAccess(FChild).OnMouseMove;
TControlAccess(FChild).OnMouseMove := ChildMouseMove;
FOldChildOnMouseUp := TControlAccess(FChild).OnMouseUp;
TControlAccess(FChild).OnMouseUp := ChildMouseUp;
end;
end;
end;
end.
Note: When no drag is initiated (mouse movement < Mouse.DragThreshold), all mouse and click events of the clicked child remain intact. Otherwise only Child.OnMouseDown will fire!
For testing purposes, this answer is incorporated in the code above.
With thanks to #TLama for suggesting to use WM_PARENTNOTIFY.
The CheckBox component displays a checkmark when checked.
I would like to display an 'X' instead.
You could do something like this:
unit CheckboxEx;
interface
uses
SysUtils, Windows, Messages, Graphics, Classes, Controls, UxTheme;
type
TCrossType = (ctChar, ctGDI);
TCheckboxEx = class(TCustomControl)
private type
THoverState = (hsNormal = 1, hsHover = 2, hsPushed = 3);
private const
DEFAULT_PADDING = 3;
DEFAULT_CHECK_CHAR = '✘';
CHECK_LINE_PADDING = 4;
private
{ Private declarations }
FCaption: TCaption;
FChecked: boolean;
FPadding: integer;
FCheckWidth, FCheckHeight: integer;
FCheckRect, FTextRect: TRect;
theme: HTHEME;
FHoverState: THoverState;
FCheckFont: TFont;
FCheckChar: Char;
FMouseHover: boolean;
FCrossType: TCrossType;
procedure SetCaption(const Caption: TCaption);
procedure SetChecked(Checked: boolean);
procedure SetPadding(Padding: integer);
procedure UpdateMetrics;
procedure CheckFontChange(Sender: TObject);
procedure SetCheckChar(const CheckChar: char);
procedure DetermineState;
procedure SetCrossType(CrossType: TCrossType);
protected
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure WndProc(var Message: TMessage); override;
procedure Click; override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure KeyUp(var Key: Word; Shift: TShiftState); override;
{ Protected declarations }
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
{ Public declarations }
published
{ Published declarations }
property ParentColor;
property ParentFont;
property Color;
property Visible;
property Enabled;
property TabStop default true;
property TabOrder;
property OnDblClick;
property OnEnter;
property OnExit;
property OnKeyUp;
property OnKeyPress;
property OnKeyDown;
property OnMouseActivate;
property OnMouseLeave;
property OnMouseEnter;
property OnMouseMove;
property OnMouseUp;
property OnMouseDown;
property OnClick;
property Font;
property CheckFont: TFont read FCheckFont write FCheckFont;
property Caption: TCaption read FCaption write SetCaption;
property Checked: boolean read FChecked write SetChecked default false;
property Padding: integer read FPadding write SetPadding default DEFAULT_PADDING;
property CheckChar: Char read FCheckChar write SetCheckChar default DEFAULT_CHECK_CHAR;
property CrossType: TCrossType read FCrossType write SetCrossType default ctGDI;
end;
procedure Register;
implementation
uses Math;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TCheckboxEx]);
end;
var
Hit: boolean;
function _EnumFontsProcBool(var LogFont: TLogFont; var TextMetric: TTextMetric;
FontType: Integer; Data: Pointer): Integer; stdcall;
begin
hit := SameStr(LogFont.lfFaceName, Pstring(Data)^);
result := IfThen(hit, 0, 1);
end;
function FontInstalled(const FontName: TFontName): boolean;
var
LF: TLogFont;
fn: string;
begin
hit := false;
FillChar(LF, sizeOf(LF), 0);
LF.lfCharSet := DEFAULT_CHARSET;
fn := FontName;
EnumFontFamiliesEx(GetDC(0), LF, #_EnumFontsProcBool, cardinal(#fn), 0);
result := hit;
end;
function IsKeyDown(const VK: integer): boolean;
begin
IsKeyDown := GetKeyState(VK) and $8000 <> 0;
end;
{ TCheckboxEx }
procedure TCheckboxEx.CheckFontChange(Sender: TObject);
begin
Invalidate;
end;
procedure TCheckboxEx.Click;
begin
inherited;
if Enabled then
begin
SetChecked(not FChecked);
SetFocus;
end;
end;
constructor TCheckboxEx.Create(AOwner: TComponent);
begin
inherited;
TabStop := true;
FMouseHover := false;
FChecked := false;
FPadding := DEFAULT_PADDING;
FCheckChar := DEFAULT_CHECK_CHAR;
FCrossType := ctGDI;
theme := 0;
FHoverState := hsNormal;
FCheckFont := TFont.Create;
FCheckFont.Assign(Font);
if FontInstalled('Arial Unicode MS') then
FCheckFont.Name := 'Arial Unicode MS';
FCheckFont.OnChange := CheckFontChange;
end;
destructor TCheckboxEx.Destroy;
begin
FCheckFont.Free;
if theme <> 0 then
CloseThemeData(theme);
inherited;
end;
procedure TCheckboxEx.DetermineState;
var
OldState: THoverState;
begin
inherited;
OldState := FHoverState;
FHoverState := hsNormal;
if FMouseHover then
FHoverState := hsHover;
if (csLButtonDown in ControlState) or (IsKeyDown(VK_SPACE) and Focused) then
FHoverState := hsPushed;
if (FHoverState <> OldState) and UseThemes then
Invalidate;
end;
procedure TCheckboxEx.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited;
if Key = VK_SPACE then
DetermineState;
end;
procedure TCheckboxEx.KeyUp(var Key: Word; Shift: TShiftState);
begin
inherited;
if Key = VK_SPACE then
begin
Click;
DetermineState;
end;
end;
procedure TCheckboxEx.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
DetermineState;
end;
procedure TCheckboxEx.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
inherited;
FMouseHover := true;
DetermineState;
end;
procedure TCheckboxEx.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
DetermineState;
end;
procedure TCheckboxEx.Paint;
var
ext: TSize;
frect: TRect;
begin
inherited;
Canvas.Brush.Color := Self.Color;
Canvas.FillRect(ClientRect);
if UseThemes then
begin
if theme = 0 then
begin
theme := OpenThemeData(Handle, 'BUTTON');
UpdateMetrics;
end;
if Enabled then
DrawThemeBackground(theme,
Canvas.Handle,
BP_CHECKBOX,
ord(FHoverState),
FCheckRect,
nil)
else
DrawThemeBackground(theme,
Canvas.Handle,
BP_CHECKBOX,
CBS_UNCHECKEDDISABLED,
FCheckRect,
nil);
end
else
if Enabled then
DrawFrameControl(Canvas.Handle,
FCheckRect,
DFC_BUTTON,
DFCS_BUTTONCHECK)
else
DrawFrameControl(Canvas.Handle,
FCheckRect,
DFC_BUTTON,
DFCS_BUTTONCHECK or DFCS_INACTIVE);
Canvas.TextFlags := TRANSPARENT;
Canvas.Brush.Style := bsClear;
Canvas.Font.Assign(Font);
DrawText(Canvas.Handle,
PChar(FCaption),
length(FCaption),
FTextRect,
DT_SINGLELINE or DT_VCENTER or DT_LEFT);
if Focused then
begin
ext := Canvas.TextExtent(FCaption);
frect := Rect(FTextRect.Left,
(ClientHeight - ext.cy) div 2,
FTextRect.Left + ext.cx,
(ClientHeight + ext.cy) div 2);
Canvas.DrawFocusRect(frect);
end;
if FChecked then
case FCrossType of
ctChar:
begin
Canvas.Font.Assign(FCheckFont);
DrawText(Canvas.Handle,
CheckChar,
1,
FCheckRect,
DT_SINGLELINE or DT_VCENTER or DT_CENTER);
end;
ctGDI:
begin
Canvas.Pen.Width := 2;
Canvas.Pen.Color := clBlack;
Canvas.Pen.Mode := pmCopy;
Canvas.MoveTo(FCheckRect.Left + CHECK_LINE_PADDING, FCheckRect.Top + CHECK_LINE_PADDING);
Canvas.LineTo(FCheckRect.Right - CHECK_LINE_PADDING, FCheckRect.Bottom - CHECK_LINE_PADDING);
Canvas.MoveTo(FCheckRect.Right - CHECK_LINE_PADDING, FCheckRect.Top + CHECK_LINE_PADDING);
Canvas.LineTo(FCheckRect.Left + CHECK_LINE_PADDING, FCheckRect.Bottom - CHECK_LINE_PADDING);
end;
end;
end;
procedure TCheckboxEx.SetCaption(const Caption: TCaption);
begin
if not SameStr(FCaption, Caption) then
begin
FCaption := Caption;
Invalidate;
end;
end;
procedure TCheckboxEx.SetCheckChar(const CheckChar: char);
begin
if FCheckChar <> CheckChar then
begin
FCheckChar := CheckChar;
if FChecked then Invalidate;
end;
end;
procedure TCheckboxEx.SetChecked(Checked: boolean);
begin
if FChecked <> Checked then
begin
FChecked := Checked;
Invalidate;
end;
end;
procedure TCheckboxEx.SetCrossType(CrossType: TCrossType);
begin
if FCrossType <> CrossType then
begin
FCrossType := CrossType;
if FChecked then Invalidate;
end;
end;
procedure TCheckboxEx.SetPadding(Padding: integer);
begin
if FPadding <> Padding then
begin
FPadding := Padding;
UpdateMetrics;
Invalidate;
end;
end;
procedure TCheckboxEx.UpdateMetrics;
var
size: TSize;
begin
FCheckWidth := GetSystemMetrics(SM_CXMENUCHECK);
FCheckHeight := GetSystemMetrics(SM_CYMENUCHECK);
if UseThemes then
begin
UxTheme.GetThemePartSize(theme, Canvas.Handle, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, nil, TS_DRAW, size);
FCheckWidth := size.cx;
FCheckHeight := size.cy;
end;
FCheckRect := Rect(0,
(ClientHeight - FCheckHeight) div 2,
FCheckWidth,
(ClientHeight + FCheckHeight) div 2);
FTextRect := Rect(FCheckWidth + FPadding,
0,
ClientWidth,
ClientHeight);
end;
procedure TCheckboxEx.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
CM_MOUSELEAVE:
begin
FMouseHover := false;
DetermineState;
end;
WM_SIZE:
begin
UpdateMetrics;
Invalidate;
end;
WM_SETFOCUS, WM_KILLFOCUS:
Invalidate;
end;
end;
end.
Now (with CrossType set to ctChar) you can use any Unicode character as the checkmark, the default choice being ✘ (U+2718: HEAVY BALLOT X). The images below illustrate that the control works both with and without visual themes:
The following image illustrates that you can choose any character as your checkmark:
This character is ✿ (U+273F: BLACK FLORETTE).
If you set CrossType to ctGDI instead of ctChar, the control will draw a cross manually and not a character:
I didn't use double-buffering this time, because there is no noticable flickering with themes enabled. Without themes, however, there is flickering. To remedy this, simply use a FBuffer: TBitmap and draw on FBuffer.Canvas instead of Self.Canvas and then BitBlt at the end of Paint, as I do in my other controls here at SO.
You'll have to write a custom control and paint it yourself.
If this is a real check box then it's a bad idea to avoid the system's default drawing. However, if you want to do something like a voting form then I could see why you might opt to do this.
I would go the opposite way, anyway, select all items by default and let the user remove the ones who should be left out from the list.
Having checkbutton a serious limitation in designs, who want to stay in VCL, can use BitBtn as a check, using "Kind" property to paint the Cancel or Ok images when user click on it. Also delete after every condition change, the "Caption" property, because the BitBtn must have a square layout to simulate a check. Use also a tLabel at left or right hand as you wish.
if lAutoMode = False then
begin
lAutoMode := True;
BitBtn1.Kind := bkOK;
BitBtn1.Caption := '';
end
else
begin
lAutoMode := False;
BitBtn1.Kind := bkAbort;
BitBtn1.Caption := '';
end;
When create the Form, set the initial state for the BitBtn.
I realize this one is a bit strange, so I'll explain. For a simple internet radio player I need a control to specify rating (1-5 "stars"). I have no experience or talent for graphical design, so all my attempts at drawing bitmaps look ridiculous/awful, take your pick. I couldn't find a 3rd party control with that functionality and look that fits standard VCL controls. So...
It occurred to me that I could achieve an OK look and consistency with Windows UI by using standard radiobuttons without captions, like this:
I had a vague (and incorrect) recollection of a GroupIndex property; assigning a different value to each radiobutton would let multiple radiobuttons be checked at the same time. Alas, TRadioButton does not have a GroupIndex property, so that's that.
Is it possible to completely override the natural radiobutton behavior, so that more than one button can show up as checked at the same time? Or,
Can I acquire all the bitmaps Windows uses for radiobuttons (I assume they're bitmaps) from the system and draw them directly, including theming support? In this case I would still like to retain all the effects of a radiobutton, including the mouse hover "glow", etc, so that means getting all the "native" bitmaps and drawing them as necessary, perhaps on a TPaintBox.
For maximum convenience, you could write a small control that draws native, themed, radio boxes:
unit StarRatingControl;
interface
uses
SysUtils, Windows, Messages, Graphics, Classes, Controls, UxTheme;
type
TStarRatingControl = class(TCustomControl)
private const
DEFAULT_SPACING = 4;
DEFAULT_NUM_STARS = 5;
FALLBACK_BUTTON_SIZE: TSize = (cx: 16; cy: 16);
private
{ Private declarations }
FRating: integer;
FBuffer: TBitmap;
FSpacing: integer;
FNumStars: integer;
FButtonStates: array of integer;
FButtonPos: array of TRect;
FButtonSize: TSize;
FDown: boolean;
PrevButtonIndex: integer;
PrevState: integer;
FOnChange: TNotifyEvent;
procedure SetRating(const Rating: integer);
procedure SetSpacing(const Spacing: integer);
procedure SetNumStars(const NumStars: integer);
procedure SwapBuffers;
procedure SetState(const ButtonIndex: integer; const State: integer);
protected
{ Protected declarations }
procedure WndProc(var Message: TMessage); override;
procedure Paint; override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
{ Public declarations }
published
{ Published declarations }
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property Rating: integer read FRating write SetRating default 3;
property Spacing: integer read FSpacing write SetSpacing default DEFAULT_SPACING;
property NumStars: integer read FNumStars write SetNumStars default DEFAULT_NUM_STARS;
property OnDblClick;
property OnKeyUp;
property OnKeyPress;
property OnKeyDown;
property OnMouseWheelDown;
property OnMouseWheelUp;
property OnMouseWheel;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseActivate;
property OnMouseMove;
property OnMouseUp;
property OnMouseDown;
property OnClick;
property Align;
property Anchors;
property Color;
end;
procedure Register;
implementation
uses Math;
function IsIntInInterval(x, xmin, xmax: integer): boolean; inline;
begin
IsIntInInterval := (xmin <= x) and (x <= xmax);
end;
function PointInRect(const X, Y: integer; const Rect: TRect): boolean; inline;
begin
PointInRect := IsIntInInterval(X, Rect.Left, Rect.Right) and
IsIntInInterval(Y, Rect.Top, Rect.Bottom);
end;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TStarRatingControl]);
end;
{ TStarRatingControl }
constructor TStarRatingControl.Create(AOwner: TComponent);
var
i: Integer;
begin
inherited;
FBuffer := TBitmap.Create;
FRating := 3;
FSpacing := DEFAULT_SPACING;
FNumStars := DEFAULT_NUM_STARS;
SetLength(FButtonStates, FNumStars);
SetLength(FButtonPos, FNumStars);
for i := 0 to high(FButtonStates) do
FButtonStates[i] := RBS_NORMAL;
FDown := false;
PrevButtonIndex := -1;
PrevState := -1;
end;
destructor TStarRatingControl.Destroy;
begin
FBuffer.Free;
inherited;
end;
procedure TStarRatingControl.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
i: integer;
begin
inherited;
FDown := true;
for i := 0 to FNumStars - 1 do
if PointInRect(X, Y, FButtonPos[i]) then
begin
SetState(i, RBS_PUSHED);
Exit;
end;
end;
procedure TStarRatingControl.MouseMove(Shift: TShiftState; X, Y: Integer);
var
i: Integer;
begin
inherited;
if FDown then Exit;
for i := 0 to FNumStars - 1 do
if PointInRect(X, Y, FButtonPos[i]) then
begin
SetState(i, RBS_HOT);
Exit;
end;
SetState(-1, -1);
end;
procedure TStarRatingControl.MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
i: Integer;
begin
inherited;
for i := 0 to FNumStars - 1 do
if PointInRect(X, Y, FButtonPos[i]) and (i = PrevButtonIndex) and (FRating <> i + 1) then
begin
SetRating(i + 1);
if Assigned(FOnChange) then
FOnChange(Self);
end;
FDown := false;
MouseMove(Shift, X, Y);
end;
procedure TStarRatingControl.Paint;
var
t: HTHEME;
i: Integer;
begin
inherited;
FBuffer.Canvas.Brush.Color := Color;
FBuffer.Canvas.FillRect(ClientRect);
FButtonSize := FALLBACK_BUTTON_SIZE;
if UseThemes then
begin
t := OpenThemeData(Handle, 'BUTTON');
if t <> 0 then
try
GetThemePartSize(t, FBuffer.Canvas.Handle, BP_RADIOBUTTON, RBS_NORMAL, nil, TS_DRAW, FButtonSize);
for i := 0 to FNumStars - 1 do
with FButtonPos[i] do
begin
Left := i * (Spacing + FButtonSize.cx);
Top := (Self.Height - FButtonSize.cy) div 2;
Right := Left + FButtonSize.cx;
Bottom := Top + FButtonSize.cy;
end;
for i := 0 to FNumStars - 1 do
DrawThemeBackground(t,
FBuffer.Canvas.Handle,
BP_RADIOBUTTON,
IfThen(FRating > i, RBS_CHECKEDNORMAL) + FButtonStates[i],
FButtonPos[i],
nil);
finally
CloseThemeData(t);
end;
end
else
begin
for i := 0 to FNumStars - 1 do
with FButtonPos[i] do
begin
Left := i * (Spacing + FButtonSize.cx);
Top := (Self.Height - FButtonSize.cy) div 2;
Right := Left + FButtonSize.cx;
Bottom := Top + FButtonSize.cy;
end;
for i := 0 to FNumStars - 1 do
DrawFrameControl(FBuffer.Canvas.Handle,
FButtonPos[i],
DFC_BUTTON,
DFCS_BUTTONRADIO or IfThen(FRating > i, DFCS_CHECKED));
end;
SwapBuffers;
end;
procedure TStarRatingControl.SetNumStars(const NumStars: integer);
var
i: integer;
begin
if FNumStars <> NumStars then
begin
FNumStars := NumStars;
SetLength(FButtonStates, FNumStars);
SetLength(FButtonPos, FNumStars);
for i := 0 to high(FButtonStates) do
FButtonStates[i] := RBS_NORMAL;
Paint;
end;
end;
procedure TStarRatingControl.SetRating(const Rating: integer);
begin
if FRating <> Rating then
begin
FRating := Rating;
Paint;
end;
end;
procedure TStarRatingControl.SetSpacing(const Spacing: integer);
begin
if FSpacing <> Spacing then
begin
FSpacing := Spacing;
Paint;
end;
end;
procedure TStarRatingControl.SetState(const ButtonIndex, State: integer);
var
i: Integer;
begin
for i := 0 to FNumStars - 1 do
if i = ButtonIndex then
FButtonStates[i] := State
else
FButtonStates[i] := RBS_NORMAL;
if (PrevButtonIndex <> ButtonIndex) or (PrevState <> State) then
Paint;
PrevButtonIndex := ButtonIndex;
PrevState := State;
end;
procedure TStarRatingControl.SwapBuffers;
begin
BitBlt(Canvas.Handle,
0,
0,
Width,
Height,
FBuffer.Canvas.Handle,
0,
0,
SRCCOPY);
end;
procedure TStarRatingControl.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
WM_SIZE:
begin
FBuffer.SetSize(Width, Height);
Paint;
end;
end;
end;
end.
Just adjust the properties NumStars, Rating, and Spacing, and have fun!
Of course, you could also write a component that uses custom bitmaps instead of the native Windows radio buttons.
Making radio buttons that look like radio buttons but behave differently would confuse the user. Also, you would end up needing half-check marks when you decide to display existing ratings. So something like a progress bar (maybe custom-colored or custom-drawn) to display, how "complete" user satisfaction is could be a better option.
I agree with Eugene and Craig that something like stars would be better, but, to answer the question posed:
The unthemed radio button images are available by calling LoadBitmap with OBM_CHECKBOXES. You can assign that directly to a TBitmap's Handle property, and then divide the width by 4 and the height by 3 to get the subbitmap measurements. Use TCanvas.BrushCopy to do the drawing.
To draw the themed images you need to use Delphi's Themes.pas. Specifically call ThemeServices.GetElementDetails with tbRadioButtonUncheckedNormal or tbRadioButtonCheckedNormal and pass the result to ThemeServices.DrawElement along with the client rect.
Here's a simple override that makes a TCheckBox draw as a checked radio button so you can see how it works:
TCheckBox = class(StdCtrls.TCheckBox)
constructor Create(AOwner: TComponent); override;
procedure PaintWindow(DC: HDC); override;
end;
constructor TCheckBox.Create(AOwner: TComponent);
begin
inherited;
ControlState := ControlState + [csCustomPaint];
end;
procedure TCheckBox.PaintWindow(DC: HDC);
begin
ThemeServices.DrawElement(DC,
ThemeServices.GetElementDetails(tbRadioButtonCheckedNormal), ClientRect);
end;
You could place each radiobutton on a separate (tiny) panel, and that would make a substitute for the missing GroupIndex property.
Maybe not the nicest method, still relatively cheap, it seems to me.
Good inspiration gave you Andreas Rejbrand (+1). I'll provide you just some small piece of code of what you are probably looking for. It's form with two overlapped images with one common event - OnMouseDown. It contains just some mad formula - unfortunately with constants, which I've made some time ago. But sorry I'm not mathematician, so please be patient with me and let's take this also as the inspiration :)