Show Balloon popup via WinApi32

0

I want to show a balloon popup without adding a dependency on Windows.Forms. So I decided to use Shell_NotifyIcon from Shell32. Here is a code that I wrote (translated to C# from F#), which should create a notification icon and then show a balloon.

struct NotifyIconData {
  public System.Int32 cbSize;
  public System.IntPtr hWnd; // HWND
  public System.Int32 uID; // UINT
  public System.Int32 uFlags; // UINT
  public System.Int32 uCallbackMessage;  // UINT
  public System.IntPtr hIcon;  // HICON
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
  public System.String szTip;   // char[128]
  public System.Int32 dwState;  // DWORD
  public System.Int32 dwStateMask;  // DWORD
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
  public System.String szInfo; // char[256]
  public System.Int32 uTimeoutOrVersion;  // UINT
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)]
  public System.String szInfoTitle; // char[64]
  public System.Int32 dwInfoFlags;  // DWORD
  public Guid guidItem;
  public IntPtr hBalloonIcon; //HIcon
}

[DllImport("shell32.dll", SetLastError = true)]
extern bool Shell_NotifyIcon(uint dwMessage, ref NotifyIconData pnid);

[DllImport("kernel32.dll")]
extern IntPtr GetConsoleWindow();

public NotifyIconData CreateNotify (IntPtr hwnd)
{
    var data = new NotifyIconData();
    data.cbSize = sizeof(NotifyIconData);
    data.hWnd = hwnd;
    data.uID = 0;
    data.uFlags = 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000020 | 0x00000080; // NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_STATE | NIF_GUID | NIF_SHOWTIP;
    data.szTip = "Tooltip";
    data.dwState = 0;
    data.dwStateMask = 0x00000001 ||| 0x00000002; // NIS_SHAREDICON | NIS_HIDDEN
    data.guidItem = Guid.NewGuid ();
    data.hIcon = PInvoke.User32.LoadIcon(IntPtr.Zero, IntPtr(32512));
    var result = Shell_NotifyIcon(0x00000000u, &(data));

    data.uVersion = 4;

    var versionResult = Shell_NotifyIcon(0x00000004u, &(data));

    return data;
}

public bool CreateBalloon (string message, NotifyIconData data)
{
    data.uFlags = 0x00000010 | 0x00000020; // NIF_INFO | NIF_GUID
    data.dwInfoFlags = 0x00000000; // NO ICON
    data.szInfo = message;
    data.szInfoTitle = "Balloon title";
    data.hBalloonIcon = PInvoke.User32.LoadIcon(IntPtr.Zero, IntPtr(32512));
    
    var result = Shell_NotifyIcon(0x000000001u, &(data));

    return result
}

public bool ShowBalloon (string message)
{
 
 let hwnd = GetConsoleWindow();
 
 var notify = CreateNotify (hwnd);
 return CreateBalloon (message, notify);
}

I can observe in logs, that everything seems to be fine, no errors could be seen.

2020-10-13T15:34:23.880+00 724 INFO create notify error 0, true
2020-10-13T15:34:23.889+00 724 INFO set version error 0, true
2020-10-13T15:34:23.889+00 724 INFO baloon error 0, true

But the balloon doesn't appear on a screen. I'm wondering where the error could be located (maybe I'm missing calling some function or the icon should be set (right now I ignore it)?). I also tried to:

  • create a balloon without creating at first a notify,
  • invoke SetThreadDesktop(OpenInputDesktop(0u, true, GENERIC_ALL)) from user32 before calling "show balloon".

But it still doesn't work.

Here is the original code wrote in F#:

type NotifyIconData =
    struct
      val mutable cbSize: System.Int32  // DWORD
      val mutable hWnd: System.IntPtr // HWND
      val mutable uID: System.Int32 // UINT
      val mutable uFlags: System.Int32  // UINT
      val mutable uCallbackMessage: System.Int32  // UINT
      val mutable hIcon: System.IntPtr  // HICON
      [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)>]
      val mutable szTip: System.String  // char[128]
      val mutable dwState: System.Int32  // DWORD
      val mutable dwStateMask: System.Int32  // DWORD
      [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)>]
      val mutable szInfo: System.String // char[256]
      val mutable uTimeoutOrVersion: System.Int32  // UINT
      [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)>]
      val mutable szInfoTitle: System.String // char[64]
      val mutable dwInfoFlags: System.Int32  // DWORD
      val mutable guidItem: Guid
      val mutable hBalloonIcon: IntPtr //HIcon
    end

  [<DllImport("shell32.dll", SetLastError = true)>]
  extern bool Shell_NotifyIcon(uint dwMessage, NotifyIconData& pnid)

  [<DllImport("kernel32.dll")>]
  extern IntPtr GetConsoleWindow();

  let createNotify hwnd =
    let mutable data = NotifyIconData()
    data.cbSize <- sizeof<NotifyIconData>
    data.hWnd <- hwnd
    data.uID <- 0
    data.uFlags <- 0x00000001 ||| 0x00000002 ||| 0x00000004 ||| 0x00000008 ||| 0x00000020 ||| 0x00000080
    data.szTip <- "Tooltip"
    data.dwState <- 0
    data.dwStateMask <- 0x00000001 ||| 0x00000002
    data.guidItem <- Guid.NewGuid ()
    data.hIcon <- PInvoke.User32.LoadIcon(IntPtr.Zero, IntPtr(32512))
    let result = Shell_NotifyIcon(uint NotifyCommand.Add, &(data))

    data.uTimeoutOrVersion <- 4

    let versionResult = Shell_NotifyIcon(uint NotifyCommand.SetVersion, &(data))
    data

  let createBalloon message (d: NotifyIconData) =
    let mutable data = d
    
    data.uFlags <-  0x00000010 ||| 0x00000020
    data.dwInfoFlags <- 4
    data.hBalloonIcon <- PInvoke.User32.LoadIcon(IntPtr.Zero, IntPtr(32512))
    data.szInfo <- message
    data.szInfoTitle <- "Balloon title"
    data.uTimeoutOrVersion <- 50000
    let result = Shell_NotifyIcon(uint NotifyCommand.Modify, &(data))

    result

  let showBallon hwnd message =
    GetConsoleWindow ()
    |> createNotify 
    |> createBalloon message
