Silverlight 2 创建自定义控件

超酷代码
为 Silverlight 2 创建自定义控件
Jeff Prosise

代码下载位置: WickedCode2008_08.exe (585 KB)
在线浏览代码
本文将介绍以下内容:
  • WPF 控件模型
  • 创建控件模板
  • 派生控件
  • 添加事件
本文使用以下技术:
Silverlight 2
本文基于 Silverlight 2 的 Beta 2 版本。文中的所有信息均有可能发生变更。
Silverlight™ 2 有别于 Silverlight 1.0 的 众多功能之一是支持控件。Silverlight 2 以丰富且强大可靠的控件模型闻名,该模型是平台中包括的控件和第三方控件包的基础。您也可以使用此控件模型构建自己的控件,但对于不熟悉 Windows ® Presentation Foundation (WPF) 控件模型的开发人员来讲,第一次构建 Silverlight 自定义控件会令人生畏。撰写此专栏时,即在发布 Silverlight 2 Beta 2 之前,几乎没有文档可供参考,而在 Web 中快速搜索出的少数几篇教程为我指明了方向。虽然我正在讨论此主题,但应该提醒大家的是,我正在使用的 Beta 2 在其最终发布之前,可能会做进一步的更改。
在了解如何为新平台编写自定义控件时,我经常先复制一些内置控件:按钮和列表框等等。这些控件可能表面看起来简单,但他们总是揭示了控件模型的关键功能并可以测试人们对这些功能的掌握程度。另外,如果您无法首先创建一个简单的按压按钮,则无法创建 super-duper-multicolor-multithreaded-all-in-one-do-it-all 小组件控件。
了解 Silverlight 2 自定义控件的最佳方法是一步一步一点一点地创建一个 Silverlight 2 自定义控件。这样,您不仅能了解控件的各个组成部分,还能了解它们是如何组合到一起的。以下教程描述了如何构建 SimpleButton 控件,该控件复制内置 Button 控件的外观和行为的重要方面,并且可以从该控件直接看到控件开发的情况,即 Silverlight 样式。

步骤 1:创建新的 Silverlight 项目
创建自定义控件的第一步是启动 Visual Studio ® 2008(确保您已为 Visual Studio 安装了 Silverlight 加载项,这样才可以创建 Silverlight 项目)并创建一个项目。通常您需要创建 Silverlight 类库项目,这样该控件才可以编译到自己的程序集并作为引用添加到将使用该控件的项目。我将采用一个稍微不同的途径来创建一个 Silverlight 应用程序项目,以便在同一项目中构建并使用该控件。所以,我们首先创建一个名为 SimpleButtonDemo 的新 Silverlight 应用程序项目,如 图 1 所示。当 Visual Studio 试图为 Silverlight 项目创建随附的 Web 项目时,选择“是”。
图 1 创建 SimpleButtonDemo 项目(单击图像可查看大图)

步骤 2:从控件(或 ContentControl)派生
下一步是添加代表此控件的 C# 类。至少,为了继承基本的控件功能,该控件类应从 Silverlight System.Windows.Controls.Control 类派生。但是,它也可以从 ContentControl 和 ItemsControl 等 Control 派生类派生。许多内置控件可以直接或间接从添加了 Content 属性的 ContentControl 派生,而该属性允许对控件的内容(如按压按钮表面上的内容)进行自定义。ListBox 控件则从 ItemsControl 派生,ItemsControl 可以实现用来向用户提供项目集合的控件的基本行为。因为我们要实现按钮,所以将从 ContentControl 派生。
使用 Visual Studio 中的“添加新项”命令为 SimpleButtonDemo 项目添加新 C# 类。将文件命名为 SimpleButton.cs。然后打开 SimpleButton.cs 并修改 Simple­Button 类,这样它就可以从 ContentControl 派生:
namespace SimpleButtonDemo
{
    public class SimpleButton : ContentControl
    {
    }
}
此时,您已实现了单纯的自定义控件,该控件可在 XAML 文档中通过声明进行实例化。为了说明此问题,将下列语句添加到 Page.xaml:
<custom:SimpleButton />
为了使 Silverlight 可以识别此声明,您还需要将以下属性添加到 Page.xaml 的根 User­Control 元素:
xmlns:custom="clr-namespace:SimpleButtonDemo; assembly=SimpleButtonDemo"
您可以看到,clr-namespace 能够识别在其中定义 SimpleButton 类的命名空间,而程序集可以识别包含此控件的程序集。在此示例中,控件程序集和应用程序程序集是同一个程序集。如果 SimpleButton 在名为 MyControls.dll 的单独程序集中实现,您需要将程序集设为与“MyControls”相同。 图 2 中的代码显示了完成上述修改后的 Page.xaml 的内容。顺便说一下,您不必将 custom 作为自定义控件的前缀;只需直接使用 foo 或贵公司的名称作为前缀即可。
<UserControl x:Class="SimpleButtonDemo.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo"
    Width="400" Height="300">

    <Grid x:Name="LayoutRoot" Background="White">
      <custom:SimpleButton />
    </Grid>

