UWP--简单日程制作

工具:Visual Studio 2017、Blend
语言:XAML,C#

日程应用的要求如下:实现自适应UI,即应用样式跟随屏幕宽度改变而改变;实现日程创建,删除与查看功能。
说明:在这个日程的制作中,具体的xaml和c#语法不再赘述,不了解的地方可以自行百度。
如图,先来看看最终样式:(说明:左侧页面标题为MyLists,右侧页面为Details;日程名称为ListItem)

1. 完整视图:(width≥800px)


2. Detail消失:  (600px≤width<800px)


3. Image消失:(width<600px)                        4. Detail界面:(width<800px, 点击左侧日程)

               

下面进行具体实现:

  • 总体实现规划:

       主要分为三个部分:视图View(.xaml)、逻辑Logic(.xaml.cs)和数据绑定;

       1. 视图部分:(XAML分为两个页面,MainPage和NewPage)

      MainPage主要写左侧日程列表的布局,并在页尾加上一个可以导向到NewPage的Frame框架;布局采用Grid+ListView来写,Grid分为两行,第一行写标题,第二行分为两列,一边容纳ListView列表(即主页面的主要部分),另一边容纳右边详情(Detail);关于页面自适应,使用VisualStateGroup来实现,在Blend中设计三个不同的VisualState状态,来控制页面宽度不同时Detail界面和Image元素是否显示,其中比较关键的是Grid.ColumnSpan的应用,此属性表示所选Frame所占用的列数;关于Image的自适应实现,VisualStateGroup必须写在DateTemplate里面,并且用UserControl标签包含;最后在MainPage页面末尾加上AppBarbutton即可。

       Newpage的实现比较简单,采用Grid和ScrollViewer、StackPanel标签来实现,Grid也分为两行,第一行可以写标题(Detail),第二行用来容纳ScrollViewer标签,ScrollViewer实现滚动条,里面写上用Stackpanel包含的各个空间元素即可以实现;最后页面末尾加上AppBarButton即可;

       页面布局大致就是如此,最后可以加上背景图,利用Page.Background标签。

        2. 逻辑部分:(逻辑部分主要也分为MainPage和NewPage)

       MainPage下主要实现CheckBoxClick、ListView_ItemClick、AddBarButtonClick、MenuFlyEdit_Click、MenuFlyDelete_Click五个Click事件的函数及相应控件的数据绑定。

    NewPage 下主要实现Create_Update、Cancel、GetPicture、AddBarbuttonClick、DeleteBarButtonClick五个绑定函数的功能。

        3. 数据绑定: 数据绑定是一种把数据绑定到用户界面元素(控件)的通用机制。可以使用x:Bind即Binding来实现,推荐使用x:Bind(x:Bind可以解决大部分绑定问题)

  • 代码文件结构:

       第一次做UWP开发,并且也是第一次使用MVVM框架,所以有些使用不当的地方,还望谅解。文件具体目录结构如下:

           

       Models文件夹下是ListItem.cs文件,书写日程的类模板;ViewModels文件夹下是ListItemViewModels.cs文件,书写ListItem的处理方法。由于之后的视图、逻辑还有数据绑定实现过程需要ListItem类及其处理方法,所以先分别实现这两个模块:

ListItem.cs:

namespace List.Models
{
     class ListItem : INotifyPropertyChanged
     {
        public event PropertyChangedEventHandler PropertyChanged;

        //public string Id { get; set; }

        public ImageSource Img { get; set; }

        public double Size { get; set; }

        public string Title { get; set; }

        public string Detail { get; set; }

        public DateTimeOffset Date { get; set; }

        public bool? Finish { get; set; }

        public ListItem(ImageSource img, double size, string title,string detail, DateTimeOffset date)
        {
            //this.Id = Guid.NewGuid().ToString();
            this.Img = (img == null ? new BitmapImage(new Uri("Assets/pic3.ico")) : img);
            this.Size = size;
            this.Date = date;
            this.Title = title;
            this.Detail = detail;
            this.Finish = false;
        }

