I'm referring to question
Passing the variable to another Form
Is there also a way to handover data - for example from a settings form to the application's main form without using a global variable?
Since you are talking about a "settings form", I assume that the form is shown modally. Then it is actually almost trivial.
As an example, create a new VCL application with a label and a button:
Then create a settings form used to set the font of the main label in the middle. It can look like this, with two TLabel controls, two TEdit controls, two TCheckBox controls, and two TButton controls.
Don't forget to make sure the tab order is correct, that each control has a unique access key (use the FocusControl property of the label to make the connection to the appropriate edit box), that the OK button has Default = True and ModalResult = mrOk, and that the Cancel button has Cancel = True and ModalResult = mrCancel.
(As a bonus, set NumbersOnly = True on the size edit box.)
Now, to pass information between the forms, it is as simple as this:
procedure TfrmMain.btnSettingsClick(Sender: TObject);
var
dlg: TfrmSettings;
begin
dlg := TfrmSettings.Create(Self);
try
// Populate dialog
dlg.eFont.Text := lblCaption.Font.Name;
dlg.eSize.Text := lblCaption.Font.Size.ToString;
dlg.cbBold.Checked := fsBold in lblCaption.Font.Style;
dlg.cbItalic.Checked := fsItalic in lblCaption.Font.Style;
if dlg.ShowModal = mrOk then
begin
// Apply settings from dialog
lblCaption.Font.Name := dlg.eFont.Text;
lblCaption.Font.Size := StrToInt(dlg.eSize.Text);
if dlg.cbBold.Checked then
lblCaption.Font.Style := lblCaption.Font.Style + [fsBold]
else
lblCaption.Font.Style := lblCaption.Font.Style - [fsBold];
if dlg.cbItalic.Checked then
lblCaption.Font.Style := lblCaption.Font.Style + [fsItalic]
else
lblCaption.Font.Style := lblCaption.Font.Style - [fsItalic];
end;
finally
dlg.Free;
end;
end;
The settings form has several possibilities to handover data to application mainform without using global variable. I'll assume that the setting form has bee created by the mainform like this:
SettingForm := TSettingForm.Create(Self);
SettingForm.ShowModal;
When the setting form is done (closed), ShowModal returns and mainform can access any filed (variable) or property of the setting form, before destroying it:
ShowMessage(SettingForm.SomeVariable.ToString);
SettingForm.Free;
Another way to do is to use an event.
type
TSettingFormValueAvailableEvent = procedure (Sender : TObject; Value : Integer) of object;
// Create the form and assign an event handler then show the form
SettingForm := TSettingForm.Create(Self);
SettingForm.OnValueAvailable := SettingFormValueAvailable;
SettingForm.ShowModal;
// The event handler in main form
procedure TForm1.SettingFormValueAvailable(Sender: TObject; Value : Integer);
begin
ShowMessage(Value.ToString);
end;
// The event declaration in TFormSetting
private
FOnValueAvailable : TSettingFormValueAvailableEvent ;
public
property OnValueAvailable : TSettingFormValueAvailableEvent read FOnValueAvailable write FOnValueAvailable;
// The use of the event in the form setting
procedure TFormSetting.Button1.Click(Sender : TObject);
begin
if Assigned(FOnValueAvailable) then
FOnValueAvailable(Self, 1234); // Pass value 1234
end;
Using an event is a little bit more code but it is "real time". The main form can react immediately when something happens while SettingForm is being displayed.
Related
I'm using a TGridPanel to hold some panels. At design time, I've set the grid panel to have 1 row and 5 columns.
I can add a panel to the grid using this code, which works well:
procedure TForm6.AddPanelToGrid(const ACaption: string);
var
pnl: TPanel;
begin
pnl := TPanel.Create(gpOne);
pnl.Caption := ACaption;
pnl.Parent := gpOne;
pnl.Name := 'pnlName' + ACaption;
pnl.OnClick := gpOne.OnClick;
pnl.ParentBackground := false;
pnl.ParentColor := false;
pnl.Color := clLime;
pnl.Font.Size := 14;
gpOne.ControlCollection.AddControl(pnl);
pnl.Height := pnl.Width;
end;
What I want to do is remove a TPanel from the grid when I click on it (which is why I have set the on click handler to that of the grid panel in the above code).
In that click handler I do this, which almost works:
procedure TForm6.gpOneClick(Sender: TObject);
begin
if not (sender is TPanel) then exit;
gpOne.ControlCollection.RemoveControl(Sender as TPanel);
(Sender as TPanel).Free;
gpOne.UpdateControlsColumn( 0 ); <<<-------
gpOne.UpdateControlsRow(0);
gpOne.Refresh();
end;
Using a parameter for UpdateControlColumn() causes the order of the panels in the grid to change - the first and second swap places.
I can get around this by adding the column idex to the panel's tag property, then pass that to UpdateControlColumn(). This then works, but once a panel has been removed the higher tag numbers are no longer valid - the panels have moved column.
So, how can I get the column that a panel is in from within the OnClick handler?
I'm using Delphi 10.1 Berlin - if that makes any difference.
To test this, I started a new project, added a TGridPanel, set it to have 1 row and 5 equally widthed columns. I added 6 TButton controls and created an OnClick handler for each with the following code:
AddPanelToGrid('One'); // changing the string for each button.
Click a few buttons to add some panels, then click the panels to remove them.
TCustomGridPanel has a pair of useful functions, CellIndexToCell() and CellToCellIndex, but they are not public and thus not directly accessible from a TGridPanel.
To make them available declare TGridPanel anew as below:
type
TGridPanel = class(Vcl.ExtCtrls.TGridPanel) // add this
end; // -"-
TForm27 = class(TForm)
Button1: TButton;
gpOne: TGridPanel;
...
end;
Then add rand c variables for row and col, add the call to CellIndexToCell() and use c as argument for UpdateControlsColumn:
procedure TForm27.gpOneClick(Sender: TObject);
var
r, c: integer;
begin
if not (sender is TPanel) then exit;
gpOne.CellIndexToCell(gpOne.ControlCollection.IndexOf(Sender as TPanel), c, r); // add this
gpOne.ControlCollection.RemoveControl(Sender as TPanel);
(Sender as TPanel).Free;
gpOne.UpdateControlsColumn( c ); // <<<-------
gpOne.UpdateControlsRow(0);
gpOne.Refresh();
end;
And follow advise of Remy Lebeau, regarding freeing the panel. ( I just noticed his comment).
If you haven't already, you may also want to take a look at TFlowPanel and its FlowStyle property. TflowPanel reordering after deletion is more predictable if you use more than one row, but depends of course on what you need.
This one is driving me up a wall. Most of the conversion from Delphi 6 to XE5 is proceeding smoothly, but I have various routines to dynamically build various TForm descendents (NO DFM), pop it up and generally return a value. I have a number of them that work fine in D6. Generally, I choose a place I want to pop something up (like over a panel), and what I want to popup (editbox, memo, listbox...). I create the form, set initial values and call showmodal and return some result.
The same code, compiled in XE5 has execution (glitches). One is that the created form accepts left,top and such, but does NOT display itself there. The values are correctly in the properties, but the form is in the wrong place. A second, probably related (glitch) is that when I create a TMemo or TListbox and store some text in it, "ShowModal" displays the data properly, but "Show" does not.
It has taken me several hours to digest the problem down to its simplest form, removing virtual all of my personal code. AS SHOWN HERE, IT WORKS PERFECTLY
If I comment out this line, it does not work - the form is displayed in the wrong place
XX.ClientToScreen(Point(0,0)); // EXTREMELY WEIRD PATCH
This line is a function call which OUGHT NOT affect anything else, and I don't use the returned value.
The commented out "Show" line demonstrates the other problem (data not being displayed).
I have tried Application.ProcessMessages in all sorts of places, but it never makes things better, and at times make things worse.
Color me "puzzled".
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
type TMemoForm = class(TForm)
private
public
XMemo : TMemo;
end;
Function PopUpMemoStr(txt : AnsiString; x : integer = 200; y : integer = 200; w : integer = 400 ; h : integer = 400 ) : AnsiString; // more or less a dummy for testing on XE5 2/28/14
var XX : TMemoForm;
begin
XX := TMemoForm.CreateNew(Application);
XX.ClientToScreen(Point(0,0)); // *** EXTREMELY WEIRD FIX ***
XX.Left := X; XX.Top := Y; XX.Width := w; XX.height := h;
XX.caption := 'Dummy PopUpMemo';
XX.XMemo := TMemo.create(XX);
XX.XMemo.parent := XX;
XX.XMemo.align := alClient;
XX.XMemo.text := txt;
//logit('PopUpMemoStr R='+TRectToStr(MyGetScreenRect(XX)));
XX.showmodal;
//XX.show; delay(3.00); // other "no data" problem
XX.free;
end;
//exercise code -- Panel2 is just a visible spot to see if positioning works correctly
var s : AnsiString;
var R : TRect;
begin
//R := MyGetScreenRect(Panel2);
R := Rect(414,514,678,642); // just a useful screen location for testing
s := 'One'+CRLF+'Two'+CRLF+'Three'+CRLF+'Four'; // "CRLF is #13#10
PopUpMemoStr(s,R.Left,R.Top,R.Right-R.Left,R.Bottom-R.Top);
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
To fix the form positioning problem, you need to set the form's Position to poDesigned.
For your second problem, you can't delay like that. You are not giving the Form a chance to process messages. Changing it to something like the code below displays the data correctly (although you really should not be doing this sort of thing either):
begin
XX := TMemoForm.CreateNew(nil);
try
XX.Position := poDesigned; // This line needs to be added for the positioning
XX.SetBounds(X, Y, w, h);
XX.Caption := 'Dummy PopUpMemo';
XX.XMemo := TMemo.Create(XX);
XX.XMemo.Parent := XX;
XX.XMemo.Align := alClient;
XX.XMemo.Text := txt;
//logit('PopUpMemoStr R='+TRectToStr(MyGetScreenRect(XX)));
// XX.ShowModal;
// This displays the data correctly but is not advisable
XX.Show;
for I := 1 to 6 do
begin
Sleep(500);
Application.ProcessMessages;
end;
finally
XX.Free;
end;
end;
If you want to use Show() for a Form like that, you should use the Form's OnClose event and set its Action parameter to caFree and just do the Show() in your code. Put a timer on the Form for x seconds and Close() it when the timer finishes. A bit like this:
type
TMemoForm = class(TForm)
public
XMemo : TMemo;
XTimer: TTimer;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure TimerElapsed(Sender: TObject);
end;
procedure TMemoForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
procedure TMemoForm.TimerElapsed(Sender: TObject);
begin
Close;
end;
begin
XX := TMemoForm.CreateNew(nil);
try
XX.Position := poDesigned; // This line needs to be added for the positioning
XX.SetBounds(X, Y, w, h);
XX.Caption := 'Dummy PopUpMemo';
XX.OnClose := XX.FormClose;
XX.XMemo := TMemo.Create(XX);
XX.XMemo.Parent := XX;
XX.XMemo.Align := alClient;
XX.XMemo.Text := txt;
XX.XTimer := TTimer.Create(XX);
XX.XTimer.Interval := 3000;
XX.XTimer.OnTimer := XX.TimerElapsed;
XX.Active := True;
XX.Show; // Just show the form. The rest is in the Form itself.
except
XX.Free;
raise;
end;
end;
Your extremely weird patch, calling ClientToScreen on the newly created form, should fix the issue as it does, even if you don't use the point that's returned.
In the case when you don't use it, when you set your form's bounds, since the window of the form has not yet been created, the VCL keeps this information to be later passed to the API when the window is about to be shown. But this information will be discarded since VCL also tells the API to use default window position because of the poDefaultPosOnly setting of Position property.
In the case when you use it, to be able to determine the position of the form in the screen the VCL first creates the window of the form. Hence when you later set the bounds of the form, they are actually implemented through SetWindowPos.
As such, if you've used
XX.HandleNeeded;
instead of
XX.ClientToScreen(Point(0,0));
it would be a more direct workaround.
Of course the correct solution is in Graymatter's answer.
I cannot comment on Show not displaying data, the code you posted in the question should not exhibit that kind of behavior.
TAction has a OnHint event, but unfortunately not a OnHideHint event. This has the following drawback:
I have ToolButtons and other controls associated with actions. Whenever the mouse is over such a control, the hint of the Action is shown in the StatusBar; (I have set the StatusBar's AutoHint property to True). But when the mouse leaves the ToolButton, the previous custom text in the StatusBar (which is not from a hint) is NOT automatically restored!
Now I could write an OnMouseLeave event handler for each and every control on the form to restore my custom text in the StatusBar, but this would be cumbersome!
Isn't there something which automatically restores the previous text in the StatusBar?
An OnHideHint event in TAction would be ideal!
That is default behavior, When AutoHint is True, the status bar automatically responds to hint actions by displaying the long version of the hint's text in the first panel.
The issue that you are having is that when you leave a control with your mouse, you are basically entering another window, it's parent control. And because that parent has no Hint string value assigned to it, the HintAction is updated to an empty string.
If you want to return the default value when there is no hint to display then drop a TApplicationEvents component on the form and use the TApplication.OnHint event like this:
var
OriginalPanelText : String = 'BLA';
procedure TForm1.ApplicationEvents1Hint(Sender: TObject);
begin
if StatusBar1.SimplePanel or (StatusBar1.Panels.Count = 0)
then
if Application.Hint <> ''
then
StatusBar1.SimpleText := Application.Hint
else
StatusBar1.SimpleText := OriginalPanelText
else
if Application.Hint <> ''
then
StatusBar1.Panels[0].Text := Application.Hint
else
StatusBar1.Panels[0].Text := OriginalPanelText;
end;
The AutoHint magic all happens in TStatusBar.ExecuteAction. When the hint stops showing that code sets the status bar text to be empty. You could modify the behaviour like this:
type
TStatusBar = class(ComCtrls.TStatusBar)
private
FRestoreTextAfterHintAction: string;
public
function ExecuteAction(Action: TBasicAction): Boolean; override;
end;
function TStatusBar.ExecuteAction(Action: TBasicAction): Boolean;
var
HintText: string;
begin
if AutoHint and not (csDesigning in ComponentState) and
(Action is THintAction) and not DoHint then begin
HintText := THintAction(Action).Hint;
if SimplePanel or (Panels.Count=0) then begin
if HintText='' then begin
SimpleText := FRestoreTextAfterHintAction;
end else begin
FRestoreTextAfterHintAction := SimpleText;
SimpleText := HintText;
end;
end else begin
if HintText='' then begin
Panels[0].Text := FRestoreTextAfterHintAction;
end else begin
FRestoreTextAfterHintAction := Panels[0].Text;
Panels[0].Text := HintText;
end;
end;
Result := True;
end else begin
Result := inherited ExecuteAction(Action);
end;
end;
I've used a rather crude interposer class and a brittle instance variable to store the text to be restored. You could tart this up to be a little more robust if you wish. The code above at least shows you the place you need to add your hooks.
When the user clicks 'x' on a Pinned Form OnClose is called.
When the user clicks 'x' on an Unpinned Form OnHide is called
When the user clicks 'UnPin' on a Pinned Form OnHide is called.
I'm trying to synchronise the visible forms with a menu system but I don't know how to determine the difference in the OnHide event between when the user clicks 'x' and when the user clicks 'UnPin'. I want to intercept the 'x' and call Close instead.
Each child is a descendant of TManagerPanel which in turn is a descendant of TForm with the border style set to bsSizeToolWin, Drag Kind set to dkDock and Drag Mode is dmAutomatic.
type
TPanelManager = class(TForm)
...
private
...
Panels: TManagerPanelList;
Settings: TSettings; //User Settings
...
end;
...
function TPanelManager.InitChild(ChildClass: TManagerPanelClass): TManagerPanel;
var
Child: TManagerPanel;
begin
Child := ChildClass.Create(Self);
Child.Connection := MSConnection1;
Child.Settings := Settings;
Child.Styles := Styles;
...
Child.OnPanelClosed := PanelClosed;
Child.OnPercentChanged := PercentChanged;
...
Child.OnPanelHide := PanelHide;
Child.Font := Font;
Child.Initialise;
Child.ManualDock(DockTarget);
Panels.AddPanel(Child);
Result := Child;
end;
procedure TPanelManager.PanelClosed(Sender: TObject; var Action: TCloseAction);
var
MenuItem: TMenuItem;
Child: TManagerPanel;
begin
if Sender is TManagerPanel then
begin
Child := TManagerPanel(Sender);
Action := caFree;
MenuItem := MenuItemFromChild(Child);
MenuItem.Checked := False;
Settings[RemoveAmpersand(MenuItem.Caption)] := MenuItem.Checked;
Panels.Remove(Child);
end;
end;
EDIT:
What I mean by a "Pinned" Form: A docked form with the pin set such that it always visible.
What I mean by a "UnPinned" Form: A docked form with the pin released such that a tab appears in a dock tab set and the form appears when the tab is selected.
Delphi Version is 2007
it seems that pinning and unpinning a docked form changes it's parent between a TTabDockPanel and the TPanel I'm docking it to.
Adding an OnHide method to the Demo Dock Form...
procedure TfrmDock.FormHide(Sender: TObject);
begin
if Assigned(Self.Parent) then
ShowMessage(Self.Parent.ClassName)
else
ShowMessage('No Parent');
end;
I can now distinguish between "Floating", "Docked,Pinned" and "Docked, Unpinned" when the form gets hidden.
EDIT
I've found a better way of doing this
procedure TfrmDock.FormHide(Sender: TObject);
begin
if Assigned(Parent) then
begin
if Not (csDocking in ControlState) then //This was the original test above
begin
if Parent is TTabDockPanel then // This is now a safety check
begin
if TTabDockPanel(Parent).AnimateSpeed = 1 then //Additional Test
//form is closing
else
//form is hiding (Unpinned focused changed)
end;
end
else
//form is being unpinned.
end;
end;
In DockCaptionMouseUp the Animation Speed is set to 1 so that the panel appears to close (Hides really fast). The same happens for "Unpinning" but control state changes.
I am using Delphi, and I want to show custom text in the buttons of a MessageDlg, as described here. What is the best way to do that?
Answering my own question.... I wrote the below unit which works well for me.
Delphi provides CreateMessageDialog() to give you a dialog template, which you can modify before displaying. I used that to create a function I called MessageDlgCustom, which takes the same parameters as a standard MessageDlg, but adds one more for replacement button titles.
It correctly handles custom fonts and automatically adjusts buttons to be wide enough for their message. If the buttons overflow the dialog, then that gets adjusted too.
After using that unit, the below sample works:
case MessageDlgCustom('Save your changes?',mtConfirmation,
[mbYes,mbNo,mbCancel],
['&Yes, I would like to save them with this absurdly long button',
'&No, I do not care about my stupid changes',
'&Arg! What are you talking about? Do not close the form!'],
nil) //nil = no custom font
of
mrYes:
begin
SaveChanges;
CloseTheForm;
end; //mrYes (save & close)
mrNo:
begin
CloseForm;
end; //mrNo (close w/o saving)
mrCancel:
begin
//do nothing
end; //mrCancel (neither save nor close)
end; //case
If someone else knows a better way, please share it.
unit CustomDialog;
interface
uses
Dialogs, Forms, Graphics, StdCtrls;
function MessageDlgCustom(const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons; ToCaptions: array of string;
customFont: TFont) : integer;
procedure ModifyDialog(var frm: TForm; ToCaptions : array of string;
customFont : TFont = nil);
implementation
uses
Windows, SysUtils;
function GetTextWidth(s: string; fnt: TFont; HWND: THandle): integer;
var
canvas: TCanvas;
begin
canvas := TCanvas.Create;
try
canvas.Handle := GetWindowDC(HWND);
canvas.Font := fnt;
Result := canvas.TextWidth(s);
finally
ReleaseDC(HWND,canvas.Handle);
FreeAndNil(canvas);
end; //try-finally
end;
function MessageDlgCustom(const Msg: string;
DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; ToCaptions: array of string;
customFont: TFont): integer;
var
dialog : TForm;
begin
try
dialog := CreateMessageDialog(Msg, DlgType, Buttons);
dialog.Position := poScreenCenter;
ModifyDialog(dialog,ToCaptions,customFont);
Result := dialog.ShowModal;
finally
dialog.Release;
end; //try-finally
end;
procedure ModifyDialog(var frm: TForm; ToCaptions: array of string;
customFont: TFont);
const
c_BtnMargin = 10; //margin of button around caption text
var
i,oldButtonWidth,newButtonWidth,btnCnt : integer;
begin
oldButtonWidth := 0;
newButtonWidth := 0;
btnCnt := 0;
for i := 0 to frm.ComponentCount - 1 do begin
//if they asked for a custom font, assign it here
if customFont <> nil then begin
if frm.Components[i] is TLabel then begin
TLabel(frm.Components[i]).Font := customFont;
end;
if frm.Components[i] is TButton then begin
TButton(frm.Components[i]).Font := customFont;
end;
end;
if frm.Components[i] is TButton then begin
//check buttons for a match with a "from" (default) string
//if found, replace with a "to" (custom) string
Inc(btnCnt);
//record the button width *before* we changed the caption
oldButtonWidth := oldButtonWidth + TButton(frm.Components[i]).Width;
//if a custom caption has been provided use that instead,
//or just leave the default caption if the custom caption is empty
if ToCaptions[btnCnt - 1]<>'' then
TButton(frm.Components[i]).Caption := ToCaptions[btnCnt - 1];
//auto-size the button for the new caption
TButton(frm.Components[i]).Width :=
GetTextWidth(TButton(frm.Components[i]).Caption,
TButton(frm.Components[i]).Font,frm.Handle) + c_BtnMargin;
//the first button can stay where it is.
//all other buttons need to slide over to the right of the one b4.
if (1 < btnCnt) and (0 < i) then begin
TButton(frm.Components[i]).Left :=
TButton(frm.Components[i-1]).Left +
TButton(frm.Components[i-1]).Width + c_BtnMargin;
end;
//record the button width *after* changing the caption
newButtonWidth := newButtonWidth + TButton(frm.Components[i]).Width;
end; //if TButton
end; //for i
//whatever we changed the buttons by, widen / shrink the form accordingly
frm.Width := Round(frm.Width + (newButtonWidth - oldButtonWidth) +
(c_BtnMargin * btnCnt));
end;
end.
As an alternative you can use the Open Source SynTaskDialog unit. SynTaskDialog uses the Windows TaskDialog API natively on newer Windows versions and emulates it on older versions. You even can use it with FireMonkey.
For an example of a customizable MessageDlg function have a look at this answer.
You may have a look at the TDam component available on GitHub (https://github.com/digao-dalpiaz/Dam).
This component allows you to create customized Message Dialogs with pre-defined buttons, using formatted text (HTML Text), and allowing to customize a lot of aspects of dialogs.
Besides that, you can manage all your app dialogs into a "container", which stores all dialogs as objects (TDamMsg).
TDam Message Example
TDamMsg properties allows to customize message dialog, like:
Button1 - button 1 caption
Button2 - button 2 caption
Button3 - button 3 caption
Buttons: TDamMsgButtons = Defines the buttons in the message dialog:
dbOK: Defines one button OK
dbYesNo: Defines two buttons Yes/No
dbOne: Defines one button by Button1 defined caption
dbTwo: Defines two buttons by Button1 and Button2 defined captions
dbThree: Defines three buttons by Button1, Button2 and Button3 defined captions
Also, make sure that your 3rd party controls also
call your custom message dlg and not standard
MessageDlg function. That is if they're actually
using it. It is possible that 3rd party controls
do not use the Delphi messagedlg and call the
MessageBox API directly. If that's case, you might
end up with inconsistencies in showing message
boxes.