</UserControl>
现在,通过启动 SimpleButtonDemo_Web 项目(Visual Studio 已将该项目添加到解决方案)中的 SimpleButtonDemoTestPage.aspx 或 SimpleButtonDemoTestPage.html 测试页面,您可以看到您的努力成果了。 图 3 显示了 SimpleButtonDemoTestPage.html 在浏览器中的外观。当然,目前主页中还没有什么内容,但在下一步骤中情况将有所变化。
图 3 显示 SimpleButton 控件(单击图像可查看大图)

步骤 3:创建控件模板
在上一步骤中,浏览器窗口空白的原因是,虽然已对 SimpleButton 实例化,但它不呈现用户界面 (UI)。您可以通过修改 Page.xaml 中的 SimpleButton 声明使其包含控件模板来纠正此问题。 图 4 中的代码显示了修改后的控件声明。
<custom:SimpleButton>
  <custom:SimpleButton.Template>
    <ControlTemplate>
      <Grid x:Name="RootElement">
        <Rectangle x:Name="BodyElement" Width="200" Height="100"
          Fill="Lavender" Stroke="Purple" RadiusX="16" RadiusY="16" />
        <TextBlock Text="Click Me" HorizontalAlignment="Center"
          VerticalAlignment="Center" />
      </Grid>
    </ControlTemplate>
  </custom:SimpleButton.Template>
</custom:SimpleButton>
现在,此声明可以初始化控件的 Template 属性(该属性定义控件的可视树),以包含位于 1 行 1 列的 Grid 内部的 Rectangle 和 TextBlock。再次在浏览器中打开 SimpleButtonDemoTestPage.html,输出内容将发生很大变化(请参见 图 5)。现在,SimpleButton 具有了外观!
图 5 SimpleButton 控件(单击图像可查看大图)

步骤 4:创建默认控件模板
要求开发人员使用您的控件定义他们自己的控件模板是不合理的。自定义控件应具有默认的模板,因此即使简单的声明(如 图 2 中所示)也会在页面上显示某些内容。提供默认模板后,人们仍可以使用其他模板(如 图 4 中的模板)将其覆盖,但如果不要求提供模板,您的控件会更加吸引人。
用于为自定义控件定义默认模板的机制是从 WPF 借用的。首先,向控件项目添加名为 Generic.xaml 的文件。(是的,此文件必须命名为 Generic.xaml。大小写无关紧要,但文件名不能改变。)然后,在 Generic.xaml 中定义样式,该样式使用属性 setter 将值分配给控件的 Template 属性。Silverlight 运行时自动在控件程序集(Generic.xaml 作为数据源嵌入其中)中查找 Generic.xaml 并将样式应用到控件实例。除了定义默认模板外,此样式还可以将默认值分配给其他控件属性,如 Width 和 Height 属性。
要亲身体验,请使用 Visual Studio 的“添加新项”命令将名为 Generic.xaml 的文本文件添加到 SimpleButtonDemo 项目中。然后使用 图 6 中的代码替换 Generic.xaml 的内容。现在,Generic.xaml 包含一个未命名的样式,而该样式已应用到 SimpleButton 的所有实例中(请注意 TargetType 属性)。该样式包括 SimpleButton 的 Template 属性的默认值,该值与显示分配给 图 5 中的控件的 Template 值相同。
<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid x:Name="RootElement">
            <Rectangle x:Name="BodyElement" Width="200" Height="100"
              Fill="Lavender" Stroke="Purple" RadiusX="16" RadiusY="16" />
            <TextBlock Text="Click Me" HorizontalAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>
