As an exercise for myself, I'm trying to recreate the To-Do app from the (fascinating) todomvc.com web site. The UI looks like this:
A user writes a To-Do item in an Edit box control (above the crossed out "buy milk") and presses Enter. To-Do items appear below.
As you can see, each line includes a stylized radio control, the text, and a button with an image (red x). The button appears when a user hovers the cursor inside the line.
I don't care about the button, having an image, or appearing only upon OnEnter. I can't figure out how to make a similarly styled (ListView? ComboBox?) control with a radio control and button.
I'm using Delphi VCL, but could switch to FMX.
There really isn't any shortcut here: you simply need to write quite a lot of code. The Windows OS doesn't provide anything like this. I would implement from scratch using an empty window with custom GDI painting and mouse and keyboard input processing. It's not difficult at all, but it does require quite a lot of code.
That was a lot of words and no code.
As a remedy, here is a very quick demonstration control based on Direct2D (because I realised I really do need anti aliasing):
unit ItemListBox;
interface
uses
Windows, SysUtils, Types, UITypes, Classes, Controls, Graphics, Generics.Defaults,
Generics.Collections, Forms, Messages, Direct2D, D2D1;
type
TItem = class
strict private
FCaption: TCaption;
FChecked: Boolean;
FTag: NativeInt;
FOnChanged: TNotifyEvent;
procedure Changed;
procedure SetCaption(const Value: TCaption);
procedure SetChecked(const Value: Boolean);
public
property Caption: TCaption read FCaption write SetCaption;
property Checked: Boolean read FChecked write SetChecked;
property Tag: NativeInt read FTag write FTag;
property OnChanged: TNotifyEvent read FOnChanged write FOnChanged;
end;
TPart = (ilbpText, ilbpCheckBox, ilbpClearButton);
TItemListBox = class(TCustomControl)
strict private
FItems: TObjectList<TItem>;
FItemHeight: Integer;
FCanvas: TDirect2DCanvas;
FIndex: Integer;
FPart: TPart;
FMouseDownIndex: Integer;
FMouseDownPart: TPart;
FFocusIndex: Integer;
function GetItem(Index: Integer): TItem;
function GetItemCount: Integer;
procedure ItemChanged(Sender: TObject);
procedure DrawItem(Index: Integer; Item: TItem);
procedure DrawCheckBox(Index: Integer; Item: TItem; Hot: Boolean = False);
procedure DrawClearButton(Index: Integer; Visible: Boolean; Hot: Boolean = False);
function ItemRect(Index: Integer): TRect;
function TextRect(Index: Integer): TRect;
function CheckBoxRect(Index: Integer): TRect;
function ClearButtonRect(Index: Integer): TRect;
procedure CreateDeviceResources;
procedure HitTest(const P: TPoint; out Index: Integer; out Part: TPart);
procedure StateChange(ANewIndex: Integer; ANewPart: TPart);
function CanvasWidth: Integer;
function CanvasHeight: Integer;
protected
procedure Paint; override;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
procedure WMSize(var Message: TWMSize); message WM_SIZE;
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure CreateWnd; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
Y: Integer); override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
procedure WMSetFocus(var Message: TWMSetFocus); message WM_SETFOCUS;
procedure WMKillFocus(var Message: TWMKillFocus); message WM_KILLFOCUS;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Canvas: TDirect2DCanvas read FCanvas;
function AddItem(const ACaption: string; AChecked: Boolean;
ATag: NativeInt = 0): Integer;
procedure RemoveItem(AIndex: Integer);
property Items[Index: Integer]: TItem read GetItem;
property ItemCount: Integer read GetItemCount;
published
property Align;
property AlignWithMargins;
property Anchors;
property Cursor;
property Font;
property Hint;
property PopupMenu;
property TabOrder;
property TabStop default True;
end;
procedure Register;
implementation
uses
Math;
procedure Register;
begin
RegisterComponents('Rejbrand 2020', [TItemListBox]);
end;
function Scale(X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, 96);
end;
{ TItem }
procedure TItem.Changed;
begin
if Assigned(FOnChanged) then
FOnChanged(Self);
end;
procedure TItem.SetCaption(const Value: TCaption);
begin
if FCaption <> Value then
begin
FCaption := Value;
Changed;
end;
end;
procedure TItem.SetChecked(const Value: Boolean);
begin
if FChecked <> Value then
begin
FChecked := Value;
Changed;
end;
end;
{ TItemListBox }
function TItemListBox.AddItem(const ACaption: string; AChecked: Boolean;
ATag: NativeInt): Integer;
var
Item: TItem;
begin
Item := TItem.Create;
Item.Caption := ACaption;
Item.Checked := AChecked;
Item.OnChanged := ItemChanged;
Result := FItems.Add(Item);
InvalidateRect(Handle, ItemRect(Result), True);
end;
function TItemListBox.ClearButtonRect(Index: Integer): TRect;
begin
Result := Rect(CanvasWidth - 32, Index * FItemHeight, CanvasWidth,
(Index + 1) * FItemHeight);
end;
procedure TItemListBox.CMMouseLeave(var Message: TMessage);
begin
inherited;
StateChange(-1, ilbpText);
end;
constructor TItemListBox.Create(AOwner: TComponent);
begin
inherited;
FItems := TObjectList<TItem>.Create;
FItemHeight := 32;
FIndex := -1;
FMouseDownIndex := -1;
FFocusIndex := -1;
Color := clWindow;
TabStop := True;
end;
procedure TItemListBox.CreateDeviceResources;
begin
FreeAndNil(FCanvas);
FCanvas := TDirect2DCanvas.Create(Handle);
end;
procedure TItemListBox.CreateWnd;
begin
inherited;
CreateDeviceResources;
end;
destructor TItemListBox.Destroy;
begin
FreeAndNil(FItems);
FreeAndNil(FCanvas);
inherited;
end;
procedure TItemListBox.DrawClearButton(Index: Integer; Visible: Boolean; Hot: Boolean);
var
R: TRect;
begin
if not Visible then
Exit;
R := ClearButtonRect(Index);
InflateRect(R, -7, -7);
Canvas.Pen.Color := IfThen(Hot, clRed, clMaroon);
Canvas.Pen.Width := 2;
Canvas.MoveTo(R.Left, R.Top);
Canvas.LineTo(R.Right, R.Bottom);
Canvas.MoveTo(R.Right, R.Top);
Canvas.LineTo(R.Left, R.Bottom);
end;
procedure TItemListBox.DrawItem(Index: Integer; Item: TItem);
var
R: TRect;
S: string;
begin
// Background
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Color := clWindowText;
Canvas.Pen.Width := 1;
Canvas.Pen.Style := psSolid;
R := ItemRect(Index);
Canvas.FillRect(R);
// Text
R := TextRect(Index);
S := Item.Caption;
Canvas.Font.Assign(Font);
Canvas.Font.Color := IfThen(Item.Checked, clGrayText, clWindowText);
if Item.Checked then
Canvas.Font.Style := [fsStrikeOut]
else
Canvas.Font.Style := [];
Canvas.TextRect(R, S, [tfSingleLine, tfEndEllipsis, tfVerticalCenter]);
// Check box
DrawCheckBox(Index, Item, (FIndex = Index) and (FPart = ilbpCheckBox));
// Clear button
DrawClearButton(Index, FIndex = Index, (FIndex = Index) and (FPart = ilbpClearButton));
// Focus indicator
if InRange(FFocusIndex, 0, FItems.Count - 1) and Focused then
begin
Canvas.Pen.Color := clSilver;
Canvas.Pen.Width := 1;
Canvas.Pen.Style := psSolid;
Canvas.Brush.Style := bsClear;
R := TextRect(FFocusIndex);
InflateRect(R, 0, -2);
Canvas.Rectangle(R);
end;
end;
procedure TItemListBox.DrawCheckBox(Index: Integer; Item: TItem;
Hot: Boolean);
var
R: TRect;
begin
R := CheckBoxRect(Index);
InflateRect(R, -5, -5);
Canvas.Pen.Color := clSilver;
Canvas.Pen.Width := 1;
Canvas.Brush.Color := IfThen(Hot, clSilver, clWhite);
Canvas.Ellipse(R);
if Assigned(Item) and Item.Checked then
begin
Canvas.Pen.Color := clGreen;
Canvas.Pen.Width := 2;
Canvas.MoveTo(R.Left + R.Width div 5, R.Bottom - R.Height div 2);
Canvas.LineTo(R.Left + Round(R.Width / 2.5), R.Bottom - Round(R.Height / 3.8));
Canvas.LineTo(R.Right - Round(R.Width / 4.5), R.Top + R.Height div 5);
end;
end;
function TItemListBox.GetItem(Index: Integer): TItem;
begin
Result := FItems[Index];
end;
function TItemListBox.GetItemCount: Integer;
begin
Result := FItems.Count;
end;
procedure TItemListBox.HitTest(const P: TPoint; out Index: Integer;
out Part: TPart);
var
i: Integer;
Q: TPoint;
begin
Q.X := MulDiv(P.X, 96, Screen.PixelsPerInch);
Q.Y := MulDiv(P.Y, 96, Screen.PixelsPerInch);
for i := 0 to FItems.Count - 1 do
if ItemRect(i).Contains(Q) then
begin
Index := i;
if CheckBoxRect(i).Contains(Q) then
Part := ilbpCheckBox
else if ClearButtonRect(i).Contains(Q) then
Part := ilbpClearButton
else
Part := ilbpText;
Exit;
end;
Index := -1;
Part := ilbpText;
end;
procedure TItemListBox.ItemChanged(Sender: TObject);
begin
Invalidate;
end;
function TItemListBox.ItemRect(Index: Integer): TRect;
begin
Result := Rect(0, Index * FItemHeight, CanvasWidth, (Index + 1) * FItemHeight);
end;
procedure TItemListBox.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited;
case Key of
VK_DOWN:
if Succ(FFocusIndex) <= FItems.Count - 1 then
begin
Inc(FFocusIndex);
Invalidate;
end;
VK_UP:
if Pred(FFocusIndex) >= 0 then
begin
Dec(FFocusIndex);
Invalidate;
end;
VK_HOME:
if FFocusIndex <> 0 then
begin
FFocusIndex := 0;
Invalidate;
end;
VK_END:
if FFocusIndex <> FItems.Count - 1 then
begin
FFocusIndex := FItems.Count - 1;
Invalidate;
end;
VK_SPACE:
if InRange(FFocusIndex, 0, FItems.Count - 1) then
FItems[FFocusIndex].Checked := not FItems[FFocusIndex].Checked;
VK_DELETE:
if InRange(FFocusIndex, 0, FItems.Count - 1) then
RemoveItem(FFocusIndex);
end;
end;
procedure TItemListBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
if CanFocus then
SetFocus;
HitTest(Point(X, Y), FMouseDownIndex, FMouseDownPart);
if FFocusIndex <> FMouseDownIndex then
begin
FFocusIndex := FMouseDownIndex;
Invalidate;
end;
end;
procedure TItemListBox.MouseMove(Shift: TShiftState; X, Y: Integer);
var
NewIndex: Integer;
NewPart: TPart;
begin
inherited;
HitTest(Point(X, Y), NewIndex, NewPart);
StateChange(NewIndex, NewPart);
end;
procedure TItemListBox.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
var
Index: Integer;
Part: TPart;
begin
HitTest(Point(X, Y), Index, Part);
if (Index <> -1) and (Index = FMouseDownIndex) and (Button = mbLeft) then
begin
if (Part = ilbpCheckBox) and (Part = FMouseDownPart) then
FItems[Index].Checked := not FItems[Index].Checked
else if (Part = ilbpClearButton) and (Part = FMouseDownPart) then
RemoveItem(Index);
end;
end;
procedure TItemListBox.Paint;
var
i: Integer;
begin
Canvas.RenderTarget.Clear(D2D1ColorF(clWhite));
for i := 0 to FItems.Count - 1 do
DrawItem(i, FItems[i]);
end;
procedure TItemListBox.RemoveItem(AIndex: Integer);
begin
FItems.Delete(AIndex);
FFocusIndex := EnsureRange(FFocusIndex, 0, FItems.Count - 1);
Invalidate;
end;
procedure TItemListBox.StateChange(ANewIndex: Integer; ANewPart: TPart);
var
OldIndex: Integer;
OldPart: TPart;
begin
OldIndex := FIndex;
OldPart := FPart;
FIndex := ANewIndex;
FPart := ANewPart;
if FIndex = OldIndex then
begin
if FPart <> OldPart then
begin
if ilbpCheckBox in [FPart, OldPart] then
InvalidateRect(Handle, CheckBoxRect(FIndex), True);
if ilbpClearButton in [FPart, OldPart] then
InvalidateRect(Handle, ClearButtonRect(FIndex), True);
end;
end
else
begin
InvalidateRect(Handle, ItemRect(OldIndex), True);
InvalidateRect(Handle, ItemRect(FIndex), True);
end;
end;
function TItemListBox.CanvasHeight: Integer;
begin
Result := MulDiv(ClientHeight, 96, Screen.PixelsPerInch);
end;
function TItemListBox.CanvasWidth: Integer;
begin
Result := MulDiv(ClientWidth, 96, Screen.PixelsPerInch);
end;
function TItemListBox.CheckBoxRect(Index: Integer): TRect;
begin
Result := Rect(0, Index * FItemHeight, 32, (Index + 1) * FItemHeight);
end;
function TItemListBox.TextRect(Index: Integer): TRect;
begin
Result := Rect(40, Index * FItemHeight, CanvasWidth - 40,
(Index + 1) * FItemHeight);
end;
procedure TItemListBox.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TItemListBox.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
inherited;
Message.Result := Message.Result or DLGC_WANTARROWS;
end;
procedure TItemListBox.WMKillFocus(var Message: TWMKillFocus);
begin
inherited;
Invalidate;
end;
procedure TItemListBox.WMPaint(var Message: TWMPaint);
var
PaintStruct: TPaintStruct;
res: HRESULT;
begin
BeginPaint(Handle, PaintStruct);
try
if Assigned(FCanvas) then
begin
FCanvas.BeginDraw;
try
Paint;
finally
res := FCanvas.RenderTarget.EndDraw;
if res = D2DERR_RECREATE_TARGET then
CreateDeviceResources;
end;
end;
finally
EndPaint(Handle, PaintStruct);
end;
end;
procedure TItemListBox.WMSetFocus(var Message: TWMSetFocus);
begin
inherited;
Invalidate;
end;
procedure TItemListBox.WMSize(var Message: TWMSize);
var
S: TD2DSizeU;
begin
if Assigned(FCanvas) then
begin
S := D2D1SizeU(ClientWidth, ClientHeight);
ID2D1HwndRenderTarget(FCanvas.RenderTarget).Resize(S);
end;
Invalidate;
inherited;
end;
end.
Example (with a simple TEdit at the top):
But please notice that this is not a finished control; it's merely a very primitive sketch or prototype. It is not fully tested. In addition, a real control would have scrolling support and a keyboard interface. Since it is very late in Sweden right now, I don't really have time to add that at the moment.
Update: I added high-DPI support and a keyboard interface (up, down, home, end, space, delete):
How can I put Image in TDBGrid column heading?
I tried, but the image kept showing and kept disappearing when i put the mouse over the title.
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
if Column.FieldName = order then
Begin
Column.Title.Font.Color := clBlue;
//if gdFixed in State then // didn't work.. I don't know why!!!
if Rect.Top < 30 then
ImageList1.Draw(DBGrid1.Canvas, Rect.Right-18, Rect.Top-18, 0);
end
else
Column.Title.Font.Color := clWindowText;
end;
You can use a interposer class for TDBGrid and override the DrawCell procedure.
type
TDBGrid = Class(DBGrids.TDBGrid)
private
FIcon:TIcon;
FImageList: TImageList;
procedure SetImageList(const Value: TImageList);
Destructor Destroy;override;
published
procedure DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState); override;
Property Imagelist: TImageList read FImageList Write SetImageList;
End;
TForm2 = class(TForm)
.......
implementation
{$R *.dfm}
{ TDBGrid }
destructor TDBGrid.Destroy;
begin
if Assigned(FIcon) then FIcon.Free;
inherited;
end;
procedure TDBGrid.SetImageList(const Value: TImageList);
begin
FImageList := Value;
FreeAndNil(FIcon);
if Assigned(FImageList) then
begin
FIcon := TIcon.Create;
FImageList.GetIcon(0, FIcon);
end;
end;
procedure TDBGrid.DrawCell(ACol, ARow: Integer; ARect: TRect; AState: TGridDrawState);
var
L_Col: Integer;
begin
if dgIndicator in Options then
L_Col := ACol - 1
else
L_Col := ACol;
inherited;
if Assigned(FIcon) and (L_Col > -1) and (ARow = 0) and (Columns[L_Col].FieldName = 'ID') and (gdFixed in AState) then
begin
Canvas.Draw(ARect.Right - 18, ARect.Bottom - 18, FIcon);
//FImagelist.Draw(Canvas,ARect.Right - 18, ARect.Bottom - 18,0); // would cause more flickering
end;
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
DBGrid1.DoubleBuffered := true;
DBGrid1.Imagelist := ImageList1;
ReportMemoryLeaksOnShutDown := true;
end;
I've found this great component and installed it, it's running great, but I have a slight problem with it. Which unfortunately I don't know how to do it myself.
Can someone help me add a new feature to this component . That would allow it take Images from ImageList ? I would fill up ImageList dynamicaly during my execution time.
Right now I'm doing the following to show a preview of the TILE :
procedure TTools.Preview_ImageExecute(Sender: TObject);
var image_temp : TBitmap;
begin
image_temp := TBitmap.Create;
LoadBitMap(ComboBox1.Text,image_temp,Main.ASDb1);
Image1.Picture.Bitmap:=image_temp;
image_temp.Free;
end;
Would like to use this somehow with the Image Grid... it should somehow allow me to load all my tiles.. I would use a For loop to fill it up...
Meanwhile I was playing with ListBox, and managed to do this :
procedure TTools.Lst1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var CenterText : integer;
begin
Lst1.Canvas.FillRect(Rect);
Il1.Draw(lst1.Canvas,rect.Left +4, rect.Top +4, Index);
Centertext := (rect.Bottom - rect.Top -lst1.Canvas.TextHeight(text)) div 2;
Lst1.Canvas.TextOut(rect.left + il1.width + 8, rect.Top + CenterText, lst1.Items.Strings[index]);
end;
procedure TTools.Lst1MeasureItem(Control: TWinControl; Index: Integer;
var Height: Integer);
begin
Height := IL1.Height+4;
end;
procedure TTools.Button4Click(Sender: TObject);
var i : integer;
image_temp : TBitmap;
begin
image_temp := TBitmap.Create;
for i:=0 to Main.Images.Count-1 do
begin
Lst1.Items.Add(Main.Images.Item[i].Name);
LoadBitMap(Main.Images.Item[i].Name,image_temp,Main.ASDb1);
IL1.AddMasked(image_temp, clNone);
end;
Image_temp.Free;
end;
This works, if I have 0 Columns, but I cant get it working with say 4 columns , can someone help ?
Greetings
Robert
Never Mind ... I solved it like this :
procedure TTools.Button4Click(Sender: TObject);
var i : integer;
image_temp : TBitmap;
begin
image_temp := TBitmap.Create;
for i:=0 to Main.Images.Count-1 do
begin
LoadBitMap(Main.Images.Item[i].Name,image_temp,Main.ASDb1);
ListView1.Items.Add.Caption:=Main.Images.Item[i].Name;
ListView1.Items.Item[i].ImageIndex:=i;
IL1.AddMasked(image_temp, clNone);
end;
Image_temp.Free;
end;
This load's the image names as well as the images from my Asphyre Image list...
In my Firebird database I have a Blob field that contain a Bitmap. I'll have to load and display in a TImage located on my Form. Subsequently I'll have to save in the same field the image selected by a OpenDialog.
Procedure LoadBitmapFromBlob(Bitmap: TBitmap; Blob: TBlobField);
var
ms, ms2: TMemoryStream;
begin
ms := TMemoryStream.Create;
try
Blob.SaveToStream(ms);
ms.Position := 0;
Bitmap.LoadFromStream(ms);
finally
ms.Free;
end;
end;
example usage
procedure TForm4.Button1Click(Sender: TObject);
var
bmp: TBitmap;
begin
bmp := TBitmap.Create;
try
LoadBitmapFromBlob(bmp, TBlobField(Dataset.FieldByName('Image')));
Image1.Picture.Assign(bmp);
bmp.SaveToFile(OpenDialog.FileName);
finally
bmp.Free;
end;
end;
Just a follow up question to this one here => link
Is it possible to change the text colour of a TabSheet caption to another colour (eg. White) and change the font style to 'bold'?
Maybe this might give you such inspiration. Again, please note this will work only on Windows and with themes disabled in your application.
uses
ComCtrls, Windows, LCLType;
type
TPageControl = class(ComCtrls.TPageControl)
private
procedure CNDrawItem(var Message: TWMDrawItem); message WM_DRAWITEM;
protected
procedure CreateParams(var Params: TCreateParams); override;
end;
implementation
procedure TPageControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params do
begin
if not (csDesigning in ComponentState) then
Style := Style or TCS_OWNERDRAWFIXED;
end;
end;
procedure TPageControl.CNDrawItem(var Message: TWMDrawItem);
var
FontHandle: HFONT;
FontColor: COLORREF;
FontObject: TLogFont;
BrushColor: COLORREF;
BrushHandle: HBRUSH;
begin
with Message.DrawItemStruct^ do
begin
GetObject(Font.Handle, SizeOf(FontObject), #FontObject);
case itemID of
0:
begin
BrushColor := RGB(235, 24, 33);
FontColor := clWhite;
FontObject.lfWeight := FW_NORMAL;
FontObject.lfItalic := 0;
end;
1:
begin
BrushColor := RGB(247, 200, 34);
FontColor := clGreen;
FontObject.lfWeight := FW_NORMAL;
FontObject.lfItalic := 1;
end;
2:
begin
BrushColor := RGB(178, 229, 26);
FontColor := clGreen;
FontObject.lfWeight := FW_BOLD;
FontObject.lfItalic := 1;
end
else
BrushColor := ColorToRGB(clBtnFace);
end;
BrushHandle := CreateSolidBrush(BrushColor);
FillRect(hDC, rcItem, BrushHandle);
FontHandle := CreateFontIndirect(FontObject);
try
SelectObject(hDC, FontHandle);
SetTextColor(hDC, FontColor);
SetBkMode(hDC, TRANSPARENT);
DrawTextEx(hDC, PChar(Page[itemID].Caption), -1, rcItem, DT_CENTER or
DT_VCENTER or DT_SINGLELINE, nil);
finally
DeleteObject(FontHandle);
end;
end;
Message.Result := 1;
end;
Here is how it looks like:
Replace hDc with _hDc
and drawtextex with drawtext and