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;
Related
I'm trying to figure out how I can make a custom control in a way where user can scroll in all directions, but with a fixed row and column. A grid is not suitable for what I'm trying to do, because it scrolls column by column. I need horizontal scrolling to be smooth, pixel by pixel. I have no use for columns, only visual grid lines. Vertical scrolling should scroll not only the area on the right, but also the fixed region on the left. Same with horizontal scrolling: the header row should move along with the horizontal scrollbar.
This is just a rough draft of the final control I'm working on.
Note how the scrollbars do not cover the full control, only the larger region. The fixed column/row should also be able to move along with their corresponding scrollbar.
How should I implement the scrollbars to make this possible?
PS - This is to replace a much more thorough question which was deleted for being a mis-leading request. So sorry if I'm lacking details which you might need to know.
First, I thought you could do with this component (sample image) which is capable of holding controls in cells, but from your comment I understand that you want to draw everything yourself. So I wrote a 'THeaderGrid' component:
procedure TForm1.FormCreate(Sender: TObject);
begin
with THeaderGrid.Create(Self) do
begin
Align := alClient;
OnDrawCell := DrawCell;
OnDrawColHeader := DrawCell;
OnDrawRowHeader := DrawCell;
Parent := Self;
end;
end;
procedure TForm1.DrawCell(Sender: TObject; ACanvas: TCanvas; ACol,
ARow: Integer; R: TRect);
begin
ACanvas.TextOut(R.Left + 2, R.Top + 2, Format('(%d,%d)', [ACol, ARow]));
end;
The component is build up out of three TPaintScroller controls (a TPaintBox on a TScrollBox). Actually, for both headers, TScrollBox is a little bit heavyweighted, but it was kind of handy to use the same control as for the data region with the cells.
There are three OnDraw events, one for both headers and one for the cells, but you could all set them to the same handler, alike the example above. Distinguish each by the column or row index being -1.
unit HeaderGrid;
interface
uses
Classes, Controls, Windows, Messages, Graphics, Forms, ExtCtrls, StdCtrls;
type
TPaintEvent = procedure(ACanvas: TCanvas) of object;
TPaintScroller = class(TScrollingWinControl)
private
FOnPaint: TPaintEvent;
FOnScroll: TNotifyEvent;
FPainter: TPaintBox;
function GetPaintHeight: Integer;
function GetPaintWidth: Integer;
function GetScrollBars: TScrollStyle;
procedure SetPaintHeight(Value: Integer);
procedure SetPaintWidth(Value: Integer);
procedure SetScrollBars(Value: TScrollStyle);
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure WMHScroll(var Message: TWMScroll); message WM_HSCROLL;
procedure WMVScroll(var Message: TWMScroll); message WM_VSCROLL;
protected
procedure CreateParams(var Params: TCreateParams); override;
function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean; override;
procedure DoPaint(Sender: TObject); virtual;
procedure DoScroll; virtual;
procedure PaintWindow(DC: HDC); override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
published
property OnPaint: TPaintEvent read FOnPaint write FOnPaint;
property OnScroll: TNotifyEvent read FOnScroll write FOnScroll;
property PaintHeight: Integer read GetPaintHeight write SetPaintHeight;
property PaintWidth: Integer read GetPaintWidth write SetPaintWidth;
property ScrollBars: TScrollStyle read GetScrollBars write SetScrollBars
default ssBoth;
end;
TDrawCellEvent = procedure(Sender: TObject; ACanvas: TCanvas; ACol,
ARow: Integer; R: TRect) of object;
THeaderGrid = class(TCustomControl)
private
FCellScroller: TPaintScroller;
FColCount: Integer;
FColHeader: TPaintScroller;
FColWidth: Integer;
FOnDrawCell: TDrawCellEvent;
FOnDrawColHeader: TDrawCellEvent;
FOnDrawRowHeader: TDrawCellEvent;
FRowCount: Integer;
FRowHeader: TPaintScroller;
FRowHeight: Integer;
procedure CellsScrolled(Sender: TObject);
function GetColHeaderHeight: Integer;
function GetRowHeaderWidth: Integer;
procedure PaintCells(ACanvas: TCanvas);
procedure PaintColHeader(ACanvas: TCanvas);
procedure PaintRowHeader(ACanvas: TCanvas);
procedure SetColCount(Value: Integer);
procedure SetColHeaderHeight(Value: Integer);
procedure SetColWidth(Value: Integer);
procedure SetRowCount(Value: Integer);
procedure SetRowHeaderWidth(Value: Integer);
procedure SetRowHeight(Value: Integer);
procedure UpdateSize;
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
protected
procedure CreateParams(var Params: TCreateParams); override;
procedure DoDrawCell(ACanvas: TCanvas; ACol, ARow: Integer;
R: TRect); virtual;
procedure DoDrawColHeader(ACanvas: TCanvas; ACol: Integer;
R: TRect); virtual;
procedure DoDrawRowHeader(ACanvas: TCanvas; ARow: Integer;
R: TRect); virtual;
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
procedure MouseWheelHandler(var Message: TMessage); override;
published
property ColCount: Integer read FColCount write SetColCount default 5;
property ColHeaderHeight: Integer read GetColHeaderHeight
write SetColHeaderHeight default 24;
property ColWidth: Integer read FColWidth write SetColWidth default 64;
property OnDrawCell: TDrawCellEvent read FOnDrawCell write FOnDrawCell;
property OnDrawColHeader: TDrawCellEvent read FOnDrawColHeader
write FOnDrawColHeader;
property OnDrawRowHeader: TDrawCellEvent read FOnDrawRowHeader
write FOnDrawRowHeader;
property RowCount: Integer read FRowCount write SetRowCount default 5;
property RowHeaderWidth: Integer read GetRowHeaderWidth
write SetRowHeaderWidth default 64;
property RowHeight: Integer read FRowHeight write SetRowHeight default 24;
published
property Color;
property Font;
property ParentColor default False;
property TabStop default True;
end;
implementation
{ TPaintScroller }
constructor TPaintScroller.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
HorzScrollBar.Tracking := True;
VertScrollBar.Tracking := True;
Width := 100;
Height := 100;
FPainter := TPaintBox.Create(Self);
FPainter.SetBounds(0, 0, 100, 100);
FPainter.OnPaint := DoPaint;
FPainter.Parent := Self;
end;
procedure TPaintScroller.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params.WindowClass do
Style := Style and not (CS_HREDRAW or CS_VREDRAW);
end;
function TPaintScroller.DoMouseWheel(Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint): Boolean;
begin
VertScrollBar.Position := VertScrollBar.Position - WheelDelta;
DoScroll;
Result := True;
end;
procedure TPaintScroller.DoPaint(Sender: TObject);
begin
if Assigned(FOnPaint) then
FOnPaint(FPainter.Canvas);
end;
procedure TPaintScroller.DoScroll;
begin
if Assigned(FOnScroll) then
FOnScroll(Self);
end;
function TPaintScroller.GetPaintHeight: Integer;
begin
Result := FPainter.Height;
end;
function TPaintScroller.GetPaintWidth: Integer;
begin
Result := FPainter.Width;
end;
function TPaintScroller.GetScrollBars: TScrollStyle;
begin
if HorzScrollBar.Visible and VertScrollBar.Visible then
Result := ssBoth
else if not HorzScrollBar.Visible and VertScrollBar.Visible then
Result := ssVertical
else if HorzScrollBar.Visible and not VertScrollBar.Visible then
Result := ssHorizontal
else
Result := ssNone;
end;
procedure TPaintScroller.PaintWindow(DC: HDC);
begin
with FPainter do
ExcludeClipRect(DC, 0, 0, Width + Left, Height + Top);
FillRect(DC, ClientRect, Brush.Handle);
end;
procedure TPaintScroller.Resize;
begin
DoScroll;
inherited Resize;
end;
procedure TPaintScroller.SetPaintHeight(Value: Integer);
begin
FPainter.Height := Value;
end;
procedure TPaintScroller.SetPaintWidth(Value: Integer);
begin
FPainter.Width := Value;
end;
procedure TPaintScroller.SetScrollBars(Value: TScrollStyle);
begin
HorzScrollBar.Visible := (Value = ssBoth) or (Value = ssHorizontal);
VertScrollBar.Visible := (Value = ssBoth) or (Value = ssVertical);
end;
procedure TPaintScroller.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TPaintScroller.WMHScroll(var Message: TWMScroll);
begin
inherited;
DoScroll;
end;
procedure TPaintScroller.WMVScroll(var Message: TWMScroll);
begin
inherited;
DoScroll;
end;
{ THeaderGrid }
procedure THeaderGrid.CellsScrolled(Sender: TObject);
begin
FColHeader.FPainter.Left := -FCellScroller.HorzScrollBar.Position;
FRowHeader.FPainter.Top := -FCellScroller.VertScrollBar.Position;
end;
constructor THeaderGrid.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
ParentColor := False;
TabStop := True;
FCellScroller := TPaintScroller.Create(Self);
FCellScroller.Anchors := [akLeft, akTop, akRight, akBottom];
FCellScroller.OnPaint := PaintCells;
FCellScroller.OnScroll := CellsScrolled;
FCellScroller.AutoScroll := True;
FCellScroller.Parent := Self;
FColHeader := TPaintScroller.Create(Self);
FColHeader.Anchors := [akLeft, akTop, akRight];
FColHeader.OnPaint := PaintColHeader;
FColHeader.ScrollBars := ssNone;
FColHeader.Parent := Self;
FRowHeader := TPaintScroller.Create(Self);
FRowHeader.Anchors := [akLeft, akTop, akBottom];
FRowHeader.OnPaint := PaintRowHeader;
FRowHeader.ScrollBars := ssNone;
FRowHeader.Parent := Self;
Width := 320;
Height := 120;
ColCount := 5;
RowCount := 5;
ColWidth := 64;
RowHeight := 24;
ColHeaderHeight := 24;
RowHeaderWidth := 64;
end;
procedure THeaderGrid.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params.WindowClass do
Style := Style and not (CS_HREDRAW or CS_VREDRAW);
end;
procedure THeaderGrid.DoDrawCell(ACanvas: TCanvas; ACol, ARow: Integer;
R: TRect);
begin
if Assigned(FOnDrawCell) then
FOnDrawCell(Self, ACanvas, ACol, ARow, R);
end;
procedure THeaderGrid.DoDrawColHeader(ACanvas: TCanvas; ACol: Integer;
R: TRect);
begin
if Assigned(FOnDrawColHeader) then
FOnDrawColHeader(Self, ACanvas, ACol, -1, R);
end;
procedure THeaderGrid.DoDrawRowHeader(ACanvas: TCanvas; ARow: Integer;
R: TRect);
begin
if Assigned(FOnDrawRowHeader) then
FOnDrawRowHeader(Self, ACanvas, -1, ARow, R);
end;
function THeaderGrid.GetColHeaderHeight: Integer;
begin
Result := FColHeader.Height;
end;
function THeaderGrid.GetRowHeaderWidth: Integer;
begin
Result := FRowHeader.Width;
end;
procedure THeaderGrid.MouseWheelHandler(var Message: TMessage);
begin
with Message do
Result := FCellScroller.Perform(CM_MOUSEWHEEL, WParam, LParam);
if Message.Result = 0 then
inherited MouseWheelHandler(Message);
end;
procedure THeaderGrid.Paint;
var
R: TRect;
begin
Canvas.Brush.Color := Color;
R := Rect(0, 0, RowHeaderWidth, ColHeaderHeight);
if IntersectRect(R, R, Canvas.ClipRect) then
Canvas.FillRect(R);
Canvas.Brush.Color := clBlack;
R := Rect(0, ColHeaderHeight, Width, ColHeaderHeight + 1);
if IntersectRect(R, R, Canvas.ClipRect) then
Canvas.FillRect(R);
R := Rect(RowHeaderWidth, 0, RowHeaderWidth + 1, Height);
if IntersectRect(R, R, Canvas.ClipRect) then
Canvas.FillRect(R);
end;
procedure THeaderGrid.PaintCells(ACanvas: TCanvas);
var
Col: Integer;
Row: Integer;
R: TRect;
Dummy: TRect;
begin
ACanvas.Brush.Color := Color;
ACanvas.Font := Font;
ACanvas.FillRect(ACanvas.ClipRect);
for Row := 0 to FRowCount - 1 do
begin
R := Bounds(0, Row * FRowHeight, FColWidth, FRowHeight);
for Col := 0 to FColCount - 1 do
begin
if IntersectRect(Dummy, R, ACanvas.ClipRect) then
begin
DoDrawCell(ACanvas, Col, Row, R);
if ACanvas.Pen.Style <> psSolid then
ACanvas.Pen.Style := psSolid;
if ACanvas.Pen.Color <> clSilver then
ACanvas.Pen.Color := clSilver;
ACanvas.MoveTo(R.Left, R.Bottom - 1);
ACanvas.LineTo(R.Right - 1, R.Bottom - 1);
ACanvas.LineTo(R.Right - 1, R.Top - 1);
end;
OffsetRect(R, FColWidth, 0);
end;
end;
end;
procedure THeaderGrid.PaintColHeader(ACanvas: TCanvas);
var
Col: Integer;
R: TRect;
Dummy: TRect;
begin
ACanvas.Brush.Color := Color;
ACanvas.Font := Font;
ACanvas.FillRect(ACanvas.ClipRect);
R := Rect(0, 0, FColWidth, ColHeaderHeight);
for Col := 0 to FColCount - 1 do
begin
if IntersectRect(Dummy, R, ACanvas.ClipRect) then
DoDrawColHeader(ACanvas, Col, R);
OffsetRect(R, FColWidth, 0);
end;
end;
procedure THeaderGrid.PaintRowHeader(ACanvas: TCanvas);
var
Row: Integer;
R: TRect;
Dummy: TRect;
begin
ACanvas.Brush.Color := Color;
ACanvas.Font := Font;
ACanvas.FillRect(ACanvas.ClipRect);
R := Rect(0, 0, RowHeaderWidth, FRowHeight);
for Row := 0 to FRowCount - 1 do
begin
if IntersectRect(Dummy, R, ACanvas.ClipRect) then
begin
DoDrawRowHeader(ACanvas, Row, R);
if ACanvas.Pen.Style <> psSolid then
ACanvas.Pen.Style := psSolid;
if ACanvas.Pen.Color <> clSilver then
ACanvas.Pen.Color := clSilver;
ACanvas.MoveTo(R.Left, R.Bottom - 1);
ACanvas.LineTo(R.Right - 1, R.Bottom - 1);
end;
OffsetRect(R, 0, FRowHeight);
end;
end;
procedure THeaderGrid.SetColCount(Value: Integer);
begin
if FColCount <> Value then
begin
FColCount := Value;
UpdateSize;
end;
end;
procedure THeaderGrid.SetColHeaderHeight(Value: Integer);
begin
if Value >= 0 then
begin
FColHeader.Height := Value;
FRowHeader.BoundsRect := Rect(0, Value + 1, RowHeaderWidth, Height);
FCellScroller.BoundsRect := Rect(RowHeaderWidth + 1, Value + 1, Width,
Height);
end;
end;
procedure THeaderGrid.SetColWidth(Value: Integer);
begin
if FColWidth <> Value then
begin
FColWidth := Value;
FCellScroller.HorzScrollBar.Increment := Value;
UpdateSize;
end;
end;
procedure THeaderGrid.SetRowCount(Value: Integer);
begin
if FRowCount <> Value then
begin
FRowCount := Value;
UpdateSize;
end;
end;
procedure THeaderGrid.SetRowHeaderWidth(Value: Integer);
begin
if Value >= 0 then
begin
FRowHeader.Width := Value;
FColHeader.BoundsRect := Rect(Value + 1, 0, Width, ColHeaderHeight);
FCellScroller.BoundsRect := Rect(Value + 1, ColHeaderHeight + 1, Width,
Height);
end;
end;
procedure THeaderGrid.SetRowHeight(Value: Integer);
begin
if FRowHeight <> Value then
begin
FRowHeight := Value;
FCellScroller.VertScrollBar.Increment := Value;
UpdateSize;
end;
end;
procedure THeaderGrid.UpdateSize;
begin
FColHeader.PaintWidth := FColCount * FColWidth;
FRowHeader.PaintHeight := FRowCount * FRowHeight;
FCellScroller.PaintWidth := FColCount * FColWidth;
FCellScroller.PaintHeight := FRowCount * FRowHeight;
end;
procedure THeaderGrid.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
end.
The easiest way is to make a control without scrollbars and then put scrollbars over it with fine control of their size and position.
With Delphi 3-5 you could then encapsulate it as your new control using Custom Containers Pack, and drop onto new forms just like u do with regular grid.
Since D5 CCP is no more available but limited analogue is given as VCL TFrame.
OR you can create those scrollbars in runtime - you need to search for Windows Handle creating routine, (trace TControl.Handle getter method), that might be ReCreateWnd or such, and as GDI handle created - create your scroll-bars over it.
I need a VCL tag editor component for Delphi or C++Builder, similar to what's available for JavaScript: e.g. this one, or this one or StackOverflow's own tags editor.
Is there something like this available or do I need to make it from scratch?
Some specific things that I need are:
Editor should allow either scrolling or become multi-line if more tags are present than the editor's width allows. If multi-line, there should be an option to define some maximum height however, preventing it from becoming too tall
Option to select whether tags are created when pressing space or comma key
Prompt text in the editor, when it is not focused (for example "Add new tag")
Ideally, you should be able to move between tags (highlighting them) using the keyboard arrows, so you can delete any tag using the keyboard only
Of course you want to do this yourself! Writing GUI controls is fun and rewarding!
You could do something like
unit TagEditor;
interface
uses
Windows, Messages, SysUtils, Classes, Controls, StdCtrls, Forms, Graphics,
Types, Menus;
type
TClickInfo = cardinal;
GetTagIndex = word;
const TAG_LOW = 0;
const TAG_HIGH = MAXWORD - 2;
const EDITOR = MAXWORD - 1;
const NOWHERE = MAXWORD;
const PART_BODY = $00000000;
const PART_REMOVE_BUTTON = $00010000;
function GetTagPart(ClickInfo: TClickInfo): cardinal;
type
TTagClickEvent = procedure(Sender: TObject; TagIndex: integer;
const TagCaption: string) of object;
TRemoveConfirmEvent = procedure(Sender: TObject; TagIndex: integer;
const TagCaption: string; var CanRemove: boolean) of object;
TTagEditor = class(TCustomControl)
private
{ Private declarations }
FTags: TStringList;
FEdit: TEdit;
FBgColor: TColor;
FBorderColor: TColor;
FTagBgColor: TColor;
FTagBorderColor: TColor;
FSpacing: integer;
FTextColor: TColor;
FLefts, FRights, FWidths,
FTops, FBottoms: array of integer;
FCloseBtnLefts, FCloseBtnTops: array of integer;
FCloseBtnWidth: integer;
FSpaceAccepts: boolean;
FCommaAccepts: boolean;
FSemicolonAccepts: boolean;
FTrimInput: boolean;
FNoLeadingSpaceInput: boolean;
FTagClickEvent: TTagClickEvent;
FAllowDuplicates: boolean;
FPopupMenu: TPopupMenu;
FMultiLine: boolean;
FTagHeight: integer;
FEditPos: TPoint;
FActualTagHeight: integer;
FShrunk: boolean;
FEditorColor: TColor;
FTagAdded: TNotifyEvent;
FTagRemoved: TNotifyEvent;
FOnChange: TNotifyEvent;
FOnRemoveConfirm: TRemoveConfirmEvent;
FMouseDownClickInfo: TClickInfo;
FCaretVisible: boolean;
FDragging: boolean;
FAutoHeight: boolean;
FNumRows: integer;
procedure SetBorderColor(const Value: TColor);
procedure SetTagBgColor(const Value: TColor);
procedure SetTagBorderColor(const Value: TColor);
procedure SetSpacing(const Value: integer);
procedure TagChange(Sender: TObject);
procedure SetTags(const Value: TStringList);
procedure SetTextColor(const Value: TColor);
procedure ShowEditor;
procedure HideEditor;
procedure EditKeyPress(Sender: TObject; var Key: Char);
procedure mnuDeleteItemClick(Sender: TObject);
procedure SetMultiLine(const Value: boolean);
procedure SetTagHeight(const Value: integer);
procedure EditExit(Sender: TObject);
function Accept: boolean;
procedure SetBgColor(const Value: TColor);
function GetClickInfoAt(X, Y: integer): TClickInfo;
function GetSeparatorIndexAt(X, Y: integer): integer;
procedure CreateCaret;
procedure DestroyCaret;
function IsFirstOnRow(TagIndex: integer): boolean; inline;
function IsLastOnRow(TagIndex: integer): boolean;
procedure SetAutoHeight(const Value: boolean);
protected
{ Protected declarations }
procedure Paint; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure KeyPress(var Key: Char); override;
procedure WndProc(var Message: TMessage); override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
property TabOrder;
property TabStop;
property Color;
property Anchors;
property Align;
property Tag;
property Cursor;
property BgColor: TColor read FBgColor write SetBgColor;
property BorderColor: TColor read FBorderColor write SetBorderColor;
property TagBgColor: TColor read FTagBgColor write SetTagBgColor;
property TagBorderColor: TColor read FTagBorderColor
write SetTagBorderColor;
property Spacing: integer read FSpacing write SetSpacing;
property Tags: TStringList read FTags write SetTags;
property TextColor: TColor read FTextColor write SetTextColor;
property SpaceAccepts: boolean read FSpaceAccepts write FSpaceAccepts
default true;
property CommaAccepts: boolean read FCommaAccepts write FCommaAccepts
default true;
property SemicolonAccepts: boolean read FSemicolonAccepts
write FSemicolonAccepts default true;
property TrimInput: boolean read FTrimInput write FTrimInput default true;
property NoLeadingSpaceInput: boolean read FNoLeadingSpaceInput
write FNoLeadingSpaceInput default true;
property AllowDuplicates: boolean read FAllowDuplicates write FAllowDuplicates
default false;
property MultiLine: boolean read FMultiLine write SetMultiLine default false;
property TagHeight: integer read FTagHeight write SetTagHeight default 32;
property EditorColor: TColor read FEditorColor write FEditorColor
default clWindow;
property AutoHeight: boolean read FAutoHeight write SetAutoHeight;
property OnTagClick: TTagClickEvent read FTagClickEvent write FTagClickEvent;
property OnTagAdded: TNotifyEvent read FTagAdded write FTagAdded;
property OnTagRemoved: TNotifyEvent read FTagRemoved write FTagRemoved;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property OnRemoveConfirm: TRemoveConfirmEvent read FOnRemoveConfirm
write FOnRemoveConfirm;
end;
procedure Register;
implementation
uses Math, Clipbrd;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TTagEditor]);
end;
function IsKeyDown(const VK: integer): boolean;
begin
IsKeyDown := GetKeyState(VK) and $8000 <> 0;
end;
function GetTagPart(ClickInfo: TClickInfo): cardinal;
begin
result := ClickInfo and $FFFF0000;
end;
{ TTagEditor }
constructor TTagEditor.Create(AOwner: TComponent);
var
mnuItem: TMenuItem;
begin
inherited;
FEdit := TEdit.Create(Self);
FEdit.Parent := Self;
FEdit.BorderStyle := bsNone;
FEdit.Visible := false;
FEdit.OnKeyPress := EditKeyPress;
FEdit.OnExit := EditExit;
FTags := TStringList.Create;
FTags.OnChange := TagChange;
FBgColor := clWindow;
FBorderColor := clWindowFrame;
FTagBgColor := clSkyBlue;
FTagBorderColor := clNavy;
FSpacing := 8;
FTextColor := clWhite;
FSpaceAccepts := true;
FCommaAccepts := true;
FSemicolonAccepts := true;
FTrimInput := true;
FNoLeadingSpaceInput := true;
FAllowDuplicates := false;
FMultiLine := false;
FTagHeight := 32;
FShrunk := false;
FEditorColor := clWindow;
FCaretVisible := false;
FDragging := false;
FPopupMenu := TPopupMenu.Create(Self);
mnuItem := TMenuItem.Create(PopupMenu);
mnuItem.Caption := 'Delete';
mnuItem.OnClick := mnuDeleteItemClick;
mnuItem.Hint := 'Deletes the selected tag.';
FPopupMenu.Items.Add(mnuItem);
TabStop := true;
end;
procedure TTagEditor.EditExit(Sender: TObject);
begin
if FEdit.Text <> '' then
Accept
else
HideEditor;
end;
procedure TTagEditor.mnuDeleteItemClick(Sender: TObject);
begin
if Sender is TMenuItem then
begin
FTags.Delete(TMenuItem(Sender).Tag);
if Assigned(FTagRemoved) then
FTagRemoved(Self);
end;
end;
procedure TTagEditor.TagChange(Sender: TObject);
begin
Invalidate;
if Assigned(FOnChange) then
FOnChange(Self);
end;
procedure TTagEditor.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
WM_SETFOCUS:
Invalidate;
WM_KILLFOCUS:
begin
if FCaretVisible then DestroyCaret;
FDragging := false;
Invalidate;
end;
WM_COPY:
Clipboard.AsText := FTags.DelimitedText;
WM_CLEAR:
FTags.Clear;
WM_CUT:
begin
Clipboard.AsText := FTags.DelimitedText;
FTags.Clear;
end;
WM_PASTE:
begin
if Clipboard.HasFormat(CF_TEXT) then
if FTags.Count = 0 then
FTags.DelimitedText := Clipboard.AsText
else
FTags.DelimitedText := FTags.DelimitedText + ',' + Clipboard.AsText;
end;
end;
end;
function TTagEditor.Accept: boolean;
begin
Assert(FEdit.Visible);
result := false;
if FTrimInput then
FEdit.Text := Trim(FEdit.Text);
if (FEdit.Text = '') or
((not AllowDuplicates) and (FTags.IndexOf(FEdit.Text) <> -1)) then
begin
beep;
Exit;
end;
FTags.Add(FEdit.Text);
result := true;
HideEditor;
if Assigned(FTagAdded) then
FTagAdded(Self);
Invalidate;
end;
procedure TTagEditor.EditKeyPress(Sender: TObject; var Key: Char);
begin
if (Key = chr(VK_SPACE)) and (FEdit.Text = '') and FNoLeadingSpaceInput then
begin
Key := #0;
Exit;
end;
if ((Key = chr(VK_SPACE)) and FSpaceAccepts) or
((Key = ',') and FCommaAccepts) or
((Key = ';') and FSemicolonAccepts) then
Key := chr(VK_RETURN);
case ord(Key) of
VK_RETURN:
begin
Accept;
ShowEditor;
Key := #0;
end;
VK_BACK:
begin
if (FEdit.Text = '') and (FTags.Count > 0) then
begin
FTags.Delete(FTags.Count - 1);
if Assigned(FTagRemoved) then
FTagRemoved(Sender);
end;
end;
VK_ESCAPE:
begin
HideEditor;
Self.SetFocus;
Key := #0;
end;
end;
end;
destructor TTagEditor.Destroy;
begin
FPopupMenu.Free;
FTags.Free;
FEdit.Free;
inherited;
end;
procedure TTagEditor.HideEditor;
begin
FEdit.Text := '';
FEdit.Hide;
// SetFocus;
end;
procedure TTagEditor.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited;
case Key of
VK_END:
ShowEditor;
VK_DELETE:
Perform(WM_CLEAR, 0, 0);
VK_INSERT:
Perform(WM_PASTE, 0, 0);
end;
end;
procedure TTagEditor.KeyPress(var Key: Char);
begin
inherited;
case Key of
^C:
begin
Perform(WM_COPY, 0, 0);
Key := #0;
Exit;
end;
^X:
begin
Perform(WM_CUT, 0, 0);
Key := #0;
Exit;
end;
^V:
begin
Perform(WM_PASTE, 0, 0);
Key := #0;
Exit;
end;
end;
ShowEditor;
FEdit.Perform(WM_CHAR, ord(Key), 0);
end;
function TTagEditor.GetClickInfoAt(X, Y: integer): TClickInfo;
var
i: integer;
begin
result := NOWHERE;
if (X >= FEditPos.X) and (Y >= FEditPos.Y) then
Exit(EDITOR);
for i := 0 to FTags.Count - 1 do
if InRange(X, FLefts[i], FRights[i]) and InRange(Y, FTops[i], FBottoms[i]) then
begin
result := i;
if InRange(X, FCloseBtnLefts[i], FCloseBtnLefts[i] + FCloseBtnWidth) and
InRange(Y, FCloseBtnTops[i], FCloseBtnTops[i] + FActualTagHeight) and
not FShrunk then
result := result or PART_REMOVE_BUTTON;
break;
end;
end;
function TTagEditor.IsFirstOnRow(TagIndex: integer): boolean;
begin
result := (TagIndex = 0) or (FTops[TagIndex] > FTops[TagIndex-1]);
end;
function TTagEditor.IsLastOnRow(TagIndex: integer): boolean;
begin
result := (TagIndex = FTags.Count - 1) or (FTops[TagIndex] < FTops[TagIndex+1]);
end;
function TTagEditor.GetSeparatorIndexAt(X, Y: integer): integer;
var
i: Integer;
begin
result := FTags.Count;
Y := Max(Y, FSpacing + 1);
for i := FTags.Count - 1 downto 0 do
begin
if Y < FTops[i] then Continue;
if (IsLastOnRow(i) and (X >= FRights[i])) or
((X < FRights[i]) and (IsFirstOnRow(i) or (FRights[i-1] < X))) then
begin
result := i;
if (IsLastOnRow(i) and (X >= FRights[i])) then inc(result);
Exit;
end;
end;
end;
procedure TTagEditor.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
FMouseDownClickInfo := GetClickInfoAt(X, Y);
if GetTagIndex(FMouseDownClickInfo) <> EDITOR then
SetFocus;
end;
procedure TTagEditor.CreateCaret;
begin
if not FCaretVisible then
FCaretVisible := Windows.CreateCaret(Handle, 0, 0, FActualTagHeight);
end;
procedure TTagEditor.DestroyCaret;
begin
if not FCaretVisible then Exit;
Windows.DestroyCaret;
FCaretVisible := false;
end;
procedure TTagEditor.MouseMove(Shift: TShiftState; X, Y: Integer);
var
SepIndex: integer;
begin
inherited;
if IsKeyDown(VK_LBUTTON) and
InRange(GetTagIndex(FMouseDownClickInfo), TAG_LOW, TAG_HIGH) then
begin
FDragging := true;
Screen.Cursor := crDrag;
SepIndex := GetSeparatorIndexAt(X, Y);
TForm(Parent).Caption := IntToStr(SepIndex);
CreateCaret;
if SepIndex = FTags.Count then
SetCaretPos(FLefts[SepIndex - 1] + FWidths[SepIndex - 1] + FSpacing div 2,
FTops[SepIndex - 1])
else
SetCaretPos(FLefts[SepIndex] - FSpacing div 2, FTops[SepIndex]);
ShowCaret(Handle);
Exit;
end;
case GetTagIndex(GetClickInfoAt(X,Y)) of
NOWHERE: Cursor := crArrow;
EDITOR: Cursor := crIBeam;
TAG_LOW..TAG_HIGH: Cursor := crHandPoint;
end;
end;
procedure TTagEditor.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
var
pnt: TPoint;
CanRemove: boolean;
ClickInfo: TClickInfo;
i: word;
p: cardinal;
SepIndex: integer;
begin
inherited;
if FDragging then
begin
DestroyCaret;
FDragging := false;
Screen.Cursor := crDefault;
SepIndex := GetSeparatorIndexAt(X, Y);
if not InRange(SepIndex, GetTagIndex(FMouseDownClickInfo),
GetTagIndex(FMouseDownClickInfo) + 1) then
FTags.Move(GetTagIndex(FMouseDownClickInfo), SepIndex -
IfThen(SepIndex > GetTagIndex(FMouseDownClickInfo), 1, 0));
Exit;
end;
ClickInfo := GetClickInfoAt(X, Y);
if ClickInfo <> FMouseDownClickInfo then Exit;
i := GetTagIndex(ClickInfo);
p := GetTagPart(ClickInfo);
case i of
EDITOR:
ShowEditor;
NOWHERE: ;
else
case Button of
mbLeft:
begin
case p of
PART_BODY:
if Assigned(FTagClickEvent) then
FTagClickEvent(Self, i, FTags[i]);
PART_REMOVE_BUTTON:
begin
if Assigned(FOnRemoveConfirm) then
begin
CanRemove := false;
FOnRemoveConfirm(Self, i, FTags[i], CanRemove);
if not CanRemove then Exit;
end;
FTags.Delete(i);
if Assigned(FTagRemoved) then
FTagRemoved(Self);
end;
end;
end;
mbRight:
begin
FPopupMenu.Items[0].Tag := i;
pnt := ClientToScreen(Point(X,Y));
FPopupMenu.Items[0].Caption := 'Delete tag "' + FTags[i] + '"';
FPopupMenu.Popup(pnt.X, pnt.Y);
end;
end;
end;
end;
procedure TTagEditor.Paint;
var
i: integer;
w: integer;
x, y: integer;
R: TRect;
MeanWidth: integer;
S: string;
DesiredHeight: integer;
begin
inherited;
Canvas.Brush.Color := FBgColor;
Canvas.Pen.Color := FBorderColor;
Canvas.Rectangle(ClientRect);
Canvas.Font.Assign(Self.Font);
SetLength(FLefts, FTags.Count);
SetLength(FRights, FTags.Count);
SetLength(FTops, FTags.Count);
SetLength(FBottoms, FTags.Count);
SetLength(FWidths, FTags.Count);
SetLength(FCloseBtnLefts, FTags.Count);
SetLength(FCloseBtnTops, FTags.Count);
FCloseBtnWidth := Canvas.TextWidth('×');
FShrunk := false;
// Do metrics
FNumRows := 1;
if FMultiLine then
begin
FActualTagHeight := FTagHeight;
x := FSpacing;
y := FSpacing;
for i := 0 to FTags.Count - 1 do
begin
FWidths[i] := Canvas.TextWidth(FTags[i] + ' ×') + 2*FSpacing;
FLefts[i] := x;
FRights[i] := x + FWidths[i];
FTops[i] := y;
FBottoms[i] := y + FTagHeight;
if x + FWidths[i] + FSpacing > ClientWidth then
{ no need to make room for the editor, since it can reside on the next row! }
begin
x := FSpacing;
inc(y, FTagHeight + FSpacing);
inc(FNumRows);
FLefts[i] := x;
FRights[i] := x + FWidths[i];
FTops[i] := y;
FBottoms[i] := y + FTagHeight;
end;
FCloseBtnLefts[i] := x + FWidths[i] - FCloseBtnWidth - FSpacing;
FCloseBtnTops[i] := y;
inc(x, FWidths[i] + FSpacing);
end;
end
else // i.e., not FMultiLine
begin
FActualTagHeight := ClientHeight - 2*FSpacing;
x := FSpacing;
y := FSpacing;
for i := 0 to FTags.Count - 1 do
begin
FWidths[i] := Canvas.TextWidth(FTags[i] + ' ×') + 2*FSpacing;
FLefts[i] := x;
FRights[i] := x + FWidths[i];
FTops[i] := y;
FBottoms[i] := y + FActualTagHeight;
inc(x, FWidths[i] + FSpacing);
FCloseBtnLefts[i] := FRights[i] - FCloseBtnWidth - FSpacing;
FCloseBtnTops[i] := y;
end;
FShrunk := x + 64 {FEdit} > ClientWidth;
if FShrunk then
begin
// Enough to remove close buttons?
x := FSpacing;
y := FSpacing;
for i := 0 to FTags.Count - 1 do
begin
FWidths[i] := Canvas.TextWidth(FTags[i]) + 2*FSpacing;
FLefts[i] := x;
FRights[i] := x + FWidths[i];
FTops[i] := y;
FBottoms[i] := y + FActualTagHeight;
inc(x, FWidths[i] + FSpacing);
FCloseBtnLefts[i] := FRights[i] - FCloseBtnWidth - FSpacing;
FCloseBtnTops[i] := y;
end;
if x + 64 {FEdit} > ClientWidth then // apparently no
begin
MeanWidth := (ClientWidth - 2*FSpacing - 64 {FEdit}) div FTags.Count - FSpacing;
x := FSpacing;
for i := 0 to FTags.Count - 1 do
begin
FWidths[i] := Min(FWidths[i], MeanWidth);
FLefts[i] := x;
FRights[i] := x + FWidths[i];
inc(x, FWidths[i] + FSpacing);
end;
end;
end;
end;
FEditPos := Point(FSpacing, FSpacing + (FActualTagHeight - FEdit.Height) div 2);
if FTags.Count > 0 then
FEditPos := Point(FRights[FTags.Count - 1] + FSpacing,
FTops[FTags.Count - 1] + (FActualTagHeight - FEdit.Height) div 2);
if FMultiLine and (FEditPos.X + 64 > ClientWidth) and (FTags.Count > 0) then
begin
FEditPos := Point(FSpacing,
FTops[FTags.Count - 1] + FTagHeight + FSpacing +
(FActualTagHeight - FEdit.Height) div 2);
inc(FNumRows);
end;
DesiredHeight := FSpacing + FNumRows*(FTagHeight+FSpacing);
if FMultiLine and FAutoHeight and (ClientHeight <> DesiredHeight) then
begin
ClientHeight := DesiredHeight;
Invalidate;
Exit;
end;
// Draw
for i := 0 to FTags.Count - 1 do
begin
x := FLefts[i];
y := FTops[i];
w := FWidths[i];
R := Rect(x, y, x + w, y + FActualTagHeight);
Canvas.Brush.Color := FTagBgColor;
Canvas.Pen.Color := FTagBorderColor;
Canvas.Rectangle(R);
Canvas.Font.Color := FTextColor;
Canvas.Brush.Style := bsClear;
R.Left := R.Left + FSpacing;
S := FTags[i];
if not FShrunk then
S := S + ' ×';
DrawText(Canvas.Handle, PChar(S), -1, R, DT_SINGLELINE or DT_VCENTER or
DT_LEFT or DT_END_ELLIPSIS or DT_NOPREFIX);
Canvas.Brush.Style := bsSolid;
end;
if FEdit.Visible then
begin
FEdit.Left := FEditPos.X;
FEdit.Top := FEditPos.Y;
FEdit.Width := ClientWidth - FEdit.Left - FSpacing;
end;
if Focused then
begin
R := Rect(2, 2, ClientWidth - 2, ClientHeight - 2);
SetBkColor(Canvas.Handle, clWhite);
SetTextColor(clBlack);
Canvas.DrawFocusRect(R);
end;
end;
procedure TTagEditor.SetAutoHeight(const Value: boolean);
begin
if FAutoHeight <> Value then
begin
FAutoHeight := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetBgColor(const Value: TColor);
begin
if FBgColor <> Value then
begin
FBgColor := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetBorderColor(const Value: TColor);
begin
if FBorderColor <> Value then
begin
FBorderColor := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetMultiLine(const Value: boolean);
begin
if FMultiLine <> Value then
begin
FMultiLine := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetTagBgColor(const Value: TColor);
begin
if FTagBgColor <> Value then
begin
FTagBgColor := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetTagBorderColor(const Value: TColor);
begin
if FTagBorderColor <> Value then
begin
FTagBorderColor := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetTagHeight(const Value: integer);
begin
if FTagHeight <> Value then
begin
FTagHeight := Value;
Invalidate;
end;
end;
procedure TTagEditor.SetTags(const Value: TStringList);
begin
FTags.Assign(Value);
Invalidate;
end;
procedure TTagEditor.SetTextColor(const Value: TColor);
begin
if FTextColor <> Value then
begin
FTextColor := Value;
Invalidate;
end;
end;
procedure TTagEditor.ShowEditor;
begin
FEdit.Left := FEditPos.X;
FEdit.Top := FEditPos.Y;
FEdit.Width := ClientWidth - FEdit.Left - FSpacing;
FEdit.Color := FEditorColor;
FEdit.Text := '';
FEdit.Show;
FEdit.SetFocus;
end;
procedure TTagEditor.SetSpacing(const Value: integer);
begin
if FSpacing <> Value then
begin
FSpacing := Value;
Invalidate;
end;
end;
initialization
Screen.Cursors[crHandPoint] := LoadCursor(0, IDC_HAND); // Get the normal hand cursor
end.
which yields
Sample video
Demo (Compiled EXE)
If I get more time later on today I will do some more work on this control, e.g., button highlighting on mouse hover, tag click event, button max width etc.
Update: Added a lot of features.
Update: Added multi-line feature.
Update: More features.
Update: Added clipboard interface, fixed some issues, etc.
Update: Added drag-and-drop reordering and fixed some minor issues. By the way, this is the last version I'll post here. Later versions (if there will be any) will be posted at http://specials.rejbrand.se/dev/controls/.
Update: Added AutoHeight property, made edit box vertically centred, and changed the drag cursor. (Yeah, I couldn't resist making yet another update.)
Im using Delphi 2006.
I have a custom header control that I wrote from scratch. Its almost finished except that I don't know how to draw the semi transparent drag image of the header section when the user is draging a header section in order to change its position.
The THeaderControl in Delphi does this quite nicely however it is a subclass of the windows header control, mine is not, its written from scratch. So I was wondering if there is a windows function that draws this for you or you have to draw it yourself.
Thank you
Implement GetDragImages. E.g. as follows:
type
THeader = class(TCustomControl)
private
FColWidth: Integer;
FDragImages: TDragImageList;
FDragIndex: Integer;
FDragPos: TPoint;
protected
procedure DragOver(Source: TObject; X, Y: Integer; State: TDragState;
var Accept: Boolean); override;
procedure DoEndDrag(Target: TObject; X, Y: Integer); override;
function GetDragImages: TDragImageList; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
end;
{ THeader }
constructor THeader.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := ControlStyle + [csDisplayDragImage];
DragCursor := crNone;
FColWidth := 100;
end;
procedure THeader.DoEndDrag(Target: TObject; X, Y: Integer);
begin
FreeAndNil(FDragImages);
// Eat inherited if you do not publish the default drag events
end;
procedure THeader.DragOver(Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
begin
// Eat inherited if you do not publish the default drag events
Accept := Source = Self;
end;
function THeader.GetDragImages: TDragImageList;
var
Bmp: TBitmap;
begin
if FDragImages = nil then
begin
FDragImages := TDragImageList.Create(nil);
Bmp := TBitmap.Create;
try
Bmp.Width := FColWidth;
Bmp.Height := Height;
BitBlt(Bmp.Canvas.Handle, 0, 0, FColWidth, Height, Canvas.Handle,
FDragIndex * FColWidth, 0, SRCCOPY);
FDragImages.Width := FColWidth;
FDragImages.Height := Height;
FDragImages.SetDragImage(FDragImages.Add(Bmp, nil), FDragPos.X,
FDragPos.Y);
finally
Bmp.Free;
end;
end;
Result := FDragImages;
end;
procedure THeader.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited MouseDown(Button, Shift, X, Y);
FDragIndex := X div FColWidth;
FDragPos.X := X mod FColWidth;
FDragPos.Y := Y;
end;
procedure THeader.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
inherited MouseMove(Shift, X, Y);
if ssLeft in Shift then
BeginDrag(False, Mouse.DragThreshold);
end;
procedure THeader.Paint;
var
i: Integer;
R: TRect;
begin
for i := 0 to 3 do
begin
SetRect(R, i * FColWidth, 0, (i + 1) * FColWidth, Height);
Canvas.Brush.Color := clSilver;
Canvas.Font.Color := clWhite;
DrawFrameControl(Canvas.Handle, R, DFC_BUTTON, DFCS_BUTTONPUSH or
DFCS_PUSHED or DFCS_ADJUSTRECT);
Canvas.TextRect(R, R.Left + 2, R.Top + 2, 'Column ' + IntToStr(i + 1));
end;
end;
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
with THeader.Create(Self) do
begin
SetBounds(0, 100, 500, 30);
Parent := Self;
end;
end;
And if you do not want the vertical movement of the drag image (like in the default THeaderControl), then you have to rebuild the drag image every time the mouse moves. See Drag image change while drag....
Im looking for a delphi component that looks and functions like the Windows 7 control panel buttons when you "view by category". Anybody know if something like this already exists?
I just created a small component that looks sort of what you want. It is double-buffered, and hence completely flicker-free, and works both with visual themes enabled and disabled.
unit TaskButton;
interface
uses
SysUtils, Forms, Messages, Windows, Graphics, Classes, Controls, UxTheme,
ImgList, PNGImage;
type
TIconSource = (isImageList, isPNGImage);
TTaskButtonLinkClickEvent = procedure(Sender: TObject; LinkIndex: integer) of object;
TTaskButton = class(TCustomControl)
private
{ Private declarations }
FCaption: TCaption;
FHeaderRect: TRect;
FImageSpacing: integer;
FLinks: TStrings;
FHeaderHeight: integer;
FLinkHeight: integer;
FLinkSpacing: integer;
FHeaderSpacing: integer;
FLinkRects: array of TRect;
FPrevMouseHoverIndex: integer;
FMouseHoverIndex: integer;
FImages: TImageList;
FImageIndex: TImageIndex;
FIconSource: TIconSource;
FImage: TPngImage;
FBuffer: TBitmap;
FOnLinkClick: TTaskButtonLinkClickEvent;
procedure UpdateMetrics;
procedure SetCaption(const Caption: TCaption);
procedure SetImageSpacing(ImageSpacing: integer);
procedure SetLinkSpacing(LinkSpacing: integer);
procedure SetHeaderSpacing(HeaderSpacing: integer);
procedure SetLinks(Links: TStrings);
procedure SetImages(Images: TImageList);
procedure SetImageIndex(ImageIndex: TImageIndex);
procedure SetIconSource(IconSource: TIconSource);
procedure SetImage(Image: TPngImage);
procedure SwapBuffers;
function ImageWidth: integer;
function ImageHeight: integer;
procedure SetNonThemedHeaderFont;
procedure SetNonThemedLinkFont(Hovering: boolean = false);
protected
{ Protected declarations }
procedure Paint; override;
procedure WndProc(var Message: TMessage); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
property Caption: TCaption read FCaption write SetCaption;
property Links: TStrings read FLinks write SetLinks;
property ImageSpacing: integer read FImageSpacing write SetImageSpacing default 16;
property HeaderSpacing: integer read FHeaderSpacing write SetHeaderSpacing default 2;
property LinkSpacing: integer read FLinkSpacing write SetLinkSpacing default 2;
property Images: TImageList read FImages write SetImages;
property ImageIndex: TImageIndex read FImageIndex write SetImageIndex;
property Image: TPngImage read FImage write SetImage;
property IconSource: TIconSource read FIconSource write SetIconSource default isPNGImage;
property OnLinkClick: TTaskButtonLinkClickEvent read FOnLinkClick write FOnLinkClick;
end;
procedure Register;
implementation
uses Math;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TTaskButton]);
end;
function IsIntInInterval(x, xmin, xmax: integer): boolean; inline;
begin
IsIntInInterval := (xmin <= x) and (x <= xmax);
end;
function PointInRect(const Point: TPoint; const Rect: TRect): boolean; inline;
begin
PointInRect := IsIntInInterval(Point.X, Rect.Left, Rect.Right) and
IsIntInInterval(Point.Y, Rect.Top, Rect.Bottom);
end;
{ TTaskButton }
constructor TTaskButton.Create(AOwner: TComponent);
begin
inherited;
InitThemeLibrary;
FBuffer := TBitmap.Create;
FLinks := TStringList.Create;
FImage := TPngImage.Create;
FImageSpacing := 16;
FHeaderSpacing := 2;
FLinkSpacing := 2;
FPrevMouseHoverIndex := -1;
FMouseHoverIndex := -1;
FIconSource := isPNGImage;
end;
destructor TTaskButton.Destroy;
begin
FLinkRects := nil;
FImage.Free;
FLinks.Free;
FBuffer.Free;
inherited;
end;
function TTaskButton.ImageHeight: integer;
begin
result := 0;
case FIconSource of
isImageList:
if Assigned(FImages) then
result := FImages.Height;
isPNGImage:
if Assigned(FImage) then
result := FImage.Height;
end;
end;
function TTaskButton.ImageWidth: integer;
begin
result := 0;
case FIconSource of
isImageList:
if Assigned(FImages) then
result := FImages.Width;
isPNGImage:
if Assigned(FImage) then
result := FImage.Width;
end;
end;
procedure TTaskButton.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
Paint;
end;
procedure TTaskButton.MouseMove(Shift: TShiftState; X, Y: Integer);
var
i: Integer;
begin
inherited;
FMouseHoverIndex := -1;
for i := 0 to high(FLinkRects) do
if PointInRect(point(X, Y), FLinkRects[i]) then
begin
FMouseHoverIndex := i;
break;
end;
if FMouseHoverIndex <> FPrevMouseHoverIndex then
begin
Cursor := IfThen(FMouseHoverIndex <> -1, crHandPoint, crDefault);
Paint;
end;
FPrevMouseHoverIndex := FMouseHoverIndex;
end;
procedure TTaskButton.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
Paint;
if (FMouseHoverIndex <> -1) and Assigned(FOnLinkClick) then
FOnLinkClick(Self, FMouseHoverIndex);
end;
procedure TTaskButton.Paint;
var
theme: HTHEME;
i: Integer;
pnt: TPoint;
r: PRect;
begin
inherited;
if FLinks.Count <> length(FLinkRects) then
UpdateMetrics;
FBuffer.Canvas.Brush.Color := Color;
FBuffer.Canvas.FillRect(ClientRect);
if GetCursorPos(pnt) then
if PointInRect(Self.ScreenToClient(pnt), ClientRect) then
begin
if UxTheme.UseThemes then
begin
theme := OpenThemeData(Handle, 'BUTTON');
if theme <> 0 then
try
DrawThemeBackground(theme,
FBuffer.Canvas.Handle,
BP_COMMANDLINK,
CMDLS_HOT,
ClientRect,
nil);
finally
CloseThemeData(theme);
end;
end
else
begin
New(r);
try
r^ := ClientRect;
DrawEdge(FBuffer.Canvas.Handle, r^, EDGE_RAISED, BF_RECT);
finally
Dispose(r);
end;
end;
end;
case FIconSource of
isImageList:
if Assigned(FImages) then
FImages.Draw(FBuffer.Canvas, 14, 16, FImageIndex);
isPNGImage:
if Assigned(FImage) then
FBuffer.Canvas.Draw(14, 16, FImage);
end;
if UxTheme.UseThemes then
begin
theme := OpenThemeData(Handle, 'CONTROLPANEL');
if theme <> 0 then
try
DrawThemeText(theme,
FBuffer.Canvas.Handle,
CPANEL_SECTIONTITLELINK,
CPSTL_NORMAL,
PChar(Caption),
length(Caption),
DT_LEFT or DT_END_ELLIPSIS or DT_TOP or DT_SINGLELINE,
0,
FHeaderRect);
for i := 0 to FLinks.Count - 1 do
DrawThemeText(theme,
FBuffer.Canvas.Handle,
CPANEL_CONTENTLINK,
IfThen(FMouseHoverIndex = i, IfThen(csLButtonDown in ControlState, CPCL_PRESSED, CPCL_HOT), CPCL_NORMAL),
PChar(FLinks[i]),
length(FLinks[i]),
DT_LEFT or DT_END_ELLIPSIS or DT_TOP or DT_SINGLELINE,
0,
FLinkRects[i]
);
finally
CloseThemeData(theme);
end;
end
else
begin
SetNonThemedHeaderFont;
DrawText(FBuffer.Canvas.Handle,
PChar(Caption),
-1,
FHeaderRect,
DT_LEFT or DT_END_ELLIPSIS or DT_TOP or DT_SINGLELINE);
for i := 0 to FLinks.Count - 1 do
begin
SetNonThemedLinkFont(FMouseHoverIndex = i);
DrawText(FBuffer.Canvas.Handle,
PChar(FLinks[i]),
-1,
FLinkRects[i],
DT_LEFT or DT_END_ELLIPSIS or DT_TOP or DT_SINGLELINE);
end;
end;
SwapBuffers;
end;
procedure TTaskButton.SetCaption(const Caption: TCaption);
begin
if not SameStr(FCaption, Caption) then
begin
FCaption := Caption;
UpdateMetrics;
Paint;
end;
end;
procedure TTaskButton.SetHeaderSpacing(HeaderSpacing: integer);
begin
if FHeaderSpacing <> HeaderSpacing then
begin
FHeaderSpacing := HeaderSpacing;
UpdateMetrics;
Paint;
end;
end;
procedure TTaskButton.SetIconSource(IconSource: TIconSource);
begin
if FIconSource <> IconSource then
begin
FIconSource := IconSource;
UpdateMetrics;
Paint;
end;
end;
procedure TTaskButton.SetImage(Image: TPngImage);
begin
FImage.Assign(Image);
UpdateMetrics;
Paint;
end;
procedure TTaskButton.SetImageIndex(ImageIndex: TImageIndex);
begin
if FImageIndex <> ImageIndex then
begin
FImageIndex := ImageIndex;
UpdateMetrics;
Paint;
end;
end;
procedure TTaskButton.SetImages(Images: TImageList);
begin
FImages := Images;
UpdateMetrics;
Paint;
end;
procedure TTaskButton.SetImageSpacing(ImageSpacing: integer);
begin
if FImageSpacing <> ImageSpacing then
begin
FImageSpacing := ImageSpacing;
UpdateMetrics;
Paint;
end;
end;
procedure TTaskButton.SetLinks(Links: TStrings);
begin
FLinks.Assign(Links);
UpdateMetrics;
Paint;
end;
procedure TTaskButton.SetLinkSpacing(LinkSpacing: integer);
begin
if FLinkSpacing <> LinkSpacing then
begin
FLinkSpacing := LinkSpacing;
UpdateMetrics;
Paint;
end;
end;
procedure TTaskButton.SwapBuffers;
begin
BitBlt(Canvas.Handle, 0, 0, Width, Height, FBuffer.Canvas.Handle, 0, 0, SRCCOPY);
end;
procedure TTaskButton.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
WM_SIZE:
UpdateMetrics;
CM_MOUSEENTER:
Paint;
CM_MOUSELEAVE:
Paint;
WM_ERASEBKGND:
Message.Result := 1;
end;
end;
procedure TTaskButton.UpdateMetrics;
var
theme: HTHEME;
cr, r: TRect;
i, y: Integer;
begin
FBuffer.SetSize(Width, Height);
SetLength(FLinkRects, FLinks.Count);
if UxTheme.UseThemes then
begin
theme := OpenThemeData(Handle, 'CONTROLPANEL');
if theme <> 0 then
try
with cr do
begin
Top := 10;
Left := ImageWidth + FImageSpacing;
Right := Width - 4;
Bottom := Self.Height;
end;
GetThemeTextExtent(theme,
FBuffer.Canvas.Handle,
CPANEL_SECTIONTITLELINK,
CPSTL_NORMAL,
PChar(Caption),
-1,
DT_LEFT or DT_END_ELLIPSIS or DT_TOP or DT_SINGLELINE,
#cr,
r);
FHeaderHeight := r.Bottom - r.Top;
with FHeaderRect do
begin
Top := 10;
Left := 14 + ImageWidth + FImageSpacing;
Right := Width - 4;
Bottom := Top + FHeaderHeight;
end;
with cr do
begin
Top := 4;
Left := 14 + ImageWidth + FImageSpacing;
Right := Width - 4;
Bottom := Self.Height;
end;
y := FHeaderRect.Bottom + FHeaderSpacing;
for i := 0 to high(FLinkRects) do
begin
GetThemeTextExtent(theme,
FBuffer.Canvas.Handle,
CPANEL_CONTENTLINK,
CPCL_NORMAL,
PChar(FLinks[i]),
-1,
DT_LEFT or DT_END_ELLIPSIS or DT_TOP or DT_SINGLELINE,
#cr,
r);
FLinkHeight := r.Bottom - r.Top;
FLinkRects[i].Left := FHeaderRect.Left;
FLinkRects[i].Top := y;
FLinkRects[i].Right := FLinkRects[i].Left + r.Right - r.Left;
FLinkRects[i].Bottom := FLinkRects[i].Top + FLinkHeight + FLinkSpacing;
inc(y, FLinkHeight + FLinkSpacing);
end;
finally
CloseThemeData(theme);
end;
end
else
begin
SetNonThemedHeaderFont;
FHeaderHeight := FBuffer.Canvas.TextHeight(FCaption);
with FHeaderRect do
begin
Top := 10;
Left := 14 + ImageWidth + FImageSpacing;
Right := Width - 4;
Bottom := Top + FHeaderHeight;
end;
SetNonThemedLinkFont;
y := FHeaderRect.Bottom + FHeaderSpacing;
for i := 0 to high(FLinkRects) do
with FBuffer.Canvas.TextExtent(FLinks[i]) do
begin
FLinkHeight := cy;
FLinkRects[i].Left := FHeaderRect.Left;
FLinkRects[i].Top := y;
FLinkRects[i].Right := FLinkRects[i].Left + cx;
FLinkRects[i].Bottom := FLinkRects[i].Top + FLinkHeight + FLinkSpacing;
inc(y, FLinkHeight + FLinkSpacing);
end;
end;
end;
procedure TTaskButton.SetNonThemedHeaderFont;
begin
with FBuffer.Canvas.Font do
begin
Color := clNavy;
Style := [];
Size := 14;
end;
end;
procedure TTaskButton.SetNonThemedLinkFont(Hovering: boolean = false);
begin
with FBuffer.Canvas.Font do
begin
Color := clNavy;
if Hovering then
Style := [fsUnderline]
else
Style := [];
Size := 10;
end;
end;
initialization
// Override Delphi's ugly hand cursor with the nice Windows hand cursor
Screen.Cursors[crHandPoint] := LoadCursor(0, IDC_HAND);
end.
Screenshots:
If I get time over I will add a keyboard interface to it.
I guess this is a customized ListView with activated Tile View.
See "About List-View Controls" on MSDN.
That is part of the Windows shell. It looks like these components wrap the windows shell functionality.
I used this on a form and created it like 10 times. That was ok, until I tried to pass this number. Then it started eating system resources. Is there any way I could create a component like this? It is for a Simulator project, 8bits needed to indicate the value of the register in binary.
any help, comments, ideas are really appreciated.
ty.
I was slightly bored, and I wanted to play with my new Delphi XE, so I've made a component for you. It should work in older Delphi's just fine.
You can download it here: BitEditSample.zip
How does it work?
It inherits from customcontrol, so you can focus the component.
It contains an array of labels and checkboxes.
The bit number is stored in the "tag" property of each checkbox
Each checkbox gets an onchange handler that reads the tag, to see which bit needs to be manipulated.
How to use it
It has a property "value". If you change it, the checkboxes will update.
If you click the checkboxes, the value will change.
Set the property "caption" to change the text that says "Register X:"
You can create an "onchange" event handler, so that when the value changes (because of a mouseclick for example), you'll be notified.
The zipfile contains a component, a package, and a sample application (including a compiled exe, so you can try it out quickly).
unit BitEdit;
interface
uses
SysUtils, Classes, Controls, StdCtrls, ExtCtrls;
type
TBitEdit = class(TCustomControl)
private
FValue : Byte; // store the byte value internally
FBitLabels : Array[0..7] of TLabel; // the 7 6 5 4 3 2 1 0 labels
FBitCheckboxes : Array[0..7] of TCheckBox;
FCaptionLabel : TLabel;
FOnChange : TNotifyEvent;
function GetValue: byte;
procedure SetValue(const aValue: byte);
procedure SetCaption(const aValue: TCaption);
procedure SetOnChange(const aValue: TNotifyEvent);
function GetCaption: TCaption;
{ Private declarations }
protected
{ Protected declarations }
procedure DoBitCheckboxClick(Sender:TObject);
procedure UpdateGUI;
procedure DoOnChange;
public
constructor Create(AOwner: TComponent); override;
{ Public declarations }
published
property Value:byte read GetValue write SetValue;
property Caption:TCaption read GetCaption write SetCaption;
property OnChange:TNotifyEvent read FOnChange write SetOnChange;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TBitEdit]);
end;
{ TBitEdit }
constructor TBitEdit.Create(AOwner: TComponent);
var
I:Integer;
begin
inherited;
Width := 193;
Height := 33;
FCaptionLabel := TLabel.Create(self);
FCaptionLabel.Left := 0;
FCaptionLabel.Top := 10;
FCaptionLabel.Caption := 'Register X :';
FCaptionLabel.Width := 60;
FCaptionLabel.Parent := self;
FCaptionLabel.Show;
for I := 0 to 7 do
begin
FBitCheckboxes[I] := TCheckBox.Create(self);
FBitCheckboxes[I].Parent := self;
FBitCheckboxes[I].Left := 5 + FCaptionLabel.Width + (16 * I);
FBitCheckboxes[I].Top := 14;
FBitCheckboxes[I].Caption := '';
FBitCheckboxes[I].Tag := 7-I;
FBitCheckboxes[I].Hint := 'bit ' + IntToStr(FBitCheckboxes[I].Tag);
FBitCheckboxes[I].OnClick := DoBitCheckboxClick;
end;
for I := 0 to 7 do
begin
FBitLabels[I] := TLabel.Create(Self);
FBitLabels[I].Parent := self;
FBitLabels[I].Left := 8 + FCaptionLabel.Width + (16 * I);
FBitLabels[I].Top := 0;
FBitLabels[I].Caption := '';
FBitLabels[I].Tag := 7-I;
FBitLabels[I].Hint := 'bit ' + IntToStr(FBitLabels[I].Tag);
FBitLabels[I].Caption := IntToStr(FBitLabels[I].Tag);
FBitLabels[I].OnClick := DoBitCheckboxClick;
end;
end;
procedure TBitEdit.DoBitCheckboxClick(Sender: TObject);
var
LCheckbox:TCheckbox;
FOldValue:Byte;
begin
if not (Sender is TCheckBox) then
Exit;
FOldValue := FValue;
LCheckbox := Sender as TCheckbox;
FValue := FValue XOR (1 shl LCheckbox.Tag);
if FOldValue <> FValue then
DoOnChange;
end;
procedure TBitEdit.DoOnChange;
begin
if Assigned(FOnChange) then
FOnChange(Self);
end;
function TBitEdit.GetCaption: TCaption;
begin
Result := FCaptionLabel.Caption;
end;
function TBitEdit.GetValue: byte;
begin
Result := FValue;
end;
procedure TBitEdit.SetCaption(const aValue: TCaption);
begin
FCaptionLabel.Caption := aValue;
end;
procedure TBitEdit.SetOnChange(const aValue: TNotifyEvent);
begin
FOnChange := aValue;
end;
procedure TBitEdit.SetValue(const aValue: byte);
begin
if aValue=FValue then
Exit;
FValue := aValue;
DoOnChange;
UpdateGUI;
end;
procedure TBitEdit.UpdateGUI;
var
I:Integer;
begin
for I := 0 to 7 do
FBitCheckboxes[I].Checked := FValue shr FBitCheckboxes[I].Tag mod 2 = 1;
end;
end.
Resources
I guess the problem that the OP was facing is a feedback loop, where two event handlers call each other.
Other resources don't seem to increase in an unusual way when using more bit editors. I've tested it with an application with many instances of the bit edit component:
[MANY] | [1]
-------------------------+--------------
#Handles |
User : 314 | 35
GDI : 57 | 57
System : 385 | 385
#Memory |
Physical : 8264K | 7740K
Virtual : 3500K | 3482K
#CPU |
Kernel time: 0:00:00.468 | 0:00:00.125
User time : 0:00:00.109 | 0:00:00.062
I agree that there shouldn't be a problem with a hundred checkboxes on a form. But for fun's sake, I just wrote a component that does all drawing manually, so there is only one window handle per control (that is, per eight checkboxes). My control works both with visual themes enabled and with themes disabled. It is also double-buffered, and completely flicker-free.
unit ByteEditor;
interface
uses
Windows, SysUtils, Classes, Messages, Controls, Graphics, Themes, UxTheme;
type
TWinControlCracker = class(TWinControl); // because necessary method SelectNext is protected...
TByteEditor = class(TCustomControl)
private
{ Private declarations }
FTextLabel: TCaption;
FBuffer: TBitmap;
FValue: byte;
CheckboxRect: array[0..7] of TRect;
LabelRect: array[0..7] of TRect;
FSpacing: integer;
FVerticalSpacing: integer;
FLabelSpacing: integer;
FLabelWidth, FLabelHeight: integer;
FShowHex: boolean;
FHexPrefix: string;
FMouseHoverIndex: integer;
FKeyboardFocusIndex: integer;
FOnChange: TNotifyEvent;
FManualLabelWidth: integer;
FAutoLabelSize: boolean;
FLabelAlignment: TAlignment;
procedure SetTextLabel(const TextLabel: TCaption);
procedure SetValue(const Value: byte);
procedure SetSpacing(const Spacing: integer);
procedure SetVerticalSpacing(const VerticalSpacing: integer);
procedure SetLabelSpacing(const LabelSpacing: integer);
procedure SetShowHex(const ShowHex: boolean);
procedure SetHexPrefix(const HexPrefix: string);
procedure SetManualLabelWidth(const ManualLabelWidth: integer);
procedure SetAutoLabelSize(const AutoLabelSize: boolean);
procedure SetLabelAlignment(const LabelAlignment: TAlignment);
procedure UpdateMetrics;
protected
{ Protected declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Paint; override;
procedure WndProc(var Msg: TMessage); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure KeyUp(var Key: Word; Shift: TShiftState); override;
public
{ Public declarations }
published
{ Published declarations }
property Color;
property LabelAlignment: TAlignment read FLabelAlignment write SetLabelAlignment default taRightJustify;
property AutoLabelSize: boolean read FAutoLabelSize write SetAutoLabelSize default true;
property ManualLabelWidth: integer read FManualLabelWidth write SetManualLabelWidth default 64;
property TextLabel: TCaption read FTextLabel write SetTextLabel;
property Value: byte read FValue write SetValue default 0;
property Spacing: integer read FSpacing write SetSpacing default 3;
property VerticalSpacing: integer read FVerticalSpacing write SetVerticalSpacing default 3;
property LabelSpacing: integer read FLabelSpacing write SetLabelSpacing default 8;
property ShowHex: boolean read FShowHex write SetShowHex default false;
property HexPrefix: string read FHexPrefix write SetHexPrefix;
property TabOrder;
property TabStop;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
end;
procedure Register;
implementation
const
PowersOfTwo: array[0..7] of byte = (1, 2, 4, 8, 16, 32, 64, 128); // PowersOfTwo[n] := 2^n
BasicCheckbox: TThemedElementDetails = (Element: teButton; Part: BP_CHECKBOX; State: CBS_UNCHECKEDNORMAL);
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TByteEditor]);
end;
function IsIntInInterval(x, xmin, xmax: integer): boolean; inline;
begin
IsIntInInterval := (xmin <= x) and (x <= xmax);
end;
function PointInRect(const Point: TPoint; const Rect: TRect): boolean; inline;
begin
PointInRect := IsIntInInterval(Point.X, Rect.Left, Rect.Right) and
IsIntInInterval(Point.Y, Rect.Top, Rect.Bottom);
end;
function GrowRect(const Rect: TRect): TRect;
begin
result.Left := Rect.Left - 1;
result.Top := Rect.Top - 1;
result.Right := Rect.Right + 1;
result.Bottom := Rect.Bottom + 1;
end;
{ TByteEditor }
constructor TByteEditor.Create(AOwner: TComponent);
begin
inherited;
FLabelAlignment := taRightJustify;
FManualLabelWidth := 64;
FAutoLabelSize := true;
FTextLabel := 'Register:';
FValue := 0;
FSpacing := 3;
FVerticalSpacing := 3;
FLabelSpacing := 8;
FMouseHoverIndex := -1;
FKeyboardFocusIndex := 7;
FHexPrefix := '$';
FShowHex := false;
FBuffer := TBitmap.Create;
end;
destructor TByteEditor.Destroy;
begin
FBuffer.Free;
inherited;
end;
procedure TByteEditor.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited;
case Key of
VK_TAB:
if TabStop then
begin
if ssShift in Shift then
if FKeyboardFocusIndex = 7 then
TWinControlCracker(Parent).SelectNext(Self, false, true)
else
inc(FKeyboardFocusIndex)
else
if FKeyboardFocusIndex = 0 then
TWinControlCracker(Parent).SelectNext(Self, true, true)
else
dec(FKeyboardFocusIndex);
Paint;
end;
VK_SPACE:
SetValue(FValue xor PowersOfTwo[FKeyboardFocusIndex]);
end;
end;
procedure TByteEditor.KeyUp(var Key: Word; Shift: TShiftState);
begin
inherited;
end;
procedure TByteEditor.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
if TabStop then SetFocus;
FKeyboardFocusIndex := FMouseHoverIndex;
Paint;
end;
procedure TByteEditor.MouseMove(Shift: TShiftState; X, Y: Integer);
var
i: Integer;
OldIndex: integer;
begin
inherited;
OldIndex := FMouseHoverIndex;
FMouseHoverIndex := -1;
for i := 0 to 7 do
if PointInRect(point(X, Y), CheckboxRect[i]) then
begin
FMouseHoverIndex := i;
break;
end;
if FMouseHoverIndex <> OldIndex then
Paint;
end;
procedure TByteEditor.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
Paint;
if (FMouseHoverIndex <> -1) and (Button = mbLeft) then
begin
SetValue(FValue xor PowersOfTwo[FMouseHoverIndex]);
if Assigned(FOnChange) then
FOnChange(Self);
end;
end;
const
DTAlign: array[TAlignment] of cardinal = (DT_LEFT, DT_RIGHT, DT_CENTER);
procedure TByteEditor.Paint;
var
details: TThemedElementDetails;
i: Integer;
TextRect: TRect;
HexStr: string;
begin
inherited;
FBuffer.Canvas.Brush.Color := Color;
FBuffer.Canvas.FillRect(ClientRect);
TextRect := Rect(0, 0, FLabelWidth, Height);
DrawText(FBuffer.Canvas.Handle, FTextLabel, length(FTextLabel), TextRect,
DT_SINGLELINE or DT_VCENTER or DTAlign[FLabelAlignment] or DT_NOCLIP);
for i := 0 to 7 do
begin
if ThemeServices.ThemesEnabled then
with details do
begin
Element := teButton;
Part := BP_CHECKBOX;
if FMouseHoverIndex = i then
if csLButtonDown in ControlState then
if FValue and PowersOfTwo[i] <> 0 then
State := CBS_CHECKEDPRESSED
else
State := CBS_UNCHECKEDPRESSED
else
if FValue and PowersOfTwo[i] <> 0 then
State := CBS_CHECKEDHOT
else
State := CBS_UNCHECKEDHOT
else
if FValue and PowersOfTwo[i] <> 0 then
State := CBS_CHECKEDNORMAL
else
State := CBS_UNCHECKEDNORMAL;
ThemeServices.DrawElement(FBuffer.Canvas.Handle, details, CheckboxRect[i]);
end
else
begin
if FMouseHoverIndex = i then
if csLButtonDown in ControlState then
if FValue and PowersOfTwo[i] <> 0 then
DrawFrameControl(FBuffer.Canvas.Handle, CheckboxRect[i], DFC_BUTTON, DFCS_BUTTONCHECK or DFCS_CHECKED or DFCS_PUSHED)
else
DrawFrameControl(FBuffer.Canvas.Handle, CheckboxRect[i], DFC_BUTTON, DFCS_BUTTONCHECK or DFCS_PUSHED)
else
if FValue and PowersOfTwo[i] <> 0 then
DrawFrameControl(FBuffer.Canvas.Handle, CheckboxRect[i], DFC_BUTTON, DFCS_BUTTONCHECK or DFCS_CHECKED or DFCS_HOT)
else
DrawFrameControl(FBuffer.Canvas.Handle, CheckboxRect[i], DFC_BUTTON, DFCS_BUTTONCHECK or DFCS_HOT)
else
if FValue and PowersOfTwo[i] <> 0 then
DrawFrameControl(FBuffer.Canvas.Handle, CheckboxRect[i], DFC_BUTTON, DFCS_BUTTONCHECK or DFCS_CHECKED)
else
DrawFrameControl(FBuffer.Canvas.Handle, CheckboxRect[i], DFC_BUTTON, DFCS_BUTTONCHECK)
end;
TextRect := LabelRect[i];
DrawText(FBuffer.Canvas.Handle, IntToStr(i), 1, TextRect, DT_SINGLELINE or DT_TOP or DT_CENTER or DT_NOCLIP);
end;
if Focused then
DrawFocusRect(FBuffer.Canvas.Handle, GrowRect(CheckboxRect[FKeyboardFocusIndex]));
if FShowHex then
begin
TextRect.Left := CheckboxRect[7].Left;
TextRect.Right := CheckboxRect[0].Right;
TextRect.Top := CheckboxRect[7].Bottom + FVerticalSpacing;
TextRect.Bottom := TextRect.Top + FLabelHeight;
HexStr := 'Value = ' + IntToStr(FValue) + ' (' + FHexPrefix + IntToHex(FValue, 2) + ')';
DrawText(FBuffer.Canvas.Handle, HexStr, length(HexStr), TextRect,
DT_SINGLELINE or DT_CENTER or DT_NOCLIP);
end;
BitBlt(Canvas.Handle, 0, 0, Width, Height, FBuffer.Canvas.Handle, 0, 0, SRCCOPY);
end;
procedure TByteEditor.SetShowHex(const ShowHex: boolean);
begin
if ShowHex <> FShowHex then
begin
FShowHex := ShowHex;
Paint;
end;
end;
procedure TByteEditor.SetSpacing(const Spacing: integer);
begin
if Spacing <> FSpacing then
begin
FSpacing := Spacing;
UpdateMetrics;
Paint;
end;
end;
procedure TByteEditor.SetVerticalSpacing(const VerticalSpacing: integer);
begin
if VerticalSpacing <> FVerticalSpacing then
begin
FVerticalSpacing := VerticalSpacing;
UpdateMetrics;
Paint;
end;
end;
procedure TByteEditor.SetAutoLabelSize(const AutoLabelSize: boolean);
begin
if FAutoLabelSize <> AutoLabelSize then
begin
FAutoLabelSize := AutoLabelSize;
UpdateMetrics;
Paint;
end;
end;
procedure TByteEditor.SetHexPrefix(const HexPrefix: string);
begin
if not SameStr(FHexPrefix, HexPrefix) then
begin
FHexPrefix := HexPrefix;
Paint;
end;
end;
procedure TByteEditor.SetLabelAlignment(const LabelAlignment: TAlignment);
begin
if FLabelAlignment <> LabelAlignment then
begin
FLabelAlignment := LabelAlignment;
Paint;
end;
end;
procedure TByteEditor.SetLabelSpacing(const LabelSpacing: integer);
begin
if LabelSpacing <> FLabelSpacing then
begin
FLabelSpacing := LabelSpacing;
UpdateMetrics;
Paint;
end;
end;
procedure TByteEditor.SetManualLabelWidth(const ManualLabelWidth: integer);
begin
if FManualLabelWidth <> ManualLabelWidth then
begin
FManualLabelWidth := ManualLabelWidth;
UpdateMetrics;
Paint;
end;
end;
procedure TByteEditor.SetTextLabel(const TextLabel: TCaption);
begin
if not SameStr(TextLabel, FTextLabel) then
begin
FTextLabel := TextLabel;
UpdateMetrics;
Paint;
end;
end;
procedure TByteEditor.SetValue(const Value: byte);
begin
if Value <> FValue then
begin
FValue := Value;
Paint;
end;
end;
procedure TByteEditor.WndProc(var Msg: TMessage);
begin
inherited;
case Msg.Msg of
WM_GETDLGCODE:
Msg.Result := Msg.Result or DLGC_WANTTAB or DLGC_WANTARROWS or DLGC_WANTALLKEYS;
WM_ERASEBKGND:
Msg.Result := 1;
WM_SIZE:
begin
UpdateMetrics;
Paint;
end;
WM_SETFOCUS, WM_KILLFOCUS:
Paint;
end;
end;
procedure TByteEditor.UpdateMetrics;
var
CheckboxWidth, CheckboxHeight: integer;
i: Integer;
begin
FBuffer.SetSize(Width, Height);
FBuffer.Canvas.Font.Assign(Font);
with FBuffer.Canvas.TextExtent(FTextLabel) do
begin
if FAutoLabeLSize then
FLabelWidth := cx
else
FLabelWidth := FManualLabelWidth;
FLabelHeight := cy;
end;
CheckboxWidth := GetSystemMetrics(SM_CXMENUCHECK);
CheckboxHeight := GetSystemMetrics(SM_CYMENUCHECK);
for i := 0 to 7 do
begin
with CheckboxRect[i] do
begin
Left := (FLabelWidth + FLabelSpacing) + (7-i) * (CheckboxWidth + FSpacing);
Right := Left + CheckboxWidth;
Top := (Height - (CheckboxHeight)) div 2;
Bottom := Top + CheckboxHeight;
end;
LabelRect[i].Left := CheckboxRect[i].Left;
LabelRect[i].Right := CheckboxRect[i].Right;
LabelRect[i].Top := CheckboxRect[i].Top - FLabelHeight - FVerticalSpacing;
LabelRect[i].Bottom := CheckboxRect[i].Top;
end;
Width := (FLabelWidth + FLabelSpacing) + 8 * (CheckboxWidth + FSpacing);
end;
end.
Example:
(High-Res)
You have these options, in order of difficulty:
Create a frame, and reuse it
Create a compound control (using
maybe a panel, labels and
checkboxes). Each control will
handle its own keyboard/mouse
interaction.
Create a whole new control - all
elements are drawn using the proper
APIs and all keyboard/mouse
interaction is handled by the
control code.