创建默认模板后,返回 SimpleButton.cs 并将以下语句添加到类构造函数中:
this.DefaultStyleKey = typeof(SimpleButton);
然后打开 Page.xaml 并修改控件声明,使修改后的内容如下所示:
<custom:SimpleButton />
在浏览器中打开此测试页面,该控件的外观应与以前完全相同。但是这次,获取此外观将比较简单。

步骤 5:添加模板绑定
SimpleButton 现存的一个问题是,控件模板不支持分配给该控件的属性值。也就是说,如果按如下所示声明该控件,该控件仍然宽 200,高 100,因为这些值已硬编码到控件模板中。
<custom:SimpleButton Width="250" Height="150" />
从控件开发人员的角度而言,Silverlight 2 最重要的功能之一就是模板绑定。模板绑定允许分配给控件的属性值向下传递到控件模板,并且是使用 {TemplateBinding} 标记扩展在 XAML 中声明的。请不要使用类似下面的硬编码值定义构成 SimpleButton 主体的 Rectangle 的 Width 和 Height 属性:
Width="200" Height="100"
您应按如下方式定义上述属性:
Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"
现在,分配给控件的宽度和高度即是分配给 Rectangle 的宽度和高度。
图 7 显示了 Generic.xaml 修改后的版本,它将默认值分配给从基类继承来的 Width、Height 和 Background 属性,并使用模板绑定在控件模板中引用这些属性值。
<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Width" Value="200" />
    <Setter Property="Height" Value="100" />
    <Setter Property="Background" Value="Lavender" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid x:Name="RootElement">
            <Rectangle x:Name="BodyElement"
              Width="{TemplateBinding Width}"
              Height="{TemplateBinding Height}"
              Fill="{TemplateBinding Background}"
              Stroke="Purple" RadiusX="16" RadiusY="16" />
            <TextBlock Text="Click Me"
              HorizontalAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>
通过编辑 Page.xaml 中的控件声明来测试修改后的控件模板,如下所示:
<custom:SimpleButton Width="250" Height="150" Background="Yellow" />
输出内容如 图 8 所示。TemplateBindings 是正确进行操作的至关重要的步骤,因为现在 SimpleButton 的实例已支持为其分配的属性值。
图 8 改进的 SimpleButton 控件

步骤 6:使用 ContentPresenter 替换 TextBlock
SimpleButton 从 ContentControl 派生意味着它具有 Content 属性,开发人员可以使用此属性自定义按钮表面上的内容 — 至少理论上如此。使用 图 9 中的 XAML 可以自定义内置 Button 控件以生成 图 10 中显示的外观。
<Button Width="250" Height="150">
  <Button.Content>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <Ellipse Width="75" Height="75" Margin="10">
        <Ellipse.Fill>
          <RadialGradientBrush GradientOrigin="0.25,0.25">
            <GradientStop Offset="0.25" Color="White" />
            <GradientStop Offset="1.0" Color="Red" />
          </RadialGradientBrush>
        </Ellipse.Fill>
      </Ellipse>
      <TextBlock Text="Click Me" VerticalAlignment="Center" />
    </StackPanel>
  </Button.Content>
</Button>
图 10 包含自定义内容的按钮
如果尝试对 SimpleButton 执行相同的操作,您很快会发现其内容仍是普通文本。实际上,您甚至无法设置 Content="Test" 来更改按钮文本,这是因为该控件模板当前包含具有硬编码文本的硬编码 TextBlock。
您可以将 SimpleButton 的默认控件模板中的 TextBlock 替换为 ContentPresenter 来弥补这一缺陷,如 图 11 所示。TextBlock 只能呈现文本,而 ContentPresenter 可以呈现分配给此控件的 Content 属性的任何 XAML。完成这些更改后, 图 12 中的 XAML SimpleButton 声明生成 图 13 所示的输出内容。现在,SimpleButton 支持两个自定义级别。可以使用自定义模板重新定义其整个可视树,或者仅使用 Content 属性重新定义其内容。而且,您还可以使用简单的 Content 属性更改按钮文本。SimpleButton 的行为越来越接近真正的按钮控件。
<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Width" Value="200" />
    <Setter Property="Height" Value="100" />
    <Setter Property="Background" Value="Lavender" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid x:Name="RootElement">
            <Rectangle x:Name="BodyElement"
              Width="{TemplateBinding Width}"
              Height="{TemplateBinding Height}"
              Fill="{TemplateBinding Background}"
              Stroke="Purple" RadiusX="16" RadiusY="16" />
            <ContentPresenter Content="{TemplateBinding Content}"
              HorizontalAlignment="Center" VerticalAlignment="Center"
              FontSize="{TemplateBinding FontSize}" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>