        private void NotityPropertyChanged(string propertyname)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
        }
    }
}
ListItemViewModels.cs:
namespace List.ViewModels
{
    class ListItemViewModel
    {
        private ObservableCollection<Models.ListItem> allItems = new ObservableCollection<Models.ListItem>();
        public ObservableCollection<Models.ListItem> AllItems { get { return this.allItems; } }
        private Models.ListItem selectedItem;
        public Models.ListItem SelectedItem
        {
            get { return selectedItem; }
            set { this.selectedItem = value; }
        }

        public void AddListItem(ImageSource img, double size, string title, string detail, DateTimeOffset date)
        {
            this.allItems.Add(new Models.ListItem(img, size, title, detail, date));
        }

        public void RemoveListItem(Models.ListItem SelectedItem1)
        {
            this.allItems.Remove(SelectedItem1);
            this.selectedItem = null;
        }

        public void UpdateListItem(ImageSource img, double size, string title,string detail, DateTimeOffset date)
        {
            this.selectedItem.Img = img;
            this.selectedItem.Size = size;
            this.selectedItem.Title = title;
            this.selectedItem.Detail = detail;
            this.selectedItem.Date = date;
            this.selectedItem = null;
        }
    }
}

  • 详细实现流程:
  1. 实现简单逻辑,完成控件的实现,分为MainPage和NewPage两个界面:        
         a)实现MainPage中MyLists列表内CheckBox和Line控件样式布局,并进行简单的绑定;

            

            XMAL代码如下:(此代码中实现了上述图片中的所有控件,注意数据绑定x:Bind的使用)

<CheckBox x:Name="checkBox" Grid.Column="0" IsChecked="{x:Bind Finish, Mode=TwoWay}" Checked="Checked" Unchecked="Unchecked" Margin="7 5 3 5" Height="32" Width="32"/>
<Image x:Name="image" Grid.Column="1" Source="{x:Bind Img, Mode=TwoWay}" Height="80" Width="80" />
<Line x:Name="line" Grid.Column="2" Stretch="Fill" Stroke="Black" X1="1" StrokeThickness="3" VerticalAlignment="Center" Opacity="0"/>
<TextBlock Grid.Column="2" Text="{x:Bind Title, Mode=TwoWay}" VerticalAlignment="Center" FontSize="20"/>
<AppBarButton Grid.Column="3" Icon="Setting"  VerticalAlignment="Center" Margin="0 23 0 10"  HorizontalAlignment="Left"  IsCompact="True">
    <AppBarButton.Flyout>
        <MenuFlyout>
            <MenuFlyoutItem Text="Edit" Click="MenuFlyoutEdit_Click"  />
            <MenuFlyoutItem Text="Delete" Click="MenuFlyoutDelete_Click"/>
        </MenuFlyout>
    </AppBarButton.Flyout>
</AppBarButton>

            c#代码如下:(下面的代码可以用一个函数实现,即CheckBox的Click函数)

private void Checked(object sender, RoutedEventArgs e)
{
    var parent = VisualTreeHelper.GetParent(sender as DependencyObject);
    Line line = VisualTreeHelper.GetChild(parent, 2) as Line;
    line.Opacity = 1;
}

private void Unchecked(object sender, RoutedEventArgs e)
{
     var parent = VisualTreeHelper.GetParent(sender as DependencyObject);
     Line line = VisualTreeHelper.GetChild(parent, 2) as Line;
     line.Opacity = 0;
}


     b)实现NewPage中Details列表的样式布局,并进行逻辑判断实现Create和Cancel功能(利用MessageDialog进行失败/成功创建的提示);

   

            xaml代码如下:(采用StackPanel和Grid两个布局容器来实现,在此我将自定义选择图片和放大缩小功能实现了,其中有部分数据绑定(需要使用Binding, x:Bind在此实现不了)的实现,Image的ScaleX,ScaleY与slider的Value绑定)

