一个简单的自动保存/恢复画图应用程序






4.93/5 (9投票s)
一个解释自动保存功能基本实现的应用程序
引言
当您花费很长时间在一个应用程序上,然后应用程序崩溃时,您会有多么恼火?您忘记了进行“文件 -> 保存”或“Ctrl + S”,导致数据完全丢失。您所付出的努力都白费了。也许下次,您会更加谨慎,并定期按下“Ctrl + S”。也许您会遵循几次,然后又忘了这样做。
应用程序崩溃可能发生的原因有很多,例如停电、应用程序中的错误导致应用程序崩溃或恶意病毒。您可以防范停电或病毒,但直到获得补丁/新版本之前,您无法应对错误。
背景
有些应用程序具有自动保存功能,可以在一定时间间隔内保存工作。如果应用程序崩溃,用户会被提示加载自动保存的工作。Microsoft Word 内置了此功能,可以按如下方式进行配置。可以通过左上角的 Office 按钮 -> Word 选项 -> 保存选项卡进行访问。
另一个例子是 Microsoft Outlook 和 Gmail 在一定时间间隔后保存电子邮件。
我将编写一个简单的画图应用程序,它可以在一定时间间隔内保存设计。它使用 WPF 和 C# 开发。该应用程序只是为了理解自动保存的概念,不包含 Microsoft Word 中的高级功能。但是,它可以扩展以创建这样的功能。我会尽量保持简单。
自动保存画图应用程序
应用程序功能列表如下
- 新建 – 清除设计
- 保存 – 将工作设计保存到指定位置
- 打开 – 打开选定的 Ink 序列化格式 (*.ink) 文件
- 退出 – 退出应用程序
- 自动保存 – 在后台以指定的时间间隔将设计保存到隐藏位置
- 恢复 – 应用程序崩溃后重新启动应用程序时,在启动时加载最后一次自动保存的文件
让我们开始创建应用程序。
代码
- 打开 Visual Studio -> 新建项目 -> 其他项目类型 -> Visual Studio 解决方案 -> 空解决方案。将其命名为“
AutosaveAndRecovery
”。 - 添加一个新的 WPF 项目“
AutosavePaint
”。 - 用下面的 XAML 和 .cs 文件替换 MainWindow.xaml 和 MainWindow.xaml.cs。
MainWindow.xaml
<Window x:Class="AutosavePaint.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Menu HorizontalAlignment="Left" Height="38" VerticalAlignment="Top"
Width="517" RenderTransformOrigin="0.32,-0.079">
<MenuItem Header="File">
<MenuItem Command="ApplicationCommands.New">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.New"
Executed="New"
CanExecute="CanNew"/>
</MenuItem.CommandBindings>
</MenuItem>
<MenuItem Header="Open" Command="ApplicationCommands.Open"
HorizontalAlignment="Left" Width="157.507"
RenderTransformOrigin="0.502,0.502" Height="26" Margin="0,0,-13.17,0">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open"
Executed="Open"
CanExecute="CanOpen"/>
</MenuItem.CommandBindings>
</MenuItem>
<MenuItem Header="Save" Margin="0,0,12,0" Command="ApplicationCommands.Save"
Height="25" RenderTransformOrigin="0.552,0.482">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.Save"
Executed="Save"
CanExecute="CanSave"/>
</MenuItem.CommandBindings>
</MenuItem>
<MenuItem Header="Exit" Command="ApplicationCommands.Close">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
Executed="Exit"
CanExecute="CanExit"/>
</MenuItem.CommandBindings>
</MenuItem>
</MenuItem>
</Menu>
<InkCanvas Name="inkCanvas" HorizontalAlignment="Left" Height="229"
Margin="10,43,0,0" VerticalAlignment="Top" Width="497" Background="#FF9C9898"/>
<StatusBar x:Name="statusBar" Height="33" Margin="10,277,10,0"
VerticalAlignment="Top" Background="#FFD4CFCF">
<StatusBarItem x:Name="statusBarItem" Content="StatusBarItem"
Height="33" VerticalAlignment="Top"/>
</StatusBar>
</Grid>
</Window>
MainWindow.xaml.cs
我们已经构建了应用程序的结构,但没有功能。运行应用程序并确保它能正常启动。我使用了 InkCanvas
进行绘图。将鼠标移动到中间的灰色区域。
现在,我们将逐一添加上述功能。
1. 新建:清除设计
添加以下代码以清除画布,在按下“新建”按钮时,设计将被清除。
/// <summary>
/// Clears the design
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void New(object sender, ExecutedRoutedEventArgs e)
{
inkCanvas.Strokes.Clear();
statusBarItem.Content = "Ready";
}
2. 保存:将工作设计保存到指定位置
添加以下代码将设计保存到文件。
/// <summary>
/// Saves currently working design
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Save(object sender, ExecutedRoutedEventArgs e)
{
var saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "Ink Serialized Format (*.isf)|*.isf|";
if (saveFileDialog.ShowDialog() == true)
{
Save(saveFileDialog.FileName);
}
}
/// <summary>
/// Saves the design to the specified file
/// </summary>
/// <param name="fileName">File to be saved</param>
/// <returns>true, if saving is successful</returns>
private bool Save(string fileName)
{
FileStream fs = null;
try
{
fs = File.Open(fileName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (fs != null)
fs.Close();
}
return true;
}
设计将以 .ink* 扩展名的文件保存。设计也可以保存为位图格式。为了保持代码简洁,我没有包含它,本文的主要目的是演示自动保存和恢复。 这是一篇精彩的文章,由 Sacha Barber 撰写,关于使用 InkCanvas
的 Paint
应用程序。
3. 打开:打开选定的 Ink 序列化格式 (*.ink) 文件
添加以下代码以打开 *.ink 文件并解析引用。
/// <summary>
/// Opens a selected design
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Open(object sender, ExecutedRoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.CheckFileExists = true;
openFileDialog.Filter = "Ink Serialized Format (*.isf)|*.isf|" +
"All files (*.*)|*.*";
if (openFileDialog.ShowDialog(this) == true)
{
string fileName = openFileDialog.FileName;
if (!fileName.ToLower().EndsWith(".isf"))
{
MessageBox.Show("The requested file is not a Ink Serialized Format
file\r\n\r\nplease retry", Title);
}
else
{
if (Open(fileName))
{
statusBarItem.Content = "Loaded";
}
else
{
statusBarItem.Content = "An error occured while opening the file";
}
}
}
}
/// <summary>
/// Opens the specified file in canvas
/// </summary>
/// <param name="fileName">File to be opened</param>
/// <returns>true, if opening is successful</returns>
private bool Open(string fileName)
{
FileStream fs = null;
try
{
this.inkCanvas.Strokes.Clear();
fs = new FileStream(fileName, FileMode.Open);
inkCanvas.Strokes = new System.Windows.Ink.StrokeCollection(fs);
}
catch (Exception)
{
return false;
}
finally
{
if (fs != null)
fs.Close();
}
return true;
}
4. 退出:退出应用程序
添加以下代码以退出应用程序
/// <summary>
/// Exits the application
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Exit(object sender, ExecutedRoutedEventArgs e)
{
this.Close();
}
5. 自动保存:在后台以指定的时间间隔将设计保存到隐藏位置
为了实现自动保存,我们需要考虑以下几点。
- 位置 – 自动保存文件的位置。我们将对其隐藏用户。
- 后台操作 - 保存操作必须在后台的工作线程中执行,以使应用程序保持响应。
- 自动保存间隔 – 这决定了两次连续保存操作之间的时间间隔。
首先定义位置。我使用了执行程序集的位置,并在其中创建了 Autosave 目录。此外,该目录需要被隐藏。添加了两个辅助方法 MakeDirectoryHidden(…)
和 MakeFileHidden(…)
。有关实现,请参阅下载的代码。
private readonly string _backupFilePath;
private string _backupFile;
private const string BACKUP_FILE_NAME = "PaintAppBackup.bk";
/// <summary>
/// Constructor to initialize this class
/// </summary>
public MainWindow()
{
InitializeComponent();
statusBarItem.Content = "Ready";
_backupFilePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) +
"\\" + BACKUP_DIRECTORY;
_backupFile = _backupFilePath + "\\" + BACKUP_FILE_NAME;
if (!Directory.Exists(_backupFilePath))
{
Directory.CreateDirectory(_backupFilePath);
MakeDirectoryHidden(_backupFilePath);
}
}
运行应用程序,如果在调试模式下运行应用程序,您会发现在 ..\\ AutosaveAndRecovery\AutosavePaint\bin\Debug\Autosave 目录下创建了 Autosave 目录。
为了在后台保存设计,我使用了 BackgroundWorker
。
已添加两个方法来实现此功能。
/// <summary>
/// This is called when saving of design asynchronously is completed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnSaveAsyncCompletion(object sender, RunWorkerCompletedEventArgs e)
{
// First, handle the case where an exception was thrown.
if (e.Error != null)
{
statusBarItem.Content = "An error occured while saving the design";
DeleteBackupFile();
}
else
{
// Finally, handle the case where the operation
// succeeded.
statusBarItem.Content = "Saved";
MakeFileHidden(_backupFile);
}
}
现在,最后一件事情是以一定的间隔定期调用 SaveAync(…)
。我选择了 20 秒作为保存间隔。为了定期调用它,我使用了 DispatcherTimer
。DispatcherTimer
定期调用以下方法。
/// <summary>
/// This method is periodically called at a specified interval
/// </summary>
/// <param name="o"></param>
/// <param name="e"></param>
void PeriodicSave(object o, EventArgs e)
{
statusBarItem.Content = "Saving";
if (!Directory.Exists(_backupFilePath))
{
Directory.CreateDirectory(_backupFilePath);
MakeDirectoryHidden(_backupFilePath);
}
DeleteBackupFile();
SaveAsync(_backupFile);
}
为了启动/停止 Dispatcher
,已添加窗口的 Loaded
和 Closing
事件。
/// <summary>
/// Called when window is loaded
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_dispatcherTimer = new DispatcherTimer();
_dispatcherTimer.Interval = TimeSpan.FromMilliseconds(AUTOSAVE_INTERVAL_SECONDS * 1000);
_dispatcherTimer.Tick += PeriodicSave;
_dispatcherTimer.Start();
statusBarItem.Content = "Ready";
}
/// <summary>
/// Called when window is closing
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Closing(object sender, CancelEventArgs e)
{
if (_dispatcherTimer != null)
{
_dispatcherTimer.Stop();
_dispatcherTimer = null;
}
}
运行应用程序,您会注意到在 20 秒后保存设计时发生了错误。引发的异常是跨线程异常。我们正在尝试从后台线程访问 UI 组件 (InkCanvas
)。要克服这个问题,请像下面这样将 UI 访问调用封送回 UI 线程。
/// <summary>
/// Saves the design to the specified file
/// </summary>
/// <param name="fileName">File to be saved</param>
/// <returns>true, if saving is successful</returns>
private bool Save(string fileName)
{
FileStream fs = null;
try
{
fs = File.Open(fileName, FileMode.Create);
ExecuteOnUIThread(() => inkCanvas.Strokes.Save(fs)); <<ç=
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (fs != null)
fs.Close();
}
return true;
}
/// <summary>
/// Marshall the call to the UI thread
/// </summary>
/// <param name="action"></param>
private void ExecuteOnUIThread(Action action)
{
var dispatcher = Application.Current.Dispatcher;
if (dispatcher != null)
{
dispatcher.Invoke(action);
}
else
{
action();
}
}
运行应用程序,进行一些草图,20 秒后,文件将被保存。
6. 恢复:在应用程序崩溃后重新启动应用程序时,加载最后一次自动保存的文件
这是最后一个功能,如果我们无法恢复最后一次自动保存的文件,自动保存就没有意义了。下面的流程图描绘了恢复机制。
CheckAndLoadBackupFile(…)
方法负责恢复。它在应用程序启动后立即在 loaded
事件中被调用。此外,DeleteBackupFile(…)
在应用程序的 closing
事件中被调用。
设计自动保存后的应用程序。
结论
自动保存和恢复是在应用程序中让用户生活更舒适的绝佳功能。我只是介绍了如何实现它。它可以扩展为高级功能。它可以用于将设计、草图或表单数据保存到文件/数据库。任何提供打开/保存选项的应用程序都是此类应用程序的理想选择。
欢迎评论和建议!
参考文献
- https://codeproject.org.cn/Articles/324/Autosave-and-Crash-Recovery
- https://codeproject.org.cn/Articles/4574/Save-and-Restore-User-Preferences?q=autosave
- https://codeproject.org.cn/Articles/18166/Scratchpad-An-Auto-Save-Notepad
- https://codeproject.org.cn/Articles/19102/Adventures-into-Ink-API-using-WPF
- https://codeproject.org.cn/Articles/16579/Saving-Rebuilding-InkCanvas-Strokes
- https://codeproject.org.cn/Articles/617868/Scribble-WPF-InkCanvas-Application-Using-PRISM-MVV