Office中国论坛/Access中国论坛

标题: 【示例】WORD下实现多窗口TASKPAN [打印本页]

作者: faunus    时间: 2014-3-22 16:08
标题: 【示例】WORD下实现多窗口TASKPAN
  示例代码包含一个应用程序WordDocEditTimer,它维护着Word文档的一个编辑次数列表。本节将详细解释这个应用程序的代码,因为该应用程序演示了前面介绍的所有内容,还包含一些有益的提示。
  这个应用程序的一般操作是只要创建或加载了文档,就启动一个链接到文档名称上的计时器。如果关闭文档,该文档的计时器就暂停。如果打开了以前计时的文档,计时器就恢复。另外,如果使用Save As把文档保存为另一个文件名,计时器就更新为使用新文件名。
这个应用程序是一个Word应用程序级的插件,使用一个定制任务面板和一个ribbon菜单。
ribbon菜单包含一个按钮和一个复选框,按钮用于开关任务面板,复选框用于暂停当前活动的文档的计时器。包含这些控件的组添加到Home ribbon选项卡的最后。任务面板显示一组活动的计时器。

效果如下:
[attach]53641[/attach]



作者: faunus    时间: 2014-3-22 16:10
计时器通过DocumentTimer类来维护:
  1. public class DocumentTimer
  2. {
  3. public Word.Document Document { get; set; }
  4. public DateTime LastActive { get; set; }
  5. public bool IsActive { get; set; }
  6. public TimeSpan EditTime { get; set; }
  7. }
复制代码
  这段代码保存了对Microsoft.Office.Interop.Word. Document对象的一个引用、总编辑时间、计时器是否激活,以及它上一次激活的时间。ThisAddIn类维护这些对象的一个集合,这些对象与文档名关联起来:
  1. public partial class ThisAddIn
  2. {
  3. private Dictionary < string, DocumentTimer > documentEditTimes;
复制代码




作者: faunus    时间: 2014-3-22 16:17
  因此,每个计时器都可以通过文档引用或文档名来定位。这是必要的,因为文档引用可以跟踪文档名的变化(这里没有可用于监控文档名变化的事件),文档名允许跟踪关闭、再次打开的文档。
  ThisAddIn类还维护一个CustomTaskPane对象列表(如前所述,Word中的每个窗口都需要一个CustomTaskPane对象):
  1. private List < Tools.CustomTaskPane > timerDisplayPanes;
复制代码
  插件启动时,ThisAddIn_Startup()方法执行了几个任务。首先它初始化两个集合:
  1. private void ThisAddIn_Startup(object sender, System.EventArgs e)
  2. {
  3. // Initialize timers and display panels
  4. documentEditTimes = new Dictionary < string, DocumentTimer > ();
  5. timerDisplayPanes = new
  6. List < Microsoft.Office.Tools.CustomTaskPane > ();
复制代码






作者: faunus    时间: 2014-3-22 16:18
  接着通过ApplicationEvents4_Event接口添加几个事件处理程序:
  1. // Add event handlers
  2. Word.ApplicationEvents4_Event eventInterface = this.Application;
  3. eventInterface.DocumentOpen += new Microsoft.Office.Interop.Word
  4. .ApplicationEvents4_DocumentOpenEventHandler(
  5. eventInterface_DocumentOpen);
  6. eventInterface.NewDocument += new Microsoft.Office.Interop.Word
  7. .ApplicationEvents4_NewDocumentEventHandler(
  8. eventInterface_NewDocument);
  9. eventInterface.DocumentBeforeClose += new Microsoft.Office.Interop.Word
  10. .ApplicationEvents4_DocumentBeforeCloseEventHandler(
  11. eventInterface_DocumentBeforeClose);
  12. eventInterface.WindowActivate += new Microsoft.Office.Interop.Word
  13. .ApplicationEvents4_WindowActivateEventHandler(
  14. eventInterface_WindowActivate);
复制代码




作者: faunus    时间: 2014-3-22 16:18
  这些事件处理程序用于监控文档的打开、创建和关闭,并确保ribbon上的Pause复选框保持最新状态。后一个功能是使用WindowsActivate事件跟踪窗口的激活状态来实现的。
  在这个事件处理程序中,最后一个任务是开始监控当前文档,把定制的任务面板添加到包含文档的窗口中:
  1. // Start monitoring active document
  2. MonitorDocument(this.Application.ActiveDocument);
  3. AddTaskPaneToWindow(this.Application.ActiveDocument.ActiveWindow);
  4. }
复制代码





作者: faunus    时间: 2014-3-22 16:19
  MonitorDocument()实用方法为文档添加一个计时器:
  1. internal void MonitorDocument(Word.Document Doc)
  2. {
  3. // Monitor doc
  4. documentEditTimes.Add(Doc.Name, new DocumentTimer
  5. {
  6. Document = Doc,
  7. EditTime = new TimeSpan(0),
  8. IsActive = true,
  9. LastActive = DateTime.Now
  10. });
  11. }
复制代码




作者: faunus    时间: 2014-3-22 16:19
  这个方法仅为文档创建了一个新的DocumentTimer对象。DocumentTimer引用文档,其编辑次数是0,且是在当前时间激活的。接着把这个计时器添加到documentEditTimes集合中,并关联到文档名中。
  AddTaskPaneToWindow()方法把定制任务面板添加到窗口中。这个方法首先检查已有的任务面板,确保窗口中还没有任务面板。Word中的另一个古怪的特性是如果在加载应用程序后,立即打开一个旧文档,默认的Document1文档就会消失,且不触发关闭事件。在访问包含任务面板的文档窗口时,这可能导致异常,所以该方法还检查表示是否出现该异常的ArgumentNullException:
  1. private void AddTaskPaneToWindow(Word.Window Wn)
  2. {
  3. // Check for task pane in window
  4. Tools.CustomTaskPane docPane = null;
  5. Tools.CustomTaskPane paneToRemove = null;
  6. foreach (Tools.CustomTaskPane pane in timerDisplayPanes)
  7. {
  8. try
  9. {
  10. if (pane.Window == Wn)
  11. {
  12. docPane = pane;
  13. break;
  14. }
  15. }
  16. catch (ArgumentNullException)
  17. {
  18. // pane.Window is null, so document1 has been unloaded.
  19. paneToRemove = pane;
  20. }
  21. }
复制代码





作者: faunus    时间: 2014-3-22 16:20
  如果抛出了一个异常,就从集合中删除错误的任务面板:
  1. // Remove pane if necessary
  2. timerDisplayPanes.Remove(paneToRemove);
复制代码

  如果窗口中没有任务面板,这个方法就添加一个:
  1. // Add task pane to doc
  2. if (docPane == null)
  3. {
  4. Tools.CustomTaskPane pane = this.CustomTaskPanes.Add(
  5. new TimerDisplayPane(documentEditTimes),
  6. "Document Edit Timer",
  7. Wn);
  8. timerDisplayPanes.Add(pane);
  9. pane.VisibleChanged +=
  10. new EventHandler(timerDisplayPane_VisibleChanged);
  11. }
  12. }
复制代码




作者: faunus    时间: 2014-3-22 16:21
  添加的任务面板是TimerDisplayPane类的一个实例。稍后介绍这个类。它添加时使用的名称是Document Edit Timer。另外,在调用CustomTaskPanes.Add()方法后,还为得到的CustomTaskPane的VisibleChanged事件添加了一个处理程序,这样在第一次显示任务面板时,可以刷新显示:
  1. private void timerDisplayPane_VisibleChanged(object sender, EventArgs e)
  2. {
  3. // Get task pane and toggle visibility
  4. Tools.CustomTaskPane taskPane = (Tools.CustomTaskPane)sender;
  5. if (taskPane.Visible)
  6. {
  7. TimerDisplayPane timerControl = (TimerDisplayPane)taskPane.Control;
  8. timerControl.RefreshDisplay();
  9. }
  10. }
复制代码




作者: faunus    时间: 2014-3-22 16:21
  TimerDisplayPane类有一个RefreshDisplay()方法,它在上面的代码中调用。这个方法刷新timerControl对象的显示。
  接着的代码确保监控所有的文档。首先创建新文档时,调用eventInterface_New- Document()事件处理程序,调用MonitorDocument()和前面介绍过的AddTaskPaneTo- Window()方法监控文档。
  1. private void eventInterface_NewDocument(Word.Document Doc)
  2. {
  3. // Monitor new doc
  4. MonitorDocument(Doc);
  5. AddTaskPaneToWindow(Doc.ActiveWindow);
复制代码





作者: faunus    时间: 2014-3-22 16:22
  新文档在计时器运行时启动,此时这个方法还清除了ribbon菜单中的Pause复选框。这是通过一个实用方法SetPauseStatus()实现的,该方法在ribbon中定义:
  1. // Set checkbox
  2. Globals.Ribbons.TimerRibbon.SetPauseStatus(false);
  3. }
复制代码




作者: faunus    时间: 2014-3-22 16:22
  关闭文档之前,调用eventInterface_DocumentBeforeClose()事件处理程序。这个方法冻结了文档的计时器,更新了总编辑时间,清除了Document引用,删除了文档窗口中的任务面板(使用稍后介绍的RemoveTaskPaneFromWindow()方法),之后关闭窗口。
  1. private void eventInterface_DocumentBeforeClose(Word.Document Doc,
  2. ref bool Cancel)
  3. {
  4. // Freeze timer
  5. documentEditTimes[Doc.Name].EditTime += DateTime.Now
  6. - documentEditTimes[Doc.Name].LastActive;
  7. documentEditTimes[Doc.Name].IsActive = false;
  8. documentEditTimes[Doc.Name].Document = null;
  9. // Remove task pane
  10. RemoveTaskPaneFromWindow(Doc.ActiveWindow);
  11. }
复制代码




作者: faunus    时间: 2014-3-22 16:23
  打开文档时,调用eventInterface_DocumentOpen()方法。该方法完成了许多工作,因为在监控文档之前,这个方法必须查看计时器的名称,确定文档是否已有计时器:
  1. private void eventInterface_DocumentOpen(Word.Document Doc)
  2. {
  3. if (documentEditTimes.ContainsKey(Doc.Name))
  4. {
  5. // Monitor old doc
  6. documentEditTimes[Doc.Name].LastActive = DateTime.Now;
  7. documentEditTimes[Doc.Name].IsActive = true;
  8. documentEditTimes[Doc.Name].Document = Doc;
  9. AddTaskPaneToWindow(Doc.ActiveWindow);
  10. }
复制代码




作者: faunus    时间: 2014-3-22 16:23
  如果还没有监控文档,就为文档配置一个新监控器:
  1. else
  2. {
  3. // Monitor new doc
  4. MonitorDocument(Doc);
  5. AddTaskPaneToWindow(Doc.ActiveWindow);
  6. }
  7. }
复制代码

作者: faunus    时间: 2014-3-22 16:24
  RemoveTaskPaneFromWindow()方法用于从窗口中删除任务面板。其代码首先检查特定的窗口中是否有任务面板:
  1. private void RemoveTaskPaneFromWindow(Word.Window Wn)
  2. {
  3. // Check for task pane in window
  4. Tools.CustomTaskPane docPane = null;
  5. foreach (Tools.CustomTaskPane pane in timerDisplayPanes)
  6. {
  7. if (pane.Window == Wn)
  8. {
  9. docPane = pane;
  10. break;
  11. }
  12. }
复制代码

作者: faunus    时间: 2014-3-22 16:24
  如果找到了任务面板,就调用CustomTaskPanes.Remove()方法删除它。还要从任务面板引用的本地集合中删除它。
  1. // Remove document task pane
  2. if (docPane != null)
  3. {
  4. this.CustomTaskPanes.Remove(docPane);
  5. timerDisplayPanes.Remove(docPane);
  6. }
  7. }
复制代码

作者: faunus    时间: 2014-3-22 16:24
  这个类中的最后一个事件处理程序是eventInterface_WindowActivate(),在激活窗口时调用它。这个方法获得活动文档的计时器,选中ribbon菜单中的复选框,以更新文档的复选框:
  1. private void eventInterface_WindowActivate(Word.Document Doc,
  2. Word.Window Wn)
  3. {
  4. // Ensure pause checkbox in ribbon is accurate, start by getting timer
  5. DocumentTimer documentTimer =
  6. documentEditTimes[this.Application.ActiveDocument.Name];
  7. // Set checkbox
  8. Globals.Ribbons.TimerRibbon.SetPauseStatus(!documentTimer.IsActive);
  9. }
复制代码

作者: faunus    时间: 2014-3-22 16:25
  ThisAddIn的代码还包含两个实用方法。第一个方法ToggleTaskPaneDisplay()用于设置CustomTaskPanes.Visible属性,为当前活动的文档显示或隐藏任务面板。
  1. internal void ToggleTaskPaneDisplay()
  2. {
  3. // Ensure window has task window
  4. AddTaskPaneToWindow(this.Application.ActiveDocument.ActiveWindow);
  5. // toggle document task pane
  6. Tools.CustomTaskPane docPane = null;
  7. foreach (Tools.CustomTaskPane pane in timerDisplayPanes)
  8. {
  9. if (pane.Window == this.Application.ActiveDocument.ActiveWindow)
  10. {
  11. docPane = pane;
  12. break;
  13. }
  14. }
  15. docPane.Visible = !docPane.Visible;
  16. }
复制代码

作者: faunus    时间: 2014-3-22 16:25
  上述代码中的ToggleTaskPaneDisplay()方法由ribbon控件上的事件处理程序调用,如后面所述。
  最后,该类有另一个从ribbon菜单中调用的方法,它允许ribbon控件暂停或恢复文档的计时器:
  1. internal void PauseOrResumeTimer(bool pause)
  2. {
  3. // Get timer
  4. DocumentTimer documentTimer =
  5. documentEditTimes[this.Application.ActiveDocument.Name];
  6. if (pause & & documentTimer.IsActive)
  7. {
  8. // Freeze timer
  9. documentTimer.EditTime += DateTime.Now - documentTimer.LastActive;
  10. documentTimer.IsActive = false;
  11. }
  12. else if (!pause & & !documentTimer.IsActive)
  13. {
  14. // Resume timer
  15. documentTimer.IsActive = true;
  16. documentTimer.LastActive = DateTime.Now;
  17. }
  18. }
  19. }
复制代码

作者: faunus    时间: 2014-3-22 16:26
  这个类定义中的其他代码是Shutdown的空事件处理程序以及VSTO为关联Startup和Shutdown事件处理程序而生成的代码。
  接着布置项目中的ribbon,即TimerRibbon,如图所示。

作者: faunus    时间: 2014-3-22 16:26
  这个ribbon包含一个RibbonButton、一个RibbonSeparator、一个RibbonCheckBox和一个DialogBoxLauncher。按钮使用大显示样式,其OfficeImageId设置为StartAfterPrevious,显示如图40-13所示的钟表图像。(这些图像在设计期间不可见)。ribbon使用TabHome选项卡类型,其内容追加到Home选项卡上。
  ribbon有3个事件处理程序,每个处理程序都调用前面介绍的ThisAddIn中的一个实用方法:
  1. private void group1_DialogLauncherClick(object sender,
  2. RibbonControlEventArgs e)
  3. {
  4. // Show or hide task pane
  5. Globals.ThisAddIn.ToggleTaskPaneDisplay();
  6. }
  7. private void pauseCheckBox_Click(object sender, RibbonControlEventArgs e)
  8. {
  9. // Pause timer
  10. Globals.ThisAddIn.PauseOrResumeTimer(pauseCheckBox.Checked);
  11. }
  12. private void toggleDisplayButton_Click(object sender,
  13. RibbonControlEventArgs e)
  14. {
  15. // Show or hide task pane
  16. Globals.ThisAddIn.ToggleTaskPaneDisplay();
  17. }
复制代码

作者: faunus    时间: 2014-3-22 16:26
  ribbon还包含自己的实用方法SetPauseStatus(),如前所述,该方法由ThisAddIn中的代码调用,以选中复选框或取消复选框的选中。
  1. internal void SetPauseStatus(bool isPaused)
  2. {
  3. // Ensure checkbox is accurate
  4. pauseCheckBox.Checked = isPaused;
  5. }
复制代码

作者: faunus    时间: 2014-3-22 17:30
这个解决方案中的另一个组件是任务面板中使用的TimerDisplayPane用户控件,这个控件的布局如图
作者: faunus    时间: 2014-3-22 17:30
这个控件包含一个按钮、一个标签和一个列表框--这些都是很普通的显示控件,也可以用更漂亮的WPF控件替代它们。
该控件的代码保存了对文档计时器的一个本地引用,该引用在构造函数中设置:
  1. public partial class TimerDisplayPane : UserControl
  2. {
  3. private Dictionary < string, DocumentTimer > documentEditTimes;
  4. public TimerDisplayPane()
  5. {
  6. InitializeComponent();
  7. }
  8. public TimerDisplayPane(Dictionary < string, DocumentTimer >
  9. documentEditTimes) : this()
  10. {
  11. // Store reference to edit times
  12. this.documentEditTimes = documentEditTimes;
  13. }
复制代码





作者: faunus    时间: 2014-3-22 17:30
按钮事件处理程序调用RefreshDisplay()方法刷新计时器的显示:
  1. private void refreshButton_Click(object sender, EventArgs e)
  2. {
  3. RefreshDisplay();
  4. }
复制代码

作者: faunus    时间: 2014-3-22 17:31
RefreshDisplay()方法也从ThisAddIn中调用,如前所述。考虑到该方法的任务,这是一个相当复杂的方法,它还检查被监控文档的列表,与已加载文档的列表比较,并解决出现的问题。这段代码在VSTO应用程序中常常是必不可少的,因为COM Office对象模型的接口偶尔不能像期望的那样工作。这里的规则是防御式编码。
该方法首先清除timerList列表框中的当前计时器列表:
  1. internal void RefreshDisplay()
  2. {
  3. // Clear existing list
  4. this.timerList.Items.Clear();
复制代码

作者: faunus    时间: 2014-3-22 17:31
接着检查监控器。这个方法迭代Globals.ThisAddIn.Application.Documents集合中的每个文档,确定文档是被监控、未被监控、或被监控了但在上次刷新时改变了文件名。
要找出被监控的文档,只需比较当前的文档名和键的documentEditTimes集合中的文档名:
  1. // Ensure all docs are monitored
  2. foreach (Word.Document doc in Globals.ThisAddIn.Application.Documents)
  3. {
  4. bool isMonitored = false;
  5. bool requiresNameChange = false;
  6. DocumentTimer oldNameTimer = null;
  7. string oldName = null;
  8. foreach (string documentName in documentEditTimes.Keys)
  9. {
  10. if (doc.Name == documentName)
  11. {
  12. isMonitored = true;
  13. break;
  14. }
复制代码

作者: faunus    时间: 2014-3-22 17:31
如果文档名不匹配,就比较文档引用,以检测对文档名的修改,如下面的代码所示:
  1. else
  2. {
  3. if (documentEditTimes[documentName].Document == doc)
  4. {
  5. // Monitored, but name changed!
  6. oldName = documentName;
  7. oldNameTimer = documentEditTimes[documentName];
  8. isMonitored = true;
  9. requiresNameChange = true;
  10. break;
  11. }
  12. }
  13. }
复制代码

作者: faunus    时间: 2014-3-22 17:32
对于未监控的文档,需要创建一个新的监控器:
  1. // Add monitor if not monitored
  2. if (!isMonitored)
  3. {
  4. Globals.ThisAddIn.MonitorDocument(doc);
  5. }
  6. 名称改变的文档需要通过用于旧文档的监控器重新关联起来:
  7. // Rename if necessary
  8. if (requiresNameChange)
  9. {
  10. documentEditTimes.Remove(oldName);
  11. documentEditTimes.Add(doc.Name, oldNameTimer);
  12. }
  13. }
复制代码

作者: faunus    时间: 2014-3-22 17:32
调整了文档编辑计时器后,生成一个列表。代码还会检测引用的文档是否加载了,对于没有加载的文档,把IsActive属性设置为false,暂停该文档的计时器。这也是防御性编程方式:
  1. // Create new list
  2. foreach (string documentName in documentEditTimes.Keys)
  3. {
  4. // Check to see if doc is still loaded
  5. bool isLoaded = false;
  6. foreach (Word.Document doc in
  7. Globals.ThisAddIn.Application.Documents)
  8. {
  9. if (doc.Name == documentName)
  10. {
  11. isLoaded = true;
  12. break;
  13. }
  14. }
  15. if (!isLoaded)
  16. {
  17. documentEditTimes[documentName].IsActive = false;
  18. documentEditTimes[documentName].Document = null;
  19. }
复制代码

作者: faunus    时间: 2014-3-22 17:32
对于每个监控器,把一个列表项添加到列表框中,其中包含了文档名和总编辑时间:
  1. // Add item
  2. this.timerList.Items.Add(string.Format("{0}: {1}", documentName,
  3. documentEditTimes[documentName].EditTime +
  4. (documentEditTimes[documentName].IsActive ?
  5. (DateTime.Now - documentEditTimes[documentName].LastActive) :
  6. new TimeSpan(0))));
  7. }
  8. }
  9. }
复制代码

作者: faunus    时间: 2014-3-22 17:33
这就完成了这个例子中的代码。这个例子说明了如何使用ribbon和任务面板控件,如何维护多个Word文档中的任务面板。
作者: Amas    时间: 2014-3-22 22:32
[attach]53642[/attach]


自己试了试,基本成功。
环境:Win8.1 + VS2013 + Office2013
可能是代码版本问题,我在VS2013稍许修改了几处,基本能运行。因水平有限,只有理解到这个程度,所以出现莫名其妙错误时不要骂我哟。


作者: faunus    时间: 2014-3-23 16:22
Amas 发表于 2014-3-22 22:32
自己试了试,基本成功。
环境:Win8.1 + VS2013 + Office2013
可能是代码版本问题,我在VS2013稍许 ...

做得非常之好,写个教程吧..




欢迎光临 Office中国论坛/Access中国论坛 (http://www.office-cn.net/) Powered by Discuz! X3.3