I want to position a MessageBox in a particular position with respect to the active cell in a string grid and this is no problem using MessageDlgPos() except that I want to prevent the box running off the right or bottom of the screen when the active cell is close to the right or bottom. What I need for this is a way of getting the dimensions of the box but I cannot see a simple way of getting these. Anyone know how without creating my own box?
The MessageDlg...() functions do not support what you are asking for. The dimensions of the dialog are not known until the dialog is being displayed, and you have no way to access the dialog window directly to query/re-position it, except maybe with a WH_CBT hook from SetWindowsHookEx().
That being said...
On Windows Vista+ with Vcl.Dialogs.UseLatestCommonDialogs=true and Visual Styles enabled, the MessageDlg...() functions call the Win32 TaskDialogIndirect() API to display a message box. You have no control over that dialog's dimensions, so you would have to wait for that dialog to issue a TDN_DIALOG_CONSTRUCTED notification to then query its actual dimensions before it is displayed, so you can then adjust its position as needed. However, the MessageDlg...() functions do not provide access to any of TaskDialogIndirect()'s notifications (TCustomTaskDialog, which is used internally, does have an OnDialogConstructed event, amongst other events). So, if you wanted to reposition this dialog, you would have to call TaskDialogIndirect() yourself with a custom callback function (or, use the VCL's TTaskDialog wrapper).
On pre-Vista, or with UseLatestCommonDialogs=false or Visual Styles disabled, the MessageDlg...() functions display a custom VCL TForm via Vcl.Dialogs.CreateMessageDialog() instead, which you can call directly, and then pretty much query, manipulate, and show the returned TForm however you want. Just be sure to Free() it when you are done using it.
You could use an actual TTaskDialog. You can create you own version of it, add a TaskDialogConstructed procedure and get the dimension in the TaskDialogConstructed procedure. Something along the lines of the following.
type
TTaskDialog = class(Vcl.Dialogs.TTaskDialog)
protected
procedure TaskDialogConstructed(Sender: TObject);
end;
procedure TTaskDialog.TaskDialogConstructed(Sender: TObject);
var
TaskDialog: TTaskDialog;
R: TRect;
begin
TaskDialog := Sender as TTaskDialog;
Win32Check(GetWindowRect(TaskDialog.Handle, R));
{... Do whatever with R ...}
end;
function ExecuteTaskDialog(AOwner: TComponent; ATitle, AText: string; ACommonButtons: TTaskDialogCommonButtons = [tcbOK]): integer;
var
TaskDialog: TTaskDialog;
begin
TaskDialog := TTaskDialog.Create(AOwner);
with TaskDialog do
begin
Caption := Application.Title;
Title := ATitle;
Text := AText;
MainIcon := tdiNone;
Flags := Flags + [tfUseHiconMain];
CommonButtons := ACommonButtons;
CustomMainIcon.LoadFromResourceName(HInstance, 'MAINICON');
OnDialogConstructed := TaskDialogConstructed;
Execute;
Result := ModalResult;
Free;
end;
end;
Create the MessageDlg yourself. Add an OnActivate or OnShow event. In this method, ask / change the properties of the dialog.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.StdCtrls;
type
Tfrm = class(TForm)
btn: TButton;
procedure btnClick(Sender: TObject);
private
procedure OnDlgActivate(Sender: TObject);
public
{ Public-Deklarationen }
end;
var
frm: Tfrm;
implementation
uses
Vcl.Dialogs, System.TypInfo;
{$R *.dfm}
procedure Tfrm.btnClick(Sender: TObject);
var
Ldlg : TForm;
LiRet : integer;
begin
Ldlg := CreateMessageDialog('Hallo World!', mtInformation,mbYesNo, mbYes);
try
Ldlg.OnActivate := OnDlgActivate;
LiRet := Ldlg.ShowModal;
finally
Ldlg.free;
end;
end;
procedure Tfrm.OnDlgActivate(Sender: TObject);
var
Lfrm: TForm;
LcTxt: string;
begin
Lfrm := Sender as TForm;
LcTxt := Format('%s %sLeft: %d / Top: %d', [Lfrm.ClassName, sLineBreak, Lfrm.Left, Lfrm.Top]);
ShowMessage(LcTxt);
end;
end.
Related
I'm trying to create a custom property editor for some custom component. The custom property editor is intended to edit some set property, like
type
TButtonOption = (boOption1, boOption2, boOption3);
TButtonOptions = set of TButtonOption;
my property editor descends from TSetProperty class. The problem is: my custom property editor doesn't get registered and Delphi IDE seems to use its own default set property editor, because ShowMessage() calls inside property editor methods never executes! I've created a sample package/component from scratch, as simple as possible, showing this issue. Here is the code:
unit Button1;
interface
uses
System.SysUtils, System.Classes, Vcl.Controls, Vcl.StdCtrls, DesignIntf, DesignEditors;
type
TButtonOption = (boOption1, boOption2, boOption3);
TButtonOptions = set of TButtonOption;
TButtonEx = class(TButton)
private
FOptions: TButtonOptions;
function GetOptions: TButtonOptions;
procedure SetOptions(Value: TButtonOptions);
published
property Options: TButtonOptions read GetOptions write SetOptions default [];
end;
TMySetProperty = class(TSetProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetProperties(Proc: TGetPropProc); override;
function GetValue: string; override;
end;
procedure Register;
implementation
uses
Dialogs;
// TButtonEx - sample component
function TButtonEx.GetOptions: TButtonOptions;
begin
Result := FOptions;
end;
procedure TButtonEx.SetOptions(Value: TButtonOptions);
begin
if FOptions <> Value then
begin
FOptions := Value;
end;
end;
// register stuff
procedure Register;
begin
RegisterComponents('Samples', [TButtonEx]);
RegisterPropertyEditor(TypeInfo(TButtonOptions), nil, '', TMySetProperty);
end;
function TMySetProperty.GetAttributes: TPropertyAttributes;
begin
ShowMessage('GetAttributes');
Result := inherited GetAttributes;
end;
procedure TMySetProperty.GetProperties(Proc: TGetPropProc);
begin
ShowMessage('GetProperties');
inherited;
end;
function TMySetProperty.GetValue: string;
begin
ShowMessage('GetValue');
Result := inherited GetValue;
end;
end.
Please note that:
I'm registering the new property editor (TMySetProperty) for ALL components having a TButtonOptions property. I also tried to do it for TButtonEx only, but the result is the same.
I've added ShowMessage() calls inside all overriden methods of my custom property editor and those methods NEVER get called.
I've already debugged the package and RegisterPropertyEditor() executes. Nevertheless, my custom code in overridden methods never execute.
I've seen other 3rd party components using such property editor (TSetProperty descendants) running in older Delphi IDEs and I could not find any relevant difference in code. Maybe Delphi XE2+ requires something else?
So the question is:
Why my custom property editor does not register/work?
Note: This issue happens in Delphi XE2, XE3, XE4 and also XE5 at least. Other IDEs were not tested but probably have the same behavior.
Finally I got a solution... After testing everything I could imagine - without success - I started searching for something "new" in DesignEditors.pas and DesignIntf.pas units. Reading GetEditorClass() function, I discovered that it first checks for a PropertyMapper. A property mapper can be registered using RegisterPropertyMapper() function. Using it instead of RegisterPropertyEditor() works just as expected. Here is my modified, working code, also showing some interesting application for this: show or hide some options of my set-based property, based on some criteria:
unit Button1;
interface
uses
System.SysUtils, System.Classes, Vcl.Controls, Vcl.StdCtrls,
DesignIntf, DesignEditors;
type
TButtonOption = (boOptionA, boOptionB, boOptionC);
TButtonOptions = set of TButtonOption;
type
TButtonEx = class(TButton)
private
FOptions: TButtonOptions;
function GetOptions: TButtonOptions;
procedure SetOptions(Value: TButtonOptions);
published
property Options: TButtonOptions read GetOptions write SetOptions default [];
end;
TMySetProperty = class(TSetProperty)
private
FProc: TGetPropProc;
procedure InternalGetProperty(const Prop: IProperty);
public
procedure GetProperties(Proc: TGetPropProc); override;
end;
procedure Register;
implementation
uses
TypInfo;
// TButtonEx - sample component
function TButtonEx.GetOptions: TButtonOptions;
begin
Result := FOptions;
end;
procedure TButtonEx.SetOptions(Value: TButtonOptions);
begin
if FOptions <> Value then
begin
FOptions := Value;
end;
end;
// Returns TMySetProperty as the property editor used for Options in TButtonEx class
function MyCustomPropMapper(Obj: TPersistent; PropInfo: PPropInfo): TPropertyEditorClass;
begin
Result := nil;
if Assigned(Obj) and (Obj is TButtonEx) and SameText(String(PropInfo.Name), 'Options') then begin
Result := TMySetProperty;
end;
end;
procedure Register;
begin
RegisterComponents('Samples', [TButtonEx]);
// RegisterPropertyEditor does not work for set-based properties.
// We use RegisterPropertyMapper instead
RegisterPropertyMapper(MyCustomPropMapper);
end;
procedure TMySetProperty.GetProperties(Proc: TGetPropProc);
begin
// Save the original method received
FProc := Proc;
// Call inherited, but passing our internal method as parameter
inherited GetProperties(InternalGetProperty);
end;
procedure TMySetProperty.InternalGetProperty(const Prop: IProperty);
var
i: Integer;
begin
if not Assigned(FProc) then begin // just in case
Exit;
end;
// Now the interesting stuff. I just want to show boOptionA and boOptionB in Object inspector
// So I call the original Proc in those cases only
// boOptionC still exists, but won't be visible in object inspector
for i := 0 to PropCount - 1 do begin
if SameText(Prop.GetName, 'boOptionA') or SameText(Prop.GetName, 'boOptionB') then begin
FProc(Prop); // call original method
end;
end;
end;
end.
So, lately we (me and my coworkers) have been chatting about migrating to FireDac, we are currently using IBO and DBX, but mostly IBO. And then we decided to take everything from IBO to FireDac, but entering in every form, changing every IBOQuery, adding all fields, settings all the display format, etc, etc, etc, would take too much time, so we decided to make a component do it, seemed like an easy task, but I just started and I'm already stuck in something that seems simple, but that I never came across before. First let's look at the component code:
unit UMyComponent;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, IB_Components, IB_Access,
IBODataset, Vcl.StdCtrls, Vcl.Buttons, Vcl.Grids, Vcl.DBGrids, Data.DB,
uADStanIntf, uADStanOption, uADStanParam, uADStanError,
uADDatSManager, uADPhysIntf, uADDAptIntf, uADStanAsync, uADDAptManager,
uADCompDataSet, uADCompClient;
type
TMyComponent = class(TComponent)
private
FADConnection: TADConnection;
FConverter: String;
procedure Iniciar;
procedure SetADConnection(const Value: TADConnection);
procedure SetConverter(const Value: String);
published
property Converter: String read FConverter write SetConverter;
property ADConnection: TADConnection read FADConnection write SetADConnection;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('MyComponents', [TMyComponent]);
end;
{ TMyComponent }
procedure TMyComponent.Iniciar;
var
Form: TForm;
IBOQuery: TIBOQuery;
i: Integer;
procedure _ConverterIBOQuery;
var
ADQuery: TADQuery;
qName: String;
begin
qName := IBOQuery.Name;
if qName.Contains('OLD_') then
Exit;
IBOQuery.Name := 'OLD_'+ qName;
if (FindComponent(qName) = nil) then
begin
ADQuery := TADQuery.Create(Form);
ADQuery.Name := qName;
ADQuery.Connection := FADConnection;
ADQuery.SQL := IBOQuery.SQL;
{
I need to add the fields here, but I need them having a reference,
like the ones you Right Click > Fields Editor > Add All Fields (CTRL + F)
because in the final form of this component, it won't rename the old query
with an 'OLD_' prefix, it will destroy it, and the fields will be gone too,
so I need to add them (having the reference) in order to not rewrite any of my code
}
end;
end;
begin
if Owner is TForm then
Form := TForm(Owner);
if Assigned(Form) then
begin
for i := 0 to (Form.ComponentCount -1) do
{
I know it will stop in the first query it come across,
but I'm trying to full convert only one to know if it's actually possible
}
if (Form.Components[i] is TIBOQuery) then
begin
IBOQuery := TIBOQuery(Form.Components[i]);
Break;
end;
if Assigned(IBOQuery) then
_ConverterIBOQuery;
end;
end;
procedure TMyComponent.SetConverter(const Value: String);
begin
FConverter := UpperCase(Value[1]);
if (FConverter = 'S') then
Iniciar;
FConverter := '';
end;
procedure TMyComponent.SetADConnection(const Value: TADConnection);
begin
FADConnection := Value;
end;
end.
I already tried some of methods found on the internet, such as:
Creating a variable of TField
Using FieldDefs/FieldDefList, updating them and creating the fields
"Hacking" the ADQuery with a "fake" class in order to use the
CreateFields procedure
And none of them did what I was expecting, so I'm questioning
Can I create the field references via code? And, if it's possible, how?
And with references I mean, for example, you have IBOQuery1, and the SQL is
SELECT NAME
FROM COUNTRY
After that, you go to the Fields Editor > Add All Fields (CTRL + F), and then you have the reference IBOQuery1NAME, which is a TStringField and you can just call IBOQuery1NAME.AsString instead of IBOQuery1.FieldByName('NAME').AsString
TL;DR
Trying to create a component that migrate a IBOQuery to ADQuery, but I can't create the references
After many attempts and research, I found an old question with a problem similar to mine, and happily there was a answer with exactly what I wanted
How to add a field programatically to a TAdoTable in Delphi
The answer was provided by the user: Мסž
procedure AddAllFields(DataSet: TDataset);
var
FieldsList: TStringList;
FieldName: WideString;
Field: TField;
WasActive: boolean;
FieldDef: TFieldDef;
i: Integer;
begin
WasActive := DataSet.Active;
if WasActive then
DataSet.Active := False;
try
FieldsList := TStringList.Create;
try
DataSet.FieldDefs.Update;
// make a list of all the field names that aren't already on the DataSet
for i := 0 to DataSet.FieldDefList.Count - 1 do
with DataSet.FieldDefList[i] do
if (FieldClass <> nil) and not(faHiddenCol in Attributes) then
begin
FieldName := DataSet.FieldDefList.Strings[i];
Field := DataSet.FindField(FieldName);
if (Field = nil) or (Field.Owner <> DataSet.Owner) then
FieldsList.Add(FieldName);
end;
// add those fields to the dataset
for i := 0 to FieldsList.Count - 1 do
begin
FieldDef := DataSet.FieldDefList.FieldByName(FieldName);
Field := FieldDef.CreateField(DataSet.Owner, nil, FieldName, False);
try
Field.name := FieldName + IntToStr(random(MaxInt)); // make the name unique
except
Field.Free;
raise ;
end;
end;
finally
FieldsList.Free;
end;
finally
if WasActive then
DataSet.Active := true;
end;
end;
When support for runtime DPI switching was added to the forms class, no consideration was given to basic UI elements like menus.
Menu drawing is fundamentally broken because it relies on Screen.MenuFont, which is a system wide metric, not specific to monitors. So while the form itself can be properly scaled relatively simply, the menus that display over it only work correctly IF that scaling happens to match whatever metrics were loaded into the Screen object.
This is a problem for the main menu bar, its popup menus, and all popup menus on the form. None of these scale if the form is moved to a monitor with a different DPI than the system metrics.
The only way to really make this work is to fix the VCL. Waiting for Embarcadero to flesh out multi-DPI is not really an option.
Looking at the VCL code, the basic issue is that the Screen.MenuFont property is assigned to a menu canvas rather than selecting a font appropriate for the monitor on which the menu will appear. Affected classes can be found simply by searching for Screen.MenuFont in the VCL source.
What is the correct way to work around this limitation, without having to completely re-write the classes involved?
My first inclination is to use a detour to keep track of menu popups and override the Screen.MenuFont property when it is being used to set up a menu. That seems like too much of a hack.
Here is one solution that is working for now. Using the Delphi Detours Library, adding this unit to the dpr uses list (I had to put it near the top of my list before other forms) causes the correct font size to be applied to the menu canvas, based on the form that holds the menu items in any popup menu. This solution deliberately ignores toplevel menues (main menu bars) because the VCL doesn't properly deal with owner measured items there.
unit slMenuDPIFix;
// add this unit to the main application dpr file BEFORE ANY FORMS in the uses list.
interface
implementation
uses
Winapi.Windows, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Menus, slScaleUtils, Math,
DDetours;
type
TMenuClass = class(TMenu);
TMenuItemClass = class(TMenuItem);
var
TrampolineMenuCreate: procedure(const Self: TMenuClass; AOwner: TComponent) = nil;
TrampolineMenuItemAdvancedDrawItem: procedure(const Self: TMenuItemClass; ACanvas: TCanvas; ARect: TRect; State: TOwnerDrawState; TopLevel: Boolean) = nil;
TrampolineMenuItemMeasureItem: procedure(const Self: TMenuItemClass; ACanvas: TCanvas; var Width, Height: Integer) = nil;
function GetPopupDPI(const MenuItem: TMenuItemClass): Integer;
var
pm: TMenu;
pcf: TCustomForm;
begin
Result := Screen.PixelsPerInch;
pm := MenuItem.GetParentMenu;
if Assigned(pm) and (pm.Owner is TControl) then
pcf := GetParentForm(TControl(pm.Owner))
else
pcf := nil;
if Assigned(pcf) and (pcf is TForm) then
Result := TForm(pcf).PixelsPerInch;
end;
procedure MenuCreateHooked(const Self: TMenuClass; AOwner: TComponent);
begin
TrampolineMenuCreate(Self, AOwner);
Self.OwnerDraw := True; // force always ownerdraw.
end;
procedure MenuItemAdvancedDrawItemHooked(const Self: TMenuItemClass; ACanvas: TCanvas; ARect: TRect; State: TOwnerDrawState; TopLevel: Boolean);
begin
if (not TopLevel) then
begin
ACanvas.Font.Height := MulDiv(ACanvas.Font.Height, GetPopupDPI(Self), Screen.PixelsPerInch);
end;
TrampolineMenuItemAdvancedDrawItem(Self, ACanvas, ARect, State, TopLevel);
end;
procedure MenuItemMeasureItemHooked(const Self: TMenuItemClass; ACanvas: TCanvas; var Width, Height: Integer);
var
lHeight: Integer;
pdpi: Integer;
begin
pdpi := GetPopupDPI(Self);
if (Self.Caption <> cLineCaption) and (pdpi <> Screen.PixelsPerInch) then
begin
ACanvas.Font.Height := MulDiv(ACanvas.Font.Height, pdpi, Screen.PixelsPerInch);
lHeight := ACanvas.TextHeight('|') + MulDiv(6, pdpi, Screen.PixelsPerInch);
end else
lHeight := 0;
TrampolineMenuItemMeasureItem(Self, ACanvas, Width, Height);
if lHeight > 0 then
Height := Max(Height, lHeight);
end;
initialization
TrampolineMenuCreate := InterceptCreate(#TMenuClass.Create, #MenuCreateHooked);
TrampolineMenuItemAdvancedDrawItem := InterceptCreate(#TMenuItemClass.AdvancedDrawItem, #MenuItemAdvancedDrawItemHooked);
TrampolineMenuItemMeasureItem := InterceptCreate(#TMenuItemClass.MeasureItem, #MenuItemMeasureItemHooked);
finalization
InterceptRemove(#TrampolineMenuCreate);
InterceptRemove(#TrampolineMenuItemAdvancedDrawItem);
InterceptRemove(#TrampolineMenuItemMeasureItem);
end.
One could just as easily patch Vcl.Menus, but I did not want to do that.
Embarcadero fixed a lot of bugs with (popup)menus in Delphi 10.2.3 Tokyo, but the TPopupMenu is still not correct. I've updated the code above to work correct in the latest Delphi version.
unit slMenuDPIFix;
// add this unit to the main application dpr file BEFORE ANY FORMS in the uses list.
interface
implementation
uses
Winapi.Windows, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Menus, SysUtils,
DDetours;
type
TMenuClass = class(TMenu);
TMenuItemClass = class(TMenuItem);
type
TMenuItemHelper = class helper for TMenuItem
public
function GetDevicePPIproc: Pointer;
end;
var
TrampolineMenuCreate: procedure(const Self: TMenuClass; AOwner: TComponent) = nil;
TrampolineMenuItemGetDevicePPI: function(const Self: TMenuItemClass): Integer;
procedure MenuCreateHooked(const Self: TMenuClass; AOwner: TComponent);
begin
TrampolineMenuCreate(Self, AOwner);
Self.OwnerDraw := True; // force always ownerdraw.
end;
function GetDevicePPIHooked(const Self: TMenuItemClass): Integer;
var
DC: HDC;
LParent: TMenu;
LPlacement: TWindowPlacement;
LMonitor: TMonitor;
LForm: TCustomForm;
begin
LParent := Self.GetParentMenu;
if (LParent <> nil) and (LParent.Owner is TWinControl) and CheckWin32Version(6,3) then
begin
LForm := GetParentForm(TControl(LParent.Owner));
LPlacement.length := SizeOf(TWindowPlacement);
if (TWinControl(LForm).Handle > 0) and GetWindowPlacement(TWinControl(LForm).Handle, LPlacement) then
LMonitor := Screen.MonitorFromPoint(LPlacement.rcNormalPosition.CenterPoint)
else
LMonitor := Screen.MonitorFromWindow(Application.Handle);
if LMonitor <> nil then
Result := LMonitor.PixelsPerInch
else
Result := Screen.PixelsPerInch;
end
else
begin
DC := GetDC(0);
Result := GetDeviceCaps(DC, LOGPIXELSY);
ReleaseDC(0, DC);
end;
end;
{ TMenuItemHelper }
function TMenuItemHelper.GetDevicePPIproc: Pointer;
begin
Result := #TMenuItem.GetDevicePPI;
end;
initialization
TrampolineMenuCreate := InterceptCreate(#TMenuClass.Create, #MenuCreateHooked);
TrampolineMenuItemGetDevicePPI := InterceptCreate(TMenuItemClass.GetDevicePPIproc, #GetDevicePPIHooked);
finalization
InterceptRemove(#TrampolineMenuCreate);
InterceptRemove(#TrampolineMenuItemGetDevicePPI);
end.
I am trying to write a separate unit for my main form to call, all of my other units are working except for one that uses TTimer.
Basically what the function is supposed to be doing is that the main form uDataReceived calls BlinkRect(Gateway) which is processed in rRectControl unit and the according Rectangle will blink in the main form.
Here are the codes:
unit uRectControl;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
System.Variants, System.IOUtils, FMX.Graphics, FMX.Types, FMX.Objects;
var
Blinks: array [0 .. 2] of record Rectangle: TRectangle;
Timer: TTimer;
end;
type
TMyClass = Class(TObject)
private
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
public
procedure BlinkRect(Gateway: integer);
end;
procedure AssignRectangles;
implementation
uses uDataReceived;
// Error shows "Cannot resolve unit name 'uDataReceived'
{ TMyClass }
procedure AssignRectangles;
var
i: integer;
begin
Blinks[0].Rectangle := TC_Theft_Detection.rect1;
// Error shows Undeclared Identifier TC_Theft_Detection (which is the name of the main form)
Blinks[0].Timer := nil;
Blinks[1].Rectangle := TC_Theft_Detection.rect2;
Blinks[1].Timer := nil;
Blinks[2].Rectangle := TC_Theft_Detection.rect3;
Blinks[2].Timer := nil;
for i := 0 to 2 do
Blinks[i].Rectangle.Fill.Color := TAlphacolors.blue;
end;
procedure TMyClass.BlinkRect(Gateway: integer);
begin
Blinks[Gateway].Rectangle.Fill.Color := TAlphacolors.Red;
Blinks[Gateway].Rectangle.Fill.Kind := TBrushKind.Solid;
Blinks[Gateway].Rectangle.Stroke.Thickness := 0.3;
Blinks[Gateway].Rectangle.Stroke.Color := TAlphacolors.Black;
if Blinks[Gateway].Timer = nil then
begin
Blinks[Gateway].Timer := TTimer.Create(nil);
Blinks[Gateway].Timer.OnTimer := Timer1Timer;
Blinks[Gateway].Timer.Interval := 500;
Blinks[Gateway].Timer.Tag := Gateway;
Blinks[Gateway].Timer.Enabled := True;
end;
end;
procedure TMyClass.Timer1Timer(Sender: TObject);
var
Timer: TTimer;
begin
Timer := TTimer(Sender);
Blinks[Timer.Tag].Rectangle.Visible := not Blinks[Timer.Tag]
.Rectangle.Visible;
end;
end.
I know there must be something wrong with the unit shown above, and my question is:
How to work with TTimer in a separate unit and how to call the procedure BlinkRect(Gateway) on the main form.
Thanks a lot!!
Your code in uRectControl works provided AssignRectangles is called before you attempt to call BlinkRect. However there are a number of issues to be addressed.
1) Cross dependency of units
The form (uDataReceived) apparently uses uRectControl and that is fine. The way uRectControl is written it needs to use (uses uDataReceived in the implementation) the form and this is not good.
This error is simple to correct, because the AssignRectangles procedure is the only place where the form is referred to. AssignRectangles could just as well be in the form, since the Blinks[] array is global (in the interface of uRectControl) and can therefore be accessed by the form.
2) Global variables
Global variables should be avoided as much as possible. You have defined both the Blinks[] array and the Timer to be global, so you might by mistake access and modify them from anywhere in your program just by adding uRectControl to a uses clause. In future development you might add new forms that have indicators you want to blink and add TRectangles to the Blinks[] array possibly overwriting value that are already there and you end up in a mess. I will address this issue in my suggestion below.
3) Hardcoded entities
In Proof Of Concept code it is acceptable (or not) to hardcode constants, sizes of arrays etc. but not in production code. Just think about all changes you need to do just to add one more blinking rectangle to the form. Dynamical arrays or better TList and its derivatives etc. comes to rescue here. You have also limited yourself to only TRectangles. What if you would like to have circular indicators in your form?
4) Unsyncronized blinking
It may look cool (not really) when indicators are blinking all over the place, but actually it is just distracting. I guess you tried to change this with the timer in TMyClass, but you still left the individual timers in the Blinks records. I will address this also in my suggestion below.
Here is a suggestion
unit ShapeBlinker;
interface
uses
System.SysUtils, System.UITypes, System.Classes, System.Generics.Collections,
FMX.Graphics, FMX.Types, FMX.Objects;
type
TBlinkState = (bsOff, bsBlinking, bsSteady);
I have a background in Fire Alarm Systems, and it is common to have three states; off, blinking and steady lit. TBlinkState represents these.
Then comes a class that represent indicators in the UI. An indicator can be any TShape derivative like TRectangle, TCircle, TPath etc. Each state can have its own color.
type
[...]
TBlinkingShape = class
private
FShape: TShape;
FState: TBlinkState;
FOffColor: TAlphaColor;
FBlinkColor: TAlphaColor;
FSteadyColor: TAlphaColor;
public
constructor Create(AShape: TShape);
procedure SetBlinkState(NewState: TBlinkState);
end;
The field FShape holds a reference to a TShape derivative. Through this reference we have access to the actual component on the UI form and can change its color. We will see later how the TShape is passed to the constructor.
Then the second class which manages a collection of TBlinkingShape, timing and actual color changes of the indicators on the form.
type
[...]
TShapeBlinker = class
private
FBlinkingShapes: TObjectList<TBlinkingShape>;
FBlinkPhase: integer;
FTimer: TTimer;
public
constructor Create;
destructor Destroy; override;
procedure RegisterShape(Shape: TShape; OffColor, BlinkColor, SteadyColor: TAlphaColor);
procedure UnRegisterShape(Shape: TShape);
procedure BlinkTimer(Sender: TObject);
procedure SetBlinkState(Shape: TShape; NewState: TBlinkState);
function GetBlinkState(Shape: TShape): TBlinkState;
end;
FBlinkingShapes is the object list that holds instances of TBlinkingShapes.
FBlinkPhase syncronizes blinking of the indicators so that all blinking indicators change to the BlinkColor simultaneously. FTimer is common for all indicators.
Procedure RegisterShape is called by the UI when it wants to add an indicator to the list. UnRegister is called when an indicator is to be removed from the list. SetBlinkState is used to change state and GetBlinkState to retrieve the state of an indicator.
The unit is designed to be usable by any number of forms, synchronizing blinking for all of them. This requires that the TShapeBlinker is a singleton. It is therefore created in the initialization section of the unit, and freed in the finalization.
The instance is held by a var in the implementation, thus inaccessible directly from any other unit. Access is provided by a function declared as the last item in the interface of the unit:
function ShapeBlinker: TShapeBlinker;
This effectively prevents a mistake to accidentally call ShapeBlinker.Create.
Instead of commenting on each method I just copy the implementation here:
implementation
var
SShapeBlinker: TShapeBlinker;
function ShapeBlinker: TShapeBlinker;
begin
result := SShapeBlinker;
end;
{ TBlinkingShape }
constructor TBlinkingShape.Create(AShape: TShape);
begin
FShape := AShape;
FState := bsOff;
end;
procedure TBlinkingShape.SetBlinkState(NewState: TBlinkState);
begin
FState := NewState;
case NewState of
bsOff: begin
FShape.Fill.Color := FOffColor;
end;
bsBlinking: begin
FShape.Fill.Color := FBlinkColor;
end;
bsSteady: begin
FShape.Fill.Color := FSteadyColor;
end;
end;
end;
{ TShapeBlinker }
constructor TShapeBlinker.Create;
begin
FBlinkingShapes := TObjectList<TBlinkingShape>.Create;
FTimer := TTimer.Create(nil);
FTimer.OnTimer := BlinkTimer;
FTimer.Interval := 500;
FTimer.Enabled := False;
end;
destructor TShapeBlinker.Destroy;
begin
FTimer.Enabled := False;
FTimer.Free;
FBlinkingShapes.Free;
inherited;
end;
function TShapeBlinker.GetBlinkState(Shape: TShape): TBlinkState;
var
RegShape: TBlinkingShape;
begin
result := bsOff;
for RegShape in FBlinkingShapes do
if Shape = RegShape.FShape then result := RegShape.FState;
end;
procedure TShapeBlinker.SetBlinkState(Shape: TShape; NewState: TBlinkState);
var
RegShape: TBlinkingShape;
begin
for RegShape in FBlinkingShapes do
if Shape = RegShape.FShape then RegShape.SetBlinkState(NewState);
self.FTimer.Enabled := True;
end;
procedure TShapeBlinker.BlinkTimer(Sender: TObject);
var
i: integer;
begin
FTimer.Enabled := False;
FBlinkPhase := (FBlinkPhase + 1) mod 2;
for i := 0 to FBlinkingShapes.Count-1 do
with FBlinkingShapes[i] do
begin
case FState of
bsOff: begin
FShape.Fill.Color := FOffColor;
end;
bsBlinking: begin
if FBlinkPhase = 1 then
FShape.Fill.Color := FOffColor // alt. FSteadyColor
else
FShape.Fill.Color := FBlinkColor;
FTimer.Enabled := True;
end;
bsSteady: begin
FShape.Fill.Color := FSteadyColor;
end;
end;
end;
end;
procedure TShapeBlinker.RegisterShape(Shape: TShape; OffColor, BlinkColor, SteadyColor: TAlphaColor);
begin
with FBlinkingShapes[FBlinkingShapes.Add(TBlinkingShape.Create(Shape))] do
begin
FOffColor := OffColor; //TAlphaColors.Silver;
FBlinkColor := BlinkColor; //TAlphaColors.Red;
FSteadyColor := SteadyColor; //TAlphaColors.Yellow;
end;
end;
procedure TShapeBlinker.UnRegisterShape(Shape: TShape);
var
i: integer;
begin
for i := FBlinkingShapes.Count-1 downto 0 do
if FBlinkingShapes[i].FShape = Shape then
FBlinkingShapes.Delete(i);
end;
initialization
SShapeBlinker := TShapeBlinker.Create;
finalization
SShapeBlinker.Free;
end.
Finally a few words about usage. Consider a form, say TAlarmView, with 2 TRectangle and 1 TCircle.
In FormCreate you might register these for blinking as follows
procedure TAlarmView.FormCreate(Sender: TObject);
begin
ShapeBlinker.RegisterShape(Rect1, TAlphaColors.Silver, TAlphaColors.Red, TAlphaColors.Yellow);
ShapeBlinker.RegisterShape(Circle1, TAlphaColors.Silver, TAlphaColors.Red, TAlphaColors.Yellow);
ShapeBlinker.RegisterShape(Rect3, TAlphaColors.Silver, TAlphaColors.Red, TAlphaColors.Yellow);
end;
and then test them with button clicks like
procedure TAlarmView.Button1Click(Sender: TObject);
begin
case ShapeBlinker.GetBlinkState(Rect1) of
bsOff: ShapeBlinker.SetBlinkState(Rect1, bsBlinking);
bsBlinking: ShapeBlinker.SetBlinkState(Rect1, bsSteady);
else ShapeBlinker.SetBlinkState(Rect1, bsOff);
end;
end;
As you see I just go through the different states for each click.
I am using the FireMonkey Grid control but have an on-going issue in trying to right align a column. From other users postings, I have managed to create a new TColumn type, apply a style to this (text as HorzAlign=taTrailing) and in theory - thought that this would be solution. The values are provided by the OnGetValue function to the Grid control.
The problem is however that although at first it looks OK, if you scroll the bar/mouse wheel etc. the new TColumn type column does not appear to refresh correctly using the method/code below. It could be a bug/feature of the Grid (or the way I am doing it). I have tried .ReAlign etc...; but to no avail. The only way to get the grid back in line is do a column resize for example - which then redraws correctly?
The code below shows that it is a simple TGrid, with 2 cols, 1 the standard StringColumn and 1 my new StringColNum (wuth right alignment applied). - Any help appreciated as this one is a basic requirement of any grid work.
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.Objects, FMX.Grid,
FMX.Layouts, FMX.Edit;
type
TForm1 = class(TForm)
Grid1: TGrid;
Button1: TButton;
StyleBook1: TStyleBook;
procedure Grid1GetValue(Sender: TObject; const Col, Row: Integer;
var Value: Variant);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TStringColNum = class(TStringColumn)
private
function CreateCellControl: TStyledControl; override;
public
constructor Create(AOwner: TComponent); override;
published
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
constructor TStringColNum.Create(AOwner: TComponent);
begin
inherited;
end;
function TStringColNum.CreateCellControl: TStyledControl;
var
t:TEdit;
begin
Result:=TStringColNum.Create(Self);
Result.StyleLookup := 'textrightalign';
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Grid1.AddObject(TStringColumn.Create(Self));
Grid1.AddObject(TStringColNum.Create(Self)); // Right Aligned column?
Grid1.RowCount:=5000;
Grid1.ShowScrollBars:=True;
end;
procedure TForm1.Grid1GetValue(Sender: TObject; const Col, Row: Integer;
var Value: Variant);
var
cell: TStyledControl;
t: TText;
begin
if Col=0 then
Value:='Row '+IntToStr(Row);;
if Col=1 then
begin
cell := Grid1.Columns[Col].CellControlByRow(Row);
if Assigned(cell) then
begin
t := (Cell.FindStyleResource('text') as TText);
if Assigned(t) then
t.Text:='Row '+IntToStr(Row);
end;
end;
end;
end.
Kind regards. Ian.
All of which reminds me that I still haven't written my blog post about this.
Anyway, a grid cell can be any descendant of TStyledControl (basically any control). The default for a text cell is TTextCell, which is simply a TEdit. Being a TEdit means changing the alignment is really easy: just change the TextAlign property. No need to mess with styles (unless you really want to).
Your column needs to create your cells in the CreateCellControl method. You're actually creating an instance of your column which is your main problem.
You don't need the Create method for your column (it's doing nothing), so delete it (unless you need it for something else) and amend your CreateCellControl.
function TStringColNum.CreateCellControl: TStyledControl;
begin
Result:=inherited;
TTextCell(Result).TextAlign := taTrailing;
end;
Finally, your GetValue event handler needs do nothing more than return the value:
procedure TForm1.Grid1GetValue(Sender: TObject; const Col, Row: Integer;
var Value: Variant);
begin
if Col=0 then
Value:='Row '+IntToStr(Row);
if Col=1 then
Value := 'Row '+IntToStr(Row);
end;
I think it is a laziness of Embarcadero.
adding/modifying 3 lines in FMX.Grid.pas solves this problem.
instead of modifiying original FMX.Grid pas, I recommend copying original FMX.Grid pas to your Project directory, including in your Project (add to Project) and adding/modifiying following lines.
TColumn = class(TStyledControl)
private const
HorzTextMargin = 2;
VertTextMargin = 1;
private
FReadOnly: Boolean;
FHorizontalAlign:TTextAlign;//Add this Line *********
FEditMode: Integer;
FApplyImmediately: boolean;
...
...
procedure UpdateCell(ARow: Integer);
published
property HorizontalAlign: TTextAlign read FHorizontalAlign write FHorizontalAlign;//add this line *******
property Align;
property ClipChildren default False;
procedure TColumn.DefaultDrawCell(const Canvas: TCanvas; const Bounds: TRectF; const Row: Integer;
const Value: TValue; const State: TGridDrawStates);
var
R: TRectF;
Layout: TTextLayout;
LocalRow: Integer;
begin
if FDrawable <> nil then
FDrawable.DrawCell(Canvas, Bounds, Row, Value, State)
else
...
...
Layout.Opacity := AbsoluteOpacity;
(*remark this line *****************
Layout.HorizontalAlign := Grid.TextSettingsControl.ResultingTextSettings.HorzAlign;
*)
Layout.HorizontalAlign := HorizontalAlign;//add this line *****
finally you can set the new property in your Project. e.g:
MyColumn.HorizontalAlign:=TTextAlign.taCenter;
Descending columns does not work well with livebindings as the bindmanager creates the columns so you have to mess with descending that. Neither elegant nor practical in my view.
Simply align your cells in the grid OnPainting event.
I := Col;
for J := 0 to Grid1.RowCount - 1 do
begin
T := TTextCell(Grid1.Columns[I].Children[J]);
T.TextAlign := TTextAlign.taTrailing;
end;
If you use livebindings when you have less chance to customize the column class which is being created, but you can create helpers for Column which sets some attributes of individual cell controls. Not too elegant but simple and works:
unit GridColumnHelper;
interface
uses
Fmx.Types, Fmx.Controls, Fmx.Grid, Fmx.Edit;
type
TGridColumnHelper = class helper for TColumn
public
procedure SetEditMaxLength(aValue: Integer);
procedure SetEditTextAlign(aValue: TTextAlign);
end;
implementation
{ TGridColumnHelper }
procedure TGridColumnHelper.SetEditMaxLength(aValue: Integer);
var
lControl: TStyledControl;
begin
for lControl in FCellControls do
begin
if lControl is TEdit then
(lControl as TEdit).MaxLength := aValue;
end;
end;
procedure TGridColumnHelper.SetEditTextAlign(aValue: TTextAlign);
var
lControl: TStyledControl;
begin
for lControl in FCellControls do
begin
if lControl is TEdit then
(lControl as TEdit).TextAlign := aValue;
end;
end;
end.
After the binding has filled the grid, you can call the helpers:
MyGrid.Columns[0].SetEditTextAlign(TTextAlign.taTrailing);
MyGrid.Columns[1].SetEditMaxLength(15);
Solution of "suat dmk" is working fine you have to recompile Fmx.Bind.DBLinks.pas and Fmx.Bind.Editors.pas if you are gonna use DB links.
After that, you simply put in OnPainting event:
SGrid1.ColumnByIndex(1).HorizontalAlign := TTextAlign.Leading;
another solution:
Grid1.ApplyStyleLookup();
MyCol1.DefaultTextSettings.HorzAlign:=TTextAlign.taCenter;