【问题标题】:getting winform treeview into wpf treeview将 winform 树视图转换为 wpf 树视图
【发布时间】:2015-10-16 20:41:44
【问题描述】:

我已经构建了一个在 winforms 中生成树视图的函数。它包括带有递归的子文件夹和文件。现在我想把它翻译成 wpf。

我无法弄清楚如何处理这些课程。我知道我必须创建自己的自定义类“treenode”,它具有类似于 winforms treenode 的属性“parent”。

但是在 wpf 中,我需要两种不同类型的树节点,以便我可以按数据类型正确绑定 wpf。我在 wpf 中有一个使用家庭的工作示例,我只是不确定如何将我的 winform 版本翻译成 wpf。有人可以帮我让我的 winform 版本在 wpf 中工作吗?

然后最终目标是让我在 WPF 中的树视图使用我的 winforms 示例中看到的目录和文件进行填充。然而,WPF 版本的样式应该保持文件和文件夹的“图标”显示。

我希望有人可以帮助我使其正常工作。欢迎任何建议和cmets。


ViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Linq;

namespace WpfApplication1
{
    public class ViewModel : ObservableObject
    {
        // Properties
        private ObservableCollection<DirectoryNode> directoryNodes;
        public ObservableCollection<DirectoryNode> DirectoryNodes
        {
            get { return directoryNodes; }
            set
            {
                directoryNodes = value;
                NotifyPropertyChanged("DirectoryNodes");
            }
        }

        private ObservableCollection<string> formats;
        public ObservableCollection<string> Formats
        {
            get { return formats; }
            set
            {
                formats = value;
                NotifyPropertyChanged("Formats");
            }
        }

        private ObservableCollection<string> directories;
        public ObservableCollection<string> Directories
        {
            get { return directories; }
            set
            {
                directories = value;
                NotifyPropertyChanged("Directories");
            }
        }

        // Creating data for testings
        public ViewModel()
        {
            Formats = new ObservableCollection<string>();
            Directories = new ObservableCollection<string>();
            DirectoryNodes = new ObservableCollection<DirectoryNode>();

            // create some dummy test data, eventually will be push to GUI
            Formats.Add(".txt");
            Formats.Add(".png");
            Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE"));

            PopulateTree(Directories);
        }

        // Functions
        static bool IsValidFileFormat(string filename, ObservableCollection<string> formats)
        {
            if (formats.Count == 0) return true;

            string ext = Path.GetExtension(filename);
            bool results = formats.Any(fileType => fileType.Equals(ext, StringComparison.OrdinalIgnoreCase));
            return results;
        }

        public static DirectoryNode CreateDirectoryNode(DirectoryInfo directoryInfo)
        {
            DirectoryNode directoryNode = new DirectoryNode(){Filename=directoryInfo.Name};

            foreach (var directory in directoryInfo.GetDirectories())
            {
                try
                {
                    directoryNode.Children.Add(CreateDirectoryNode(directory));
                }
                catch (UnauthorizedAccessException) { }
            }
            foreach (var file in directoryInfo.GetFiles())
            {
                if (IsValidFileFormat(file.FullName, Formats))
                {
                    FileNode node = new FileNode() { Filename = file.FullName };
                    directoryNode.Children.Add(node);
                }
            }

            return directoryNode;
        }

        public void PopulateTree(ObservableCollection<string> directories)
        {
            foreach (string directoryPath in directories)
            {
                if (Directory.Exists(directoryPath))
                {
                    DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
                    DirectoryNodes.Add(CreateDirectoryNode(directoryInfo));
                }
            }
        }
    }

    public class FileNode
    {
        public string Filepath { get; set; }
        public string Filename { get; set; }
        public DirectoryNode Parent { get; set; }
    }

    public class DirectoryNode
    {
        public string Filepath { get; set; }
        public string Filename { get; set; }
        public DirectoryNode Parent { get; set; }
        public ObservableCollection<FileNode> Children { get; set; }
    }

    public class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

MainWindow.Xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:self="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="300"
        WindowStartupLocation="CenterScreen">

    <Window.DataContext>
        <self:ViewModel/>
    </Window.DataContext>

    <Grid Margin="5">        
        <TreeView ItemsSource="{Binding Directories}" Grid.Row="1" Grid.ColumnSpan="2">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type self:DirectoryNode}" ItemsSource="{Binding Children}">
                    <StackPanel Orientation="Horizontal">
                        <Label VerticalAlignment="Center" FontFamily="WingDings" Content="1"/>
                        <TextBlock Text="{Binding Filename}" />
                    </StackPanel>
                </HierarchicalDataTemplate>
                <DataTemplate DataType="{x:Type self:FileNode}">
                    <StackPanel Orientation="Horizontal">
                        <Label VerticalAlignment="Center" FontFamily="WingDings" Content="2"/>
                        <TextBlock Text="{Binding Filename}" />
                    </StackPanel>
                </DataTemplate>
            </TreeView.Resources>
        </TreeView>
    </Grid>

</Window>

工作 Winforms 示例

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Linq;


namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public static List<string> formats = new List<string>();

        public Form1()
        {
            InitializeComponent();

            //add userfolder
            List<string> Directories = new List<string>();
            Directories.Add(System.Environment.GetEnvironmentVariable("USERPROFILE"));

            // get formats accepted
            formats.Add(".txt");
            formats.Add(".png");

            PopulateTree(Directories, formats);
        }

        static bool IsValidFileFormat(string filename, List<string> formats)
        {
            if (formats.Count == 0) return true;

            string ext = Path.GetExtension(filename);
            bool results = formats.Any(fileType => fileType.Equals(ext, StringComparison.OrdinalIgnoreCase));
            return results;
        }

        public static TreeNode CreateDirectoryNode(DirectoryInfo directoryInfo)
        {
            TreeNode directoryNode = new TreeNode(directoryInfo.Name);

            foreach (var directory in directoryInfo.GetDirectories())
            {
                try
                {
                    directoryNode.Nodes.Add(CreateDirectoryNode(directory));
                }
                catch (UnauthorizedAccessException) { }
            }
            foreach (var file in directoryInfo.GetFiles())
            {
                if (IsValidFileFormat(file.FullName, formats))
                {
                    TreeNode node = new TreeNode(file.FullName);
                    node.ForeColor = Color.Red;
                    directoryNode.Nodes.Add(node);
                }
            }

            return directoryNode;
        }

        public void PopulateTree(List<string> directories, List<string> formats)
        {
            // main collection of nodes which are used to populate treeview
            List<TreeNode> treeNodes = new List<TreeNode>();

            foreach (string directoryPath in directories)
            {
                if (Directory.Exists(directoryPath))
                {
                    DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
                    treeNodes.Add(CreateDirectoryNode(directoryInfo));
                }
            }

            treeView1.Nodes.AddRange(treeNodes.ToArray());
        }
    }
}

【问题讨论】:

  • WPF 与 winform 有着根本的不同。在 WPF 中,您使用 data 进行操作,而不是无休止地修改 UI 元素并希望它能够以某种方式工作,这就是您在 winforms 中所做的。
  • 只需将“家庭”相关数据更改为您想要的任何内容,并调整 XAML 以使用新属性。
  • 添加了更多关于翻译成 wpf 的具体问题,见上文
  • would these classes work alright - 是的。如果需要,只有那个 DirectoryItem 应该有一个 ObservableCollection&lt;FileItem&gt; 以便在 TreeView 中显示目录中的文件。
  • 好的,谢谢,如果你不介意,因为我会进行调整,所以我会更新帖子,希望你们能指导我一些方法,我想我会用 sn 来解决大部分问题-ps 的帮助。谢谢

标签: c# wpf treeview


【解决方案1】:

看你的例子,我不确定到底发生了什么。您可以查看您的输出,看看问题是否源于在运行时未找到绑定。

不过,我建议您将逻辑进一步拆分,将其中的一部分移入您的模型中。我还建议您将模型隐藏在界面后面。这允许您的视图模型保存单个集合,而视图根据类型呈现该集合的内容。您当前的实现仅限于将文件显示为目录的子目录,而不是目录 文件。下面是一个适合您的工作示例。

模型

INode

创建INode 接口将允许您为要呈现到树视图的每个内容项创建不同的实现。

namespace DirectoryTree
{
    public interface INode
    {
        string Name { get; }
        string Path { get; }
    }
}

我们的INode 只需要两个属性。一个代表节点的名称(通常是文件夹或文件名),另一个代表它所代表的文件夹或文件的完整路径。

目录节点

这是我们所有节点的根节点。在大多数情况下,所有其他节点都将通过父子关系与DirectoryNode 关联。 DirectoryNode 将负责构建自己的子节点集合。这会将逻辑移动到模型中,在那里它可以验证自身并创建 EmptyFolderNodes 或根据需要生成 FileNodes 的集合。这稍微清理了视图模型,因此它所要做的就是促进与视图本身的交互。