c#
.net
winapi
f#
windows-10
asked on Stack Overflow Oct 13, 2020 by MNie • edited Oct 15, 2020 by MNie

2 Answers

1

The sample you provide seems to work in C# console app:

public static NotifyIconData CreateNotify(IntPtr hwnd)
    {
        var data = new NotifyIconData();
        data.cbSize = Marshal.SizeOf(typeof(NotifyIconData));
        data.hWnd = hwnd;
        data.uID = 0;
        data.uFlags = 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000020 | 0x00000080; // NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_STATE | NIF_GUID | NIF_SHOWTIP;
        data.szTip = "Tooltip";
        data.dwState = 0;
        data.dwStateMask = 0x00000001 | 0x00000002; // NIS_SHAREDICON | NIS_HIDDEN
        data.guidItem = Guid.NewGuid();
        var result = Shell_NotifyIcon(0x00000000u,  ref data);
        data.uTimeoutOrVersion = 4;
        var versionResult = Shell_NotifyIcon(0x00000004u, ref data);
        return data;
    }

    public static bool CreateBalloon(string message, NotifyIconData data)
    {
        data.uFlags = 0x00000010 | 0x00000020; // NIF_INFO | NIF_GUID
        data.dwInfoFlags = 0x00000000; // NO ICON
        data.szInfo = message;
        data.szInfoTitle = "Balloon title";
        var result = Shell_NotifyIcon(0x000000001u, ref data);
        return result;
    }

    public static bool ShowBalloon(string message)
    {
        IntPtr hwnd = GetConsoleWindow();
        var notify = CreateNotify(hwnd);
        return CreateBalloon(message, notify);
    }
    static void Main(string[] args)
    {
        bool ret = ShowBalloon("test");
        Console.WriteLine("Hello World!");
    }

And there is also a simple sample work for me:

static void Main(string[] args)
{
    //ShowBalloon("test");
    var data = new NOTIFYICONDATA();
    data.cbSize = Marshal.SizeOf(typeof(NOTIFYICONDATA));
    data.uID = 0;
    data.uFlags = 0x00000010; // NIF_INFO
    data.dwInfoFlags = 0x00000000; // NO ICON
    data.szInfo = "test";
    data.szInfoTitle = "Balloon title";
    bool result = Shell_NotifyIcon(0x00000001, ref data);
    Console.WriteLine("Hello World!");
}
answered on Stack Overflow Oct 15, 2020 by Drake Wu
1

@Drake answer shows me where the error could be located so I added more robust logging to a working C# example and F# example (which is added at the end of the question I didn't at it at first since I thought that the code is 1:1 between C# and F#). The problem was with setting the cbSize value. I used sizeof to calculate the size of a NotifyIconData structure. But as mentioned here, sizeof would return the size of a System.Type instead of NotifyIconData. So I changed sizeof to a Marshal.SizeOf(typedefof<NotifyIconData>) and everything starts working as expected.

Here are the logs from a C# app (which as Drake mentioned is working just fine)

... when I create notify icon
cbSize: 528, hwnd: 983580, uid: 0, uflags: 175, sztip: Tooltip, dwState: 0, dwStateMask: 3, guidItem: 9367e9cd-1fc2-4312-83dd-22b6e03486b9