<StackPanel Height="Auto" HorizontalAlignment="Center">
    <Image x:Name="pic" Width="300" Source="Assets/pic3.ico" Stretch="Fill" Height="180" RenderTransformOrigin="0.5,0.5">
        <Image.RenderTransform>
            <CompositeTransform ScaleX="{Binding Value, ElementName=slider}" ScaleY="{Binding Value, ElementName=slider}"/>
        </Image.RenderTransform>
    </Image>
    <Slider Width="300" Minimum="0.4" Maximum="1.0" StepFrequency="0.001" x:Name="slider"/>
    <RelativePanel Grid.Row="2" Width="300" HorizontalAlignment="Center">
        <AppBarButton Icon="Pictures" Label="select" RelativePanel.AlignRightWithPanel="True" Click="GetPicture" />
    </RelativePanel>
    <TextBlock HorizontalAlignment="Left" Text="Title" TextWrapping="Wrap" VerticalAlignment="Bottom" Margin="1"/>
    <TextBox x:Name="title" HorizontalAlignment="Left" Text="" VerticalAlignment="Bottom" Width="300" Margin="5"/>
    <TextBlock HorizontalAlignment="Left" Text="Detail" TextWrapping="Wrap" VerticalAlignment="Bottom" Margin="1"/>
    <TextBox x:Name="detail" HorizontalAlignment="Left" Text="" VerticalAlignment="Bottom"  Width="300" Height="72" Margin="5"/>
    <TextBlock  HorizontalAlignment="Left" Text="Due Date" TextWrapping="Wrap" VerticalAlignment="Bottom" Margin="1"/>
    <DatePicker x:Name="datePicker" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="300" Margin="5"/>
</StackPanel>

<Grid Width="300">
    <Button x:Name="create_update" Click="Create_Update" Content="Create" HorizontalAlignment="Left" />
    <Button x:Name="cancel" Click="Cancel" Content="Cancel" HorizontalAlignment="Right" />
</Grid>

            c#代码如下:

private async void Create_Update(object sender, RoutedEventArgs e)
{
    string message = "";
    if (title.Text == "")
        message += "Title";
    if (detail.Text == "")
    {
        if (message != "")  message += ",Detail";
        else                message += "Detail";
    }
    if (message != "")      message += "不得为空\n";

    if (datePicker.Date < DateTimeOffset.Now.LocalDateTime.AddDays(-1))
        message += "时间不得小于当前日期";

    if (message != "")
        await new MessageDialog(message).ShowAsync();
    else if (create_update.Content.ToString() == "Create")
    {
        Frame rootFrame = Window.Current.Content as Frame;
        MainPage.ViewModel1.AddListItem(pic.Source, slider.Value, title.Text, detail.Text, datePicker.Date);
        rootFrame.Navigate(typeof(MainPage));
        await new MessageDialog("Create successfully!").ShowAsync();
    }
    else
    {
        Frame rootFrame = Window.Current.Content as Frame;
        MainPage.ViewModel1.UpdateListItem(pic.Source, slider.Value, title.Text, detail.Text, datePicker.Date);
        rootFrame.Navigate(typeof(MainPage));
        await new MessageDialog("Update successfully!").ShowAsync();
    }
}

private void Cancel(object sender, RoutedEventArgs e)
{
    pic.Source = Item.Img; //Item是中间变量,存储的是Cancel前的日程信息
    title.Text = Item.Title;
    slider.Value = Item.Size;
    detail.Text = Item.Detail;
    datePicker.Date = Item.Date;
}

private void GetPicture(object sender, RoutedEventArgs e)
{
    var getSelectPicture = new GetSelectPicture();
    getSelectPicture.selectPic(pic);
}

           上面的GetPicture函数中的GeiSelectPicture需要自己实现一个内部类:

internal class GetSelectPicture
{
    public async void selectPic(Image pic)
    {
        var fop = new FileOpenPicker();
        fop.ViewMode = PickerViewMode.Thumbnail;
        fop.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
        fop.FileTypeFilter.Add(".jpg");
        fop.FileTypeFilter.Add(".jpeg");
        fop.FileTypeFilter.Add(".png");
        fop.FileTypeFilter.Add(".gif");

        Windows.Storage.StorageFile file = await fop.PickSingleFileAsync();
        try
        {
            using (IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read))
            {
                BitmapImage bitmapImage = new BitmapImage();
                await bitmapImage.SetSourceAsync(fileStream);
                pic.Source = bitmapImage;
            }
        }
        catch (Exception)
        {
            return;
        }
    }
}

      2. 实现+(右下角新建按钮),<-(左上角back按钮)页面跳转功能;

        a) +(右下角新建按钮)功能实现:


            XMAL代码:

    <Page.BottomAppBar>
        <CommandBar>
            <AppBarButton x:Name="AddAppBarButton" Icon="Add" Label="Add" Click="AddBarButtonClick"/>
        </CommandBar>
    </Page.BottomAppBar>

            c#代码如下:

private void AddBarButtonClick(object sender, RoutedEventArgs e)
{
    if (right.Visibility.ToString() == "Collapsed")
    {
        Frame rootFrame = Window.Current.Content as Frame;
        rootFrame.Navigate(typeof(NewPage));
    }
    else
        right.Navigate(typeof(NewPage));
}

        b) <-(左上角back按钮)页面跳转功能实现:(写到App.xaml.cs里面,一下代码即使事件委托的使用,需要好好理解事件委托)

                                

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
    Frame rootFrame = Window.Current.Content as Frame;
    if (rootFrame == null)
    {
        rootFrame = new Frame();
        rootFrame.Navigated += OnNavigated;
        SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;

        rootFrame.NavigationFailed += OnNavigationFailed;

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            //TODO: 从之前挂起的应用程序加载状态
        }

        // 将框架放在当前窗口中
        Window.Current.Content = rootFrame;
    }

    if (e.PrelaunchActivated == false)
    {
        if (rootFrame.Content == null)
        {
            // 当导航堆栈尚未还原时,导航到第一页,
            // 并通过将所需信息作为导航参数传入来配置
            // 参数
            rootFrame.Navigate(typeof(MainPage), e.Arguments);
        }
        // 确保当前窗口处于活动状态
        Window.Current.Activate();
    }

}

private void OnBackRequested(object sender, Windows.UI.Core.BackRequestedEventArgs e)
{
    Frame rootFrame = Window.Current.Content as Frame;
    if (rootFrame == null) return;

    if (rootFrame.CanGoBack && e.Handled == false)
    {
        e.Handled = true;
        rootFrame.GoBack();
    }
}

private void OnNavigated(object sender, NavigationEventArgs e)
{
    SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = ((Frame)sender).CanGoBack ?
        AppViewBackButtonVisibility.Visible :
        AppViewBackButtonVisibility.Collapsed;
}

      3. UI自适应设计:

                  

            XAML代码如下:(利用VisualStateGroups、VisualState实现,使用Blend会更加方便;下方代码实现的是页面宽度在0-600px、600-800px和800px以上的视图变化)

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="VisualStateGroup">

        <VisualStateGroup.Transitions>
            <VisualTransition GeneratedDuration="0"/>
        </VisualStateGroup.Transitions>

        <VisualState x:Name="VisualStateMin0">
            <VisualState.Setters>
                <Setter Target="left.(Grid.ColumnSpan)" Value="2" />
                <Setter Target="right.(UIElement.Visibility)" Value="Collapsed"/>
            </VisualState.Setters>
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="1" />
            </VisualState.StateTriggers>
        </VisualState>

        <VisualState x:Name="VisualStateMin600">
            <VisualState.Setters>
                <Setter Target="left.(Grid.ColumnSpan)" Value="2" />
                <Setter Target="right.(UIElement.Visibility)" Value="Collapsed"/>
            </VisualState.Setters>
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="600" />
            </VisualState.StateTriggers>
        </VisualState>

        <VisualState x:Name="VisualStateMin800">
            <VisualState.Setters>
                <Setter Target="right.(UIElement.Visibility)" Value="Visible"/>
            </VisualState.Setters>
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="800" />
            </VisualState.StateTriggers>
        </VisualState>

    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

          注意到上述代码并没有实现Image在Width<600px消失的功能,那是因为,在设计中,日程ListItem被定义为了一个DateTemplate,如果要对DateTemplate内的元素进行视觉状态调整,需要在DateTemplate内书写VisualStateGroups,并且使用UserControl标签嵌套。代码如下:

<ListView Grid.Row="1" IsItemClickEnabled="True" ItemClick="ListView_ItemClick" ItemsSource="{x:Bind ViewModel.AllItems}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="md:ListItem">
            <UserControl>

                <Grid Height="100">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="42"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="100"/>
                    </Grid.ColumnDefinitions>

                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="VisualStateGroup">

                            <VisualState x:Name="VisualStateMin0">
                                <VisualState.Setters>
                                    <Setter Target="image.Visibility" Value="Collapsed" />
                                </VisualState.Setters>
                                <VisualState.StateTriggers>
                                    <AdaptiveTrigger MinWindowWidth="1" />
                                </VisualState.StateTriggers>
                            </VisualState>

                            <VisualState x:Name="VisualStateMin600">
                                <VisualState.Setters>
                                    <Setter Target="image.Visibility" Value="Visible" />
                                </VisualState.Setters>
                                <VisualState.StateTriggers>
                                    <AdaptiveTrigger MinWindowWidth="600" />
                                </VisualState.StateTriggers>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    …… //此处省略了ListItem控件的代码
                </Grid>
            </UserControl>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

      4. 齿轮功能和删除按钮实现:

                      

          a) MainPage下主要实现CheckBoxClick(check,uncheck功能已经实现)、ListView_ItemClick、AddBarButtonClick、MenuFlyEdit_Click、MenuFlyDelete_Click五个绑定事件Click的函数(此后的代码会会经常用到ViewModel视图模板)。

private void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
    ViewModel1.SelectedItem = e.ClickedItem as ListItem;
    if (right.Visibility.ToString() == "Collapsed")
    {
        Frame rootFrame = Window.Current.Content as Frame;
        rootFrame.Navigate(typeof(NewPage), e.ClickedItem as ListItem);
    }
    else
        right.Navigate(typeof(NewPage), e.ClickedItem as ListItem);
}

private void AddBarButtonClick(object sender, RoutedEventArgs e)
{
    if (right.Visibility.ToString() == "Collapsed")
    {
        Frame rootFrame = Window.Current.Content as Frame;
        rootFrame.Navigate(typeof(NewPage));
    }
    else
        right.Navigate(typeof(NewPage));
}

private void MenuFlyoutEdit_Click(object sender, RoutedEventArgs e) //齿轮Edit功能
{
    ViewModel1.SelectedItem = (sender as MenuFlyoutItem).DataContext as ListItem;  //DateContext获取当前元素的上下文
    if (right.Visibility.ToString() == "Collapsed")
    {
        Frame rootFrame = Window.Current.Content as Frame;
        rootFrame.Navigate(typeof(NewPage), ViewModel.SelectedItem);
    }
    else
        right.Navigate(typeof(NewPage), ViewModel.SelectedItem);
}

private async void MenuFlyoutDelete_Click(object sender, RoutedEventArgs e) //齿轮Delete功能
{
    ViewModel1.SelectedItem = (sender as MenuFlyoutItem).DataContext as ListItem;
    ViewModel1.RemoveListItem(ViewModel1.SelectedItem);
    await new MessageDialog("Delete successfully!").ShowAsync();
    Frame rootFrame = Window.Current.Content as Frame;
    rootFrame.Navigate(typeof(MainPage));
}

          b) NewPage 下主要实现Create_Update(之前已经实现)、Cancel、GetPicture(之前已经实现)、AddBarbuttonClick、DeleteBarButtonClick五个绑定函数的功能。

