0

I've been wanting to incorporate systray functionality into my Flutter app so I went to modify the native C++ code that initiates the window etc to see if I could hook into it. Despite not having much prior experience in C++ I have been able to create an icon for my app in the systray with a menu that allows the window to be shown again when hidden (using ShowWindow(hwnd, SW_HIDE);) and to quit entirely.

However when an option in my systray menu is selected to show the window again using ShowWindow(hwnd, SW_NORMAL); after being hidden, the app stays blank like this:

enter image description here

Then, when the window is finally interacted with, the contents of the window show again:

enter image description here

Here is the code that I have added so far to my win32_window.cpp (from a default Flutter application). I haven't included the entire functions because I thought it would make things less clear, but I will also attach the full win32_window.cpp at the end of this post. Win32Window::CreateAndShow():

//Systray:
HICON hMainIcon;
hMainIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_APP_ICON));
nidApp.cbSize = sizeof(NOTIFYICONDATA); // sizeof the struct in bytes
nidApp.hWnd = (HWND) window;              //handle of the window which will process this app. messages
nidApp.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; //ORing of all the flags
nidApp.hIcon = hMainIcon; // handle of the Icon to be displayed, obtained from LoadIcon
nidApp.uCallbackMessage = WM_USER_SHELLICON;
StringCchCopy(nidApp.szTip, ARRAYSIZE(nidApp.szTip), L"All Platforms Test");
Shell_NotifyIcon(NIM_ADD, &nidApp);

return OnCreate();

Win32Window::WndProc():