<custom:SimpleButton Width="250" Height="150">
  <custom:SimpleButton.Content>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <Ellipse Width="75" Height="75" Margin="10">
        <Ellipse.Fill>
          <RadialGradientBrush GradientOrigin="0.25,0.25">
            <GradientStop Offset="0.25" Color="White" />
            <GradientStop Offset="1.0" Color="Red" />
          </RadialGradientBrush>
        </Ellipse.Fill>
      </Ellipse>
      <TextBlock Text="Click Me" VerticalAlignment="Center" />
    </StackPanel>
  </custom:SimpleButton.Content>
</custom:SimpleButton>
图 13 包含自定义内容的 SimpleButton

步骤 7:添加 Click 事件
制作 SimpleButton 的下一个步骤是使其引发 Click 事件。实现 Silverlight 控件中的事件通常与实现普通的 Microsoft ® .NET Framework 类中的事件一样:您只需声明控件类中的事件,然后编写代码引发这些事件。
但是与 Silverlight 中事件引发稍有不同的一点是:Silverlight 支持路由事件。在 WPF 中,事件路由含义更加丰富,因为在可视树中事件可以上下传递;而在 Siverlight 中,事件只能向上传递,即“冒泡”操作。路由事件由 RoutedEventHandler 委托定义,并且路由事件处理程序接收 RoutedEventArgs 对象,该对象包含用于确认引发此事件的对象的 Source 属性(如果此事件最初由可视树中的深层对象引发,则该属性不同于传递给事件处理程序的发送者参数)。内置 Button 控件的 Click 事件是一个路由事件,因此 SimpleButton 的 Click 事件也应该是路由事件。
图 14 显示了为引发路由 Click 事件,必须对 SimpleButton.cs 中的控件类所做的修改。现在,SimpleButton 的构造函数为 MouseLeftButtonUp 事件注册了一个处理程序。如果至少存在一个注册的侦听器,则此处理程序就可以引发 Click 事件。如果 button-down 事件先于 button-up 事件,则传统的按钮控件只能引发 Click 事件。为使源代码尽可能简单,SimpleButton 中省略了该逻辑。
public class SimpleButton : ContentControl
{
    public event RoutedEventHandler Click;

    public SimpleButton()
    {
        this.DefaultStyleKey = typeof(SimpleButton);
        this.MouseLeftButtonUp += new MouseButtonEventHandler
            (SimpleButton_MouseLeftButtonUp);
    }

    void SimpleButton_MouseLeftButtonUp(object sender,
        MouseButtonEventArgs e)
    {
        if (Click != null)
            Click(this, new RoutedEventArgs());
    }
}
要测试 Click 事件,您可以修改 Page.xaml 中的控件声明,如下所示:
<custom:SimpleButton Content="Click Me" Click="OnClick" />
然后添加在 Page.xaml.cs 旁边显示的事件处理程序。现在,单击 SimpleButton 应生成一个包含单词“Click!”的警告框 — 证明已引发并正确处理了此事件:
protected void OnClick(Object sender, RoutedEventArgs e)
{
    System.Windows.Browser.HtmlPage.Window.Alert("Click!");
}

