自动保存和崩溃恢复






4.70/5 (7投票s)
2000年2月20日

151175
如何在应用程序中实现自动保存和自动恢复功能。
引言
如何自动保存文件,然后在程序崩溃后恢复这些文件,这是一个很少被讨论的问题,但它能增强程序的强大功能和灵活性。本文将描述如何实现一个类似于MS Office使用的自动保存和自动恢复方法。
工作原理
自动保存非常简单。在经过一段特定时间后,您的程序必须将加载的文档序列化保存到磁盘上的特定位置。这些序列化操作不能覆盖用户当前正在使用的文件,因为这会剥夺用户在退出程序时选择是否保存文件的权利。每次程序加载时,都必须搜索自动保存的文件并按需恢复它们。
与Office一样,此实现会将这些临时文件保存在Windows目录下的一个临时目录中,并在加载时搜索该目录以查找自动保存的文件。如果找到任何文件,它将开始恢复过程。
指定自动保存目录
首先,您必须选择要保存文件的位置。此目录必须保持不变,否则您将不得不搜索整个硬盘来查找自动保存的文件……这显然是不可能的。出于实际目的,您的程序应将其保存在硬盘上最稳定的目录中的一个文件夹内,即Windows目录。此目录应存储在环境变量WINDIR
中。如果由于某种原因,用户的系统不包含WINDIR
环境变量,或者环境变量设置不正确,您的程序需要检测到这一点并选择另一个目录。您可以使用CFileFind
类来搜索它,从而轻松检查存储的目录是否存在。以下代码将执行此操作(gm_autosave
目录应为字符串,可以是全局的,如本例所示,也可以通过应用程序的成员函数访问)。最好在应用程序的InitInstance
函数中执行此操作。
::GetEnvironmentVariable("WINDIR",buffer,512); CFileFind CFF; if(CFF.FindFile(buffer,0)) { base=buffer; } else base = "C:\\TEMP"; gm_autosaveDirectory = base+"\\TEMP";
另外,您还可以使用GetTempPath(DWORD nBufferLength, LPTSTR lpBuffer);
来检索临时路径。但是,上述实现允许您指定一个特定目录来存放文件,例如“Temporary Autosaved Files”。
自动保存文件
现在已经确定了路径,您的程序将知道在哪里保存文件。因此,接下来的部分是在应用程序中实现保存例程。为避免占用过多的系统资源,请在主框架窗口中使用OnTimer
函数。在初始化时,程序需要使用适当的值调用SetTimer(...)
函数。SetTimer(0,m_autosavetime * 60 * 1000,NULL)
将为您安装计时器(假设m_autosavetime
的单位是分钟)。您的主框架的OnTimer()
函数将在计时器到期时被调用。在此函数中,您需要向应用程序中的所有视图发送自动保存消息。使用EnumChildWindows
来为每个子窗口调用一个过程。
::EnumChildWindows(m_hWnd,AutosaveTimerChildProc,NULL);
AutosaveTimerChildProc
需要验证给定的子窗口是否是文档的视图,然后再将消息发送到窗口。将传递给过程的指针进行DYNAMIC_DOWNCAST
以验证它是否是视图,然后使用PostMessage
通知您的视图更改。
BOOL CALLBACK AutosaveTimerChildProc( HWND hwnd, LPARAM lParam) { if(DYNAMIC_DOWNCAST(CMyView,CWnd::FromHandle(hwnd)) != NULL) { ::PostMessage(hwnd,WM_MYVIEW_AUTOSAVETIMER,0,0); } return(TRUE); }
注意:WM_MYVIEW_AUTOSAVETIMER
应定义为**应用程序消息**,它基于WM_APP
。
现在,您的视图必须处理该消息并以可恢复的方式保存文件。添加如下消息映射:
ON_MESSAGE(WM_MYVIEW_AUTOSAVETIMER,OnAutosaveTimer)
并在视图的类定义中将该函数声明为接受标准WPARAM
和LPARAM
消息的void函数。这个函数可能会有点棘手。问题在于,如果用户将文件保存在另一个位置,那么该文件的恢复路径也会改变。由于自动保存的名称基于实际文件名(以防用户在某些奇怪的情况下需要手动访问文件),并且您不希望有多个自动保存文件的副本,因此您的程序必须始终存储上次自动保存备份的名称,并在写入文件副本之前删除自动保存的备份。此实现使用CString
的vector
来存储备份文件名(此vector将包含备份名称或为空)。此外,您的程序必须确保在保存到自动保存目录之前已创建该目录!如果在过程中发生错误,如何恢复或处理该错误由您决定。
void CMyView::OnAutosaveTimer(WPARAM w, LPARAM l) { CFileFind CFF; if(CFF.FindFile(gm_autosaveDirectory.GetBuffer(1))==FALSE) { if(CreateDirectory(gm_autosaveDirectory.GetBuffer(1), NULL) ==0) //create directory { //an error has occured, process the error here } } // document name and your autosave extension CString fname = (gm_autosaveDirectory + "\\"+GetDocument()->GetTitle()+".MBK"); if(m_autosave_names.size() > 0) //delete old file { if(CFF.FindFile((*m_autosave_names.begin()))==TRUE) { if(::DeleteFile(((*m_autosave_names.begin()))) == 0) { //an error has occured, process the error here if you want //however, if the file simply does not exist, //that is fine, ignore the error and continue } } // remove the old filename from the vector m_autosave_names.erase(m_autosave_names.begin()); } m_autosave_names.push_back(fname); //add the new filename to the vector //call store function GetDocument()->StoreAutoRecoveryInformation(fname); }
StoreAutoRecoveryInformation
函数依赖于具体实现。一个简单的实现可以简单地将文档的真实路径序列化到存档中,然后调用Serialize
函数来保存路径后的文件。对于我们的例子,这将足够了。
CMyDoc::StoreAutoRecoveryInformation(CString path) { CFile f; if(f.Open(path,CFile::modeWrite | CFile::modeCreate)!=0) { CArchive ar(f,CArchive::store); ar.WriteString(path); try { Serialize(ar); } catch(CException *e) { //write error occured, process here } ar.Close(); f.Close(); } else { //open error occured, process here } }
自动保存恢复
与其为恢复的文件编写全新的文档和视图类,不如直接修改文档的Serialize(...)
函数,以便在序列化时检查文件扩展名。如果扩展名是自动保存文件类型的扩展名,则执行相应的操作。
//helper function to get file extension from a given path CString MakeExt(CString fname) { for(int i = fname.GetLength()-1; i >0; i--) { if(fname.GetAt(i)=='.') return fname.Mid(i); if(fname.GetAt(i)=="\\") break; } return ""; } . . . CMyDoc::Serialize(CArchive ar) { CString ext=MakeExt(ar.GetFile()->GetFileName()); ext.MakeLower(); if(ext == ".mbk" && ar.IsLoading()) { CString s; //read path to restore path member variable m_restorepath = ar.ReadString(s) // read old path to member variable m_oldpath = ar.GetFile()->GetFileName(); CMyBaseDocument::Serialize(); //call the base document's serialize function, // or perform your default serialization here } // put your default serialization code here else CMyBaseDocument::Serialize(); }
由于MFC文档序列化代码的工作方式,文档的视图必须负责将文档的路径更改为正确的位置。(默认情况下,路径将指向自动恢复的副本,但您希望恢复原始路径,以便用户尽可能轻松地继续他们上次中断的地方。)在视图的OnInitialUpdate
函数中,检查文档的恢复路径是否已设置。如果已设置,则您知道程序刚刚加载了一个自动恢复的文件,并且文档和视图的路径和标题必须更改为正确的位置。
CMyDoc *doc=GetDocument(); if(doc->m_restorepath.GetLength() > 0) { doc->SetPathName(doc->m_restorepath,TRUE); doc->SetModifiedFlag(TRUE); doc->UpdateAllViews(NULL); m_autosave_names.push_back(doc->m_oldpath); }
每次加载程序时,程序都必须自动恢复文件。这是通过在InitInstance
函数中搜索自动保存目录中的所有自动保存文件并按需加载来完成的。如果您允许多个程序实例,请确保当前实例是恢复自动保存文件的唯一实例。
BOOL bFound = FALSE; HANDLE hMutexOneInstance = NULL; #ifdef _WIN32 hMutexOneInstance = CreateMutex(NULL,TRUE,_T("PreventSecondInstanceMutex")); if(GetLastError() == ERROR_ALREADY_EXISTS) bFound = TRUE; #else if(m_hPrevInstance != NULL) bFound = TRUE; #endif #ifdef _WIN32 if(hMutexOneInstance) ReleaseMutex(hMutexOneInstance); #endif . . . if(bFound) { if(CFF.FindFile((gm_autosaveDirectory+"\\"+ "*.MBK").GetBuffer(1),0)==TRUE) { if(::MessageBox(NULL, "Autosaved files found. Would you like to recover them?\n" "(WARNING: IF YOU PRESS NO, YOU WILL NOT BE ABLE TO RECOVER" " THE FILES IN THE FUTURE).", "MYDOC", MB_YESNO) != IDNO) { while(CFF.FindNextFile() !=0) { CMyDoc *doc= (CMyDoc *)OpenDocumentFile(CFF.GetFilePath().GetBuffer(1)); } OpenDocumentFile(CFF.GetFilePath().GetBuffer(1)); } else { while(CFF.FindNextFile() !=0) { DeleteFile(CFF.GetFilePath().GetBuffer(1)); } DeleteFile(CFF.GetFilePath().GetBuffer(1)); } } }
使用类向导将PostNcDestroy
成员添加到视图类中,然后在视图关闭时删除自动保存的文件(只有当窗口正常关闭时才会删除)。以下代码块将为您完成此操作。
CFileFind CFF; if(m_autosave_names.size() > 0) { if(CFF.FindFile(((*m_autosave_names.begin())).c_str())==TRUE) { ::DeleteFile(((*m_autosave_names.begin())).c_str()); } m_autosave_names.erase(m_autosave_names.begin()); }
最后,在程序关闭时,应删除这些文件并移除目录。在您的ExitInstance
函数中,删除该目录。
CFileFind CFF; if(CFF.FindFile(gm_autosaveDirectory.GetBuffer(1))==TRUE) RemoveDirectory(gm_autosaveDirectory.GetBuffer(1));