Selaa lähdekoodia

feat: 适配windows底层窗口

Tony 3 kuukautta sitten
vanhempi
commit
892f79200b

+ 7 - 0
windows/runner/CMakeLists.txt

@@ -9,8 +9,12 @@ project(runner LANGUAGES CXX)
 add_executable(${BINARY_NAME} WIN32
   "flutter_window.cpp"
   "main.cpp"
+  "dump.cpp"
   "utils.cpp"
   "win32_window.cpp"
+  "file_icon.cpp"
+  "file_info.cpp"
+  "tray_icon.cpp"
   "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
   "Runner.rc"
   "runner.exe.manifest"
@@ -34,6 +38,9 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
 # dependencies here.
 target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
 target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_link_libraries(${BINARY_NAME} PRIVATE "version.lib")
+target_link_libraries(${BINARY_NAME} PRIVATE "gdiplus.lib")
+target_link_libraries(${BINARY_NAME} PRIVATE "Shlwapi.lib")
 target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
 
 # Run the Flutter tool portions of the build. This must not be removed.

+ 6 - 6
windows/runner/Runner.rc

@@ -89,13 +89,13 @@ BEGIN
     BEGIN
         BLOCK "040904e4"
         BEGIN
-            VALUE "CompanyName", "com.example" "\0"
-            VALUE "FileDescription", "ixvpn" "\0"
+            VALUE "CompanyName", "app.nomo" "\0"
+            VALUE "FileDescription", "nomo" "\0"
             VALUE "FileVersion", VERSION_AS_STRING "\0"
-            VALUE "InternalName", "ixvpn" "\0"
-            VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
-            VALUE "OriginalFilename", "ixvpn.exe" "\0"
-            VALUE "ProductName", "ixvpn" "\0"
+            VALUE "InternalName", "nomo" "\0"
+            VALUE "LegalCopyright", "Copyright (C) 2025 app.nomo. All rights reserved." "\0"
+            VALUE "OriginalFilename", "nomo.exe" "\0"
+            VALUE "ProductName", "nomo" "\0"
             VALUE "ProductVersion", VERSION_AS_STRING "\0"
         END
     END

+ 68 - 0
windows/runner/dump.cpp

@@ -0,0 +1,68 @@
+#include <Windows.h>
+#include <DbgHelp.h>
+
+#include "dump.h"
+
+int GenerateMiniDump(PEXCEPTION_POINTERS pExceptionPointers) {
+  typedef BOOL(WINAPI * MiniDumpWriteDumpT)(
+    HANDLE,
+    DWORD,
+    HANDLE,
+    MINIDUMP_TYPE,
+    PMINIDUMP_EXCEPTION_INFORMATION,
+    PMINIDUMP_USER_STREAM_INFORMATION,
+    PMINIDUMP_CALLBACK_INFORMATION
+    );
+ 
+  MiniDumpWriteDumpT pfnMiniDumpWriteDump = NULL;
+  HMODULE hDbgHelp = LoadLibrary(L"dbghelp.dll");
+  if (NULL == hDbgHelp) {
+    return EXCEPTION_CONTINUE_EXECUTION;
+  }
+ 
+  pfnMiniDumpWriteDump = (MiniDumpWriteDumpT)GetProcAddress(hDbgHelp, "MiniDumpWriteDump");
+  if (NULL == pfnMiniDumpWriteDump) {
+    FreeLibrary(hDbgHelp);
+    return EXCEPTION_CONTINUE_EXECUTION;
+  }
+
+  TCHAR szFileName[MAX_PATH] = { 0 };
+  //TCHAR* szVersion = SOFTWARE_VERSION;
+
+  // convert flutter version chat to tchar
+  TCHAR szVersion[MAX_PATH] = { 0 };
+  const char* pszMultiByteVersion = FLUTTER_VERSION;
+  int iSize = MultiByteToWideChar(CP_ACP, 0, pszMultiByteVersion , -1, NULL, 0);
+  MultiByteToWideChar(CP_ACP, 0, pszMultiByteVersion, -1, szVersion, iSize);
+
+  SYSTEMTIME stLocalTime;
+  GetLocalTime(&stLocalTime);
+  wsprintf(szFileName, L"%04d%02d%02d_%02d%02d%02d_%s.dmp",
+    stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,
+    stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond, szVersion);
+ 
+  HANDLE hDumpFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE,
+    FILE_SHARE_WRITE | FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
+  if (INVALID_HANDLE_VALUE == hDumpFile) {
+    FreeLibrary(hDbgHelp);
+    return EXCEPTION_CONTINUE_EXECUTION;
+  }
+
+  MINIDUMP_EXCEPTION_INFORMATION expParam;
+  expParam.ThreadId = GetCurrentThreadId();
+  expParam.ExceptionPointers = pExceptionPointers;
+  expParam.ClientPointers = FALSE;
+  pfnMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
+    hDumpFile, MiniDumpWithDataSegs, (pExceptionPointers ? &expParam : NULL), NULL, NULL);
+ 
+  CloseHandle(hDumpFile);
+  FreeLibrary(hDbgHelp);
+  return EXCEPTION_EXECUTE_HANDLER;
+}
+
+LONG ApplicationCrashHandler(LPEXCEPTION_POINTERS lpExceptionInfo){
+  if (IsDebuggerPresent()) {
+    return EXCEPTION_CONTINUE_SEARCH;
+  }  
+  return GenerateMiniDump(lpExceptionInfo);
+}

+ 6 - 0
windows/runner/dump.h

@@ -0,0 +1,6 @@
+#pragma once
+
+//#define SOFTWARE_VERSION L"V1.0.202203"
+
+int GenerateMiniDump(PEXCEPTION_POINTERS pExceptionPointers);
+LONG ApplicationCrashHandler(LPEXCEPTION_POINTERS lpExceptionInfo);

+ 379 - 0
windows/runner/file_icon.cpp

