delphidelphi-11-alexandria

How to make Windows notification display application title instead of EXE filename?


I'm using the code below to send Windows notifications. It's working, but the notification window's title is showing the exe filename instead of the name of my application as defined in Application.Title.

I want the notification to display 'My application' on the title, but instead, it's displaying 'notif', as displayed on the image below :

enter image description here

What am I missing?

Project source :

program notif;

uses
  Vcl.Forms,
  unit1 in '..\unit1.pas' {Form1};

{$R *.res}

begin
  Application.Initialize;
  Application.Title := 'My application';
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Unit source :

unit unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Notification;

type
  TForm1 = class(TForm)
    Button1: TButton;
    NotificationCenter1: TNotificationCenter;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  MyNotification: TNotification; //Defines a TNotification variable
  begin
  MyNotification := NotificationCenter1.CreateNotification; //Creates the notification
  try
    MyNotification.Name := 'My application name'; //Defines the name of the notification.
    MyNotification.Title := 'Title'; //Defines the name that appears when the notification is presented.
    MyNotification.AlertBody := 'Hello, im here'; //Defines the body of the notification that appears below the title.

    NotificationCenter1.PresentNotification(MyNotification); //Presents the notification on the screen.
  finally
    MyNotification.Free; //Frees the variable
  end;
end;

end.

