You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

VSTO Outlook:撰写窗口弹出子级自定义通知实现问题

VSTO Outlook 撰写窗口内嵌浮动通知实现方案

问题描述

开发VSTO Outlook Add-in时,需要在邮件撰写窗口弹出携带自定义文本的浮动通知,支持指定时长(如5秒)后自动消失,且通知窗口必须为撰写窗口的子窗口。

初始实现代码如下:

Outlook.Inspector currentInspector = this.Window as Outlook.Inspector;
IOleWindow ioleWnd = (IOleWindow)currentInspector;
ioleWnd.GetWindow(out IntPtr phwnd);

NativeWindow nativeWnd = new System.Windows.Forms.NativeWindow();
nativeWnd.AssignHandle(phwnd);

// frm为自定义通知窗口:无边框、无最大/最小/关闭按钮、无标题栏
Form frm = new Form();
frm.ControlBox = false;
frm.FormBorderStyle = FormBorderStyle.None;


NativeMethods.Rect rect = new NativeMethods.Rect();
NativeMethods.GetWindowRect(phwnd, ref rect);


frm.Left = rect.Right - 85;
frm.Top = rect.Bottom - 55;
frm.Width = 80;
frm.Height = 50;
frm.Location = new System.Drawing.Point(rect.Right - 85, rect.Bottom - 55);
frm.BackColor = System.Drawing.Color.Red;

TextBox txtBox = new TextBox();
txtBox.BackColor = System.Drawing.Color.Red;
txtBox.ForeColor = System.Drawing.Color.White;
txtBox.Location = new System.Drawing.Point(5, 5);
txtBox.Visible = true;
txtBox.Text = "This is a notification message";

frm.Controls.Add(txtBox);
frm.Show(nativeWnd);

代码运行后存在三个问题:

  • 通知窗口未正确定位在撰写窗口的右下角位置
  • 移动撰写窗口时,通知窗口位置固定,不会随撰写窗口同步移动
  • 通知窗口在Windows任务栏中显示为独立程序项,未归属到对应撰写窗口,设置frm.Owner = nativeWnd无效

解决方案

核心问题修复点

  1. 任务栏独立项修复
    设置通知窗体的ShowInTaskbar = false,同时通过Win32 API SetParent将通知窗口设置为撰写窗口的真正子窗口,修改窗口样式移除WS_POPUP属性、添加WS_CHILD属性,从窗口层级上归属到撰写窗口,彻底解决独立任务栏条目问题。

  2. 定位偏移修复
    原有代码使用GetWindowRect获取的是窗口包含外边框、阴影的全尺寸坐标,计算时会出现偏移。改为先获取撰写窗口客户区大小,再通过ClientToScreen将客户区右下角坐标转换为屏幕坐标,以此为基准计算通知位置,适配不同DPI、窗口边框样式下的定位需求。

  3. 跟随移动修复
    子窗口默认不会自动响应父窗口的移动、大小调整事件,需要挂钩父窗口的WM_MOVE、WM_SIZE、WM_WINDOWPOSCHANGED消息,收到消息时重新计算并更新通知窗口位置,即可实现同步移动。


完整实现代码

首先定义所需的Win32 API:

public static class NativeMethods
{
    public const int GWL_STYLE = -16;
    public const int WS_CHILD = 0x40000000;
    public const int WS_POPUP = unchecked((int)0x80000000);
    public const int WM_WINDOWPOSCHANGED = 0x0047;
    public const int WM_MOVE = 0x0003;
    public const int WM_SIZE = 0x0005;
    public const int GWLP_WNDPROC = -4;

    [StructLayout(LayoutKind.Sequential)]
    public struct Rect
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct Point
    {
        public int X;
        public int Y;
    }

    [DllImport("user32.dll")]
    public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

    [DllImport("user32.dll")]
    public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

    [DllImport("user32.dll")]
    public static extern int GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll")]
    public static extern bool GetClientRect(IntPtr hWnd, ref Rect lpRect);

