WPF的Application类

  每个运行的WPF应用程序都有System.Windows.Application类的一个实例表示。该类跟踪在应用程序中打开的所有窗口,决定何时关闭应用程序,并引发可以执行初始化和清除操作的应用程序事件。本节详细分析Application类。学习如果使用该类执行类似捕获未处理的错误、显示初始屏幕,以及检索命令行参数等任务。理解了Application类的基础结构后,将介绍如何创建并使用程序集资源(assembly resources)。每个资源是一块可以嵌入到编译过的应用程序中的二进制数据。

7.1 应用程序的生命周期

  在WPF中,应用程序会经历一个简单的生命周期。在应用程序启动后,将立即创建应用程序对象。在应用程序运行时,触发各种应用程序事件,可以选择监视其中的某些事件。当应用程序对象被释放时,应用程序结束。

7.1.1 创建Application对象

  使用Application类的最简单方式是手动创建它。如下:在应用程序入口(Main()方法)创建一个名为Window1的窗口,并启动一个新的应用程序。

using System;
using System.Windows;

namespace CreateWindow
{
    public class Startup
    {
        [STAThread()]
        static void Main()
        {
            Application app = new Application();
            Window win = new Window();
            win.Title = "Window1";
            app.Run(win);
        }
    }
}
View Code

当向Application.Run()方法传递一个窗口时,该窗口就被设置为主窗口,并且可以通过Application.MainWindow属性在应用程序中访问这个窗口。然后Run()触发Application.Startup事件并显示主窗口。可以使用更长更复杂的代码获得相同效果。

using System;
using System.Windows;

namespace CreateWindow
{
    public class Startup
    {
        [STAThread()]
        static void Main()
        {
            Application app = new Application();
            Window win = new Window();
            win.Title = "Window1";
            app.MainWindow = win;
            win.Show();
            app.Run();
        }
    }
}
View Code

这两种方法都给予了应用程序所需要的所有动力。当以这种方式开始后,应用程序继续运行,直到主窗口和所有其他窗口关闭为止。这时Run()方法返回,并在应用程序关闭之前,执行在Main()方法中的其他所有代码。

7.1.2 派生一个自定义的Application类

  虽然之前给出的方法可以创建一个Application对象,但是当创建一个新的WPF应用程序时,Visual Studio并不使用这一模式。反而,Visual Studio从Application类派生一个自定义类。在简单的应用程序中,这种方法没什么意义。但是,如果计划处理应用程序事件,那么这种方法就可以提供一个更整洁的模型,因为可以在派生自Application的类中放置所有事件处理代码。

  Visual Studio为Application类使用的模型,在本质上和用于窗口的模型相同。起点是一个XAML模板,默认情况下该模板被命名为App.xaml,他看起来如下所示:

<Application x:Class="TestApplication.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

在第2章中以介绍过,在XAML中使用Class特性创建一个派生自元素的类。因此,该类创建一个派生自Application的类,该类名为TestApplication.App(TestApplication是项目名称,也是在其中定义类的名称空间,App是Visual Studio为派生自Application的自定义类使用的名称)。Application标签不仅创建了一个自定义的应用程序类,还设置了StartupUri属性来确定代表主窗口的XAML文档。因此,不需要使用代码显示的实例化这个窗口,XAML解析器会自动完成这一工作。与窗口一样,应用程序类也是在两个独立的部分中进行定义,并且在编译时融合到一起。自动生成的部分在项目中是不可见的,但是该部分包含了Main()入口点以及启动应用程序的代码。该部分如下所示:

    public partial class App : System.Windows.Application {

        public void InitializeComponent() {
                        this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
        }
        

        public static void Main() {
            TestApplication.App app = new TestApplication.App();
            app.InitializeComponent();
            app.Run();
        }
    }
View Code

