65.9K
CodeProject 正在变化。 阅读更多。
Home

XAML 和 WPF 中的图形

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (16投票s)

2008 年 1 月 6 日

CPOL

12分钟阅读

viewsIcon

121063

XAML 和 WPF 中的图形

注意

本文最初发布于 我的网站。所有文字和代码示例都相同。但是,在我的网站上,您还可以看到 WPF 和 Silverlight 示例的实际效果。因此,在阅读 CodeProject 上的文章后,我邀请您也访问我的页面。

引言

在过去的几周里,我的同事 Karin Huber 和我一直在为软件架构师打基础。软件架构师专注于 COTS(现成商业软件)。不过,这并不是本文的主题(如果您想了解更多关于这家公司的情况,请访问我们的网站:此处此处)。在本文中,我想在图形的上下文中描述 XAML、WPF 和 Silverlight 的功能。我将使用我们新公司的公司徽标作为本文的示例,因为其 XAML 实现使用了相当多的 XAML 图形功能。

遗憾的是,我无法在此文档中深入探讨。如果您对更深入的信息感兴趣,可以阅读 MSDN 库,或者(如果您能阅读德语)可以查看我们最新的关于 XAML 和 WPF 的书籍

WPF_und_XAML_Programmierhandbuch.jpg

以下是我将在本文中讨论的内容

从 Inkscape 到 XAML

如果您是程序员,那么在图形设计方面,您很可能需要与设计专业人士合作。根据我的经验,如果您在聘请设计专业人士之前就开始考虑内容、基本布局和设计原则,就能取得最佳效果。我们总是尝试勾勒出我们想要的东西的基本草图,然后将其交给我们的设计伙伴。我们发现,我们自己能实现的设计效果并不理想,仅仅是因为我们学习的是软件开发而不是设计。然而,如果您在请求设计工作时无法表达您的需求,那么效果也不是最好的。

  • 您的软件开发团队中应该有一位成员了解图形设计的基本原则。他可以成为软件技术人员和设计师之间的“沟通桥梁”。
    请继续关注我的博客;几天后,我将发布一些关于非设计师的设计原则的书籍推荐。
  • 这些人使用的工具与我们软件开发人员使用的工具非常不同。在图形设计领域,我认识的几乎所有专业人士都使用 Adobe 的工具(例如,Adobe Illustrator)。您需要找到一个共同点来与他们交换工作。

我们先来关注第二点。如果您运气好,您的软件开发团队也可以负担得起 Adobe 许可证。网上有适用于 Adobe 工具的 XAML 导出器和转换器。尝试使用它们,看看它们是否能帮助您构建从开发到设计的桥梁。然而,这些工具非常强大,您需要花一些时间来学习如何使用它们。

另一个选择是使用微软的对应产品:Microsoft Expression Studio。这些工具的功能范围与我之前提到的 Adobe 工具类似。问题是,我认识的大多数设计专业人士目前都不使用它们。它们最大的优点是微软的设计工具原生支持“XAML”。它们的 XAML 导出功能非常好!但是,与 Adobe 工具一样,您需要一段时间才能熟悉 Expression。

如果您从事的是低预算项目,或者您想使用一个更容易使用的设计程序(因为它功能较少),您可以使用一个开源设计工具:Inkscape。我个人非常喜欢 Inkscape。Inkscape 的一个美妙之处在于它的文件格式:它使用 SVG(可缩放矢量图形)。您可以使用 Inkscape 绘制基本布局的创意,并将 SVG 文件交给您的设计专业人士。您可以确信他们能够处理 SVG;这种文件格式对他们来说并不陌生。

根据您的设计专业人士使用的工具,他们可以直接给您 SVG,或者直接导出 XAML。如果您收到 SVG,您很容易就可以将其转换为 XAML。几个月前,我在德国 MSDN 杂志上写了一篇关于如何手动将 SVG 路径转换为 XAML 路径的文章。在我的博客文章 XAML 中的世界 中,我用英语总结了内容。Jon Galloway 在他的博客中出色地描述了将 SVG 转换为 XAML 的方法。

