Delphi tray icon (NSStatusItem) for OSX - macos

I am trying to add NSStatusItem in a Delphi application for OSX. Searched for sample code to help me with that but got stuck when defining an interface:
Here is the code:
// Source: https://forums.embarcadero.com/thread.jspa?threadID=108449
unit Unit2;
interface
uses Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation, Macapi.AppKit,
Macapi.Helpers, Macapi.ObjcRuntime, System.TypInfo, FMX.Platform, FMX.Platform.Mac;
type
TFMXTrayItem = class(TOCLocal)
private
NSStatItem : NSStatusItem;
public
constructor Create;
destructor Destroy; override;
function GetObjectiveCClass: PTypeInfo; override;
procedure call_mymethod; cdecl;
end;
implementation
constructor TFMXTrayItem.Create;
var
NSContMenu : NSMenu;
NSContItem : NSMenuItem;
NSStatBar : NSStatusBar;
NSImg : NSImage;
AppBundle : NSBundle;
NSpImg: Pointer;
Path: String;
begin
inherited Create;
NSStatBar := TNSStatusBar.Create;
NSStatBar := TNSStatusBar.Wrap(TNSStatusBar.OCClass.systemStatusBar);
NSStatItem:= NSStatBar.statusItemWithLength(NSVariableStatusItemLength);
NSStatItem.setTarget(GetObjectID);
// Create context menu
NSContMenu := TNSMenu.Create;
NSContMenu := TNSMenu.Wrap(NSContMenu.initWithTitle(StrToNSStr('The caption')));
NSContItem:=TNSMenuItem.Create;
NSContItem:=TNSMenuItem.Wrap(NSContItem.initWithTitle(StrToNSStr('1. menuitem'),sel_getUid(PAnsiChar('call_mymethod')),StrToNSStr('')));
NSContItem.setTarget(GetObjectID);
NSContMenu.addItem(NSContItem);
NSContItem.release;
// Add menu
NSStatItem.retain;
NSStatItem.setHighlightMode(true);
NSStatItem.setMenu(NSContMenu);
NSContMenu.release;
// Get path to dir
AppBundle := TNSBundle.Wrap(TNSBundle.OCClass.mainBundle);
Path:=AppBundle.bundlePath.UTF8String+'/Contents/yourimage16x16.png';
NSpImg := TNSImage.Alloc.initWithContentsOfFile(StrToNSStr(Path));
// Create Icon
NSImg := TNSImage.Create;
NSImg := TNSImage.Wrap(NSpImg);
NSStatItem.setImage(NSImg);
NSImg.release;
end;
destructor TFMXTrayItem.Destroy;
begin
NSStatItem.release;
inherited;
end;
function TFMXTrayItem.GetObjectiveCClass: PTypeInfo;
begin
Result :=TypeInfo(IFMXTrayItem);
end;
procedure TFMXTrayItem.call_properties;
begin
// your event code of the menu item
end;
end.
Does anyone have any idea on how to declare the IFMXTrayItem interface?

Got it to work like this:
type
IFMXTrayItem = interface(NSObject)
['{7d2e4b38-61d9-4cf4-b78b-5f7c4188e9c0}']
procedure call_mymethod; cdecl;
end;
later edit:
Added a GUID to the interface after reading this:
This GUID is used by the compiler to identify uniquely this interface.
Strictly speaking, you can use an interface without the GUID, but you
can’t get very far using them as much of the RTL and most frameworks
that take advantage of interfaces will require that they have a GUID.
So that is a random GUID I generated but if you use this in your code you should generate your own GUID.

Related

How to use smart pointer on default constructor