如果确实对查看XAML模板创建的自定义应用程序类感兴趣,尅查找位于项目目录中的obj\Debug文件中的App.g.cs文件。在这里给出的自动生成的代码和手工编写的自定义应用程序代码之间唯一的区别是,自动生成的类使用StartupUri属性,而不是设置MainWindow属性或把主窗口作为一个参数传递给Run()方法。只要使用相同的URI格式,就可以自由的使用这种方法创建自定义应用程序类。需要创建一个相对的Uri对象,用于命名项目中的XAML文档。自定义应用程序类的第二部分存储在项目中的一个和App.xaml.cs类似的文件中。该部分包含开发人员添加的处理事件的代码。它最初是空的:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;

namespace TestApplication
{
    public partial class App : Application
    {
    }
}
View Code

这个文件通过部分类技术和自动生成的应用程序代码融合到一起。

7.1.3 应用程序的关闭方式

  通常,只要有一个窗口还没有被关闭,Application类就保持应用程序处于有效状态。如果这不是所期望的行为,可以调整Application.ShutdownMode属性。如果手动实例化Application对象,就需要在调用Run()方法之前设置ShutdownMode属性,如果使用App.xaml文件,则可以在XAML文件中简单的设置ShutdownMode属性。下面是三种关闭模式:

OnLastWindowClose 这是默认行为——只有至少有一个窗口还存在,应用程序就保持运行状态。如果关闭了主窗口,Application.MainWindow属性仍然引用代表已关闭的窗口的对象。
OnMainWindowClose 这是传统的方式——只要主窗口还处于打开状态,应用程序就保持运行状态
OnExplicitShutdown 应用程序永远不结束(即使所有窗口都关闭),除非调用Application.Shutdown()方法。如果应用程序是长时间运行的后台任务的前端,或者只是希望使用更复杂的逻辑来决定应用程序应当何时关闭,使用这种方法可能会有意义。

例如,如果希望使用OnMainWindowClose方式,并且正在使用App.xaml文件,需要添加ShutdownMode属性

<Application x:Class="TestApplication.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml" ShutdownMode="OnMainWindowClose">
    <Application.Resources>
         
    </Application.Resources>
</Application>
View Code

不管选择哪种关闭方法,总是可以使用Application.Shutdown()方法立即终止应用程序。

7.1.4 应用程序事件

  最初,App.xaml.cs文件没有包含任何代码。尽管不需要代码,但是可以添加代码来处理应用程序事件。Application类提供了一些非常有用的为数不多的事件。如下给出了其中最重要的几个。

Startup 该事件在调用Application.Run()方法之后,并且在主窗口显示之前(如果把主窗口传递给Run()方法)发生。可以使用该事件检查所有命令行参数,命令行参数是通过StartupEventArg.Args水洗那个作为一个数组提供的。还可以使用该事件创建和显示主窗口(而不使用App.xaml文件中的StartUri属性)
Exit 该事件在应用程序关闭时,并在Run()方法即将返回之前发生。这时不能取消关闭,尽管可以通过代码在Main()方法中重新启动应用程序。可以使用Exit事件设置从Run()方法返回的整数类型的退出代码。
SessionEnding 该事件在Windows对话结束时发生——例如,当用户注销或关闭计算机时(可以通过检查SessionEndingCancelEventArgs.ReasonSessionEnding属性确定原因)。也可以通过将SessionEndingEventArgs.Cancel属性设置为true来取消关闭应用程序。否则,当事件处理程序结束时WPF将调用Application.Shutdown()方法。
Activated 当应用程序中的一个窗口被激活时发生该事件。当从另一个Windows程序切换到该应用程序时胡发生该事件。当第一次显示一个窗口时也会发生该事件
Deactivated 当应用程序中的一个窗口被取消激活时发生该事件。当切换到另一个Windows应用程序时会发生该事件
DispatcherUnhandledException 在应用程序(主应用程序线程)中的任何位置,只要发生一个未处理的异常,就会发生该事件(这个应用程序会捕获这些异常)。通过响应事件,可以记录重要错误,甚至可以选择不处理这些异常,并通过将DispatcherUnhandledExceptionExceptionEventArgs.Handled属性设置为true继续运行应用程序。只有当可以确保应用程序仍然处于合法状态并且可以继续运行时,才可以这样处理。

有两种选择用于处理事件:关联事件处理程序,或重写相应的受保护的方法。如果选择处理应采用程序事件,不需要使用委托来关联事件处理程序。而可以使用App.xaml文件中的一个特性来关联事件处理程序。如下:

        private void App_DispatcherUnhandledException(object sender,
            DispatcherUnhandledExceptionEventArgs e)
        {
            MessageBox.Show("An unhandled " + e.Exception.GetType().ToString() + 
                " exception was caught and ignored.");
            e.Handled = true;
        }
View Code

XAML类来接上面的事件处理程序:

<Application x:Class="PreventSessionEnd.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml" DispatcherUnhandledException="App_DispatcherUnhandledException"
    >
    <Application.Resources>
         
    </Application.Resources>
</Application>
View Code

对于每一个应用程序事件,可以调用相应的方法来引发该事件。这个方法的名称就是事件的名称,只是在前面加上了前缀On,因此Startup变成了OnStartup()。这种方式在.NET中非常普遍。唯一例外是DispatcherExceptionUnhandled事件——该事件没有相应的OnDispatcherExceptionUnhandled()方法,所以始终需要使用事件处理程序。如下的是一个自定义的应用程序类,它重写了OnSessionEnding方法,并且如果设置了相应的标志,该方法会阻止关闭系统和应用程序自身。

    public partial class App : Application
    {
        private bool unsavedData = false;
        public bool UnsavedData
        {
            get { return unsavedData; }
            set { unsavedData = value; }
        }
        
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            UnsavedData = true;
        }

        protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
        {
            base.OnSessionEnding(e);

            if (UnsavedData)
            {
                e.Cancel = true;
                MessageBox.Show("The application attempted to close as a result of " +
                    e.ReasonSessionEnding.ToString() +
                    ". This is not allowed, as you have unsaved data.");
            }
        }
}
View Code

当重写应用程序方法时,首先调用基类的实现是一个好主意。通常,基类的实现只是引发相应的应用程序事件。

7.2 Application类的任务

7.2.1 显示初始界面

  WPF应用程序运行速度很快,但不可能瞬间启动。当第一次启动应用程序时,会有一些延迟,因为公共语言运行时(common language runtim, CLR)首先需要初始化.NET环境,然后启动应用程序。如果具有更加耗时的初始化步骤,或者如果只是希望通过显示一个打开的图形使应用程序显得更加专业,这时可以使用WPF提供的简单初始界面特性。

  1. 为项目添加一个图像文件(通常是.bmp、.png、.jpg文件)
  2. 在Solution Explorer中选择该文件
  3. 将Build Action 修改为SplashScreen

下次运行应用程序时,该图形会立即出现在屏幕中央显示出来。一旦准备好了运行时环境,并且Application_Startup方法执行完毕,应用程序的第一个窗口显示出来,这时初始界面图形很快会逐渐消失。

7.2.2 处理命令行参数

  为了处理命令行参数,需要响应Application.Startup事件。命令行参数是通过StartupEventArgs属性作为字符串数组提供的。例如,假定希望加载一个文档,文档的名称作为命令行参数传递。在这种情况下,就有必要读取命令行参数并进行所需要的一些额外的初始化操作。如下示例中,通过响应Application.Startup事件实现了这一模式。在该示例中没有设置Application.StartupUrl属性,而是使用代码实例化主窗口:

    public partial class App : Application
    {
        private void App_Startup(object sender, StartupEventArgs e)
        {           
            FileViewer win = new FileViewer();

            if (e.Args.Length > 0)
            {
                string file = e.Args[0];
                if (File.Exists(file))
                {                 
                    win.LoadFile(file);
                }
            }
            win.Show();
        }
    }
View Code

上面的方法初始化主窗口,然后当APP_Startup()方法结束时显示主窗口。上面的代码假定FileViewer类有一个名为LoadFile()的共有方法(自己添加的)。这只是一个示例,他简单的读取指定文件的文本:

    public partial class FileViewer : Window
    {

        public FileViewer()
        {
            InitializeComponent();
        }

        public void LoadFile(string path)
        {
            this.Content = File.ReadAllText(path);  
            this.Title = path;
        }
    }