即使您的设计师创建的是位图图像而不是矢量图形工具,Inkscape 也能提供帮助。该程序有一个内置的算法,用于矢量化位图。这很有用,但请准备好对矢量化过程的结果进行返工。

让我们来看看我们在创建新徽标 XAML 时是如何进行这个过程的。我们首先思考我们想要什么。公司名称已经确定(software architects)。我们开始定义关于我们希望徽标如何设计的一些基本指南。

  • 我们不想使用复杂的剪贴画。徽标应使用有趣的字体显示公司名称。
  • 公司名称相当长。因此,字体必须是紧凑的。否则,宽度和高度的比例会很奇怪。
  • ...

基于这些设计理念,我们寻找了一种可以作为我们徽标基础的字体。经过一番研究,我们决定使用由国际字体公司创建的“ITC Franklin Gothic Book”的一个子类型。我们注意到 "software" 的最后三个字母和 "architects" 的前三个字母非常相似,因此我们决定利用这一点来实现图形效果。结果,我们向我们的设计专业人士提供了以下徽标草图(使用 Inkscape 构建的 SVG)以及我们的书面想法。

LogoHandSketch2.png

我们提供给设计专业人士的基本想法

LogoStep1.png

在我看来,这是一个很好的例子,说明了设计专业人士能带来多大的不同。在我们的例子中,设计师采纳了我们的想法并进行了一些改进。他以一种略有不同的方式组合了这两个词,使效果更加有趣。

LogoStep2.png

作为第二步,他将字体转换为路径,并更改了某些字母,以创建网格,将徽标分解成不同的水平部分。

LogoStep3.png

此外,他还创建了一个增强版的徽标,包括渐变填充和反射效果。

LogoStep4v2.png

过了一会儿,我们在 Inkscape(SVG 格式)中得到了矢量图形的徽标。

LogoInInkscape.png

从那时起,我们就回到了我们原来的职业——编码。在 SVG 文件中,我们已经准备好了徽标的所有路径表达式,可以直接复制出来。我们可以在 XAML 中直接使用它们。这是 SVG 中路径表达式的一个例子。

SvgPath.png

正如您所见,路径是使用一种小型语言定义的,其中指定了所有点、线和曲线。这种小型语言在 XAML 和 SVG 之间非常非常相似。通常,您可以直接将 SVG 中的路径复制到 XAML 中,而无需进行任何更改。您可以在 W3C 上找到有关 SVG 中路径规范的详细信息,在微软的 MSDN 库 中可以找到有关 XAML 中路径的详细信息。

形状和绘图对象

下一步是在 XAML 中实现徽标。在 XAML 中,您有两种方式可以指定图形对象:您可以使用派生自 Shape(例如 Rectangle、Ellipse 等)的类,或者使用 Drawing 对象(派生自 Drawing 的类)。我们书籍“XAML und WPF Programmierhandbuch”中的以下 UML 类图显示了 WPF 中与 2D 图形相关的类的继承树。

Figure_7_10_WPF_und_XAML.png
XAML und WPF Programmierhandbuch
图 7.10,第 442 页

Shapes 和 Drawing Objects 的主要区别在于:

  • Shapes 是 FrameworkElement。因此,您可以像使用其他控件(例如 ButtonTextBox 等)一样在用户界面中使用它们。它们也派生自 Visual。因此,它们知道如何渲染自身。
  • Drawing Objects 不是 FrameworkElement!它们是 Freezable(派生自 Freezable)。因此,它们可以被设置为只读状态。通过这种方式,您可以显著提高性能。但是,处于只读状态的 Freezable 不能通过动画或数据绑定进行修改。这导致一个结论:只有当 Freezable 代表静态图形且在运行时不发生变化时,才应“冻结”它们。
  • Drawing Objects 不是 Visual!要显示 Drawing Objects,您需要一个 FrameworkElement 辅助对象。下图显示了这种关系。
Figure_7_27_WPF_und_XAML.png
XAML und WPF Programmierhandbuch
图 7.27,第 467 页

乍一看,Geometries(派生自 Geometry 的对象)似乎与 Shapes 相似。实际上它们非常不同。Geometries 不代表可以直接显示在屏幕上的图形对象。它们仅指定对象的形状。