if (message == WM_NCCREATE) { ... }
else if (message == WM_USER_SHELLICON) { //interacting with systray icon
    if (LOWORD(lparam) == WM_RBUTTONDOWN) { //right clicked
        POINT lpClickPoint;
        GetCursorPos(&lpClickPoint);
        hPopMenu = CreatePopupMenu();
        InsertMenu(hPopMenu,0xFFFFFFFF,MF_BYPOSITION|MF_STRING,IDM_SHOW,_T("Show"));
        InsertMenu(hPopMenu,0xFFFFFFFF,MF_BYPOSITION|MF_STRING,IDM_EXIT,_T("Quit"));
        SetForegroundWindow(window);
        TrackPopupMenu(hPopMenu,TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_BOTTOMALIGN,lpClickPoint.x, lpClickPoint.y,0,window,NULL);
    }
    else if (LOWORD(lparam) == WM_LBUTTONDOWN) { //left clicked
        ShowWindow(window, SW_NORMAL);
        //LOOK: works but shows blank screen until is interacted with (mouse enters or key is pressed etc)
    }
}
else if (message == WM_COMMAND) { //if message is a command event such as a click on the exit menu option
    int wmId;
    wmId = LOWORD(wparam);

    if (wmId == IDM_EXIT) { //if quit has been pressed
        Shell_NotifyIcon(NIM_DELETE,&nidApp);
        DestroyWindow(window);
    }
    else if (wmId == IDM_SHOW) {
        ShowWindow(window, SW_NORMAL);
        //LOOK: works but shows blank screen until is interacted with (mouse enters or key is pressed etc)
    }

Win32Window::MessageHandler():

switch (message) {
  ...
  case WM_CLOSE: //stop window from closing normally, can only be closed when DestroyWindow() is run from systray
    //Hide window and continue running in background.
    ShowWindow(hwnd, SW_HIDE);
    return 0;
}

Link to full win32_window.cpp here.

What's going on here? I thought using UpdateWindow() would help but then I realise the app is painted upon ShowWindow() anyway. My guess is that this has something to do with Flutter's run loop being blocked but I can't figure out where to go next, especially considering I usually don't dabble in C++ but just wanted to add an extra feature to my app when running on Windows.

Any help would be greatly appreciated, thanks.

Letal1s
  • 47
  • 2
  • 9
  • Please show your actual code, or at least a [mcve] – Remy Lebeau May 18 '21 at 17:58
  • Sorry about that, I've updated the post with the code I added to the default project, including a link to the full win32_window.cpp. – Letal1s May 18 '21 at 18:22
  • Small nitpick: after `TrackPopupMenu()` exits, call `PostMessage(window, WM_NULL, 0, 0);` per the [documentation](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackpopupmenu). Also, consider using `Shell_NotifyIcon(NIM_SETVERSION)` to set `uVersion=4` and then you can handle `WM_CONTEXTMENU` (and optionally `NIN_KEYSELECT` and `NIN_SELECT`) instead of `WM_RBUTTONDOWN` (why not `UP`?), and mouse coordinates will be provided in the `wParam` – Remy Lebeau May 18 '21 at 19:23
  • Thanks for the suggestions. I've added PostMessage() after TrackPopupMenu() and set uVersion to 4, although after looking at the documentation I can't figure out what the PostMessage() is meant to actually do. What's the point of it? I ended up changing out WM_RBUTTONDOWN to UP too, I tried using WM_CONTEXTMENU but it didn't seem to be working correctly, and WM_RBUTTONUP seems to be better than what I had previous. – Letal1s May 18 '21 at 19:37
  • "*I've added PostMessage() after TrackPopupMenu() ... I can't figure out what the PostMessage() is meant to actually do.*" - did you read the explanation in the Remarks section of the [documentation](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackpopupmenu)? This is a very well-known workaround for an issue with using `TrackPopupMenu()` for a systray menu. But MS did not acknowledge or document the issue for many years. Now they have, well sort of. – Remy Lebeau May 18 '21 at 19:45
  • Ah gotcha, I have given it a look but I was a little confused because I never seemed to experience the immediately appearing and disappearing issue it was on about, but I think I understand what it's trying to do. I wonder if a PostMessage with the correct message might be able to fix my problem after ShowWindow(window, SW_NORMAL) is ran, as something seems to be unaware of being active until the mouse enters? – Letal1s May 18 '21 at 20:11
  • According to the [MSDN](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackpopupmenu#remarks): However, when the current window is the foreground window, the second time this menu is displayed, it appears and then immediately disappears. To correct this, you must force a task switch to the application that called TrackPopupMenu. This is done by posting a benign message to the window or thread, as shown in the following code sample. So if you PostMessage correctly, you can solve this problem. – Zeus May 19 '21 at 05:39
  • I've added the PostMessage() after TrackPopupMenu() like detailed in the documentation but it doesn't make any difference: `TrackPopupMenu(hPopMenu,TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_BOTTOMALIGN,lpClickPoint.x, lpClickPoint.y,0,window,NULL); PostMessage(window, WM_NULL, 0, 0);` Is there another message I could pass into this when showing the window once again? Something else I have noticed is that when minimizing the blank window and pressing the taskbar icon again that the window draws fine, so there must be something my code is missing that is done when showing a window from the taskbar. – Letal1s May 19 '21 at 15:54
  • I think the function has been implemented, maybe the window is not redrawn normally, you need to check whether the window implements the redraw function immediately. – Zeus May 20 '21 at 03:24
  • Well I would have figured that when ShowWindow() is called that the window is automatically drawn as I remember reading that somewhere in the documentation. Anyhow I did try adding UpdateWindow() to force a redraw after the window is shown, with the same result still. I also tried using InvalidateRect() before UpdateWindow() to make sure the entire client area is set to be redrawn, but still nothing. Should I be trying to redraw elsewhere in the code instead of directly after ShowWindow() is ran? – Letal1s May 20 '21 at 17:44

1 Answers1

0

Ok so I've worked out why it wasn't working. When closing the window, I couldn't just use SW_HIDE, but SW_MINIMIZE too. Otherwise attempting to redraw the window wouldn't work correctly:

ShowWindow(hwnd, SW_MINIMIZE);
ShowWindow(hwnd, SW_HIDE);

After that, when showing the window it got drawn but wasn't the active window, but adding SetForegroundWindow() fixed that:

ShowWindow(window, SW_NORMAL);
SetForegroundWindow(window);

Thanks for everyone's help :)

Letal1s
  • 47
  • 2
  • 9