Skip to content

Instantly share code, notes, and snippets.

@valinet
Last active September 22, 2024 08:38
Show Gist options
  • Save valinet/39451ad061f82d62f3d99df20020f890 to your computer and use it in GitHub Desktop.
Save valinet/39451ad061f82d62f3d99df20020f890 to your computer and use it in GitHub Desktop.
Send a toast notification in Windows 10/11 using plain C including COM activator
#include <initguid.h>
#include <Windows.h>
#include <roapi.h>
#include <Windows.ui.notifications.h>
#include <notificationactivationcallback.h>
#include <tchar.h>
#include <stdio.h>
#pragma comment(lib, "runtimeobject.lib")
DWORD dwMainThreadId = 0;
/*
* Our AUMID and argument that tells our app when it was launched by interacting with a toast notification.
*/
#define APP_ID L"ToastActivatorPureC"
#define TOAST_ACTIVATED_LAUNCH_ARG "-ToastActivated"
/*
* The GUID that we associate with our factory that produces our INotificationActivationCallback interface.
*/
#define GUID_Impl_INotificationActivationCallback_Textual "0F82E845-CB89-4039-BDBF-67CA33254C76"
DEFINE_GUID(GUID_Impl_INotificationActivationCallback,
0xf82e845, 0xcb89, 0x4039, 0xbd, 0xbf, 0x67, 0xca, 0x33, 0x25, 0x4c, 0x76);
/*
* The XML that describes the notification that will be shown. Of course, this can be built at runtime,
* and more can be done with it, but for this basic example, this will suffice.
*/
const wchar_t wszBannerText[] =
L"<toast scenario=\"reminder\" "
L"activationType=\"foreground\" launch=\"action=mainContent\" duration=\"short\">\r\n"
L" <visual>\r\n"
L" <binding template=\"ToastGeneric\">\r\n"
L" <text><![CDATA[This is a demo notification]]></text>\r\n"
L" <text><![CDATA[It contains 2 lines of text]]></text>\r\n"
L" <text placement=\"attribution\"><![CDATA[Created by Valentin-Gabriel Radu (github.com/valinet)]]></text>\r\n"
L" </binding>\r\n"
L" </visual>\r\n"
L" <actions>\r\n"
L" <input id=\"tbReply\" type=\"text\" placeHolderContent=\"Send a message to the app\"/>\r\n"
L" <action content=\"Send\" activationType=\"foreground\" arguments=\"action=reply\"/>\r\n"
L" <action content=\"Visit GitHub\" activationType=\"protocol\" arguments=\"https://github.com/valinet\"/>\r\n"
L" <action content=\"Close app\" activationType=\"foreground\" arguments=\"action=closeApp\"/>\r\n"
L" </actions>\r\n"
L" <audio src=\"ms-winsoundevent:Notification.Default\" loop=\"false\" silent=\"false\"/>\r\n"
L"</toast>\r\n";
/*
* IIDs of other interfaces we use throughout this example.
*/
DEFINE_GUID(IID_IToastNotificationManagerStatics,
0x50ac103f, 0xd235, 0x4598, 0xbb, 0xef, 0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4);
DEFINE_GUID(IID_IToastNotificationFactory,
0x04124b20, 0x82c6, 0x4229, 0xb1, 0x09, 0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53);
DEFINE_GUID(IID_IXmlDocument,
0xf7f3a506, 0x1e87, 0x42d6, 0xbc, 0xfb, 0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94);
DEFINE_GUID(IID_IXmlDocumentIO,
0x6cd0e74e, 0xee65, 0x4489, 0x9e, 0xbf, 0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37);
/*
* All the objects we allocate in this example (our class factory and our INotificationActivationCallback
* implementation) have this memory layout, and all inherit from IUnknown.
*/
#pragma region "IGeneric : IUnknown implementation"
typedef struct Impl_IGeneric
{
IUnknownVtbl* lpVtbl;
LONG64 dwRefCount;
} Impl_IGeneric;
static ULONG STDMETHODCALLTYPE Impl_IGeneric_AddRef(Impl_IGeneric* _this)
{
return InterlockedIncrement64(&(_this->dwRefCount));
}
static ULONG STDMETHODCALLTYPE Impl_IGeneric_Release(Impl_IGeneric* _this)
{
LONG64 dwNewRefCount = InterlockedDecrement64(&(_this->dwRefCount));
if (!dwNewRefCount) free(_this);
return dwNewRefCount;
}
#pragma endregion
/*
* Our INotificationActivationCallback implementation.
*/
#pragma region "INotificationActivationCallback : IGeneric implementation"
static HRESULT STDMETHODCALLTYPE Impl_INotificationActivationCallback_QueryInterface(Impl_IGeneric* _this, REFIID riid, void** ppvObject)
{
if (!IsEqualIID(riid, &IID_INotificationActivationCallback) && !IsEqualIID(riid, &IID_IUnknown))
{
*ppvObject = NULL;
return E_NOINTERFACE;
}
*ppvObject = _this;
_this->lpVtbl->AddRef(_this);
return S_OK;
}
/*
* This is where the magic happens when someone interacts with our notification: this method will be called
* (on another thread !!!).
*/
static HRESULT STDMETHODCALLTYPE Impl_INotificationActivationCallback_Activate(INotificationActivationCallback* _this, LPCWSTR appUserModelId, LPCWSTR invokedArgs, const NOTIFICATION_USER_INPUT_DATA* data, ULONG count)
{
wprintf(L"Interacted with notification from AUMID \"%s\" with arguments: \"%s\". User input count: %d.\n", appUserModelId, invokedArgs, count);
if (!_wcsicmp(invokedArgs, L"action=closeApp"))
{
PostThreadMessageW(dwMainThreadId, WM_QUIT, 0, 0);
}
else if (!_wcsicmp(invokedArgs, L"action=reply"))
{
for (unsigned int i = 0; i < count; ++i)
{
if (!_wcsicmp(data[i].Key, L"tbReply"))
{
wprintf(L"Reply was \"%s\".\n", data[i].Value);
}
}
}
return S_OK;
}
static const INotificationActivationCallbackVtbl Impl_INotificationActivationCallback_Vtbl = {
.QueryInterface = Impl_INotificationActivationCallback_QueryInterface,
.AddRef = Impl_IGeneric_AddRef,
.Release = Impl_IGeneric_Release,
.Activate = Impl_INotificationActivationCallback_Activate
};
#pragma endregion
/*
* Our IClassFactory implementation.
*/
#pragma region "IClassFactory : IGeneric implementation"
static HRESULT STDMETHODCALLTYPE Impl_IClassFactory_QueryInterface(Impl_IGeneric* _this, REFIID riid, void** ppvObject)
{
if (!IsEqualIID(riid, &IID_IClassFactory) && !IsEqualIID(riid, &IID_IUnknown))
{
*ppvObject = NULL;
return E_NOINTERFACE;
}
*ppvObject = _this;
_this->lpVtbl->AddRef(_this);
return S_OK;
}
static HRESULT STDMETHODCALLTYPE Impl_IClassFactory_LockServer(IClassFactory* _this, BOOL flock)
{
return S_OK;
}
static HRESULT STDMETHODCALLTYPE Impl_IClassFactory_CreateInstance(IClassFactory* _this, IUnknown* punkOuter, REFIID vTableGuid, void** ppv)
{
HRESULT hr = E_NOINTERFACE;
Impl_IGeneric* thisobj = NULL;
*ppv = 0;
if (punkOuter) hr = CLASS_E_NOAGGREGATION;
else
{
BOOL bOk = FALSE;
if (!(thisobj = malloc(sizeof(Impl_IGeneric)))) hr = E_OUTOFMEMORY;
else
{
thisobj->lpVtbl = &Impl_INotificationActivationCallback_Vtbl;
bOk = TRUE;
}
if (bOk)
{
thisobj->dwRefCount = 1;
hr = thisobj->lpVtbl->QueryInterface(thisobj, vTableGuid, ppv);
thisobj->lpVtbl->Release(thisobj);
}
else
{
return hr;
}
}
return hr;
}
static const IClassFactoryVtbl Impl_IClassFactory_Vtbl = {
.QueryInterface = Impl_IClassFactory_QueryInterface,
.AddRef = Impl_IGeneric_AddRef,
.Release = Impl_IGeneric_Release,
.LockServer = Impl_IClassFactory_LockServer,
.CreateInstance = Impl_IClassFactory_CreateInstance
};
#pragma endregion
int main(int argc, char** argv)
{
HRESULT hr = S_OK;
Impl_IGeneric* pClassFactory = NULL;
BOOL bOk = FALSE;
dwMainThreadId = GetCurrentThreadId();
BOOL bInvokedFromToast = (argc > 1);
/*
* Initialize COM and Windows Runtime on this thread. Make sure that the threading models of the two match.
*/
if (SUCCEEDED(hr))
{
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
}
if (SUCCEEDED(hr))
{
hr = RoInitialize(RO_INIT_MULTITHREADED);
}
/*
* Allocate class factory. This factory produces our implementation of the INotificationActivationCallback interface.
* This interface has an ::Activate member method that gets called when someone interacts with the toast notification.
*/
if (SUCCEEDED(hr))
{
if (!(pClassFactory = malloc(sizeof(Impl_IGeneric)))) hr = E_OUTOFMEMORY;
else
{
pClassFactory->lpVtbl = &Impl_IClassFactory_Vtbl;
pClassFactory->dwRefCount = 1;
}
}
/*
* Instead of having to register our COM class in the registry beforehand, we opt to registering it at runtime;
* we associate our GUID with the class factory that provides our INotificationActivationCallback interface.
*/
DWORD dwCookie = 0;
if (SUCCEEDED(hr))
{
hr = CoRegisterClassObject(&GUID_Impl_INotificationActivationCallback, pClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwCookie);
}
/*
* Construct the path to our EXE that will be used to launch it when something requests our interface.
* As said above, registration is dynamic - as long as this app runs, COM knows about the fact that this
* app implements our INotificationActivationCallback interface. The info here is used when this app has
* closed and someone clicks the toast notification for example; in that case, since our app is not
* running, thus CoRegisterClassObject was not called, COM needs info on what EXE contains the implementation
* of the interface, and we specify that here; without setting this, clicking on notifications will do nothing
*/
wchar_t wszExePath[MAX_PATH + 100];
ZeroMemory(wszExePath, MAX_PATH + 100);
if (SUCCEEDED(hr))
{
hr = (GetModuleFileNameW(NULL, wszExePath + 1, MAX_PATH) != 0 ? S_OK : E_FAIL);
}
if (SUCCEEDED(hr))
{
wszExePath[0] = L'"';
wcscat_s(wszExePath, MAX_PATH + 100, L"\" " _T(TOAST_ACTIVATED_LAUNCH_ARG));
}
if (SUCCEEDED(hr))
{
hr = HRESULT_FROM_WIN32(RegSetValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\CLSID\\{" _T(GUID_Impl_INotificationActivationCallback_Textual) L"}\\LocalServer32", REG_SZ, wszExePath, wcslen(wszExePath) + 1));
}
/*
* Here we set some info about our app and associate our AUMID with the GUID from above
* (the one that is associated with our class factory which produces our INotificationActivationCallback interface)
*/
if (SUCCEEDED(hr))
{
hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"DisplayName", REG_SZ, L"Toast Activator Pure C Example", 31 * sizeof(wchar_t)));
}
if (SUCCEEDED(hr))
{
hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"IconBackgroundColor", REG_SZ, L"FF00FF00", 9 * sizeof(wchar_t)));
}
if (SUCCEEDED(hr))
{
hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\AppUserModelId\\" APP_ID, L"CustomActivator", REG_SZ, L"{" _T(GUID_Impl_INotificationActivationCallback_Textual) L"}", 39 * sizeof(wchar_t)));
}
/*
* We will display a notification only when this app is launched standalone (not by interacting with a notification)
*/
HSTRING_HEADER hshAppId;
HSTRING hsAppId = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = WindowsCreateStringReference(APP_ID, (UINT32)(sizeof(APP_ID) / sizeof(TCHAR) - 1), &hshAppId, &hsAppId);
}
HSTRING_HEADER hshToastNotificationManager;
HSTRING hsToastNotificationManager = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager, (UINT32)(sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) / sizeof(wchar_t) - 1), &hshToastNotificationManager, &hsToastNotificationManager);
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics* pToastNotificationManager = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID*)&pToastNotificationManager);
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotifier* pToastNotifier = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier);
}
HSTRING_HEADER hshToastNotification;
HSTRING hsToastNotification = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)(sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotification) / sizeof(wchar_t) - 1), &hshToastNotification, &hsToastNotification);
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory* pNotificationFactory = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID*)&pNotificationFactory);
}
HSTRING_HEADER hshXmlDocument;
HSTRING hsXmlDocument = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = WindowsCreateStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument, (UINT32)(sizeof(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument) / sizeof(wchar_t) - 1), &hshXmlDocument, &hsXmlDocument);
}
HSTRING_HEADER hshBanner;
HSTRING hsBanner = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = WindowsCreateStringReference(wszBannerText, (UINT32)(sizeof(wszBannerText) / sizeof(wchar_t) - 1), &hshBanner, &hsBanner);
}
IInspectable* pInspectable = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = RoActivateInstance(hsXmlDocument, &pInspectable);
}
__x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument* pXmlDocument = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = pInspectable->lpVtbl->QueryInterface(pInspectable, &IID_IXmlDocument, &pXmlDocument);
}
__x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO* pXmlDocumentIO = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = pXmlDocument->lpVtbl->QueryInterface(pXmlDocument, &IID_IXmlDocumentIO, &pXmlDocumentIO);
}
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = pXmlDocumentIO->lpVtbl->LoadXml(pXmlDocumentIO, hsBanner);
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotification* pToastNotification = NULL;
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = pNotificationFactory->lpVtbl->CreateToastNotification(pNotificationFactory, pXmlDocument, &pToastNotification);
}
if (SUCCEEDED(hr) && !bInvokedFromToast)
{
hr = pToastNotifier->lpVtbl->Show(pToastNotifier, pToastNotification);
}
if (SUCCEEDED(hr))
{
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
if (pToastNotification)
{
pToastNotification->lpVtbl->Release(pToastNotification);
}
if (pXmlDocumentIO)
{
pXmlDocumentIO->lpVtbl->Release(pXmlDocumentIO);
}
if (pXmlDocument)
{
pXmlDocument->lpVtbl->Release(pXmlDocument);
}
if (pInspectable)
{
pInspectable->lpVtbl->Release(pInspectable);
}
if (hsBanner)
{
WindowsDeleteString(hsBanner);
}
if (hsXmlDocument)
{
WindowsDeleteString(hsXmlDocument);
}
if (pNotificationFactory)
{
pNotificationFactory->lpVtbl->Release(pNotificationFactory);
}
if (hsToastNotification)
{
WindowsDeleteString(hsToastNotification);
}
if (pToastNotifier)
{
pToastNotifier->lpVtbl->Release(pToastNotifier);
}
if (pToastNotificationManager)
{
pToastNotificationManager->lpVtbl->Release(pToastNotificationManager);
}
if (hsToastNotificationManager)
{
WindowsDeleteString(hsToastNotificationManager);
}
if (hsAppId)
{
WindowsDeleteString(hsAppId);
}
if (dwCookie)
{
CoRevokeClassObject(dwCookie);
}
if (pClassFactory)
{
pClassFactory->lpVtbl->Release(pClassFactory);
}
RoUninitialize();
CoUninitialize();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment