Can I create an undo transaction in Word or Excel? (VSTO)

匿名 (未验证) 提交于 2019-12-03 01:57:01

问题:

I notice that Project 2007 has the functions that allow operations that can be undone to be placed in a single stack item, or "undo transaction". For example:

Application.OpenUndoTransaction "Create 6 tasks" Dim i As Integer For i = 1 To 6     ActiveProject.Tasks.Add "UndoMe " & i Next Application.CloseUndoTransaction  

What this means is that the user can undo all of the actions in a single undo action, rather than 6 times.

This would be great to implement in Word and/or Excel, as I'm doing some things in VSTO that make multiple changes at once, and it'll be a bit annoying for the user if they have to click on Undo several times if they make a mistake. Although those specific functions don't appear to exist, does anyone know if / how this can be done in some way?

回答1:

You can simulate transactional behavior in Word by overwriting the Undo and Redo command routines in VBA (I don't think that overwriting built-in Word commands is possible using VSTO alone, though). The start of a transaction is marked by adding a bookmark, the end is marked by removing the bookmark.

When calling undo, we check whether the transaction mark bookmark is present and repeat the undo until the marker is gone. Redo is working the same way. This mechanism supports transactional undo/redo of all modifications done to the document content. However, to allow undo/redo of modifications to the document properties a special mechanism needs to be implemented using the SetCustomProp macro. Document properties should not be set directly but via this macro only.

Update: I forgot to clearly mention that this approach only works with the keyboard shortcuts and the menu commands, clicking the toolbar button still does a single-step undo. We therefore decided to replace the toolbar buttons with custom ones. The code has been in use for quite a while With Word 2003 (it's not tested with Word 2007, so be prepared for surprise ;)

Option Explicit  ' string constants for Undo mechanism Public Const BM_IN_MACRO As String = "_InMacro_"  Public Const BM_DOC_PROP_CHANGE As String = "_DocPropChange_" Public Const BM_DOC_PROP_NAME As String = "_DocPropName_" Public Const BM_DOC_PROP_OLD_VALUE As String = "_DocPropOldValue_" Public Const BM_DOC_PROP_NEW_VALUE As String = "_DocPropNewValue_"  '----------------------------------------------------------------------------------- ' Procedure : EditUndo ' Purpose   : Atomic undo of macros '             Note: This macro only catches the menu command and the keyboard shortcut, '                   not the toolbar command '----------------------------------------------------------------------------------- Public Sub EditUndo() ' Catches Ctrl-Z      'On Error Resume Next     Dim bRefresh As Boolean     bRefresh = Application.ScreenUpdating     Application.ScreenUpdating = False      Do         If ActiveDocument.Bookmarks.Exists(BM_DOC_PROP_CHANGE) Then             Dim strPropName As String             Dim strOldValue As String              strPropName = ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Range.Text             strOldValue = ActiveDocument.Bookmarks(BM_DOC_PROP_OLD_VALUE).Range.Text             ActiveDocument.CustomDocumentProperties(strPropName).Value = strOldValue         End If      Loop While (ActiveDocument.Undo = True) _        And ActiveDocument.Bookmarks.Exists(BM_IN_MACRO)      Application.ScreenUpdating = bRefresh End Sub  '----------------------------------------------------------------------------------- ' Procedure : EditRedo ' Purpose   : Atomic redo of macros '             Note: This macro only catches the menu command and the keyboard shortcut, '                   not the toolbar command '----------------------------------------------------------------------------------- Public Sub EditRedo() ' Catches Ctrl-Y      Dim bRefresh As Boolean     bRefresh = Application.ScreenUpdating     Application.ScreenUpdating = False      Do         If ActiveDocument.Bookmarks.Exists(BM_DOC_PROP_CHANGE) Then             Dim strPropName As String             Dim strNewValue As String              strPropName = ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Range.Text             strNewValue = ActiveDocument.Bookmarks(BM_DOC_PROP_NEW_VALUE).Range.Text             ActiveDocument.CustomDocumentProperties(strPropName).Value = strNewValue         End If      Loop While (ActiveDocument.Redo = True) _        And ActiveDocument.Bookmarks.Exists(BM_IN_MACRO)      Application.ScreenUpdating = bRefresh  End Sub  '----------------------------------------------------------------------------------- ' Procedure : SetCustomProp ' Purpose   : Sets a custom document property '----------------------------------------------------------------------------------- Public Function SetCustomProp(oDoc As Document, strName As String, strValue As String)      Dim strOldValue As String      On Error GoTo existsAlready     strOldValue = ""     oDoc.CustomDocumentProperties.Add _         Name:=strName, LinkToContent:=False, Value:=Trim(strValue), _         Type:=msoPropertyTypeString     GoTo exitHere  existsAlready:     strOldValue = oDoc.CustomDocumentProperties(strName).Value     oDoc.CustomDocumentProperties(strName).Value = strValue  exitHere:     ' support undo / redo of changes to the document properties     'On Error Resume Next     Dim bCalledWithoutUndoSupport  As Boolean      If Not ActiveDocument.Bookmarks.Exists(BM_IN_MACRO) Then         ActiveDocument.Range.Bookmarks.Add BM_IN_MACRO, ActiveDocument.Range         bCalledWithoutUndoSupport = True     End If      Dim oRange As Range     Set oRange = ActiveDocument.Range      oRange.Collapse wdCollapseEnd     oRange.Text = " "     oRange.Bookmarks.Add "DocPropDummy_", oRange      oRange.Collapse wdCollapseEnd     oRange.Text = strName     oRange.Bookmarks.Add BM_DOC_PROP_NAME, oRange      oRange.Collapse wdCollapseEnd     oRange.Text = strOldValue     oRange.Bookmarks.Add BM_DOC_PROP_OLD_VALUE, oRange      oRange.Collapse wdCollapseEnd     oRange.Text = strValue     oRange.Bookmarks.Add BM_DOC_PROP_NEW_VALUE, oRange      oRange.Bookmarks.Add BM_DOC_PROP_CHANGE     ActiveDocument.Bookmarks(BM_DOC_PROP_CHANGE).Delete      Set oRange = ActiveDocument.Bookmarks(BM_DOC_PROP_NEW_VALUE).Range     ActiveDocument.Bookmarks(BM_DOC_PROP_NEW_VALUE).Delete     If Len(oRange.Text) > 0 Then oRange.Delete      Set oRange = ActiveDocument.Bookmarks(BM_DOC_PROP_OLD_VALUE).Range     ActiveDocument.Bookmarks(BM_DOC_PROP_OLD_VALUE).Delete     If Len(oRange.Text) > 0 Then oRange.Delete      Set oRange = ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Range     ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Delete     If Len(oRange.Text) > 0 Then oRange.Delete      Set oRange = ActiveDocument.Bookmarks("DocPropDummy_").Range     ActiveDocument.Bookmarks("DocPropDummy_").Delete     If Len(oRange.Text) > 0 Then oRange.Delete      If bCalledWithoutUndoSupport And ActiveDocument.Bookmarks.Exists(BM_IN_MACRO) Then         ActiveDocument.Bookmarks(BM_IN_MACRO).Delete     End If  End Function  '----------------------------------------------------------------------------------- ' Procedure : SampleUsage ' Purpose   : Demonstrates a transaction '----------------------------------------------------------------------------------- Private Sub SampleUsage()      On Error Resume Next      ' mark begin of transaction     ActiveDocument.Range.Bookmarks.Add BM_IN_MACRO      Selection.Text = "Hello World"     ' do other stuff      ' mark end of transaction     ActiveDocument.Bookmarks(BM_IN_MACRO).Delete  End Sub 


回答2:

Word 2010 provides the ability to do this via the Application.UndoRecord object. See http://msdn.microsoft.com/en-us/library/hh128816.aspx



回答3:

I've been chewing on this one for a while. Here's my attempt at using a hidden document, then grabbing the WordOpenXML from the hidden document and placing it in the real document when needed to make any amount of VSTO actions a single undo.

//Usage from ThisDocument VSTO Document level project public partial class ThisDocument {        //Used to buffer writing text & formatting to document (to save undo stack)     public static DocBuffer buffer;      //Attached Template     public static Word.Template template;      private void ThisDocument_Startup(object sender, System.EventArgs e)     {                    //Ignore changes to template (removes prompt to save changes to template)         template = (Word.Template)this.Application.ActiveDocument.get_AttachedTemplate();         template.Saved = true;                      //Document buffer         buffer = new DocBuffer();          //Start buffer         ThisDocument.buffer.Start();          //This becomes one "undo"         Word.Selection curSel = Globals.ThisDocument.Application.Selection;         curSel.TypeText(" ");         curSel.TypeBackspace();         curSel.Font.Bold = 1;         curSel.TypeText("Hello, world!");         curSel.Font.Bold = 0;         curSel.TypeText(" ");          //end buffer, print out text         ThisDocument.buffer.End();     }      void Application_DocumentBeforeClose(Microsoft.Office.Interop.Word.Document Doc, ref bool Cancel)     {         buffer.Close();     }      private void ThisDocument_Shutdown(object sender, System.EventArgs e)     {         buffer.Close();              } } 

Here is the DocBuffer Class:

public class DocBuffer {     //Word API Objects     Word._Document HiddenDoc;     Word.Selection curSel;     Word.Template template;      //ref parameters     object missing = System.Type.Missing;     object FalseObj = false; //flip this for docbuffer troubleshooting     object templateObj;      //Is docbuffer running?     public Boolean started{ get; private set; }      //Open document on new object     public DocBuffer()     {         //Clear out unused buffer bookmarks         Word.Bookmarks bookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;         bookmarks.ShowHidden = true;          foreach (Word.Bookmark mark in bookmarks)         {             if (mark.Name.Contains("_buf"))             {                 mark.Delete();             }         }          //Remove trail of undo's for clearing out the bookmarks         Globals.ThisDocument.UndoClear();          //Set up template         template = ThisDocument.template;         templateObj = template;          //Open Blank document, then attach styles *and update         HiddenDoc = Globals.ThisDocument.Application.Documents.Add(ref missing, ref missing, ref missing, ref FalseObj);         HiddenDoc.set_AttachedTemplate(ref templateObj);         HiddenDoc.UpdateStyles();          //Tell hidden document it has been saved to remove rare prompt to save document         HiddenDoc.Saved = true;          //Make primary document active         Globals.ThisDocument.Activate();      }      ~DocBuffer()     {         try         {             HiddenDoc.Close(ref FalseObj, ref missing, ref missing);         }         catch { }     }      public void Close()     {         try         {             HiddenDoc.Close(ref FalseObj, ref missing, ref missing);         }         catch { }     }      public void Start()     {         try         {             //Make hidden document active to receive selection             HiddenDoc.Activate(); //results in a slight application focus loss         }         catch (System.Runtime.InteropServices.COMException ex)         {             if (ex.Message == "Object has been deleted.")             {                 //Open Blank document, then attach styles *and update                 HiddenDoc = Globals.ThisDocument.Application.Documents.Add(ref missing, ref missing, ref missing, ref FalseObj);                 HiddenDoc.set_AttachedTemplate(ref templateObj);                 HiddenDoc.UpdateStyles();                 HiddenDoc.Activate();             }             else                 throw;         }          //Remove Continue Bookmark, if exists         Word.Bookmarks hiddenDocBookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;         if (hiddenDocBookmarks.Exists("Continue"))         {             object deleteMarkObj = "Continue";             Word.Bookmark deleteMark = hiddenDocBookmarks.get_Item(ref deleteMarkObj);             deleteMark.Select();             deleteMark.Delete();         }          //Tell hidden document it has been saved to remove rare prompt to save document         HiddenDoc.Saved = true;          //Keep track when started         started = true;     }      //Used for non-modal dialogs to bring active document back up between text insertion     public void Continue()     {         //Exit quietly if buffer hasn't started         if (!started) return;          //Verify hidden document is active         if ((HiddenDoc as Word.Document) != Globals.ThisDocument.Application.ActiveDocument)         {             HiddenDoc.Activate();         }          //Hidden doc selection         curSel = Globals.ThisDocument.Application.Selection;          //Hidden doc range         Word.Range bufDocRange;          //Select entire doc, save range         curSel.WholeStory();         bufDocRange = curSel.Range;          //Find end, put a bookmark there         bufDocRange.SetRange(curSel.End, curSel.End);         object bookmarkObj = bufDocRange;          //Generate "Continue" hidden bookmark         Word.Bookmark mark = Globals.ThisDocument.Application.ActiveDocument.Bookmarks.Add("Continue", ref bookmarkObj);         mark.Select();          //Tell hidden document it has been saved to remove rare prompt to save document         HiddenDoc.Saved = true;          //Make primary document active         Globals.ThisDocument.Activate();     }      public void End()     {         //Exit quietly if buffer hasn't started         if (!started) return;          //Turn off buffer started flag         started = false;          //Verify hidden document is active         if ((HiddenDoc as Word.Document) != Globals.ThisDocument.Application.ActiveDocument)         {             HiddenDoc.Activate();         }          //Remove Continue Bookmark, if exists         Word.Bookmarks hiddenDocBookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;         hiddenDocBookmarks.ShowHidden = true;         if (hiddenDocBookmarks.Exists("Continue"))         {             object deleteMarkObj = "Continue";             Word.Bookmark deleteMark = hiddenDocBookmarks.get_Item(ref deleteMarkObj);             deleteMark.Delete();         }          //Hidden doc selection         curSel = Globals.ThisDocument.Application.Selection;          //Hidden doc range         Word.Range hiddenDocRange;         Word.Range bufDocRange;          //Select entire doc, save range         curSel.WholeStory();         bufDocRange = curSel.Range;          //If cursor bookmark placed in, move there, else find end of text, put a bookmark there         Boolean cursorFound = false;         if (hiddenDocBookmarks.Exists("_cursor"))         {             object cursorBookmarkObj = "_cursor";             Word.Bookmark cursorBookmark = hiddenDocBookmarks.get_Item(ref cursorBookmarkObj);             bufDocRange.SetRange(cursorBookmark.Range.End, cursorBookmark.Range.End);             cursorBookmark.Delete();             cursorFound = true;         }         else         {             //The -2 is done because [range object].WordOpenXML likes to drop bookmarks at the end of the range             bufDocRange.SetRange(curSel.End - 2, curSel.End - 2);         }          object bookmarkObj = bufDocRange;          //Generate GUID for hidden bookmark         System.Guid guid = System.Guid.NewGuid();         String id = "_buf" + guid.ToString().Replace("-", string.Empty);         Word.Bookmark mark = Globals.ThisDocument.Application.ActiveDocument.Bookmarks.Add(id, ref bookmarkObj);          //Get OpenXML Text (Text with formatting)         curSel.WholeStory();         hiddenDocRange = curSel.Range;         string XMLText = hiddenDocRange.WordOpenXML;          //Clear out contents of buffer         hiddenDocRange.Delete(ref missing, ref missing); //comment this for docbuffer troubleshooting          //Tell hidden document it has been saved to remove rare prompt to save document         HiddenDoc.Saved = true;          //Make primary document active         Globals.ThisDocument.Activate();          //Get selection from new active document         curSel = Globals.ThisDocument.Application.Selection;          //insert buffered formatted text into main document         curSel.InsertXML(XMLText, ref missing);          //Place cursor at bookmark+1 (this is done due to WordOpenXML ignoring bookmarks at the end of the selection)         Word.Bookmarks bookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;         bookmarks.ShowHidden = true;          object stringObj = id;         Word.Bookmark get_mark = bookmarks.get_Item(ref stringObj);         bufDocRange = get_mark.Range;          if (cursorFound) //Canned language actively placed cursor             bufDocRange.SetRange(get_mark.Range.End, get_mark.Range.End);         else //default cursor at the end of text             bufDocRange.SetRange(get_mark.Range.End + 1, get_mark.Range.End + 1);         bufDocRange.Select(); } 


回答4:

Excel has some (limited) built-in support for undo and redo as part of its VBA architecture.

I'm not familiar with vsto, so I don't know if this will help you out, but you can take a look at this SO question for more details.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!