Tzero Ocne
Tzero Ocne

Reputation: 11

Notify icon window won't exit program if hwnd set as foreground window

I have a notification icon app in zig using Win32 API. It has this code as its WndProc:

const std = @import("std");
const windows = std.os.windows;
const zeroes = std.mem.zeroes;

pub const WNDPROC = *const fn (
    hwnd: windows.HWND,
    uMsg: windows.UINT,
    wParam: windows.WPARAM,
    lParam: windows.LPARAM,
) callconv(windows.WINAPI) windows.LRESULT;

pub const WNDCLASSEXA = extern struct {
    cbSize: windows.UINT = @sizeOf(WNDCLASSEXA),
    style: windows.UINT,
    lpfnWndProc: WNDPROC,
    cbClsExtra: i32 = 0,
    cbWndExtra: i32 = 0,
    hInstance: windows.HINSTANCE,
    hIcon: ?windows.HICON,
    hCursor: ?windows.HCURSOR,
    hbrBackground: ?windows.HBRUSH,
    lpszMenuName: ?[*:0]const u8,
    lpszClassName: [*:0]const u8,
    hIconSm: ?windows.HICON,
};

pub const NOTIFYICONDATAW = extern struct {
    cbSize: windows.DWORD = @sizeOf(NOTIFYICONDATAW),
    hWnd: windows.HWND,
    uID: windows.UINT,
    uFlags: windows.UINT,
    uCallbackMessage: windows.UINT,
    hIcon: windows.HICON,
    szTip: [128]u8,
    dwState: windows.DWORD,
    dwStateMask: windows.DWORD,
    szInfo: [256]u16,
    DUMMYUNIONNAME: extern union {
        uTimeout: windows.UINT,
        uVersion: windows.UINT,
    },
    szInfoTitle: [64]u16,
    dwInfoFlags: windows.DWORD,
    guidItem: windows.GUID,
    hBalloonIcon: windows.HICON,
};

pub const MSG = extern struct {
    hWnd: ?windows.HWND,
    message: windows.UINT,
    wParam: windows.WPARAM,
    lParam: windows.LPARAM,
    time: windows.DWORD,
    pt: windows.POINT,
    lPrivate: windows.DWORD,
};

pub const HBITMAP = *opaque {};

pub const MENUITEMINFOW = extern struct {
    cbSize: windows.UINT = @sizeOf(MENUITEMINFOW),
    fMask: windows.UINT,
    fType: windows.UINT,
    fState: windows.UINT,
    wID: windows.UINT,
    hSubMenu: windows.HMENU,
    hbmpChecked: HBITMAP,
    hbmpUnchecked: HBITMAP,
    dwItemData: windows.ULONG_PTR,
    dwTypeData: windows.LPCWSTR,
    cch: windows.UINT,
    hbmpItem: HBITMAP,
};

pub const NIF_ICON = 0x00000002;
pub const NIF_INFO = 0x00000010;
pub const NIF_MESSAGE = 0x00000001;
pub const NIM_ADD = 0x00000000;
pub const NIM_DELETE = 0x00000002;
pub const SW_HIDE = 0;
pub const TPM_LEFTALIGN = 0x0000;
pub const TPM_NONOTIFY = 0x0080;
pub const TPM_RETURNCMD = 0x100;
pub const TPM_RIGHTBUTTON = 0x0002;
pub const WM_CLOSE = 0x0010;
pub const WM_COMMAND = 0x0111;
pub const WM_DESTROY = 0x0002;
pub const WM_LBUTTONUP = 0x0202;
pub const WM_RBUTTONUP = 0x0205;
pub const WM_USER = 0x0400;

pub extern "shell32" fn Shell_NotifyIconW(
    dwMessage: windows.DWORD,
    lpData: [*c]NOTIFYICONDATAW,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "shell32" fn ExtractIconExA(
    lpszFile: windows.LPCSTR,
    nIconIndex: i32,
    phiconLarge: [*c]windows.HICON,
    phiconSmall: [*c]windows.HICON,
    nIcons: windows.UINT,
) callconv(windows.WINAPI) windows.UINT;

pub extern "user32" fn CreateWindowExA(
    dwExStyle: windows.DWORD,
    lpClassName: [*:0]const u8,
    lpWindowName: [*:0]const u8,
    dwStyle: windows.DWORD,
    X: i32,
    Y: i32,
    nWidth: i32,
    nHeight: i32,
    hWindParent: ?windows.HWND,
    hMenu: ?windows.HMENU,
    hInstance: windows.HINSTANCE,
    lpParam: ?windows.LPVOID,
) callconv(windows.WINAPI) ?windows.HWND;

pub extern "user32" fn PostQuitMessage(
    nExitCode: i32,
) callconv(windows.WINAPI) void;

pub extern "user32" fn DestroyWindow(
    hWnd: windows.HWND,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "user32" fn GetWindowLongPtrA(
    hWnd: windows.HWND,
    nIndex: i32,
) callconv(windows.WINAPI) windows.LONG_PTR;

pub extern "user32" fn SetWindowLongPtrA(
    hWnd: windows.HWND,
    nIndex: i32,
    dwNewLong: windows.LONG_PTR,
) callconv(windows.WINAPI) windows.LONG_PTR;

pub extern "user32" fn DefWindowProcA(
    hWnd: windows.HWND,
    Msg: windows.UINT,
    wParam: windows.WPARAM,
    lParam: windows.LPARAM,
) callconv(windows.WINAPI) windows.LRESULT;

pub extern "user32" fn GetMessageA(
    lpMsg: *MSG,
    hWnd: ?windows.HWND,
    wMsgFilterMin: windows.UINT,
    wMsgFilterMax: windows.UINT,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "user32" fn RegisterClassExA(
    *const WNDCLASSEXA,
) callconv(windows.WINAPI) windows.ATOM;

pub extern "user32" fn TranslateMessage(
    lpMsg: *const MSG,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "user32" fn DispatchMessageA(
    lpMsg: *const MSG,
) callconv(windows.WINAPI) windows.LRESULT;

pub extern "user32" fn CreatePopupMenu() callconv(windows.WINAPI) windows.HMENU;

pub extern "user32" fn GetCursorPos(
    lpPoint: [*c]windows.POINT,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "user32" fn SetForegroundWindow(
    hWnd: windows.HWND,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "user32" fn TrackPopupMenu(
    hMenu: windows.HMENU,
    uFlags: windows.UINT,
    x: i32,
    y: i32,
    nReserved: i32,
    hWnd: windows.HWND,
    prcRect: [*c]const windows.RECT,
) callconv(windows.WINAPI) windows.BOOL;

pub extern "user32" fn InsertMenuItemW(
    hMenu: std.os.windows.HMENU,
    item: std.os.windows.UINT,
    fByPosition: std.os.windows.BOOL,
    lpmi: [*c]MENUITEMINFOW,
) callconv(std.os.windows.WINAPI) std.os.windows.BOOL;

fn registerClassExA(window_class: *const WNDCLASSEXA) !windows.ATOM {
    const atom = RegisterClassExA(window_class);
    if (atom != 0) return atom;
    switch (windows.kernel32.GetLastError()) {
        .CLASS_ALREADY_EXISTS => return error.AlreadyExists,
        .INVALID_PARAMETER => unreachable,
        else => |err| return windows.unexpectedError(err),
    }
}

fn createWindowExA(
    dwExStyle: u32,
    lpClassName: [*:0]const u8,
    lpWindowName: [*:0]const u8,
    dwStyle: u32,
    X: i32,
    Y: i32,
    nWidth: i32,
    nHeight: i32,
    hWindParent: ?windows.HWND,
    hMenu: ?windows.HMENU,
    hInstance: windows.HINSTANCE,
    lpParam: ?*anyopaque,
) !windows.HWND {
    const window = CreateWindowExA(dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWindParent, hMenu, hInstance, lpParam);
    if (window) |win| return win;
    switch (windows.kernel32.GetLastError()) {
        .CLASS_DOES_NOT_EXIST => return error.ClassDoesNotExist,
        .INVALID_PARAMETER => unreachable,
        else => |err| return windows.unexpectedError(err),
    }
}

pub fn createIconFromFile(path: [:0]const u8) !windows.HICON {
    var icon: windows.HICON = undefined;
    const ret = ExtractIconExA(
        path,
        0,
        null,
        &icon,
        1,
    );
    if (ret != 1) {
        return error.NotIcon;
    }
    return icon;
}

var isRunning = true;

fn ProcessIconMessage(hwnd: windows.HWND, _: windows.UINT, _: windows.WPARAM, lParam: windows.LPARAM, tray: *Tray) void {
    if (lParam == WM_LBUTTONUP) {
        _ = DestroyWindow(hwnd);
        isRunning = false;
    } else if (lParam == WM_RBUTTONUP) {
        var point: windows.POINT = undefined;
        _ = GetCursorPos(&point);
        _ = SetForegroundWindow(hwnd);
        _ = TrackPopupMenu(tray.hmenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, point.x, point.y, 0, hwnd, null);
    }
}

fn WndProc(hwnd: windows.HWND, uMsg: windows.UINT, wParam: windows.WPARAM, lParam: windows.LPARAM) callconv(windows.WINAPI) windows.LRESULT {
    const tray_pointer = GetWindowLongPtrA(hwnd, 0);
    if (tray_pointer == 0) {
        return DefWindowProcA(hwnd, uMsg, wParam, lParam);
    }
    const tray = @as(*Tray, @ptrFromInt(@as(usize, @intCast(tray_pointer))));
    switch (uMsg) {
        WM_CLOSE => {
            _ = DestroyWindow(hwnd);
        },
        WM_DESTROY => {
            _ = PostQuitMessage(0);
        },
        WM_TRAY_CALLBACK_MESSAGE => ProcessIconMessage(hwnd, uMsg, wParam, lParam, tray),
        WM_COMMAND => ProcessIconMessage(hwnd, uMsg, wParam, lParam, tray),
        else => return DefWindowProcA(hwnd, uMsg, wParam, lParam),
    }

    return 0;
}

pub const ID_TRAY_FIRST = 1000;
pub const MFT_STRING = 0x00000000;
pub const WM_TRAY_CALLBACK_MESSAGE = WM_USER + 1;
pub const WC_TRAY_CLASS_NAME = "TRAY";

fn createMenu(id: *windows.UINT) windows.HMENU {
    const hmenu = CreatePopupMenu();
    {
        defer id.* += 1;
        var mitem = zeroes(MENUITEMINFOW);
        mitem.cbSize = @sizeOf(MENUITEMINFOW);
        mitem.fType = MFT_STRING;
        mitem.fState = 0;
        _ = InsertMenuItemW(hmenu, id.*, windows.TRUE, &mitem);
    }
    return hmenu;
}

pub const Tray = struct {
    wc: WNDCLASSEXA = zeroes(WNDCLASSEXA),
    hmenu: windows.HMENU = undefined,

    pub fn init(self: *Tray) !void {
        self.wc.cbSize = @sizeOf(WNDCLASSEXA);
        self.wc.lpfnWndProc = WndProc;
        self.wc.cbWndExtra = @sizeOf(*Tray);
        self.wc.hInstance = @as(windows.HINSTANCE, @ptrCast(windows.kernel32.GetModuleHandleW(null)));
        self.wc.lpszClassName = WC_TRAY_CLASS_NAME;
        _ = try registerClassExA(&self.wc);
    }

    pub fn setupMenu(self: *Tray, _: windows.HWND, _: NOTIFYICONDATAW) !void {
        var id: windows.UINT = ID_TRAY_FIRST;
        self.hmenu = createMenu(&id);
    }
};

pub fn main() !void {
    var tray = Tray{};
    const icon = try createIconFromFile("icon.ico");
    try tray.init();

    // try create window
    const hwnd = try createWindowExA(0, WC_TRAY_CLASS_NAME, WC_TRAY_CLASS_NAME, 0, 0, 0, 0, 0, null, null, tray.wc.hInstance, null);
    _ = SetWindowLongPtrA(hwnd, 0, @as(windows.LONG_PTR, @intCast(@intFromPtr(&tray))));
    const text = "Tray Icon";
    var szTip: [128]u8 = zeroes([128]u8); // Initialize the array to zeros

    // Copy the string literal into the fixed-size array
    std.mem.copyForwards(u8, &szTip, text);
    var nid: NOTIFYICONDATAW = zeroes(NOTIFYICONDATAW);
    nid.cbSize = @sizeOf(NOTIFYICONDATAW);
    nid.hWnd = hwnd;
    nid.uID = 1;
    nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_INFO;
    nid.uCallbackMessage = WM_TRAY_CALLBACK_MESSAGE;
    nid.hIcon = icon;
    nid.szTip = szTip;

    _ = Shell_NotifyIconW(NIM_ADD, &nid);

    try tray.setupMenu(hwnd, nid);

    while (isRunning) {
        var msg: MSG = undefined;
        _ = GetMessageA(&msg, hwnd, 0, 0);
        _ = TranslateMessage(&msg);
        _ = DispatchMessageA(&msg);
    }
}

The program will exit correctly if I just left-click on my app notification icon.

If I right-click first so it calls SetForegroundWindow(), then left-click, the icon is deleted from the notification area but the program doesn't exit, it just hangs there. If I Ctrl+C then the app exits properly.

I was expecting it to also just exit the program if I right-click before left-click.

This is the last few console output when just left clicking the icon where it quit properly

info: WndProc os.windows.HWND__opaque_2021@1b0d24 1025 1 512
info: Tray: 48926552960
info: WndProc os.windows.HWND__opaque_2021@1b0d24 1025 1 513
info: Tray: 48926552960
info: WndProc os.windows.HWND__opaque_2021@1b0d24 1025 1 514
info: Tray: 48926552960
info: WndProc os.windows.HWND__opaque_2021@1b0d24 144 0 0
info: Tray: 48926552960
info: WndProc os.windows.HWND__opaque_2021@1b0d24 2 0 0
info: Tray: 48926552960
info: WndProc os.windows.HWND__opaque_2021@1b0d24 130 0 0
info: Tray: 48926552960
  main
   ~\....\zig-notification-area
➜

And here is if I right click first

info: WndProc os.windows.HWND__opaque_2021@3a190e 1025 1 512
info: Tray: 149451435920
info: WndProc os.windows.HWND__opaque_2021@3a190e 1025 1 513
info: Tray: 149451435920
info: WndProc os.windows.HWND__opaque_2021@3a190e 1025 1 514
info: Tray: 149451435920
info: WndProc os.windows.HWND__opaque_2021@3a190e 144 0 0
info: Tray: 149451435920
info: WndProc os.windows.HWND__opaque_2021@3a190e 2 0 0
info: Tray: 149451435920
info: WndProc os.windows.HWND__opaque_2021@3a190e 130 0 0
info: Tray: 149451435920

It's almost identical except the app just stuck in there hanging not quitting the program, I'm expecting both of them result the program exit correctly

Upvotes: 1

Views: 102

Answers (0)

Related Questions