在我们的例子中,使用 Drawing Objects 实现徽标是有意义的,因为它是一个相对静态的图形对象。以下是徽标基本形状的 XAML 代码。请注意 PresentationOptionsnamespace 声明。您需要此声明才能冻结 Freezables。

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:PresentationOptions=
        "http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="PresentationOptions" >

我们将徽标放入页面的资源集合中。因此,徽标可以轻松地提取到一个单独的 XAML 文件中(例如,App.xaml 或其他文件)。

  <Page.Resources>

在这里,您可以看到两个图形对象(“software”和“architects”)的声明。请注意,我们使用了冻结的 Drawing 对象。您还应该看看我们如何使用 GeometryGroup 组合多个 PathGeometry 对象。稍后我将详细介绍这个类。

    <!-- ****** SOFTWARE ******************************************* -->
    <!-- Geometry for the word "software" -->
    <GeometryGroup x:Key="LogoSoftware"
      PresentationOptions:Freeze="True" >
      <PathGeometry Figures="M 0.31163807,145.75739 ... z" />
      <PathGeometry Figures="M 70.074606,115.57876 ... z" />
      <PathGeometry Figures="M 173.53291,0.40830421 ... z" />
      <PathGeometry Figures="M 307.03223,184.21003 ... z" />
      <PathGeometry Figures="M 391.22711,96.183574 ... z" />
      <PathGeometry Figures="M 426.25331,184.22683 ... z" />
      <PathGeometry Figures="M 501.63047,145.55673 ... z" />
    </GeometryGroup>

    <!-- ****** ARCHITECTS **************************************** -->
    <!-- Geometry for the word "architects" -->
    <GeometryGroup x:Key="LogoArchitects"
      PresentationOptions:Freeze="True" >
      <PathGeometry Figures="M 391.29841,156.18357 ... z" />
      <PathGeometry Figures="M 426.35415,242.80498 ... z" />
      <PathGeometry Figures="M 590.3878,242.94013 ... z" />
      <PathGeometry Figures="M 625.36802,242.94013 ... z" />
      <PathGeometry Figures="M 682.10338,226.72431 ... z" />
      <PathGeometry Figures="M 735.83215,206.36345 ... z" />
      <PathGeometry Figures="M 502.24129,206.22951 ... z" />
      <PathGeometry Figures="M 805.67431,206.22951 ... z" />
      <PathGeometry Figures="M 869.59918,226.66181 ... z" />
      <PathGeometry Figures="M 873.62206,206.17249 ... z" />
    </GeometryGroup>

在这里,我们定义了徽标中使用的渐变填充。

    <!-- ****** BRUSHES ******************************************* -->
    <!-- Brush for the word "software" -->
    <LinearGradientBrush x:Key="SoftwareBrush" StartPoint="0,1"
      EndPoint="0,0" PresentationOptions:Freeze="True">
      <GradientStop Color="#76ba52" Offset="0.0" />
      <GradientStop Color="#c0dd89" Offset="1.0" />
    </LinearGradientBrush>
    <!-- Brush for the word "architects" -->
    <LinearGradientBrush x:Key="ArchitectsBrush" StartPoint="0,1"
      EndPoint="0,0" PresentationOptions:Freeze="True">
      <GradientStop Color="#264da6" Offset="0.0" />
      <GradientStop Color="#15306c" Offset="1.0" />
    </LinearGradientBrush>

正如我们之前所说,Drawing Objects 需要一个 FrameworkElement 辅助对象才能在屏幕上显示它们。在我们的例子中,Image 类被用于此目的。Image 需要一个 ImageSource 的子类作为图像的源。因此,我们在资源字典中提供了一个 DrawingImage 对象(DrawingImage 派生自 ImageSource)。

    <!-- ****** LOGO ********************************************** -->
    <DrawingImage x:Key="SoftwareArchitectsLogo"
      PresentationOptions:Freeze="True" >
      <DrawingImage.Drawing>
        <DrawingGroup>
          <GeometryDrawing Brush="{StaticResource SoftwareBrush}"
            Geometry="{StaticResource LogoSoftware}" />
          <GeometryDrawing Brush="{StaticResource ArchitectsBrush}"
            Geometry="{StaticResource LogoArchitects}" />
        </DrawingGroup>
      </DrawingImage.Drawing>
    </DrawingImage>
  </Page.Resources>

