Sunday, 6 September 2020

Delphi and the WinRT API

WinRT

The WinRT (Windows Runtime) API was introduced with Windows 8, although it’s fair to say that most of us have tried our level best to forget that particular Windows release and pretend it never happened. Anyway, Microsoft has been trying to convince us that new Windows APIs will be introduced in the WinRT API and what we normally refer to as the Windows API might well remain as it is. We’ll see how true that becomes. The WinRT API reference can be found documented here.

The nice thing about WinRT is that it is not another step on the .NET journey; indeed it is nothing to do with .NET whatsoever. WinRT is implemented as native code and it is made available to whatever language wishes to consume it by language projections. Microsoft offers a C# language projection to consume it in .NET but it also offers a native C++ language projection (C++/WinRT).

Each WinRT class implements various interfaces to offer up methods and properties to call. There may also be static methods (i.e. class methods) and factory methods through which you can create an instance of the class. The Microsoft language projections make all the methods implemented through all the supported interfaces directly available once you create an instance of a WinRT class.

WinRT metadata

Anyone else can make their own language projections by analysing the API metadata (which are available in .winmd files in the C:\Windows\System32\WinMetadata folder). Metadata files are rather like COM type libraries in that they describe all the WinRT classes and methods etc., but they are actually files that share the same format as Win32 executables and .NET assemblies: PE files. You can find detailed information on these files from Microsoft here. Microsoft’s C++ .winmd file parser is open-sourced and available on github.

WinRT and Delphi

Embarcadero folks started looking at how to consume the WinRT API back in 2011 and Thom Gerdes wrote a few posts on how got on. He talks about these metadata files in this old post available on the Wayback Machine.

So, do we have a Delphi language projection? Well, kinda... We have import units that pull in a good chunk of the WinRT API, but things aren’t quite as transparent and trivial to use as they are in C++and C#. However, depending on how persistent we are we can get results. Indeed there are some uses of the WinRT API in the Delphi / C++Builder RTL; it is used for Windows desktop notifications and for share contracts.

This post looks into how we might use the WinRT API directly to perform a simple task, namely to trigger a Windows notification message. This might seem an odd thing to do, given the RTL already covers this quite nicely in the TNotificationCenter class but it serves as a simple, visual example.

If you want you can also jump right to a sample from Marco Cantu that does the same thing, but that sample has more code in that somewhat camouflages the detail of what is required for the job. Marco’s code is on github and is mentioned a bit in the post A tale of 3 APIs: VCL integration with WinAPI, COM & ShellAPI, WinRT, which itself is a post summarising a webinar of the same title available for replay.

The WinRT import units were added to the RTL in RAD Studio 10 Seattle, so far as I can tell. You can find them in this subfolder of the installation folder: source\rtl\win\winrt. These were extended in 10.1 Berlin, and extended again in 10.3 Rio. In the 10.2 Tokyo timeframe Al Mannarino wrote a post on using the WinRT support.

WinRT and toast notifications in Delphi

To create a toast requires the toast content to be represented in XML. Something like this will do the trick:

var LToastXml := '<toast duration="short">' +
                   '<visual>' +
                     '<binding template="ToastText02">' +
                       '<text id="1">Windows 10 Notification</text>' +
                       '<text id="2">RAD Studio 10.4 Sydney Update 1</text>' +
                     '</binding>' +
                   '</visual>' +
                 '</toast>';