... when version is set
cbSize: 528, hwnd: 983580, uid: 0, uflags: 175, sztip: Tooltip, dwState: 0, dwStateMask: 3, guidItem: 9367e9cd-1fc2-4312-83dd-22b6e03486b9

... when we enter `createBalloon` function
cbSize: 528, hwnd: 983580, uid: 0, uflags: 175, sztip: Tooltip, dwState: 0, dwStateMask: 3, guidItem: 9367e9cd-1fc2-4312-83dd-22b6e03486b9

... after balloon creation
cbSize: 528, hwnd: 983580, uid: 0, uflags: 48, sztip: Tooltip, dwState: 0, dwStateMask: 3, guidItem: 9367e9cd-1fc2-4312-83dd-22b6e03486b9

Here are the logs from a F# version:

... when I create notify icon



cbSize: 96, hwnd: 591150n, uid: 0, uflags: 175, sztip: "Tooltip", dwState: 0, dwStateMask: 3, guidItem: 1ee87d18-ef8d-48ba-aee9-f197de6425a4

... when version is set
cbSize: 96, hwnd: 591150n, uid: 0, uflags: 175, sztip: "Tooltip", dwState: 0, dwStateMask: 3, guidItem: 1ee87d18-ef8d-48ba-aee9-f197de6425a4

... when we enter `createBalloon` function
cbSize: 96, hwnd: 591150n, uid: 0, uflags: 175, sztip: "Tooltip", dwState: 0, dwStateMask: 3, guidItem: 1ee87d18-ef8d-48ba-aee9-f197de6425a4

... after balloon creation
cbSize: 96, hwnd: 591150n, uid: 0, uflags: 48, sztip: "Tooltip", dwState: 0, dwStateMask: 3, guidItem: 1ee87d18-ef8d-48ba-aee9-f197de6425a4

And here is the fully working F# example:

  type NotifyIconData =
    struct
      val mutable cbSize: System.Int32  // DWORD
      val mutable hWnd: System.IntPtr // HWND
      val mutable uID: System.Int32 // UINT
      val mutable uFlags: System.Int32  // UINT
      val mutable uCallbackMessage: System.Int32  // UINT
      val mutable hIcon: System.IntPtr  // HICON
      [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)>]
      val mutable szTip: System.String  // char[128]
      val mutable dwState: System.Int32  // DWORD
      val mutable dwStateMask: System.Int32  // DWORD
      [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)>]
      val mutable szInfo: System.String // char[256]
      val mutable uTimeoutOrVersion: System.Int32  // UINT
      [<MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)>]
      val mutable szInfoTitle: System.String // char[64]
      val mutable dwInfoFlags: System.Int32  // DWORD
      val mutable guidItem: Guid
      val mutable hBalloonIcon: IntPtr //HIcon
    end

  [<DllImport("shell32.dll", SetLastError = true)>]
  extern bool Shell_NotifyIcon(uint dwMessage, NotifyIconData& pnid)

  [<DllImport("kernel32.dll")>]
  extern IntPtr GetConsoleWindow();

  let createNotify hwnd =
    let mutable data = NotifyIconData()
    data.cbSize <- Marshal.SizeOf(typedefof<NotifyIconData>)
    data.hWnd <- hwnd
    data.uID <- 0
    data.uFlags <- 0x00000001 ||| 0x00000002 ||| 0x00000004 ||| 0x00000008 ||| 0x00000020 ||| 0x00000080
    data.szTip <- "Tooltip"
    data.dwState <- 0
    data.dwStateMask <- 0x00000001 ||| 0x00000002
    data.guidItem <- Guid.NewGuid ()
    data.hIcon <- PInvoke.User32.LoadIcon(IntPtr.Zero, IntPtr(32512))
    let result = Shell_NotifyIcon(uint NotifyCommand.Add, &(data))

    data.uTimeoutOrVersion <- 4

    let versionResult = Shell_NotifyIcon(uint NotifyCommand.SetVersion, &(data))
    data

  let createBalloon message (d: NotifyIconData) =
    let mutable data = d
    
    data.uFlags <-  0x00000010 ||| 0x00000020
    data.dwInfoFlags <- 4
    data.hBalloonIcon <- PInvoke.User32.LoadIcon(IntPtr.Zero, IntPtr(32512))
    data.szInfo <- message
    data.szInfoTitle <- "Balloon title"
    data.uTimeoutOrVersion <- 50000
    let result = Shell_NotifyIcon(uint NotifyCommand.Modify, &(data))

    result

  let showBallon hwnd message =
    GetConsoleWindow ()
    |> createNotify 
    |> createBalloon message
answered on Stack Overflow Oct 15, 2020 by MNie

User contributions licensed under CC BY-SA 3.0