private void Cancel(object sender, RoutedEventArgs e)
{
    pic.Source = Item.Img;
    title.Text = Item.Title;
    slider.Value = Item.Size;
    detail.Text = Item.Detail;
    datePicker.Date = Item.Date;
}

private async void DeleteBarButtonClick(object sender, RoutedEventArgs e)
{
    DeleteAppBarButton.Visibility = Visibility.Collapsed;
    MainPage.ViewModel1.RemoveListItem(MainPage.ViewModel1.SelectedItem);
    await new MessageDialog("Delete successfully!").ShowAsync();
    Frame rootFrame = Window.Current.Content as Frame;
    rootFrame.Navigate(typeof(MainPage));
}

private void AddBarButtonClick(object sender, RoutedEventArgs e)
{
    if (SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility != AppViewBackButtonVisibility.Visible)
    {
        Frame rootFrame = Window.Current.Content as Frame;
        if (Window.Current.Bounds.Width >= 800)
            rootFrame.Navigate(typeof(MainPage));
        else
            rootFrame.Navigate(typeof(NewPage));
    }
}

       之所以两个cs文件中都有AddBarButtonClick函数的实现,是因为在程序启动之后,如果初始状态页面小于800px,则此时的AddAppBarButton为MainPage中实现的控件,点击AddAppBarButtonrootFrame导航的是NewPage,所以MainPage的AddAppBarButton已经不复存在AddAppBarButton就变为NewPage中实现的控件;当页面大于800px,则此时的AddAppBarButton控件为NewPage中实现的,NewPage中AddAppBarButton控件将MainPage中的覆盖了。

        删除按钮之所以不需要设置两个,是因为在任何可以点击删除按钮的时候,必定是你选中某一个日程的时候,这是Details页面一定会出现,即NewPage一定存在,所以只需要NewPage中有一个删除按钮即可。

        除此之外,每次点击相应的ListItem,Details界面显示的是选中的ListItem的详情,所以我们需要重载OnNavigatedTo函数来实现这个功能,(此函数是每次跳转到NewPage后都会执行的函数(相对应的每次离开页面执行的函数为OnNavigatedFrom):

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    if (e.Parameter != null)
    {
        Item = e.Parameter as ListItem;  //此处的Item即为前面所用到的中间量Item,用来记录当前ListItem信息

        DeleteAppBarButton.Visibility = Visibility.Visible;
        pic.Source = Item.Img;
        slider.Value = Item.Size;
        title.Text = Item.Title;
        detail.Text = Item.Detail;
        datePicker.Date = Item.Date;
        create_update.Content = "Update";
    }
}

      5. 优化调整页面之间的导航跳转:

        第二步中,我们已经实现了页面之间的跳转,但是你会发现,回到主页面的时候,有些情况下<-back回退按钮依然可以点击;还有+新建按钮也可以一直点击,造成页面越积越多,并且用户体验也会变差。

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Collapsed;
}

        如上所示,在MainPage.xaml.cs中重载OnNavigatedTo函数,在该函数中将<-回退按钮隐藏,这样用户就可以知道这是主界面。

private void AddBarButtonClick(object sender, RoutedEventArgs e)
{
    if (SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility != AppViewBackButtonVisibility.Visible)
    {
        Frame rootFrame = Window.Current.Content as Frame;
        if (Window.Current.Bounds.Width >= 800)
            rootFrame.Navigate(typeof(MainPage));
        else
            rootFrame.Navigate(typeof(NewPage));
    }
}
        上面的if条件句即可实现 + 新建按钮不可以重复点击。(原因前文也提到了。)

         实现这些函数需要理清楚每个函数所要处理的页面跳转,信息保留的关系,在此基础之上利用各种API完成这些内容。

        以上便是这个日程制作的全部内容,如果有哪些不当的地方,还请大家批评指正。另外,附上所写源码链接:点击打开链接

猜你喜欢

转载自blog.csdn.net/liuyh73/article/details/79767624