To pass this into a toast notification we need to load it into a Windows.Data.Xml.Dom.XmlDocument class. In the Winapi.DataRT.pas unit there is a representation of this class in the proxy class TXml_Dom_XmlDocument, which looks like this:

  // Windows.Data.Xml.Dom.XmlDocument
  // WinRT Only
  // WhiteListed
  // Implements: Windows.Data.Xml.Dom.IXmlDocument
  // Implements: Windows.Data.Xml.Dom.IXmlNode
  // Implements: Windows.Data.Xml.Dom.IXmlNodeSerializer
  // Implements: Windows.Data.Xml.Dom.IXmlNodeSelector
  // Implements: Windows.Data.Xml.Dom.IXmlDocumentIO
  // Implements: Windows.Data.Xml.Dom.IXmlDocumentIO2
  // Statics: "Windows.Data.Xml.Dom.IXmlDocumentStatics"
  // Instantiable: "Xml_Dom_IXmlDocument"
  TXml_Dom_XmlDocument = class(TWinRTGenericImportSI<Xml_Dom_IXmlDocumentStatics, Xml_Dom_IXmlDocument>)
  public
    // -> Xml_Dom_IXmlDocumentStatics
    class function LoadFromUriAsync(uri: IUriRuntimeClass): IAsyncOperation_1__Xml_Dom_IXmlDocument; overload; static; inline;
    class function LoadFromUriAsync(uri: IUriRuntimeClass;
      loadSettings: Xml_Dom_IXmlLoadSettings): IAsyncOperation_1__Xml_Dom_IXmlDocument; overload; static; inline;
    class function LoadFromFileAsync(&file: IStorageFile): IAsyncOperation_1__Xml_Dom_IXmlDocument; overload; static; inline;
    class function LoadFromFileAsync(&file: IStorageFile;
      loadSettings: Xml_Dom_IXmlLoadSettings): IAsyncOperation_1__Xml_Dom_IXmlDocument; overload; static; inline;
  end;

There is a static Create method available in a proxy class such as this, which is expected to create the corresponding WinRT class and return an interface reference to the instantiable class Xml_Dom_IXmlDocument.

Disappointingly, while this works with many WinRT proxy classes it does not work with this one. This is because the Xml_Dom_IXmlDocument interface, which is in the WinAPI.CommonTypes.pas unit rather than the Winapi.DataRT.pas unit has lost an important attribute. It should have an attribute defined like this:

[WinRTClassNameAttribute(SXml_Dom_XmlDocument)]

where SXml_Doc_XmlDocument is defined like this in Winapi.CommonNames.pas:

SXml_Dom_XmlDocument = 'Windows.Data.Xml.Dom.XmlDocument';

But alas all the interfaces in Winapi.CommonTypes seem to have lost attributes like this. As a consequence a function like this one does not work at runtime:

unit XMLHelper;

interface

uses
  Winapi.CommonTypes;

function GetXmlDocument: Xml_Dom_IXmlDocument;

implementation

uses
  Winapi.DataRT;

function GetXmlDocument: Xml_Dom_IXmlDocument;
begin
  // This fails because Xml_Dom_IXmlDocument in Winapi.CommonTypes does not have this attribute
  // as the other interfaces involved in XmlDocument from Winapi.DataRT do:
  // [WinRTClassNameAttribute(SXml_Dom_XmlDocument)]
  // Might work in 10.5...? If we cross our fingers...
  Result := TXml_Dom_XmlDocument.Create
end;

end.

It triggers an exception due to the lack of attribute, as the relevant code therefore does not know which WinRT class to instantiate.

By the way, I have made enquiries and I am reliably informed that this issue is known within Embarcadero and is recorded in an internal JIRA.

All that notwithstanding, it turns out that we can do what that proxy class wanted to do for ourselves, manually filling in the missing information. Here’s an alternative implementation of the same function:

unit XMLHelper;

interface

uses
  Winapi.CommonTypes;

function GetXmlDocument: Xml_Dom_IXmlDocument;

implementation

uses
  System.Win.WinRT,
  Winapi.WinRT,
  Winapi.CommonNames;

function GetXmlDocument: Xml_Dom_IXmlDocument;
begin
  var LWinRTClassName := TWindowsString.Create(SXml_Dom_XmlDocument);
  var LWinRTClassNameHString: HString := LWinRTClassName;
  var LXmlDocumentInsp := TWinRTImportHelper.CreateInstance(TypeInfo(Xml_Dom_IXmlDocument), LWinRTClassNameHString);
  Result := LXmlDocumentInsp as Xml_Dom_IXmlDocument;
end;

end.

This code takes the aforementioned WinRT class name and creates a HSTRING version of it. HSTRING is a string handle and is the string type of choice in the WinRT API (documentation available here).

Some RTL helper can take the type information for the target instantiable interface along with the WinRT class name and do the ‘magic’ required to create the WinRT object instance and return us an IInspectable interface reference to it. This interface reference will gladly offer up the sought Xml_Dom_IXmlDocument interface when asked.