Solution

  • Important update

    The notificaiton library has now been published on a GitHub repository by me, with updated code and lots of new functions, such as notify events, controls, buttons and more.

    The library with any required dependencies included can be found here.

    The original response with the first iteration of the library is unchanged below this update.

    Original response

    You can use WinRT for this with the help of Winapi.UI.Notifications. I have made a custom class to make this easier.

    Still a work in progress, so not everything works as expected, but It should be able to do the basics.

    Example:

    gif animation of notification

    {***********************************************************}
    {                Codruts Notification Manager               }
    {                                                           }
    {                        version 1.0                        }
    {                                                           }
    {                                                           }
    {                                                           }
    {                                                           }
    {                                                           }
    {              Copyright 2023 Codrut Software               }
    {***********************************************************}
    
    {$SCOPEDENUMS ON}
    
    unit Cod.NotificationManager;
    
    interface
      uses
      // System
      Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Forms,
    
      // Windows RT (Runtime)
      Winapi.Winrt,
      Winapi.Winrt.Utils,
      Winapi.DataRT,
      Winapi.UI.Notifications,
    
      // Winapi
      Winapi.CommonTypes,
      Winapi.Foundation;
    
      type
        // Cardinals
        TSoundEventValue = (
          Default,
          NotificationDefault,
          NotificationIM,
          NotificationMail,
          NotificationReminder,
          NotificationSMS,
          NotificationLoopingAlarm,
          NotificationLoopingAlarm2,
          NotificationLoopingAlarm3,
          NotificationLoopingAlarm4,
          NotificationLoopingAlarm5,
          NotificationLoopingAlarm6,
          NotificationLoopingAlarm7,
          NotificationLoopingAlarm8,
          NotificationLoopingAlarm9,
          NotificationLoopingAlarm10,
          NotificationLoopingCall,
          NotificationLoopingCall2,
          NotificationLoopingCall3,
          NotificationLoopingCall4,
          NotificationLoopingCall5,
          NotificationLoopingCall6,
          NotificationLoopingCall7,
          NotificationLoopingCall8,
          NotificationLoopingCall9,
          NotificationLoopingCall10
        );
    
        TWinBoolean = (Default, False, True);
        TImagePlacement = (Default, Hero, LogoOverride);
        TImageCrop = (Default, Circle);
        TInputType = (Text, Selection);
    
        TXMLInterface = Xml_Dom_IXmlDocument;
    
        // Records
        (* https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-header *)
        TNotificationHeader = record
          ID: string;
          Title: string;
          Arguments: string;
          ActivationType: string;
    
          function ToXML: string;
        end;
    
        (* https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml *)
        TInputItem = record
          ID: string;
          Content: string;
    
          function ToXML: string;
        end;
    
        TNotificationInput = record
          ID: string;
          InputType: TInputType;
          PlaceHolder: string;
          Title: string;
    
          Selections: TArray<TInputItem>;
    
          function ToXML: string;
        end;
    
        (* https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image *)
        TNotificationImage = record
          ImageQuery: string;
          Alt: string;
          Source: string; // URL or Local file path
          Placement: TImagePlacement;
          HintCrop: TImageCrop;
    
          function ToXML: string;
        end;
    
        (* https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio *)
        TNotificationAudio = record
          Sound: TSoundEventValue;
          Loop: TWinBoolean;
          Silent: TWinBoolean;
    
          function ToXML: string;
        end;
    
        (* https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-progress *)
        TNotificationProgress = record
          Title: string;
          Status: string; // Name, such as "installing" or "downloading"
          Value: single; // 0.0 - 1.0
          ValueOverride: string; // Override percentage text
    
          function ToXML: string;
        end;
    
        // Classes
        TNotification = class(TObject)
        private
          FTitle: string;
          FText: string;
          FTextExtra: string;
    
          FXML: TXMLInterface;
    
          FToast: IToastNotification;
    
          function BuildXMLDoc: TXMLInterface;
    
        public
          // Basic
          property Title: string read FTitle write FTitle;
          property Text: string read FText write FText;
          property TextExtra: string read FTextExtra write FTextExtra;
    
          // Advanced
          var
          Image: TNotificationImage;
          Audio: TNotificationAudio;
          Progress: TNotificationProgress;
          Input: TNotificationInput;
          Header: TNotificationHeader;
    
          property Toast: IToastNotification read FToast;
    
          // Procs
          function ToXML: string;
    
          procedure UpdateToastInterface;
    
          // Constructors
          constructor Create;
          destructor Destroy; override;
    
        end;
    
        TNotificationManager = class(TObject)
        private
          FNotifier: IToastNotifier;
          FApplicationName: string;
    
          // Notifier
          procedure RebuildNotifier;
    
          // Setters
          procedure SetAppName(const Value: string);
    
        public
          // Notificaitons
          procedure ShowNotification(Notification: TNotification);
          procedure HideNotification(Notification: TNotification);
    
          // App
          property ApplicationName: string read FApplicationName write SetAppName;
    
          // Utils
          function CreateNewNotification: TNotification;
    
          // Constructors
          constructor Create;
          destructor Destroy; override;
        end;
    
      // Utils
      function AudioTypeToString(AType: TSoundEventValue): string;
      function WinBooleanToString(AType: TWinBoolean): string;
      function StringToRTString(Value: string): HSTRING; // Needs to be freed with WindowsDeleteString
    
      // XML
      function EncapsulateXML(Name: string; Container: string; Tags: string = ''): string;
      function CreateNewXMLInterface: TXMLInterface;
      function StringToXMLDocument(Str: string): TXMLInterface;
      function XMLDocumentToString(AXML: TXMLInterface): string;
      procedure XMLDocumentEdit(AXML: TXMLInterface; NewContents: string);
    
    implementation
    
    const
      NOTIF_BUILD_ERR = 'Notification toast has not been built. E:UpdateToastInterface';
    
    { TNotificationAudio }
    
    function AudioTypeToString(AType: TSoundEventValue): string;
    begin
      case AType of
        TSoundEventValue.NotificationDefault: Result := 'ms-winsoundevent:Notification.Default';
        TSoundEventValue.NotificationIM: Result := 'ms-winsoundevent:Notification.IM';
        TSoundEventValue.NotificationMail: Result := 'ms-winsoundevent:Notification.Mail';
        TSoundEventValue.NotificationReminder: Result := 'ms-winsoundevent:Notification.Reminder';
        TSoundEventValue.NotificationSMS: Result := 'ms-winsoundevent:Notification.SMS';
        TSoundEventValue.NotificationLoopingAlarm: Result := 'ms-winsoundevent:Notification.Looping.Alarm';
        TSoundEventValue.NotificationLoopingAlarm2: Result := 'ms-winsoundevent:Notification.Looping.Alarm2';
        TSoundEventValue.NotificationLoopingAlarm3: Result := 'ms-winsoundevent:Notification.Looping.Alarm3';
        TSoundEventValue.NotificationLoopingAlarm4: Result := 'ms-winsoundevent:Notification.Looping.Alarm4';
        TSoundEventValue.NotificationLoopingAlarm5: Result := 'ms-winsoundevent:Notification.Looping.Alarm5';
        TSoundEventValue.NotificationLoopingAlarm6: Result := 'ms-winsoundevent:Notification.Looping.Alarm6';
        TSoundEventValue.NotificationLoopingAlarm7: Result := 'ms-winsoundevent:Notification.Looping.Alarm7';
        TSoundEventValue.NotificationLoopingAlarm8: Result := 'ms-winsoundevent:Notification.Looping.Alarm8';
        TSoundEventValue.NotificationLoopingAlarm9: Result := 'ms-winsoundevent:Notification.Looping.Alarm9';
        TSoundEventValue.NotificationLoopingAlarm10: Result := 'ms-winsoundevent:Notification.Looping.Alarm10';
        TSoundEventValue.NotificationLoopingCall: Result := 'ms-winsoundevent:Notification.Looping.Call';
        TSoundEventValue.NotificationLoopingCall2: Result := 'ms-winsoundevent:Notification.Looping.Call2';
        TSoundEventValue.NotificationLoopingCall3: Result := 'ms-winsoundevent:Notification.Looping.Call3';
        TSoundEventValue.NotificationLoopingCall4: Result := 'ms-winsoundevent:Notification.Looping.Call4';
        TSoundEventValue.NotificationLoopingCall5: Result := 'ms-winsoundevent:Notification.Looping.Call5';
        TSoundEventValue.NotificationLoopingCall6: Result := 'ms-winsoundevent:Notification.Looping.Call6';
        TSoundEventValue.NotificationLoopingCall7: Result := 'ms-winsoundevent:Notification.Looping.Call7';
        TSoundEventValue.NotificationLoopingCall8: Result := 'ms-winsoundevent:Notification.Looping.Call8';
        TSoundEventValue.NotificationLoopingCall9: Result := 'ms-winsoundevent:Notification.Looping.Call9';
        TSoundEventValue.NotificationLoopingCall10: Result := 'ms-winsoundevent:Notification.Looping.Call10';
    
        else Result := '';
      end;
    end;
    
    function WinBooleanToString(AType: TWinBoolean): string;
    begin
      case AType of
        TWinBoolean.Default: Result := 'default';
        TWinBoolean.False: Result := 'false';
        TWinBoolean.True: Result := 'true';
      end;
    end;
    
    function StringToRTString(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 EncapsulateXML(Name: string; Container, Tags: string): string;
    var
      TagBegin, TagEnd: string;
    begin
      if Container <> '' then
        TagEnd := Format('</%S>', [Name])
      else
        TagEnd := '';
    
      TagBegin := Format('<%S', [Name]);
      if Tags <> '' then
        TagBegin := Format('%S %S', [TagBegin, Tags]);
    
      if Container <> '' then
        TagBegin := TagBegin + '>'
      else
        TagBegin := TagBegin + '/>';
    
      Result := TagBegin + Container + TagEnd;
    end;
    
    function StringToXMLDocument(Str: string): TXMLInterface;
    begin
      Result := CreateNewXMLInterface;
      XMLDocumentEdit(Result, Str);
    end;
    
    function CreateNewXMLInterface: TXMLInterface;
    var
      Manager: TToastNotificationManager;
    begin
      Manager := TToastNotificationManager.Create;
      try
        Result := Manager.GetTemplateContent(ToastTemplateType.ToastText01);
        XMLDocumentEdit(Result, '<xml />');
      finally
        Manager.Free;
      end;
    end;
    
    function XMLDocumentToString(AXML: TXMLInterface): string;
      function HStringToString(Src: HSTRING): String;
      var
        c: Cardinal;
      begin
        c := WindowsGetStringLen(Src);
        Result := WindowsGetStringRawBuffer(Src, @c);
      end;
    
    begin
      Result := HStringToString(
        ( AXML.DocumentElement as Xml_Dom_IXmlNodeSerializer ).GetXml
      );
    end;
    
    procedure XMLDocumentEdit(AXML: TXMLInterface; NewContents: string);
    var
      hXML: HSTRING;
    begin
      hXML := StringToRTString( NewContents );
      try
        (AXML as Xml_Dom_IXmlDocumentIO).LoadXml( hXML );
      finally
        WindowsDeleteString( hXML );
      end;
    end;
    
    function TNotificationAudio.ToXML: string;
    begin
      Result := '';
    
      if Sound <> TSoundEventValue.Default then
        Result := Result + 'src="' + AudioTypeToString(Sound) + '" ';
    
      if Loop <> TWinBoolean.Default then
        Result := Result + 'loop="' + WinBooleanToString(Loop) + '" ';
    
      if Silent <> TWinBoolean.Default then
        Result := Result + 'silent="' + WinBooleanToString(Silent) + '"';
    
      // Encapsulate
      if Result <> '' then
        Result := EncapsulateXML('audio', '', Result);
    end;
    
    { TNotification }
    
    function TNotification.BuildXMLDoc: TXMLInterface;
    begin
      Result := StringToXMLDocument( ToXML );
    end;
    
    constructor TNotification.Create;
    begin
      inherited;
      FTitle := 'Hello world!';
      FText := 'This is a notification';
    
      FXML := CreateNewXMLInterface;
    end;
    
    destructor TNotification.Destroy;
    begin
      FXML := nil;
    
      inherited;
    end;
    
    function TNotification.ToXML: string;
    function TextToXML(Value: string): string;
    begin
      if Value <> '' then
        Result := EncapsulateXML('text', Value)
      else
        Result := '';
    end;
    begin
      Result := '';
    
      Result := Result + '<toast activationType="protocol">';
        // Visual
        Result := Result + '<visual>';
          Result := Result + '<binding template="ToastGeneric">';
            // Text
            Result := Result + TextToXML(Title);
            Result := Result + TextToXML(Text);
            Result := Result + TextToXML(TextExtra);
    
            // Extra
            Result := Result + Image.ToXML;
            Result := Result + Progress.ToXML;
          Result := Result + '</binding>';
        Result := Result + '</visual>';
    
        // Input
        const Input = Input.ToXML;
        if Input <> '' then
        Result := Result + EncapsulateXML('actions', Input);
    
        // Audio
        Result := Result + Audio.ToXML;
    
        // Header
        Result := Result + Header.ToXML;
      Result := Result + '</toast>';
    end;
    
    procedure TNotification.UpdateToastInterface;
    begin
      FToast := nil;
    
      FToast := TToastNotification.CreateToastNotification(BuildXMLDoc);
    end;
    
    { TNotificationProgress }
    
    function TNotificationProgress.ToXML: string;
    begin
      Result := '';
    
      // Check disabled/invalid
      if Status = '' then
        Exit;
    
      if Title <> '' then
        Result := Result + 'title="' + Title + '" ';
    
      Result := Result + 'status="' + Status + '" ';
      Result := Result + 'value="' + Value.ToString + '" ';
    
      if ValueOverride <> '' then
        Result := Result + 'valueStringOverride="' + ValueOverride + '"';
    
      // Encapsulate
      if Result <> '' then
        Result := EncapsulateXML('progress', '', Result);
    end;
    
    { TNotificationImage }
    
    function TNotificationImage.ToXML: string;
    begin
      Result := '';
    
      // Check disabled/invalid
      if Source = '' then
        Exit;
    
      if ImageQuery <> '' then
        Result := Result + 'addImageQuery="' + ImageQuery + '" ';
    
      if Alt <> '' then
        Result := Result + 'alt="' + Alt + '" ';
    
      Result := Result + 'src="' + Source + '" ';
    
      case Placement of
        TImagePlacement.Hero: Result := Result + 'placement="hero" ';
        TImagePlacement.LogoOverride: Result := Result + 'placement="appLogoOverride" ';
      end;
    
      case HintCrop of
        TImageCrop.Circle: Result := Result + 'hint-crop="circle" ';
      end;
    
      // Encapsulate
      if Result <> '' then
        Result := EncapsulateXML('image', '', Result);
    end;
    
    { TInputItem }
    
    function TInputItem.ToXML: string;
    begin
      Result := '';
    
      // Check disabled/invalid
      if ID = '' then
        Exit;
    
      Result := Result + 'id="' + ID + '" ';
      Result := Result + 'content="' + Content + '" ';
    
      // Encapsulate
      if Result <> '' then
        Result := EncapsulateXML('selection', '', Result);
    end;
    
    { TNotificationInput }
    
    function TNotificationInput.ToXML: string;
    var
      Inputs: string;
      I: integer;
    begin
      Result := '';
    
      // Check disabled/invalid
      if ID = '' then
        Exit;
    
      Result := Result + 'id="' + ID + '" ';
    
      case InputType of
        TInputType.Text: Result := Result + 'type="text" ';
        TInputType.Selection: Result := Result + 'type="selection" ';
      end;
    
      if PlaceHolder <> '' then
        Result := Result + 'placeHolderContent="' + PlaceHolder + '" ';
    
      if Title <> '' then
        Result := Result + 'title="' + Title + '" ';
    
      // Inputs
      Inputs := '';
      if Length(Selections) > 0 then
        for I := 0 to High(Inputs) do
          Inputs := Inputs + Selections[I].ToXML;
    
      // Encapsulate
      if Result <> '' then
        Result := EncapsulateXML('input', Inputs, Result);
    end;
    
    { TNotificationManager }
    
    constructor TNotificationManager.Create;
    begin
      FApplicationName := ExtractFileName(Application.ExeName);
    
      RebuildNotifier;
    end;
    
    function TNotificationManager.CreateNewNotification: TNotification;
    begin
      Result := TNotification.Create;
    end;
    
    destructor TNotificationManager.Destroy;
    begin
      FNotifier := nil;
      inherited;
    end;
    
    procedure TNotificationManager.HideNotification(Notification: TNotification);
    begin
      if Notification.Toast = nil then
        raise Exception.Create(NOTIF_BUILD_ERR);
    
      FNotifier.Hide(Notification.Toast);
    end;
    
    procedure TNotificationManager.RebuildNotifier;
    var
      AName: HSTRING;
    begin
      FNotifier := nil;
    
      AName := StringToRTString(FApplicationName);
      FNotifier := TToastNotificationManager.CreateToastNotifier(AName);
      WindowsDeleteString(AName);
    end;
    
    procedure TNotificationManager.SetAppName(const Value: string);
    begin
      if FApplicationName = Value then
        Exit;
    
      FApplicationName := Value;
      RebuildNotifier;
    end;
    
    procedure TNotificationManager.ShowNotification(Notification: TNotification);
    begin
      if Notification.Toast = nil then
        raise Exception.Create(NOTIF_BUILD_ERR);
    
      FNotifier.Show(Notification.Toast);
    end;
    
    { TNotificationHeader }
    
    function TNotificationHeader.ToXML: string;
    begin
      Result := '';
    
      // Check disabled/invalid
      if (ID = '') or (Title = '') or (Arguments = '') then
        Exit;
    
      Result := Result + 'id="' + ID + '" ';
      Result := Result + 'title="' + Title + '" ';
      Result := Result + 'arguments="' + Arguments + '" ';
    
      if ActivationType <> '' then
        Result := Result + 'activationType="' + ActivationType + '" ';
    
      // Encapsulate
      if Result <> '' then
        Result := EncapsulateXML('header', '', Result);
    end;
    
    end.
    

    To open a notification, do as follows:

    var
      Manager: TNotificationManager;
      Notif: TNotification;
    begin
      Manager := TNotificationManager.Create;
      try
        Manager.ApplicationName := 'Awesome Application';
    
        Notif := Manager.CreateNewNotification;
    
        Notif.Title := 'This is the title';
        Notif.Text := 'This is the text';
    
        Notif.Image.Source := 'C:\Windows\System32\@facial-recognition-windows-hello.gif';
        Notif.Image.Placement := TImagePlacement.Hero;
    
        Notif.Audio.Sound := TSoundEventValue.NotificationIM;
        Notif.Progress.Value := 0.5;
        Notif.Progress.Status := 'Downloading data';
    
        Notif.UpdateToastInterface;
    
        Manager.ShowNotification(Notif);
      finally
        Manager.Free;
      end;