How to handle menu scaling after runtime DPI change in Delphi Seattle - delphi

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.

Related

How do I get the MessageDlgPos dimensions?

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.

Delphi - change ribbon menu color when VCL theme is applied

I'm using TRibbon on an Delphi XE7 application with VCL theme applied and I'd like to change the menu color (because it's difficult to see the items in dark themes), as following:
I've tried the following code, but it only works when style is disabled:
Ribbon1.ApplicationMenu.Menu.ColorMap.MenuColor:= clYellow;
Also no effect with this line:
Ribbon1.ApplicationMenu.Menu.ColorMap.MenuColor:= StyleServices.GetStyleColor(scButtonHot);
Does anyone know if it is possible?
Thanks a lot!
Create your own style with the color you like.
After some try, I found a solution. I don't know if it's the best approach, but it worked for me and could be useful for someone else.
The problem is the method bellow (Vcl.ActnMenus.pas), when StyleServices is enabled:
procedure TCustomActionPopupMenu.DrawBackground;
begin
inherited;
if StyleServices.Enabled and not StyleServices.IsSystemStyle then
StyleServices.DrawElement(Canvas.Handle, StyleServices.GetElementDetails(tmPopupBackground),
Rect(0, 0, Width, Height))
else
begin
Canvas.Brush.Color := ColorMap.MenuColor;
Canvas.FillRect(ClientRect);
end;
end;
So, in order to bypass this method, I just hooked it (adapting from here):
unit MethodHooker;
interface
uses Windows, Vcl.ActnMenus;
type
PInstruction = ^TInstruction;
TInstruction = packed record
Opcode: Byte;
Offset: Integer;
end;
TCustomActionPopupMenu = class(Vcl.ActnMenus.TCustomActionPopupMenu)
procedure DrawBackgroundEx;
end;
implementation
procedure Patch(Address: Pointer; const NewCode; Size: NativeUInt);
var
NumberOfBytes: NativeUInt;
begin
WriteProcessMemory(GetCurrentProcess, Address, #NewCode, Size, NumberOfBytes);
end;
procedure Redirect(OldAddress, NewAddress: Pointer);
var
NewCode: TInstruction;
begin
NewCode.Opcode := $E9;//jump relative
NewCode.Offset := Integer(NewAddress)-Integer(OldAddress)-SizeOf(NewCode);
Patch(OldAddress, NewCode, SizeOf(NewCode));
end;
{ TCustomActionPopupMenu }
procedure TCustomActionPopupMenu.DrawBackgroundEx;
begin
Canvas.Brush.Color := $00EEEAE9;
Canvas.FillRect(ClientRect);
end;
initialization
Redirect(#TCustomActionPopupMenu.DrawBackground, #TCustomActionPopupMenu.DrawBackgroundEx);
end.
That's it. Just save this unit and add it to the project. No need to call this anywhere.

VCL richedit, slow to change word colors

I have the following code in a delphi program (VCL based desktop application) to iterate through lines of text (sentences of between about 8-15 words) in a richedit, find instances of a user selected word, and then color that word 'red' should it appear on a line.
The problem: The color changing proceeds painfully slowly (several minutes elapse) if the procedure must work through more than a few thousand lines. I'm left sitting here while the cursor dances around. Here's the procedure that is the source of the delay:
procedure Color_Words(RE: TRichEdit; Word: String; Color: TColor);
var
i, startPos, CharPos2, nosChars: Integer;
begin
startPos := 0;
nosChars := 0;
charpos2:=0;
RE.lines.beginupdate;
for i := 0 to Pred(RE.Lines.Count) do
begin
nosChars := nosChars + Length(RE.Lines[i]);
CharPos2 := RE.FindText(word, startPos,nosChars,stmatchcase]);
startPos := CharPos2+1;
RE.SelStart := CharPos2;
RE.SelLength :=(Length(word));
RE.SelAttributes.Color := Color;
end;
RE.Lines.EndUpdate;
end;
Can someone come up with a procedure that is much, much quicker, or advise me how to solve matters? Also, if you could explain the slow processing in layman's terms that would be wonderful. (I am but a hobbyist).
First thing to do is change your code to use the version 4.1 of RichEdit control (introduced with Windows XP SP1), that alone might speed things up.
"RichEdit20W": Riched20.dll (Windows 98)
"RICHEDIT50W": Msftedit.dll (Windows XP SP1)
Windows continues to support the older versions of the RichEdit control, but Delphi stubbornly continues to use the old version, as you can see in Vcl.ComCtrls.pas:
procedure TCustomRichEdit.CreateParams(var Params: TCreateParams);
const
RichEditClassName = 'RICHEDIT20W';
begin
inherited CreateParams(Params);
CreateSubClass(Params, RichEditClassName); //<-- 'RICHEDIT20W'
//...
end;
Tell Delphi to use the Windows XP era RichEdit 4.1
There are a couple ways to fix this; the least intrusive is to create a new unit:
MicrosoftEdit.pas
unit MicrosoftEdit;
interface
uses
Vcl.ComCtrls, Winapi.RichEdit, Vcl.Controls, Winapi.Windows, System.Classes;
type
TRichEdit = class(Vcl.ComCtrls.TRichEdit)
protected
procedure CreateParams(var Params: TCreateParams); override;
end;
implementation
{ TMicrosoftEdit }
procedure TRichEdit.CreateParams(var Params: TCreateParams);
const
MSFTEDIT_CLASS = 'RICHEDIT50W'; //Richedit 4.1, Msftedit.dll
begin
LoadLibrary('msftedit.dll');
inherited CreateParams({var}Params);
CreateSubClass({var}Params, MSFTEDIT_CLASS); //"RICHEDIT50W"
end;
end.
And then include MicrosoftEdit.pas as the last unit in the interface section of your form's uses clause. And you can even be doubly sure that it works by re-declaring TRichEdit to be your new TRichEdit:
unit MyForm;
uses
Forms, RichEdit, MicrosoftEdit;
type
TRichEdit = MicrosoftEdit.TRichEdit; //use our own TRichEdit
TMyForm = class(TForm)
RichEdit1: TRichEdit;
private
protected
public
end;
//...
OnChange?
If you are making formatting changes to the text in a RichEdit:
procedure TMyForm.Button1Click(Sender: TObject);
begin
Color_Words(RichEdit1, 'Trump', clRed);
end;
and you have an OnChange handler attached to the RichEdit, it will fire the OnChange every time the formatting changes. You need to stop that:
procedure TMyForm.Button1Click(Sender: TObject);
var
oldOnChange: TNotifyEvent;
begin
oldOnChange := RichEdit1.OnChange;
RichEdit1.OnChange := nil;
try
Color_Words(RichEdit1, 'Trump', clRed);
finally
RichEdit1.OnChange := oldOnChange;
end;
end;
Undos
In addition, every coloring change you make will be recorded in the Undo list! As well as the RichEdit redrawing every time. Stop those:
procedure TMyForm.Button1Click(Sender: TObject);
var
oldOnChange: TNotifyEvent;
begin
oldOnChange := RichEdit1.OnChange;
RichEdit1.OnChange := nil;
try
RichEditSuspendAll(RichEdit1, True);
try
Color_Words(RichEdit1, 'Trump', clRed);
finally
RichEditSuspendAll(RichEdit1, False);
end;
finally
RichEdit1.OnChange := oldOnChange;
end;
end;
With a helper function:
procedure RichEditSuspendAll(ARichEdit: TRichEdit; bSuspend: Boolean);
var
doc: ITextDocument;
re: IUnknown;
begin
{
http://bcbjournal.org/articles/vol3/9910/Faster_rich_edit_syntax_highlighting.htm
int eventMask = ::SendMessage(RichEdit1->Handle, EM_SETEVENTMASK, 0, 0);
SendMessage(RichEdit1->Handle, WM_SETREDRAW, false, 0);
ParseAllText(RichEdit1);
SendMessage(RichEdit1->Handle, WM_SETREDRAW, true, 0);
InvalidateRect(RichEdit1->Handle, 0, true);
SendMessage(RichEdit1->Handle, EM_SETEVENTMASK, 0, eventMask);
}
{
http://support.microsoft.com/KB/199852
How To Suspend and Resume the Undo Functionality in Richedit 3.0
If it is necessary to Undo an action that is performed before a suspend, after resuming the Undo, then,
tomFalse must be replaced with "tomSuspend" and tomTrue must be replaced with "tomResume".
This method retains the contents of the Undo buffer even when Undo is suspended.
Applications can retrieve an ITextDocument pointer from a rich edit control.
To do this, send an EM_GETOLEINTERFACE message to retrieve an IRichEditOle
object from a rich edit control. Then, call the object's
IUnknown::QueryInterface method to retrieve an ITextDocument pointer.
}
if ARichEdit = nil then
raise Exception.Create('ARichEdit is nil');
if SendMessage(ARichEdit.Handle, EM_GETOLEINTERFACE, 0, LPARAM(#re)) = 0 then
raise Exception.Create('Could not get OleInterface from RichEdit');
doc := re as ITextDocument;
doc := RichEditGetTextDocument(ARichEdit);
if bSuspend then
begin
RichEdit.Perform(WM_SETREDRAW, 0, 0); //disable all painting of the control
doc.Undo(Integer(tomSuspend)); // Suspends Undo.
end
else
begin
doc.Undo(Integer(tomResume)); // Resumes Undo.
RichEdit.Perform(WM_SETREDRAW, 0, 0); //disable all painting of the control
end;
end;

TListView columns order bug after windows theme change

TListView's column contents become incorrect after windows theme change. I've narrowed it down to CM_RECREATE message, that's when VCL recreates TListView's window handle in response to system theme change. Below are some screenshots illustrating the problem.
Original list view state
Last column has been moved moved to the first position. Everything is fine.
After Windows theme was changed, the columns positions are preserved, however, the contents are no longer correct.
Currently I overcome the issue by simply recreating the columns manually in my custom CM_RECREATEWND handler. Is it a bug? It it a good solution to recreate columns or is there a better way?
I'm using Delphi10 but the same behavior was observed in the previous versions as well.
I'll post my workaround in case anyone needs a quick fix for this bug. Just include this unit as a last used unit in a Form's uses list.
unit LVFix;
interface
uses
Winapi.Windows, Winapi.Messages, System.Classes, System.UITypes,
Vcl.Controls, Vcl.ComCtrls;
type
TListView = class(Vcl.ComCtrls.TListView)
strict private
type
TColumnRec = record
Alignment: TAlignment;
AutoSize: Boolean;
Caption: String;
ImageIndex: TImageIndex;
MaxWidth, MinWidth, Width: TWidth;
Tag: Integer;
ID: Integer;
end;
var
FSavedCols: TArray<TColumnRec>;
FSavedColOrder: TArray<Integer>;
private
procedure SaveColumnState;
procedure RestoreColumnState;
protected
procedure CMRecreate(var M: TMessage); message CM_RECREATEWND;
end;
implementation
uses
Winapi.CommCtrl;
{ TListView }
procedure TListView.CMRecreate(var M: TMessage);
begin
SaveColumnState;
inherited;
RestoreColumnState;
end;
procedure TListView.RestoreColumnState;
var
I: Integer;
begin
Items.BeginUpdate; //lock to prevent unnecessary events firing
try
//recreate columns
Columns.Clear;
for I := 0 to High(FSavedCols) do
begin
with Columns.Add do
begin
Alignment := FSavedCols[I].Alignment;
AutoSize := FSavedCols[I].AutoSize;
Caption := FSavedCols[I].Caption;
ImageIndex := FSavedCols[I].ImageIndex;
MinWidth := FSavedCols[I].MinWidth;
MaxWidth := FSavedCols[I].MaxWidth;
Width := FSavedCols[I].Width;
Tag := FSavedCols[I].Tag;
end;
end;
//restore column order
if Length(FSavedColOrder) <> 0 then
ListView_SetColumnOrderArray(Handle, Columns.Count, PInteger(FSavedColOrder));
finally
Items.EndUpdate;
end;
end;
procedure TListView.SaveColumnState;
var
R: LongBool;
I: Integer;
J: Integer;
T: TColumnRec;
begin
//save column order
SetLength(FSavedColOrder, Columns.Count);
R := ListView_GetColumnOrderArray(Handle, Columns.Count, PInteger(FSavedColOrder));
if not R then
SetLength(FSavedColOrder, 0);
//save original columns in original order
SetLength(FSavedCols, Columns.Count);
for I := 0 to Columns.Count - 1 do
begin
FSavedCols[I].Alignment := Columns[I].Alignment;
FSavedCols[I].AutoSize := Columns[I].AutoSize;
FSavedCols[I].Caption := Columns[I].Caption;
FSavedCols[I].ImageIndex := Columns[I].ImageIndex;
FSavedCols[I].MinWidth := Columns[I].MinWidth;
FSavedCols[I].MaxWidth := Columns[I].MaxWidth;
FSavedCols[I].Width := Columns[I].Width;
FSavedCols[I].Tag := Columns[I].Tag;
FSavedCols[I].ID := Columns[I].ID;
end;
for I := 0 to High(FSavedCols) - 1 do
for J := I + 1 to High(FSavedCols) do
if FSavedCols[J].ID < FSavedCols[I].ID then
begin
T := FSavedCols[I];
FSavedCols[I] := FSavedCols[J];
FSavedCols[J] := T;
end;
end;
end.

Call Procedure on Separate Unit with Timer

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.

Resources