If we wanted to do the WinRT object instantiation manually, using Windows native routines then we can swap out the RTL helper for a WinRT API primitive RoActivateInstance. As you can see it actually slightly reduces the required typing:

function GetXmlDocument: Xml_Dom_IXmlDocument;
begin
  var LWinRTClassName := TWindowsString.Create(SXml_Dom_XmlDocument);
  var LXmlDocumentInsp: IInspectable;
  RoActivateInstance(LWinRTClassName, LXmlDocumentInsp);
  Result := LXmlDocumentInsp as Xml_Dom_IXmlDocument;
end;

OK, so now we have a couple of options for instantiating a WinRT XML document and the helper can be called:

var LXmlDocument := GetXmlDocument;

In addition to the XML defining the toast notification we also need to use the ToastNotificationManager to create a ToastNotifier object via which we will raise the toast notification. We maybe want to react to the user clicking the toast notification so we might want an event handler involved as well.

uses
  System.Hash,
  System.Win.WinRT,
  Winapi.UI.Notifications,
...
const
  AppId = 'Blong.WinRT.';

function GetAppUserModelID: string;
begin
  Result := AppID + THashBobJenkins.GetHashString(ParamStr(0))
end;
...
  var LWSAppID := TWindowsString.Create(GetAppUserModelID);
  var LToastNotifier := TToastNotificationManager.Statics.CreateToastNotifier(LWSAppID);
  DoToast(LXmlDocument, LToastXml, LToastNotifier, OnToastActivated);
...
procedure TfrmMain.OnToastActivated(Sender: IToastNotification; const Args: IInspectable);
begin
  ShowMessage('Hello from Delphi!');
end;

That sets us up passing everything into the DoToast routine. It looks like this:

procedure DoToast(const XmlDoc: Xml_Dom_IXmlDocument; const ToastXML: string;
  const ToastNotifier: IToastNotifier; ActivatedEventHandler: TToastActivatedEvent);
begin
  (XmlDoc as Xml_Dom_IXmlDocumentIO).LoadXml(TWindowsString.Create(ToastXml));
  // If we want to view the XML we can do this:
  //var LWSXML := (XmlDoc as Xml_Dom_IXmlNodeSerializer).GetXml;
  //var LXML := TWindowsString.HStringToString(LWSXML);
  var LToastNotification := TToastNotification.Factory.CreateToastNotification(XmlDoc);
  var LDelegateActivated := TToastActivated.Create(ActivatedEventHandler);
  LToastNotification.add_Activated(LDelegateActivated);
  ToastNotifier.Show(LToastNotification);
end;

The first statement loads in the XML to the XML document. You’ll notice that since we are working from an interface reference, then in order to call other methods in the WinRT class we have to use the relevant interface reference type that defines the methods we wish to use. The XML document is then passed along while creating a ToastNotification object.

The passed in event handler method is handed to a new delegate object that is given to the ToastNotification as a handler for the Activated event. Finally the ToastNotifier is used to display the notification.

The only thing left not filled in from what we’ve seen so far is the delegate class, which looks like this:

type
  TToastActivatedEvent = procedure(Sender: IToastNotification; const Args: IInspectable) of object;

  // Here we have 2 interfaces implemented for the delegate
  TToastActivated = class(TInspectableObject,
    TypedEventHandler_2__IToastNotification__IInspectable,
    TypedEventHandler_2__IToastNotification__IInspectable_Delegate_Base)
  private
    FToastActivated: TToastActivatedEvent;
  public
    constructor Create(EventHandler: TToastActivatedEvent);
    procedure Invoke(Sender: IToastNotification; Args: IInspectable); safecall;
  end;
...
constructor TToastActivated.Create(EventHandler: TToastActivatedEvent);
begin
  inherited Create;
  FToastActivated := EventHandler;
end;

procedure TToastActivated.Invoke(Sender: IToastNotification; Args: IInspectable);
begin
  if Assigned(FToastActivated) then
    FToastActivated(Sender, Args)
end;

