Is there a Delphi function to enable or disable mouse clicks for a stringgrid?
I'm using a derivation of stringgrid called Tadvstringgrid which allows coloring cells based on contens
I need to prevent mouse clicks inside a stringgrid while populating control with data from various threads.
Only disabling the control is not enough. If I click in random cells, the info gets screwed up meaning that some strings are placed in the last cell I've clicked.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, StdCtrls;
type
TForm1 = class(TForm)
StringGrid1: TStringGrid;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
type
TThread_populate_stringgrid = class(TThread)
strict private
f_stringgrid_to_populate:Tstringgrid;
f_name:string;
protected
procedure Execute; override;
public
constructor Create(a_name:string;a_stringgrid_to_populate:Tstringgrid);
end;
constructor TThread_populate_stringgrid.Create(a_name:string;a_stringgrid_to_populate:Tstringgrid);
begin
inherited Create(False);
freeonterminate:=true;
priority:=tpNormal ;
f_name:=a_name;
f_stringgrid_to_populate:=a_stringgrid_to_populate;
end;
procedure TThread_populate_stringgrid.Execute;
begin
Synchronize(
procedure
begin
f_stringgrid_to_populate.cells[0,0]:='DATE';
f_stringgrid_to_populate.cells[1,0]:='NAME';
f_stringgrid_to_populate.cells[2,0]:='ADRESS';
f_stringgrid_to_populate.cells[3,0]:='CITY';
f_stringgrid_to_populate.cells[4,0]:='COUNTRY';
f_stringgrid_to_populate.Cols[0].Add(FormatDatetime('dd-mm-yyyy hh:mm:ss', Now));
f_stringgrid_to_populate.Cols[1].Add(f_name);
f_stringgrid_to_populate.Cols[2].Add('58 RED ROAD');
f_stringgrid_to_populate.Cols[3].Add('ENGLAND');
f_stringgrid_to_populate.Cols[3].Add('UK');
end
)
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
TThread_populate_stringgrid.Create('Andrei',form1.StringGrid1);
TThread_populate_stringgrid.Create('Matei',form1.StringGrid1);
TThread_populate_stringgrid.Create('Iulia',form1.StringGrid1);
TThread_populate_stringgrid.Create('Petru',form1.StringGrid1);
TThread_populate_stringgrid.Create('Gheorghe',form1.StringGrid1);
TThread_populate_stringgrid.Create('Tudor',form1.StringGrid1);
TThread_populate_stringgrid.Create('Cineva',form1.StringGrid1);
TThread_populate_stringgrid.Create('Altcine',form1.StringGrid1);
end;
end.
Thank you!
Here is the solution that works for me:
-In the main form place a "ApplicationEvents" component.
-Go to the Events section of the ApplicationEvents
-double click on "Onmessage" property and add the following procedure
procedure TForm1.deactivate_mouse_in_advstringgrid(var Msg: tagMSG;
var Handled: Boolean);
var
pnt: TPoint;
ctrl: TWinControl;
begin
if (
(Msg.message = SB_VERT) OR
(Msg.message = SB_HORZ) OR
(Msg.message = WS_HSCROLL) OR
(Msg.message = WS_VSCROLL) OR
(Msg.message = WM_VSCROLL) OR
(Msg.message = WM_MOUSEWHEEL) OR
(Msg.message = WM_LBUTTONDOWN) OR
(Msg.message = WM_LBUTTONUP) OR
(Msg.message = WM_LBUTTONDBLCLK) OR
(Msg.message = WM_MBUTTONDOWN) OR
(Msg.message = WM_MBUTTONUP) OR
(Msg.message = WM_MBUTTONDBLCLK) OR
(Msg.message = WM_RBUTTONDOWN) OR
(Msg.message = WM_RBUTTONUP) OR
(Msg.message = WM_RBUTTONDBLCLK) OR
(Msg.message = WM_KEYUP) OR
(Msg.message = WM_KEYDOWN)
)
then
begin
if not GetCursorPos(pnt) then Exit;
ctrl := FindVCLWindow(pnt);
if Assigned(CTRL) then
begin
if Ctrl is TAdvstringgrid then
begin
// Msg.hwnd:=ctrl.Handle;
//Msg.hwnd := advsg1.Handle;
if thread_activ>0 then
begin
Msg.hwnd := 0;
Exit;
end
else
begin
Msg.hwnd:=ctrl.Handle;
end;
end;
end;
end;
end;
If you use a Tadvstringgrid component change this code to "if Ctrl is TAdvstringgrid".
If you use a Stringgrid change this to "if Ctrl is TStringgrid".
The above procedure uses a global variable called "thread_activ" that contains the number of active threads. So if there are active threads using the Advstringgrid component the mouse clicks, mouse wheel, scroll bars, and key presses are supressed.
The thread is stored in a second unit having the following code:
unit thread_stringgrid;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, StdCtrls;
type
TThread_populate_stringgrid = class(TThread)
strict private
f_stringgrid_to_populate:Tstringgrid;
f_name:string;
protected
procedure Execute; override;
public
CriticalSection: TRTLCriticalSection;
constructor Create(a_name:string;a_stringgrid_to_populate:Tstringgrid);
end;
implementation
{use the unit that holds the global vaiable "thread_activ"}
uses unit1;
constructor TThread_populate_stringgrid.Create(a_name:string;a_stringgrid_to_populate:Tstringgrid);
begin
inherited Create(False);
freeonterminate:=true;
priority:=tpNormal ;
f_name:=a_name;
f_stringgrid_to_populate:=a_stringgrid_to_populate;
end;
procedure TThread_populate_stringgrid.Execute;
begin
//before the threads starts
InitializeCriticalSection(CriticalSection);
//in the thread
EnterCriticalSection(CriticalSection);
//From now on, you can safely make
//changes to the variables.
{increment variable from main unit1}
inc(unit1.thread_activ);
//End of safe block
LeaveCriticalSection(CriticalSection);
Synchronize(
procedure
begin
f_stringgrid_to_populate.cells[0,0]:='DATE';
f_stringgrid_to_populate.cells[1,0]:='NAME';
f_stringgrid_to_populate.cells[2,0]:='ADRESS';
f_stringgrid_to_populate.cells[3,0]:='CITY';
f_stringgrid_to_populate.cells[4,0]:='COUNTRY';
f_stringgrid_to_populate.Cols[0].Add(FormatDatetime('dd-mm-yyyy hh:mm:ss', Now));
f_stringgrid_to_populate.Cols[1].Add(f_name);
f_stringgrid_to_populate.Cols[2].Add('58 RED ROAD');
f_stringgrid_to_populate.Cols[3].Add('ENGLAND');
f_stringgrid_to_populate.Cols[3].Add('UK');
end
);
{eliminate thread counter from global variable "thread_activ"}
EnterCriticalSection(CriticalSection);
try
{decrement variable from main unit1}
dec(unit1.thread_activ);
finally
LeaveCriticalSection(CriticalSection);
end;
end;
(*
procedure TForm1.Button1Click(Sender: TObject);
begin
TThread_populate_stringgrid.Create('Andrei',form1.StringGrid1);
TThread_populate_stringgrid.Create('Matei',form1.StringGrid1);
TThread_populate_stringgrid.Create('Iulia',form1.StringGrid1);
TThread_populate_stringgrid.Create('Petru',form1.StringGrid1);
TThread_populate_stringgrid.Create('Gheorghe',form1.StringGrid1);
TThread_populate_stringgrid.Create('Tudor',form1.StringGrid1);
TThread_populate_stringgrid.Create('Cineva',form1.StringGrid1);
TThread_populate_stringgrid.Create('Altcine',form1.StringGrid1);
end;
*)
(*
procedure TForm1.GridDrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
var
S: string;
RectForText: TRect;
begin
// Check for your cell here (in this case the cell in column 4 and row 2 will be colored)
//if (ACol = 1) and (ARow = 1) then
if ((ACol = 1) and (ARow>0)) then
begin
S := form1.StringGrid1.Cells[ACol, ARow];
if (s='Andrei') then
begin
// Fill rectangle with colour
//form1.StringGrid1.Canvas.Brush.Color := clBlack;
form1.StringGrid1.Canvas.FillRect(Rect);
// Next, draw the text in the rectangle
form1.StringGrid1.Canvas.Font.Color := clGreen;
RectForText := Rect;
// Make the rectangle where the text will be displayed a bit smaller than the cell
// so the text is not "glued" to the grid lines
InflateRect(RectForText, -2, -2);
// Edit: using TextRect instead of TextOut to prevent overflowing of text
form1.StringGrid1.Canvas.TextRect(RectForText, S);
end;
end;
// if (ACol = 2) and (ARow = 1) then
if ((ACol = 2)and (ARow>0)) then
begin
S := form1.StringGrid1.Cells[ACol, ARow];
if (s='58 RED ROAD') then
begin
// Fill rectangle with colour
//form1.StringGrid1.Canvas.Brush.Color := clwhite;
form1.StringGrid1.Canvas.FillRect(Rect);
// Next, draw the text in the rectangle
form1.StringGrid1.Canvas.Font.Color := clRed;
RectForText := Rect;
// Make the rectangle where the text will be displayed a bit smaller than the cell
// so the text is not "glued" to the grid lines
InflateRect(RectForText, -2, -2);
// Edit: using TextRect instead of TextOut to prevent overflowing of text
form1.StringGrid1.Canvas.TextRect(RectForText, S);
end
else
begin
// Fill rectangle with colour
//form1.StringGrid1.Canvas.Brush.Color := clwhite;
form1.StringGrid1.Canvas.FillRect(Rect);
// Next, draw the text in the rectangle
form1.StringGrid1.Canvas.Font.Color := clBlue;
RectForText := Rect;
// Make the rectangle where the text will be displayed a bit smaller than the cell
// so the text is not "glued" to the grid lines
InflateRect(RectForText, -2, -2);
// Edit: using TextRect instead of TextOut to prevent overflowing of text
form1.StringGrid1.Canvas.TextRect(RectForText, S);
end;
end;
//if (ACol = 3) and (ARow = 1) then
if ((ACol = 3)and (ARow>0)) then
begin
S := form1.StringGrid1.Cells[ACol, ARow];
if s='Altcine' then
begin
// Fill rectangle with colour
//form1.StringGrid1.Canvas.Brush.Color := clwhite;
form1.StringGrid1.Canvas.FillRect(Rect);
// Next, draw the text in the rectangle
form1.StringGrid1.Canvas.Font.Color := clYellow;
RectForText := Rect;
// Make the rectangle where the text will be displayed a bit smaller than the cell
// so the text is not "glued" to the grid lines
InflateRect(RectForText, -2, -2);
// Edit: using TextRect instead of TextOut to prevent overflowing of text
form1.StringGrid1.Canvas.TextRect(RectForText, S);
end;
end;
*)
end.
I hope that this helps others. Have a great day!
Related
I'm having issues getting a TCustomControl to work with transparency in Delphi 2007. I've currently reduced the problem to the code below. The issue is that when the form is initially created the controls are drawing in the reverse order they are added to the form. When the form is resized, they paint in the correct order. What am I doing wrong? Excluding 3rd party solutions is there a more appropriate path to follow?
Here's my sample project demonstrating the issue in Delphi 2007.
unit Main;
interface
uses
Forms, Classes, Controls, StdCtrls, Messages,
ExtCtrls;
type
// Example of a TWinControl derived control
TMyCustomControl = class(TCustomControl)
protected
procedure CreateParams(var params: TCreateParams); override;
procedure WMEraseBkGnd(var msg: TWMEraseBkGnd);
message WM_ERASEBKGND;
procedure Paint; override;
end;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
private
YellowBox: TMyCustomControl;
GreenBox: TMyCustomControl;
end;
var
Form1: TForm1;
implementation
uses
Windows, Graphics;
{$R *.dfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
self.OnPaint := FormPaint;
GreenBox := TMyCustomControl.Create(self);
GreenBox.Parent := self;
GreenBox.SetBounds(10,10,200,200);
GreenBox.color := clGreen;
YellowBox := TMyCustomControl.Create(self);
YellowBox.Parent := self;
YellowBox.SetBounds(100,100,200,200);
YellowBox.color := clYellow;
end;
// Paint bars on form background
procedure TForm1.FormPaint(Sender: TObject);
var
Idx: Integer;
begin
for Idx := 0 to ClientHeight div 8 do
begin
if Odd(Idx) then
Canvas.Brush.Color := clWhite
else
Canvas.Brush.Color := clSilver; // pale yellow
Canvas.FillRect(Rect(0, Idx * 8, ClientWidth, Idx * 8 + 8));
end;
end;
{ TMyCustomControl }
procedure TMyCustomControl.CreateParams(var params: TCreateParams);
begin
inherited;
params.ExStyle := params.ExStyle or WS_EX_TRANSPARENT;
end;
procedure TMyCustomControl.WMEraseBkGnd(var msg: TWMEraseBkGnd);
begin
SetBkMode (msg.DC, TRANSPARENT);
msg.result := 1;
end;
procedure TMyCustomControl.Paint;
begin
Canvas.Brush.Color := color;
Canvas.RoundRect(0,0,width,height,50,50);
end;
end.
What is wrong is your expectancy of the order of painting of your controls. The order of controls receiving WM_PAINT messages is documented to be actually in the exact opposite order, the top-most control receives the message first. More on the documentation later, since having WS_EX_TRANSPARENT styled siblings leaves us in undocumented territory. As you have already noted, you have a case where the order of the controls receiving WM_PAINT messages is not deterministic - when resizing the window the order changes.
I've modified a bit of your reproduction case to see what is happening. The modifications are the inclusion of two panels and a debug output when they receive WM_PAINT.
unit Unit1;
interface
uses
Forms, Classes, Controls, StdCtrls, Messages, ExtCtrls;
type
TMyCustomControl = class(TCustomControl)
protected
procedure CreateParams(var params: TCreateParams); override;
procedure WMEraseBkGnd(var msg: TWMEraseBkGnd);
message WM_ERASEBKGND;
procedure Paint; override;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
end;
TPanel = class(extctrls.TPanel)
protected
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
end;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
private
YellowBox: TMyCustomControl;
GreenBox: TMyCustomControl;
Panel1, Panel2: TPanel;
end;
var
Form1: TForm1;
implementation
uses
sysutils, windows, graphics;
{$R *.dfm}
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
Width := 590;
Height := 270;
OnPaint := FormPaint;
GreenBox := TMyCustomControl.Create(self);
GreenBox.Parent := self;
GreenBox.SetBounds(20, 20, 140, 140);
GreenBox.color := clGreen;
GreenBox.Name := 'GreenBox';
//{
Panel1 := TPanel.Create(Self);
Panel1.Parent := Self;
Panel1.SetBounds(240, 40, 140, 140);
Panel1.ParentBackground := False;
Panel1.Color := clMoneyGreen;
Panel1.Name := 'Panel1';
Panel2 := TPanel.Create(Self);
Panel2.Parent := Self;
Panel2.SetBounds(260, 60, 140, 140);
Panel2.ParentBackground := False;
Panel2.Color := clCream;
Panel2.Name := 'Panel2';
//}
YellowBox := TMyCustomControl.Create(self);
YellowBox.Parent := self;
YellowBox.SetBounds(80, 80, 140, 140);
YellowBox.color := clYellow;
YellowBox.Name := 'YellowBox';
YellowBox.BringToFront;
end;
// Paint bars on form background
procedure TForm1.FormPaint(Sender: TObject);
var
Idx: Integer;
begin
for Idx := 0 to ClientHeight div 8 do
begin
if Odd(Idx) then
Canvas.Brush.Color := clWhite
else
Canvas.Brush.Color := clSilver; // pale yellow
Canvas.FillRect(Rect(0, Idx * 8, ClientWidth, Idx * 8 + 8));
end;
end;
{ TPanel }
procedure TPanel.WMPaint(var Message: TWMPaint);
begin
OutputDebugString(PChar(Format(' %s painting..', [Name])));
inherited;
end;
{ TMyCustomControl }
procedure TMyCustomControl.CreateParams(var params: TCreateParams);
begin
inherited;
params.ExStyle := params.ExStyle or WS_EX_TRANSPARENT;
end;
procedure TMyCustomControl.WMEraseBkGnd(var msg: TWMEraseBkGnd);
begin
msg.Result := 1;
end;
procedure TMyCustomControl.WMPaint(var Message: TWMPaint);
begin
OutputDebugString(PChar(Format(' %s painting..', [Name])));
inherited;
end;
procedure TMyCustomControl.Paint;
begin
Canvas.Brush.Color := Color;
Canvas.RoundRect(0, 0, Width, Height, 50, 50);
end;
end.
Which produces this form:
As determined by order of creation, the z-order is, from bottom to top,
GreenBox,
Panel1,
Panel2,
YellowBox.
The debug output for the WM_PAINT messages is this:
Debug Output: Panel2 painting.. Process Project1.exe (12548)
Debug Output: Panel1 painting.. Process Project1.exe (12548)
Debug Output: YellowBox painting.. Process Project1.exe (12548)
Debug Output: GreenBox painting.. Process Project1.exe (12548)
There are two things worth to note in this order.
First, Panel2 receives the paint message before Panel1, although Panel2 is higher in the z-order.
So how is it that while we see Panel2 as a whole, but we see only part of Panel1 even though it is painted later? This is where update regions come into play. The WS_CLIPSIBLINGS style flags in controls tell the OS that part of a control occupied by a sibling higher in the z-order is not going to be painted.
Clips child windows relative to each other; that is, when a particular
child window receives a WM_PAINT message, the WS_CLIPSIBLINGS
style clips all other overlapping child windows out of the region of
the child window to be updated.
Let's dig into a bit more in the WM_PAINT handler of Panel1 and see how the OS' update region looks like.
{ TPanel }
// not declared in D2007
function GetRandomRgn(hdc: HDC; hrgn: HRGN; iNum: Integer): Integer; stdcall;
external gdi32;
const
SYSRGN = 4;
procedure TPanel.WMPaint(var Message: TWMPaint);
var
PS: TPaintStruct;
Rgn: HRGN;
TestDC: HDC;
begin
OutputDebugString(PChar(Format(' %s painting..', [Name])));
Message.DC := BeginPaint(Handle, PS);
Rgn := CreateRectRgn(0, 0, 0, 0);
if (Name = 'Panel1') and (GetRandomRgn(Message.DC, Rgn, SYSRGN) = 1) then begin
OffsetRgn(Rgn, - Form1.ClientOrigin.X + Width + 40, - Form1.ClientOrigin.Y);
TestDC := GetDC(Form1.Handle);
SelectObject(TestDC, GetStockObject(BLACK_BRUSH));
PaintRgn(TestDC, Rgn);
ReleaseDC(Form1.Handle, TestDC);
DeleteObject(Rgn);
end;
inherited;
EndPaint(Handle, PS);
end;
The BeginPaint will clip the update region with the system update region which you can then retrieve with GetRandomRgn. I've dumped the clipped update region to the right of the form. Don't mind the Form1 references or missing error checks, we are only debugging. Anyway, this produces the below form:
So, whatever you draw in the client area of Panel1, it will get clipped into the black shape, hence it cannot be visually come into front of Panel2.
Second, remember that the green box is created first, then the panels and then the yellow last. So why is it that the two transparent controls are painted after the two panels?
First, remember that controls are painted from top to bottom. Now, how can it be possible for a transparent control to draw onto something which is drawn after it? Obviously it is not possible. So the entire painting algorithm have to change. There is no documentation on this and the best explanation I've found is from a blog entry of Raymond Chen:
... The WS_EX_TRANSPARENT extended window style alters the painting
algorithm as follows: If a WS_EX_TRANSPARENT window needs to be
painted, and it has any non-WS_EX_TRANSPARENT windows siblings (which
belong to the same process) which also need to be painted, then the
window manager will paint the non-WS_EX_TRANSPARENT windows first.
The top to bottom painting order makes it a difficult one when you have transparent controls. Then there is the case of overlapping transparent controls - which is more transparent than the other? Just accept the fact that overlapping transparent controls produce undetermined behavior.
If you investigate the system update regions of the transparent boxes in the above test case, you'll find both to be exact squares.
Let's shift the panels to in-between the boxes.
procedure TForm1.FormCreate(Sender: TObject);
begin
Width := 590;
Height := 270;
OnPaint := FormPaint;
GreenBox := TMyCustomControl.Create(self);
GreenBox.Parent := self;
GreenBox.SetBounds(20, 20, 140, 140);
GreenBox.color := clGreen;
GreenBox.Name := 'GreenBox';
//{
Panel1 := TPanel.Create(Self);
Panel1.Parent := Self;
Panel1.SetBounds(40, 40, 140, 140);
Panel1.ParentBackground := False;
Panel1.Color := clMoneyGreen;
Panel1.Name := 'Panel1';
Panel2 := TPanel.Create(Self);
Panel2.Parent := Self;
Panel2.SetBounds(60, 60, 140, 140);
Panel2.ParentBackground := False;
Panel2.Color := clCream;
Panel2.Name := 'Panel2';
//}
YellowBox := TMyCustomControl.Create(self);
YellowBox.Parent := self;
YellowBox.SetBounds(80, 80, 140, 140);
YellowBox.color := clYellow;
YellowBox.Name := 'YellowBox';
YellowBox.BringToFront;
end;
...
procedure TMyCustomControl.WMPaint(var Message: TWMPaint);
var
PS: TPaintStruct;
Rgn: HRGN;
TestDC: HDC;
begin
OutputDebugString(PChar(Format(' %s painting..', [Name])));
Message.DC := BeginPaint(Handle, PS);
Rgn := CreateRectRgn(0, 0, 0, 0);
if (Name = 'GreenBox') and (GetRandomRgn(Message.DC, Rgn, SYSRGN) = 1) then begin
OffsetRgn(Rgn, - Form1.ClientOrigin.X + Width + 260, - Form1.ClientOrigin.Y);
TestDC := GetDC(Form1.Handle);
SelectObject(TestDC, GetStockObject(BLACK_BRUSH));
PaintRgn(TestDC, Rgn);
ReleaseDC(Form1.Handle, TestDC);
DeleteObject(Rgn);
end;
inherited;
EndPaint(Handle, PS);
end;
The right-most black shape is the system update region for the GreenBox. After all the system can apply clipping to a transparent control. I think it would suffice to conclude that the painting algorithm is not perfect when you've got a bunch of transparent controls.
As promised, the documentation quote for the WM_PAINT order. One reason I've left this to last is that it includes a possible solution (of course we already found one solution, scatter some non-transparent controls in-between your transparent controls):
... If a window in the parent chain is composited (a window with
WX_EX_COMPOSITED), sibling windows receive WM_PAINT messages in the
reverse order of their position in the Z order. Given this, the window
highest in the Z order (on the top) receives its WM_PAINT message
last, and vice versa. If a window in the parent chain is not
composited, sibling windows receive WM_PAINT messages in Z order.
For as little as I tested, setting WS_EX_COMPOSITED on the parent form seems to work. But I don't know if it is applicable in your case.
------------------------- ORIGINAL QUESTION -------------------------
Greetings to all Delphi developers! In a Delphi 2006 non MDI application, I create a non-sizeable, autoscrollable, autosizeable form. This is an excerpt from the form's unit:
uses Grid;
TGridFrm = class(TForm)
public
Grid : TGrid;
constructor Create(AOwner : TComponent; Asize : TPoint);
end;
implementation
constructor TGridFrm.Create(AOwner: TComponent; Asize : TPoint);
begin
inherited Create(aowner);
borderstyle := bsSingle; // users are not allowed to resize the form
windowstate := wsNormal;
borderwidth := 0;
autosize := True;
autoscroll := True;
constraints.maxwidth := screen.width - 1;
constraints.maxheight := screen.height - 1;
grid := TGrid.Create(asize.x, asize.y, self);
end;
Now, TGrid is a custom control with its own canvas of course. This is an excerpt from its unit:
TGrid = class (TCustomControl)
public
NoOfCellsX,
NoOfCellsY,
CellSize : integer;
procedure SetZoom(z : integer);
constructor Create(AWidth, AHeight : Integer; AParent : TForm = nil);
end;
implementation
constructor TGrid.Create(AWidth, AHeight : Integer; AParent : TForm = nil);
begin
inherited Create(AParent);
Parent := AParent;
align := alCustom;
left := 0;
top := 0;
end;
procedure TGrid.SetZoom(zoom : integer);
begin
cellsize := zoom * 10 div 100;
width := noofcellsx * cellsize;
height := noofcellsy * cellsize;
end;
In the form's unit I have arranged things up (through an ApplicationEvents object) so that SetZoom is called with some zoom value, whenever the numeric +/- keys are pressed. The idea behind all this was to have my custom control snap to the upper left corner of the form (with some predefined margin/borderwidth), and have the entire form automatically adjust its size whenever I zoom in or out of the custom control, but never extending beyond the screen limits. It's working, but only up to the point where the scrollbars must become visible: they never show up. Since this is an autoscrollable form, aren't they supposed to show up whenever a control inside the form (Grid in this case) gets larger than the constrained form and get out of the way when it gets smaller? I even tried some refactoring by moving SetZoom to the form's class, but to no avail. What am i missing here?
----------------- COMPILABLE CODE ADDED AFTERWARDS ------------------
The project file:
program MyApp;
uses
Forms,
Grid in 'Source\Grid.pas',
GridForm in 'Source\GridForm.pas' {GridFrm},
Main in 'Source\Main.pas' {MainFrm};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TMainFrm, MainFrm);
Application.Run;
end.
The Main.pas:
unit Main;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TMainFrm = class(TForm)
CreateNewFormButton: TButton;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure CreateNewFormButtonClick(Sender: TObject);
end;
var
MainFrm: TMainFrm;
implementation
{$R *.dfm}
uses
GridForm;
procedure TMainFrm.CreateNewFormButtonClick(Sender: TObject);
var aform : TForm;
begin
aform := TGridFrm.Create(self, point(15, 15));
aform.show;
tgridfrm(aform).grid.SetZoom(100);
end;
procedure TMainFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
end.
The GridForm.pas:
unit GridForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grid, AppEvnts;
type
TGridFrm = class(TForm)
ApplicationEvents1: TApplicationEvents;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
private
TheGrid : TGrid;
public
property Grid : TGrid READ TheGrid WRITE TheGrid;
constructor Create(AOwner : TComponent; ASize : TPoint);
end;
var
GridFrm: TGridFrm;
implementation
{$R *.dfm}
procedure TGridFrm.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
var keystate : TKeyboardState;
begin
if not Active then begin exit; end;
if msg.message = WM_KEYDOWN then
begin
getkeyboardstate(keystate);
case msg.wparam of
vk_Add : begin // zoom in
grid.setzoom(grid.zoom + 10);
handled := True;
end;
vk_Subtract : begin // zoom out
grid.setzoom(grid.zoom - 10);
handled := True;
end;
// other keys down here...
end;
end;
end;
constructor TGridFrm.Create(AOwner : TComponent; ASize : TPoint);
begin
inherited Create(AOwner);
borderstyle := bsSingle;
borderwidth := 2;
autosize := True;
autoscroll := True;
constraints.maxwidth := screen.width - 1;
constraints.maxheight := screen.height - 1;
visible := False;
grid := TGrid.Create(asize.x, asize.y, random(800) + 500, self);
end;
procedure TGridFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
end.
And the Grid.pas:
unit Grid;
interface
uses
StdCtrls, SysUtils, Controls, Forms, Graphics, Dialogs;
type
TGrid = class (TCustomControl)
Lbl1, Lbl2,
GridSizeInfoLbl,
FormSizeInfoLbl,
WarningLbl : TLabel;
public
NoOfCellsX,
NoOfCellsY,
SquareSize, // in 1/1000ths of centimeter
CellSize, // in pixels
Zoom : integer;
procedure SetZoom(z : integer);
constructor Create(x, y, asquaresize : integer; AParent : TForm = nil);
end;
implementation
uses
GridForm;
constructor TGrid.Create(x, y, asquaresize : integer; AParent : TForm = nil);
begin
inherited Create(AParent);
parent := AParent;
color := clTeal;
align := alCustom;
left := 0;
top := 0;
noofcellsx := x;
noofcellsy := y;
squaresize := asquaresize;
Lbl1 := TLabel.Create(self);
Lbl2 := TLabel.Create(self);
GridSizeInfoLbl := TLabel.Create(self);
FormSizeInfoLbl := TLabel.Create(self);
WarningLbl := TLabel.Create(self);
with Lbl1 do
begin
parent := self;
caption := 'Size of grid: ';
width := 55;
height := 18;
left := 2;
top := 1;
end;
with Lbl2 do
begin
parent := self;
caption := 'Size of form: ';
width := 75;
height := 18;
left := 2;
top := 19;
end;
with GridSizeInfoLbl do
begin
parent := self;
width := 100;
height := 18;
left := 65;
top := 1;
end;
with FormSizeInfoLbl do
begin
parent := self;
width := 100;
height := 18;
left := 65;
top := 19;
end;
with WarningLbl do
begin
parent := self;
width := 150;
height := 18;
left := 2;
top := 39;
end;
end;
procedure TGrid.SetZoom(z : integer);
begin
zoom := z;
cellsize := (screen.pixelsperinch * squaresize * zoom) div (1000 * 254);
width := noofcellsx * cellsize;
height := noofcellsy * cellsize;
GridSizeInfoLbl.caption := inttostr(Width) +
'x' + inttostr(Height) +
' (zoom: ' + inttostr(zoom) +
', cellsize zoomed: ' + inttostr(cellsize) +
', squaresize: ' + inttostr(squaresize) +
'mm, squares: ' + inttostr(noofcellsx) + 'x' + inttostr(noofcellsy) + ')';
with tgridfrm(parent) do
begin
left := (screen.Width - width) div 2;
top := (screen.Height - height) div 2;
FormSizeInfoLbl.caption := inttostr(Width) + 'x' + inttostr(Height) +
' (clientarea: ' + inttostr(clientwidth) + 'x' + inttostr(clientheight) + ')';
if self.width > clientwidth then
if self.Height > clientheight then
warninglbl.caption := 'Both scrollbars should appear!'
else
warninglbl.caption := 'Horizontal scrollbar should appear!'
else if self.Height > clientheight then
warninglbl.caption := 'Vertical scrollbar should appear!'
else
warninglbl.caption := 'No scrollbars needed';
end;
end;
end.
Code synopsis: A click on the main form' s button creates an autosizeable form, which in turn creates a child grid of random initial size. Numeric +/- keys make the grid larger or smaller and the form is autosized accordingly, but no scrollbars ever show up, no matter how large the grid becomes (the labels I added provide visual feedback).
Your problem is twofold.
The first is, as Jerry commented to the question, AutoSize. The purpose of autosize is to resize the form such that content is visible. There can be no scrollbars when all content is visible, so clearly the two properties are contradictory.
As such VCL developers have took their precaution. Below is from D2007 source:
function TScrollingWinControl.AutoScrollEnabled: Boolean;
begin
Result := not AutoSize and not (DockSite and UseDockManager);
end;
As you can see setting AutoScroll has no effect when AutoSize is set.
You could override this behavior, this is a virtual method, if it wouldn't interfere with the second fold.
Now that you've decided to leave autosize off and calculate and set the required size of your form yourself depending on the workarea size, meet your second problem: alignment of your grid control.
The below is the D2007 code when a vertical scroll bar wants to see if it needs to adjust:
procedure ProcessVert(Control: TControl);
begin
if Control.Visible then
case Control.Align of
alTop, alNone:
if (Control.Align = alTop) or (Control.Anchors * [akTop, akBottom] = [akTop]) then
NewRange := Max(NewRange, Position + Control.Top + Control.Height);
alBottom: Inc(AlignMargin, Control.Height);
end;
end;
As you can see a control will not have an effect on an automatic vertical scroll bar if it doesn't have either alTop, alBottom or alNone alignment. Yours have alCustom.
This is also why overriding autosizing behavior won't help, AutoSize depends on controls having "left", "right", "top", "bottom" or "none" aligned controls.
You have to redesign your control taking into consideration how VCL internally works. Not all of the internal dependency aspects can be documented, so you have to use the source for this kind of enhanced development.
I was inspired by this question: How to make a combo box with full-text search autocomplete support?
The answer works just fine but I want to adjust the suggestion list Height/DropDownCount when the user types the text while the list is already dropped down.
Here is an MCVE with minor modifications - When the user starts typing, the drop down list will drop-down, and I also fixed the mouse cursor effect not being set to arrow when the list dropped down:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, StrUtils, ExtCtrls;
type
TComboBox = class(StdCtrls.TComboBox)
private
FStoredItems: TStringList;
FOldCursor: TCursor; // NEW !!!
procedure FilterItems;
procedure StoredItemsChange(Sender: TObject);
procedure SetStoredItems(const Value: TStringList);
procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
procedure AdjustDropDownHeight; // NEW !!!
protected
// NEW !!!
procedure KeyPress(var Key: Char); override;
procedure DropDown; override;
procedure CloseUp; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property StoredItems: TStringList read FStoredItems write SetStoredItems;
end;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
constructor TComboBox.Create(AOwner: TComponent);
begin
inherited;
AutoComplete := False;
FStoredItems := TStringList.Create;
FStoredItems.OnChange := StoredItemsChange;
end;
destructor TComboBox.Destroy;
begin
FStoredItems.Free;
inherited;
end;
procedure TComboBox.CNCommand(var AMessage: TWMCommand);
begin
// we have to process everything from our ancestor
inherited;
// if we received the CBN_EDITUPDATE notification
if AMessage.NotifyCode = CBN_EDITUPDATE then
// fill the items with the matches
FilterItems;
end;
procedure TComboBox.FilterItems;
var
I: Integer;
Selection: TSelection;
begin
// store the current combo edit selection
SendMessage(Handle, CB_GETEDITSEL, WPARAM(#Selection.StartPos),
LPARAM(#Selection.EndPos));
// begin with the items update
Items.BeginUpdate;
try
// if the combo edit is not empty, then clear the items
// and search through the FStoredItems
if Text <> '' then
begin
// clear all items
Items.Clear;
// iterate through all of them
for I := 0 to FStoredItems.Count - 1 do
// check if the current one contains the text in edit
if ContainsText(FStoredItems[I], Text) then
// and if so, then add it to the items
Items.Add(FStoredItems[I]);
end
// else the combo edit is empty
else
// so then we'll use all what we have in the FStoredItems
Items.Assign(FStoredItems)
finally
// finish the items update
Items.EndUpdate;
end;
// and restore the last combo edit selection
SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos,
Selection.EndPos));
// NEW !!! - if the list is dropped down adjust the list height
if DroppedDown then
AdjustDropDownHeight;
end;
procedure TComboBox.StoredItemsChange(Sender: TObject);
begin
if Assigned(FStoredItems) then
FilterItems;
end;
procedure TComboBox.SetStoredItems(const Value: TStringList);
begin
if Assigned(FStoredItems) then
FStoredItems.Assign(Value)
else
FStoredItems := Value;
end;
// NEW !!!
procedure TComboBox.KeyPress(var Key: Char);
begin
inherited;
if not (Ord(Key) in [VK_RETURN, VK_ESCAPE]) then
begin
if (Items.Count <> 0) and not DroppedDown then
// SendMessage(Handle, CB_SHOWDROPDOWN, 1, 0);
DroppedDown := True;
end;
end;
procedure TComboBox.DropDown;
begin
FOldCursor := Screen.Cursor;
Screen.Cursor := crArrow;
inherited;
end;
procedure TComboBox.CloseUp;
begin
Screen.Cursor := FOldCursor;
inherited;
end;
procedure TComboBox.AdjustDropDownHeight;
var
Count: Integer;
begin
Count := Items.Count;
SetWindowPos(FDropHandle, 0, 0, 0, Width, ItemHeight * Count +
Height + 2, SWP_NOMOVE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_NOREDRAW or
SWP_HIDEWINDOW);
SetWindowPos(FDropHandle, 0, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE or
SWP_NOZORDER or SWP_NOACTIVATE or SWP_NOREDRAW or SWP_SHOWWINDOW);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
ComboBox: TComboBox;
begin
// here's one combo created dynamically
ComboBox := TComboBox.Create(Self);
ComboBox.Parent := Self;
ComboBox.Left := 10;
ComboBox.Top := 10;
// here's how to fill the StoredItems
ComboBox.StoredItems.BeginUpdate;
try
ComboBox.StoredItems.Add('Mr John Brown');
ComboBox.StoredItems.Add('Mrs Amanda Brown');
ComboBox.StoredItems.Add('Mr Brian Jones');
ComboBox.StoredItems.Add('Mrs Samantha Smith');
finally
ComboBox.StoredItems.EndUpdate;
end;
end;
end.
I have added the AdjustDropDownHeight (inspired by TCustomCombo.AdjustDropDown) in the FilterItems method, but it not seems to work as expected. The window hides and it's height is not adjusted according to the actual items in the TComboBox while it is dropped down.
Seems like the FDropHandle is not responding (or handeling) the SetWindowPos(FDropHandle, ... in the AdjustDropDownHeight method.
Can this be fixed? how to adjust the height of the drop down while it is dropped down according to the actual items?
EDIT: Setting DropDownCount := Items.Count (as suggested in the answer) was the first thing I have tried (it sets the max number of items). However the drop down Window does not change its height while typing the text (while it is already dropped down). SetDropDownCount setter simply sets FDropDownCount := Value. this will set the drop down count/height the next time the drop down list is dropped. and I need it to change while it is dropped down. Hope its more clear now.
(Maybe newer Delphi versions have a different SetDropDownCount setter?)
To show better what I want:
User types Mr
Then Mrs (the height of the list is adjusted)
Then user press backspace to Mr (list height adjusted again):
EDIT 2:
#Dsm was correct, and gave me the right direction. newer Delphi version SetDropDownCount setter sends extra CB_SETMINVISIBLE message, and this works as expected:
procedure TCustomCombo.SetDropDownCount(const Value: Integer);
begin
if Value <> FDropDownCount then
begin
FDropDownCount := Value;
if HandleAllocated and CheckWin32Version(5, 1) and ThemeServices.ThemesEnabled then
SendMessage(Handle, CB_SETMINVISIBLE, WPARAM(FDropDownCount), 0);
end;
end;
For older version define:
const
CBM_FIRST = $1700;
CB_SETMINVISIBLE = CBM_FIRST + 1;
It is actually as simple as this
procedure TComboBox.AdjustDropDownHeight;
begin
DropDownCount := Items.Count;
end;
I tested using your MCVE and it works well.
I have a few controls (namely, TDBChart) inside a TFlowPanel. When the user clicks on one of them, I'd like it to fill the entire flow panel's client area. But, it seems that changing the visible and align property of child controls inside a flow panel at run time doesn't have any effect. Is there a special trick to this? I found the Realign() method, but it doesn't seem to have any effect on the control's layout. Here's the code to my OnClick event:
var
AChart: TDBChart;
V: Boolean;
i: Integer;
begin
AChart := TDBChart(Sender);
if AChart.Align = alNone then
begin
V := False;
AChart.Align := alClient;
end else begin
V := True;
AChart.Align := alNone;
end;
for i := 0 to FlowPanel1.ControlCount - 1 do
if FlowPanel1.Controls[i] is TDBChart then
if FlowPanel1.Controls[i] <> AChart then
FlowPanel1.Controls[i].Visible := V;
end;
The charts are hidden or shown as expected, but ADBChart doesn't fill the entire flow panel's client area.
As by design, T(Custom)FlowPanel uses customized aligning of child controls, which is implemented in an overriden AlignControls method.
You can prevent this default behaviour by skipping it, falling back on that from its ancestor. Also, hiding all adjacent controls is not necessary. Bringing the clicked chart to front will suffice.
type
TFlowPanel = class(Vcl.ExtCtrls.TFlowPanel)
private
FFlowDisabled: Boolean;
procedure SetFlowDisabled(Value: Boolean);
protected
procedure AlignControls(AControl: TControl; var Rect: TRect); override;
public
property FlowDisabled: Boolean read FFlowDisabled write SetFlowDisabled;
end;
...
{ TFlowPanel }
type
TWinControlAccess = class(TWinControl);
TAlignControls = procedure(Instance: TObject; AControl: TControl;
var Rect: TRect);
procedure TFlowPanel.AlignControls(AControl: TControl; var Rect: TRect);
begin
if FFlowDisabled then
// Skip inherited in TCustomFlowPanel:
TAlignControls(#TWinControlAccess.AlignControls)(Self, AControl, Rect)
else
inherited;
end;
procedure TFlowPanel.SetFlowDisabled(Value: Boolean);
begin
if FFlowDisabled <> Value then
begin
FFlowDisabled := Value;
Realign;
end;
end;
{ TForm1 }
procedure TForm1.DBChartClick(Sender: TObject);
const
FlowAligns: array[Boolean] of TAlign = (alNone, alClient);
var
Chart: TDBChart;
Panel: TFlowPanel;
DisableFlow: Boolean;
begin
Chart := TDBChart(Sender);
Panel := Chart.Parent as TFlowPanel;
DisableFlow := not Panel.FlowDisabled;
Chart.Align := FlowAligns[DisableFlow];
Chart.BringToFront;
Panel.FlowDisabled := DisableFlow;
end;
A FlowPanel does not care its controls' alignment settings, much like it doesn't care for their position - it is designed only to flow them.
One solution can be to derive a new class and override AlignControls, and in it, resize the control that would fill the surface accordingly. As an example:
type
TFlowPanel = class(extctrls.TFlowPanel)
protected
procedure AlignControls(AControl: TControl; var Rect: TRect); override;
end;
..
procedure TFlowPanel.AlignControls(AControl: TControl; var Rect: TRect);
var
i, VisibleCount, VisibleControl: Integer;
begin
VisibleCount := 0;
VisibleControl := 0;
for i := 0 to ControlCount - 1 do
if Controls[i].Visible then begin
Inc(VisibleCount);
VisibleControl := i;
end;
if (VisibleCount = 1) and (Controls[VisibleControl] = AControl) and
(AControl.Align = alClient) then begin
// preserve 'Explicit..' settings
AControl.ControlState := AControl.ControlState + [csAligning];
AControl.SetBounds(1, 1, ClientWidth - 1, ClientHeight -1);
AControl.ControlState := AControl.ControlState - [csAligning];
end;
inherited;
end;
Then you can set all of your charts' click event to this handler:
var
AChart: TTDBChart;
procedure SetVisibility(Visible: Boolean);
var
i: Integer;
begin
for i := 0 to FlowPanel1.ControlCount - 1 do
if FlowPanel1.Controls[i] is TDBChart then
if FlowPanel1.Controls[i] <> AChart then
FlowPanel1.Controls[i].Visible := Visible;
end;
begin
AChart := TDBChart(Sender);
if AChart.Align = alNone then
begin
SetVisibility(False);
AChart.Align := alClient;
end else begin
AChart.Align := alNone; // set before changing visible
SetVisibility(True);
AChart.SetBounds(0, 0, AChart.ExplicitWidth, AChart.ExplicitHeight);
end;
end;
I should note that this is only good for a fixed sized flowpanel.
I have a TFrame with a TImage as background.
This frame serves as ancestor for other frames that I put on a limited space in the main TForm.
So it is just a user interface base for the other frames.
I need to put many controls inside these frames, because they will handle large database forms.
As the main form has limited space, I need to put a TScrollBox in all the TFrame space except for the title bar. But this covers the backgroud image.
How do I make this ScrollBar to be background transparent?
Or is it better to make a new component with that functionality, and how to do it?
I saw some examples in other sites, but they are buggy at the run-time
Thank You!
Edit2:
I found the TElScrollBox from ElPack from LMD Inovative.
This is background transparent and allow us to put an image as background.
But the same problem occurs: When we scroll it at run-time, it moves the ancestor's background in it's area of effect.
Edit1:
I've tried to make a descendant but the scrollbar only shows when we pass hover the mouse where it should be, and the form's background move inside the scrollbox when we scroll it.
And also, the controls inside of it get some paint errors...
interface
uses
Windows, SysUtils, Messages, Classes, Graphics, Controls, Forms,
Dialogs, Menus, StdCtrls, ExtCtrls;
type
TTransScrollBox = class(TScrollBox)
private
{ Private declarations }
protected
procedure CreateParams(var params: TCreateParams); override;
procedure WMEraseBkGnd(var msg: TWMEraseBkGnd); message WM_ERASEBKGND;
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Eduardo', [TTransScrollBox]);
end;
procedure TTransScrollBox.CreateParams(var params: TCreateParams);
begin
inherited CreateParams(params);
params.ExStyle := params.ExStyle or WS_EX_TRANSPARENT;
end;
procedure TTransScrollBox.WMEraseBkGnd(var Msg: TWMEraseBkGnd);
begin
SetBkMode (Msg.DC, TRANSPARENT);
Msg.Result := 1;
end;
f you don't want the image to scroll you will have to roll your own scroller, which is not too difficult (It still raining here in England so I'm bored!)
To test, Create the frame put the image on and alighn to client.
Put a scrollbar on the frame set to vertical and align right.
enlarge the frame at design time.
Put controls on anywhere and then shrink it so some are not visible (below the bottom).
On the main form in form show (for testing), or when you create a new frame call Frame.BeforeShow to do the setup.
[LATER] EDIT It's raining & Still Bored So I finished it for ya!
unit ScrollingBaseFrameU;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, jpeg, ExtCtrls, StdCtrls, contnrs, Generics.Collections, Grids,
DBGrids;
const
MaxLargeSteps = 5; // maximum large scroll clicks to scroll to max
IgnoreTag = 99; // Controls with this tag value are igored for scrolling
TitleHeight = 23; // Height of title bar as we are dealing with the image mouse co-ordinates
RightMargin = 25; // space after right-most control
BottomMargin = 25; // space after bottom-most control
StrControl = 'ControlName'; // prefix for controls with no name
type
TControlPos = class(Tobject) // Little object to save initial control positions
public
Name: string;
X,
Y: Integer;
end;
TScrollingBaseFrame = class(TFrame)
BackGroundImage: TImage;
HorzScrollBar: TScrollBar;
VertScrollBar: TScrollBar;
pnlTitle: TPanel;
procedure VertScrollBarChange(Sender: TObject);
procedure BackGroundImageMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure FrameResize(Sender: TObject);
procedure HorzScrollBarChange(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
ShowHScroller,
ShowVScroller : Boolean; // scroller needed at all?
PosList: TList<TControlPos>; // list of initial positions
procedure BeforeShow; virtual; // override in descendants for specific behaviour
procedure BeforeClose; virtual; // override in descendants for specific behaviour
function IndexOfPos(AName:string): Integer;
end;
implementation
{$R *.dfm}
procedure TScrollingBaseFrame.BeforeClose;
// Clean up
var
p: TControlPos;
begin
for p in PosList do
p.free;
PosList.Free;
end;
procedure TScrollingBaseFrame.BeforeShow;
//Setup scroller and save initial control positions
var
i,XMax,YMax,Idx: Integer;
AControl: TControl;
begin
pnlTitle.Height := TitleHeight;
PosList := TList<TControlpos>.Create;
XMax := 0;
YMax := 0;
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TControl then
begin
AControl := TControl(Components[i]);
if Acontrol.Tag <> IgnoreTag then
begin
Idx := PosList.Add(TcontrolPos.Create);
if AControl.Name = '' then // deal with empty names
AControl.Name := StrControl + IntToStr(i);
PosList[Idx].Name := AControl.Name;
PosList[Idx].X := AControl.Left;
PosList[Idx].Y := AControl.Top;
if YMax < AControl.Top + AControl.Height then
YMax := AControl.Top + AControl.Height;
if XMax < AControl.Left + AControl.Width then
XMax := AControl.Left + AControl.Width;
end; // Ignored
end; // is control
end; // count
VertScrollBar.Max := (YMax + BottomMargin) - Height; // bit of a bottom margin :)
VertScrollBar.LargeChange := VertScrollBar.Max div MaxLargeSteps;
ShowVScroller := VertScrollBar.Max > BottomMargin;
VertScrollBar.Visible := ShowVScroller;
HorzScrollBar.Max := (XMax + RightMargin) - Width; // bit of a Right margin :)
HorzScrollBar.LargeChange := HorzScrollBar.Max div MaxLargeSteps;
ShowHScroller := HorzScrollBar.Max > RightMargin;
HorzScrollBar.Visible := ShowHScroller;
end;
procedure TScrollingBaseFrame.FrameResize(Sender: TObject);
begin
BackGroundImage.Width := Width;
BackGroundImage.Height := Height;
end;
procedure TScrollingBaseFrame.HorzScrollBarChange(Sender: TObject);
// Move the controls left and right relative to thier initail positions
var
i,j: Integer;
AControl: TControl;
begin
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TControl then
begin
AControl := TControl(Components[i]);
j := IndexOfPos(AControl.Name);
if j >= 0 then // could be ignored or the background image
Acontrol.Left := PosList[j].X - HorzScrollBar.Position;
end;
end;
end;
procedure TScrollingBaseFrame.BackGroundImageMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
// Show/Hide the scrollbars using mouse position
var
ScrollBarWidth: Integer;
begin
ScrollBarWidth := GetSystemmetrics(SM_CXVSCROLL); // assume the same for horizontal
VertScrollBar.Visible := (X > Width - ScrollBarWidth) and ShowVScroller;
HorzScrollBar.Visible := (Y > (Height - TitleHeight) - ScrollBarWidth) and ShowHScroller;
end;
function TScrollingBaseFrame.IndexOfPos(AName:string): Integer;
// Find a control position in the list by name
var
Idx: Integer;
begin
Result := -1;
Idx := 0;
while (Result < 0) and (Idx < PosList.Count) do
begin
if PosList[idx].Name = AName then
Result := idx;
inc(idx);
end;
end;
procedure TScrollingBaseFrame.VertScrollBarChange(Sender: TObject);
// Move the controls Up and down relative to thier initail positions
var
i,j: Integer;
AControl: TControl;
begin
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TControl then
begin
AControl := TControl(Components[i]);
j := IndexOfPos(AControl.Name);
if j >= 0 then // could be ignored
Acontrol.Top := PosList[j].Y - VertScrollBar.Position;
end;
end;
end;
end.
and the DFM for completeness:
object ScrollingBaseFrame: TScrollingBaseFrame
Left = 0
Top = 0
Width = 830
Height = 634
DoubleBuffered = True
ParentDoubleBuffered = False
TabOrder = 0
OnResize = FrameResize
object BackGroundImage: TImage
Tag = 99
Left = 0
Top = 23
Width = 813
Height = 594
Align = alClient
Picture.Data = { **Removed as it was so big!**}
Transparent = True
OnMouseMove = BackGroundImageMouseMove
ExplicitTop = 0
ExplicitWidth = 1600
ExplicitHeight = 1200
end
object HorzScrollBar: TScrollBar
Tag = 99
Left = 0
Top = 617
Width = 830
Height = 17
Align = alBottom
PageSize = 0
TabOrder = 0
OnChange = HorzScrollBarChange
ExplicitLeft = 231
ExplicitTop = 293
ExplicitWidth = 121
end
object VertScrollBar: TScrollBar
Tag = 99
Left = 813
Top = 23
Width = 17
Height = 594
Align = alRight
Kind = sbVertical
PageSize = 0
TabOrder = 1
OnChange = VertScrollBarChange
ExplicitTop = 29
end
object pnlTitle: TPanel
Tag = 99
Left = 0
Top = 0
Width = 830
Height = 23
Align = alTop
Caption = 'pnlTitle'
TabOrder = 2
ExplicitLeft = 184
ExplicitTop = 3
ExplicitWidth = 185
end
end
[2ND EDIT] Well, Not wanting my spare time to go to waste, the below should work with Delphi 6 onwards.
unit ScrollingBaseFrameU;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, jpeg, ExtCtrls, StdCtrls, contnrs, Grids,
DBGrids;
const
MaxLargeSteps = 5; // maximum large scroll clicks to scroll to max
IgnoreTag = 99; // Controls with this tag value are igored for scrolling
TitleHeight = 23; // Height of title bar as we are dealing with the image mouse co-ordinates
RightMargin = 25; // space after right-most control
BottomMargin = 25; // space after bottom-most control
StrControl = 'ControlName'; // prefix for controls with no name
type
TControlPos = class(Tobject) // Little object to save initial control positions
public
Name: string;
X,
Y: Integer;
end;
TControlPosList = class(TObject)
private
function GetCount: Integer;
function GetItems(Index: Integer): TControlPos;
procedure SetItems(Index: Integer; const Value: TControlPos);
public
TheList: TObjectList;
Constructor Create; virtual;
Destructor Destroy; override;
function Add(APos: TControlPos): Integer;
function IndexOfPos(AName: string): Integer;
property Count: Integer read GetCount;
property Items[Index: Integer]: TControlPos read GetItems write SetItems; default;
end;
TScrollingBaseFrame = class(TFrame)
BackGroundImage: TImage;
HorzScrollBar: TScrollBar;
VertScrollBar: TScrollBar;
pnlTitle: TPanel;
procedure VertScrollBarChange(Sender: TObject);
procedure BackGroundImageMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure FrameResize(Sender: TObject);
procedure HorzScrollBarChange(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
ShowHScroller,
ShowVScroller : Boolean; // scroller needed at all?
PosList: TControlPosList; // list of initial positions
procedure BeforeShow; virtual; // override in descendants for specific behaviour
procedure BeforeClose; virtual; // override in descendants for specific behaviour
end;
implementation
{$R *.dfm}
procedure TScrollingBaseFrame.BeforeClose;
// Clean up
begin
PosList.Free;
end;
procedure TScrollingBaseFrame.BeforeShow;
//Setup scroller and save initial control positions
var
i,XMax,YMax,Idx: Integer;
AControl: TControl;
begin
pnlTitle.Height := TitleHeight;
PosList := TControlPosList.Create;
XMax := 0;
YMax := 0;
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TControl then
begin
AControl := TControl(Components[i]);
if Acontrol.Tag <> IgnoreTag then
begin
Idx := PosList.Add(TcontrolPos.Create);
if AControl.Name = '' then // deal with empty names
AControl.Name := StrControl + IntToStr(i);
PosList[Idx].Name := AControl.Name;
PosList[Idx].X := AControl.Left;
PosList[Idx].Y := AControl.Top;
if YMax < AControl.Top + AControl.Height then
YMax := AControl.Top + AControl.Height;
if XMax < AControl.Left + AControl.Width then
XMax := AControl.Left + AControl.Width;
end; // Ignored
end; // is control
end; // count
VertScrollBar.Max := (YMax + BottomMargin) - Height; // bit of a bottom margin :)
VertScrollBar.LargeChange := VertScrollBar.Max div MaxLargeSteps;
ShowVScroller := VertScrollBar.Max > BottomMargin;
VertScrollBar.Visible := ShowVScroller;
HorzScrollBar.Max := (XMax + RightMargin) - Width; // bit of a Right margin :)
HorzScrollBar.LargeChange := HorzScrollBar.Max div MaxLargeSteps;
ShowHScroller := HorzScrollBar.Max > RightMargin;
HorzScrollBar.Visible := ShowHScroller;
end;
procedure TScrollingBaseFrame.FrameResize(Sender: TObject);
begin
BackGroundImage.Width := Width;
BackGroundImage.Height := Height;
end;
procedure TScrollingBaseFrame.HorzScrollBarChange(Sender: TObject);
// Move the controls left and right relative to thier initail positions
var
i,j: Integer;
AControl: TControl;
begin
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TControl then
begin
AControl := TControl(Components[i]);
j := PosList.IndexOfPos(AControl.Name);
if j >= 0 then // could be ignored
Acontrol.Left := PosList[j].X - HorzScrollBar.Position;
end;
end;
end;
procedure TScrollingBaseFrame.BackGroundImageMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
// Show/Hide the scrollbars using mouse position
var
ScrollBarWidth: Integer;
begin
ScrollBarWidth := GetSystemmetrics(SM_CXVSCROLL); // assume the same for horizontal
VertScrollBar.Visible := (X > Width - ScrollBarWidth) and ShowVScroller;
HorzScrollBar.Visible := (Y > (Height - TitleHeight) - ScrollBarWidth) and ShowHScroller;
end;
procedure TScrollingBaseFrame.VertScrollBarChange(Sender: TObject);
// Move the controls Up and down relative to thier initail positions
var
i,j: Integer;
AControl: TControl;
begin
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TControl then
begin
AControl := TControl(Components[i]);
j := PosList.IndexOfPos(AControl.Name);
if j >= 0 then // could be ignored
Acontrol.Top := PosList[j].Y - VertScrollBar.Position;
end;
end;
end;
{ TcontrolPosList }
function TControlPosList.Add(APos: TControlPos): Integer;
begin
Result := TheList.Add(APos);
end;
constructor TControlPosList.Create;
begin
TheList := TObjectList.Create;
TheList.OwnsObjects := True;
end;
destructor TControlPosList.Destroy;
begin
TheList.Free;
inherited;
end;
function TControlPosList.GetCount: Integer;
begin
Result := TheList.Count;
end;
function TControlPosList.GetItems(Index: Integer): TControlPos;
begin
Result := TControlPos(TheList[Index]);
end;
function TControlPosList.IndexOfPos(AName: string): Integer;
// Find a control position in the list by name
var
Idx: Integer;
begin
Result := -1;
Idx := 0;
while (Result < 0) and (Idx < TheList.Count) do
begin
if Items[idx].Name = AName then
Result := idx;
inc(idx);
end;
end;
procedure TControlPosList.SetItems(Index: Integer; const Value: TControlPos);
begin
TheList[Index] := Value;
end;
end.
Reverse the order on the Base frame :)
Put the ScrollBox on, then put the image on the Scrollbox (align Client) and make it transparent. Then Place controls all over it and it allows scrolling...
I'm sure you will have tried this, so what gives you a problem...