So, once again I am learning new things and I came across Smart Pointers. I had code
procedure TForm3.BitBtn1Click(Sender: TObject);
var
_StringList: ISmartPointer<TStringList>;
begin
_StringList := TSmartPointer<TStringList>.Create(TStringList.Create);
end;
As you see variable declaration is kinda odd, and simplification is needed. I came across another solution
procedure TForm3.btnDelphiClick(Sender: TObject);
var
_StringList: TStringList;
begin
_StringList := SmartGuard.SmartGuard<TStringList>(TStringList.Create(False));
end;
Sadly, it does not work with parameterless constructor
procedure TForm3.btnDelphiClick(Sender: TObject);
var
_StringList: TStringList;
begin
_StringList := SmartGuard.SmartGuard<TStringList>(TStringList.Create);
end;
[dcc32 Error] Main.pas(47): E2089 Invalid typecast
Am I out of luck here?
P.S. I know some of you would argue I should stick to try..finally block, but this is out of curiosity.
unit SmartGuard;
interface
type
IGuard = interface
['{CE522D5D-41DE-4C6F-BC84-912C2AEF66B3}']
end;
TGuard = class(TInterfacedObject, IGuard)
private
FObject: TObject;
public
constructor Create(AObject: TObject);
destructor Destroy; override;
end;
SmartGuard<T: class> = record
private
FGuard: IGuard;
FGuardedObject: T;
public
class operator Implicit(GuardedObject: T): SmartGuard<T>;
class operator Implicit(Guard: SmartGuard<T>): T;
end;
implementation
uses
{Delphi}
System.SysUtils
{Project}
;
constructor TGuard.Create(AObject: TObject);
begin
FObject := AObject;
end;
destructor TGuard.Destroy;
begin
FObject.Free;
inherited;
end;
{ SmartGuard }
class operator SmartGuard<T>.Implicit(GuardedObject: T): SmartGuard<T>;
begin
Result.FGuard := TGuard.Create(GuardedObject);
Result.FGuardedObject := GuardedObject;
end;
class operator SmartGuard<T>.Implicit(Guard: SmartGuard<T>): T;
begin
Result := Guard.FGuardedObject;
end;
end.
I would love to find a solution that would not require additional "method" calling as in here https://github.com/marcocantu/DelphiSessions/blob/master/DelphiLanguageCodeRage2018/02_SmartPointers/SmartPointerClass.pas e.g. _StringList.Value.Add('foo'); and "special" brackets e.g. _StringList := TSmartPointer<TStringList>.Create(TStringList.Create)();
The compiler needs help disambiguating
TStringList.Create
The compiler doesn't know whether this is a reference to a method, or a call to the method.
Disambiguate by adding parens to indicate that it is a call.
_StringList := SmartGuard.SmartGuard<TStringList>(TStringList.Create());

Where is the 'EnablePinning' property in the ribbon framework's recent items?