Notice the dual interfaces implemented in this delegate class. This is 'a thing' that you have to do with Delphi interfaces that represent WinRT instantiated template interface types. The interface type that describes itself as the base type has the IID that you need to be bringing in for the delegate to work, but the other type (inherited from the base one) defines the method(s) you need to implement...

If you run this sort of code you do indeed get a desktop notification sweeping in from the bottom right edge of the screen:

Hmm, but something is missing….. The general look of these notifications is that, at the very least, they show the application icon. Where is it?

Well, there is a requirement that must be met in order to get this to appear as discussed in this – you need to create a shortcut (as in a .lnk file). This shortcut file must have been set up with an Application User Model ID (more information here) assigned, which is the app-specific hashed string returned by the GetAppKey function above. This is all illustrated with some C++ code in this Microsoft page, How to enable desktop toast notifications through an AppUserModelID. If we implement a routine to ensure a suitable shortcut exists (code below), called on app start, then the notification does manage to pick up the app icon.

uses
  System.SysUtils,
  System.Hash,
  Winapi.Windows,
  Winapi.ShlObj,
  Winapi.ActiveX,
  Winapi.KnownFolders,
  Winapi.PropKey,
  Winapi.PropSys;

const
  AppId = 'Blong.WinRT.';

function GetAppUserModelID: string;
begin
  Result := AppID + THashBobJenkins.GetHashString(ParamStr(0))
end;

function CreateShortcut: Boolean;
var
  Path: PChar;
  LBufferPath: array [0..MAX_PATH] of Char;
  LShellLink: IShellLink;
  LAppIdPropVar: TPropVariant;
  LFindData: TWin32FindData;
begin
  Result := False;
  if Succeeded(SHGetKnownFolderPath(FOLDERID_Programs, 0, 0, Path)) then
  begin
    var ShortcutPath := string(Path) + '\' + ChangeFileExt(ExtractFileName(ParamStr(0)), '.lnk');
    if FileExists(ShortcutPath) and
       Succeeded(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, LShellLink)) and
       Succeeded((LShellLink as IPersistFile).Load(PChar(ShortcutPath), 0)) and
       Succeeded(LShellLink.GetPath(LBufferPath, MAX_PATH, LFindData, 0)) and
       (LBufferPath = ParamStr(0)) then
        Result := True
    else
      if Succeeded(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER, IShellLink, LShellLink)) then
      begin
        LShellLink.SetPath(PChar(ParamStr(0)));
        LShellLink.SetWorkingDirectory(PChar(ExtractFilePath(ParamStr(0))));
        var LPropertyStore := LShellLink as IPropertyStore;
        ZeroMemory(@LAppIdPropVar, SizeOf(LAppIdPropVar));
        if Succeeded(InitPropVariantFromString(PChar(GetAppUserModelID), LAppIdPropVar)) then
          try
            if Succeeded(LPropertyStore.SetValue(PKEY_AppUserModel_ID, LAppIdPropVar)) and
               Succeeded(LPropertyStore.Commit) then
            begin
              var LSaveLink := True;
              if FileExists(ShortcutPath) then
                LSaveLink := System.SysUtils.DeleteFile(ShortcutPath);
              if LSaveLink then
                Result := Succeeded((LShellLink as IPersistFile).Save(PChar(ShortcutPath), True));
            end;
          finally
            PropVariantClear(LAppIdPropVar);
          end;
      end;
  end;
end;

Note that because the RTL supports Windows 10 desktop notifications (aka toast notifications), then it does all this stuff as well (look in System.Win.Notification.pas). The GetAppUserModelID function is much the same as TNotificationCenterWinRT.GetAppNotificationKey and CreateShortcut is very similar to TNotificationCenterWinRT.CreateShortcut.

I am hoping to come back to this general subject soon to look at doing more with WinRT, but this is time-dependent and I have a few other things to write about first. But hopefully this has given a general idea of how you can make use of WinRT API from a Delphi application.

2 comments:

  1. Great post. I have used it to work out toast notifications... however I am not able to set ExpirationTime. Do you have any hints for it?

    ReplyDelete
    Replies
    1. Interesting. I haven't yet noticed how to customise expiration time...

      Delete