View Code

7.2.3 访问当前Application对象

  通过静态的Application.Current属性,可以在应用程序的任何位置获取当前应用程序实例。从而可以在窗口之间进行基本的交互,因为任何窗口都可以访问当前Application对象,并且通过Application对象还可以获取主窗口的引用。

Window main=Application.Current.MainWindow;
MessageBox.Show("The main window is"+main.Title);

当然,如果希望访问在自定义的主窗口类中添加的任何方法、属性或事件,需要将窗口对象转换为正确的类型。如果主窗口是一个自定义的MainWindow类的实例,可以使用与下面类似的代码:

MainWindow main=(MainWindow)Application.Current.MainWindow;
main.DoSomething();

在一个窗口中还可以检查Application.Windows集合的内容,这个集合提供了所有当前打开窗口的引用:

foreach(Window window in Application.Current.Windows)
{
    MessageBox.Show(window.Title + "is open.");
}

大多数应用程序通常使用一种更加结构化的方式在窗口之间进行交互,如果有几个长时间运行的窗口同时打开,并且它们之间需要以某种方式进行通信,在一个自定义的应用程序类中保存这些窗口的引用可能更有意义。通过这种方式总是可以找到所需的窗口。类似的,如果有一个基于文档的应用程序,则可以选择创建一个跟踪文档窗口的集合,而不跟踪其他内容。

7.2.4 在窗口之间进行交互

  自定义的应用程序类是放置相应不同应用程序事件的代码,还可以在应用程序类中添加代码完成其他任务:保存重要窗口的引用,从而是一个窗口可以访问列一个窗口。例如,假设希望跟踪应用程序使用的所有文档窗口。为了达到这一目的,可以在自定义的应用程序类中创建一个专门的集合。下面是一个使用泛型列表集合保存一组自定义窗口对象的示例。在此示例中,每个文档窗口中由一个名为Document的类的实例表示:

    public partial class App : Application
    {
        private List<Document> documents = new List<Document>();
        
        public List<Document> Documents
        {
            get { return documents; }
            set { documents = value; }
        }
    }
View Code

现在,当创建一个新文档时,只需要记住把它添加到Documents集合中即可,下面是响应一个按钮单击事件的事件处理程序,并且该事件处理程序完成了所需要的工作:

        private void cmdCreate_Click(object sender, RoutedEventArgs e)
        {
            Document doc = new Document();
            doc.Owner = this;
            doc.Show();
            ((App)Application.Current).Documents.Add(doc);
        }
View Code

同样,也可以在Document类中响应Window.Loaded这类事件,以确保当创建文档对象时,总会在Document集合中注册该文档对象。现在可以在代码中的其他任何地方使用集合来遍历所有文档,并且使用公有成员。在这个示例中,Document类包含了一个自定义的用于更新显示的SetContent()方法:

        private void cmdUpdate_Click(object sender, RoutedEventArgs e)
        {
            foreach (Document doc in ((App)Application.Current).Documents)
            {
                doc.SetContent("Refreshed at " + DateTime.Now.ToLongTimeString() + ".");
            }            
        }
View Code

上述示例交互方式是值得注意的,它演示了一种通过自定义应用程序类在窗口之间进行交互的安全的、规范的方式。这种方式比使用Windows属性药好一些,因为它是强类型,并且只包含Document窗口(而不是包含应用程序中所有窗口的集合)。通过这种方式还可以使用另一种方式对窗口进行分类,例如,可以使用另一种更有用的方式——使用字典集合,从而可以通过键名更方便的查找文档。在基于文档的应用程序中,可以通过文件名来索引集合中的窗口。

7.2.5 单实例应用程序

  通常,只要愿意就可以加载WPF应用程序的任意多个副本。对于单实例应用程序,WPF本身并未提供决绝方法,但是可以使用几种变通的方法。基本的技术是当触发Application.Startup事件时,检查是否另一个应用程序实例已经在运行。最简单的方法是使用一个全局的mutex对象(mutex对象是操作系统提供的一个用于进程间通信的同步对象)。这种方法很简单但是功能有限——最重要的是,应用程序的新实例无法和已经存在的实例进行通信。对于基于文档的应用程序而言这确实是一个问题,因为新实例可能需要告诉已经存在的应用程序进行通信。

