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无效
解决方案
核心问题修复点
任务栏独立项修复
设置通知窗体的ShowInTaskbar = false,同时通过Win32 APISetParent将通知窗口设置为撰写窗口的真正子窗口,修改窗口样式移除WS_POPUP属性、添加WS_CHILD属性,从窗口层级上归属到撰写窗口,彻底解决独立任务栏条目问题。定位偏移修复
原有代码使用GetWindowRect获取的是窗口包含外边框、阴影的全尺寸坐标,计算时会出现偏移。改为先获取撰写窗口客户区大小,再通过ClientToScreen将客户区右下角坐标转换为屏幕坐标,以此为基准计算通知位置,适配不同DPI、窗口边框样式下的定位需求。跟随移动修复
子窗口默认不会自动响应父窗口的移动、大小调整事件,需要挂钩父窗口的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
相关产品推荐
相关产品推荐

