I’m currently working on a project that involves sending alerts and notifications to users on Windows 11 systems.
During development, I learned that—for local testing purposes—it’s possible to generate toast notifications using built-in PowerShell functionality. Specifically, the ToastNotificationManager and CreateToastNotifier APIs make it straightforward to display dead simple, native notifications without any external dependencies.
$body = 'Hello from PowerShell! Behold, a toast notification.'
$toastXml = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText01)
$toastXml.SelectSingleNode('//text[@id="1"]').InnerText = $body
$appId = 'App'
$toast = [Windows.UI.Notifications.ToastNotification]::new($toastXml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
Of course, you can also set up toast notifications with C++ in a Win32 shell environment, too. But Windows will only send toast notifications for apps that have both a shortcut in the start menu, and an AppUserModelID property within that shortcut!
To do this, we can also use a PowerShell script to:
1: Create a Windows shortcut .lnk file
2: Set the AppUserModelID property on that shortcut
3: Save it to disk
First, we set up our shortcut path, the target binary path, and define an AppUserModelID, then use PowerShell's built-in .NET to include functionality for interop services and COM objects.
So, we instantiate a new COM object using the correct interface GUID (which you can find on Pinvoke.net), and create a pointer to it with var link = (IShellLinkW)new ShellLink();
.
Next, we cast it to IPropertyStore
so we can set properties: var store = (IPropertyStore)link;
followed by store.SetValue(ref key, ref pv);
.
Then we set up the required COM structs — PROPERTYKEY
to identify the property, and PROPVARIANT
to hold the value. And once all the properties are set, we save the shortcut to disk via (IPersistFile)link;
.
$ShortcutPath = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\ToastyApp.lnk"
$TargetPath = "C:\Path\To\App.exe"
$AppUserModelID = "App.ID"
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
class ShellLink {}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
interface IShellLinkW {
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pszFile, int cchMaxPath, out IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPropertyStore {
void GetCount(out uint cProps);
void GetAt(uint iProp, out PROPERTYKEY pkey);
void GetValue(ref PROPERTYKEY key, out PROPVARIANT pv);
void SetValue(ref PROPERTYKEY key, ref PROPVARIANT pv);
void Commit();
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct PROPERTYKEY {
public Guid fmtid;
public uint pid;
}
[StructLayout(LayoutKind.Explicit)]
struct PROPVARIANT {
[FieldOffset(0)]
public ushort vt;
[FieldOffset(8)]
public IntPtr pszVal;
public static PROPVARIANT FromString(string value) {
var pv = new PROPVARIANT();
pv.vt = 31; // VT_LPWSTR
pv.pszVal = Marshal.StringToCoTaskMemUni(value);
return pv;
}
}
public static class ShellLinkHelper {
static readonly Guid PKEY_AppUserModel_ID_fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3");
const uint PKEY_AppUserModel_ID_pid = 5;
public static void CreateShortcut(string shortcutPath, string exePath, string appId) {
var link = (IShellLinkW)new ShellLink();
link.SetPath(exePath);
var store = (IPropertyStore)link;
var key = new PROPERTYKEY() { fmtid = PKEY_AppUserModel_ID_fmtid, pid = PKEY_AppUserModel_ID_pid };
var pv = PROPVARIANT.FromString(appId);
store.SetValue(ref key, ref pv);
store.Commit();
var file = (IPersistFile)link;
file.Save(shortcutPath, false);
}
}
"@ -Language CSharp
# Call helper from PowerShell
[ShellLinkHelper]::CreateShortcut($ShortcutPath, $TargetPath, $AppUserModelID)
Write-Host "Shortcut created at $ShortcutPath with AppUserModelID = $AppUserModelID"
With our shortcut and AppID properly set up, we can use the following C++ for a bare bones Toast Notification test. After compiling our C++ program below, we will return to the .lnk shortcut we created with PowerShell at %APPDATA%\Microsoft\Windows\Start Menu\Programs\ToastyApp.lnk
, to make one small change--configuring its properties to point to wherever our compiled C++ binary is.
To ensure the following C++ code compiles, you will need to open Visual Studio Community and click Projects -> Properties -> Linker -> Input and manually add "runtimeobject.lib" to your additional dependencies.
Additionally, this build only compiles using the ISO C++17 Standard. C++17 is mandatory for building the code below. You may configure your project to use the standard within the C/C++ -> Language selector in the same Project Properties dialogue as mentioned above.
#include <windows.h>
#include <wrl/client.h>
#include <wrl/wrappers/corewrappers.h>
#include <windows.ui.notifications.h>
#include <winrt/base.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <winrt/Windows.UI.Notifications.h>
#include <string>
#include <iostream>
#include <shobjidl.h>
#pragma comment(lib, "Shell32.lib")
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace winrt;
using namespace winrt::Windows::Data::Xml::Dom;
using namespace winrt::Windows::UI::Notifications;
int main() {
RoInitialize(RO_INIT_SINGLETHREADED);
// Set AppUserModelID
SetCurrentProcessExplicitAppUserModelID(L"Your.App.ID");
// Create Toast Notifier
auto toastNotifier = ToastNotificationManager::CreateToastNotifier(L"Your.App.ID");
// Create XML content
XmlDocument toastXml;
try {
std::wstring xmlString = L"Hello from C++! ";
toastXml.LoadXml(xmlString);
}
catch (const hresult_error& ex) {
std::wcerr << L"Failed to load XML: " << ex.message().c_str() << std::endl;
RoUninitialize();
return 1;
}
// Create Toast Notification
auto toast = ToastNotification(toastXml);
// Show Toast
toastNotifier.Show(toast);
RoUninitialize();
return 0;
}
Comments
Post a Comment