1创建单实例应用程序包装器

  使用这种方法的第一步是添加对Microsoft.VisualBasic.dll程序集的引用,并从Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase类继承一个自定义类。该类提供了三个用于管理示例的重要成员:

  • IsSingleInstance属性允许启用单实例应用程序。在构造函数中将该属性设置为true。
  • 当应用程序启动时触发的OnStartup()方法。此时重写该方法并创建WPF应用程序对象
  • 当另一个应用程序实例启动时触发OnStartupNextInstance()方法。这个方法提供了访问命令行参数的功能。此时,可以调用WPF应用程序类中的方法来显示一个新窗口,但不创建另一个应用程序对象。

下面是派生自WindowsFormsApplicationBase的自定义类的代码:

    public class SingleInstanceApplicationWrapper : 
        Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase
    {        
        public SingleInstanceApplicationWrapper()
        {
            this.IsSingleInstance = true;
        }

        private WpfApp app;
        protected override bool OnStartup(
            Microsoft.VisualBasic.ApplicationServices.StartupEventArgs e)
        {            
            string extension = ".testDoc";
            string title = "SingleInstanceApplication";
            string extensionDescription = "A Test Document";

            app = new WpfApp();
            app.Run();

            return false;
        }

        protected override void OnStartupNextInstance(
            Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs e)
        {
            if (e.CommandLine.Count > 0)
            {                
                app.ShowDocument(e.CommandLine[0]);
            }
        }
    }
View Code

当应用程序启动时,该类创建WpfApp类的一个实例,WpfApp是一个自定义的WPF应用程序类(派生自Application类)。WpfApp类包含一些启动逻辑,包括显示主窗口、使用自定义的ShowDocument()窗口为指定的文件加载文档窗口。每次文件名称通过命令行参数传给SingleInstanceApplicationWrapper类时,SingleInstanceApplicationWrapper类调用WpfApp.ShowDocument()方法。下面是WpfApp类的代码:

    public class WpfApp : System.Windows.Application
    {
        protected override void OnStartup(System.Windows.StartupEventArgs e)
        {
            base.OnStartup(e);
            
            // Load the main window.
            DocumentList list = new DocumentList();
            this.MainWindow = list;
            list.Show();

            // Load the document that was specified as an argument.
            if (e.Args.Length > 0) ShowDocument(e.Args[0]);
        }

        // An ObservableCollection is a List that provides notification
        // when items are added, deleted, or removed. It's preferred for data binding.
        private ObservableCollection<DocumentReference> documents = 
            new ObservableCollection<DocumentReference>();
        public ObservableCollection<DocumentReference> Documents
        {
            get { return documents; }
            set { documents = value; }
        }        

        public void ShowDocument(string filename)
        {
            try
            {                
                Document doc = new Document();
                DocumentReference docRef = new DocumentReference(doc, filename);
                doc.LoadFile(docRef);                
                doc.Owner = this.MainWindow;
                doc.Show();
                doc.Activate();
                Documents.Add(docRef);
            }
            catch
            {
                MessageBox.Show("Could not load document.");
            }
        }
    }   
View Code

现在唯一没有提到的细节是应用程序的入口,因为应用程序需要在App类之间创建SingleInstanceApplicationWrapper类,应用程序需要使用传统的Main()方法来启动,而不能使用App.xaml文件。如下代码

    public class Startup
    {
        [STAThread]
        public static void Main(string[] args)
        {            
            SingleInstanceApplicationWrapper wrapper = new SingleInstanceApplicationWrapper();
            wrapper.Run(args);           
        }
    }

 这三个类——SingleInstanceApplicationWraper类、WpfApp类和Startup类构成了单实例WPF应用程序的基础。使用这一基本框架模型,可以创建出更加完善的示例。如上示例。

