From d2e89f694e76ed6914acc47478b4c591e4c432c4 Mon Sep 17 00:00:00 2001 From: wangxx1809 Date: Wed, 28 Feb 2024 10:26:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ui=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- UI/Resource.h | 30 + UI/ToastUi/UI0.cpp | 109 +++ UI/ToastUi/wintoastlib.cpp | 1367 ++++++++++++++++++++++++++++++++++++ UI/ToastUi/wintoastlib.h | 310 ++++++++ UI/UI.cpp | 20 + UI/UI.h | 3 + UI/UI.ico | Bin 0 -> 46227 bytes UI/UI.rc | Bin 0 -> 6408 bytes UI/UI.vcxproj | 154 ++++ UI/UI.vcxproj.filters | 61 ++ UI/UI.vcxproj.user | 6 + UI/framework.h | 15 + UI/small.ico | Bin 0 -> 46227 bytes UI/targetver.h | 6 + 15 files changed, 2087 insertions(+), 1 deletion(-) create mode 100644 UI/Resource.h create mode 100644 UI/ToastUi/UI0.cpp create mode 100644 UI/ToastUi/wintoastlib.cpp create mode 100644 UI/ToastUi/wintoastlib.h create mode 100644 UI/UI.cpp create mode 100644 UI/UI.h create mode 100644 UI/UI.ico create mode 100644 UI/UI.rc create mode 100644 UI/UI.vcxproj create mode 100644 UI/UI.vcxproj.filters create mode 100644 UI/UI.vcxproj.user create mode 100644 UI/framework.h create mode 100644 UI/small.ico create mode 100644 UI/targetver.h diff --git a/.gitignore b/.gitignore index 9529b79..c1f844b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,9 @@ /output/Release/*.exe *.sln /logs -/*.log \ No newline at end of file +/*.log + +/UI/.vs +/UI/output/ +/UI/x64/ +/UI/*.sln \ No newline at end of file diff --git a/UI/Resource.h b/UI/Resource.h new file mode 100644 index 0000000..94aecb5 --- /dev/null +++ b/UI/Resource.h @@ -0,0 +1,30 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ 生成的包含文件。 +// 使用者 UI.rc + +#define IDS_APP_TITLE 103 + +#define IDR_MAINFRAME 128 +#define IDD_UI_DIALOG 102 +#define IDD_ABOUTBOX 103 +#define IDM_ABOUT 104 +#define IDM_EXIT 105 +#define IDI_UI 107 +#define IDI_SMALL 108 +#define IDC_UI 109 +#define IDC_MYICON 2 +#ifndef IDC_STATIC +#define IDC_STATIC -1 +#endif +// 新对象的下一组默认值 +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS + +#define _APS_NO_MFC 130 +#define _APS_NEXT_RESOURCE_VALUE 129 +#define _APS_NEXT_COMMAND_VALUE 32771 +#define _APS_NEXT_CONTROL_VALUE 1000 +#define _APS_NEXT_SYMED_VALUE 110 +#endif +#endif diff --git a/UI/ToastUi/UI0.cpp b/UI/ToastUi/UI0.cpp new file mode 100644 index 0000000..67e9e4b --- /dev/null +++ b/UI/ToastUi/UI0.cpp @@ -0,0 +1,109 @@ +// UI.cpp : 定义应用程序的入口点。 +// + +#include "../framework.h" +#include "../UI.h" +#include "wintoastlib.h" +#include + +class MyToastHandler :public WinToastLib::IWinToastHandler { +public: + + MyToastHandler() {} + + void toastActivated()const { + //LOG(DEBUG) << "The user clicked in this toast"; + + } + + void toastActivated(int actionIndex)const { + //LOG(DEBUG) << "The user clicked on action #" << actionIndex; + m_resultCode = actionIndex; + } + + + void toastDismissed(WinToastDismissalReason state) const { + switch (state) { + case UserCanceled: + //LOG(DEBUG) << "The user dismissed this toast"; + //exit(1); + break; + case TimedOut: + //LOG(DEBUG) << "The toast has timed out"; + //exit(2); + break; + case ApplicationHidden: + //LOG(DEBUG) << "The application hid the toast using ToastNotifier.hide()"; + //exit(3); + break; + default: + //LOG(DEBUG) << "Toast not activated"; + //exit(4); + break; + } + //m_packet->SetShowToast(false); + } + void toastFailed() const { + //LOG(ERROR) << "Error showing current toast"; + } + + + static int m_resultCode; +}; + +int MyToastHandler::m_resultCode = -1; + + + +int APIENTRY wWinMain0(_In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, + _In_ LPWSTR lpCmdLine, + _In_ int nCmdShow) +{ + UNREFERENCED_PARAMETER(hPrevInstance); + UNREFERENCED_PARAMETER(lpCmdLine); + + + + std::wstring appUserModelID = L"警告"; + WinToastLib::WinToast::instance()->setAppName(L"警告"); + WinToastLib::WinToast::instance()->setAppUserModelId(appUserModelID); + WinToastLib::WinToastTemplate toast(WinToastLib::WinToastTemplate::ImageAndText02); + toast.setTextField(L"10s后将关闭plm客户端...", WinToastLib::WinToastTemplate::FirstLine); + + char path[MAX_PATH] = { 0 }; + GetModuleFileNameA(NULL, path, MAX_PATH); + + std::string filePath = path; + size_t nPos = filePath.find_last_of('\\'); + filePath = filePath.substr(0, nPos); + std::wstring_convert> converter; + std::wstring wstr = converter.from_bytes(filePath + "\\notify.webp"); + toast.setImagePath(wstr); + + //toast.addAction(L"确定"); + //toast.addAction(L"取消"); + + toast.setScenario(WinToastLib::WinToastTemplate::Scenario::Alarm); //报警 + toast.setExpiration(10000); //10s + + INT64 id = -1; + if (WinToastLib::WinToast::instance()->initialize()) { + id = WinToastLib::WinToast::instance()->showToast(toast, new MyToastHandler()); + } + + int count = 100,ret = -1; + while (count--) { + ret = (MyToastHandler::m_resultCode == 0 || MyToastHandler::m_resultCode == 1) ? MyToastHandler::m_resultCode : ret; + if (ret != -1) { + ++ret; //1:确定 2:取消 + break; + } + Sleep(100); + } + + WinToastLib::WinToast::instance()->hideToast(id); + + return ret; +} + diff --git a/UI/ToastUi/wintoastlib.cpp b/UI/ToastUi/wintoastlib.cpp new file mode 100644 index 0000000..e57b1b2 --- /dev/null +++ b/UI/ToastUi/wintoastlib.cpp @@ -0,0 +1,1367 @@ +/** + * MIT License + * + * Copyright (C) 2016-2023 WinToast v1.3.0 - Mohammed Boujemaoui + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "wintoastlib.h" + +#include +#include +#include +#include +#include + +#pragma comment(lib, "shlwapi") +#pragma comment(lib, "user32") + +#ifdef NDEBUG +# define DEBUG_MSG(str) \ + do { \ + } while (false) +#else +# define DEBUG_MSG(str) \ + do { \ + std::wcout << str << std::endl; \ + } while (false) +#endif + +#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" +#define DEFAULT_LINK_FORMAT L".lnk" +#define STATUS_SUCCESS (0x00000000) + + // Quickstart: Handling toast activations from Win32 apps in Windows 10 + // https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/ +using namespace WinToastLib; +namespace DllImporter { + + // Function load a function from library + template + HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function& func) { + if (!library) { + return E_INVALIDARG; + } + func = reinterpret_cast(GetProcAddress(library, name)); + return (func != nullptr) ? S_OK : E_FAIL; + } + + typedef HRESULT(FAR STDAPICALLTYPE* f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); + typedef HRESULT(FAR STDAPICALLTYPE* f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); + typedef HRESULT(FAR STDAPICALLTYPE* f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, + _COM_Outptr_ void** factory); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, + _Out_ HSTRING_HEADER* hstringHeader, + _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING* string); + typedef PCWSTR(FAR STDAPICALLTYPE* f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_opt_ UINT32* length); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsDeleteString)(_In_opt_ HSTRING string); + + static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; + static f_PropVariantToString PropVariantToString; + static f_RoGetActivationFactory RoGetActivationFactory; + static f_WindowsCreateStringReference WindowsCreateStringReference; + static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; + static f_WindowsDeleteString WindowsDeleteString; + + template + __inline _Check_return_ HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { + return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory)); + } + + template + inline HRESULT Wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) noexcept { + return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf()); + } + + inline HRESULT initialize() { + HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL"); + HRESULT hr = + loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); + if (SUCCEEDED(hr)) { + HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL"); + hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); + if (SUCCEEDED(hr)) { + HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL"); + bool const succeded = + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); + return succeded ? S_OK : E_FAIL; + } + } + return hr; + } +} // namespace DllImporter + +class WinToastStringWrapper { +public: + WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) noexcept { + HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + WinToastStringWrapper(_In_ std::wstring const& stringRef) noexcept { + HRESULT hr = + DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); + if (FAILED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + ~WinToastStringWrapper() { + DllImporter::WindowsDeleteString(_hstring); + } + + inline HSTRING Get() const noexcept { + return _hstring; + } + +private: + HSTRING _hstring; + HSTRING_HEADER _header; +}; + +class InternalDateTime : public IReference { +public: + static INT64 Now() { + FILETIME now; + GetSystemTimeAsFileTime(&now); + return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime); + } + + InternalDateTime(DateTime dateTime) : _dateTime(dateTime) {} + + InternalDateTime(INT64 millisecondsFromNow) { + _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000; + } + + virtual ~InternalDateTime() = default; + + operator INT64() { + return _dateTime.UniversalTime; + } + + HRESULT STDMETHODCALLTYPE get_Value(DateTime* dateTime) { + *dateTime = _dateTime; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) { + *ppvObject = static_cast(static_cast*>(this)); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE Release() { + return 1; + } + + ULONG STDMETHODCALLTYPE AddRef() { + return 2; + } + + HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) { + return E_NOTIMPL; + } + +protected: + DateTime _dateTime; +}; + +namespace Util { + + typedef LONG NTSTATUS, * PNTSTATUS; + typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + inline RTL_OSVERSIONINFOW getRealOSVersion() { + HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll"); + if (hMod) { + RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion"); + if (fxPtr != nullptr) { + RTL_OSVERSIONINFOW rovi = { 0 }; + rovi.dwOSVersionInfoSize = sizeof(rovi); + if (STATUS_SUCCESS == fxPtr(&rovi)) { + return rovi; + } + } + } + RTL_OSVERSIONINFOW rovi = { 0 }; + return rovi; + } + + inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize); + DEBUG_MSG("Default executable path: " << path); + return (written > 0) ? S_OK : E_FAIL; + } + + inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize); + HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) { + errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link path: " << path); + } + return hr; + } + + inline HRESULT defaultShellLinkPath(_In_ std::wstring const& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + HRESULT hr = defaultShellLinksDirectory(path, nSize); + if (SUCCEEDED(hr)) { + const std::wstring appLink(appname + DEFAULT_LINK_FORMAT); + errno_t result = wcscat_s(path, nSize, appLink.c_str()); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link file path: " << path); + } + return hr; + } + + inline PCWSTR AsString(_In_ ComPtr& xmlDocument) { + HSTRING xml; + ComPtr ser; + HRESULT hr = xmlDocument.As(&ser); + hr = ser->GetXml(&xml); + if (SUCCEEDED(hr)) { + return DllImporter::WindowsGetStringRawBuffer(xml, nullptr); + } + return nullptr; + } + + inline PCWSTR AsString(_In_ HSTRING hstring) { + return DllImporter::WindowsGetStringRawBuffer(hstring, nullptr); + } + + inline HRESULT setNodeStringValue(_In_ std::wstring const& string, _Out_opt_ IXmlNode* node, _Out_ IXmlDocument* xml) { + ComPtr textNode; + HRESULT hr = xml->CreateTextNode(WinToastStringWrapper(string).Get(), &textNode); + if (SUCCEEDED(hr)) { + ComPtr stringNode; + hr = textNode.As(&stringNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = node->AppendChild(stringNode.Get(), &appendedChild); + } + } + return hr; + } + + template + inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, + _In_ INT64 expirationTime, _Out_ EventRegistrationToken& activatedToken, + _Out_ EventRegistrationToken& dismissedToken, _Out_ EventRegistrationToken& failedToken, + _In_ FunctorT&& markAsReadyForDeletionFunc) { + HRESULT hr = notification->add_Activated( + Callback, ITypedEventHandler>>( + [eventHandler, markAsReadyForDeletionFunc](IToastNotification* notify, IInspectable* inspectable) { + ComPtr activatedEventArgs; + HRESULT hr = inspectable->QueryInterface(activatedEventArgs.GetAddressOf()); + if (SUCCEEDED(hr)) { + HSTRING argumentsHandle; + hr = activatedEventArgs->get_Arguments(&argumentsHandle); + if (SUCCEEDED(hr)) { + PCWSTR arguments = Util::AsString(argumentsHandle); + if (arguments && *arguments) { + eventHandler->toastActivated(static_cast(wcstol(arguments, nullptr, 10))); + DllImporter::WindowsDeleteString(argumentsHandle); + markAsReadyForDeletionFunc(); + return S_OK; + } + DllImporter::WindowsDeleteString(argumentsHandle); + } + } + eventHandler->toastActivated(); + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &activatedToken); + + if (SUCCEEDED(hr)) { + hr = notification->add_Dismissed( + Callback, ITypedEventHandler>>( + [eventHandler, expirationTime, markAsReadyForDeletionFunc](IToastNotification* notify, IToastDismissedEventArgs* e) { + ToastDismissalReason reason; + if (SUCCEEDED(e->get_Reason(&reason))) { + if (reason == ToastDismissalReason_UserCanceled && expirationTime && + InternalDateTime::Now() >= expirationTime) { + reason = ToastDismissalReason_TimedOut; + } + eventHandler->toastDismissed(static_cast(reason)); + } + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &dismissedToken); + if (SUCCEEDED(hr)) { + hr = notification->add_Failed( + Callback, ITypedEventHandler>>( + [eventHandler, markAsReadyForDeletionFunc](IToastNotification* notify, IToastFailedEventArgs* e) { + eventHandler->toastFailed(); + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &failedToken); + } + } + return hr; + } + + inline HRESULT addAttribute(_In_ IXmlDocument* xml, std::wstring const& name, IXmlNamedNodeMap* attributeMap) { + ComPtr srcAttribute; + HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = srcAttribute.As(&node); + if (SUCCEEDED(hr)) { + ComPtr pNode; + hr = attributeMap->SetNamedItem(node.Get(), &pNode); + } + } + return hr; + } + + inline HRESULT createElement(_In_ IXmlDocument* xml, _In_ std::wstring const& root_node, _In_ std::wstring const& element_name, + _In_ std::vector const& attribute_names) { + ComPtr rootList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList); + if (SUCCEEDED(hr)) { + ComPtr root; + hr = rootList->Item(0, &root); + if (SUCCEEDED(hr)) { + ComPtr audioElement; + hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement); + if (SUCCEEDED(hr)) { + ComPtr audioNodeTmp; + hr = audioElement.As(&audioNodeTmp); + if (SUCCEEDED(hr)) { + ComPtr audioNode; + hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = audioNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + for (auto const& it : attribute_names) { + hr = addAttribute(xml, it, attributes.Get()); + } + } + } + } + } + } + } + return hr; + } +} // namespace Util + +WinToast* WinToast::instance() { + static WinToast instance; + return &instance; +} + +WinToast::WinToast() : _isInitialized(false), _hasCoInitialized(false) { + if (!isCompatible()) { + DEBUG_MSG(L"Warning: Your system is not compatible with this library "); + } +} + +WinToast::~WinToast() { + clear(); + + if (_hasCoInitialized) { + CoUninitialize(); + } +} + +void WinToast::setAppName(_In_ std::wstring const& appName) { + _appName = appName; +} + +void WinToast::setAppUserModelId(_In_ std::wstring const& aumi) { + _aumi = aumi; + DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str()); +} + +void WinToast::setShortcutPolicy(_In_ ShortcutPolicy shortcutPolicy) { + _shortcutPolicy = shortcutPolicy; +} + +bool WinToast::isCompatible() { + DllImporter::initialize(); + return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) || (DllImporter::PropVariantToString == nullptr) || + (DllImporter::RoGetActivationFactory == nullptr) || (DllImporter::WindowsCreateStringReference == nullptr) || + (DllImporter::WindowsDeleteString == nullptr)); +} + +bool WinToastLib::WinToast::isSupportingModernFeatures() { + constexpr auto MinimumSupportedVersion = 6; + return Util::getRealOSVersion().dwMajorVersion > MinimumSupportedVersion; +} + +bool WinToastLib::WinToast::isWin10AnniversaryOrHigher() { + return Util::getRealOSVersion().dwBuildNumber >= 14393; +} + +std::wstring WinToast::configureAUMI(_In_ std::wstring const& companyName, _In_ std::wstring const& productName, + _In_ std::wstring const& subProduct, _In_ std::wstring const& versionInformation) { + std::wstring aumi = companyName; + aumi += L"." + productName; + if (subProduct.length() > 0) { + aumi += L"." + subProduct; + if (versionInformation.length() > 0) { + aumi += L"." + versionInformation; + } + } + + if (aumi.length() > SCHAR_MAX) { + DEBUG_MSG("Error: max size allowed for AUMI: 128 characters."); + } + return aumi; +} + +std::wstring const& WinToast::strerror(WinToastError error) { + static const std::unordered_map Labels = { + {WinToastError::NoError, L"No error. The process was executed correctly" }, + {WinToastError::NotInitialized, L"The library has not been initialized" }, + {WinToastError::SystemNotSupported, L"The OS does not support WinToast" }, + {WinToastError::ShellLinkNotCreated, L"The library was not able to create a Shell Link for the app" }, + {WinToastError::InvalidAppUserModelID, L"The AUMI is not a valid one" }, + {WinToastError::InvalidParameters, L"Invalid parameters, please double-check the AUMI or App Name" }, + {WinToastError::NotDisplayed, L"The toast was created correctly but WinToast was not able to display the toast"}, + {WinToastError::UnknownError, L"Unknown error" } + }; + + auto const iter = Labels.find(error); + assert(iter != Labels.end()); + return iter->second; +} + +enum WinToast::ShortcutResult WinToast::createShortcut() { + if (_aumi.empty() || _appName.empty()) { + DEBUG_MSG(L"Error: App User Model Id or Appname is empty!"); + return SHORTCUT_MISSING_PARAMETERS; + } + + if (!isCompatible()) { + DEBUG_MSG(L"Your OS is not compatible with this library! =("); + return SHORTCUT_INCOMPATIBLE_OS; + } + + if (!_hasCoInitialized) { + HRESULT initHr = CoInitializeEx(nullptr, COINIT::COINIT_MULTITHREADED); + if (initHr != RPC_E_CHANGED_MODE) { + if (FAILED(initHr) && initHr != S_FALSE) { + DEBUG_MSG(L"Error on COM library initialization!"); + return SHORTCUT_COM_INIT_FAILURE; + } + else { + _hasCoInitialized = true; + } + } + } + + bool wasChanged; + HRESULT hr = validateShellLinkHelper(wasChanged); + if (SUCCEEDED(hr)) { + return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED; + } + + hr = createShellLinkHelper(); + return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED; +} + +bool WinToast::initialize(_Out_opt_ WinToastError* error) { + _isInitialized = false; + setError(error, WinToastError::NoError); + + if (!isCompatible()) { + setError(error, WinToastError::SystemNotSupported); + DEBUG_MSG(L"Error: system not supported."); + return false; + } + + if (_aumi.empty() || _appName.empty()) { + setError(error, WinToastError::InvalidParameters); + DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?"); + return false; + } + + if (_shortcutPolicy != SHORTCUT_POLICY_IGNORE) { + if (createShortcut() < 0) { + setError(error, WinToastError::ShellLinkNotCreated); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + } + + if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) { + setError(error, WinToastError::InvalidAppUserModelID); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + + _isInitialized = true; + return _isInitialized; +} + +bool WinToast::isInitialized() const { + return _isInitialized; +} + +std::wstring const& WinToast::appName() const { + return _appName; +} + +std::wstring const& WinToast::appUserModelId() const { + return _aumi; +} + +HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { + WCHAR path[MAX_PATH] = { L'\0' }; + Util::defaultShellLinkPath(_appName, path); + // Check if the file exist + DWORD attr = GetFileAttributesW(path); + if (attr >= 0xFFFFFFF) { + DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path); + return E_FAIL; + } + + // Let's load the file as shell link to validate. + // - Create a shell link + // - Create a persistant file + // - Load the path as data for the persistant file + // - Read the property AUMI and validate with the current + // - Review if AUMI is equal. + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Load(path, STGM_READWRITE); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar); + if (SUCCEEDED(hr)) { + WCHAR AUMI[MAX_PATH]; + hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); + wasChanged = false; + if (FAILED(hr) || _aumi != AUMI) { + if (_shortcutPolicy == SHORTCUT_POLICY_REQUIRE_CREATE) { + // AUMI Changed for the same app, let's update the current value! =) + wasChanged = true; + PropVariantClear(&appIdPropVar); + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) { + hr = persistFile->Save(path, TRUE); + } + } + } + } + else { + // Not allowed to touch the shortcut to fix the AUMI + hr = E_FAIL; + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::createShellLinkHelper() { + if (_shortcutPolicy != SHORTCUT_POLICY_REQUIRE_CREATE) { + return E_FAIL; + } + + WCHAR exePath[MAX_PATH]{ L'\0' }; + WCHAR slPath[MAX_PATH]{ L'\0' }; + Util::defaultShellLinkPath(_appName, slPath); + Util::defaultExecutablePath(exePath); + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + hr = shellLink->SetPath(exePath); + if (SUCCEEDED(hr)) { + hr = shellLink->SetArguments(L""); + if (SUCCEEDED(hr)) { + hr = shellLink->SetWorkingDirectory(exePath); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Save(slPath, TRUE); + } + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + } + return hr; +} + +INT64 WinToast::showToast(_In_ WinToastTemplate const& toast, _In_ IWinToastHandler* eventHandler, _Out_ WinToastError* error) { + std::shared_ptr handler(eventHandler); + setError(error, WinToastError::NoError); + INT64 id = -1; + if (!isInitialized()) { + setError(error, WinToastError::NotInitialized); + DEBUG_MSG("Error when launching the toast. WinToast is not initialized."); + return id; + } + if (!handler) { + setError(error, WinToastError::InvalidHandler); + DEBUG_MSG("Error when launching the toast. Handler cannot be nullptr."); + return id; + } + + ComPtr notificationManager; + HRESULT hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + ComPtr notifier; + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + if (SUCCEEDED(hr)) { + ComPtr notificationFactory; + hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); + if (SUCCEEDED(hr)) { + ComPtr xmlDocument; + hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); + if (SUCCEEDED(hr) && toast.isToastGeneric()) { + hr = setBindToastGenericHelper(xmlDocument.Get()); + } + if (SUCCEEDED(hr)) { + for (UINT32 i = 0, fieldsCount = static_cast(toast.textFieldsCount()); i < fieldsCount && SUCCEEDED(hr); i++) { + hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i); + } + + // Modern feature are supported Windows > Windows 10 + if (SUCCEEDED(hr) && isSupportingModernFeatures()) { + // Note that we do this *after* using toast.textFieldsCount() to + // iterate/fill the template's text fields, since we're adding yet another text field. + if (SUCCEEDED(hr) && !toast.attributionText().empty()) { + hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText()); + } + + std::array buf; + for (std::size_t i = 0, actionsCount = toast.actionsCount(); i < actionsCount && SUCCEEDED(hr); i++) { + _snwprintf_s(buf.data(), buf.size(), _TRUNCATE, L"%zd", i); + hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf.data()); + } + + if (SUCCEEDED(hr)) { + hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default) + ? hr + : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); + } + + if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) { + hr = addDurationHelper(xmlDocument.Get(), + (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); + } + + if (SUCCEEDED(hr)) { + hr = addScenarioHelper(xmlDocument.Get(), toast.scenario()); + } + + } + else { + DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version"); + } + + if (SUCCEEDED(hr)) { + bool isWin10AnniversaryOrAbove = WinToast::isWin10AnniversaryOrHigher(); + bool isCircleCropHint = isWin10AnniversaryOrAbove ? toast.isCropHintCircle() : false; + hr = toast.hasImage() + ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath(), toast.isToastGeneric(), isCircleCropHint) + : hr; + if (SUCCEEDED(hr) && isWin10AnniversaryOrAbove && toast.hasHeroImage()) { + hr = setHeroImageHelper(xmlDocument.Get(), toast.heroImagePath(), toast.isInlineHeroImage()); + } + if (SUCCEEDED(hr)) { + ComPtr notification; + hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification); + if (SUCCEEDED(hr)) { + INT64 expiration = 0, relativeExpiration = toast.expiration(); + if (relativeExpiration > 0) { + InternalDateTime expirationDateTime(relativeExpiration); + expiration = expirationDateTime; + hr = notification->put_ExpirationTime(&expirationDateTime); + } + + EventRegistrationToken activatedToken, dismissedToken, failedToken; + + GUID guid; + HRESULT hrGuid = CoCreateGuid(&guid); + id = guid.Data1; + if (SUCCEEDED(hr) && SUCCEEDED(hrGuid)) { + hr = Util::setEventHandlers(notification.Get(), handler, expiration, activatedToken, dismissedToken, + failedToken, [this, id]() { markAsReadyForDeletion(id); }); + if (FAILED(hr)) { + setError(error, WinToastError::InvalidHandler); + } + } + + if (SUCCEEDED(hr)) { + if (SUCCEEDED(hr)) { + _buffer.emplace(id, NotifyData(notification, activatedToken, dismissedToken, failedToken)); + DEBUG_MSG("xml: " << Util::AsString(xmlDocument)); + hr = notifier->Show(notification.Get()); + if (FAILED(hr)) { + setError(error, WinToastError::NotDisplayed); + } + } + } + } + } + } + } + } + } + } + return FAILED(hr) ? -1 : id; +} + +ComPtr WinToast::notifier(_In_ bool* succeded) const { + ComPtr notificationManager; + ComPtr notifier; + HRESULT hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + } + *succeded = SUCCEEDED(hr); + return notifier; +} + +void WinToast::markAsReadyForDeletion(_In_ INT64 id) { + // Flush the buffer by removing all the toasts that are ready for deletion + for (auto it = _buffer.begin(); it != _buffer.end();) { + if (it->second.isReadyForDeletion()) { + it->second.RemoveTokens(); + it = _buffer.erase(it); + } + else { + ++it; + } + } + + // Mark the toast as ready for deletion (if it exists) so that it will be removed from the buffer in the next iteration + auto const iter = _buffer.find(id); + if (iter != _buffer.end()) { + _buffer[id].markAsReadyForDeletion(); + } +} + +bool WinToast::hideToast(_In_ INT64 id) { + if (!isInitialized()) { + DEBUG_MSG("Error when hiding the toast. WinToast is not initialized."); + return false; + } + + auto iter = _buffer.find(id); + if (iter == _buffer.end()) { + return false; + } + + auto succeded = false; + auto notify = notifier(&succeded); + if (!succeded) { + return false; + } + + auto& notifyData = iter->second; + auto result = notify->Hide(notifyData.notification()); + if (FAILED(result)) { + DEBUG_MSG("Error when hiding the toast. Error code: " << result); + return false; + } + + notifyData.RemoveTokens(); + _buffer.erase(iter); + return SUCCEEDED(result); +} + +void WinToast::clear() { + auto succeded = false; + auto notify = notifier(&succeded); + if (!succeded) { + return; + } + + for (auto& data : _buffer) { + auto& notifyData = data.second; + notify->Hide(notifyData.notification()); + notifyData.RemoveTokens(); + } + _buffer.clear(); +} + +// +// Available as of Windows 10 Anniversary Update +// Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts +// +// NOTE: This will add a new text field, so be aware when iterating over +// the toast's text fields or getting a count of them. +// +HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text) { + Util::createElement(xml, L"binding", L"text", { L"placement" }); + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 nodeListLength; + hr = nodeList->get_Length(&nodeListLength); + if (SUCCEEDED(hr)) { + for (UINT32 i = 0; i < nodeListLength; i++) { + ComPtr textNode; + hr = nodeList->Item(i, &textNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = textNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode); + if (FAILED(hr) || !editedNode) { + continue; + } + hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml); + if (SUCCEEDED(hr)) { + return setTextFieldHelper(xml, text, i); + } + } + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addDurationHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& duration) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(duration).Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& scenario) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"scenario").Get(), WinToastStringWrapper(scenario).Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text, _In_ UINT32 pos) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(pos, &node); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(text, node.Get(), xml); + } + } + return hr; +} + +HRESULT WinToast::setBindToastGenericHelper(_In_ IXmlDocument* xml) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"binding").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isToastGeneric, + _In_ bool isCropHintCircle) { + assert(path.size() < MAX_PATH); + + wchar_t imagePath[MAX_PATH] = L"file:///"; + HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); + if (SUCCEEDED(hr)) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + + ComPtr imageElement; + HRESULT hrImage = node.As(&imageElement); + if (SUCCEEDED(hr) && SUCCEEDED(hrImage) && isToastGeneric) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"placement").Get(), WinToastStringWrapper(L"appLogoOverride").Get()); + if (SUCCEEDED(hr) && isCropHintCircle) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"hint-crop").Get(), WinToastStringWrapper(L"circle").Get()); + } + } + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + Util::setNodeStringValue(imagePath, editedNode.Get(), xml); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, + _In_opt_ WinToastTemplate::AudioOption option) { + std::vector attrs; + if (!path.empty()) { + attrs.push_back(L"src"); + } + if (option == WinToastTemplate::AudioOption::Loop) { + attrs.push_back(L"loop"); + } + if (option == WinToastTemplate::AudioOption::Silent) { + attrs.push_back(L"silent"); + } + Util::createElement(xml, L"toast", L"audio", attrs); + + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (!path.empty()) { + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(path, editedNode.Get(), xml); + } + } + } + + if (SUCCEEDED(hr)) { + switch (option) { + case WinToastTemplate::AudioOption::Loop: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + break; + case WinToastTemplate::AudioOption::Silent: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + default: + break; + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addActionHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& content, _In_ std::wstring const& arguments) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr actionsNode; + if (length > 0) { + hr = nodeList->Item(0, &actionsNode); + } + else { + hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), + WinToastStringWrapper(L"ToastGeneric").Get()); + } + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), + WinToastStringWrapper(L"long").Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionsElement; + hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement); + if (SUCCEEDED(hr)) { + hr = actionsElement.As(&actionsNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild); + } + } + } + } + } + } + } + if (SUCCEEDED(hr)) { + ComPtr actionElement; + hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement); + if (SUCCEEDED(hr)) { + hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get()); + } + if (SUCCEEDED(hr)) { + hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = actionElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::setHeroImageHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isInlineImage) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"binding").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr bindingNode; + if (length > 0) { + hr = nodeList->Item(0, &bindingNode); + } + if (SUCCEEDED(hr)) { + ComPtr imageElement; + hr = xml->CreateElement(WinToastStringWrapper(L"image").Get(), &imageElement); + if (SUCCEEDED(hr) && isInlineImage == false) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"placement").Get(), WinToastStringWrapper(L"hero").Get()); + } + if (SUCCEEDED(hr)) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"src").Get(), WinToastStringWrapper(path).Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = imageElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = bindingNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + +void WinToast::setError(_Out_opt_ WinToastError* error, _In_ WinToastError value) { + if (error) { + *error = value; + } +} + +WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) { + constexpr static std::size_t TextFieldsCount[] = { 1, 2, 2, 3, 1, 2, 2, 3 }; + _textFields = std::vector(TextFieldsCount[type], L""); +} + +WinToastTemplate::~WinToastTemplate() { + _textFields.clear(); +} + +void WinToastTemplate::setTextField(_In_ std::wstring const& txt, _In_ WinToastTemplate::TextField pos) { + auto const position = static_cast(pos); + if (position >= _textFields.size()) { + DEBUG_MSG("The selected template type supports only " << _textFields.size() << " text lines"); + return; + } + _textFields[position] = txt; +} + +void WinToastTemplate::setImagePath(_In_ std::wstring const& imgPath, _In_ CropHint cropHint) { + _imagePath = imgPath; + _cropHint = cropHint; +} + +void WinToastTemplate::setHeroImagePath(_In_ std::wstring const& imgPath, _In_ bool inlineImage) { + _heroImagePath = imgPath; + _inlineHeroImage = inlineImage; +} + +void WinToastTemplate::setAudioPath(_In_ std::wstring const& audioPath) { + _audioPath = audioPath; +} + +void WinToastTemplate::setAudioPath(_In_ AudioSystemFile file) { + static const std::unordered_map Files = { + {AudioSystemFile::DefaultSound, L"ms-winsoundevent:Notification.Default" }, + {AudioSystemFile::IM, L"ms-winsoundevent:Notification.IM" }, + {AudioSystemFile::Mail, L"ms-winsoundevent:Notification.Mail" }, + {AudioSystemFile::Reminder, L"ms-winsoundevent:Notification.Reminder" }, + {AudioSystemFile::SMS, L"ms-winsoundevent:Notification.SMS" }, + {AudioSystemFile::Alarm, L"ms-winsoundevent:Notification.Looping.Alarm" }, + {AudioSystemFile::Alarm2, L"ms-winsoundevent:Notification.Looping.Alarm2" }, + {AudioSystemFile::Alarm3, L"ms-winsoundevent:Notification.Looping.Alarm3" }, + {AudioSystemFile::Alarm4, L"ms-winsoundevent:Notification.Looping.Alarm4" }, + {AudioSystemFile::Alarm5, L"ms-winsoundevent:Notification.Looping.Alarm5" }, + {AudioSystemFile::Alarm6, L"ms-winsoundevent:Notification.Looping.Alarm6" }, + {AudioSystemFile::Alarm7, L"ms-winsoundevent:Notification.Looping.Alarm7" }, + {AudioSystemFile::Alarm8, L"ms-winsoundevent:Notification.Looping.Alarm8" }, + {AudioSystemFile::Alarm9, L"ms-winsoundevent:Notification.Looping.Alarm9" }, + {AudioSystemFile::Alarm10, L"ms-winsoundevent:Notification.Looping.Alarm10"}, + {AudioSystemFile::Call, L"ms-winsoundevent:Notification.Looping.Call" }, + {AudioSystemFile::Call1, L"ms-winsoundevent:Notification.Looping.Call1" }, + {AudioSystemFile::Call2, L"ms-winsoundevent:Notification.Looping.Call2" }, + {AudioSystemFile::Call3, L"ms-winsoundevent:Notification.Looping.Call3" }, + {AudioSystemFile::Call4, L"ms-winsoundevent:Notification.Looping.Call4" }, + {AudioSystemFile::Call5, L"ms-winsoundevent:Notification.Looping.Call5" }, + {AudioSystemFile::Call6, L"ms-winsoundevent:Notification.Looping.Call6" }, + {AudioSystemFile::Call7, L"ms-winsoundevent:Notification.Looping.Call7" }, + {AudioSystemFile::Call8, L"ms-winsoundevent:Notification.Looping.Call8" }, + {AudioSystemFile::Call9, L"ms-winsoundevent:Notification.Looping.Call9" }, + {AudioSystemFile::Call10, L"ms-winsoundevent:Notification.Looping.Call10" }, + }; + auto const iter = Files.find(file); + assert(iter != Files.end()); + _audioPath = iter->second; +} + +void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) { + _audioOption = audioOption; +} + +void WinToastTemplate::setFirstLine(_In_ std::wstring const& text) { + setTextField(text, WinToastTemplate::FirstLine); +} + +void WinToastTemplate::setSecondLine(_In_ std::wstring const& text) { + setTextField(text, WinToastTemplate::SecondLine); +} + +void WinToastTemplate::setThirdLine(_In_ std::wstring const& text) { + setTextField(text, WinToastTemplate::ThirdLine); +} + +void WinToastTemplate::setDuration(_In_ Duration duration) { + _duration = duration; +} + +void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) { + _expiration = millisecondsFromNow; +} + +void WinToastLib::WinToastTemplate::setScenario(_In_ Scenario scenario) { + switch (scenario) { + case Scenario::Default: + _scenario = L"Default"; + break; + case Scenario::Alarm: + _scenario = L"Alarm"; + break; + case Scenario::IncomingCall: + _scenario = L"IncomingCall"; + break; + case Scenario::Reminder: + _scenario = L"Reminder"; + break; + } +} + +void WinToastTemplate::setAttributionText(_In_ std::wstring const& attributionText) { + _attributionText = attributionText; +} + +void WinToastTemplate::addAction(_In_ std::wstring const& label) { + _actions.push_back(label); +} + +std::size_t WinToastTemplate::textFieldsCount() const { + return _textFields.size(); +} + +std::size_t WinToastTemplate::actionsCount() const { + return _actions.size(); +} + +bool WinToastTemplate::hasImage() const { + return _type < WinToastTemplateType::Text01; +} + +bool WinToastTemplate::hasHeroImage() const { + return hasImage() && !_heroImagePath.empty(); +} + +std::vector const& WinToastTemplate::textFields() const { + return _textFields; +} + +std::wstring const& WinToastTemplate::textField(_In_ TextField pos) const { + auto const position = static_cast(pos); + assert(position < _textFields.size()); + return _textFields[position]; +} + +std::wstring const& WinToastTemplate::actionLabel(_In_ std::size_t position) const { + assert(position < _actions.size()); + return _actions[position]; +} + +std::wstring const& WinToastTemplate::imagePath() const { + return _imagePath; +} + +std::wstring const& WinToastTemplate::heroImagePath() const { + return _heroImagePath; +} + +std::wstring const& WinToastTemplate::audioPath() const { + return _audioPath; +} + +std::wstring const& WinToastTemplate::attributionText() const { + return _attributionText; +} + +std::wstring const& WinToastLib::WinToastTemplate::scenario() const { + return _scenario; +} + +INT64 WinToastTemplate::expiration() const { + return _expiration; +} + +WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const { + return _type; +} + +WinToastTemplate::AudioOption WinToastTemplate::audioOption() const { + return _audioOption; +} + +WinToastTemplate::Duration WinToastTemplate::duration() const { + return _duration; +} + +bool WinToastTemplate::isToastGeneric() const { + return hasHeroImage() || _cropHint == WinToastTemplate::Circle; +} + +bool WinToastTemplate::isInlineHeroImage() const { + return _inlineHeroImage; +} + +bool WinToastTemplate::isCropHintCircle() const { + return _cropHint == CropHint::Circle; +} \ No newline at end of file diff --git a/UI/ToastUi/wintoastlib.h b/UI/ToastUi/wintoastlib.h new file mode 100644 index 0000000..fd84e0f --- /dev/null +++ b/UI/ToastUi/wintoastlib.h @@ -0,0 +1,310 @@ +/** + * MIT License + * + * Copyright (C) 2016-2023 WinToast v1.3.0 - Mohammed Boujemaoui + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef WINTOASTLIB_H +#define WINTOASTLIB_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Windows::Foundation; + +namespace WinToastLib { + + class IWinToastHandler { + public: + enum WinToastDismissalReason { + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + }; + + virtual ~IWinToastHandler() = default; + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastDismissed(WinToastDismissalReason state) const = 0; + virtual void toastFailed() const = 0; + }; + + class WinToastTemplate { + public: + enum class Scenario { Default, Alarm, IncomingCall, Reminder }; + enum Duration { System, Short, Long }; + enum AudioOption { Default = 0, Silent, Loop }; + enum TextField { FirstLine = 0, SecondLine, ThirdLine }; + + enum WinToastTemplateType { + ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, + ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, + ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, + ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04 + }; + + enum AudioSystemFile { + DefaultSound, + IM, + Mail, + Reminder, + SMS, + Alarm, + Alarm2, + Alarm3, + Alarm4, + Alarm5, + Alarm6, + Alarm7, + Alarm8, + Alarm9, + Alarm10, + Call, + Call1, + Call2, + Call3, + Call4, + Call5, + Call6, + Call7, + Call8, + Call9, + Call10, + }; + + enum CropHint { + Square, + Circle, + }; + + WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); + ~WinToastTemplate(); + + void setFirstLine(_In_ std::wstring const& text); + void setSecondLine(_In_ std::wstring const& text); + void setThirdLine(_In_ std::wstring const& text); + void setTextField(_In_ std::wstring const& txt, _In_ TextField pos); + void setAttributionText(_In_ std::wstring const& attributionText); + void setImagePath(_In_ std::wstring const& imgPath, _In_ CropHint cropHint = CropHint::Square); + void setHeroImagePath(_In_ std::wstring const& imgPath, _In_ bool inlineImage = false); + void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio); + void setAudioPath(_In_ std::wstring const& audioPath); + void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); + void setDuration(_In_ Duration duration); + void setExpiration(_In_ INT64 millisecondsFromNow); + void setScenario(_In_ Scenario scenario); + void addAction(_In_ std::wstring const& label); + + std::size_t textFieldsCount() const; + std::size_t actionsCount() const; + bool hasImage() const; + bool hasHeroImage() const; + std::vector const& textFields() const; + std::wstring const& textField(_In_ TextField pos) const; + std::wstring const& actionLabel(_In_ std::size_t pos) const; + std::wstring const& imagePath() const; + std::wstring const& heroImagePath() const; + std::wstring const& audioPath() const; + std::wstring const& attributionText() const; + std::wstring const& scenario() const; + INT64 expiration() const; + WinToastTemplateType type() const; + WinToastTemplate::AudioOption audioOption() const; + Duration duration() const; + bool isToastGeneric() const; + bool isInlineHeroImage() const; + bool isCropHintCircle() const; + + private: + std::vector _textFields{}; + std::vector _actions{}; + std::wstring _imagePath{}; + std::wstring _heroImagePath{}; + bool _inlineHeroImage{ false }; + std::wstring _audioPath{}; + std::wstring _attributionText{}; + std::wstring _scenario{ L"Default" }; + INT64 _expiration{ 0 }; + AudioOption _audioOption{ WinToastTemplate::AudioOption::Default }; + WinToastTemplateType _type{ WinToastTemplateType::Text01 }; + Duration _duration{ Duration::System }; + CropHint _cropHint{ CropHint::Square }; + }; + + class WinToast { + public: + enum WinToastError { + NoError = 0, + NotInitialized, + SystemNotSupported, + ShellLinkNotCreated, + InvalidAppUserModelID, + InvalidParameters, + InvalidHandler, + NotDisplayed, + UnknownError + }; + + enum ShortcutResult { + SHORTCUT_UNCHANGED = 0, + SHORTCUT_WAS_CHANGED = 1, + SHORTCUT_WAS_CREATED = 2, + + SHORTCUT_MISSING_PARAMETERS = -1, + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + SHORTCUT_CREATE_FAILED = -4 + }; + + enum ShortcutPolicy { + /* Don't check, create, or modify a shortcut. */ + SHORTCUT_POLICY_IGNORE = 0, + /* Require a shortcut with matching AUMI, don't create or modify an existing one. */ + SHORTCUT_POLICY_REQUIRE_NO_CREATE = 1, + /* Require a shortcut with matching AUMI, create if missing, modify if not matching. This is the default. */ + SHORTCUT_POLICY_REQUIRE_CREATE = 2, + }; + + WinToast(void); + virtual ~WinToast(); + static WinToast* instance(); + static bool isCompatible(); + static bool isSupportingModernFeatures(); + static bool isWin10AnniversaryOrHigher(); + static std::wstring configureAUMI(_In_ std::wstring const& companyName, _In_ std::wstring const& productName, + _In_ std::wstring const& subProduct = std::wstring(), + _In_ std::wstring const& versionInformation = std::wstring()); + static std::wstring const& strerror(_In_ WinToastError error); + virtual bool initialize(_Out_opt_ WinToastError* error = nullptr); + virtual bool isInitialized() const; + virtual bool hideToast(_In_ INT64 id); + virtual INT64 showToast(_In_ WinToastTemplate const& toast, _In_ IWinToastHandler* eventHandler, + _Out_opt_ WinToastError* error = nullptr); + virtual void clear(); + virtual enum ShortcutResult createShortcut(); + + std::wstring const& appName() const; + std::wstring const& appUserModelId() const; + void setAppUserModelId(_In_ std::wstring const& aumi); + void setAppName(_In_ std::wstring const& appName); + void setShortcutPolicy(_In_ ShortcutPolicy policy); + + protected: + struct NotifyData { + NotifyData() {}; + NotifyData(_In_ ComPtr notify, _In_ EventRegistrationToken activatedToken, + _In_ EventRegistrationToken dismissedToken, _In_ EventRegistrationToken failedToken) : + _notify(notify), _activatedToken(activatedToken), _dismissedToken(dismissedToken), _failedToken(failedToken) {} + + ~NotifyData() { + RemoveTokens(); + } + + void RemoveTokens() { + if (!_readyForDeletion) { + return; + } + + if (_previouslyTokenRemoved) { + return; + } + + if (!_notify.Get()) { + return; + } + + _notify->remove_Activated(_activatedToken); + _notify->remove_Dismissed(_dismissedToken); + _notify->remove_Failed(_failedToken); + _previouslyTokenRemoved = true; + } + + void markAsReadyForDeletion() { + _readyForDeletion = true; + } + + bool isReadyForDeletion() const { + return _readyForDeletion; + } + + IToastNotification* notification() { + return _notify.Get(); + } + + private: + ComPtr _notify{ nullptr }; + EventRegistrationToken _activatedToken{}; + EventRegistrationToken _dismissedToken{}; + EventRegistrationToken _failedToken{}; + bool _readyForDeletion{ false }; + bool _previouslyTokenRemoved{ false }; + }; + + bool _isInitialized{ false }; + bool _hasCoInitialized{ false }; + ShortcutPolicy _shortcutPolicy{ SHORTCUT_POLICY_REQUIRE_CREATE }; + std::wstring _appName{}; + std::wstring _aumi{}; + std::map _buffer{}; + + void markAsReadyForDeletion(_In_ INT64 id); + HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); + HRESULT createShellLinkHelper(); + HRESULT setImageFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isToastGeneric, bool isCropHintCircle); + HRESULT setHeroImageHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isInlineImage); + HRESULT setBindToastGenericHelper(_In_ IXmlDocument* xml); + HRESULT + setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, + _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text, _In_ UINT32 pos); + HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text); + HRESULT addActionHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& action, _In_ std::wstring const& arguments); + HRESULT addDurationHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& duration); + HRESULT addScenarioHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& scenario); + ComPtr notifier(_In_ bool* succeded) const; + void setError(_Out_opt_ WinToastError* error, _In_ WinToastError value); + }; +} // namespace WinToastLib +#endif // WINTOASTLIB_H \ No newline at end of file diff --git a/UI/UI.cpp b/UI/UI.cpp new file mode 100644 index 0000000..cf53d0f --- /dev/null +++ b/UI/UI.cpp @@ -0,0 +1,20 @@ +// UI.cpp : 定义应用程序的入口点。 +// + +#include "framework.h" +#include "UI.h" + + +int APIENTRY wWinMain(_In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, + _In_ LPWSTR lpCmdLine, + _In_ int nCmdShow) +{ + UNREFERENCED_PARAMETER(hPrevInstance); + UNREFERENCED_PARAMETER(lpCmdLine); + + // 获取桌面窗口的句柄 + HWND desktopWnd = GetDesktopWindow(); + int result = MessageBox(desktopWnd, TEXT("10s后会退出PLM系统,立即退出点击确定..."), TEXT("警告"), MB_OKCANCEL | MB_ICONWARNING| MB_SYSTEMMODAL); + return result==1 || result == 2 ? result:0; //1:确定 2:取消 +} diff --git a/UI/UI.h b/UI/UI.h new file mode 100644 index 0000000..d00d47e --- /dev/null +++ b/UI/UI.h @@ -0,0 +1,3 @@ +#pragma once + +#include "resource.h" diff --git a/UI/UI.ico b/UI/UI.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3ec03bd617f32e58128fa977fd6ac9605124f4b GIT binary patch literal 46227 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 z + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {e14fe1cc-27a8-494f-b023-0993e73246c4} + UI + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)output\$(Configuration)\ + + + + Level3 + true + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + true + true + + + + + Level3 + true + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + + + + + Level3 + true + true + true + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters new file mode 100644 index 0000000..fa7d46a --- /dev/null +++ b/UI/UI.vcxproj.filters @@ -0,0 +1,61 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {540242c3-90b6-4ec0-92de-c175af531715} + + + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + ToastUi + + + + + 源文件 + + + ToastUi + + + ToastUi + + + + + 资源文件 + + + + + 资源文件 + + + 资源文件 + + + \ No newline at end of file diff --git a/UI/UI.vcxproj.user b/UI/UI.vcxproj.user new file mode 100644 index 0000000..c8702e5 --- /dev/null +++ b/UI/UI.vcxproj.user @@ -0,0 +1,6 @@ + + + + WindowsLocalDebugger + + \ No newline at end of file diff --git a/UI/framework.h b/UI/framework.h new file mode 100644 index 0000000..20835fd --- /dev/null +++ b/UI/framework.h @@ -0,0 +1,15 @@ +// header.h: 标准系统包含文件的包含文件, +// 或特定于项目的包含文件 +// + +#pragma once + +#include "targetver.h" +#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容 +// Windows 头文件 +#include +// C 运行时头文件 +#include +#include +#include +#include diff --git a/UI/small.ico b/UI/small.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3ec03bd617f32e58128fa977fd6ac9605124f4b GIT binary patch literal 46227 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