Search code examples
windowsdelphinotificationsdelphi-10.1-berlin

How to create toast notification in delphi with ToastGeneric


I am developing on Desktop with delphi. I would like to create toast notification with ToastGeneric type notification LToastFactory.CreateToastNotification(LXMLTemplate);

In addition , i am using an xml as in https://learn.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-adaptive-interactive-toasts

My question is how can i make delphi to accept this xml, i haven't found a way to convert that string into Xml_Dom_IXmlDocument type.


Solution

  • I had the same problem.

    My objective was to create a Toast notification from Delphi that uses toastGeneric. However, I could not find any examples in any language that do this by manipulating the xml - all the examples use classes that do not appear to be accessible from Delphi.

    My solution was to create a standard template and then overwrite the xml in that standard template with the xml required for a custom template. Below is some Delphi code that should give you the idea. It is a complete console app. This code compiles under Delphi 10.2 Tokyo. There may be some tweaks required for earlier versions. You will be interested in the OverwriteToastTemplateXML function.

    My code was based around a standard toast template example referenced in the final comment in Marco Cantu's blog post here: http://blog.marcocantu.com/blog/2015-june-windows10-notifications-vcl-winrt.html

    Note the reference in my XML to a 'hero' image jpg file. For the notification to work fully, make sure you have a jpg at c:\notifications\hero.jpg, or comment out that line in the xml.

    As well as converting an XML string into a custom toast template, the code also converts a toast template back into a string, which is useful for debugging - this is the ToastTemplateToString function. These are my main functional modifications to the original example. For my own understanding, I have also changed the structure of the example code, so that variable scope and how each line of code relates to the others is also more apparent.

    Let me know if this works for you - I have found that toast notification are hard work from Delphi!

    Cheers

    Steve

    program ConsoleNotifier;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      System.SysUtils,
    
      // Required to create the Toast Notification
      WinAPI.WinRT,
      WinAPI.DataRT,
      WinAPI.UI.Notifications,
      WinAPI.ActiveX,
      WinAPI.CommonTypes,
    
      // Required for creating the Desktop Shell Link
      WinAPI.PropKey,
      WinAPI.PropSys,
      WinAPI.ShlObj,
      System.Win.ComObj,
      Windows
      ;
    
    function CreateDesktopShellLink(const TargetName: string): Boolean;
    
      function GetStartMenuFolder: string;
      var
        Buffer: array [0 .. MAX_PATH - 1] of Char;
      begin
        Result := '';
        GetEnvironmentVariable(PChar('APPDATA'), Buffer, MAX_PATH - 1);
        Result := Buffer + '\Microsoft\Windows\Start Menu\Programs\Desktop Delphi Toasts App.lnk';
      end;
    
    var
      IObject: IUnknown;
      ISLink: IShellLink;
      IPFile: IPersistFile;
      LinkName: string;
    
      LStore: WinAPI.PropSys.IPropertyStore;
      LValue: TPropVariant;
    begin
      Result := False;
    
      IObject := CreateComObject(CLSID_ShellLink);
      ISLink := IObject as IShellLink;
      IPFile := IObject as IPersistFile;
      LStore := IObject as WinAPI.PropSys.IPropertyStore;
    
      with ISLink
      do begin
        SetPath(PChar( ParamStr(0) ));
      end;
      ISLink.SetArguments(PChar(''));
    
      if Succeeded(InitPropVariantFromStringAsVector(PWideChar('Delphi.DesktopNotification.Sample'), LValue))
      then begin
        if Succeeded(LStore.SetValue(PKEY_AppUserModel_ID, LValue))
        then LStore.Commit;
      end;
    
      LinkName := GetStartMenuFolder;
    
      if not FileExists(LinkName)
      then
        if IPFile.Save(PWideChar(LinkName), True) = S_OK
        then Result := True;
    end;
    
    function HStr( Value:String ): HString;
    begin
      if NOT Succeeded(
        WindowsCreateString(PWideChar(Value), Length(Value), Result)
      )
      then raise Exception.CreateFmt('Unable to create HString for %s', [ Value ] );
    end;
    
    function ToastTemplateToString( Const Template:Xml_Dom_IXmlDocument ): String;
    
      function HStringToString(Src: HSTRING): String;
      var
        c: Cardinal;
      begin
        c := WindowsGetStringLen(Src);
        Result := WindowsGetStringRawBuffer(Src, @c);
      end;
    
    begin
      Result := HStringToString(
        ( Template.DocumentElement as Xml_Dom_IXmlNodeSerializer ).GetXml
      );
    end;
    
    function GetFactory( Const Name:String; Const GUID:String ): IInspectable;
    var
      FactoryHString : HString;
      FactoryGUID    : TGUID;
    begin
      FactoryHString := HStr( Name );
      try
        FactoryGUID := TGUID.Create(GUID);
    
        if NOT Succeeded(
          RoGetActivationFactory(FactoryHString, FactoryGUID, Result)
        )
        then raise Exception.CreateFmt('Error creating factory: %s %s', [ Name, GUID ] );
      finally
        WindowsDeleteString( FactoryHString );
      end;
    end;
    
    procedure OverwriteToastTemplateXML( Const Template: Xml_Dom_IXmlDocument; Const XML:String );
    var
      hXML: HSTRING;
    begin
      hXML := HStr( XML );
      try
        (Template as Xml_Dom_IXmlDocumentIO).LoadXml( hXML );
      finally
        WindowsDeleteString( hXML );
      end;
    end;
    
    procedure SteveNotification( Const AppID:String; Const XML:String );
    var
      ToastNotificationManagerStatics : IToastNotificationManagerStatics;
      ToastTemplate                   : Xml_Dom_IXmlDocument;
      LToastNotification              : IToastNotification;
      ToastNotificationManagerFactory : IInspectable;
      ToastNotificationFactory        : IInspectable;
      hAppID                          : HString;
    begin
      ToastNotificationManagerFactory := GetFactory( sToastNotificationManager, '{50AC103F-D235-4598-BBEF-98FE4D1A3AD4}' );
      ToastNotificationManagerStatics := IToastNotificationManagerStatics(ToastNotificationManagerFactory);
      ToastTemplate := ToastNotificationManagerStatics.GetTemplateContent(ToastTemplateType.ToastText01);
    
      OverwriteToastTemplateXML( ToastTemplate, XML );
    
      WriteLn( 'XML: ', ToastTemplateToString( ToastTemplate ) );
    
      ToastNotificationFactory := GetFactory( SToastNotification, '{04124B20-82C6-4229-B109-FD9ED4662B53}' );
    
      LToastNotification := IToastNotificationFactory(ToastNotificationFactory).CreateToastNotification(ToastTemplate);
    
      hAppID := HStr( AppID );
      try
        ToastNotificationManagerStatics
        .CreateToastNotifier( hAppID )
        .Show(LToastNotification);
      finally
        WindowsDeleteString( hAppID );
      end;
    end;
    
    Const
      AppID = 'My Application ID';
      XML   = '<toast activationType="protocol" launch="http://www.ecutek.com" >'
            + '  <visual>'
            + '    <binding template="ToastGeneric">'
            + '      <text>Body Text ABC</text>'
            + '      <text>More Text</text>'
            + '      <image placement="hero" src="file:///c:\notifications\hero.jpg"/>'
            + '    </binding>'
            + '  </visual>'
            + '  <actions>'
            + '    <action content="Open Google" activationType="protocol" arguments="http://www.google.com" />'
            + '  </actions>'
            + '</toast>';
    
    var
      c : char;
    begin
      try
        if TOSVersion.Major < 10
        then raise Exception.Create('Windows 10 Required');
    
        RoInitialize(RO_INIT_MULTITHREADED);
    
        CreateDesktopShellLink( ParamStr(0) );
    
        SteveNotification( AppID, XML );
    
        // Wait for a KeyPress
        Read( c ); write( c );
      except
        on E: Exception do
          Writeln(E.ClassName, ': ', E.Message);
      end;
    end.