只需三行代码即可在任何 WPF 窗口或页面中显示徽标。其他所有内容都已在资源中定义。

  <Canvas>
    <Image Source="{StaticResource SoftwareArchitectsLogo}" />
  </Canvas>

</Page>

在这里,您可以在 XAMLPad 中看到使用上述代码实现的徽标。XAMLPad 是一个小型工具,您可以使用它来原型化您的 XAML 代码。它包含在免费的 Windows SDK 中。

LogoInXamlPad.png

组合几何图形对象

在 WPF 中,您有两种方法可以组合 Geometries。您可以在集合中将它们分组(如上面的代码所示),在这种情况下,您使用 GeometryGroup 类。请注意,GeometryGroup 实际上只是一个集合,仅此而已。如果您想通过组合两个 Geometry 来创建一个全新的 Geometry (例如,求交集、并集等),则必须使用 CombinedGeometry

在我们的例子中,我们组合了两个 Geometries 来创建“software”一词的实色底部。目标不是为徽标的这个区域创建单独的路径。相反,我们希望基于先前声明的路径,并排除灰色矩形(见下图)。

LogoDarkBottom.png

<Page.Resources>
  [...]

请注意,我们如何使用 StaticResource 标记扩展来引用现有的“software”一词的 Geometry

  <!-- Geometry for the dark area at the bottom of the letters
       of the word "software" -->
  <CombinedGeometry x:Key="LogoSoftwareBottomShape"
    GeometryCombineMode="Exclude"
    Geometry1="{StaticResource LogoSoftware}"
    PresentationOptions:Freeze="True">
    <CombinedGeometry.Geometry2>
      <RectangleGeometry Rect="0,0,519,145" />
    </CombinedGeometry.Geometry2>
  </CombinedGeometry>
  [...]

  <!-- Brush for the dark area at the bottom of the letters
       of the word "software" -->
  <SolidColorBrush x:Key="SoftwareBottomShapeBrush"
    Color="#76BA52" PresentationOptions:Freeze="True" />

  [...]

  <!-- ****** LOGO ********************************************** -->
  <DrawingImage x:Key="SoftwareArchitectsLogo"
    PresentationOptions:Freeze="True" >
    <DrawingImage.Drawing>
      <DrawingGroup>
        <GeometryDrawing Brush="{StaticResource SoftwareBrush}"
          Geometry="{StaticResource LogoSoftware}" />
        <GeometryDrawing
          Brush="{StaticResource SoftwareBottomShapeBrush}"
          Geometry="{StaticResource LogoSoftwareBottomShape}" />
        <GeometryDrawing Brush="{StaticResource ArchitectsBrush}"
          Geometry="{StaticResource LogoArchitects}" />
      </DrawingGroup>
    </DrawingImage.Drawing>
  </DrawingImage>

</Page.Resources>

变换对象

完成徽标所缺少的最后一块是两个单词的反射效果。该效果可以通过在 Y 轴上翻转它们来创建。对于这种情况,WPF 提供了 Transform 类。该库包含 Transform 的各种子类,您可以使用它们来创建不同的效果。下图概述了可供您使用的内容。

Figure_7_39_WPF_und_XAML.png
XAML und WPF Programmierhandbuch
图 7.39,第 484 页

在我们的例子中,我们不仅使用了一个 Transform 对象。此外,我们还使用了 CombinedGeometry 来截断镜像的单词。如果我们不这样做,横向跟随徽标的对象将与徽标保持奇怪的距离。