2 注册文件类型

  为了测试单实例应用程序,需要使用Windows注册文件扩展名(.testDoc),并将其与应用程序向关联。从而,单击一个.testDoc文件时,应用程序将立即启动。创建文件类型注册的一种方法是,使用Windows资源管理器手动注册:

  1. 右击一个.testDoc文件,并选择打开方式选项
  2. 在打开方式中浏览打开方式,找到应用程序的.exe文件,并双击它。
  3. 如果不希望使用应用程序默认处理这种文件类型,在Open With对话框中确保不选中默认打开方式选项。这种情况下,不能通过双击启动应用程序。

创建文件类型注册的另一种方法是运行一些编辑注册表的代码。以下代码可以实现这一功能:

            string extension = ".testDoc";
            string title = "SingleInstanceApplication";
            string extensionDescription = "A Test Document";
            // 取消注释该行创建的文件登记
            // 在Windows Vista中,你需要以管理员身份运行该应用程序
            FileRegistrationHelper.SetFileAssociation(
              extension, title + "." + extensionDescription);
View Code

FileRegistrationHelper类使用Microsoft.Win32名称空间中的类注册.testDoc文件扩展名。

7.3 程序及资源

  WPF应用程序中程序集资源和其他.NET应用程序中的程序及资源在本质上是相同的。基本概念是为项目添加一个文件,从而Visual Studio可以将其嵌入到编译过的应用程序的Exe或DLL文件中。WPF程序集资源和其他应用程序中的程序集资源之间的重要区别是引用它们的寻址系统不同。

7.3.1 添加资源

  可以通过向项目添加文件,并将其生成操作属性设置为资源,来添加自己的资源。这就是需要全部完成的全部工作。为了能够成功的使用程序及资源,需要注意一下两点:

  • 不能将生成操作属性错误的设置为嵌入资源。尽管所有的程序集都被定义为嵌入的资源,但是嵌入资源生成操作会在另一个更难访问的位置放置二进制数据。在WPF应用程序中,假定总是使用资源生成类型。
  • 不要在项目属性窗口中使用资源选项卡。WPF不支持这种类型的资源URI。

7.3.2 检索资源

  有多种方法可以使用资源。低级的方法是检索包装数据的StreamResourceInfo对象,然后再决定如何使用该对象。可以通过代码,使用静态的Application.GetResourceStream()方法,完成该工作。例如,下面的代码为winter.jpg图像获取StreamResourceInfo对象:

StreamResourceInfo img.Source = Application.GetResourceStream(new Uri("images/winter.jpg", UriKind.Relative));

一旦得到了StreamResourceInfo对象,就可以得到两部分信息。ContentType属性返回一个描述数据类型的字符串——在该示例中是imgage/jpg。Stream属性返回一个UnmanagedMemoryStream对象,可以使用该对象读取数据,一次读取一个字节。GetResourceStream方法封装了ResourceManager类和ResourceSet类。

  虽然有GetResourceStream方法的帮助,但是直接检索资源可能还是会遇到麻烦。问题是使用该方法得到的是相对低级的UnmanagedMemoryStream对象,该对象本身没有什么用处,需要将它转换成一些更有意义的数据,例如,具有属性和方法的高级对象。WPF提供了几个专门使用资源的类。这些类不要求提取资源,他们使用资源的名称访问资源。例如,使用Image标签显示图片,也可以使用代码完成相同的工作。对于Image元素,只需要将Source属性设置为BitmapImage对象,该对象使用URI确定希望显示的图像的位置。可以像下面这样指定一个完全限定的文件路径:

img.Source=new BitmapImage(new Uri(@d:\Photo\Backgrounds\arch.jpj"));

但是如果使用相对URI,就可以从程序集中提取不同的资源,并将它传递给图像,而且不需要使用UnmanagedMemoryStream对象:

img.Source = new BitmapImage(new Uri("images/winter.jpg", UriKind.Relative));

该技术通过在基本应用程序URI的末尾上加上images/winter.jpg构造了一个URI。大多数情况下,不需要考虑URI语法——只需要遵循相对URI,剩下的工作就交给程序集负责。

 

posted on 2013-11-17 21:51  松竹柏柳  阅读(1017)  评论(0编辑  收藏  举报

导航