The Windows ribbon framework markup supports an EnablePinning attribute for the recent items menu in the application menu:
<ApplicationMenu.RecentItems>
<RecentItems CommandName="MRU" EnablePinning="true" />
</ApplicationMenu.RecentItems>
I expected that there would be a matching property that can be queried/updated at runtime, but I can't find a property key. Does anyone know if there is one, and, if so, what it is?
Alternatively, is there another way to turn pinning on/off at runtime? Neither the element nor its parent support application modes.
TIA
Clarification: What I'm trying to do is enable/disable pinning for the entire menu at runtime. I'm not concerned about the pin states of the individual items.
I'm not sure if you can modify the pinned state from existing entries but it's definitely possible to programmatically query the state and add new items with a specific state using the UI_PKEY_Pinned property:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd940401(v=vs.85).aspx
Wrappers such as the Windows Ribbon Framework for Delphi or the Windows Ribbon for WinForms (.NET) provide an easy access to the API model. This CodeProject article also describes how to query/add recent items using C#.
If you want to change the state during runtime, you could for example query the state of all items, remove them from the list, adjust whetever you need and add them to the list again. Didn't do that yet, could be worth a try however.
Hmm... this will be quite difficult to accomplish as the flag is defined in the XML which will be compiled into a resource file that is linked to the application and then loaded on start up. You could create another resource definition and reload the ribbon if you want to disable/enable the flagging, but that's quite a lot overhead and certainly noticeable from an users perspective as it requires the creation of a new window handle.
I place the recent items by inside UpdateProperty
TRecentItem = class(TInterfacedObject, IUISimplePropertySet)
private
FRecentFile: TSSettings.TRecentFile;
protected
function GetValue(const key: TUIPropertyKey; out value: TPropVariant): HRESULT; stdcall;
public
procedure Initialize(const RecentFile: TSSettings.TRecentFile); safecall;
end;
function TMyForm.UpdateProperty(commandId: UInt32; const key: TUIPropertyKey;
currentValue: PPropVariant; out newValue: TPropVariant): HRESULT;
var
I: Integer;
psa: PSafeArray;
pv: Pointer;
RecentItem: TRecentItem;
begin
if (key = UI_PKEY_RecentItems) then
begin
psa := SafeArrayCreateVector(VT_UNKNOWN, 0, Settings.RecentFiles.Count);
if (not Assigned(psa)) then
Result := E_FAIL
else
begin
for I := 0 to Settings.RecentFiles.Count - 1 do
begin
RecentItem := TRecentItem.NewInstance() as TRecentItem;
RecentItem.Initialize(Settings.RecentFiles[I]);
pv := Pointer(IUnknown(RecentItem));
Check(SafeArrayPutElement(psa, I, pv^));
end;
Result := UIInitPropertyFromIUnknownArray(UI_PKEY_RecentItems, psa, PropVar);
SafeArrayDestroy(psa);
end;
end;
If a pin was changed, I get this command while closing the application menu:
function TMyForm.Execute(commandId: UInt32; verb: _UIExecutionVerb;
key: PUIPropertyKey; currentValue: PPropVariant;
commandExecutionProperties: IUISimplePropertySet): HRESULT; stdcall;
var
Count: Integer;
I: Integer;
Pinned: Boolean;
psa: PSafeArray;
pv: IUnknown;
RecentFile: UInt32;
SimplePropertySet: IUISimplePropertySet;
Value: TPropVariant;
begin
if ((commandId = cmdAppRecentItems)
and Assigned(key) and (key^ = UI_PKEY_RecentItems)
and Assigned(currentValue) and (currentValue^.vt = VT_ARRAY + VT_UNKNOWN)) then
begin
psa := nil;
Result := UIPropertyToIUnknownArrayAlloc(key^, currentValue^, psa);
if (Succeeded(Result)) then
begin
Result := SafeArrayGetUBound(psa, 1, Count);
for I := 0 to Count do
if (Succeeded(Result)) then
begin
Result := SafeArrayGetElement(psa, I, pv);
if (Succeeded(Result) and Assigned(pv)) then
begin
Result := pv.QueryInterface(IUISimplePropertySet, SimplePropertySet);
if (Succeeded(Result)) then
Result := SimplePropertySet.GetValue(UI_PKEY_Pinned, Value);
if (Succeeded(Result)) then
Result := UIPropertyToBoolean(UI_PKEY_Pinned, Value, Pinned);
if (Succeeded(Result)) then
Settings.RecentFiles.SetPinned(I, Pinned);
end;
end;
SafeArrayDestroy(psa);
end;
end
end;
... but I didn't find a documentation of this solution.

Why I don't need call CoInitialize in a thread created inside a COM Thread?

