Microsoft Outlook中的其他自定义面板






4.98/5 (42投票s)
Microsoft Office 应用程序用户界面中未经文档化的集成示例。
引言
我从事开发集成到 Microsoft Office 中的应用程序和插件已经很长时间了,无论您正在开发哪种类型的应用程序,总会遇到同样的挫败感:官方 Microsoft 接口(如 Outlook 对象模型或 Word 对象模型)提供的功能有限。这些接口中最薄弱的部分之一是缺乏对用户界面自定义的支持——只要您想添加自己的工具栏,那没问题,但如果您想做任何超越此范围的事情,您就会遇到一个巨大的障碍。或者,真的会这样吗?本文应能为您提供一些关于如何通过一些尽管有点“肮脏”但可靠的技巧,全部使用 100% 托管代码来克服 Word、Excel、Outlook 和其他 Office 应用程序这些限制的指导。
为了演示高级 UI 集成的原理,我决定构建一个简单的面板添加到 Microsoft Outlook 中,该面板将显示选定电子邮件的一些基本信息。我选择了 Microsoft Outlook,但这个原理可以应用于 Office 系列中的任何应用程序,例如 Word、Excel、OneNote 等。
破解 Outlook
在开始编码之前,让我们先讨论幕后的原理。与其他任何 Windows 应用程序一样,Microsoft Outlook 的主窗口由多个子窗口组成(在 .NET 概念中,它们将是 Control.Controls
集合的成员),因此应该可以将您自己的窗口添加到此层级结构中。在添加任何新窗口之前,我们首先需要了解 Outlook 的窗口结构是什么样的。我们可以使用我最喜欢的工具 Spy++ 来查看此结构。
从 Spy++ 中,您可以看到顶部有一个工具栏面板窗口(绿色),左侧有一个文件夹列表视图窗口(紫色),最后,右侧是主网格(红色)。
现在,我们知道了结构,就可以解决关键问题——我们无处放置自己的窗口。要获取对话框中的一些可用空间,我们将不得不从现有子窗口“借用”空间。我想将我的面板停靠在右侧,这使得主网格成为借用空间的最佳候选。因此,如果我们减小邮件网格的宽度,我们应该会有一些可用空间来放置我们自己的窗口。
但是,理论就到此为止——让我们看看如何实现它。
实现
我想构建一个通用的框架,允许将任何标准的 .NET UserControl 停靠到 Outlook 中,因此我决定使用以下结构。
我们将从其借用空间的窗口称为“兄弟窗口”。 Outlook 主窗口的任何子窗口都可以成为兄弟窗口;但是,在本例中,我们将使用邮件网格。如上所述,我们减小兄弟窗口的宽度,从而在对话框中创建可用空间。我们将把我们自己的窗口 PanelContainer
放置在新创建的可用空间中。PanelContainer
有两个目的:它包含一个空的 Panel
控件,可以托管任何 .NET 控件,并为背景和边框提供 Outlook 的外观和感觉(否则,背景将是白色,并且没有边框线)。一旦我们将 PanelContainer
放置好,我们就可以简单地将一个标准的 .NET UserControl
分配给它,这就完成了——PanelContainer
将完成所有繁重的工作,而嵌套的控件甚至不需要知道它“生活”在 Outlook 面板中。
要通过 Outlook 插件最轻松地显示我们的 PanelContainer
。Outlook 插件是一个 DLL 库(托管或非托管),它实现了 IDTExtensibility2
COM 接口。
public interface IDTExtensibility2
{
void OnAddInsUpdate (ref Array custom);
void OnBeginShutdown (ref Array custom);
void OnConnection (Object Application, ext_ConnectMode connectMode,
Object AddInInst, ref Array custom);
void OnDisconnection (ext_DisconnectMode RemoveMode, ref Array custom);
void OnStartupComplete (ref Array custom);
}
当 Outlook 启动时,它会加载 DLL,如果发现实现了 IDTExtensibility2
接口,它将调用 OnStartupComplete
方法。在任何进程/应用程序中,都应该始终只有一个线程执行所有子窗口的消息循环——UI 线程。对我们来说幸运的是,对 OnStartupComplete
的调用(如 IDTExtensibility2
接口的所有调用)都来自 UI 线程,这非常重要,因为我们将创建 PanelContainer
窗口,并将其分配为 Outlook 主窗口的子窗口。如果此调用来自另一个线程,我们将遇到更大的问题,因为 PanelContainer
实例将在 UI 线程之外创建,这将导致严重的稳定性问题。
public void OnStartupComplete(ref System.Array custom)
{
...
//Find Outlook window handle (HWND)
IntPtr outlookWindow = FindOutlookWindow();
//Create new container instance
_panelContainer = new PanelContainer();
//Set the parent window of the panel container to be Outlook main window
SafeNativeMethods.SetParent(_panelContainer.Handle, outlookWindow);
...
}
我们将 PanelContainer
实例变成了一个子窗口;现在,我们需要将其移动到相应的位置。但是,在此之前,我们需要减小兄弟窗口(即邮件网格)的宽度。为了找到兄弟窗口,我们将使用 FindWindowEx
API(参见 MSDN 文档)方法,该方法将返回 Outlook 主窗口中第一个具有指定窗口类的子窗口。这时 Spy++ 又派上用场了——我们可以看到邮件网格的窗口类是 rctrl_renwnd32
。
private const string SIBLING_WINDOW_CLASS = "rctrl_renwnd32";
IntPtr siblingWindow = SafeNativeMethods.FindWindowEx(outlookWindow,
IntPtr.Zero, SIBLING_WINDOW_CLASS, null);
现在,我们拥有了所有需要的窗口句柄;因此,我们最终可以调整兄弟窗口的大小并将其 PanelContainer
窗口移动到其位置。
private void ResizePanels()
{
//Get size of the sibling window and main parent window
Rectangle siblingRect = SafeNativeMethods.GetWindowRectange(this.SiblingWindow);
Rectangle parentRect = SafeNativeMethods.GetWindowRectange(this.ParentWindow);
//Calculate position of sibling window in screen coordinates
SafeNativeMethods.POINT topLeft =
new SafeNativeMethods.POINT(siblingRect.Left, siblingRect.Top);
SafeNativeMethods.ScreenToClient(this.ParentWindow, ref topLeft);
//Decrease size of the sibling window
int newWidth = parentRect.Width - topLeft.X - _panelContainer.Width;
SafeNativeMethods.SetWindowPos(this.SiblingWindow, IntPtr.Zero, 0, 0, newWidth,
siblingRect.Height, SafeNativeMethods.SWP_NOMOVE |
SafeNativeMethods.SWP_NOZORDER);
//Move the container to correct position
_panelContainer.Left = topLeft.X + newWidth;
_panelContainer.Top = topLeft.Y;
//Set correct height of the panel container
_panelContainer.Height = siblingRect.Height;
}
我们几乎完成了;但是,我们需要处理另一种情况。ResizePanels
方法会计算 PanelContainer
的正确位置;但是,一旦用户调整 Outlook 窗口的大小,它就不再有效,因为默认情况下,兄弟窗口的宽度会恢复,而 PanelContainer
会保持在原位,这不是我们想要的。不过,解决方案很简单;我们只需要确保在兄弟窗口每次调整大小时都调用 ResizePanels
。
在这种情况下,我们不能使用标准的 Control.SizeChanged
事件,因为兄弟窗口只是一个原生窗口,我们除了它的窗口句柄之外没有其他东西。幸运的是,这就是 NativeWindow
类发挥作用的地方。NativeWindow
类允许我们对任何窗口(无论它是托管的还是非托管的)的窗口过程进行子类化。通过子类化窗口,我们将接收到所有窗口消息到我们自己的 WndProc
方法;因此,通过查找 WM_SIZE
消息,我们可以检测到窗口何时被调整大小。我们将首先让默认的窗口过程处理消息,然后我们再进行自己的处理。我为此创建了一个小的派生类 SubclassedWindow
——它接受一个原生窗口句柄,并且每次窗口接收到 WM_SIZE
消息时都会引发一个 SizeChanged
事件。
sealed class SubclassedWindow : NativeWindow
{
public event EventHandler SizeChanged;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == (int)SafeNativeMethods.WindowsMessages.WM_SIZE)
OnSizeChanged();
}
private void OnSizeChanged()
{
if (SizeChanged != null)
SizeChanged(this, null);
}
}
唯一剩下的就是每次兄弟窗口大小改变时调用 ResizePanels
方法。
//Subclass sibling window to monitor SizeChange event
_subclassedSiblingWindow = new SubclassedWindow();
_subclassedSiblingWindow.AssignHandle(this.SiblingWindow);
_subclassedSiblingWindow.SizeChanged +=
new EventHandler(subclassedSiblingWindow_SizeChanged);
...
private void subclassedSiblingWindow_SizeChanged(object sender, EventArgs e)
{
//Since sibling has changed its size, we need to resize both windows again
ResizePanels();
}
PanelContainer
现在将放置在正确的位置,并且其尺寸将始终匹配可用空间。
收件人面板演示
由于我们已经就位了通用的 PanelContainer
,现在是时候创建一些演示控件并将其嵌套到容器中了。我选择了一个简单但有趣的 Outlook 集成演示——我将会在面板中显示选定电子邮件的主题和收件人。如果用户在面板中点击邮件的收件人,它将打开一个新的浏览器窗口并在 Google 上搜索该人的姓名。所有这些都将通过一个标准的 UserControl
实现,我称之为 MyPanel
。
为了将此控件与 Outlook 连接,我需要捕获 SelectionChange
事件(我们终于为此使用了 Outlook 对象模型),并根据消息网格中选定的电子邮件更新控件。
private void outlookExplorer_SelectionChange()
{
//Take the first selected item
MailItem mailItem = _outlookExplorer.Selection[1] as MailItem;
//Populate the labels
string senderName = mailItem.SenderName;
this.lblSender.Text = String.Format("{0} writes regarding", senderName);
this.lblSubject.Text = mailItem.Subject;
...
}
就是这样——我们完成了!
结语
我希望这个例子向您证明,Office 集成的可能性比 Microsoft 官方提到的要多。
然而,这段代码仍然可以进行一些改进——例如,缺少调整 PanelContainer
宽度的能力;另外,更通用的停靠机制会更好。我希望最终能实现这一点,一旦我需要它……