@@ -0,0 +1,379 @@
+#include <Windows.h>
+#include <Commctrl.h>
+#include <CommonControls.h>
+#include <algorithm>
+#include <cstddef>
+#include <cwchar>
+#include <memory>
+#include <vector>
+#include <system_error>
+#include <shlwapi.h>
+
+#include "file_icon.h"
+
+#pragma warning(disable:4458)
+#pragma warning(disable:4267)
+
+namespace Gdiplus
+{
+    using std::max;
+    using std::min;
+} // namespace Gdiplus
+#include <Gdiplus.h>
+
+class ComInit
+{
+public:
+    ComInit() { CoInitializeEx(0, COINIT_MULTITHREADED); }
+
+    ~ComInit() { CoUninitialize(); }
+
+private:
+    ComInit(const ComInit &);
+    ComInit &operator=(const ComInit &);
+};
+
+class GdiPlusInit
+{
+public:
+    GdiPlusInit()
+    {
+        Gdiplus::GdiplusStartupInput startupInput;
+        Gdiplus::GdiplusStartup(std::addressof(this->token),
+                                std::addressof(startupInput), nullptr);
+    }
+
+    ~GdiPlusInit() { Gdiplus::GdiplusShutdown(this->token); }
+
+private:
+    GdiPlusInit(const GdiPlusInit &);
+    GdiPlusInit &operator=(const GdiPlusInit &);
+
+    ULONG_PTR token;
+};
+
+struct IStreamDeleter
+{
+    void operator()(IStream *pStream) const { pStream->Release(); }
+};
+
+std::unique_ptr<Gdiplus::Bitmap> CreateBitmapFromIcon(
+    HICON hIcon, std::vector<std::int32_t> &buffer)
+{
+    ICONINFO iconInfo = {0};
+    GetIconInfo(hIcon, std::addressof(iconInfo));
+
+    BITMAP bm = {0};
+    GetObject(iconInfo.hbmColor, sizeof(bm), std::addressof(bm));
+
+    std::unique_ptr<Gdiplus::Bitmap> bitmap;
+
+    if (bm.bmBitsPixel == 32) {
+        auto hDC = GetDC(nullptr);
+
+        BITMAPINFO bmi = {0};
+        bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
+        bmi.bmiHeader.biWidth = bm.bmWidth;
+        bmi.bmiHeader.biHeight = -bm.bmHeight;
+        bmi.bmiHeader.biPlanes = 1;
+        bmi.bmiHeader.biBitCount = 32;
+        bmi.bmiHeader.biCompression = BI_RGB;
+
+        auto nBits = bm.bmWidth * bm.bmHeight;
+        buffer.resize(nBits);
+        GetDIBits(hDC, iconInfo.hbmColor, 0, bm.bmHeight,
+                  std::addressof(buffer[0]), std::addressof(bmi),
+                  DIB_RGB_COLORS);
+
+        auto hasAlpha = false;
+        for (std::int32_t i = 0; i < nBits; i++) {
+            if ((buffer[i] & 0xFF000000) != 0) {
+                hasAlpha = true;
+                break;
+            }
+        }
+
+        if (!hasAlpha) {
+            std::vector<std::int32_t> maskBits(nBits);
+            GetDIBits(hDC, iconInfo.hbmMask, 0, bm.bmHeight,
+                      std::addressof(maskBits[0]), std::addressof(bmi),
+                      DIB_RGB_COLORS);
+
+            for (std::int32_t i = 0; i < nBits; i++) {
+                if (maskBits[i] == 0) {
+                    buffer[i] |= 0xFF000000;
+                }
+            }
+        }
+
+        bitmap.reset(new Gdiplus::Bitmap(
+            bm.bmWidth, bm.bmHeight, bm.bmWidth * sizeof(std::int32_t),
+            PixelFormat32bppARGB,
+            static_cast<BYTE *>(
+                static_cast<void *>(std::addressof(buffer[0])))));
+
+        ReleaseDC(nullptr, hDC);
+    } else {
+        bitmap.reset(Gdiplus::Bitmap::FromHICON(hIcon));
+    }
+
+    DeleteObject(iconInfo.hbmColor);
+    DeleteObject(iconInfo.hbmMask);
+
+    return bitmap;
+}
+
+int GetEncoderClsid(const WCHAR *format, CLSID *pClsid)
+{
+    UINT num = 0u;
+    UINT size = 0u;
+
+    Gdiplus::GetImageEncodersSize(std::addressof(num), std::addressof(size));
+
+    if (size == 0u)
+    {
+        return -1;
+    }
+
+    std::unique_ptr<Gdiplus::ImageCodecInfo> pImageCodecInfo(
+        static_cast<Gdiplus::ImageCodecInfo *>(
+            static_cast<void *>(new BYTE[size])));
+
+    if (pImageCodecInfo == nullptr)
+    {
+        return -1;
+    }
+
+    GetImageEncoders(num, size, pImageCodecInfo.get());
+
+    for (UINT i = 0u; i < num; i++)
+    {
+        if (std::wcscmp(pImageCodecInfo.get()[i].MimeType, format) == 0)
+        {
+            *pClsid = pImageCodecInfo.get()[i].Clsid;
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+std::wstring Utf8ToWide(const std::string &src)
+{
+    const auto size =
+        MultiByteToWideChar(CP_UTF8, 0u, src.data(), -1, nullptr, 0u);
+    std::vector<wchar_t> dest(size, L'\0');
+
+    if (MultiByteToWideChar(CP_UTF8, 0u, src.data(), -1, dest.data(), dest.size()) == 0)
+    {
+        throw std::system_error{static_cast<int>(GetLastError()),
+                                std::system_category()};
+    }
+
+    return std::wstring{dest.begin(), dest.end()};
+}
+
+std::vector<unsigned char> HIconToPNGBuffer(HICON hIcon)
+{
+    GdiPlusInit init;
+
+    std::vector<std::int32_t> buffer;
+    auto bitmap = CreateBitmapFromIcon(hIcon, buffer);
+
+    CLSID encoder;
+    if (GetEncoderClsid(L"image/png", std::addressof(encoder)) == -1) {
+        return std::vector<unsigned char>{};
+    }
+
+    IStream *tmp;
+    if (CreateStreamOnHGlobal(nullptr, TRUE, std::addressof(tmp)) != S_OK) {
+        return std::vector<unsigned char>{};
+    }
+
+    std::unique_ptr<IStream, IStreamDeleter> pStream{tmp};
+
+    if (bitmap->Save(pStream.get(), std::addressof(encoder), nullptr) !=
+        Gdiplus::Status::Ok) {
+        return std::vector<unsigned char>{};
+    }
+
+    STATSTG stg = {0};
+    LARGE_INTEGER offset = {0};
+
+    if (pStream->Stat(std::addressof(stg), STATFLAG_NONAME) != S_OK ||
+        pStream->Seek(offset, STREAM_SEEK_SET, nullptr) != S_OK) {
+        return std::vector<unsigned char>{};
+    }
+
+    std::vector<unsigned char> result(
+        static_cast<std::size_t>(stg.cbSize.QuadPart));
+    ULONG ul;
+
+    if (pStream->Read(std::addressof(result[0]),
+                      static_cast<ULONG>(stg.cbSize.QuadPart),
+                      std::addressof(ul)) != S_OK ||
+        stg.cbSize.QuadPart != ul) {
+        return std::vector<unsigned char>{};
+    }
+
+    return result;
+}
+
+bool HIconToPNGFile(HICON hIcon, const std::string &pngFile)
+{
+    GdiPlusInit init;
+
+    std::vector<std::int32_t> buffer;
+    auto bitmap = CreateBitmapFromIcon(hIcon, buffer);
+
+    CLSID encoder;
+    if (GetEncoderClsid(L"image/png", std::addressof(encoder)) == -1) {
+        return false;
+    }
+
+    IStream *tmp;
+    if (SHCreateStreamOnFile(Utf8ToWide(pngFile).c_str(), STGM_WRITE|STGM_CREATE, std::addressof(tmp)) != S_OK) {
+        return false;
+    }
+
+    std::unique_ptr<IStream, IStreamDeleter> pStream{tmp};
+
+    if (bitmap->Save(pStream.get(), std::addressof(encoder), nullptr) !=
+        Gdiplus::Status::Ok) {
+        return false;
+    }
+
+    //pStream->Release();
+
+    return true;
+}
+
+std::vector<unsigned char> GetIcon(const std::string &name, int size,
+                                   UINT flag)
+{
+    ComInit init;
+
+    flag |= SHGFI_ICON;
+
+    switch (size)
+    {
+    case 16:
+        flag |= SHGFI_SMALLICON;
+        break;
+    case 32:
+        flag |= SHGFI_LARGEICON;
+        break;
+    case 64:
+    case 256:
+        flag |= SHGFI_SYSICONINDEX;
+        break;
+    }
+
+    SHFILEINFOW sfi = {0};
+    auto hr = SHGetFileInfoW(Utf8ToWide(name).c_str(), 0, std::addressof(sfi),
+                             sizeof(sfi), flag);
+    HICON hIcon;
+
+    if (FAILED(hr))
+    {
+        return std::vector<unsigned char>{};
+    }
+
+    if (size == 16 || size == 32)
+    {
+        hIcon = sfi.hIcon;
+    }
+    else
+    {
+        HIMAGELIST *imageList;
+        hr = SHGetImageList(
+            size == 64 ? SHIL_EXTRALARGE : SHIL_JUMBO, IID_IImageList,
+            static_cast<void **>(
+                static_cast<void *>(std::addressof(imageList))));
+
+        if (FAILED(hr))
+        {
+            return std::vector<unsigned char>{};
+        }
+
+        hr = static_cast<IImageList *>(static_cast<void *>(imageList))
+                 ->GetIcon(sfi.iIcon, ILD_TRANSPARENT, std::addressof(hIcon));
+
+        if (FAILED(hr))
+        {
+            return std::vector<unsigned char>{};
+        }
+    }
+
+    auto buffer = HIconToPNGBuffer(hIcon);
+    DestroyIcon(hIcon);
+    return buffer;
+}
+
+bool SaveIcon(const std::string &name, const std::string &pngName, int size, UINT flag)
+{
+    ComInit init;
+
+    flag |= SHGFI_ICON;
+
+    switch (size)
+    {
+    case 16:
+        flag |= SHGFI_SMALLICON;
+        break;
+    case 32:
+        flag |= SHGFI_LARGEICON;
+        break;
+    case 64:
+    case 256:
+        flag |= SHGFI_SYSICONINDEX;
+        break;
+    }
+
+    SHFILEINFOW sfi = {0};
+    auto hr = SHGetFileInfoW(Utf8ToWide(name).c_str(), 0, std::addressof(sfi),
+                             sizeof(sfi), flag);
+    HICON hIcon;
+
+    if (FAILED(hr)) {
+        return false;
+    }
+
+    if (size == 16 || size == 32) {
+        hIcon = sfi.hIcon;
+    } else {
+        HIMAGELIST *imageList;
+        hr = SHGetImageList(
+            size == 64 ? SHIL_EXTRALARGE : SHIL_JUMBO, IID_IImageList,
+            static_cast<void **>(
+                static_cast<void *>(std::addressof(imageList))));
+
+        if (FAILED(hr)) {
+            return false;
+        }
+
+        hr = static_cast<IImageList *>(static_cast<void *>(imageList))
+                 ->GetIcon(sfi.iIcon, ILD_TRANSPARENT, std::addressof(hIcon));
+
+        if (FAILED(hr)) {
+            return false;
+        }
+    }
+
+    auto ret = HIconToPNGFile(hIcon, pngName);
+    DestroyIcon(hIcon);
+    return ret;
+}
+
+// Napi::Buffer<char> getIcon(const Napi::CallbackInfo &info)
+// {
+//     Napi::Env env{info.Env()};
+
+//     auto path{info[0].As<Napi::String>().Utf8Value()};
+//     auto data = GetIcon(path, info[1].As<Napi::Number>().Int32Value(), 0);
+
+//     uint8_t *arr = &data[0];
+
+//     return Napi::Buffer<char>::Copy(
+//         env, static_cast<char *>(static_cast<void *>(arr)), data.size());
+// }

+ 6 - 0
windows/runner/file_icon.h

@@ -0,0 +1,6 @@
+#include <windows.h>
+#include <vector>
+#include <string>
+
+std::vector<unsigned char> GetIcon(const std::string &name, int size, UINT flag);
+bool SaveIcon(const std::string &name, const std::string &pngName, int size, UINT flag);

+ 65 - 0
windows/runner/file_info.cpp

@@ -0,0 +1,65 @@
+#include <windows.h>
+#include "file_info.h"
+
+bool FileInfoUtils::QueryValue(const std::wstring wsValueName, const std::wstring wsModuleName, std::wstring &wsRetStr)
+{
+    bool bSuccess = FALSE;
+    BYTE *lpVersionData = NULL;
+    DWORD dwLangCharset = 0;
+    TCHAR *pStr = NULL;
+
+    do {
+        if (wsValueName.empty() || wsModuleName.empty())
+            break;
+
+        DWORD dwHandle;
+        DWORD dwDataSize = ::GetFileVersionInfoSize((LPCWSTR)wsModuleName.c_str(), &dwHandle);
+        if (dwDataSize == 0)
+            break;
+
+        lpVersionData = new (std::nothrow) BYTE[dwDataSize];
+        if (NULL == lpVersionData)
+            break;
+
+        if (!::GetFileVersionInfo((LPCWSTR)wsModuleName.c_str(), dwHandle, dwDataSize, (void *)lpVersionData))
+            break;
+
+        UINT nQuerySize;
+        DWORD *pTransTable;
+        if (!::VerQueryValue(lpVersionData, L"\\VarFileInfo\\Translation", (void **)&pTransTable, &nQuerySize))
+            break;
+
+        dwLangCharset = MAKELONG(HIWORD(pTransTable[0]), LOWORD(pTransTable[0]));
+        if (lpVersionData == NULL)
+            break;
+
+        pStr = new (std::nothrow) TCHAR[128];
+        if (NULL == pStr)
+            break;
+
+        // wchar_t str2[MAX_PATH];
+        // wcscpy_s(str2, MAX_PATH, wsValueName.c_str());
+        swprintf_s(pStr, 128, L"\\StringFileInfo\\%08lx\\%s", dwLangCharset, wsValueName.c_str());
+
+        LPVOID lpData;
+
+        // GetFileVersionInfoSize, GetFileVersionInfo
+        if (::VerQueryValue((void *)lpVersionData, pStr, &lpData, &nQuerySize))
+            wsRetStr = (TCHAR *)lpData;
+
+        bSuccess = TRUE;
+
+    } while (FALSE);
+
+    if (lpVersionData) {
+        delete[] lpVersionData;
+        lpVersionData = NULL;
+    }
+    
+    if (pStr) {
+        delete[] pStr;
+        pStr = NULL;
+    }
+
+    return bSuccess;
+}

+ 17 - 0
windows/runner/file_info.h

@@ -0,0 +1,17 @@
+#include <string>
+
+class FileInfoUtils
+{
+public:
+    static bool GetFileDescription(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"FileDescription", szModuleName, RetStr); }
+    static bool GetFileVersion(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"FileVersion", szModuleName, RetStr); }
+    static bool GetInternalName(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"InternalName", szModuleName, RetStr); }
+    static bool GetCompanyName(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"CompanyName", szModuleName, RetStr); }
+    static bool GetLegalCopyright(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"LegalCopyright", szModuleName, RetStr); }
+    static bool GetOriginalFilename(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"OriginalFilename", szModuleName, RetStr); }
+    static bool GetProductName(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"ProductName", szModuleName, RetStr); }
+    static bool GetProductVersion(const std::wstring &szModuleName, std::wstring &RetStr) { return QueryValue(L"ProductVersion", szModuleName, RetStr); }
+
+private:
+    static bool QueryValue(const std::wstring wsValueName, const std::wstring wsModuleName, std::wstring &wsRetStr);
+};