DirectoryNode 将实现INotifyPropertyChange,以便我们可以将属性更改事件引发到数据绑定到我们的任何内容。这将仅由此模型上的 Children 属性决定。其余属性将是只读的。

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;

namespace DirectoryTree
{
    public class DirectoryNode : INode, INotifyPropertyChanged
    {
        private ObservableCollection<INode> children;

        public DirectoryNode(DirectoryInfo directoryInfo)
        {
            this.Directory = directoryInfo;
            this.Children = new ObservableCollection<INode>();
        }

        public DirectoryNode(DirectoryInfo directoryInfo, DirectoryNode parent) : this(directoryInfo)
        {
            this.Parent = parent;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Gets the name of the folder associated with this node.
        /// </summary>
        public string Name
        {
            get
            {
                return this.Directory == null ? string.Empty : this.Directory.Name;
            }
        }

        /// <summary>
        /// Gets the path to the directory associated with this node.
        /// </summary>
        public string Path
        {
            get
            {
                return this.Directory == null ? string.Empty : this.Directory.FullName;
            }
        }

        /// <summary>
        /// Gets the parent directory for this node.
        /// </summary>
        public DirectoryNode Parent { get; private set; }

        /// <summary>
        /// Gets the directory that this node represents.
        /// </summary>
        public DirectoryInfo Directory { get; private set; }

        /// <summary>
        /// Gets or sets the children nodes that this directory node can have.
        /// </summary>
        public ObservableCollection<INode> Children
        {
            get
            {
                return this.children;
            }

            set
            {
                this.children = value;
                this.OnPropertyChanged();
            }
        }

        /// <summary>
        /// Scans the current directory and creates a new collection of children nodes.
        /// The Children nodes collection can be filled with EmptyFolderNode, FileNode or DirectoryNode instances.
        /// The Children collection will always have at least 1 element within it.
        /// </summary>
        public void BuildChildrenNodes()
        {
            // Get all of the folders and files in our current directory.
            FileInfo[] filesInDirectory = this.Directory.GetFiles();
            DirectoryInfo[] directoriesWithinDirectory = this.Directory.GetDirectories();

            // Convert the folders and files into Directory and File nodes and add them to a temporary collection.
            var childrenNodes = new List<INode>();
            childrenNodes.AddRange(directoriesWithinDirectory.Select(dir => new DirectoryNode(dir, this)));
            childrenNodes.AddRange(filesInDirectory.Select(file => new FileNode(this, file)));

            if (childrenNodes.Count == 0)
            {
                // If there are no children directories or files, we setup the Children collection to hold
                // a single node that represents an empty directory.
                this.Children = new ObservableCollection<INode>(new List<INode> { new EmptyFolderNode(this) });
            }
            else
            {
                // We fill our Children collection with the folder and file nodes we previously created above.
                this.Children = new ObservableCollection<INode>(childrenNodes);
            }
        }

        private void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var handler = this.PropertyChanged;
            if (handler == null)
            {
                return;
            }

            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

这里有几件事需要注意。一是模型将始终被引用到它表示为节点的DirectoryInfo。接下来,可以选择给它一个父DirectoryNode。这让我们可以轻松地在我们的模型中支持向前导航(通过Children 属性)和向后导航(通过Parent 属性)。当我们将子 DirectoryInfo 项目的集合转换为 DirectoryNode 项目的集合时,我们将自己传递给每个子 DirectoryNode,因此它可以在需要时访问其父项。

Children 集合是INode 模型的集合。这意味着DirectoryNode 可以容纳各种不同类型的节点,并且可以轻松扩展以支持更多。您只需要更新BuildChildrenNodes 方法即可。

EmptyFolderNode

我们将实现的最简单的节点是一个空文件夹节点。如果您双击一个文件夹,并且没有任何内容,我们将向用户显示一个节点,让他们知道它是空的。此节点将具有预定义的Name,并且将始终属于父目录。

namespace DirectoryTree
{
    public class EmptyFolderNode : INode
    {
        public EmptyFolderNode(DirectoryNode parent)
        {
            this.Parent = parent;
            this.Name = "Empty.";
        }

        public string Name { get; private set; }

        public string Path
        {
            get
            {
                return this.Parent == null ? string.Empty : this.Parent.Path;
            }
        }

        public DirectoryNode Parent { get; private set; }
    }
}

这里没有太多内容,我们将名称指定为“Empty”并将我们的路径默认为父级。

文件节点

我们需要构建的最后一个模型是FileNode。这个节点代表我们层次结构中的一个文件,需要给它一个DirectoryNode。它还需要此节点所代表的FileInfo

using System.IO;

namespace DirectoryTree
{
    public class FileNode : INode
    {
        public FileNode(DirectoryNode parent, FileInfo file)
        {
            this.File = file;
            this.Parent = parent;
        }

        /// <summary>
        /// Gets the parent of this node.
        /// </summary>
        public DirectoryNode Parent { get; private set; }

        /// <summary>
        /// Gets the file this node represents.
        /// </summary>
        public FileInfo File { get; private set; }

        /// <summary>
        /// Gets the filename for the file associated with this node.
        /// </summary>
        public string Name
        {
            get
            {
                return this.File == null ? string.Empty : this.File.Name;
            }
        }

        /// <summary>
        /// Gets the path to the file that this node represents.
        /// </summary>
        public string Path
        {
            get
            {
                return this.File == null ? string.Empty : this.File.FullName;
            }
        }
    }
}

此时此模型的内容应该是不言自明的,所以我不会花任何时间。

视图模型

现在我们已经定义了模型,我们可以设置视图模型来与它们交互。视图模型需要实现两个接口。第一个是INotifyPropertyChanged,以便我们可以向视图触发属性更改通知。第二个是ICommand,以便视图可以告诉视图模型何时需要加载更多目录或文件。我建议将ICommand 的内容抽象到一个可以重用的单独类中,或者使用现有的库,如PrismMVVMLight,它们都有可以使用的命令对象。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace DirectoryTree
{
    public class MainWindowViewModel : INotifyPropertyChanged, ICommand
    {
        private IEnumerable<INode> rootNodes;

        private INode selectedNode;

        public MainWindowViewModel()
        {
            // We default the app to the Program Files directory as the root.
            string programFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

            // Convert our Program Files path string into a DirectoryInfo, and create our initial DirectoryNode.
            var rootDirectoryInfo = new DirectoryInfo(programFilesPath);
            var rootDirectory = new DirectoryNode(rootDirectoryInfo);

            // Tell our root node to build it's children collection.
            rootDirectory.BuildChildrenNodes();

            this.RootNodes = rootDirectory.Children;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler CanExecuteChanged;

        public IEnumerable<INode> RootNodes
        {
            get
            {
                return this.rootNodes;
            }

            set
            {
                this.rootNodes = value;
                this.OnPropertyChanged();
            }
        }

        public bool CanExecute(object parameter)
        {
            // Only execute our command if we are given a selected item.
            return parameter != null;
        }

        public void Execute(object parameter)
        {
            // Try to cast to a directory node. If it returns null then we are
            // either a FileNode or an EmptyFolderNode. Neither of which we need to react to.
            DirectoryNode currentDirectory = parameter as DirectoryNode;
            if (currentDirectory == null)
            {
                return;
            }

            // If the current directory has children, then the view is collapsing it.
            // In this scenario, we clear the children out so we don't progressively
            // consume system resources and never let go.
            if (currentDirectory.Children.Count > 0)
            {
                currentDirectory.Children.Clear();
                return;
            }

            // If the current directory does not have children, then we build that collection.
            currentDirectory.BuildChildrenNodes();
        }

        private void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var handler = this.PropertyChanged;
            if (handler == null)
            {
                return;
            }

            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

视图模型有一个RootNodes 的集合。这是视图将绑定到的INode 实例的初始集合。此初始集合将包含 Program Files 目录中的所有文件和文件夹。

当用户双击视图中的TreeViewItem 时,Execute 方法将触发。此方法将清除所选目录的子集合,或构建子集合。这样,当用户折叠视图中的文件夹时,我们自己清理并清空集合。这也意味着集合将始终在打开/关闭目录时刷新。

视图

The 是最复杂的项目,但你一看就会发现它相当简单。就像您的示例一样,每种节点类型都有模板。在我们的例子中,Treeview 数据绑定到我们的视图模型INode 集合。然后,我们为INode 接口的每个实现都有一个模板。

<Window x:Class="DirectoryTree.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DirectoryTree"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        Title="MainWindow" Height="350" Width="525">

    <!-- Assign a view model to the window. -->
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <DockPanel>
        <TreeView x:Name="FileExplorerTreeview" 
                  ItemsSource="{Binding Path=RootNodes}">

            <!--    We use an interaction trigger to map the MouseDoubleClick event onto our view model.
                    Since the view model implements ICommand, we can just bind directly to the view model. 

                    This requires that you add the System.Windows.Interactivity.dll assembly to your project references.
                    You also must add the i: namespace to your XAML window, as shown above..
            -->
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseDoubleClick">
                    <!--    When the user double clicks on a folder, we will send the selected item into the view models Execute method as a argument.
                            The view model can then react to wether or not it's a DirectoryNode or a FileNode.
                    -->
                    <i:InvokeCommandAction Command="{Binding }" CommandParameter="{Binding ElementName=FileExplorerTreeview, Path=SelectedItem}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>

            <TreeView.Resources>
                <!--    This template represents a DirectoryNode. This template databinds itself to the Children property on the DirectoryNode
                        so we can have nested folders and files as needed. 
                -->
                <HierarchicalDataTemplate DataType="{x:Type local:DirectoryNode}"
                                          ItemsSource="{Binding Path=Children}">
                    <StackPanel Orientation="Horizontal">
                        <Label Content="1"
                               FontFamily="WingDings"
                               FontWeight="Black" />
                        <!-- Need to replace w/ an image of a folder -->
                        <TextBlock Text="{Binding Path=Name}" />
                    </StackPanel>
                </HierarchicalDataTemplate>

                <!-- This template represents a FileNode. Since FileNodes can't have children, we make this a standard, flat, data template. -->
                <DataTemplate DataType="{x:Type local:FileNode}">
                    <StackPanel Orientation="Horizontal">
                        <Label Content="2"
                               FontFamily="WingDings"
                               FontWeight="Black" />
                        <!-- Need to replace w/ an image of a file -->
                        <TextBlock Text="{Binding Path=Path}" />
                    </StackPanel>
                </DataTemplate>

                <!-- This template represents an EmptyFolderNode. Since EmptyFolderNode can't have children or siblings, we make this a standard, flat, data template. -->
                <DataTemplate DataType="{x:Type local:EmptyFolderNode}">
                    <StackPanel Orientation="Horizontal">
                        <!-- Need to replace w/ an image of a file -->
                        <TextBlock Text="{Binding Path=Name}" 
                                   FontSize="10"
                                   FontStyle="Italic"/>
                    </StackPanel>
                </DataTemplate>
            </TreeView.Resources>
        </TreeView>
    </DockPanel>
</Window>

XAML 代码已记录在案以解释正在发生的事情,所以我不会添加。

最终结果如下所示:

这应该可以得到你想要的。如果没有,请告诉我。如果您只需要一个 Directory->File 关系,那么您只需更新 BuildChildrenNodes() 方法以在构建 Children 集合时跳过 Directory 查找。

要展示的最后一件事是您现在在视图中拥有的灵活性。由于FileNode 包含其父DirectoryNode 和它所代表的FileInfo,因此您可以使用数据触发器有条件地更改您在视图中显示内容的方式。下面,我将向您展示FileNode 数据模板上的两个数据触发器。如果文件扩展名为 .dll,则将 TextBlock 变为红色,如果扩展名为 .exe,则将 TextBlock 变为蓝色。

<DataTemplate DataType="{x:Type local:FileNode}">
    <StackPanel Orientation="Horizontal">
        <Label Content="2"
                FontFamily="WingDings"
                FontWeight="Black" />
        <!-- Need to replace w/ an image of a file -->
    <TextBlock Text="{Binding Path=Path}">
        <TextBlock.Style>
            <Style TargetType="TextBlock">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=File.Extension}"
                                    Value=".exe">
                        <Setter Property="Foreground"
                                Value="Blue" />
                    </DataTrigger>

                    <DataTrigger Binding="{Binding Path=File.Extension}"
                                    Value=".dll">
                        <Setter Property="Foreground"
                                Value="Red" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBlock.Style>
    </TextBlock>
    </StackPanel>
</DataTemplate>

最终结果如下所示:

您还可以在Execute 方法中执行条件逻辑,以不同方式处理每种不同类型的文件。如果调用了Execute 方法,并且文件扩展名是.exe,而不是像现在这样忽略文件,您可以启动可执行文件。此时您有很大的灵活性。

【讨论】:

  • 哇,这太棒了,非常感谢 Johnathon 帮助我。
  • 我在重建程序时遇到了一堆错误,正如您所描述的那样。例如这一行'this.Directory = directoryInfo;'说它'不能被分配它是只读的。这一行'公共字符串名称 => this.Directory.Name;'说期待';'在'=>'之后
  • 我在 Visual Studio 2015 中使用了最新版本的 C#,它支持我上面使用的一些东西。你在用什么?
  • 现在是 2013 年。今晚更新到 2015 年再试一次
  • 完美的工作答案。感谢分享。
猜你喜欢
  • 1970-01-01
  • 2011-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-07
  • 2010-12-03
  • 2012-02-20
相关资源
最近更新 更多