SOLVED: I'm trying to change the background color of the column header on an FMX StringGrid using the onDrawColumnHeader. I can change the column header color but I lose the Header Text and Header Grid Lines.
What is the proper way to change the background color of the Column Headings so I can still see the text and grid lines?
Here is the code I'm using:
procedure TfrmCustomers.GridDrawColumnHeader(Sender: TObject; const Canvas: TCanvas;
const Column: TColumn; const Bounds: TRectF);
begin
//Exit;
Canvas.Fill.Kind := TBrushKind.Solid;
Canvas.Fill.Color := TAlphaColors.LightBlue;
Canvas.FillRect(Bounds,1);
end;
object lytGrid: TLayout
Align = Client
Padding.Left = 2.000000000000000000
Padding.Top = 2.000000000000000000
Padding.Right = 2.000000000000000000
Padding.Bottom = 2.000000000000000000
Size.Width = 640.000000000000000000
Size.Height = 398.000000000000000000
Size.PlatformDefault = False
TabOrder = 2
object Grid: TStringGrid
Align = Client
CanFocus = True
ClipChildren = True
Size.Width = 636.000000000000000000
Size.Height = 394.000000000000000000
Size.PlatformDefault = False
StyleLookup = 'GridStyle1'
TabOrder = 2
RowCount = 55
OnDrawColumnHeader = GridDrawColumnHeader
Viewport.Width = 616.000000000000000000
Viewport.Height = 353.000000000000000000
end
end
Here is a screen shot of the light Blue column headings:
I needed to incorporate the Canvas.Filltext into the process.
Here's the code I finally came up with. It has a nice little side effect of centered column headings. The tricky bit was figuring out how to get the Column Header text. I found a nice piece of code in this SO answer that made it kind of easy to figure out.
procedure TfrmCustomers.GridDrawColumnHeader(Sender: TObject; const Canvas: TCanvas;
const Column: TColumn; const Bounds: TRectF);
var
R : TRectF;
begin
R := Bounds;
Canvas.Fill.Kind := TBrushKind.Solid;
Canvas.Fill.Color := TAlphaColorRec.Dimgray;
Canvas.FillRect(R,1);
R.Inflate(0,0,-0.25,-0.25);
Canvas.Fill.Color := TAlphaColorRec.Whitesmoke;
Canvas.FillRect(R,1);
Canvas.Fill.Color := TAlphaColors.Black;
Canvas.Font.Style := [TFontStyle.fsBold];
Canvas.FillText(Bounds,Grid.ColumnByIndex(Column.Index).Header,False,1,[],TTextAlign.Center,TTextAlign.Center);
end;
The first FillRect paints the entire cell Dimgray. The second FillRect paints the cell WhiteSmoke with a small little twist. Before calling the FillRect, the RectF is reduced sligtly on the right side and on the bottom edge. This lets a tiny sliver of the Dimgray from the first RectF through which acts as a border.
I have several task on button click.
eg.
Show form or please wait panel....
Load data from database (duration 5-10 seconds)
Clear all TEdit field
Hide form or please wait panel....
ShowMessage('completed')
Is it possible After click on button show please wait panel or form and after all completed hide that panel.
How to Synchronize Perform Tasks One by one.
Or any other simple solution.
This is a simple example that creates a "placeholder" which looks like this:
The rectangle has a black background and contains a layout which is aligned to Center; inside you can find a label (aligned to Top) and an arc (aligned to Client). The code is here:
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 418
ClientWidth = 490
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
OnCreate = FormCreate
DesignerMasterStyle = 0
object Rectangle1: TRectangle
Align = Client
Fill.Color = xFF222222
Size.Width = 490.000000000000000000
Size.Height = 418.000000000000000000
Size.PlatformDefault = False
Visible = False
object Layout1: TLayout
Align = Center
Size.Width = 170.000000000000000000
Size.Height = 102.000000000000000000
Size.PlatformDefault = False
TabOrder = 0
object Label1: TLabel
Align = Top
StyledSettings = [Family, Size, Style]
Size.Width = 170.000000000000000000
Size.Height = 41.000000000000000000
Size.PlatformDefault = False
TextSettings.FontColor = claWhite
TextSettings.HorzAlign = Center
Text = 'Please wait'
TabOrder = 0
end
object Arc1: TArc
Align = Center
Size.Width = 50.000000000000000000
Size.Height = 50.000000000000000000
Size.PlatformDefault = False
Stroke.Color = claCoral
EndAngle = -90.000000000000000000
object FloatAnimation1: TFloatAnimation
Enabled = True
Duration = 1.000000000000000000
Loop = True
PropertyName = 'RotationAngle'
StartValue = 0.000000000000000000
StopValue = 360.000000000000000000
end
end
end
end
end
The Visible property of the rectangle is set to False so that you won't see immediately the rectangle. Note that I have created an animation in the arc component so that you can see it spinning around:
In this way you can mimic a loading spinner. Then I have added this code in the OnCreate event of the form just as example of how you could do this.
procedure TForm1.FormCreate(Sender: TObject);
begin
TTask.Run(procedure
begin
TThread.Synchronize(nil, procedure
begin
Rectangle1.Visible := true;
//Rectangle1.BringToFront;
// ^ call the above if needed, just to be sure
// that you'll always see the rectangle on screen
end);
Sleep(4000);
TThread.Synchronize(nil, procedure
begin
Rectangle1.Visible := false;
ShowMessage('Finish!');
end);
end);
end;
The Sleep(4000) simulates a long task and this piece of code should be replaced with your tasks. Actually I'd do something like this:
procedure TForm1.FormCreate(Sender: TObject);
begin
TTask.Run(procedure
var
arr: array [0..1] of ITask;
begin
TThread.Synchronize(nil, procedure
begin
Rectangle1.Visible := true;
Rectangle1.BringToFront;
end);
arr[0] := TTask.Run(procedure
begin
//load data from the database
end);
arr[1] := TTask.Run(procedure
begin
//something else
end);
//this call is blocking but you are calling this in a worker thread!
//your UI won't freeze and at the end you'll see the message appearing
TTask.WaitForAll(arr);
TThread.Synchronize(nil, procedure
begin
Rectangle1.Visible := false;
ShowMessage('Finish!');
end);
end);
end;
Of course you should place this code in a ButtonClick and not in a FormCreate event handler!
Imagine a canvas that is tile filed both horizontally and vertically by using a single bitmap, for example:
The above is achieved with the following:
procedure TForm1.PaintBox1Paint(Sender: TObject);
var
X, Y: Integer;
begin
Y := 0;
while Y < PaintBox1.Height do
begin
X := 0;
while X < PaintBox1.Width do
begin
PaintBox1.Canvas.Draw(X, Y, Image1.Picture.Bitmap);
Inc(X, Image1.Picture.Bitmap.Width);
end;
Inc(Y, Image1.Picture.Bitmap.Height);
end;
PaintBox1.Canvas.Brush.Style := bsClear;
PaintBox1.Canvas.Rectangle(PaintBox1.ClientRect);
end;
Now what I would like to do is extend the above to allow a horizontal and vertical offset to simulate the effect that the tiled images are been wrapped / scrolled when using a scrollbar etc.
The idea is that when changing the horizontal offset for example, the tiled images would move to the left, and from the right you should see the tiled images appear as if they have been wrapped around from one side of the paintbox to the other.
I have managed to make a test project that does work how I want it to and it looks like this:
I have written this test project in Lazarus but it can be easily adapted to test in Delphi.
Below is the lfm:
object Form1: TForm1
Left = 0
Height = 625
Top = 0
Width = 782
Caption = 'Form1'
ClientHeight = 625
ClientWidth = 782
OnCreate = FormCreate
Position = poScreenCenter
Visible = False
object Label1: TLabel
Left = 25
Height = 15
Top = 16
Width = 40
Caption = 'Regular'
ParentColor = False
end
object Label2: TLabel
Left = 361
Height = 15
Top = 16
Width = 32
Caption = 'Offset'
ParentColor = False
end
object PaintBox1: TPaintBox
Left = 24
Height = 320
Top = 40
Width = 320
OnPaint = PaintBox1Paint
end
object PaintBox2: TPaintBox
Left = 361
Height = 320
Top = 40
Width = 320
OnPaint = PaintBox2Paint
end
object Image1: TImage
Left = 25
Height = 48
Top = 544
Width = 48
AutoSize = True
Picture.Data = {
1754506F727461626C654E6574776F726B47726170686963830A000089504E47
0D0A1A0A0000000D49484452000000300000003008060000005702F98700000A
4A494441546881CD997F8C5C5515C73FF7BE376F66F6C77477DBEDF677A9A4EC
A2056D34223110C0468A091122CA822151227FD45F31C400898A1AF803635249
C4F40F883424842A46A28480A6828AD820581A1A4B7F125AFBBBDBFD313BBB33
F3DEBBF7F8C77D333B3F763B3BBB267A9297B9F7BD7BDF3BDF7BCEF99E73EF28
6B85FF3779FA6F87552E1B749C199B5AD1BF24BB6A595766C5D989D286209559
7DEDE0C0536BBABCFD95B17E3B2FDEF1EA81A67B294FD3DB95E1CCD8147DDD19
B2299F53A385BA766F57864CCAE3E4C5C9A4ED7366AC90EEEDCAF4766782DEF1
E9F2DABECEF4FA251DE9354664DD47D72D5DDFD3915E7DFDE0409FA7BDDE13E3
9167BC6E56F7A4F8F99F8E8EFEE4F3830B03B0EDA60FCF7BECCE378EA4D3BE74
A53CBDD2F7D4EAEE6CB06AC3F225EB7B3A830D3D1DE9B557ADED5D9ECB0403D9
C0EBCBFA9E4EA73CB452D5F916383C1272AE10D19DF1185A11E06B087CDD5DFB
9D455B402BB57A656FE7A6E9305ED7D795599FCB066B7DAD567D7AE3C09A8EC0
1FE84CFB5D595FA73C4FE36BDDF21BB1B11CBA18727E322697F5D9D89F21E529
26C398D800A06AC72FCA023B5E3D70E5351B07760F2ECFADCA066DBDAA49CAB1
E5E048999129434FD667E34016AD15B115E248304E798CAD9FB7600B182BEC3B
7C7ACBBDD75DB12A9D5A9CF28747CABC3F1AD2DF9D62437F06AD15A1018C54D7
5B002B601703A0D102F71E3A65622BA4012B8ECD2A7EDCD89F4BF265C39EE3D3
5CB7B19B5814A111748D923A21C9C8088803B160004FECFE57B56DAC45A49982
6DC3BD5A20B5CF1C30E1743E647434C4A228C782566044A8C5AD05E20454BC18
0B7C73CB47EAFA5F3D7C7ADE736B95B722942343680CE3D365AC80566E7545EA
A3546B88C4B1921230A67E81DA02B0FD9577AB6D6305D31851F304321DC64C96
62AC588C1504B7B2612C68A5D015040AB475734293805C8C05EEDF7A755DFF9E
C7E76F818A9423C3443122328257C3AAD60AC68255159723F955587131E06B17
D70B06F0D88BEFD47DB0D1DF5B8915A16CDCAAD78940642116E7264ACD28AA11
626BB1C9F3C64FB605E0A15B37D7F5EFDEFE52132BB40651C32452B927C406A2
58507AC68534CEF7232358014F2F320F3CF2C2DBD5B6B182B582D026824651EE
8AAD10092833E3429E5208E2005820E5D86FC1007E70FB27EAFA77FEF445C0AD
A01569C9F9738958B7C2462C4A5442A52E1E8C08C6385A350667920503F8F59B
D5B61167815A69E6F9D65289E3589C825AD5D854C0183BE3F7162C8BA0D147BE
744D5DFF8EC77E37E7D8762C22022282A904A9024F25F4190B2A798FD2B2381A
7DE8B93DD5B6313661A2B9C737B2945899357B4342A346B02AA14E208A6DF20E
97A151AAC9EAEDD1E85DD7D6F56F7BF4B7ED4CAF4A158476E5851117A871A29C
46B008C68ACBCA0AC4B81831F122007C67E75FAB6DC742ED67E27A20954692D9
AD73178B101B41251CA7948B95624928858B00F0F857AEAFEBDFFAE3E7EB154A
5650E9F6D8C8E27283490A1E93B88ED60A57F281AF61AC685D55BA5000DF78EA
B56A3B36B6EAFF56A4AA7C2D905A309264EE590F11923A3F322E464CB20F5022
E86AB6538C4C5A6431007EF1B51BEBFAB73CFCAB96736416852BC1ED55EA4EE5
4A89C8D494E82E763149069E2C19268BA66143D92680FB76ECAEB68DB5583393
C0CC0232B2B5E2283451BE1C591433855C8585B5C085494B1C0BB2181A7D72DB
96BAFECDDF7B6E469959E8713E794029B058F2C53809DC99FB5A293C0DF992E5
623E464410594429B1FE96EF73ED673E45269D228A0DC658EB8AB339B87D9664
168B5437305AE1AC6884C8B8CD8A02D06ED545095614A74663577E57227AA100
8EBFFC685DFFEB4FBE96F55B304EFD4EACFE5939364C873146847CD1602D7404
9A5264B1CA31CF48DE30316550B878688CA9B600745D7E3B9B6EDECAE0D0FAA0
A73B7BC34D9B2FDB96CB386B342B3EE3CBB5A20144988A0C2282D230D09FE6D8
C922FDFD198C5862EB363BE3D3C2D9F188DAB85ED4A6BE70EC05001E78F6EF3B
1EB8EDE3F72EEB4C530C63A7989A51BC1644ED3370543915C6D5673D198FCBFB
04A533BCF77E910FADCD622C144A8673E33171ECE2412A001A10B43E2AAB91DC
E09D74ACDD4AAE33BD6959679AA97254A7EC5C759115088D5BF562546F2DAD14
EB7B35FDD910C4920934A39311A74762A2A46C90A45C1769DE925DD202B9A1E1
CADAE9E4523A9551B1B1552D5A9DFF44D6129A9903806CCAC35A4B313255C0B1
71C561A11873763CE2C2B841254B2BB8F32057A54AEB723A515A250A7BC9AF9F
B415A044A4C972B540625B29D06C93C9D3BE46A91446C4056BA57E568A722894
CA33AB5ED1B576D12F99077243C3C12C4AD7025180B2B61E8015B752715290B5
FACFC1D7CA156D8DE576E2268DF3DD30690D00C8004183D2F500C422222AB696
E9C49FE7FB274945E1B9868B50BF00CD8717D816892C063A80748DF28D16C088
780B51FC929204A8A599EBA5F29CA6186E02504C940D12103E33F15005803496
540B50B83A36711D35D39E6BBE4053355AE7CBF983BB042800A3409800C92457
BA72999A18A89C48D45EF31191E6EDA85857ACD958DC65672E1171ABE8296F4E
003520A680F3403E1993AEBD444455EAFB76E552B59324C02AE7123A2927B482
52D916CF8D44E70BA5DC91DA39B3E681FCC15D921B1A9E06C68125407715AC63
8AB612E07C4245092804AD04CF8352D9469313F18589317B267F313E519AB4A3
D1B41D11237F6C09209114D00FF430C34C00182B2D62A0B5C2E07242E06BBA02
0FA5C3383F194FC8D9E8C2FE7D8523A5097B269AB679890981349A69A578E6FC
1F361F9D2F00838B8718E8023C944A08A11E403BE7A356045F2B966483E89DE3
178FEE3D7AF6CD7FEE3FFAC67B47CC581C5EF9C5E51BFDA9890FC273CA531E0A
8D56392C23589E3EB77BF389C6F7CD09207F70579C1B1A3E0D9481CB811538AB
48CAF702CFF7E6AD7C39B64C14C3786CAAFCBE15D9F3EEF191BF3CB5F3E57746
3E387E6AFAC29982298E84C1B22D9B7AAFB8120C018AC0E51B9D41CC49A4BC53
791D67677BF7256BA1FCC15D06389F1B1A0E7114BB1E48BDBDEFD89E939FFBD8
CA8EB4DF618CAD9EE7D42A3C3E5D666C3A3C3A5628BD558CCC6B22F20F84431B
57F694BEFDD94DDCBFF56ABA37DE91D17ED0E9752F4DEB742A4070078BC662F1
324878CCE45F797A6CDF8FC6E7D251CD3719E5868633C05A609D9898CE156BB2
7D6B56AD7BE287F7FCEC86C115C1DE13A38C4E95FF9D9F2EBF558CCC9F813780
C33837444408523E5D81CF7DDFDA4EE1F471941F68208B943C2FF7C9C1DCDA2D
DF5D7A592E3CF1D6545EA474C0E45FDA69CBFBA6505D2031F943CD0769F30690
80F070F1A0252A174C794AAEBAF50B776FFBF28D03FD4B3A5EBF38593C70597F
777EAEF97D9D6936ADEA6D7EA7943C2F77CDD5DDABB73C3875569F4315F69AC2
CBCF8EEF7FACDC4AA776FFE035C004804AA5F15369F63DBFF3991D08BF7C7898
8B93458E8F14005795766752F47406F474A4E9C906746553FC7EDF09EEB9EB41
C41A929AD9803248398F8895F0D4EBA6F4C26FB0E32637345CF7F1FCC15D4D0A
B565814BC98542893313457AB201B96C8A9E6CD0D6FCDCE0B0EF77AE5A3ABA77
FBB976E6FDD700FCAFE43F765B2B40A722D3C30000000049454E44AE426082
}
Visible = False
end
object Label3: TLabel
Left = 645
Height = 15
Top = 376
Width = 36
Alignment = taRightJustify
AutoSize = False
Caption = '0'
ParentColor = False
end
object ScrollBar1: TScrollBar
Left = 361
Height = 17
Top = 374
Width = 273
Max = 5000
PageSize = 0
TabOrder = 0
OnChange = ScrollBar1Change
end
object ScrollBar2: TScrollBar
Left = 696
Height = 320
Top = 40
Width = 17
Kind = sbVertical
Max = 5000
PageSize = 0
TabOrder = 1
OnChange = ScrollBar2Change
end
object Label4: TLabel
Left = 720
Height = 15
Top = 40
Width = 34
Caption = 'Label4'
ParentColor = False
end
end
and the source
unit Unit1;
{$mode delphi}
interface
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
ExtCtrls;
{ TForm1 }
type
TForm1 = class(TForm)
Image1: TImage;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
PaintBox1: TPaintBox;
PaintBox2: TPaintBox;
ScrollBar1: TScrollBar;
ScrollBar2: TScrollBar;
procedure FormCreate(Sender: TObject);
procedure PaintBox1Paint(Sender: TObject);
procedure PaintBox2Paint(Sender: TObject);
procedure ScrollBar1Change(Sender: TObject);
procedure ScrollBar2Change(Sender: TObject);
private
FOffsetX: Integer;
FOffsetY: Integer;
public
{ public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
FOffsetX := 0;
FOffsetY := 0;
end;
procedure TForm1.PaintBox1Paint(Sender: TObject);
var
X, Y: Integer;
begin
Y := 0;
while Y < PaintBox1.Height do
begin
X := 0;
while X < PaintBox1.Width do
begin
PaintBox1.Canvas.Draw(X, Y, Image1.Picture.Bitmap);
Inc(X, Image1.Picture.Bitmap.Width);
end;
Inc(Y, Image1.Picture.Bitmap.Height);
end;
PaintBox1.Canvas.Brush.Style := bsClear;
PaintBox1.Canvas.Rectangle(PaintBox1.ClientRect);
end;
// needs improvement
procedure TForm1.PaintBox2Paint(Sender: TObject);
var
X, Y: Integer;
begin
Y := -FOffsetY;
while Y < PaintBox2.Height do
begin
X := -FOffsetX;
while X < PaintBox2.Width do
begin
PaintBox2.Canvas.Draw(X, Y, Image1.Picture.Bitmap);
Inc(X, Image1.Picture.Bitmap.Width);
end;
Inc(Y, Image1.Picture.Bitmap.Height);
end;
PaintBox2.Canvas.Brush.Style := bsClear;
PaintBox2.Canvas.Rectangle(PaintBox2.ClientRect);
end;
procedure TForm1.ScrollBar1Change(Sender: TObject);
begin
FOffsetX := ScrollBar1.Position;
Label3.Caption := IntToStr(FOffsetX);
PaintBox2.Invalidate;
end;
procedure TForm1.ScrollBar2Change(Sender: TObject);
begin
FOffsetY := ScrollBar2.Position;
Label4.Caption := IntToStr(FOffsetY);
PaintBox2.Invalidate;
end;
end.
My question is how can I improve my existing code and whether or not there is a better way of doing this, I may have over complicated the task?
For small offsets the paintbox paints quite fast but with larger offsets such as 5000 (what I am currently testing with) the scrolling becomes rather slow and I wonder if I have missed out something obvious?
There is no need to draw all of the bitmaps that are not visible. For any given width/height of bitmap (let's call it the "tile"), you only need to start/finish drawing at most 1 tile width or height beyond the edge of your canvas.
i.e. calculate the first visible tile and starting painting with that.
The illustration above tries to show what I mean... only the green tiles are in the visible area (shaded blue) so even if the offset includes those that are shown red, there is no point painting them.
Windows (the OS) will clip these anyway, but you are still spending time iterating over those not-visible tiles, which is why your larger offsets result in degraded performance... you are spending time counting up through the "invisible" space, before reaching the tiles that you actually need to paint.
Calculating the "origin" at which to start painting is simple arithmetic and doesn't require loop iterations. Instead of initialising Y and X to your absolute offsets, set them to the first multiple of the height/width of the tile from that offset, using a simple mod operation:
Y := -FOffsetY mod Image1.Height;
X := -FOffseXY mod Image1.Width;
Or similar
Jerry's observation w.r.t using an offscreen bitmap and then blitting into your control will also optimise the actual painting.
I have form (Height = 500) and TVertScrollBox on it (align set to TAlignLayout.Client and range is 5000px). I wrote simple method, which show position of mouse when I click on scroll box.
procedure TformMain.VertScrollBox1MouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
ShowMessage(FloatToStr(X) + ' ' + FloatToStr(Y));
end;
When the scroll bar is on top and I click on top of the scroll box, Y in message is 0. That's right. But when I scroll down to the half and click on top of the scroll box, Y in message is 0, too (not 2500). How can I get the position relative to scroll box?
This is my FMX code for TForm and TVertScrollBox:
object formMain: TformMain
Left = 0
Top = 0
BorderIcons = [biSystemMenu, biMinimize]
BorderStyle = Single
Caption = 'Gear Studio 1.0'
ClientHeight = 600
ClientWidth = 450
Position = DesktopCenter
StyleBook = StyleBookPanel
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
OnCreate = FormCreate
OnCloseQuery = FormCloseQuery
DesignerMasterStyle = 0
object VertScrollBox1: TVertScrollBox
Align = Client
Size.Width = 450.000000000000000000
Size.Height = 576.000000000000000000
Size.PlatformDefault = False
StyleLookup = 'VertScrollBox1Style1'
TabOrder = 1
OnMouseDown = VertScrollBox1MouseDown
Viewport.Width = 450.000000000000000000
Viewport.Height = 576.000000000000000000
end
...
...
end
That's how I am adding panels:
SetLength(MyItems, i+1);
MyItems[i] := TItem.Create(i);
with MyItems[i] do begin
...
end;
constructor TItem.Create(number: integer);
var
ThisItem: TItem;
begin
inherited Create(nil);
ThisItem := Self;
with ThisItem do begin
if number = -1 then begin
... //not important
end;
end else if number > 0 then begin
Width := 370;
Height := 35;
...
end;
Position.X := 10;
Parent := formMain.VertScrollBox1;
PopupMenu := formMain.PopupMenu1;
OnDblClick := DblClick;
OnMouseEnter := MouseEnter;
OnMouseLeave := MouseLeave;
end;
end;
MyItems is dynamical array of TItem (it is normal TPanel with added some properties).
You need to add VertScrollBox1.ViewportPosition.Y property to get the absolute coordinate.
ShowMessage(FloatToStr(X) + ' ' + FloatToStr(VertScrollBox1.ViewportPosition.Y+Y));
shows correct result.
The Options dialog in Word 2010 implements the category selector via set of white "toggle" buttons that become orange when clicked (selected).
How would one re-implement such behavior in Delphi? A conformance with the current Windows theme is required (i.e. it must be possible to specify the button color as clWindow, not clWhite).
EDIT: To clarify - I only have problems with the category selector on the left. Everything else is fairly simple.
You could use the TButtonGroup component.
Using VCL Styles is by far the easiest solution but as like you said, using styles in XE2 is quite uncomfortable, in my opinion this feature only really became viable in XE3.
Per your request to use the default painting methods I'm submitting my solution,
source code of the project available here.
This project requires an image, the image is zipped together with the project.
Compiled and tested in XE4.
type
TButtonGroup = class(Vcl.ButtonGroup.TButtonGroup)
protected
procedure Paint; override;
end;
TForm1 = class(TForm)
ButtonGroup1: TButtonGroup;
Panel1: TPanel;
procedure ButtonGroup1DrawButton(Sender: TObject; Index: Integer;
Canvas: TCanvas; Rect: TRect; State: TButtonDrawState);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
MBitmap : TBitmap;
implementation
{$R *.dfm}
procedure TButtonGroup.Paint;
var
R : TRect;
begin
inherited;
R := GetClientRect;
R.Top := Self.Items.Count * Self.ButtonHeight;
{Remove the clBtnFace background default Painting}
Self.Canvas.FillRect(R);
end;
procedure TForm1.ButtonGroup1DrawButton(Sender: TObject; Index: Integer;
Canvas: TCanvas; Rect: TRect; State: TButtonDrawState);
var
TextLeft, TextTop: Integer;
RectHeight: Integer;
ImgTop: Integer;
Text : String;
TextOffset: Integer;
ButtonItem: TGrpButtonItem;
InsertIndication: TRect;
DrawSkipLine : TRect;
TextRect: TRect;
OrgRect: TRect;
begin
//OrgRect := Rect; //icon
Canvas.Font := TButtonGroup(Sender).Font;
if bdsSelected in State then begin
Canvas.CopyRect(Rect,MBitmap.Canvas,
System.Classes.Rect(0, 0, MBitmap.Width, MBitmap.Height));
Canvas.Brush.Color := RGB(255,228,138);
end
else if bdsHot in State then
begin
Canvas.Brush.Color := RGB(194,221,244);
Canvas.Font.Color := clBlack;
end
else
Canvas.Brush.color := clWhite;
if not (bdsSelected in State)
then
Canvas.FillRect(Rect);
InflateRect(Rect, -2, -1);
{ Compute the text location }
TextLeft := Rect.Left + 4;
RectHeight := Rect.Bottom - Rect.Top;
TextTop := Rect.Top + (RectHeight - Canvas.TextHeight('Wg')) div 2; { Do not localize }
if TextTop < Rect.Top then
TextTop := Rect.Top;
if bdsDown in State then
begin
Inc(TextTop);
Inc(TextLeft);
end;
ButtonItem := TButtonGroup(Sender).Items.Items[Index];
TextOffset := 0;
{ Draw the icon - if you need to display icons}
// if (FImages <> nil) and (ButtonItem.ImageIndex > -1) and
// (ButtonItem.ImageIndex < FImages.Count) then
// begin
// ImgTop := Rect.Top + (RectHeight - FImages.Height) div 2;
// if ImgTop < Rect.Top then
// ImgTop := Rect.Top;
// if bdsDown in State then
// Inc(ImgTop);
// FImages.Draw(Canvas, TextLeft - 1, ImgTop, ButtonItem.ImageIndex);
// TextOffset := FImages.Width + 1;
// end;
{ Show insert indications }
if [bdsInsertLeft, bdsInsertTop, bdsInsertRight, bdsInsertBottom] * State <> [] then
begin
Canvas.Brush.Color := clSkyBlue;
InsertIndication := Rect;
if bdsInsertLeft in State then
begin
Dec(InsertIndication.Left, 2);
InsertIndication.Right := InsertIndication.Left + 2;
end
else if bdsInsertTop in State then
begin
Dec(InsertIndication.Top);
InsertIndication.Bottom := InsertIndication.Top + 2;
end
else if bdsInsertRight in State then
begin
Inc(InsertIndication.Right, 2);
InsertIndication.Left := InsertIndication.Right - 2;
end
else if bdsInsertBottom in State then
begin
Inc(InsertIndication.Bottom);
InsertIndication.Top := InsertIndication.Bottom - 2;
end;
Canvas.FillRect(InsertIndication);
//Canvas.Brush.Color := FillColor;
end;
if gboShowCaptions in TButtonGroup(Sender).ButtonOptions then
begin
{ Avoid clipping the image }
Inc(TextLeft, TextOffset);
TextRect.Left := TextLeft;
TextRect.Right := Rect.Right - 1;
TextRect.Top := TextTop;
TextRect.Bottom := Rect.Bottom -1;
Text := ButtonItem.Caption;
Canvas.TextRect(TextRect, Text, [tfEndEllipsis]);
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
MBitmap := TBitmap.Create;
try
MBitmap.LoadFromFile('bg.bmp');
except
on E : Exception do
ShowMessage(E.ClassName+' error raised, with message : '+E.Message);
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
MBitmap.Free;
end;
DFM :
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 398
ClientWidth = 287
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
Position = poScreenCenter
StyleElements = []
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object Panel1: TPanel
AlignWithMargins = True
Left = 5
Top = 5
Width = 137
Height = 388
Margins.Left = 5
Margins.Top = 5
Margins.Right = 5
Margins.Bottom = 5
Align = alLeft
BevelKind = bkFlat
BevelOuter = bvNone
Color = clWhite
ParentBackground = False
TabOrder = 0
StyleElements = [seFont]
object ButtonGroup1: TButtonGroup
AlignWithMargins = True
Left = 4
Top = 4
Width = 125
Height = 378
Margins.Left = 4
Margins.Top = 4
Margins.Right = 4
Margins.Bottom = 2
Align = alClient
BevelInner = bvNone
BevelOuter = bvNone
BorderStyle = bsNone
ButtonOptions = [gboFullSize, gboGroupStyle, gboShowCaptions]
DoubleBuffered = True
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Segoe UI'
Font.Style = []
Items = <
item
Caption = 'General'
end
item
Caption = 'Display'
end
item
Caption = 'Proofing'
end
item
Caption = 'Save'
end
item
Caption = 'Language'
end
item
Caption = 'Advanced'
end>
ParentDoubleBuffered = False
TabOrder = 0
OnDrawButton = ButtonGroup1DrawButton
end
end
end
There is a Panel container in there hosting the TButtonGroup, it is not needed, simply added for visual improvement.
If you want to change the color of the selection at runtime then I suggest using efg's Hue/Saturation method to change the Hue of the image, that way the color panel remains but the color will change.
To gain support for VCL Styles simply detach the ButtonGroup1DrawButton Event from the TButtonGroup component, that way the default DrawButton Event can kick in which adds support for that.
You can use a TListBox with style set to lbOwnerDrawFixed (if the size of the spacing isn't important) or lbOwnerDrawVariable if it is.
You can then handle OnDrawItem & OnMeasureItem accordingly.
Using clWindow will be no problem, however AFAIK the orange color is not part of the Windows theme, but you can obtain something that will match the theme by starting from clHighlight and then applying a hue shift, then lightness shift for the shading.
If your hue shift is constant, it'll automatically adapt to the theme colors.
Sample code (without the HueShift for the orange): drop a TListBox, set lbOwnerDrawFixed, adjust ItemHeight to 28, set font to "Segoe UI" and use the following OnDrawItem
var
canvas : TCanvas;
txt : String;
begin
canvas:=ListBox1.Canvas;
canvas.Brush.Style:=bsSolid;
canvas.Brush.Color:=clWindow;
canvas.FillRect(Rect);
InflateRect(Rect, -2, -2);
if odSelected in State then begin
canvas.Pen.Color:=RGB(194, 118, 43);
canvas.Brush.Color:=RGB(255, 228, 138);
canvas.RoundRect(Rect.Left, Rect.Top, Rect.Right, Rect.Bottom, 6, 6);
canvas.Pen.Color:=RGB(246, 200, 103);
canvas.RoundRect(Rect.Left+1, Rect.Top+1, Rect.Right-1, Rect.Bottom-1, 6, 6);
end;
canvas.Font.Color:=clWindowText;
canvas.Brush.Style:=bsClear;
txt:=ListBox1.Items[Index];
Rect.Left:=Rect.Left+10;
canvas.TextRect(Rect, txt, [tfLeft, tfSingleLine, tfVerticalCenter]);
end;
If you're going to have more than one such components, it's of course preferable to just subclass TListBox, and if you want anti-aliasing for the RoundRect, GR32 or GDI+ can be used.
Note that for backward compatibility with XP, "Segoe UI" font should be set dynamically, as it's not available in XP (in XP "Arial" is a good fallback, "Tahoma" looks closer but isn't guaranteed to be there)
We use TMS Control's Advanced Poly Pager for this look. I highly recommend it. It's a very powerful and flexible set of controls. Specifically, we use TAdvPolyList for our Office-style dialogs with some custom tweaking to the colour scheme. (Note this is different to their TAdvOfficePager which doesn't look nearly as good. Don't accidentally mix the two up!)
It allows you to:
Have a category selector on the left
Is a page control, so is easy to have your controls on pages on the right (the same as a normal page control)
Shows a visual link between the tab and page, something the Word screenshot you provided doesn't do (Word has a barrier in-between; this control doesn't. It's a better, more intuitive and well-linked UI deisgn.)
Will certainly allow you to use color constants like clWindow if you wish, though anything would
Has a wide variety of items that can go in the left panel, including images, text with images, links, etc. Your Word screenshot has subtle gray dividing lines separating some of the elements; I'm sure you can do this with this control too, whereas it would be trickier to reliably do with some of the other answers posters have given, such as custom-painting TListBox.
Looks great!
The images on their site don't really show perfectly how to mimic an Office look, but from these two screenshots (high-res on their site) you should be able to see the sort of things you can achieve:
and
Our menus look similar to the second screenshot but with simple text items (nothing complex like checkboxes and images etc - I think they've put those there just to demonstrate that you can) and uses a colour scheme more like yours, plus we added blue headers to each page.
We bought it a couple of years ago and have never regretted it. Highly recommended.
I would have thought you could use two things: a page control for the part on the right.
For the part on the left I'd think you have a few options, the main probably being a GridLayout using 1 column and speed buttons.
Not overly difficult, but a bit messy. You could probably do with a frame to contain the buttons part.
The only difficult bit would be the separation bars, but maybe you could do that by subclassing it and having specific properties.
Regards,
A