<Page.Resources>
  [...]

  <!-- Geometry for the mirror-effect of the word "software" -->
  <CombinedGeometry x:Key="LogoSoftwareMirror"
    GeometryCombineMode="Exclude"
    PresentationOptions:Freeze="True">
    <CombinedGeometry.Geometry1>
      <GeometryGroup>
        <PathGeometry Figures="M 0.31163807,145.75739 ... z" />
        <PathGeometry Figures="M 70.074606,115.57876 ... z" />
        <PathGeometry Figures="M 173.53291,0.40830421 ... z" />
        <PathGeometry Figures="M 307.03223,184.21003 ... z" />
        <PathGeometry Figures="M 391.22711,96.183574 ... z" />
        <PathGeometry Figures="M 426.25331,184.22683 ... z" />
        <PathGeometry Figures="M 501.63047,145.55673 ... z" />
      </GeometryGroup>
    </CombinedGeometry.Geometry1>
    <CombinedGeometry.Geometry2>
      <RectangleGeometry Rect="0,0,519,145" />
    </CombinedGeometry.Geometry2>

您可以通过将 Transform 的适当子类分配给要更改的对象的 Transform 属性来应用变换。

    <CombinedGeometry.Transform>
      <TransformGroup>
        <ScaleTransform CenterY="184" ScaleY="-1" />
      </TransformGroup>
    </CombinedGeometry.Transform>
  </CombinedGeometry>
  [...]


  <!-- Geometry for the mirror-effect of the word "architects" -->
  <CombinedGeometry x:Key="LogoArchitectsMirror"
    GeometryCombineMode="Exclude"
    PresentationOptions:Freeze="True">
    <CombinedGeometry.Geometry1>
      <GeometryGroup PresentationOptions:Freeze="True" >
        <PathGeometry .../>
        [...]
      </GeometryGroup>
    </CombinedGeometry.Geometry1>
    <CombinedGeometry.Geometry2>
      <RectangleGeometry Rect="330,0,604,206" />
    </CombinedGeometry.Geometry2>
    <CombinedGeometry.Transform>
      <TransformGroup>
        <ScaleTransform CenterY="243" ScaleY="-1" />
      </TransformGroup>
    </CombinedGeometry.Transform>
  </CombinedGeometry>
  [...]

  <!-- Brush for the mirror-effect of the word "software" -->
  <LinearGradientBrush x:Key="SoftwareMirrorBrush" StartPoint="0,1"
    EndPoint="0,0" PresentationOptions:Freeze="True">
    <GradientStop Color="#0076ba52" Offset="0.4" />
    <GradientStop Color="#60c0dd89" Offset="1.0" />
  </LinearGradientBrush>
  <!-- Brush for the mirror-effect of the word "architects" -->
  <LinearGradientBrush x:Key="ArchitectsMirrorBrush" StartPoint="0,1"
    EndPoint="0,0" PresentationOptions:Freeze="True">
    <GradientStop Color="#00264da6" Offset="0" />
    <GradientStop Color="#3015306c" Offset="1.0" />
  </LinearGradientBrush>
  [...]

  <!-- ****** LOGO ********************************************** -->
  <DrawingImage x:Key="SoftwareArchitectsLogo"
    PresentationOptions:Freeze="True" >
    <DrawingImage.Drawing>
      <DrawingGroup>
        <GeometryDrawing Brush="{StaticResource SoftwareBrush}"
          Geometry="{StaticResource LogoSoftware}" />
        <GeometryDrawing Brush="{StaticResource SoftwareMirrorBrush}"
          Geometry="{StaticResource LogoSoftwareMirror}" />
        <GeometryDrawing
          Brush="{StaticResource SoftwareBottomShapeBrush}"
          Geometry="{StaticResource LogoSoftwareBottomShape}" />
        <GeometryDrawing Brush="{StaticResource ArchitectsBrush}"
          Geometry="{StaticResource LogoArchitects}" />
        <GeometryDrawing Brush="{StaticResource ArchitectsMirrorBrush}"
          Geometry="{StaticResource LogoArchitectsMirror}" />
      </DrawingGroup>
    </DrawingImage.Drawing>
  </DrawingImage>

</Page.Resources>

这是在 XAMLPad 中具有所有效果的完成徽标。

FinishedLogoXAMLPad.png

WPF 和 Silverlight 之间的区别

