uwpSQLite数据库
作业要求
- 利用数据库保存及恢复应用状态以及实现todo表项的增、删、改、查 (80%)
- 学习使用第三方工具查看数据库内容( 5%)
- 在实验报告中描述LocalFolder RoamingFolder等存储位置的作用, 在实验报告中描述StringBuilder 的作用(5%)
- 拓展:10%,进行图片的存取,如每个表项的图片保存
文件结构
准备工作
安装SQLite
- 工具-拓展和更新-联机 搜索SQLite
- 安装SQLite for Univeral Windows Platform(虽然会提示可能不兼容,但是做作业过程中没有异常)
添加SQLite 引用
- 右击“引用”>>选择“添加引用”>>在弹出的窗口左侧栏选择“Universal Windows”“扩展”>>选择“SQLite for Universal Windows Platform后确定
添加SQLitePCL
- 在引用右键管理NuGet程序包
- 搜索SQLitePCL并安装
准备工作完成
如果在引用里面有第二行和第三行的内容,证明准备工作已经完毕
安装SQLiteExpert
官方地址
在本地建立的数据库地址
可以在C盘搜到包名。
具体实现
文件改动
ListItem.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Windows.UI.Xaml.Media.Imaging;
namespace first_project.Models
{
public class ListItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string id;
public string title { get; set; }
public string description { get; set; }
public DateTimeOffset date { get; set; }
public bool completed { get; set; }
public BitmapImage src { get; set; }
public string ImageName { get; set; }
public bool Completed
{
get { return this.completed; }
set
{
this.completed = value;
NotifyPropertyChanged("Completed");
}
}
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public ListItem()
{
}
public ListItem(string title,string description ,DateTimeOffset date,BitmapImage input)
{
this.id = Guid.NewGuid().ToString();
this.title = title;
this.description = description;
this.completed = false;
this.date = date;
this.src = input;
}
public ListItem(string id ,string title, string description, DateTimeOffset date, BitmapImage input)
{
this.id = id;
this.title = title;
this.description = description;
this.completed = false;
this.date = date;
this.src = input;
}
public ListItem(string id, string title, string description, DateTimeOffset date)
{
this.id = id;
this.title = title;
this.description = description;
this.completed = false;
this.date = date;
}
public ListItem(string title, string description, DateTimeOffset date,BitmapImage image,string imageName)
{
this.id = Guid.NewGuid().ToString();
this.title = title;
this.description = description;
this.completed = false;
this.date = date;
this.src = image;
this.ImageName = imageName;
}
public ListItem(string id ,string title, string description, DateTimeOffset date, BitmapImage image, string imageName)
{
this.id = id;
this.title = title;
this.description = description;
this.completed = false;
this.date = date;
this.src = image;
this.ImageName = imageName;
}
}
}
- 新加成员变量
public string ImageName { get; set; }
,为了存储图片,具体操作后面再说。 - 新加很多构造函数,因为新加了成员变量,所以相应的改动了构造函数,上面很多函数是之前不加图片,测试数据库用的,虽然冗余,感觉没必要删去,但是因为每次建立一个ListItem,就会自动生成一个id,所以为了确保id不变化,必须要重载含有id的构造函数。
ListItemViewModels
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using first_project.Models;
using Windows.UI.Xaml.Media.Imaging;
namespace first_project.ViewModels
{
public class ListItemViewModels
{
private ObservableCollection<Models.ListItem> allItems = new ObservableCollection<Models.ListItem>();
public ObservableCollection<Models.ListItem> AllItems { get { return this.allItems; } }
public void AddTodoItem(ListItem temp)
{
this.allItems.Add(temp);
}
public void RemoveTodoItem(string id)
{
ListItem temp = new ListItem();
for(int i = 0; i < allItems.Count(); i++)
{
if(allItems[i].id == id)
{
temp = allItems[i];
break;
}
}
this.allItems.Remove(temp);
((App)App.Current).dataBase.delete(id);
}
public void UpdateTodoItem(string id,string title,string description,DateTimeOffset date,BitmapImage src,string image)
{
/*ListItem temp = new ListItem();
for (int i = 0; i < allItems.Count(); i++)
{
if (allItems[i].id == id)
{
allItems[i].title = title;
allItems[i].description = description;
allItems[i].date = date;
allItems[i].src = src;
break;
}
}*/
ListItem temp = new ListItem(id, title, description, date, src,image);
this.RemoveTodoItem(id);
this.AddTodoItem(temp);
((App)App.Current).dataBase.insert(id, title, description, date,image);
}
}
}
- 修改了
public void AddTodoItem(ListItem temp)
这个函数,传入参数为ListItem,而不像以前传入ListItem的变量,或许有人想在这里面对数据库进行操作,就是新加一个ListItem,就在数据库里面添加一个,但是由于重启App的时候必定要调用读取数据库的函数,所以就和这个函数分开写了。 RemoveTodoItem
和UpdateTodoItem
函数中就嵌套了数据库函数,因为App的初始化和这两个函数没有关系。
App.xaml.cs
public bool issuspend = true;
public string currentId;
public ListItemViewModels ViewModel;
public BitmapImage srcImage;
public DataAccess dataBase;
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
dataBase = new DataAccess();
ViewModel = new ListItemViewModels();
dataBase.readDate();
TileUpdateManager.CreateTileUpdaterForApplication().Clear();
TileUpdateManager.CreateTileUpdaterForApplication().EnableNotificationQueue(true);
}
- 在上次的基础上的加上了
public DataAccess dataBase;
变量,而且在构造函数中加入dataBase.readDate();
,使得程序已启动就读取数据库中的值。
MainPage.xaml.cs
using first_project.Models;
using first_project.Services;
using first_project.ViewModels;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.AccessCache;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Notifications;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;
// https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x804 上介绍了“空白页”项模板
namespace first_project
{
public sealed partial class MainPage : Page
{
//public Content newTemp;
//public ListItemViewModels ViewModel;
public bool isClick;
//public string currentId;
public bool isCreate = true;
public ListItemViewModels ViewModel = ((App) App.Current).ViewModel;
public ListItem selectedItem;
public StorageFile imageFile;
public string name = "2.png";
public MainPage()
{
NavigationCacheMode = NavigationCacheMode.Enabled;
//ViewModel = new ListItemViewModels();
//newTemp = new Content();
this.InitializeComponent();
this.SizeChanged += (s, e) =>
{
if (e.NewSize.Width > 800)
{
isClick = false;
}
else
{
isClick = true;
}
};
/*
BitmapImage srcImage = NewImage.Source as BitmapImage;
((App)App.Current).ViewModel.AddTodoItem("First", "Just the First", DateTimeOffset.Now, srcImage);*/
}
private void Create_To_Update()
{
isCreate = false;
Create.Content = "Update";
}
private void Update_To_Create()
{
isCreate = true;
Create.Content = "Create";
}
private void addBarClick(object sender, RoutedEventArgs e)
{
if (isClick == true)
{
//Content result = new Content("NULL", ((App)App.Current).ViewModel);
this.Frame.Navigate(typeof(NewPage));
}
}
private void Delete_Button(object sender, RoutedEventArgs e)
{
((App)App.Current).ViewModel.RemoveTodoItem(((App)App.Current).currentId);
Delete.Visibility = Visibility.Collapsed;
Update_To_Create();
}
private async void CreateButton(object sender, RoutedEventArgs e)
{
if (TitleBlock.Text == "" || DetailBlock.Text == "")
{
var dialog = new ContentDialog()
{
Title = "提示",
Content = "Title or Deteil is empty",
PrimaryButtonText = "确定",
FullSizeDesired = false,
};
await dialog.ShowAsync();
}
else if (Date.Date < DateTimeOffset.Now)
{
var dialog = new ContentDialog()
{
Title = "提示",
Content = "The date is smaller than today",
PrimaryButtonText = "确定",
//SecondaryButtonText = "取消",
FullSizeDesired = false,
};
await dialog.ShowAsync();
}
else
{
if (isCreate == true)
{
BitmapImage result = NewImage.Source as BitmapImage;
ListItem temp = new ListItem(TitleBlock.Text, DetailBlock.Text, Date.Date, result,name);
((App)App.Current).ViewModel.AddTodoItem(temp);
((App)App.Current).dataBase.insert(temp.id, temp.title, temp.description,temp.date,name);
circulationUpdate();
//UpdatePrimaryTile(TitleBlock.Text,DetailBlock.Text);
var dialog = new ContentDialog()
{
Title = "提示",
Content = "Create Sucessfully",
PrimaryButtonText = "确定",
//SecondaryButtonText = "取消",
FullSizeDesired = false,
};
await dialog.ShowAsync();
}
else
{
BitmapImage result = NewImage.Source as BitmapImage;
((App)App.Current).ViewModel.UpdateTodoItem(((App)App.Current).currentId, TitleBlock.Text, DetailBlock.Text, Date.Date, result,name);
circulationUpdate();
Update_To_Create();
var dialog = new ContentDialog()
{
Title = "提示",
Content = "Update Successfully",
PrimaryButtonText = "确定",
//SecondaryButtonText = "取消",
FullSizeDesired = false,
};
await dialog.ShowAsync();
}
}
}
private void CancelClick(object sender, RoutedEventArgs e)
{
Date.Date = DateTimeOffset.Now;
TitleBlock.Text = "";
DetailBlock.Text = "";
}
public void TodoTtem_ItemClicked(object sender, ItemClickEventArgs e)
{
var temp = (ListItem)e.ClickedItem;
((App)App.Current).currentId = temp.id;
if (Window.Current.Bounds.Width > 800)
{
Create_To_Update();
Delete.Visibility = Visibility.Visible;
TitleBlock.Text = temp.title;
DetailBlock.Text = temp.description;
Date.Date = temp.date;
NewImage.Source = temp.src;
}
else
{
//Content result = new Content(currentId, ViewModel);
this.Frame.Navigate(typeof(NewPage));
}
}
private async void Select_Click(object sender, RoutedEventArgs e)
{
//文件选择器
FileOpenPicker openPicker = new FileOpenPicker();
//选择视图模式
openPicker.ViewMode = PickerViewMode.Thumbnail;
//openPicker.ViewMode = PickerViewMode.List;
//初始位置
openPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
//添加文件类型
openPicker.FileTypeFilter.Add(".jpg");
openPicker.FileTypeFilter.Add(".jpeg");
openPicker.FileTypeFilter.Add(".png");
StorageFile file = await openPicker.PickSingleFileAsync();
//StorageFile file = await openPicker.PickSingleFileAsync();
//StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/2.png"));
name = file.Name;
if (file != null)
{
imageFile = file;
ApplicationData.Current.LocalSettings.Values["TempImage"] = StorageApplicationPermissions.FutureAccessList.Add(file);
using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
{
var srcImage = new BitmapImage();
await srcImage.SetSourceAsync(stream);
NewImage.Source = srcImage;
}
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
bool suspending = ((App)App.Current).issuspend;
if (suspending)
{
var composite = new ApplicationDataCompositeValue();
composite["Title"] = TitleBlock.Text;
composite["Details"] = DetailBlock.Text;
composite["Date"] = Date.Date;
composite["Visible"] = ((App)App.Current).ViewModel.AllItems[0].Completed;
ApplicationData.Current.LocalSettings.Values["MainPage"] = composite;
}
DataTransferManager.GetForCurrentView().DataRequested -= OnShareDataRequested;
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
if (e.NavigationMode == NavigationMode.New)
{
ApplicationData.Current.LocalSettings.Values.Remove("MainPage");
ApplicationData.Current.LocalSettings.Values["TempImage"] = null;
}
if (ApplicationData.Current.LocalSettings.Values["TempImage"] != null)
{
StorageFile tempimg;
tempimg = await StorageApplicationPermissions.FutureAccessList.GetFileAsync((string)ApplicationData.Current.LocalSettings.Values["TempImage"]);
IRandomAccessStream ir = await tempimg.OpenAsync(FileAccessMode.Read);
BitmapImage bi = new BitmapImage();
await bi.SetSourceAsync(ir);
NewImage.Source = bi;
ApplicationData.Current.LocalSettings.Values["TempImage"] = null;
}
{
if (ApplicationData.Current.LocalSettings.Values.ContainsKey("MainPage"))
{
var composite = ApplicationData.Current.LocalSettings.Values["MainPage"] as ApplicationDataCompositeValue;
TitleBlock.Text = (string)composite["Title"];
DetailBlock.Text = (string)composite["Details"];
Date.Date = (DateTimeOffset)composite["Date"];
((App)App.Current).ViewModel.AllItems[0].Completed = (bool)composite["Visible"];
ApplicationData.Current.LocalSettings.Values.Remove("MainPage");
}
}
DataTransferManager.GetForCurrentView().DataRequested += OnShareDataRequested;
}
private void UpdatePrimaryTile(string input,string input2)
{
var xmlDoc = TileService.CreateTiles(new PrimaryTile(input,input2));
var updater = TileUpdateManager.CreateTileUpdaterForApplication();
TileNotification notification = new TileNotification(xmlDoc);
updater.Update(notification);
}
private void circulationUpdate()
{
TileUpdateManager.CreateTileUpdaterForApplication().Clear();
for (int i = 0;i< ((App)App.Current).ViewModel.AllItems.Count(); i++)
{
UpdatePrimaryTile(((App)App.Current).ViewModel.AllItems[i].title, ((App)App.Current).ViewModel.AllItems[i].description);
}
}
private void MenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
MenuFlyoutItem se = sender as MenuFlyoutItem;
var dc = se.DataContext as ListItem;
selectedItem = dc;
DataTransferManager.ShowShareUI();
}
public void OnShareDataRequested(DataTransferManager sender, DataRequestedEventArgs args)
{
var dp = args.Request.Data;
var deferral = args.Request.GetDeferral();
dp.Properties.Title = selectedItem.title;
dp.Properties.Description = selectedItem.description;
try
{
dp.SetBitmap(RandomAccessStreamReference.CreateFromFile(imageFile));
}
catch
{
dp.SetBitmap(RandomAccessStreamReference.CreateFromUri(selectedItem.src.UriSource));
}
dp.SetWebLink(new Uri("http://seattletimes.com/ABPub/2006/01/10/2002732410.jpg"));
deferral.Complete();
}
private async void searchButton_Click(object sender, RoutedEventArgs e)
{
StringBuilder result = ((App)App.Current).dataBase.query(searchBox.Text);
var dialog = new ContentDialog()
{
Title = "提示",
Content = result,
PrimaryButtonText = "确定",
FullSizeDesired = false,
};
await dialog.ShowAsync();
}
}
}
- 首先加入变量
public string name = "2.png";
,2.png是在Assets文件里面的一张图片的名字,作为初始图片,因为图片有选择和不选之分,所以设置默认值,不管选不选,都不会为空。 CreateButton
函数中改变
ListItem temp = new ListItem(TitleBlock.Text, DetailBlock.Text, Date.Date, result,name);
((App)App.Current).ViewModel.AddTodoItem(temp);
((App)App.Current).dataBase.insert(temp.id, temp.title, temp.description,temp.date,name);
因为我要读入ListItem的id,但是又不能在AddTodoItem里面新建一个ListItem,加入到viewmode里面,所以将AddTodoItem的参数改成了ListItem,千万不能忘记在插入dataBase时,输入id,否则程序会莫名出错。
- 剩下很重要的是在
Select_Click
函数里面,加入name = file.Name;
这一句,得到所选文件的name,如果用局部变量的话,可能得不到,不知道什么原因,所以直接给全局变量,这样在ListItem初始化和更改的时候,可以传入。 private async void searchButton_Click
这个函数处理查询的结果。
MainPage.xaml
加入下面两行,增加一个查询窗口,和查询按钮,至于布局,自己觉得好看就行。
<TextBox Grid.Row="0" x:Name="searchBox" Width="200" Margin="100,0,100,19"/>
<Button x:Name="searchButton" Grid.Row="0" Content="search" HorizontalAlignment="Right" Width="100" Margin="0,0,0,18" Click="searchButton_Click"/>
新加的文件
SQLite.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SQLitePCL;
using first_project.Models;
using System.Globalization;
using first_project;
using Windows.UI.Xaml.Media.Imaging;
using System.IO;
using Windows.Storage;
using Windows.Storage.Streams;
public class DataAccess
{
BitmapImage NewImage;
string address = "ms-appx:///Assets/";
CultureInfo provider = CultureInfo.InvariantCulture;
SQLiteConnection conn;
public DataAccess()
{ // Get a reference to the SQLite database
NewImage = new BitmapImage();
conn = new SQLiteConnection("sqlitepcldemo.db");
string sql = @"CREATE TABLE IF NOT EXISTS
ListItem (Id VARCHAR( 140 ) PRIMARY KEY NOT NULL,
Title VARCHAR( 140 ),
Description VARCHAR( 140 ),
Date VARCHAR( 140 ),
Image VARCHAR( 140 )
);";
using (var statement = conn.Prepare(sql))
{
statement.Step();
}
}
public void insert(string id, string title, string description, DateTimeOffset date)
{
using (var custstmt = conn.Prepare("INSERT INTO ListItem (Id, Title, Description,Date) VALUES (?, ?, ?, ?)"))
{
custstmt.Bind(1, id);
custstmt.Bind(2, title);
custstmt.Bind(3, description);
custstmt.Bind(4, date.ToString(provider));
custstmt.Step();
}
}
public void insert(string id, string title, string description, DateTimeOffset date,string imageName)
{
using (var custstmt = conn.Prepare("INSERT INTO ListItem (Id, Title, Description,Date,Image) VALUES (?, ?, ?, ?, ?)"))
{
custstmt.Bind(1, id);
custstmt.Bind(2, title);
custstmt.Bind(3, description);
custstmt.Bind(4, date.ToString(provider));
custstmt.Bind(5, imageName);
custstmt.Step();
}
}
public void update(string id, string title, string description, DateTimeOffset date, string image)
{
using (var custstmt = conn.Prepare("UPDATE ListItem SET Title = ?, Description = ? ,Date = ? Image = ? WHERE Id=?"))
{
custstmt.Bind(1, title);
custstmt.Bind(2, description);
custstmt.Bind(3, date.ToString(provider));
custstmt.Bind(4, image);
custstmt.Bind(5, id);
custstmt.Step();
}
}
public void delete(string id)
{
using (var statement = conn.Prepare("DELETE FROM ListItem WHERE Id = ?"))
{
statement.Bind(1, id);
statement.Step();
}
}
DateTimeOffset stringToDate(string input)
{
string format = "MM/dd/yyyy H:mm:ss zzz";
DateTimeOffset result;
result = DateTimeOffset.ParseExact(input, format, provider,
DateTimeStyles.AllowWhiteSpaces |
DateTimeStyles.AdjustToUniversal);
return result;
}
public void nameToImage(string name)
{
string temp = address + name;
if (name != null)
{
NewImage = new BitmapImage(new Uri(temp));
}
}
public void readDate()
{
using (var statement = conn.Prepare("SELECT Id,Title,Description,Date,Image FROM ListItem"))
{
while (SQLiteResult.ROW == statement.Step())
{
nameToImage((string)statement[4]);
ListItem temp = new ListItem((string)statement[0], (string)statement[1], (string)statement[2], stringToDate((string)statement[3]), NewImage,(string)statement[4]);
((App)App.Current).ViewModel.AddTodoItem(temp);
}
}
}
public StringBuilder query(string input)
{
string input2 = "%" + input + "%";
StringBuilder result = new StringBuilder();
using (var statement = conn.Prepare("SELECT Title,Description,Date FROM ListItem WHERE Title LIKE ? OR Description LIKE ? OR Date LIKE ?"))
{
statement.Bind(1, input2);
statement.Bind(2, input2);
statement.Bind(3, input2);
while (SQLiteResult.ROW == statement.Step())
{
result.Append("Title: ").Append((string)statement[0]).Append(" Description: ").Append((string)statement[1]).Append(" Date: ").Append((string)statement[2]).Append("\n");
}
}
return result;
}
}
这是这次改动的最重要的部分,所以我会好好解释这部分。
BitmapImage NewImage;
第一个成员变量,为了从数据库读取图片。string address = "ms-appx:///Assets/";
第二个成员变量,为了和图片名字拼接成一个完整的路径。CultureInfo provider = CultureInfo.InvariantCulture;
,这个变量,用来把DateTimeOffset还原时候修改格式。SQLiteConnection conn;
数据库连接的全局变量。public DataAccess()
初始化数据库,新建数据库有五个变量,分别是Id,Title,Description,Date,Image。public void insert
向数据库中插入一条记录。其中custstmt.Bind(4, date.ToString(provider));
,在ToString
函数中加入参数,以修改存入的DateTimeOffset
格式。public void update
改动数据库的数据。public void delete
删除一条数据库中的记录。DateTimeOffset stringToDate(string input)
将一个string
变量,还原成DateTimeOffset
类型,使用ParseExact
函数,一共四个参数,有兴趣可以查一下这个函数,其中的provider
就是已经在全局变量中声明过的。这样还原成的DateTimeOffset
是这种类型的string format = "MM/dd/yyyy H:mm:ss zzz";
,因为不能以"YYYY/mm/dd H:mm:ss zzz"
这种格式还原,所以在insert
时,也要改格式。nameToImage
用来将图片的名字生成一个BitmapImage,这里不能用异步的方法,否则会出现线程冲突的问题,就会很复杂。readDate
在App初始化时,读取数据库中的数据,其中DateTimeOffset
要经过stringToDate
的处理,ImageName
要经过nameToImage
的处理,而且自身也要传进去。public StringBuilder query(string input)
实现模糊查询,在Title,Description和Date中只要有匹配的就打印出来,每行加换行符。
重点提一下图片的处理
处理图片有两种方法,第一种转成二进制,我上网找了很多资料,甚至有部分人说BitmapImage
转成byte[]
是不行的,也可见转成二进制是有些困难的,而我一直都是用的地址,但是选择图片的时候,是得不到图片的UriSource
,所以我两种方法都没用,而是选择了存图片的名字并用字符串拼接的方式,得到新的BitmapImage,为什么会想到这两种方法?上次作业用email分享图片的时候,我就发现了UriSource的问题,但是用StorageFile的方式解决了问题,网上有很多BitmapImage的方法,不过很抱歉,几乎都不是uwp平台的,uwp平台的BitmapImage初始化只有两个方法,一个直接初始化,一个用Uri初始化,除此之外,还可以用setSource方法间接初始化,应该就没有其他方法了,我首先想到的是,用StorageFile的setSource方法初始化BitmapImage,就像选择图片一样的方法,用StorageFile得到图片的名字,然后再用IRandomAccessStream
的方法解决,但是不知道怎么回事就多线程冲突了,所以直接用Uri初始化,这样会导致导入的时候有一点点卡,但是这也算是好的方法了。
一点添加
虽然只可以拿Assets目录下的图片,但是并不影响,可以自己多扩展,比如想要从其他地方拿图片,
StorageFile file = await openPicker.PickSingleFileAsync();
var package = Windows.ApplicationModel.Package.Current.InstalledLocation;
var assetsFolder = await package.GetFolderAsync("Assets");
name = file.Name;
await file.MoveAsync(assetsFolder);
将Select_Click中的部分写法改成这样就行,比如将桌面的图片,移到Assets目录下,然后剩下的就和之前一样操作,当然也可以调用复制的API,判断一下重复操作之类的,不是很难的操作。