步骤 8:添加可视状态
Silverlight 控件的两个主要组件是可视状态和可视状态转换。可视状态用于定义控件在不同状态下的外观:在处于按下、鼠标点击、禁用等状态时的外观。可视状态转换用于定义控件如何从一种可视状态转换到另一种可视状态:例如,从“正常”状态转换到“按下”状态。
Silverlight 2 Beta 2 引入了一个新组件,称为视觉状态管理器 (VSM),它可以简化状态和状态转换以及协助实现更好的工具支持。借助 VSM,您可以使用 VisualState 对象封装 Storyboards 以定义状态,使用 VisualTransition 对象定义转换。然后,您可以使用 VisualStateManager 类的静态 GoToState 方法将控件转换到指定的状态以响应用户操作。
图 15 显示了如何修改 Generic.xaml 以利用 VSM。SimpleButton 定义了两种状态:Normal 和 MouseOver。这两种状态是使用 <vsm:VisualState> 元素声明的。Normal 状态不需要 Storyboard,因为它代表处于默认状态的控件。MouseOver 状态使用 ColorAnimation 将控件的背景色(实际上是代表控件主体的矩形的 Fill 属性)更改为粉色。ColorAnimation 的 Duration 属性为 0,因为动画的长度由相应的 VisualTransition 的 Duration 属性驱动。注意:已将 xmlns:vsm 属性添加到 <ResourceDictionary> 元素以启用对 VisualStateManager 的声明性使用。
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:custom="clr-namespace:SimpleButtonDemo;assembly=SimpleButtonDemo"
  xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
  <Style TargetType="custom:SimpleButton">
    <Setter Property="Width" Value="200" />
    <Setter Property="Height" Value="100" />
    <Setter Property="Background" Value="Lavender" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="custom:SimpleButton">
          <Grid>
            <vsm:VisualStateManager.VisualStateGroups>
              <vsm:VisualStateGroup x:Name="CommonStates">
                <vsm:VisualStateGroup.Transitions>
                  <vsm:VisualTransition To="Normal" Duration="0:0:0.2"/>
                  <vsm:VisualTransition To="MouseOver" Duration="0:0:0.2"/>
                </vsm:VisualStateGroup.Transitions>
                <vsm:VisualState x:Name="Normal" />
                <vsm:VisualState x:Name="MouseOver">
                  <Storyboard>
                    <ColorAnimation Storyboard.TargetName="BodyElement"
Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)"
To="Pink" Duration="0" />
                  </Storyboard>
                </vsm:VisualState>
              </vsm:VisualStateGroup>
            </vsm:VisualStateManager.VisualStateGroups>
            <Rectangle x:Name="BodyElement"
              Width="{TemplateBinding Width}"
              Height="{TemplateBinding Height}"
              Fill="{TemplateBinding Background}"
              Stroke="Purple" RadiusX="16" RadiusY="16" />
            <ContentPresenter Content="{TemplateBinding Content}"
              HorizontalAlignment="Center" VerticalAlignment="Center"
              FontSize="{TemplateBinding FontSize}" />
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>
图 16 显示了完成的 SimpleButton 类,现在已修改为使用 Generic.xaml 中声明的状态和转换。该类本身也带有 [TemplateVisualState] 属性,指示该控件支持哪些可视状态。(这些属性不是必要因素,但是它们确实可以启用 Expression Blend™ 之类的工具以提供更丰富的设计时体验。)类构造函数将 MouseEnter 和 MouseLeave 事件绑定成一对处理程序,这两个处理程序利用 VisualStateManager.GoToState 转换控件状态。现在,当鼠标点击控件时,该控件变为粉色;而当鼠标离开时,控件变回最初的颜色。
[TemplateVisualState(Name = "Normal", GroupName = "GroupCommon")]
[TemplateVisualState(Name = "StateMouseOver", GroupName = "GroupCommon")]
public class SimpleButton : ContentControl
{
    public event RoutedEventHandler Click;

    public SimpleButton()
    {
        DefaultStyleKey = typeof(SimpleButton);
        this.MouseLeftButtonUp +=
            new MouseButtonEventHandler(SimpleButton_MouseLeftButtonUp);
        this.MouseEnter +=
            new MouseEventHandler(SimpleButton_MouseEnter);
        this.MouseLeave +=
            new MouseEventHandler(SimpleButton_MouseLeave);
    }

    void SimpleButton_MouseLeftButtonUp(object sender,
        MouseButtonEventArgs e)
    {
        if (Click != null)
            Click(this, new RoutedEventArgs());
    }

    void SimpleButton_MouseEnter(object sender, MouseEventArgs e)
    {
        VisualStateManager.GoToState(this, "MouseOver", true);
    }

    void SimpleButton_MouseLeave(object sender, MouseEventArgs e)
    {
        VisualStateManager.GoToState(this, "Normal", true);
    }
}

完成的控件
SimpleButton 完成的源代码包含在本文附带的下载中。您可以通过实现其他的属性和状态动画,甚至将 UI 修饰为玻璃状外观来进一步优化控件。但是,SimpleButton 包含 Silverlight 控件的所有主要元素,如果您掌握了本文介绍的所有知识,那么您在构建自己的控件时应该不会遇到什么问题。

Jeff Prosise 是《MSDN 杂志》 的特邀编辑,他著有多部书籍,包括《Programming Microsoft .NET》。他还是 Wintellect (wintellect.com) 公司的创始人,该公司专门针对 .NET Framework 提供软件咨询和培训。要对本专栏发表评论吗?您可以通过 wicked@microsoft.com 与 Jeff 联系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值