+ 358 - 0
windows/runner/flutter_window.cpp

@@ -1,9 +1,19 @@
 #include "flutter_window.h"
+#include "utils.h"
+#include "file_info.h"
+#include "file_icon.h"
 
 #include <optional>
 
 #include "flutter/generated_plugin_registrant.h"
 
+void trayMessageHandler(TrayIcon *, UINT);
+HICON getIconHandle(std::string);
+const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key);
+
+static std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> appChannel;
+static std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> trayChannel;
+
 FlutterWindow::FlutterWindow(const flutter::DartProject& project)
     : project_(project) {}
 
@@ -27,6 +37,13 @@ bool FlutterWindow::OnCreate() {
   RegisterPlugins(flutter_controller_->engine());
   SetChildContent(flutter_controller_->view()->GetNativeWindow());
 
+  // register channel
+  registerMethodChannel(flutter_controller_->engine());
+
+  // setup trayicon
+  m_tray.setUserData(this);
+  m_tray.setListener(trayMessageHandler);
+
   flutter_controller_->engine()->SetNextFrameCallback([&]() {
     this->Show();
   });
@@ -40,6 +57,8 @@ bool FlutterWindow::OnCreate() {
 }
 
 void FlutterWindow::OnDestroy() {
+	m_tray.setVisible(false);
+
   if (flutter_controller_) {
     flutter_controller_ = nullptr;
   }
@@ -47,10 +66,20 @@ void FlutterWindow::OnDestroy() {
   Win32Window::OnDestroy();
 }
 
+void FlutterWindow::SetMinimumSize(SIZE size) {
+  min_window_size_ = size;
+}
+
 LRESULT
 FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
                               WPARAM const wparam,
                               LPARAM const lparam) noexcept {
+  // my custom message handler
+  std::optional<LRESULT> r = handleMessage(hwnd, message, wparam, lparam);
+  if (r) {    
+    return *r;
+  }
+
   // Give Flutter, including plugins, an opportunity to handle window messages.
   if (flutter_controller_) {
     std::optional<LRESULT> result =
@@ -65,7 +94,336 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
     case WM_FONTCHANGE:
       flutter_controller_->engine()->ReloadSystemFonts();
       break;
+    case WM_GETMINMAXINFO:
+      if (min_window_size_.cx > 0 && min_window_size_.cy > 0) {
+        LPMINMAXINFO info = reinterpret_cast<LPMINMAXINFO>(lparam);
+        info->ptMinTrackSize.x = min_window_size_.cx;
+        info->ptMinTrackSize.y = min_window_size_.cy;
+      }
+      break;
   }
 
   return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
 }
+
+void FlutterWindow::registerMethodChannel(flutter::FlutterEngine* engine)
+{
+  appChannel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
+    engine->messenger(), "app.nomo/app",
+    &flutter::StandardMethodCodec::GetInstance());
+  appChannel->SetMethodCallHandler([this](const auto& call, auto result) {
+    handleAppMethodCall(call, std::move(result));
+  });
+
+  trayChannel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
+    engine->messenger(), "app.nomo/tray",
+    &flutter::StandardMethodCodec::GetInstance());
+  trayChannel->SetMethodCallHandler([this](const auto& call, auto result) {
+    handleTrayMethodCall(call, std::move(result));
+  });
+}
+
+void FlutterWindow::handleAppMethodCall(
+  const flutter::MethodCall<flutter::EncodableValue>& method_call,
+  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+  if (method_call.method_name().compare("activeWindow") == 0) {
+    onActiveWindow();
+    result->Success(flutter::EncodableValue(true));
+    //
+  } else if (method_call.method_name().compare("hideWindow") == 0) {
+    onHideWindow();
+    result->Success(flutter::EncodableValue(true));
+    //
+  } else if (method_call.method_name().compare("toggleWindow") == 0) {
+    onToggleWindow();
+    result->Success(flutter::EncodableValue(true));
+    //
+  } else if (method_call.method_name().compare("destroyWindow") == 0) {
+    Destroy();
+    result->Success(flutter::EncodableValue(true));
+    //
+  } else if (method_call.method_name().compare("extractFileIcon") == 0) {
+    extractFileIcon(method_call, std::move(result));
+    //
+  } else if (method_call.method_name().compare("getFileDescription") == 0) {
+    getFileDescription(method_call, std::move(result));
+    //
+  } else {
+    result->NotImplemented();
+  }
+}
+
+void FlutterWindow::handleTrayMethodCall(
+  const flutter::MethodCall<flutter::EncodableValue>& method_call,
+  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+  if (method_call.method_name().compare("setSystemTray") == 0) {
+    setSystemTray(method_call, std::move(result));
+    //
+  } else if (method_call.method_name().compare("resetSystemTray") == 0) {
+    resetSystemTray(method_call, std::move(result));
+    //
+  } else if (method_call.method_name().compare("setContextMenu") == 0) {
+    setContextMenu(method_call, std::move(result));
+    //
+  } else if (method_call.method_name().compare("popupContextMenu") == 0) {
+    popUpContextMenu(method_call, std::move(result));
+    //
+  } else {
+    result->NotImplemented();
+  }
+}
+
+std::optional<LRESULT> FlutterWindow::handleMessage(HWND window, UINT const message, 
+  WPARAM const wparam, LPARAM const lparam)
+{
+  if (message == WM_CLOSE) {
+    if (m_tray.isVisible()) {
+      onHideWindow();
+    } else {
+      Destroy();
+    }    
+    return std::make_optional<LRESULT>(0);
+  }
+
+  if (message == WM_COMMAND) {
+    flutter::EncodableMap eventData = flutter::EncodableMap();
+    eventData[flutter::EncodableValue("id")] = flutter::EncodableValue((int)wparam);
+    trayChannel->InvokeMethod("onTrayMenuItemClick",
+                          std::make_unique<flutter::EncodableValue>(eventData));
+  }
+
+  return std::nullopt;
+}
+
+void FlutterWindow::onActiveWindow()
+{
+  bool isVisible = (::GetWindowLong(GetHandle(), GWL_STYLE) & WS_VISIBLE) != 0;
+  if (isVisible) {
+    WINDOWPLACEMENT windowPlacement;
+    GetWindowPlacement(GetHandle(), &windowPlacement);
+    if (windowPlacement.showCmd == SW_SHOWMINIMIZED) {
+      PostMessage(GetHandle(), WM_SYSCOMMAND, SC_RESTORE, 0);
+    }
+  } else {
+    onShowWindow();
+  }
+}
+
+void FlutterWindow::onToggleWindow()
+{
+  bool isVisible = (::GetWindowLong(GetHandle(), GWL_STYLE) & WS_VISIBLE) != 0;
+  if (isVisible) {
+    WINDOWPLACEMENT windowPlacement;
+    GetWindowPlacement(GetHandle(), &windowPlacement);
+    if (windowPlacement.showCmd == SW_SHOWMINIMIZED) {
+      PostMessage(GetHandle(), WM_SYSCOMMAND, SC_RESTORE, 0);
+    } else {
+      onHideWindow();
+    }
+  } else {
+    onShowWindow();
+  }
+}
+
+void FlutterWindow::onShowWindow()
+{
+  ShowWindow(GetHandle(), SW_SHOW);
+  SetForegroundWindow(GetHandle());
+  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
+  appChannel->InvokeMethod("onWindowShow", std::move(args));
+}
+
+void FlutterWindow::onHideWindow()
+{
+  ShowWindow(GetHandle(), SW_HIDE);
+  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
+  appChannel->InvokeMethod("onWindowHide", std::move(args));
+}
+
+void FlutterWindow::onTrayIconLButtonDown()
+{
+  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
+  trayChannel->InvokeMethod("onTrayIconLButtonDown", std::move(args));
+}
+
+void FlutterWindow::onTrayIconLButtonUp()
+{
+  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
+  trayChannel->InvokeMethod("onTrayIconLButtonUp", std::move(args));
+}
+
+void FlutterWindow::onTrayIconRButtonDown()
+{
+  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
+  trayChannel->InvokeMethod("onTrayIconRButtonDown", std::move(args));
+}
+
+void FlutterWindow::onTrayIconRButtonUp()
+{
+  auto args = std::make_unique<flutter::EncodableValue>(nullptr);
+  trayChannel->InvokeMethod("onTrayIconRButtonUp", std::move(args));
+}
+
+void FlutterWindow::setSystemTray(
+  const flutter::MethodCall<flutter::EncodableValue>& method_call,
+  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+    const flutter::EncodableMap& args =
+      std::get<flutter::EncodableMap>(*method_call.arguments());
+    std::string tooltip = std::get<std::string>(args.at(flutter::EncodableValue("tooltip")));
+    std::string icon = std::get<std::string>(args.at(flutter::EncodableValue("icon")));
+
+    m_tray.setName((LPCWSTR)(Utf16FromUtf8(tooltip.c_str()).c_str()));
+    m_tray.setIcon(getIconHandle(icon));
+    m_tray.setVisible(true);
+    result->Success(flutter::EncodableValue(true));
+}
+
+void FlutterWindow::resetSystemTray(
+  const flutter::MethodCall<flutter::EncodableValue>& method_call,
+  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+  m_tray.setVisible(false);
+  result->Success(flutter::EncodableValue(true));
+}
+
+void FlutterWindow::setContextMenu(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+  const flutter::EncodableMap& args =
+    std::get<flutter::EncodableMap>(*method_call.arguments());
+
+  m_menuHandle = CreatePopupMenu();
+
+  createMenu(m_menuHandle, std::get<flutter::EncodableMap>(
+    args.at(flutter::EncodableValue("menu"))));
+  
+  result->Success(flutter::EncodableValue(true));
+}
+
+void FlutterWindow::popUpContextMenu(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+	POINT pt;
+	if (GetCursorPos(&pt)) {
+    ::SetForegroundWindow(GetHandle());
+		TrackPopupMenu(m_menuHandle, TPM_BOTTOMALIGN | TPM_LEFTALIGN, 
+      pt.x, pt.y, 0, GetHandle(), NULL);
+	}
+  result->Success(flutter::EncodableValue(true));
+}
+
+void FlutterWindow::extractFileIcon(
+  const flutter::MethodCall<flutter::EncodableValue>& method_call,
+  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+  const flutter::EncodableMap& args =
+    std::get<flutter::EncodableMap>(*method_call.arguments());
+  std::string fileName = std::get<std::string>(args.at(flutter::EncodableValue("fileName")));
+  std::string iconName = std::get<std::string>(args.at(flutter::EncodableValue("iconName")));
+
+  auto ret = SaveIcon(fileName, iconName, 32, 0);
+  result->Success(flutter::EncodableValue(ret));
+}
+
+void FlutterWindow::getFileDescription(
+  const flutter::MethodCall<flutter::EncodableValue>& method_call,
+  std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+{
+  const flutter::EncodableMap& args =
+    std::get<flutter::EncodableMap>(*method_call.arguments());
+  std::wstring fileName = Utf16FromUtf8(std::get<std::string>(args.at(flutter::EncodableValue("fileName"))).c_str());
+  std::wstring description;
+  FileInfoUtils::GetFileDescription(fileName, description);
+
+  std::wcout << fileName << " " << description << std::endl;
+
+  result->Success(flutter::EncodableValue(utf8_encode(description)));
+}
+
+void FlutterWindow::createMenu(HMENU menu, flutter::EncodableMap args) {
+  flutter::EncodableList items = std::get<flutter::EncodableList>(
+      args.at(flutter::EncodableValue("items")));
+
+  int count = GetMenuItemCount(menu);
+  for (int i = 0; i < count; i++) {
+    // always remove at 0 because they shift every time
+    RemoveMenu(menu, 0, MF_BYPOSITION);
+  }
+
+  for (flutter::EncodableValue item_value : items) {
+    flutter::EncodableMap item_map =
+        std::get<flutter::EncodableMap>(item_value);
+    int id = std::get<int>(item_map.at(flutter::EncodableValue("id")));
+    std::string type =
+        std::get<std::string>(item_map.at(flutter::EncodableValue("type")));
+    std::string label =
+        std::get<std::string>(item_map.at(flutter::EncodableValue("label")));
+    auto* checked = std::get_if<bool>(ValueOrNull(item_map, "checked"));
+    bool disabled =
+        std::get<bool>(item_map.at(flutter::EncodableValue("disabled")));
+
+    UINT_PTR item_id = id;
+    UINT uFlags = MF_STRING;
+
+    if (disabled) {
+      uFlags |= MF_GRAYED;
+    }
+
+    if (type.compare("separator") == 0) {
+      AppendMenuW(menu, MF_SEPARATOR, item_id, NULL);
+    } else {
+      if (type.compare("checkbox") == 0) {
+        if (checked == nullptr) {
+          // skip
+        } else {
+          uFlags |= (*checked == true ? MF_CHECKED : MF_UNCHECKED);
+        }
+      } else if (type.compare("submenu") == 0) {
+        uFlags |= MF_POPUP;
+        HMENU sub_menu = ::CreatePopupMenu();
+        createMenu(sub_menu, std::get<flutter::EncodableMap>(item_map.at(
+                                  flutter::EncodableValue("submenu"))));
+        item_id = reinterpret_cast<UINT_PTR>(sub_menu);
+      }
+      AppendMenuW(menu, uFlags, item_id, (LPCWSTR)(Utf16FromUtf8(label.c_str()).c_str()));
+    }
+  }
+}
+
+void trayMessageHandler(TrayIcon *pTrayIcon, UINT uMsg)
+{
+	switch (uMsg) {
+  case WM_LBUTTONDOWN: 
+    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconLButtonDown();
+    break;
+  case WM_LBUTTONUP: 
+    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconLButtonUp();
+    break;
+	case WM_RBUTTONDOWN:
+    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconRButtonDown();
+    break;
+  case WM_RBUTTONUP:
+    ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconRButtonUp();
+    break;
+	}
+}
+
+HICON getIconHandle(std::string iconPath)
+{
+  return static_cast<HICON>(LoadImage(NULL, (LPCWSTR)(Utf16FromUtf8(iconPath.c_str()).c_str()),
+		IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED));
+}
+
+const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key)
+{
+  auto it = map.find(flutter::EncodableValue(key));
+  if (it == map.end()) {
+    return nullptr;
+  }
+  return &(it->second);
+}