In order to learn multithreading, I've created a thread inside a COM Thread (TRemoteDataModule).
This is my Component Factory:
TComponentFactory.Create(ComServer, TServerConn2, Class_ServerConn2, ciMultiInstance, tmApartment);
Inside the Thread, I didn't needed to Call CoInitialize to use TADOQuery.Create, .Open... .Exec
I read that I need to initialize the COM library on a thread before you call any of the library functions except CoGetMalloc, to get a pointer to the standard allocator, and the memory allocation functions.
But in this case, the absence of CoInitialize didn't brought me any trouble.
Is this related with Thread Model?
Where can I Find the explanation for this subject?
UPDATE:
When I say INSIDE, it means inside the COM method context:
interface
type
TWorker = class(TThread);
TServerConn2 = class(TRemoteDataModule, IServerConn2)
public
procedure Method(); safecall;
end;
implementation
procedure TServerConn2.Method();
var W: TWorker;
begin
W := TWorkerTread.Create(Self);
end;
UPDATE 2:
The TADOConnection used to connect to database are currently being created in the COM Thread context (TThread.Create constructor). Although, TADOConnection.Open and TADOQuery.Create/.Open are both being performed inside TThread.Execute .
UPDATE 3 - Simulacrum
Interface:
type
TServerConn2 = class;
TWorker = class(TThread)
private
FDB: TADOConnection;
FOwner: TServerConn2;
protected
procedure Execute; override;
public
constructor Create(Owner: TServerConn2);
destructor Destroy; override;
end;
TServerConn2 = class(TRemoteDataModule, IServerConn2)
ADOConnection1: TADOConnection;
procedure RemoteDataModuleCreate(Sender: TObject);
private
{ Private declarations }
protected
class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;
procedure CheckException; safecall;
public
User, Pswd, Str: String;
Ok: Boolean;
end;
Implementation:
class procedure TServerConn2.UpdateRegistry(Register: Boolean; const ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;
{ TWorker }
constructor TWorker.Create(Owner: TServerConn2);
begin
inherited Create(False);
FreeOnTerminate := True;
FDB := TADOConnection.Create(nil);
FOwner := Owner;
end;
destructor TWorker.Destroy;
begin
FDB.Free;
FOwner.Ok := True;
inherited;
end;
procedure TWorker.Execute;
var Qry: TADOQuery;
begin
FDB.LoginPrompt := False;
FDB.ConnectionString := FOwner.Str;
FDB.Open(FOwner.User, FOwner.Pswd);
Qry := TADOQuery.Create(nil);
try
Qry.Connection := FDB;
Qry.LockType := ltReadOnly;
Qry.SQL.Text := 'SELECT TOP 1 * FROM MyTable';
Qry.Open;
finally
Qry.Free;
end;
end;
procedure TServerConn2.CheckException;
var W: TWorker;
begin
W := TWorker.Create(Self);
while not Ok do Sleep(100);
end;
procedure TServerConn2.RemoteDataModuleCreate(Sender: TObject);
begin
User := 'user';
Pswd := 'pass';
Str := ADOConnection1.ConnectionString;
end;
initialization
TComponentFactory.Create(ComServer, TServerConn2,
Class_ServerConn2, ciMultiInstance, tmApartment);
end.
UPDATE 4
The error should happen here:
function CreateADOObject(const ClassID: TGUID): IUnknown;
var
Status: HResult;
FPUControlWord: Word;
begin
asm
FNSTCW FPUControlWord
end;
Status := CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result);
asm
FNCLEX
FLDCW FPUControlWord
end;
if (Status = REGDB_E_CLASSNOTREG) then
raise Exception.CreateRes(#SADOCreateError) else
OleCheck(Status);
end;
By somehow (because of TComponentFactory maybe?) CoCreateInstance identifies that TWorker is in the same context than TServerConn2 and don't raise errors?
Either or both of the following might apply:
On a thread not initialized with COM all existing interface pointers keep working until you make a COM API call or otherwise require COM marshalling which then fails detecting an uninitialized thread. That is, your "didn't brought me any trouble" might actually be too early to say.
If any thread in the process calls Co­Initialize­[Ex] with the COINIT_MULTI­THREADED flag, then that not only initializes the current thread as a member of the multi-threaded apartment, but it also says, "Any thread which has never called Co­Initialize­[Ex] is also part of the multi-threaded apartment." - so called impicit MTA thing
The TADOConnection used to connect to database are currently being created in the COM Thread context (TThread.Create constructor). Although, TADOConnection.Open and TADOQuery.Create/.Open are both being performed inside TThread.Execute .
That will not work, for 2 reasons:
TWorker.Create() and TWorker.Execute() will run in different thread contexts. Create() will run in the context of the thread that is calling TServerConn2.CheckException() (which will have already called CoInitialize/Ex() on itself beforehand), but Execute() will run in the context of the TThread thread instead. ADO is apartment threaded, which means its COM interfaces cannot be used across thread/apartment boundaries unless you marshal them, either via the IGlobalInterfaceTable interface or the CoMarshalInterThreadInterfaceInStream() and CoGetInterfaceAndReleaseStream() functions.
even if you did marshal the ADO interfaces, TWorker.Execute() must call CoInitialize/Ex() on itself. EVERY individual thread must initialize COM to establish its threading model before then accessing any COM interfaces. The threading model dictates how COM accesses interfaces (direct or through proxies), whether message queues are used, etc.
So the simple solution to your problem is to NOT create and use the ADO components across thread boundaries at all. Move your TADOConnection into Execute() instead:
constructor TWorker.Create(Owner: TServerConn2);
begin
inherited Create(False);
FreeOnTerminate := True;
FOwner := Owner;
end;
destructor TWorker.Destroy;
begin
FOwner.Ok := True;
inherited;
end;
procedure TWorker.Execute;
var
DB: TADOConnection;
Qry: TADOQuery;
begin
CoInitialize;
try
DB := TADOConnection.Create(nil);
try
DB.LoginPrompt := False;
DB.ConnectionString := FOwner.Str;
DB.Open(FOwner.User, FOwner.Pswd);
Qry := TADOQuery.Create(nil);
try
Qry.Connection := DB;
Qry.LockType := ltReadOnly;
Qry.SQL.Text := 'SELECT TOP 1 * FROM MyTable';
Qry.Open;
finally
Qry.Free;
end;
finally
DB.Free;
end;
finally
CoUninitialize;
end;
end;
When you create an apartment thread using TComponentFactory it calls CoInitialize and CoUnInitialize for you - it's right in the VCL source (System.Win.VCLCom.pas):
procedure TApartmentThread.Execute;
var
msg: TMsg;
Unk: IUnknown;
begin
try
CoInitialize(nil); // *** HERE
try
FCreateResult := FFactory.CreateInstanceLic(FUnkOuter, nil, FIID, '', Unk);
FUnkOuter := nil;
FFactory := nil;
if FCreateResult = S_OK then
CoMarshalInterThreadInterfaceInStream(FIID, Unk, IStream(FStream));
ReleaseSemaphore(FSemaphore, 1, nil);
if FCreateResult = S_OK then
while GetMessage(msg, 0, 0, 0) do
begin
DispatchMessage(msg);
Unk._AddRef;
if Unk._Release = 1 then break;
end;
finally
Unk := nil;
CoUninitialize; // ** AND HERE
end;
except
{ No exceptions should go unhandled }
end;
end;

Invoke methods in Pascal

I am in front of following problem:
My Main programming Language is C++ with the Qt4 Library, but now I have to write a Pascal Wrapper, which should give the possibility to use the functions of a C DLL in Pascal.
Now I want to make it possible to invoke a method from any Pointer. But I can't find a Pascal method to invoke a method. I want something like the QMetaObject::invokeMethod method in QT. I got following code:
unit CgPConnect;
//{$mode objfpc}{$H+}
{$mode delphi}
interface
uses
Classes, SysUtils, dynlibs;
type
Callback = Record
var callbackObject: Pointer;
var objectFunction: string;
end;
CallbackObject = Record
var objectName: string;
var callback: Callback;
end;
MutableObject = Object
var name: string;
var state: string;
var properties: array of VariantMap;
var annotations: array of VariantMap;
end;
PConnect = Class
constructor create(connectorPath: string);
destructor destroy;
private
var hostactionCallbacks: array of CallbackObject;
var mConnectorPath: string;
var mConnectorLibrary: TLibHandle;
function loadConnectorLibrary: Boolean;
public
procedure registerCallbackForHostaction(objectName, objectFunction: string; callbackObject: pointer);
procedure callHostactionCallback(receivedObject :MutableObject);
var mLibraryLoaded: Boolean;
end;
implementation
constructor PConnect.create(connectorPath: string);
begin
mConnectorPath:= connectorPath;
mLibraryLoaded:= false;
//Eventuell noch slash hinzufügen
mLibraryLoaded:= loadConnectorLibrary;
end;
destructor PConnect.destroy;
begin
UnloadLibrary(mConnectorLibrary);
end;
procedure PConnect.registerCallbackForHostaction(objectName, objectFunction: string; callbackObject: pointer);
var c: Callback;
var callbackCount: integer;
begin
if mLibraryLoaded = true then
begin
c.callbackObject:= callbackObject;
c.objectFunction:= objectFunction;
callbackCount:= Length(hostactionCallbacks)+1;
SetLength(hostactionCallbacks, callbackCount);
hostactionCallbacks[callbackCount].objectName:= objectName;
hostactionCallbacks[callbackCount].callback:= c;
end;
end;
procedure PConnect.callHostactionCallback(receivedObject :MutableObject);
var receivedObjectName, objectFunction: string;
var i, count: integer;
var callbackObject: pointer;
begin
if mLibraryLoaded = true then
begin
receivedObjectName:= receivedObject.name;
count:= Length(hostactionCallbacks);
for i:=0 to count do
begin
if hostactionCallbacks[i].objectName = receivedObjectName
begin
objectFunction:= hostactionCallbacks[i].callback.objectFunction;
callbackObject:= hostactionCallbacks[i].callback.callbackObject;
if callbackObject <> 0 then
//INVOKE METHOD (objectFunction) OF OBJECT (callbackObject)
end;
end;
end;
end;
end.
I would be happy about a fast answer :)
You cannot directly and portably call a C++ method from Pascal. If your callbackfunction is a C++ object, forget it.
Otherwise fill a TMethod object and cast that to the proper "procedure of object" declaration. Don't forget the calling convention.
For more bizarre solutions you might want to have a look at (RemObjects') Pascalscript.
P.s. this is the same that you can't even call a C++ method reliably from another C++ compiler. It is not Pascal vs C++ per se.

Delphi: Is system menu opened?

I Delphi, I need a function which determinates if the system menu (resp. window menu, the menu that appears when the icon is clicked) is opened. The reason is that I am writing a anti-keylogger functionality which sends garbage to the current active editcontrol (this also prevents keylogger which read WinAPI messages to read the content). But if system-menu is opened, the editcontrol STILL has the focus, so the garbage will invoke shortcuts.
If I use message WM_INITMENUPOPUP in my TForm1, I can determinate when the system menu opens, but I wish that I do not have to change the TForm, since I want to write a non visual component, which does not need any modifications at the TForm-derivate-class itself.
//I do not want that solution since I have to modify TForm1 for that!
procedure TForm1.WMInitMenuPopup(var Message: TWMInitMenuPopup);
begin
if message.MenuPopup=getsystemmenu(Handle, False) then
begin
SystemMenuIsOpened := true;
end;
end;
TApplicaton.HookMainWindow() does not send the WM_INITMENUPOPUP to my hook function.
function TForm1.MessageHook(var Msg: TMessage): Boolean;
begin
Result := False;
if (Msg.Msg = WM_INITMENUPOPUP) then
begin
// Msg.Msg IS NEVER WM_INITMENUPOPUP!
if LongBool(msg.LParamHi) then
begin
SystemMenuIsOpened := true;
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.HookMainWindow(MessageHook);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Application.UnhookMainWindow(MessageHook);
end;
Even after very long research I did not found any information about how to query if the system-menu is opened or not. I do not find any way to determinate the opening+closing of that menu.
Has someone a solution for me please?
Regards
Daniel Marschall
Application.HookMainWindow doesn't do what you seem to think. It hooks the hidden application window, not the main form. To intercept WM_INITMENUPOPUP on a specific form, all you need to do is write a handler for it, as you have seen.
To do this generically for any owner form of a component, you could assign WindowProc property of the form to place the hook:
unit FormHook;
interface
uses
Windows, Classes, SysUtils, Messages, Controls, Forms;
type
TFormMessageEvent = procedure(var Message: TMessage; var Handled: Boolean) of object;
TFormHook = class(TComponent)
private
FForm: TCustomForm;
FFormWindowProc: TWndMethod;
FOnFormMessage: TFormMessageEvent;
protected
procedure FormWindowProc(var Message: TMessage); virtual;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property OnFormMessage: TFormMessageEvent read FOnFormMessage write FOnFormMessage;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TFormHook]);
end;
procedure TFormHook.FormWindowProc(var Message: TMessage);
var
Handled: Boolean;
begin
if Assigned(FFormWindowProc) then
begin
Handled := False;
if Assigned(FOnFormMessage) then
FOnFormMessage(Message, Handled);
if not Handled then
FFormWindowProc(Message);
end;
end;
constructor TFormHook.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FFormWindowProc := nil;
FForm := nil;
while Assigned(AOwner) do
begin
if AOwner is TCustomForm then
begin
FForm := TCustomForm(AOwner);
FFormWindowProc := FForm.WindowProc;
FForm.WindowProc := FormWindowProc;
Break;
end;
AOwner := AOwner.Owner;
end;
end;
destructor TFormHook.Destroy;
begin
if Assigned(FForm) and Assigned(FFormWindowProc) then
begin
FForm.WindowProc := FFormWindowProc;
FFormWindowProc := nil;
FForm := nil;
end;
inherited Destroy;
end;
end.
You could then use this component on a form:
procedure TForm1.FormHook1FormMessage(var Message: TMessage; var Handled: Boolean);
begin
case Message.Msg of
WM_INITMENUPOPUP:
...
end;
end;
The problem might be that if the form has any other components which do the same thing then you need to make sure that unhooking happens in reverse order (last hooked, first unhooked). The above example hooks in the constructor and unhooks in the destructor; this seems to work even with multiple instances on the same form.
If you don't want any modifications to TForm-derivate-class, why don't try pure Windows API way to implement your current solution, that is, use SetWindowLongPtr() to intercept the WM_INITMENUPOPUP message. Delphi VCL style to intercept messages is just a wrapper of this Windows API function actually.
For that purpose, use SetWindowLongPtr() to set a new address for the window procedure and to get the original address of the window procedure, both at one blow. Remember to store the original address in a LONG_PTR variable. In 32-bit Delphi, LONG_PTR was Longint; supposing 64-bit Delphi will have been released in the future, LONG_PTR should be Int64; you can use $IFDEF directive to distinguish them as follows:
Type
{$IFDEF WIN32}
PtrInt = Longint;
{$ELSE}
PtrInt = Int64;
{$ENDIF}
LONG_PTR = PtrInt;
The value for nIndex parameter to be used for this purpose is GWLP_WNDPROC. Also, pass the new address for the window procedure to dwNewLong parameter, e.g. LONG_PTR(NewWndProc). The NewWndProc is a WindowProc Callback Function that processes messages, it is where your put your intercept criteria and override the default handling of the message you are going to intercept. The callback function can be any name, but the parameters must follow the WindowProc convention.
Note that you must call CallWindowProc() to pass any messages not processed by the new window procedure to the original window procedure.
Finally, you should call SetWindowLongPtr() again somewhere in your code to set the address of modified/new window procedure handler back to the original address. The original address has been saved before as mentioned above.
There was a Delphi code example here. It used SetWindowLong(), but now Microsoft recommends to use SetWindowLongPtr() instead to make it compatible with both 32-bit and 64-bit versions of Windows.
SetWindowLongPtr() didn't exist in Windows.pas of Delphi prior to Delphi 2009. If you use an older version of Delphi, you must declare it by yourself, or use JwaWinUser unit of JEDI API Library.
Not tried this myself, but give this a shot:
Use GetMenuItemRect to get the rect for item 0 of the menu returned by GetSystemMenu.
I (assume!) GetMenuItemRect should return 0 if the system menu is not open (because system could not know the rect of the menu item unless it is open?) If the result is non-zero, check if the coords returned are possible for the given screen resolution.
If you have the time, you could look into AutoHotKey's source code to see how to monitor when system menu is open/closed.

Resources