    [DllImport("user32.dll")]
    public static extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint);

    [DllImport("user32.dll")]
    public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);

    [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
    public static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

    [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
    public static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

    [DllImport("user32.dll")]
    public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
    {
        if (IntPtr.Size == 8)
            return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
        else
            return new IntPtr(SetWindowLongPtr32(hWnd, nIndex, dwNewLong).ToInt32());
    }
}

自定义通知窗体实现:

public class NotificationForm : Form
{
    private readonly IntPtr _parentHwnd;
    private IntPtr _oldParentWndProc;
    private readonly int _autoCloseMs;
    private NativeWindow _parentNativeWnd;
    private System.Windows.Forms.Timer _closeTimer;
    private readonly WndProcDelegate _wndProcDelegate;

    public NotificationForm(IntPtr parentHwnd, string notifyText, int autoCloseMs = 5000)
    {
        _parentHwnd = parentHwnd;
        _autoCloseMs = autoCloseMs;
        _wndProcDelegate = ParentWndProc;
        // 基础窗体配置
        this.ControlBox = false;
        this.FormBorderStyle = FormBorderStyle.None;
        this.ShowInTaskbar = false;
        this.StartPosition = FormStartPosition.Manual;
        this.BackColor = Color.Firebrick;
        this.Size = new Size(220, 60);

        // 添加通知文本
        Label txtLabel = new Label
        {
            ForeColor = Color.White,
            BackColor = Color.Transparent,
            Location = new Point(10, 20),
            AutoSize = true,
            Text = notifyText
        };
        this.Controls.Add(txtLabel);
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        // 1. 设置为父窗口子窗口,修改窗口样式
        NativeMethods.SetParent(this.Handle, _parentHwnd);
        int style = NativeMethods.GetWindowLong(this.Handle, NativeMethods.GWL_STYLE);
        style &= ~NativeMethods.WS_POPUP;
        style |= NativeMethods.WS_CHILD;
        NativeMethods.SetWindowLong(this.Handle, NativeMethods.GWL_STYLE, style);

        // 2. 子类化父窗口,监听移动/大小调整消息
        _parentNativeWnd = new NativeWindow();
        _parentNativeWnd.AssignHandle(_parentHwnd);
        _oldParentWndProc = NativeMethods.SetWindowLongPtr(_parentHwnd, NativeMethods.GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProcDelegate));

        // 3. 初始定位
        UpdatePosition();

        // 4. 配置自动关闭
        _closeTimer = new System.Windows.Forms.Timer { Interval = _autoCloseMs };
        _closeTimer.Tick += (s, args) =>
        {
            _closeTimer.Stop();
            this.Close();
        };
        _closeTimer.Start();
    }

    private void UpdatePosition()
    {
        NativeMethods.Rect clientRect = new NativeMethods.Rect();
        NativeMethods.GetClientRect(_parentHwnd, ref clientRect);
        NativeMethods.Point rightBottom = new NativeMethods.Point
        {
            X = clientRect.Right,
            Y = clientRect.Bottom
        };
        NativeMethods.ClientToScreen(_parentHwnd, ref rightBottom);
        // 右下角留10px边距
        int x = rightBottom.X - this.Width - 10;
        int y = rightBottom.Y - this.Height - 10;
        // 子窗口坐标使用父窗口客户区坐标,需要把屏幕坐标转成客户区坐标
        NativeMethods.Point clientPos = new NativeMethods.Point { X = x, Y = y };
        ScreenToClient(_parentHwnd, ref clientPos);
        NativeMethods.MoveWindow(this.Handle, clientPos.X, clientPos.Y, this.Width, this.Height, true);
    }

    private IntPtr ParentWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
    {
        if (msg == NativeMethods.WM_MOVE || msg == NativeMethods.WM_SIZE || msg == NativeMethods.WM_WINDOWPOSCHANGED)
        {
            UpdatePosition();
        }
        return NativeMethods.CallWindowProc(_oldParentWndProc, hWnd, msg, wParam, lParam);
    }

    delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    static extern bool ScreenToClient(IntPtr hWnd, ref NativeMethods.Point lpPoint);

    protected override void OnClosed(EventArgs e)
    {
        base.OnClosed(e);
        // 还原父窗口消息过程
        if (_oldParentWndProc != IntPtr.Zero)
        {
            NativeMethods.SetWindowLongPtr(_parentHwnd, NativeMethods.GWLP_WNDPROC, _oldParentWndProc);
            _parentNativeWnd.ReleaseHandle();
        }
        _closeTimer?.Dispose();
    }
}

调用时替换原有逻辑即可:

Outlook.Inspector currentInspector = this.Window as Outlook.Inspector;
IOleWindow ioleWnd = (IOleWindow)currentInspector;
ioleWnd.GetWindow(out IntPtr phwnd);

NotificationForm notify = new NotificationForm(phwnd, "This is a notification message", 5000);
notify.Show();

内容的提问来源于stack exchange,提问作者Willy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 05:06:25