+ 61 - 1
windows/runner/flutter_window.h

@@ -4,9 +4,13 @@
 #include <flutter/dart_project.h>
 #include <flutter/flutter_view_controller.h>
 
+#include <flutter/method_channel.h>
+#include <flutter/standard_method_codec.h>
+
 #include <memory>
 
 #include "win32_window.h"
+#include "tray_icon.h"
 
 // A window that does nothing but host a Flutter view.
 class FlutterWindow : public Win32Window {
@@ -14,7 +18,19 @@ class FlutterWindow : public Win32Window {
   // Creates a new FlutterWindow hosting a Flutter view running |project|.
   explicit FlutterWindow(const flutter::DartProject& project);
   virtual ~FlutterWindow();
-
+ public:
+  void SetMinimumSize(SIZE size);
+  
+  // App Events handler
+  void onHideWindow();
+  void onShowWindow();
+  void onToggleWindow();
+  void onActiveWindow();
+  // Tray Events handler
+	void onTrayIconLButtonDown();
+	void onTrayIconLButtonUp();
+	void onTrayIconRButtonDown();
+	void onTrayIconRButtonUp();
  protected:
   // Win32Window:
   bool OnCreate() override;
@@ -23,11 +39,55 @@ class FlutterWindow : public Win32Window {
                          LPARAM const lparam) noexcept override;
 
  private:
+  void createMenu(HMENU menu, flutter::EncodableMap args);
+  std::optional<LRESULT> handleMessage(HWND window, UINT const message, 
+    WPARAM const wparam, LPARAM const lparam);
+
+  void registerMethodChannel(flutter::FlutterEngine* engine);
+
+  void handleAppMethodCall(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void handleTrayMethodCall(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void setSystemTray(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void resetSystemTray(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void setContextMenu(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void popUpContextMenu(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void extractFileIcon(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+  void getFileDescription(
+    const flutter::MethodCall<flutter::EncodableValue>& method_call,
+    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+
+ private:
+  SIZE min_window_size_ = {0, 0};
   // The project to run.
   flutter::DartProject project_;
 
   // The Flutter instance hosted by this window.
   std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
+
+  // The system tray
+	TrayIcon m_tray;  
+  HMENU m_menuHandle;
 };
 
 #endif  // RUNNER_FLUTTER_WINDOW_H_

+ 110 - 3
windows/runner/main.cpp

@@ -1,12 +1,67 @@
 #include <flutter/dart_project.h>
 #include <flutter/flutter_view_controller.h>
 #include <windows.h>
+#include <VersionHelpers.h>
 
 #include "flutter_window.h"
 #include "utils.h"
+#include "dump.h"
+
+#define APP_MUTEX_NAME L"nomo.mutex"
+#define APP_WINDOW_NAME L"NOMO"
+
+UINT (*MyGetDpiForWindow) (HWND) = [] (HWND) { return 96u; };
+int (*MyGetSystemMetricsForDpi) (int, UINT) = [] (int nIndex, UINT) { return GetSystemMetrics(nIndex); };
+
+void runAsAdmin()
+{
+  WCHAR czFileName[1024] = {0};
+  GetModuleFileName(NULL, czFileName, _countof(czFileName) - 1);
+  SHELLEXECUTEINFO EI;
+  memset(&EI, 0, sizeof(EI));
+  EI.cbSize = sizeof(SHELLEXECUTEINFO);
+  EI.lpVerb = TEXT("runas");
+  EI.fMask = 0x00000040;
+  EI.lpFile = czFileName;
+  EI.nShow = SW_SHOW;
+  ShellExecuteEx(&EI);
+}
+
+BOOL isRunAsAdmin()
+{
+  BOOL isRunAsAdmin = FALSE;
+  HANDLE hToken = NULL;
+  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
+    return FALSE;
+  }
+  TOKEN_ELEVATION tokenEle;
+  DWORD dwRetLen = 0;
+  if (GetTokenInformation(hToken, TokenElevation, &tokenEle, sizeof(tokenEle), &dwRetLen)) {
+    if (dwRetLen == sizeof(tokenEle)) {
+      isRunAsAdmin = tokenEle.TokenIsElevated;
+    }
+  }
+  CloseHandle(hToken);
+  return isRunAsAdmin;
+}
 
 int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
                       _In_ wchar_t *command_line, _In_ int show_command) {
+  bool isDebugMode = false;
+  char buffer[2];
+  if (GetEnvironmentVariableA("DEBUG_MODE", buffer, sizeof(buffer)) > 0) {
+    isDebugMode = true;
+  }
+
+  // // 以管理员权限运行
+  // if (!isDebugMode && !isRunAsAdmin()) {
+  //   runAsAdmin();
+  //   exit(0);
+  // }
+
+  // 捕捉异常
+  SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ApplicationCrashHandler);
+
   // Attach to console when present (e.g., 'flutter run') or create a
   // new console when running with a debugger.
   if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
@@ -25,13 +80,65 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
   project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
 
   FlutterWindow window(project);
-  Win32Window::Point origin(10, 10);
-  Win32Window::Size size(1280, 720);
-  if (!window.Create(L"NoMo", origin, size)) {
+  UINT scrWidth, scrHeight, xShaft, yShaft;
+
+  double scale_factor_x = 1.0;
+  double scale_factor_y = 1.0;
+
+  // 计算屏幕逻辑尺寸
+  if (IsWindows10OrGreater()) {
+    UINT dpiX, dpiY;
+
+    HDC screen = GetDC(NULL);
+    dpiX = GetDeviceCaps(screen, LOGPIXELSX);
+    dpiY = GetDeviceCaps(screen, LOGPIXELSY);
+    ReleaseDC(NULL, screen);
+
+    scale_factor_x = dpiX / 96.0;
+    scale_factor_y = dpiY / 96.0;
+
+    if (auto user32 = LoadLibraryA("User32.dll")) {
+      if (auto fn = GetProcAddress(user32, "GetDpiForWindow")) {
+          MyGetDpiForWindow = (decltype(MyGetDpiForWindow)) fn;
+          MyGetSystemMetricsForDpi = (decltype(MyGetSystemMetricsForDpi)) GetProcAddress(user32, "GetSystemMetricsForDpi");
+      }
+      scrWidth = static_cast<UINT>(round(MyGetSystemMetricsForDpi(SM_CXFULLSCREEN, dpiX) / scale_factor_x));
+      scrHeight = static_cast<UINT>(round(MyGetSystemMetricsForDpi(SM_CYFULLSCREEN, dpiY) / scale_factor_y));
+    } else {
+      scrWidth = GetSystemMetrics(SM_CXSCREEN);
+      scrHeight = GetSystemMetrics(SM_CYSCREEN);
+    }
+  } else {
+    scrWidth = GetSystemMetrics(SM_CXSCREEN);
+    scrHeight = GetSystemMetrics(SM_CYSCREEN);
+  }
+
+  // 程序窗口大小
+  UINT windowWidth = 375, windowHeight = 650;
+  Win32Window::Size size(windowWidth, windowHeight);
+
+  // 计算程序显示坐标原点(屏幕中间)
+  xShaft = (scrWidth - windowWidth) / 2;
+  yShaft = (scrHeight - windowHeight) / 2;
+  Win32Window::Point origin(xShaft, yShaft);
+
+  if (!window.Create(APP_WINDOW_NAME, origin, size)) {
     return EXIT_FAILURE;
   }
   window.SetQuitOnClose(true);
 
+  // int minWidth = static_cast<int>(360 * scale_factor_x);
+  // int minHeight = static_cast<int>(640 * scale_factor_y);
+  // window.SetMinimumSize({minWidth, minHeight});
+  window.SetMinimumSize(SIZE{ 375, 650 });
+
+  // 设置窗口不可最大化以及不可缩放
+  HWND hWnd = window.GetHandle();
+  DWORD gwlStyle = GetWindowLong(hWnd, GWL_STYLE);
+  gwlStyle &= ~WS_THICKFRAME;
+  gwlStyle &= ~WS_MAXIMIZEBOX;
+  SetWindowLong(hWnd, GWL_STYLE, gwlStyle);
+
   ::MSG msg;
   while (::GetMessage(&msg, nullptr, 0, 0)) {
     ::TranslateMessage(&msg);

+ 310 - 0
windows/runner/tray_icon.cpp

@@ -0,0 +1,310 @@
+#include "tray_icon.h"
+
+#define TRAY_WINDOW_MESSAGE (WM_USER + 101)
+
+namespace {
+	// A map that never holds allocated memory when it is empty. This map will be created with placement new as a static variable,
+	// and its destructor will be never called, and it shouldn't leak memory if it contains no items at program exit.
+	// This dirty trick is useful when you create your trayicon object as static. In this case we can not control the
+	// order of destruction of this map object and the static trayicon object. However this dirty trick ensures that
+	// the map is freed exactly when the destructor of the last static trayicon is unregistering itself.
+	class TrayIconMap {
+	public:
+		typedef UINT KeyType;
+		typedef TrayIcon *ValueType;
+
+		//typedef StdMap std::map<KeyType, ValueType>;
+		struct StdMap : public std::map<KeyType, ValueType>{};
+		typedef StdMap::iterator iterator;
+
+		TrayIconMap() : m_empty(true) {}
+
+		ValueType &operator[](KeyType k) {
+			return getOrCreateStdMap()[k];
+		}
+
+		ValueType *find(KeyType k) {
+			if (m_empty) {
+				return false;
+			}
+			StdMap::iterator it = getStdMap().find(k);
+			if (it == getStdMap().end()) {
+				return NULL;
+			}		
+			return &it->second;
+		}
+
+		int erase(KeyType k) {
+			if (m_empty) {
+				return 0;
+			}		
+			StdMap &m = getStdMap();
+			int res = (int)m.erase(k);
+			if (m.empty()) {
+				m.~StdMap();
+				m_empty = true;
+			}
+			return res;
+		}
+
+		bool empty() const {
+			return m_empty;
+		}
+
+		iterator begin() {
+			assert(!m_empty);
+			return m_empty ? iterator() : getStdMap().begin();
+		}
+
+		iterator end() {
+			assert(!m_empty);
+			return m_empty ? iterator() : getStdMap().end();
+		}
+
+	private:
+		StdMap &getStdMap() {
+			assert(!m_empty);
+			return (StdMap &)m_mapBuffer;
+		}
+
+		StdMap &getOrCreateStdMap() {
+			if (m_empty) {
+				new ((void *)&m_mapBuffer) StdMap();
+				m_empty = false;
+			}
+			return (StdMap &)m_mapBuffer;
+		}
+
+	private:
+		bool m_empty;
+		char m_mapBuffer[sizeof(StdMap)];
+	};
+
+	static TrayIconMap &getTrayIconMap() {
+		// This hack prevents running the destructor of our map, so it isn't problem if someone tries to reach this from a static destructor.
+		// Because of using MyMap this will not cause a memory leak if the user removes all items from the container before exiting.
+		static char tray_icon_buffer[sizeof(TrayIconMap)];
+		static bool initialized = false;
+		if (!initialized) {
+			initialized = true;
+			new ((void *)tray_icon_buffer) TrayIconMap();
+		}
+		return (TrayIconMap &)tray_icon_buffer;
+	}
+
+	static UINT getNextTrayIconId() {
+		static UINT next_id = 1;
+		return next_id++;
+	}
+
+}
+
+
+static const UINT g_WndMsgTaskbarCreated = RegisterWindowMessage(TEXT("TaskbarCreated"));
+LRESULT CALLBACK TrayIcon::MessageProcessorWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+	if (uMsg == TRAY_WINDOW_MESSAGE) {
+		if (TrayIcon **ppIcon = getTrayIconMap().find((UINT)wParam)) {
+			(*ppIcon)->onMessage((UINT)lParam);
+		}
+		return 0;
+	} else if (uMsg == g_WndMsgTaskbarCreated) {
+		TrayIconMap &m = getTrayIconMap();
+		if (!m.empty()) {
+			for (std::map<UINT, TrayIcon *>::const_iterator it = m.begin(), eit = m.end(); it != eit; ++it) {
+				TrayIcon *pTrayIcon = it->second;
+				pTrayIcon->onTaskbarCreated();
+			}
+		}
+	}
+	return DefWindowProc(hWnd, uMsg, wParam, lParam);
+}
+
+HWND TrayIcon::GetMessageProcessorHWND()
+{
+	static HWND hWnd = NULL;
+	if (!hWnd) {
+		static const TCHAR TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME[] = TEXT("TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASS");
+		HINSTANCE hInstance = (HINSTANCE)GetModuleHandle(NULL);
+
+		WNDCLASSEX wc;
+		wc.cbSize = sizeof(wc);
+		wc.cbClsExtra = 0;
+		wc.cbWndExtra = 0;
+		wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
+		wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+		wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
+		wc.hIconSm = NULL;
+		wc.hInstance = hInstance;
+		wc.lpfnWndProc = MessageProcessorWndProc;
+		wc.lpszClassName = TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME;
+		wc.lpszMenuName = NULL;
+		wc.style = 0;
+		if (!RegisterClassEx(&wc)) {
+			return NULL;
+		}
+
+		hWnd = CreateWindowEx(
+			0,
+			TRAY_ICON_MESSAGE_PROCESSOR_WND_CLASSNAME,
+			TEXT("TRAY_ICON_MESSAGE_PROCESSOR_WND"),
+			WS_POPUP,
+			0, 0, 0, 0,
+			NULL,
+			NULL,
+			hInstance,
+			NULL);
+	}
+	return hWnd;
+}
+
+TrayIcon::TrayIcon(LPCWSTR name, bool visible, HICON hIcon, bool destroyIconInDestructor)
+	: m_id(getNextTrayIconId())
+	, m_name(name)
+	, m_hIcon(hIcon)
+	, m_visible(false)
+	, m_destroyIconInDestructor(destroyIconInDestructor)
+	, m_messageFunc(NULL)
+	, m_listener(NULL)
+	, m_userData(NULL)
+{
+	getTrayIconMap()[m_id] = this;
+	setVisible(visible);
+}
+
+TrayIcon::~TrayIcon()
+{
+	setVisible(false);
+	setIcon(NULL, m_destroyIconInDestructor);
+	getTrayIconMap().erase(m_id);
+}
+
+HICON TrayIcon::internalGetIcon() const
+{
+	return m_hIcon ? m_hIcon : ::LoadIcon(NULL, IDI_APPLICATION);
+}
+
+bool TrayIcon::addIcon()
+{
+	NOTIFYICONDATAW data;
+	fillNotifyIconData(data);
+	data.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP;
+	data.uCallbackMessage = TRAY_WINDOW_MESSAGE;
+	data.hIcon = internalGetIcon();
+
+	size_t tip_len = std::max(sizeof(data.szTip) - 1, wcslen(m_name.c_str()));
+	wmemcpy(data.szTip, m_name.c_str(), tip_len);
+	data.szTip[tip_len] = 0;
+
+	return FALSE != Shell_NotifyIcon(NIM_ADD, &data);
+}
+
+bool TrayIcon::removeIcon()
+{
+	NOTIFYICONDATAW data;
+	fillNotifyIconData(data);
+	return FALSE != Shell_NotifyIcon(NIM_DELETE, &data);
+}
+
+void TrayIcon::onTaskbarCreated()
+{
+	if (m_visible) {
+		addIcon();
+	}
+}
+
+void TrayIcon::setName(LPCWSTR name)
+{
+	m_name = name;
+	if (m_visible) {
+		NOTIFYICONDATAW data;
+		fillNotifyIconData(data);
+		data.uFlags |= NIF_TIP;
+
+		size_t tip_len = std::max(sizeof(data.szTip) - 1, wcslen(name));
+		wmemcpy(data.szTip, name, tip_len);
+		data.szTip[tip_len] = 0;
+
+		Shell_NotifyIcon(NIM_MODIFY, &data);
+	}
+}
+
+bool TrayIcon::setVisible(bool visible)
+{
+	if (m_visible == visible) {
+		return true;
+	}
+
+	m_visible = visible;
+	if (m_visible) {
+		return addIcon();
+	}
+	return removeIcon();
+}
+
+void TrayIcon::setIcon(HICON hNewIcon, bool destroyCurrentIcon)
+{
+	if (m_hIcon == hNewIcon) {
+		return;
+	}
+	if (destroyCurrentIcon && m_hIcon) {
+		DestroyIcon(m_hIcon);
+	}
+	m_hIcon = hNewIcon;
+
+	if (m_visible) {
+		NOTIFYICONDATAW data;
+		fillNotifyIconData(data);
+		data.uFlags |= NIF_ICON;
+		data.hIcon = internalGetIcon();
+		Shell_NotifyIcon(NIM_MODIFY, &data);
+	}
+}
+
+bool TrayIcon::notify(LPCWSTR title, LPCWSTR msg, NotificationIcon icon)
+{
+#ifndef NOTIFYICONDATA_V2_SIZE
+	return false;
+#else
+	if (!m_visible) {
+		return false;
+	}
+
+	NOTIFYICONDATAW data;
+	fillNotifyIconData(data);
+	data.cbSize = NOTIFYICONDATAW_V2_SIZE;
+	data.uFlags |= NIF_INFO;
+	data.dwInfoFlags = icon;
+	data.uTimeout = 10000;
+
+	wcscpy_s(data.szInfoTitle, title);
+	wcscpy_s(data.szInfo, msg);
+
+	return FALSE != Shell_NotifyIcon(NIM_MODIFY, &data);
+#endif
+}
+
+void TrayIcon::onMessage(UINT uMsg)
+{
+	if (m_messageFunc) {
+		m_messageFunc(this, uMsg);
+	}
+
+	if (m_listener) {
+		m_listener->onTrayIconMessage(this, uMsg);
+	}
+}
+
+void TrayIcon::fillNotifyIconData(NOTIFYICONDATAW &data)
+{
+	memset(&data, 0, sizeof(data));
+	// the basic functions need only V1
+#ifdef NOTIFYICONDATA_V1_SIZE
+	data.cbSize = NOTIFYICONDATA_V1_SIZE;
+#else
+	data.cbSize = sizeof(data);
+#endif
+	data.hWnd = GetMessageProcessorHWND();
+	assert(data.hWnd);
+	data.uID = m_id;
+}

+ 114 - 0
windows/runner/tray_icon.h

@@ -0,0 +1,114 @@
+#ifndef TRAY_ICON_H_
+#define TRAY_ICON_H_
+
+#include <Windows.h>
+#include <shellapi.h>
+#include <string>
+#include <map>
+#include <assert.h>
+#include <TCHAR.H>
+
+struct TrayIconListener;
+
+class TrayIcon {
+public:
+	explicit TrayIcon(LPCWSTR name = L"",
+		bool visible = false, HICON hIcon = NULL, bool destroyIconInDestructor = false);
+	virtual ~TrayIcon();
+	
+	virtual void setName(LPCWSTR name);
+	LPCWSTR getName() const { return m_name.c_str(); }
+	
+	virtual bool setVisible(bool visible);
+	bool isVisible() const { return m_visible; }
+	
+	// The destructor may destroy the specified hIcon. If you want to avoid that, call
+	// setIcon(NULL, false) or setDestroyIconInDestructor(false).
+	virtual void setIcon(HICON hNewIcon, bool destroyCurrentIcon = true);
+	HICON getIcon() const { return m_hIcon; }
+	
+	void setDestroyIconInDestructor(bool b)	{ m_destroyIconInDestructor = b; }
+	bool getDestroyIconInDestructor() const { return m_destroyIconInDestructor; }
+
+	enum NotificationIcon {
+		iconNone, // NIIF_NONE(0)
+		iconInfo, // NIIF_INFO(1)
+		iconWarning, // NIIF_WARNING(2)
+		iconError // NIIF_ERROR(3)
+	};	
+	bool notify(LPCWSTR title, LPCWSTR msg, NotificationIcon icon = iconNone);
+
+	typedef void (*TrayIconMessageFunc)(TrayIcon* pTrayIcon, UINT uMsg);
+	void setListener(TrayIconMessageFunc func) { m_messageFunc = func; }
+	void setListener(TrayIconListener *listener) { m_listener = listener; }
+
+	void setUserData(const void* data) { m_userData = data; }
+	const void* getUserData() { return m_userData; }
+protected:
+	virtual void onMessage(UINT uMsg);
+private:
+	void fillNotifyIconData(NOTIFYICONDATAW& data);
+	HICON internalGetIcon() const;
+	bool addIcon();
+	bool removeIcon();
+	void onTaskbarCreated();
+private:
+	UINT m_id;
+	std::wstring m_name;
+	HICON m_hIcon;
+	bool m_visible;
+	bool m_destroyIconInDestructor;	
+	TrayIconMessageFunc m_messageFunc;
+	TrayIconListener* m_listener;
+	const void* m_userData;
+	
+	static LRESULT CALLBACK MessageProcessorWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+	static HWND GetMessageProcessorHWND();
+};
+
+struct TrayIconListener {
+	virtual void onTrayIconMouseMove(TrayIcon* pTrayIcon) {}
+
+	virtual void onTrayIconLButtonDown(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconLButtonUp(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconLButtonDblClk(TrayIcon* pTrayIcon) {}
+
+	virtual void onTrayIconRButtonDown(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconRButtonUp(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconRButtonDblClk(TrayIcon* pTrayIcon) {}
+
+	virtual void onTrayIconMButtonDown(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconMButtonUp(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconMButtonDblClk(TrayIcon* pTrayIcon) {}
+
+	virtual void onTrayIconSelect(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconBalloonShow(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconBalloonHide(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconBalloonTimeout(TrayIcon* pTrayIcon) {}
+	virtual void onTrayIconBalloonUserClick(TrayIcon* pTrayIcon) {}
+
+	// Use GetCursorPos() if you need the location of the cursor.
+	virtual void onTrayIconMessage(TrayIcon* pTrayIcon, UINT uMsg) {
+		switch (uMsg) {
+		case WM_MOUSEMOVE: onTrayIconMouseMove(pTrayIcon); break;
+		case WM_LBUTTONDOWN: onTrayIconLButtonDown(pTrayIcon); break;
+		case WM_LBUTTONUP: onTrayIconLButtonUp(pTrayIcon); break;
+		case WM_LBUTTONDBLCLK: onTrayIconLButtonDblClk(pTrayIcon); break;
+		case WM_RBUTTONDOWN: onTrayIconRButtonDown(pTrayIcon); break;
+		case WM_RBUTTONUP: onTrayIconRButtonUp(pTrayIcon); break;
+		case WM_RBUTTONDBLCLK: onTrayIconRButtonDblClk(pTrayIcon); break;
+		case WM_MBUTTONDOWN: onTrayIconMButtonDown(pTrayIcon); break;
+		case WM_MBUTTONUP: onTrayIconMButtonUp(pTrayIcon); break;
+		case WM_MBUTTONDBLCLK: onTrayIconMButtonDblClk(pTrayIcon); break;
+#ifdef NIN_SELECT
+		case NIN_SELECT: onTrayIconSelect(pTrayIcon); break;
+		case NIN_BALLOONSHOW: onTrayIconBalloonShow(pTrayIcon); break;
+		case NIN_BALLOONHIDE: onTrayIconBalloonHide(pTrayIcon); break;
+		case NIN_BALLOONTIMEOUT: onTrayIconBalloonTimeout(pTrayIcon); break;
+		case NIN_BALLOONUSERCLICK: onTrayIconBalloonUserClick(pTrayIcon); break;
+#endif
+		}
+	}
+};
+
+#endif  // TRAY_ICON_H_

+ 93 - 17
windows/runner/utils.cpp

@@ -7,13 +7,17 @@
 
 #include <iostream>
 
-void CreateAndAttachConsole() {
-  if (::AllocConsole()) {
+void CreateAndAttachConsole()
+{
+  if (::AllocConsole())
+  {
     FILE *unused;
-    if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+    if (freopen_s(&unused, "CONOUT$", "w", stdout))
+    {
       _dup2(_fileno(stdout), 1);
     }
-    if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+    if (freopen_s(&unused, "CONOUT$", "w", stderr))
+    {
       _dup2(_fileno(stdout), 2);
     }
     std::ios::sync_with_stdio();
@@ -21,18 +25,21 @@ void CreateAndAttachConsole() {
   }
 }
 
-std::vector<std::string> GetCommandLineArguments() {
+std::vector<std::string> GetCommandLineArguments()
+{
   // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
   int argc;
-  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
-  if (argv == nullptr) {
+  wchar_t **argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+  if (argv == nullptr)
+  {
     return std::vector<std::string>();
   }
 
   std::vector<std::string> command_line_arguments;
 
   // Skip the first argument as it's the binary name.
-  for (int i = 1; i < argc; i++) {
+  for (int i = 1; i < argc; i++)
+  {
     command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
   }
 
@@ -41,25 +48,94 @@ std::vector<std::string> GetCommandLineArguments() {
   return command_line_arguments;
 }
 
-std::string Utf8FromUtf16(const wchar_t* utf16_string) {
-  if (utf16_string == nullptr) {
+std::string Utf8FromUtf16(const wchar_t *utf16_string)
+{
+  if (utf16_string == nullptr)
+  {
     return std::string();
   }
-  unsigned int target_length = ::WideCharToMultiByte(
+  int target_length = ::WideCharToMultiByte(
       CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-      -1, nullptr, 0, nullptr, nullptr)
-    -1; // remove the trailing null character
-  int input_length = (int)wcslen(utf16_string);
+      -1, nullptr, 0, nullptr, nullptr);
   std::string utf8_string;
-  if (target_length == 0 || target_length > utf8_string.max_size()) {
+  if (target_length == 0 || target_length > utf8_string.max_size())
+  {
     return utf8_string;
   }
   utf8_string.resize(target_length);
   int converted_length = ::WideCharToMultiByte(
       CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-      input_length, utf8_string.data(), target_length, nullptr, nullptr);
-  if (converted_length == 0) {
+      -1, utf8_string.data(),
+      target_length, nullptr, nullptr);
+  if (converted_length == 0)
+  {
     return std::string();
   }
   return utf8_string;
 }
+
+std::wstring Utf16FromUtf8(const char *utf8_string)
+{
+  if (utf8_string == nullptr)
+  {
+    return std::wstring();
+  }
+  int target_length = ::MultiByteToWideChar(CP_UTF8, 0, utf8_string, static_cast<int>(strlen(utf8_string)), nullptr, 0);
+  std::wstring utf16_string;
+  if (target_length == 0 || target_length > utf16_string.max_size())
+  {
+    return utf16_string;
+  }
+  utf16_string.resize(target_length);
+  int converted_length = ::MultiByteToWideChar(CP_UTF8, 0, utf8_string, static_cast<int>(strlen(utf8_string)), utf16_string.data(), target_length);
+  if (converted_length == 0)
+  {
+    return std::wstring();
+  }
+  return utf16_string;
+}
+
+std::string utf8_encode(const std::wstring &wstr)
+{
+  if (wstr.empty())
+    return std::string();
+  int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL);
+  std::string strTo(size_needed, 0);
+  WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL);
+  return strTo;
+}
+
+std::wstring utf8_decode(const std::string &str)
+{
+  if (str.empty())
+    return std::wstring();
+  int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), NULL, 0);
+  std::wstring wstrTo(size_needed, 0);
+  MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), &wstrTo[0], size_needed);
+  return wstrTo;
+}
+
+bool _isntspace(const char &ch)
+{
+  return !isspace(ch);
+}
+
+const std::string ltrim(const std::string &s)
+{
+  std::string::const_iterator iter = find_if(s.begin(), s.end(), _isntspace);
+  return std::string(iter, s.end());
+}
+
+const std::string rtrim(const std::string &s)
+{
+  std::string::const_iterator iter = find_if(s.rbegin(), s.rend(), _isntspace).base();
+  return std::string(s.begin(), iter);
+}
+
+const std::string trim(const std::string &s)
+{
+  std::string::const_iterator iter1 = find_if(s.begin(), s.end(), _isntspace);
+  std::string::const_iterator iter2 = find_if(s.rbegin(), s.rend(), _isntspace).base();
+
+  return iter1 < iter2 ? std::string(iter1, iter2) : std::string("");
+}

+ 19 - 0
windows/runner/utils.h

@@ -12,8 +12,27 @@ void CreateAndAttachConsole();
 // encoded in UTF-8. Returns an empty std::string on failure.
 std::string Utf8FromUtf16(const wchar_t* utf16_string);
 
+// Takes a null-terminated char* encoded in UTF-8 and returns a std::wstring
+// encoded in UTF-16. Returns an empty std::wstring on failure.
+std::wstring Utf16FromUtf8(const char* utf8_string);
+
+// Convert a wide Unicode string to an UTF8 string
+std::string utf8_encode(const std::wstring &wstr);
+
+// Convert an UTF8 string to a wide Unicode String
+std::wstring utf8_decode(const std::string &str);
+
 // Gets the command line arguments passed in as a std::vector<std::string>,
 // encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
 std::vector<std::string> GetCommandLineArguments();
 
+// trims whitespace from the left side of a string
+const std::string ltrim(const std::string &s);
+
+// trims whitespace from the right side of a string
+const std::string rtrim(const std::string &s);
+
+// trims whitespace from both sides of a string
+const std::string trim(const std::string &s);
+
 #endif  // RUNNER_UTILS_H_

+ 26 - 0
windows/runner/win32_window.cpp

@@ -16,6 +16,9 @@ namespace {
 #define DWMWA_USE_IMMERSIVE_DARK_MODE 20
 #endif
 
+#define APP_MUTEX_NAME L"nomo.mutex"
+#define APP_WINDOW_NAME L"NOMO"
+
 constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
 
 /// Registry key for app theme preference.
@@ -123,6 +126,29 @@ Win32Window::~Win32Window() {
 bool Win32Window::Create(const std::wstring& title,
                          const Point& origin,
                          const Size& size) {
+  // 增加应用程序互斥
+  HANDLE hMutexHandle = CreateMutex(NULL, TRUE, APP_MUTEX_NAME);
+  HWND handle = FindWindow(NULL, APP_WINDOW_NAME);
+  if (GetLastError() == ERROR_ALREADY_EXISTS) {
+    WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
+    GetWindowPlacement(handle, &place);
+    switch(place.showCmd) {
+      case SW_SHOWMAXIMIZED:
+        ShowWindow(handle, SW_SHOWMAXIMIZED);
+        break;
+      case SW_SHOWMINIMIZED:
+        ShowWindow(handle, SW_RESTORE);
+        break;
+      default:
+        ShowWindow(handle, SW_NORMAL);
+        break;
+    }
+    SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
+    SetForegroundWindow(handle);
+    ReleaseMutex(hMutexHandle);    
+    return 0;
+  }
+
   Destroy();
 
   const wchar_t* window_class =