Silverlight 是微软对 Adobe 的 Flash 的回应。与 WPF 类似,Silverlight 也使用 XAML 来指定表示用户界面元素的“对象树”。如果您熟悉 WPF 的 XAML,那么开始学习 Silverlight 将不会困难。不幸的是,Silverlight 的 XAML 并不提供您在 WPF 中习惯的所有功能(我们这里讨论的是 Silverlight 1.0,这是当前已发布的版本)。

我不想给出所有差异的完整列表,因为您可以在微软的 Silverlight 开发中心 的 MSDN 库中详细阅读。我只想列出那些在我们为 Silverlight 应用程序实现徽标时影响到我们的差异。与 WPF 的 XAML 相反,Silverlight **不**...

  • ...像 WPF 那样支持资源。Silverlight 资源仅包含用于动画的 Storyboard 对象。
  • ...了解 Freezables。
  • ...支持 PathGeometry 中的路径表达式的迷你语言。它仅在 Path 中受支持。
  • ...了解像 {StaticResource ...} {Binding ...} 这样的标记扩展。您需要使用 JavaScript 自己实现相应的功能。

以下是 Silverlight 中一个简化版徽标实现的源代码。您可以看到 Silverlight 版本与 WPF 版本非常相似。

请注意,Silverlight 版本中没有与冻结 Freezables 相关的引用。Silverlight 不了解 Freezables。

<Canvas
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" >

  <Path x:Name="SoftwarePath"
    Data="M 0.31163807,145.75739 ... z">
    <Path.Fill>
      <LinearGradientBrush StartPoint="0,1" EndPoint="0,0">
        <GradientStop Color="#76ba52" Offset="0.0" />
        <GradientStop Color="#c0dd89" Offset="1.0" />
      </LinearGradientBrush>
    </Path.Fill>
    <Path.RenderTransform>
      <TransformGroup>
        <ScaleTransform ScaleX="0.25" ScaleY="0.25" />
        <TranslateTransform x:Name="SoftwareTranslateAnimation" />
      </TransformGroup>
    </Path.RenderTransform>

出于演示目的,我们在这里添加了一个小型动画。我们使用了 DoubleAnimation 对象来让徽标飞入并淡入。

    <Path.Triggers>
      <EventTrigger RoutedEvent="Path.Loaded">
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation
              Storyboard.TargetName="SoftwareTranslateAnimation"
              Storyboard.TargetProperty="Y"
              From="-50" To="0" Duration="0:0:0.5" />
            <DoubleAnimation
              Storyboard.TargetName="SoftwarePath"
              Storyboard.TargetProperty="Opacity"
              From="0" To="1" Duration="0:0:0.5" />
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Path.Triggers>
  </Path>

  <Path x:Name="ArchitectsPath"
    Data="M 391.29841,156.18357 ... z">
    <Path.Fill>
      <LinearGradientBrush StartPoint="0,1" EndPoint="0,0">
        <GradientStop Color="#264da6" Offset="0.0" />
        <GradientStop Color="#15306c" Offset="1.0" />
      </LinearGradientBrush>
    </Path.Fill>
    <Path.RenderTransform>
      <TransformGroup>
        <ScaleTransform ScaleX="0.25" ScaleY="0.25" />
          <TranslateTransform x:Name="ArchitectsTranslateAnimation" />
      </TransformGroup>
    </Path.RenderTransform>
    <Path.Triggers>
      <EventTrigger RoutedEvent="Path.Loaded">
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation
              Storyboard.TargetName="ArchitectsTranslateAnimation"
              Storyboard.TargetProperty="Y"
              From="50" To="0" Duration="0:0:0.5" />
            <DoubleAnimation
              Storyboard.TargetName="ArchitectsPath"
              Storyboard.TargetProperty="Opacity"
              From="0" To="1" Duration="0:0:0.5" />
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Path.Triggers>
  </Path>
</Canvas>

摘要

WPF 和 XAML 是开发下一代 Windows 应用程序的绝佳工具。与 HTML 不同,您不必将要在 WPF 应用程序中使用的所有图像都转换为位图图像。只需让您的设计专业人士提供矢量格式的图像,您就会发现将其转换为 XAML 非常容易。

历史

  • 2008 年 1 月 6 日:初次发布
XAML 和 WPF 中的图形 - CodeProject - 代码之家
© . All rights reserved.