Some time ago I have decided to create my own ListControl. What is mean under ListControl - is a control that similar to standard TListBox in Delphi.
I know, it is 'reinventing a wheel', but I want to finish my control.
So, I implemented not so much features in that control like TListBox has, but my control allows:
Add items;
Select item;
Navigate through items via keyboard (arrow keys Up an Down).
I plan to implement my ScrollBar, but this is another topic.
But I have a problem: when summary height of items is more than control's height and last item selected and I try to increase control's height I got a 'blank space', but I want to 'scroll' items down to fill blank space.
At the picture above you can see that control has lack of items to draw them onto 'blank space'.
May be I explain my problem not so clear, but do next:
Put standard TListBox on form and set its height equal 100 px;
Put standard TrackBar on form, set Max value to 100 and in event OnChange write this:
ListBox1.Height := ListBox1.Height + TrackBar1.Position;
Add 12 items at this Listbox;
Compile project and select last item in Listbox, then begin to change its height via TrackBar. You will see, that 'invisible top items' are come from top to down one by one.
That effect I want to add in my control, but I have no idea why.
Control's code
unit aListBox;
interface
uses
Windows,
Messages,
SysUtils,
Classes,
Graphics,
Controls,
Forms,
StdCtrls,
ExtCtrls,
StrUtils,
Dialogs,
Math;
type
{ main class }
TaListBox = class;
{>>>>>>>>>>>>>>>>>>>>>>>>>}
TaListBox = class(TCustomControl)
private
{ Private declarations }
protected
{ Protected declarations }
FItemBmp: TBitmap;
FEnabled: Boolean;
FSelected: Boolean;
FItems: TStringList;
FItemHeight: Integer;
FCurrentItemIndex: Integer;
FMode: Integer;
FGlobalY: Integer;
FScrollOffset: Integer;
FDownScroll: Integer;
procedure SetItems(value: TStringList);
procedure WMSIZE(var Message: TWMSize); message WM_SIZE;
procedure WMGETDLGCODE(var Message: TWMGETDLGCODE); message WM_GETDLGCODE;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
function GetItemIndex: Integer;
function GetVisibleItemsCount: Integer;
function GetScrollItemIndex: Integer;
procedure PaintItemStandard(BmpInOut: TBitmap; AMode, AIndex: Integer);
procedure PaintControlStandard(ACanvas: TCanvas; AMode: Integer);
procedure Paint; override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Click; override;
property ItemIndex : Integer read FCurrentItemIndex;
published
{ Published declarations }
property Items : TStringList read FItems write FItems;
property OnClick;
end;
{<<<<<<<<<<<<<<<<<<<<<<<<<}
implementation
{ TaListBox }
procedure Register;
begin
RegisterComponents('MyControl', [TaListBox]);
end;
constructor TaListBox.Create(AOwner: TComponent);
begin
Inherited Create(AOwner);
{ standard declarations }
ControlStyle := ControlStyle + [csOpaque, csCaptureMouse, csDoubleClicks];
Width := 100;
Height := 120;
DoubleBuffered := true;
{ control's declarations }
FItemBmp := TBitmap.Create;
FEnabled := true;
FSelected := false;
FItems := TStringList.Create;
FItemHeight := 20;
FCurrentItemIndex := -1;
FScrollOffset := 0;
FDownScroll := 0;
FMode := 1;
end;
destructor TaListBox.Destroy;
begin
FreeAndNil(FItemBmp);
FreeAndNil(FItems);
Inherited Destroy;
end;
procedure TaListBox.Click;
begin
if FEnabled then
Inherited Click
else
Exit;
end;
procedure TaListBox.SetItems(value: TStringList);
begin
Invalidate;
end;
procedure TaListBox.WMSize(var Message: TWMSize);
var
LScrollIndex, LVisibleCount: Integer;
begin
inherited;
LScrollIndex := FScrollOffset div FItemHeight;
LVisibleCount := GetVisibleItemsCount;
if (FItems.Count - LScrollIndex) < LVisibleCount then
FScrollOffset := FItemHeight * max(0, FItems.Count - GetVisibleItemsCount);
end;
procedure TaListBox.WMGETDLGCODE(var Message: TWMGETDLGCODE);
begin
Inherited;
Message.Result := DLGC_WANTARROWS;
end;
procedure TaListBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
Windows.SetFocus(Handle);
if PtInRect(Rect(1, 1, Width - 1, Height - 1), Point(X, Y)) then
FGlobalY := Y - 2;
if GetItemIndex > FItems.Count - 1 then
Exit
else
begin
FSelected := true;
FCurrentItemIndex := GetItemIndex;
// prevent selecting next item if height too low
if Height >= FItemHeight then
if PtInRect(Rect(1, Height - FDownScroll - 1, Width - 1, Height - 1), Point(X, Y)) then
FScrollOffset := FScrollOffset + FItemHeight;
Invalidate;
end;
end;
Inherited MouseDown(Button, Shift, X, Y);
end;
procedure TaListBox.KeyDown(var Key: Word; Shift: TShiftState);
var
ScrollIndex: Integer;
begin
Inherited KeyDown(Key, Shift);
if FEnabled then
begin
case Key of
VK_UP:
begin
if FCurrentItemIndex = 0 then
Exit
else
begin
if (FCurrentItemIndex + 1) > 0 then
begin
Dec(FCurrentItemIndex);
ScrollIndex := FScrollOffset div FItemHeight;
if FCurrentItemIndex < ScrollIndex then
FScrollOffset := FScrollOffset - FItemHeight;
end;
end;
end;
VK_DOWN:
begin
if FCurrentItemIndex = FItems.Count - 1 then
Exit
else
begin
if (FCurrentItemIndex + 1) < FItems.Count then
begin
Inc(FCurrentItemIndex);
ScrollIndex := FScrollOffset div FItemHeight;
if (FCurrentItemIndex - GetVisibleItemsCount + 1) > ScrollIndex then
FScrollOffset := FScrollOffset + FItemHeight;
end;
end;
end;
end;
Invalidate;
end
else
Exit;
end;
function TaListBox.GetItemIndex: Integer;
begin
Result := (FGlobalY + FScrollOffset) div FItemHeight;
end;
function TaListBox.GetVisibleItemsCount: Integer;
begin
Result := Height div FItemHeight;
end;
function TaListBox.GetScrollItemIndex: Integer;
begin
Result := FScrollOffset div FItemHeight;
end;
procedure TaListBox.PaintItemStandard(BmpInOut: TBitmap; AMode, AIndex: Integer);
var
Text: String;
R: TRect;
begin
BmpInOut.Width := Width - 2;
BmpInOut.Height := FItemHeight;
case AMode of
1:
begin
if FSelected then
begin
BmpInOut.Canvas.Brush.Color := clWebCrimson;
BmpInOut.Canvas.Font.Color := clWhite;
end
else
begin
BmpInOut.Canvas.Brush.Color := clWhite;
BmpInOut.Canvas.Font.Color := clBlack;
end;
BmpInOut.Canvas.Pen.Color := clGray;
end;
4:
begin
BmpInOut.Canvas.Brush.Color := clSilver;
BmpInOut.Canvas.Pen.Color := clGray;
BmpInOut.Canvas.Font.Color := clBlack;
end;
end;
BmpInOut.Canvas.FillRect(BmpInOut.Canvas.ClipRect);
// paint item's text
if AIndex = - 1 then
Exit
else
BmpInOut.Canvas.TextOut(18, 2, FItems.Strings[AIndex]);
end;
procedure TaListBox.PaintControlStandard(ACanvas: TCanvas; AMode: Integer);
var
i: Integer;
OldSelected: Boolean;
TempBmp: TBitmap;
begin
case AMode of
1:
begin
ACanvas.Brush.Color := clWhite;
ACanvas.Pen.Color := clBlack;
end;
4:
begin
ACanvas.Brush.Color := clSilver;
ACanvas.Pen.Color := clBlack;
end;
end;
ACanvas.Rectangle(Rect(0, 0, Width, Height));
// calculate DownButton size
FDownScroll := Height - GetVisibleItemsCount * FItemHeight - 1 {top border pixel} - 1 {bottom border pixel};
// create output bitmap
TempBmp := TBitmap.Create;
TempBmp.Width := Width - 2;
TempBmp.Height := Height - 2;
// turn off selected flag
OldSelected := FSelected;
FSelected := false;
for i:=0 to FItems.Count - 1 do
begin
PaintItemStandard(FItemBmp, FMode, i);
TempBmp.Canvas.Draw(0, 0 + (FItemHeight * i) - FScrollOffset, FItemBmp);
end;
// output result
ACanvas.Draw(1, 1, TempBmp);
// restore selected flag
FSelected := OldSelected;
if FSelected then
begin
// paint selected item
PaintItemStandard(FItemBmp, FMode, FCurrentItemIndex);
ACanvas.Draw(1, 1 + (FItemHeight * FCurrentItemIndex) - FScrollOffset, FItemBmp);
end;
// free resources
FreeAndNil(TempBmp);
end;
procedure TaListBox.Paint;
begin
if FEnabled then
PaintControlStandard(Canvas, 1)
else
PaintControlStandard(Canvas, 4);
end;
end.
I hope I can find some help here.
Thank you for your attention!
P.S.
In the source code was added implementation of scrolling items by changing control's size, written by Tom Brunberg.
P.S.S.
Thanks to user fantaghirocco for formatting my question ;)
Following your directions to create a standard TListBox I noted, as you said, that the number of visible items increased when increasing the list box (regardless of any item being selected).
But, decreasing the size did not scroll items up again, regardless of any item being selected. I understand that you ask about the same functionality, since you refer to the standard TListBox.
Add to the uses clause and to the TaListBox class declaration:
uses ... Math;
...
TaListBox = class(TCustomControl)
private
procedure WMSize(var Message: TWMSize); message WM_SIZE;
and to the implementation
procedure TaListBox.WMSize(var Message: TWMSize);
var
LScrollIndex, LVisibleCount: Integer;
begin
inherited;
LScrollIndex := FScrollOffset div FItemHeight;
LVisibleCount := GetVisibleItemsCount;
if (FItems.Count - LScrollIndex) < LVisibleCount then
FScrollOffset := FItemHeight * max(0, FItems.Count - GetVisibleItemsCount);
end;
A side note: you use the following kind of expressions in many places, e.g.
Round(FScrollOffset div FItemHeight);
The div operator means integer division. It always returns an integer, thus the call to Round is meaningless. Read about div and also mod in the documentation.
The idea is simple:
Always know how many items can be displayed when your control is a certain height. That means if your clientheight is 100px and an item's height is 10px then you obviously will be able to display 10 items completely whitout anyone being clipped. Save that amount in a variable. Keep as a float because sometimes an item will be clipped. (Visible Count)
Keep a variable of which direction you scrolled last. This is important as that will help you decide whether to bring items into view from the bottom or from the top or whether to hide items at the top or bottom when the control's height decreases/increases.
Keep an index of the item that is at the top or bottom the last time you scrolled. Whether to keep the top one or the bottom one will depend on which direction you last scrolled (point 2). It will obviously change as you add items, etc.
So let's say the situation is that you have more items than can be displayed and the last time you scrolled was up, so you will keep the item index of the top most visible item. If that index is 0 (zero) then obviously you just need to bring items into view from the bottom. But if that index is for example; 5, then you will keep bringing items into view also from the bottom but only until the Visible Count grows as large or larger than the Item Count in which case you wil start to bring as many items into view from the top as is needed to fill the client area.
You just have to adapt according to last scroll direction and whether the height increases or decreases
Related
I'd like to add DBLookupComboboxes to certain columns in a DBGrid. There is a nice article on About.com on how to do this here. The problem is that with a table having many columns, if you select from the DBLookupCombobox in one column and then try to scroll left, the combobox will move left too as shown in the included images. How can the About.com code can be changed to prevent this behavior? A web search showed two others complaining of the exact same problem with no solution. Note that I want to use a DBLookupCombobox to show a name but enter the id, so using a simple picklist will not do.
procedure TForm1.DBGrid1ColExit(Sender: TObject);
begin
if DBGrid1.SelectedField.FieldName = DBLookupComboBox1.DataField then
DBLookupComboBox1.Visible := False
end;
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
if (gdFocused in State) then
begin
if (Column.Field.FieldName = DBLookupComboBox1.DataField) then
with DBLookupComboBox1 do
begin
Left := Rect.Left + DBGrid1.Left + 2;
Top := Rect.Top + DBGrid1.Top + 2;
Width := Rect.Right - Rect.Left;
Width := Rect.Right - Rect.Left;
Height := Rect.Bottom - Rect.Top;
Visible := True;
end;
end
end;
procedure TForm1.DBGrid1KeyPress(Sender: TObject; var Key: Char);
begin
if (key = Chr(9)) then Exit;
if (DBGrid1.SelectedField.FieldName = DBLookupComboBox1.DataField) then
begin
DBLookupComboBox1.SetFocus;
SendMessage(DBLookupComboBox1.Handle, WM_Char, word(Key), 0);
end
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
with DBLookupComboBox1 do
begin
DataSource := DataSource1; // -> AdoTable1 -> DBGrid1
ListSource := DataSource2;
DataField := 'resource_id'; // from AdoTable1 - displayed in the DBGrid
KeyField := 'id';
ListField := 'resource_name; id';
Visible := False;
end;
DataSource2.DataSet := AdoQuery1;
AdoQuery1.Connection := AdoConnection1;
AdoQuery1.SQL.Text := 'SELECT id,resource_name FROM resources';
AdoQuery1.Open;
end;
Here is one solution using a neat hack from François.
type
// Hack to redeclare your TDBGrid here without the the form designer going mad
TDBGrid = class(DBGrids.TDBGrid)
procedure WMHScroll(var Msg: TWMHScroll); message WM_HSCROLL;
end;
TForm1 = class(TForm)
[...]
procedure TDBGrid.WMHScroll(var Msg: TWMHScroll);
begin
if Form1.DBGrid1.SelectedField.FieldName = Form1.DBLookupComboBox1.DataField then begin
case Msg.ScrollCode of
SB_LEFT,SB_LINELEFT,SB_PAGELEFT: begin
Form1.DBGrid1.SelectedIndex := Form1.DBGrid1.SelectedIndex-1;
Form1.DBLookupComboBox1.Visible := False;
end;
SB_RIGHT,SB_LINERIGHT,SB_PAGERIGHT: begin
Form1.DBGrid1.SelectedIndex := Form1.DBGrid1.SelectedIndex+1;
Form1.DBLookupComboBox1.Visible := False;
end;
end;
end;
inherited; // to keep the expected behavior
end;
I feel my failure so far lies in search terms as information on this has to be pretty common. Basically I am looking for common solutions and best practices when performing resizes on several components while a form is resized.
I have a form with a component that is based upon TScrollBox. The ScrollBox contains rows which are added dynamically at run time. They are basically a subcomponent. Each one has an image on the left and a memo on the right. The height is set based upon the width and aspect ratio of the image. Upon the resize of the scroll box a loop sets the width of the rows triggering the rows own internal resize. The loop also sets the relative top position if the heights have changed.
Screen shot:
Around 16 rows performs fine. My goal is closer to 32 rows which is very choppy and can peg a core at 100% usage.
I have tried:
Added a check to prevent a new resize starting while the previous has yet to complete. It answered if it occured and it does sometimes.
I tried preventing it resizing more often than every 30 ms which would allow for 30 frame per second drawing. Mixed results.
Changed the rows base component from TPanel to TWinControl. Not sure if there is a performance penalty using the Panel but its an old habit.
With and without double buffering.
I would like to allow row resizing to occur during a resize as a preview to how large the image will be in the row. That eliminates one obvious solution that in some applications is an acceptable loss.
Right now the resize code internally for the row is completely dynamic and based upon the dimensions of each image. Next thing I plan to try is to basically specify the Aspect Ratio, Max Width/Height based on the largest image in the collection. This should reduce the amount of math per row. But it seems like the issues are more the resize event and the loop itself?
Full unit code for the components:
unit rPBSSVIEW;
interface
uses
Classes, Controls, Forms, ExtCtrls, StdCtrls, Graphics, SysUtils, rPBSSROW, Windows, Messages;
type
TPBSSView = class(TScrollBox)
private
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure ResizeRows(Sender: TObject);
procedure AddRow(FileName: String);
procedure FillRow(Row: Integer; ImageStream: TMemoryStream);
end;
var
PBSSrow: Array of TPBSSRow;
Resizingn: Boolean;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TScrollBox]);
end;
procedure TPBSSView.AddRow(FileName: String);
begin
SetLength(PBSSrow,(Length(PBSSrow) + 1));
PBSSrow[Length(PBSSrow)-1] := TPBSSRow.create(self);
With PBSSrow[Length(PBSSrow)-1] do
begin
Left := 2;
if (Length(PBSSrow)-1) = 0 then Top := 2 else Top := ((PBSSRow[Length(PBSSRow) - 2].Top + PBSSRow[Length(PBSSRow) - 2].Height) + 2);
Width := (inherited ClientWidth - 4);
Visible := True;
Parent := Self;
PanelLeft.Caption := FileName;
end;
end;
procedure TPBSSView.FillRow(Row: Integer; ImageStream: TMemoryStream);
begin
PBSSRow[Row].LoadImageFromStream(ImageStream);
end;
procedure TPBSSView.ResizeRows(Sender: TObject);
var
I, X: Integer;
begin
if Resizingn then exit
else
begin
Resizingn := True;
HorzScrollBar.Visible := False;
X := (inherited ClientWidth - 4);
if Length(PBSSrow) > 0 then
for I := 0 to Length(PBSSrow) - 1 do
Begin
PBSSRow[I].Width := X; //Set Width
if not (I = 0) then //Move all next ones down.
begin
PBSSRow[I].Top := (PBSSRow[(I - 1)].Top + PBSSRow[(I - 1)].Height) + 2;
end;
Application.ProcessMessages;
End;
HorzScrollBar.Visible := True;
Resizingn := False;
end;
end;
constructor TPBSSView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
OnResize := ResizeRows;
DoubleBuffered := True;
VertScrollBar.Tracking := True;
Resizingn := False;
end;
destructor TPBSSView.Destroy;
begin
inherited;
end;
end.
Row Code:
unit rPBSSROW;
interface
uses
Classes, Controls, Forms, ExtCtrls, StdCtrls, Graphics, pngimage, SysUtils;
type
TPBSSRow = class(TWinControl)
private
FImage: TImage;
FPanel: TPanel;
FMemo: TMemo;
FPanelLeft: TPanel;
FPanelRight: TPanel;
FImageWidth: Integer;
FImageHeight: Integer;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure MyPanelResize(Sender: TObject);
procedure LeftPanelResize(Sender: TObject);
published
procedure LoadImageFromStream(ImageStream: TMemoryStream);
property Image: TImage read FImage;
property Panel: TPanel read FPanel;
property PanelLeft: TPanel read FPanelLeft;
property PanelRight: TPanel read FPanelRight;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TWinControl]);
end;
procedure TPBSSRow.MyPanelResize(Sender: TObject);
begin
if (Width - 466) <= FImageWidth then FPanelLeft.Width := (Width - 466)
else FPanelLeft.Width := FImageWidth;
FPanelRight.Width := (Width - FPanelLeft.Width);
end;
procedure TPBSSRow.LeftPanelResize(Sender: TObject);
var
AspectRatio: Extended;
begin
FPanelRight.Left := (FPanelLeft.Width);
//Enforce Info Minimum Height or set Height
if FImageHeight > 0 then AspectRatio := (FImageHeight/FImageWidth) else
AspectRatio := 0.4;
if (Round(AspectRatio * FPanelLeft.Width)) >= 212 then
begin
Height := (Round(AspectRatio * FPanelLeft.Width));
FPanelLeft.Height := Height;
FPanelRight.Height := Height;
end
else
begin
Height :=212;
FPanelLeft.Height := Height;
FPanelRight.Height := Height;
end;
if Fimage.Height >= FImageHeight then FImage.Stretch := False else Fimage.Stretch := True;
if Fimage.Width >= FImageWidth then FImage.Stretch := False else Fimage.Stretch := True;
end;
procedure TPBSSRow.LoadImageFromStream(ImageStream: TMemoryStream);
var
P: TPNGImage;
n: Integer;
begin
P := TPNGImage.Create;
ImageStream.Position := 0;
P.LoadFromStream(ImageStream);
FImage.Picture.Assign(P);
FImageWidth := P.Width;
FImageHeight := P.Height;
end;
constructor TPBSSRow.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
Color := clWhite;
OnResize := MyPanelResize;
DoubleBuffered := True;
//Left Panel for Image
FPanelLeft := TPanel.Create(Self);
with FPanelLeft do
begin
SetSubComponent(true);
Align := alLeft;
Parent := Self;
//SetBounds(0,0,100,100);
ParentBackground := False;
Color := clBlack;
Font.Color := clLtGray;
Constraints.MinWidth := 300;
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
BorderStyle := bsNone;
OnResize := LeftPanelResize;
end;
//Image for left panel
FImage := TImage.Create(Self);
FImage.SetSubComponent(true);
FImage.Align := alClient;
FImage.Parent := FPanelLeft;
FImage.Center := True;
FImage.Stretch := True;
FImage.Proportional := True;
//Right Panel for Info
FPanelRight := TPanel.Create(Self);
with FPanelRight do
begin
SetSubComponent(true);
Parent := Self;
Padding.SetBounds(2,5,5,2);
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
BorderStyle := bsNone;
Color := clLtGray;
end;
//Create Memo in Right Panels
FMemo := TMemo.create(self);
with FMemo do
begin
SetSubComponent(true);
Parent := FPanelRight;
Align := alClient;
BevelOuter := bvNone;
BevelInner := bvNone;
BorderStyle := bsNone;
Color := clLtGray;
end;
end;
destructor TPBSSRow.Destroy;
begin
inherited;
end;
end.
A few tips:
TWinControl already ís a container, you do not need another panel inside it to add controls
You do not need an TImage component to view a graphic, that can also with TPaintBox, or as in my example control below, a TCustomControl,
Since all of your other panels are not recognizable (borders and bevels are disabled), loose them altogether and place the TMemo directly on your row control,
SetSubComponent is only for design time usage. You do not need it. Nor the Register procedures for that matter.
Put the global rows array inside your class definition, otherwise multiple TPBSSView controls will use the same array!
TWinControl already tracks all its child controls, so you won't need the array anyway, see my example below,
Make use of the Align property to save you from realigning manually,
If the memo control is just for showing text, then remove it and paint the text yourself.
Try this one for starters:
unit PBSSView;
interface
uses
Windows, Messages, Classes, Controls, SysUtils, Graphics, ExtCtrls, StdCtrls,
Forms, PngImage;
type
TPBSSRow = class(TCustomControl)
private
FGraphic: TPngImage;
FStrings: TStringList;
function ImageHeight: Integer; overload;
function ImageHeight(ControlWidth: Integer): Integer; overload;
function ImageWidth: Integer; overload;
function ImageWidth(ControlWidth: Integer): Integer; overload;
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
procedure WMWindowPosChanging(var Message: TWMWindowPosChanging);
message WM_WINDOWPOSCHANGING;
protected
procedure Paint; override;
procedure RequestAlign; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure LoadImageFromStream(Stream: TMemoryStream);
property Strings: TStringList read FStrings;
end;
TPBSSView = class(TScrollBox)
private
function GetRow(Index: Integer): TPBSSRow;
procedure WMEnterSizeMove(var Message: TMessage); message WM_ENTERSIZEMOVE;
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
procedure WMExitSizeMove(var Message: TMessage); message WM_EXITSIZEMOVE;
protected
procedure PaintWindow(DC: HDC); override;
public
constructor Create(AOwner: TComponent); override;
procedure AddRow(const FileName: TFileName);
procedure FillRow(Index: Integer; ImageStream: TMemoryStream);
property Rows[Index: Integer]: TPBSSRow read GetRow;
end;
implementation
{ TPBSSRow }
constructor TPBSSRow.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 300;
Height := 50;
FStrings := TStringList.Create;
end;
destructor TPBSSRow.Destroy;
begin
FStrings.Free;
FGraphic.Free;
inherited Destroy;
end;
function TPBSSRow.ImageHeight: Integer;
begin
Result := ImageHeight(Width);
end;
function TPBSSRow.ImageHeight(ControlWidth: Integer): Integer;
begin
if (FGraphic <> nil) and not FGraphic.Empty then
Result := Round(ImageWidth(ControlWidth) * FGraphic.Height / FGraphic.Width)
else
Result := Height;
end;
function TPBSSRow.ImageWidth: Integer;
begin
Result := ImageWidth(Width);
end;
function TPBSSRow.ImageWidth(ControlWidth: Integer): Integer;
begin
Result := ControlWidth div 2;
end;
procedure TPBSSRow.LoadImageFromStream(Stream: TMemoryStream);
begin
FGraphic.Free;
FGraphic := TPngImage.Create;
Stream.Position := 0;
FGraphic.LoadFromStream(Stream);
Height := ImageHeight + Padding.Bottom;
end;
procedure TPBSSRow.Paint;
var
R: TRect;
begin
Canvas.StretchDraw(Rect(0, 0, ImageWidth, ImageHeight), FGraphic);
SetRect(R, ImageWidth, 0, Width, ImageHeight);
Canvas.FillRect(R);
Inc(R.Left, 10);
DrawText(Canvas.Handle, FStrings.Text, -1, R, DT_EDITCONTROL or
DT_END_ELLIPSIS or DT_NOFULLWIDTHCHARBREAK or DT_NOPREFIX or DT_WORDBREAK);
Canvas.FillRect(Rect(0, ImageHeight, Width, Height));
end;
procedure TPBSSRow.RequestAlign;
begin
{eat inherited}
end;
procedure TPBSSRow.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TPBSSRow.WMWindowPosChanging(var Message: TWMWindowPosChanging);
begin
inherited;
if (FGraphic <> nil) and not FGraphic.Empty then
Message.WindowPos.cy := ImageHeight(Message.WindowPos.cx) + Padding.Bottom;
end;
{ TPBSSView }
procedure TPBSSView.AddRow(const FileName: TFileName);
var
Row: TPBSSRow;
begin
Row := TPBSSRow.Create(Self);
Row.Align := alTop;
Row.Padding.Bottom := 2;
Row.Parent := Self;
end;
constructor TPBSSView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
VertScrollBar.Tracking := True;
end;
procedure TPBSSView.FillRow(Index: Integer; ImageStream: TMemoryStream);
begin
Rows[Index].LoadImageFromStream(ImageStream);
end;
function TPBSSView.GetRow(Index: Integer): TPBSSRow;
begin
Result := TPBSSRow(Controls[Index]);
end;
procedure TPBSSView.PaintWindow(DC: HDC);
begin
{eat inherited}
end;
procedure TPBSSView.WMEnterSizeMove(var Message: TMessage);
begin
if not AlignDisabled then
DisableAlign;
inherited;
end;
procedure TPBSSView.WMEraseBkgnd(var Message: TWmEraseBkgnd);
var
DC: HDC;
begin
DC := GetDC(Handle);
try
FillRect(DC, Rect(0, VertScrollBar.Range, Width, Height), Brush.Handle);
finally
ReleaseDC(Handle, DC);
end;
Message.Result := 1;
end;
procedure TPBSSView.WMExitSizeMove(var Message: TMessage);
begin
inherited;
if AlignDisabled then
EnableAlign;
end;
end.
If this still performs badly, then there are multiple other enhancements possible.
Update:
Flickering eliminated by overriding/intercepting WM_ERASEBKGND (and intercepting PaintWindow for versions < XE2),
Better performance by making use of DisableAlign and EnableAlign.
I don't know if this will make a significant difference, but instead setting PBSSRow[I].Width and PBSSRow[I].Top separately, make one call to PBSSRow[I].SetBounds instead. This will save you one Resize event for that SubComponent.
I have used the code provided in this example How to implement a close button for a TTabsheet of a TPageControl to draw a close button to each tabsheet of a pagecontrol and I have replaced ThemeServices with Style Services inside the code and when applying styles the close button doesn`t show and react in no way. Could anyone point me to a different path o solving this issue. thank you! this is the code of the OnDrawTab event:
procedure TFormMain.PageControlCloseButtonDrawTab(Control: TCustomTabControl;
TabIndex: Integer; const Rect: TRect; Active: Boolean);
var
CloseBtnSize: Integer;
PageControl: TPageControl;
TabCaption: TPoint;
CloseBtnRect: TRect;
CloseBtnDrawState: Cardinal;
CloseBtnDrawDetails: TThemedElementDetails;
begin
PageControl := Control as TPageControl;
if InRange(TabIndex, 0, Length(FCloseButtonsRect) - 1) then
begin
CloseBtnSize := 14;
TabCaption.Y := Rect.Top + 3;
if Active then
begin
CloseBtnRect.Top := Rect.Top + 4;
CloseBtnRect.Right := Rect.Right - 5;
TabCaption.X := Rect.Left + 6;
end
else
begin
CloseBtnRect.Top := Rect.Top + 3;
CloseBtnRect.Right := Rect.Right - 5;
TabCaption.X := Rect.Left + 3;
end;
CloseBtnRect.Bottom := CloseBtnRect.Top + CloseBtnSize;
CloseBtnRect.Left := CloseBtnRect.Right - CloseBtnSize;
FCloseButtonsRect[TabIndex] := CloseBtnRect;
PageControl.Canvas.FillRect(Rect);
PageControl.Canvas.TextOut(TabCaption.X, TabCaption.Y, PageControl.Pages[TabIndex].Caption);
if not UseThemes then
begin
if (FCloseButtonMouseDownIndex = TabIndex) and FCloseButtonShowPushed then
CloseBtnDrawState := DFCS_CAPTIONCLOSE + DFCS_PUSHED
else
CloseBtnDrawState := DFCS_CAPTIONCLOSE;
Winapi.Windows.DrawFrameControl(PageControl.Canvas.Handle,
FCloseButtonsRect[TabIndex], DFC_CAPTION, CloseBtnDrawState);
end
else
begin
Dec(FCloseButtonsRect[TabIndex].Left);
if (FCloseButtonMouseDownIndex = TabIndex) and FCloseButtonShowPushed then
CloseBtnDrawDetails := StyleServices.GetElementDetails(twCloseButtonPushed)
else
CloseBtnDrawDetails := StyleServices.GetElementDetails(twCloseButtonNormal);
StyleServices.DrawElement(PageControl.Canvas.Handle, CloseBtnDrawDetails,
FCloseButtonsRect[TabIndex]);
end;
end;
end;
If you are using the vcl styles, you must write a vcl style hook to draw a close button in the tab controls, take a look to the Vcl.Styles.ColorTabs unit (introduced in these articles Creating colorful tabsheets with the VCL Styles, Added border to TTabColorControlStyleHook) to have an idea of what you need to write a style hook like this. Additional to the code to draw the button in the tabs you must handle the WM_MOUSEMOVE and WM_LBUTTONUP messages (in the style hook) to change the state of the button (normal, hot) and detect a click in the close button.
If you have problems implementing the style hook let me know to post a full solution here.
UPDATE
I just wrote this simple style hook to add suport for a close button in the tabsheets.
uses
Vcl.Styles,
Vcl.Themes;
type
TTabControlStyleHookBtnClose = class(TTabControlStyleHook)
private
FHotIndex : Integer;
FWidthModified : Boolean;
procedure WMMouseMove(var Message: TMessage); message WM_MOUSEMOVE;
procedure WMLButtonUp(var Message: TWMMouse); message WM_LBUTTONUP;
function GetButtonCloseRect(Index: Integer):TRect;
strict protected
procedure DrawTab(Canvas: TCanvas; Index: Integer); override;
procedure MouseEnter; override;
procedure MouseLeave; override;
public
constructor Create(AControl: TWinControl); override;
end;
constructor TTabControlStyleHookBtnClose.Create(AControl: TWinControl);
begin
inherited;
FHotIndex:=-1;
FWidthModified:=False;
end;
procedure TTabControlStyleHookBtnClose.DrawTab(Canvas: TCanvas; Index: Integer);
var
Details : TThemedElementDetails;
ButtonR : TRect;
FButtonState: TThemedWindow;
begin
inherited;
if (FHotIndex>=0) and (Index=FHotIndex) then
FButtonState := twSmallCloseButtonHot
else
if Index = TabIndex then
FButtonState := twSmallCloseButtonNormal
else
FButtonState := twSmallCloseButtonDisabled;
Details := StyleServices.GetElementDetails(FButtonState);
ButtonR:= GetButtonCloseRect(Index);
if ButtonR.Bottom - ButtonR.Top > 0 then
StyleServices.DrawElement(Canvas.Handle, Details, ButtonR);
end;
procedure TTabControlStyleHookBtnClose.WMLButtonUp(var Message: TWMMouse);
Var
LPoint : TPoint;
LIndex : Integer;
begin
LPoint:=Message.Pos;
for LIndex := 0 to TabCount-1 do
if PtInRect(GetButtonCloseRect(LIndex), LPoint) then
begin
if Control is TPageControl then
begin
TPageControl(Control).Pages[LIndex].Parent:=nil;
TPageControl(Control).Pages[LIndex].Free;
end;
break;
end;
end;
procedure TTabControlStyleHookBtnClose.WMMouseMove(var Message: TMessage);
Var
LPoint : TPoint;
LIndex : Integer;
LHotIndex : Integer;
begin
inherited;
LHotIndex:=-1;
LPoint:=TWMMouseMove(Message).Pos;
for LIndex := 0 to TabCount-1 do
if PtInRect(GetButtonCloseRect(LIndex), LPoint) then
begin
LHotIndex:=LIndex;
break;
end;
if (FHotIndex<>LHotIndex) then
begin
FHotIndex:=LHotIndex;
Invalidate;
end;
end;
function TTabControlStyleHookBtnClose.GetButtonCloseRect(Index: Integer): TRect;
var
FButtonState: TThemedWindow;
Details : TThemedElementDetails;
R, ButtonR : TRect;
begin
R := TabRect[Index];
if R.Left < 0 then Exit;
if TabPosition in [tpTop, tpBottom] then
begin
if Index = TabIndex then
InflateRect(R, 0, 2);
end
else
if Index = TabIndex then
Dec(R.Left, 2)
else
Dec(R.Right, 2);
Result := R;
FButtonState := twSmallCloseButtonNormal;
Details := StyleServices.GetElementDetails(FButtonState);
if not StyleServices.GetElementContentRect(0, Details, Result, ButtonR) then
ButtonR := Rect(0, 0, 0, 0);
Result.Left :=Result.Right - (ButtonR.Width) - 5;
Result.Width:=ButtonR.Width;
end;
procedure TTabControlStyleHookBtnClose.MouseEnter;
begin
inherited;
FHotIndex := -1;
end;
procedure TTabControlStyleHookBtnClose.MouseLeave;
begin
inherited;
if FHotIndex >= 0 then
begin
FHotIndex := -1;
Invalidate;
end;
end;
Register in this way
TStyleManager.Engine.RegisterStyleHook(TCustomTabControl, TTabControlStyleHookBtnClose);
TStyleManager.Engine.RegisterStyleHook(TTabControl, TTabControlStyleHookBtnClose);
And this is a demo
Ive been working on this example, and i got it working on the Metro UI on delphi XE6.
My workaround for getting the correct distance between the Tab name and the button was to modify this line
Result.Left := Result.Right - (ButtonR.Width);
//it was Result.Left := Result.Right - (ButtonR.Width) -5;
And setting a bigger TabWith on the PageController properties.
Also ,remind that the "Register" lines, goes on the Initialization class right before the end of the unit.
//...all the code of the unit
Initialization
TStyleManager.Engine.RegisterStyleHook(TCustomTabControl,
TTabControlStyleHookBtnClose);
TStyleManager.Engine.RegisterStyleHook(TTabControl,
TTabControlStyleHookBtnClose);
end.//final unit "end" =D
1st Question:
How do you call the part in stringgrid that is not visible? You need to scroll to see it.
For example:
There are 20 rows in a stringgrid but you can see only 10 at a time. You need to scroll to see other 10. How are the "hidden" ones called?
2nd Question:
I know this is probably not the right way to do it so some pointers would be appreciated.
I have a string grid with 1 fixed row. I add ColorButtons at runtime. So I populate 1 column with buttons.
I use this buttons to "insert/delete" rows. As long as all of the grid is in the "visible" part this works well.
Problem occcurs when I "insert" new rows and move the buttons to the "hidden" part. The last button is then drawn to Cell[0,0]. Other buttons in the "hidden" part are drawn correctly. Any idea why this happens? Should I find a way to manage this problem in the OnDraw method or is there a better (correct) way to do this?
Code:
procedure Tform1.addButton(Grid : TStringGrid; ACol : Integer; ARow : Integer);
var
bt : TColorButton;
Rect : TRect;
index : Integer;
begin
Rect := Grid.CellRect(ACol,ARow);
bt := TColorButton.Create(Grid);
bt.Parent := Grid;
bt.BackColor := clCream;
bt.Font.Size := 14;
bt.Width := 50;
bt.Top := Rect.Top;
bt.Left := Rect.Left;
bt.Caption := '+';
bt.Name := 'bt'+IntToStr(ARow);
index := Grid.ComponentCount-1;
bt :=(Grid.Components[index] as TColorButton);
Grid.Objects[ACol,ARow] := Grid.Components[index];
bt.OnMouseUp := Grid.OnMouseUp;
bt.OnMouseMove := Grid.OnMouseMove;
bt.Visible := true;
end;
procedure MoveRowPlus(Grid : TStringGrid; Arow : Integer; stRow : Integer);
var
r, index : Integer;
bt : TColorButton;
Rect : TRect;
begin
Grid.RowCount := Grid.RowCount+stRow;
for r := Grid.RowCount - 1 downto ARow+stRow do
begin
Grid.Rows[r] := Grid.Rows[r-StRow];
end;
index := Grid.ComponentCount-1;
for r := Grid.RowCount - 1 downto ARow+stRow do
begin
bt :=(Grid.Components[index] as TColorButton);
Rect := Grid.CellRect(10,r);
bt.Top := Rect.Top;
bt.Left := Rect.Left;
Grid.Objects[10,r] := Grid.Components[index];
dec(index);
end;
for r := ARow to (ARow +stRow-1) do
begin
Grid.Rows[r].Clear;
end;
end;
procedure MoveRowMinus(Grid : TStringGrid; Arow : Integer; stRow : Integer);
var
r, index : Integer;
bt : TColorButton;
Rect : TRect;
begin
for r := ARow to Grid.RowCount-stRow-1 do
begin
Grid.Rows[r] := Grid.Rows[r+StRow];
end;
index := ARow-1;
for r := ARow to Grid.RowCount-stRow-1 do
begin
Rect := Grid.CellRect(10,r);
bt :=(Grid.Components[index] as TColorButton);
bt.Top := Rect.Top;
bt.Left := Rect.Left;
Grid.Objects[10,r] := Grid.Components[index];
bt.Visible := true;
inc(index);
end;
for r := Grid.RowCount-stRow to Grid.RowCount-1 do
begin
Grid.Rows[r].Clear;
end;
Grid.RowCount := Grid.RowCount-stRow;
end;
For the visible part there exist the VisibleRowCount and VisibleColCount properties. The TGridAxisDrawInfo record type names the visible part Boundary and all parts together Extent (or vice versa, I never remember). So there is no specific by the VCL declared name for the unvisible part of a string grid. It just is the unvisible part.
I think you are making a logical error: the buttons are not moved when you scroll the grid. Though it may seem like they move, that is just the result of moving the device context contents due to an internal call to ScrollWindow. The scroll bars in the string grid component are custom added, and do not work like those of e.g. a TScrollBox.
To always show all buttons on the locations where they really are, repaint the string grid in the OnTopLeftChanged event:
procedure TForm1.StringGrid1TopLeftChanged(Sender: TObject);
begin
StringGrid1.Repaint;
end;
When the row heights of all rows and the height of string grid never change, then it is sufficient to create all buttons only once, and let them stay where they are. This means that every button no longer is "attached" to a row, and storing them in the Objects property has no significance any more. When a button is pressed, simply calculate the intended row index from the position of the button in combination with the TopRow property of the string grid which specifies the index of the first visible scrollable row in the grid.
If the grid can resize, e.g. by anchors, then update the button count in the parent's OnResize event. And if the row count of the string grid may become less then the maximum visible row count, then also update the (visible) button count.
If you want more of an answer, then please update your question and explain how the MoveRowPlus and the MoveRowMinus routines are called due to interaction with the grid and or buttons, because now I do not fully understand what it is that you want.
And about CellRect giving the wrong coordinates: that is because CellRect only works on full (or partial) visible cells. To quote the documentation:
If the indicated cell is not visible, CellRect returns an empty rectangle.
Addition due to OP's comments
I think the following code does what you want. The original row index of every button is stored in the Tag property.
unit Unit1;
interface
uses
Windows, Classes, Controls, Forms, StdCtrls, Grids;
type
TForm1 = class(TForm)
Grid: TStringGrid;
procedure GridTopLeftChanged(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
FPrevTopRow: Integer;
procedure CreateGridButtons(ACol: Integer);
procedure GridButtonClick(Sender: TObject);
procedure RearrangeGridButtons;
function GetInsertRowCount(ARow: Integer): Integer;
function GridButtonToRow(AButton: TButton): Integer;
procedure MoveGridButtons(ButtonIndex, ARowCount: Integer);
end;
implementation
{$R *.dfm}
type
TStringGridAccess = class(TStringGrid);
procedure TForm1.FormCreate(Sender: TObject);
begin
FPrevTopRow := Grid.TopRow;
CreateGridButtons(2);
end;
procedure TForm1.CreateGridButtons(ACol: Integer);
var
R: TRect;
I: Integer;
Button: TButton;
begin
R := Grid.CellRect(ACol, Grid.FixedRows);
Inc(R.Right, Grid.GridLineWidth);
Inc(R.Bottom, Grid.GridLineWidth);
for I := Grid.FixedRows to Grid.RowCount - 1 do
begin
Button := TButton.Create(Grid);
Button.BoundsRect := R;
Button.Caption := '+';
Button.Tag := I;
Button.ControlStyle := [csClickEvents];
Button.OnClick := GridButtonClick;
Button.Parent := Grid;
Grid.Objects[0, I] := Button;
OffsetRect(R, 0, Grid.DefaultRowHeight + Grid.GridLineWidth);
end;
end;
procedure TForm1.GridButtonClick(Sender: TObject);
var
Button: TButton absolute Sender;
N: Integer;
I: Integer;
begin
N := GetInsertRowCount(Button.Tag);
if Button.Caption = '+' then
begin
Button.Caption := '-';
Grid.RowCount := Grid.RowCount + N;
for I := 1 to N do
TStringGridAccess(Grid).MoveRow(Grid.RowCount - 1,
GridButtonToRow(Button) + 1);
MoveGridButtons(Button.Tag, N);
end
else
begin
Button.Caption := '+';
for I := 1 to N do
TStringGridAccess(Grid).MoveRow(GridButtonToRow(Button) + 1,
Grid.RowCount - 1);
Grid.RowCount := Grid.RowCount - N;
MoveGridButtons(Button.Tag, -N);
end;
end;
procedure TForm1.GridTopLeftChanged(Sender: TObject);
begin
RearrangeGridButtons;
FPrevTopRow := Grid.TopRow;
end;
procedure TForm1.RearrangeGridButtons;
var
I: Integer;
Shift: Integer;
begin
Shift := (Grid.TopRow - FPrevTopRow) *
(Grid.DefaultRowHeight + Grid.GridLineWidth);
for I := 0 to Grid.ControlCount - 1 do
begin
Grid.Controls[I].Top := Grid.Controls[I].Top - Shift;
Grid.Controls[I].Visible := Grid.Controls[I].Top > 0;
end;
end;
function TForm1.GetInsertRowCount(ARow: Integer): Integer;
begin
//This function should return the number of rows which is to be inserted
//below ARow. Note that ARow refers to the original row index, that is:
//without account for already inserted rows. For now, assume three rows:
Result := 3;
end;
function TForm1.GridButtonToRow(AButton: TButton): Integer;
begin
for Result := 0 to Grid.RowCount - 1 do
if Grid.Objects[0, Result] = AButton then
Exit;
Result := -1;
end;
procedure TForm1.MoveGridButtons(ButtonIndex, ARowCount: Integer);
var
I: Integer;
begin
for I := 0 to Grid.ControlCount - 1 do
if Grid.Controls[I].Tag > ButtonIndex then
Grid.Controls[I].Top := Grid.Controls[I].Top +
ARowCount * (Grid.DefaultRowHeight + Grid.GridLineWidth);
end;
end.
But may I say that this is also possible without the use of button controls: I suggest drawing fake button controls in the string grid's OnDrawCell event.
What is the best way to make a delphi application (delphi 2007 for win32 here) go completely full screen, removing the application border and covering windows task bar ?
I am looking for something similar to what IE does when you hit F11.
I wish this to be a run time option for the user not a design time decision by my good self.
As Mentioned in the accepted answer
BorderStyle := bsNone;
was part of the way to do it. Strangely I kept getting a E2010 Incompatible types: 'TFormBorderStyle' and 'TBackGroundSymbol' error when using that line (another type had bsNone defined).
To overcome this I had to use :
BorderStyle := Forms.bsNone;
Well, this has always worked for me. Seems a bit simpler...
procedure TForm52.Button1Click(Sender: TObject);
begin
BorderStyle := bsNone;
WindowState := wsMaximized;
end;
A Google search turned up the following, additional methods:
(though I think I'd try Roddy's method first)
Manually fill the screen (from: About Delphi)
procedure TSomeForm.FormShow(Sender: TObject) ;
var
r : TRect;
begin
Borderstyle := bsNone;
SystemParametersInfo
(SPI_GETWORKAREA, 0, #r,0) ;
SetBounds
(r.Left, r.Top, r.Right-r.Left, r.Bottom-r.Top) ;
end;
Variation on a theme by Roddy
FormStyle := fsStayOnTop;
BorderStyle := bsNone;
Left := 0;
Top := 0;
Width := Screen.Width;
Height := Screen.Height;
The WinAPI way (by Peter Below from TeamB)
private // in form declaration
Procedure WMGetMinMaxInfo(Var msg: TWMGetMinMaxInfo);
message WM_GETMINMAXINFO;
Procedure TForm1.WMGetMinMaxInfo(Var msg: TWMGetMinMaxInfo);
Begin
inherited;
With msg.MinMaxInfo^.ptMaxTrackSize Do Begin
X := GetDeviceCaps( Canvas.handle, HORZRES ) + (Width - ClientWidth);
Y := GetDeviceCaps( Canvas.handle, VERTRES ) + (Height - ClientHeight
);
End;
End;
procedure TForm1.Button2Click(Sender: TObject);
Const
Rect: TRect = (Left:0; Top:0; Right:0; Bottom:0);
FullScreen: Boolean = False;
begin
FullScreen := not FullScreen;
If FullScreen Then Begin
Rect := BoundsRect;
SetBounds(
Left - ClientOrigin.X,
Top - ClientOrigin.Y,
GetDeviceCaps( Canvas.handle, HORZRES ) + (Width - ClientWidth),
GetDeviceCaps( Canvas.handle, VERTRES ) + (Height - ClientHeight ));
// Label2.caption := IntToStr(GetDeviceCaps( Canvas.handle, VERTRES ));
End
Else
BoundsRect := Rect;
end;
Maximize the form and hide the title bar. The maximize line is done from memory, but I'm pretty sure WindowState is the property you want.
There's also this article, but that seems too complicated to me.
procedure TForm1.FormCreate(Sender: TObject) ;
begin
//maximize the window
WindowState := wsMaximized;
//hide the title bar
SetWindowLong(Handle,GWL_STYLE,GetWindowLong(Handle,GWL_STYLE) and not WS_CAPTION);
ClientHeight := Height;
end;
Edit: Here's a complete example, with "full screen" and "restore" options. I've broken out the different parts into little procedures for maximum clarity, so this could be greatly compressed into just a few lines.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
btnGoFullScreen: TButton;
btnNotFullScreen: TButton;
btnShowTitleBar: TButton;
btnHideTitleBar: TButton;
btnQuit: TButton;
procedure btnGoFullScreenClick(Sender: TObject);
procedure btnShowTitleBarClick(Sender: TObject);
procedure btnHideTitleBarClick(Sender: TObject);
procedure btnNotFullScreenClick(Sender: TObject);
procedure btnQuitClick(Sender: TObject);
private
SavedLeft : integer;
SavedTop : integer;
SavedWidth : integer;
SavedHeight : integer;
SavedWindowState : TWindowState;
procedure FullScreen;
procedure NotFullScreen;
procedure SavePosition;
procedure HideTitleBar;
procedure ShowTitleBar;
procedure RestorePosition;
procedure MaximizeWindow;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnQuitClick(Sender: TObject);
begin
Application.Terminate;
end;
procedure TForm1.btnGoFullScreenClick(Sender: TObject);
begin
FullScreen;
end;
procedure TForm1.btnNotFullScreenClick(Sender: TObject);
begin
NotFullScreen;
end;
procedure TForm1.btnShowTitleBarClick(Sender: TObject);
begin
ShowTitleBar;
end;
procedure TForm1.btnHideTitleBarClick(Sender: TObject);
begin
HideTitleBar;
end;
procedure TForm1.FullScreen;
begin
SavePosition;
HideTitleBar;
MaximizeWindow;
end;
procedure TForm1.HideTitleBar;
begin
SetWindowLong(Handle,GWL_STYLE,GetWindowLong(Handle,GWL_STYLE) and not WS_CAPTION);
ClientHeight := Height;
end;
procedure TForm1.MaximizeWindow;
begin
WindowState := wsMaximized;
end;
procedure TForm1.NotFullScreen;
begin
RestorePosition;
ShowTitleBar;
end;
procedure TForm1.RestorePosition;
begin
//this proc uses what we saved in "SavePosition"
WindowState := SavedWindowState;
Top := SavedTop;
Left := SavedLeft;
Width := SavedWidth;
Height := SavedHeight;
end;
procedure TForm1.SavePosition;
begin
SavedLeft := Left;
SavedHeight := Height;
SavedTop := Top;
SavedWidth := Width;
SavedWindowState := WindowState;
end;
procedure TForm1.ShowTitleBar;
begin
SetWindowLong(Handle,gwl_Style,GetWindowLong(Handle,gwl_Style) or ws_Caption or ws_border);
Height := Height + GetSystemMetrics(SM_CYCAPTION);
Refresh;
end;
end.
Put to the form onShow event such code:
WindowState:=wsMaximized;
And to the OnCanResize this:
if (newwidth<width) and (newheight<height) then
Resize:=false;
How to constrain a sub-form within the Mainform like it was an MDI app., but without the headaches! (Note: The replies on this page helped me get this working, so that's why I posted my solution here)
private
{ Private declarations }
StickyAt: Word;
procedure WMWINDOWPOSCHANGING(Var Msg: TWMWINDOWPOSCHANGING); Message M_WINDOWPOSCHANGING;
Procedure WMGetMinMaxInfo(Var msg: TWMGetMinMaxInfo); message WM_GETMINMAXINFO;
later...
procedure TForm2.WMWINDOWPOSCHANGING(var Msg: TWMWINDOWPOSCHANGING);
var
A, B: Integer;
iFrameSize: Integer;
iCaptionHeight: Integer;
iMenuHeight: Integer;
begin
iFrameSize := GetSystemMetrics(SM_CYFIXEDFRAME);
iCaptionHeight := GetSystemMetrics(SM_CYCAPTION);
iMenuHeight := GetSystemMetrics(SM_CYMENU);
// inside the Mainform client area
A := Application.MainForm.Left + iFrameSize;
B := Application.MainForm.Top + iFrameSize + iCaptionHeight + iMenuHeight;
with Msg.WindowPos^ do
begin
if x <= A + StickyAt then
x := A;
if x + cx >= A + Application.MainForm.ClientWidth - StickyAt then
x := (A + Application.MainForm.ClientWidth) - cx + 1;
if y <= B + StickyAt then
y := B;
if y + cy >= B + Application.MainForm.ClientHeight - StickyAt then
y := (B + Application.MainForm.ClientHeight) - cy + 1;
end;
end;
and yet more...
Procedure TForm2.WMGetMinMaxInfo(Var msg: TWMGetMinMaxInfo);
var
iFrameSize: Integer;
iCaptionHeight: Integer;
iMenuHeight: Integer;
Begin
inherited;
iFrameSize := GetSystemMetrics(SM_CYFIXEDFRAME);
iCaptionHeight := GetSystemMetrics(SM_CYCAPTION);
iMenuHeight := GetSystemMetrics(SM_CYMENU);
With msg.MinMaxInfo^.ptMaxPosition Do
begin
// position of top when maximised
X := Application.MainForm.Left + iFrameSize + 1;
Y := Application.MainForm.Top + iFrameSize + iCaptionHeight + iMenuHeight + 1;
end;
With msg.MinMaxInfo^.ptMaxSize Do
Begin
// width and height when maximized
X := Application.MainForm.ClientWidth;
Y := Application.MainForm.ClientHeight;
End;
With msg.MinMaxInfo^.ptMaxTrackSize Do
Begin
// maximum size when maximised
X := Application.MainForm.ClientWidth;
Y := Application.MainForm.ClientHeight;
End;
// to do: minimum size (maybe)
End;
In my case, the only working solution is:
procedure TFormHelper.FullScreenMode;
begin
BorderStyle := bsNone;
ShowWindowAsync(Handle, SW_MAXIMIZE);
end;
You need to make sure Form position is poDefaultPosOnly.
Form1.Position := poDefaultPosOnly;
Form1.FormStyle := fsStayOnTop;
Form1.BorderStyle := bsNone;
Form1.Left := 0;
Form1.Top := 0;
Form1.Width := Screen.Width;
Form1.Height := Screen.Height;
Tested and works on Win7 x64.
Try:
Align = alClient
FormStyle = fsStayOnTop
This always align to the primary monitor;
Hm. Looking at the responses I seem to remember dealing with this about 8 years ago when I coded a game. To make debugging easier, I used the device-context of a normal, Delphi form as the source for a fullscreen display.
The point being, that DirectX is capable of running any device context fullscreen - including the one allocated by your form.
So to give an app "true" fullscreen capabilities, track down a DirectX library for Delphi and it will probably contain what you need out of the box.