Last active
September 22, 2024 08:38
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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