WPF 入门(一)

一、参考

入门介绍引用博客园相关文章:

https://www.cnblogs.com/zh7791/category/1528742.html

参考书籍《深入浅出WPF》-- 刘铁锰

二、WPF定义

WPF全称:Windows Presentation Foundation

WPF是一个可创建桌面客户端应用程序的 UI 框架。 WPF 开发平台支持广泛的应用开发功能,包括应用模型、资源、控件、图形、布局、数据绑定、文档和安全性。 此框架是 .NET Framework 的一部分,因此,如果你曾经使用 ASP.NET 或 Windows 窗体通过 .NET 构建应用程序,应该会熟悉此编程体验。 WPF 使用 Extensible Application Markup Language (XAML),为应用编程提供声明性模型。

三、XAML 语言

XAML 是一种基于 XML 的标记语言,以声明形式实现应用程序的外观。 通常用它定义窗口、对话框、页面和用户控件,并填充控件、形状和图形。

下面的示例使用 XAML 来实现包含一个按钮的窗口的外观:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Window with Button"
    Width="250" Height="100">
​
  <!-- Add button to window -->
  <Button Name="button">Click Me!</Button>
</Window>

其对应的窗体如下图所示:

四、WPF布局

  • WPF布局原则

    • 一个窗口中只能包含一个元素

    • 不应显示设置元素尺寸

    • 不应使用坐标设置元素的位置

    • 可以嵌套布局容器

  • WPF布局容器

    • StackPanel: 水平或垂直排列元素、Orientation属性分别: Horizontal / Vertical

    • WrapPanel : 水平或垂直排列元素、针对剩余空间不足会进行换行或换列进行排列

    • DockPanel : 根据容器的边界、元素进行Dock.Top、Left、Right、Bottom设置

    • Grid : 类似table表格、可灵活设置行列并放置控件元素、比较常用

    • UniformGrid : 指定行和列的数量, 均分有限的容器空间

    • Canvas : 使用固定的坐标设置元素的位置、不具备锚定停靠等功能。

五、WPF控件

常见控件分类

控件类型说明
布局控件可以容纳多个控件或嵌套其他布局控件,用于在UI上组织和排列控件。Grid、StackPanel、DockPanel等控件都属于此类,它们拥有共同的父类Panel
内容控件只能容纳一个其他控件或布局控件作为它的内容。Window、Button等控件属于此类、因为只能容纳一个控件作为起内容,所以经常需要借助布局控件来规划起内容。它们共同父类是ContentControl。
带标题内容控件相当于一个内容控件,但可以添加一个标题(Header),标题部分亦可以容纳一个控件或者布局。GroupBox、TabItem等是这类控件的典型代表。它们的共同父类是HeaderedControl。
条目控件可以显示一列数据,一般情况下这列数据的类型相同。此类控件包括ListBox、CombBox等。它们的共同基类是ItemsControl。此类控件在显示集合类型的数据方面功能非常强大。
带标题条目控件相当于一个条目控件加上一个标题显示区。TreeViewItem、MenuItem都属于此类控件。这类控件往往用于显示层级关系数据。节点显示在其Header区域,子节点则显示在其条目控件区域。此类控件的共同基类是HeaderItemsControl。
特殊内容控件比如TextBox容纳的是字符串、TextBlock可以容纳自由控制格式的文本、Image容纳图片类型数据……这类控件相对比较独立。

控件派生关系图

控件说明

六、样式和触发器

样式

WPF中的各类控件元素, 都可以自由的设置其样式。 诸如:

  • 字体(FontFamily)

  • 字体大小(FontSize)

  • 背景颜色(Background)

  • 字体颜色(Foreground)

  • 边距(Margin)

  • 水平位置(HorizontalAlignment)

  • 垂直位置(VerticalAlignment) 等等。

而样式则是组织和重用以上的重要工具。不是使用重复的标记填充XAML, 通过Styles创建一系列封装所有这些细节的样式。然后通过Style属性应用封装好的样式。这点类似于CSS样式。然而, WPF样式的功能更加强大, 如控件的行为。WPF的样式还支持触发器。

为了能够直观了解到样式(Style)的使用方法, 下面演示一个从传统的定义控件样式到使用Style组织样式的方法。

下面的例子中, 给4个TextBlock设置同样的样式: 字体、字体大小、字体颜色、加粗设置。

效果图与实际代码如下所示:

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
    <TextBlock Text="TextStyle1" FontFamily="宋体" FontSize="20" Foreground="Blue" />
    <TextBlock Text="TextStyle2" FontFamily="宋体" FontSize="20" Foreground="Blue"/>
    <TextBlock Text="TextStyle3" FontFamily="宋体" FontSize="20" Foreground="Blue" />
    <TextBlock Text="TextStyle4" FontFamily="宋体" FontSize="20" Foreground="Blue" />
    <TextBlock Text="TextStyle5" FontFamily="宋体" FontSize="20" Foreground="Blue" />
</StackPanel>

上面有讲到, 样式是组织和重用的工具。 而上面的代码, 由于每个元素都是相同的, 但是每个元素XAML都重复定义。 下面将介绍通过样式如何优化上面的代码。

  • 第一步: 在Resources目录下定义一个TextBlockStyle的样式, 完整代码如下:

<Window.Resources>
    <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="FontFamily" Value="宋体" />
        <Setter Property="FontSize" Value="20" />
        <Setter Property="Foreground" Value="Blue" />
    </Style>
</Window.Resources>
  • 第二步:通过控件的Style属性 来引用x:key 的样式, 代码如下:

<Window.Resources>
    <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="FontFamily" Value="宋体" />
        <Setter Property="FontSize" Value="20" />
        <Setter Property="Foreground" Value="Blue" />
    </Style>
</Window.Resources>
<Grid>
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock Text="TextStyle1" Style="{StaticResource TextBlockStyle}"/>
        <TextBlock Text="TextStyle2" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="TextStyle3" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="TextStyle4" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="TextStyle5" Style="{StaticResource TextBlockStyle}" />
    </StackPanel>
</Grid>

注意: 当控件引用了某个样式, 在控件本身并没有定义该属性的情况下,优先使用样式中的定义,否则优先控件本身的定义。如下所示, 样式中设置了颜色为 Blue, 但是控件本身又设置了Green, 那么控件的最终效果 Green。

<Window.Resources>
    <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="FontFamily" Value="宋体" />
        <Setter Property="FontSize" Value="20" />
        <Setter Property="Foreground" Value="Blue" />
    </Style>
</Window.Resources>
<Grid>
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock Text="TextStyle1" Foreground="Green" 
                   Style="{StaticResource TextBlockStyle}"/>
        <TextBlock Text="TextStyle2" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="TextStyle3" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="TextStyle4" Style="{StaticResource TextBlockStyle}" />
        <TextBlock Text="TextStyle5" Style="{StaticResource TextBlockStyle}" />
    </StackPanel>
</Grid>

触发器

顾名思义, 触发器可以理解为, 当达到了触发的条件, 那么就执行预期内的响应, 可以是样式、数据变化、动画等。 触发器通过 Style.Triggers集合连接到样式中, 每个样式都可以有任意多个触发器, 并且每个触发器都是 System.Windows.TriggerBase的派生类实例, 以下是触发器的类型:

  • Trigger : 监测依赖属性的变化、触发器生效

  • MultiTrigger : 通过多个条件的设置、达到满足条件、触发器生效

  • DataTrigger : 通过数据的变化、触发器生效

  • MultiDataTrigger : 多个数据条件的触发器

  • EventTrigger : 事件触发器, 触发了某类事件时, 触发器生效。

Trigger

<Window.Resources>
    <Style x:Key="BorderStyle" TargetType="{x:Type Border}">
        <Setter Property="BorderThickness" Value="5" />
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="Blue"/>
                <Setter Property="BorderBrush" Value="Red"/>
            </Trigger>
            <Trigger Property="IsMouseOver" Value="False">
                <Setter Property="Background" Value="Red" />
                <Setter Property="BorderBrush" Value="Blue" />
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<Grid>
    <Border Width="100" Height="100" Style="{StaticResource BorderStyle}" />
</Grid>

实际效果:

MultiTrigger

和Trugger类似, MultiTrigger可以设置多个条件满足时, 触发, 下面以TextBox为例, 做一个简单的Demo 当鼠标进入文本框的范围, 并且光标设置到TextBox上, 则把TextBox的背景颜色改变成Red

<Window.Resources>
    <Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
        <Setter Property="BorderThickness" Value="1" />
        <Style.Triggers>
            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="IsMouseOver" Value="True"/>
                    <Condition Property="IsFocused" Value="True"/>
                </MultiTrigger.Conditions>
                <MultiTrigger.Setters>
                    <Setter Property="Background" Value="Red"/>
                </MultiTrigger.Setters>
            </MultiTrigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<Grid>
    <StackPanel VerticalAlignment="Center">
        <TextBox Width="100" Height="30" Style="{DynamicResource TextBoxStyle}"/>
        <Button Width="100" Height="30" Margin="0 10 0 0"/>
    </StackPanel>
</Grid>

实际效果:

七、WPF Binding

什么是绑定(Binding)

在winform中, 我们常常会用到各种类型的赋值, 例如:

  • button1.Text="Hello";

  • label.Text="Hello";

  • ...

类似这种赋值操作, 我们之所以不称之为绑定, 主要原因是因为他们大多数操作都是一次性的, 无论是数据还是按钮本身发生变化,对两者而言都是不可见的。 而绑定的概念则侧重于: 两者的关联,协议与两者之间的影响。 首先, 从一个简单的例子来理解什么是绑定。

  • 创建一个滑块控件, 并且希望在滑动的过程中, 把值更新到另外一个静态文本上。代码如下:

    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
        <Slider Name="slider" Width="200"/>
        <TextBlock Text="" HorizontalAlignment="Center"/>
    </StackPanel>

在winform中, 我们常规的做法会给滑块创建一个值改变事件,同时将滑块的值赋值给文本。 接下来, 我只需要在静态文本中添加一小段绑定的声明,即可完整原本很复杂的操作:

  • Text={Binding ElementName=slider,Path=Value}

    • {Binding }: Binding的声明语法, 一对尖括号,开头声明以Binding 开始。

    • ElementName= : 该声明意为, 设置元素的名称

    • Path: 设置关联元素的位置,上例中设置为元素的value属性。

那么该如何理解整句话的意义, 翻译: 静态文本TextBlock的Text属性将通过绑定的方式关联到元素名'slider'的value属性上。

<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
    <Slider Name="slider" Width="200"/>
    <TextBlock Text="{Binding ElementName=slider, Path=Value}"                                          HorizontalAlignment="Center"/>                         
</StackPanel> 

效果图所示:

可以看到,在滑块不断的滑动过程中, TextBlock也在不断的发生变化, 说明TextBlock已经得到了滑动滑动过程中的值变化, 这种关联, 我们称之为绑定, 在WPF当中, 绑定又分很多种, 而上面这种则是通过元素绑定的方式。 理解了基础的绑定之后,然后就是理解绑定的模式。

绑定的模式就类似我们商业中的合作, 是一次性回报还是持续获益, 是否可以单方面终止, 是否具有投票权等, 在WPF中绑定的模式又分为五种:

  • OneWay(单向绑定) : 当源属性发生变化更新目标属性, 类似上面的例子中, 滑动变化更新文本的数据。示例:

    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
        <Slider Name="slider" Width="200"/>
        <TextBlock Text="{Binding ElementName=slider, Path=Value ,Mode=OneWay}"
                           HorizontalAlignment="Center"/>
    </StackPanel>

效果:

  • TwoWay(双向绑定) : 当源属性发生变化更新目标属性, 目标属性发生变化也更新源属性。

    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
        <Slider Name="slider" Width="200"/>
        <TextBlock Text="{Binding ElementName=slider, Path=Value ,Mode=TwoWay}"
                           HorizontalAlignment="Center"/>
    </StackPanel>

  • OneTime(单次模式) : 根据第一次源属性设置目标属性, 在此之后所有改变都无效。

    • 如第一次绑定了数据源为0, 那么无论后面如何改变 2、3、4... 都无法更新到目标属性上。

  • OneWayToSource : 和OneWay类型一样, 只不过整个过程倒置。示例:

    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
        <Slider Name="slider" Width="200"/>
        <TextBlock Text="{Binding ElementName=slider, Path=Value ,Mode=OneWayToSource}"
                           HorizontalAlignment="Center"/>
    </StackPanel>

  • Default : 既可以是双向,也可以是单项, 除非明确表明某种模式, 否则采用该默认绑定。

绑定到非元素上

上面的代码中,使用的绑定方式是根据元素的方式: ElementName=xxx, 如需绑定到一个非元素的对象, 则有一下几属性:

  • Source : 指向一个数据源, 示例, TextBox使用绑定的方式用Source指向一个静态资源ABC:

    <Window.Resources>
        <TextBlock x:Key="txt1">abc</TextBlock>
    </Window.Resources>
    <Grid>
        <TextBox Text="{Binding Source={StaticResource txt1},Path=Text}"/>
    </Grid>
  • RelativeSource : 使用一个名为RelativeSource的对象来根据不同的模式查找源对象。

    示例, 使用RelativeSource的FindAncestor模式, 查找父元素为StackPanel的Width值

    <StackPanel Width="200">
     <StackPanel Width="300"/>
     <!--TextBlock 的Text值为200-->
     <TextBlock Text="{Binding Path=Width,
                 RelativeSource={RelativeSource Mode=FindAncestor,
                 AncestorType={x:Type StackPanel}}}"/>
    </StackPanel>

  • DataContext: 从当前的元素树向上查找到第一个非空的DataContext属性为源对象。

    示例, 该示例用后台代码创建一个只包含Name的类, Test, 通过绑定窗口的DataContext上下文:

    public partial class Window1 : Window
     {
         public Window1()
         {
             InitializeComponent();
             this.DataContext = new Test() { Name ="小明"}
         }
         public class Test
         {
             public string Name { get; set; }
         }
     }
    <Grid>
     <!-- 绑定后台生成的Name-->
     <TextBlock Text="{Binding Name}" />
    </Grid>

  • 后台代码绑定简单文本与列表

    创建一个PageModel类, 定一个ClassName为班级名称, 和一个Students学生列表,

    后台代码:

     public partial class Window1 : Window
     {
         public Window1()
         {
             InitializeComponent();
             PageModel page = new PageModel();
             page.ClassName = "高二三班";
             page.Studets = new List<Student>();
             page.Studets.Add(new Student() 
                              { Name=  "张三", Age = 18, Sex = "男" });
             page.Studets.Add(new Student()
                              { Name = "李四", Age = 19, Sex = "女" });
             page.Studets.Add(new Student() 
                              { Name = "王五", Age = 20, Sex = "男" });
             this.DataContext = page;
         }
         public class PageModel
         {
             public string ClassName { get; set; }
             public List<Student> Studets { get; set; }
             public class Student
             {
                 public string Name { get; set; }
                 public int Age { get; set; }
                 public string Sex { get; set; }
             }
         }
     }

    窗口代码:

    <Grid>
     <Grid.RowDefinitions>
         <RowDefinition Height="20"/>
         <RowDefinition/>
     </Grid.RowDefinitions>
     <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
         <Button Command="{Binding UpdateCommand}" Content="刷新"/>
         <TextBlock Margin="5 0 0 0" Text="班级名称:"/>
         <TextBlock Margin="5 0 0 0" Text="{Binding ClassName}"/>
     </StackPanel>
     <DataGrid Grid.Row="1" ItemsSource="{Binding Students}" AutoGenerateColumns="False">
         <DataGrid.Columns>
             <DataGridTextColumn Binding="{Binding Name}" Header="姓名"/>
             <DataGridTextColumn Binding="{Binding Age}" Header="年龄"/>
             <DataGridTextColumn Binding="{Binding Sex}" Header="性别"/>
         </DataGrid.Columns>
     </DataGrid>
    </Grid>

    效果预览

八、WPF事件

参考:

https://blog.csdn.net/akadiao/article/details/95757744?spm=1001.2014.3001.5501

  • 事件处理器与代码后置

    CLR事件模型/直接事件模型:在下图所示的事件模型中,事件的响应者通过订阅直接关联在事件拥有者的事件。

示例 事件拥有者:Button 事件:Click 事件响应者:窗体 事件处理器:button1_Click()方法

<Grid Margin="10">
    <Button x:Name="Button" Content="确定" Width="90" Height="30" Background="LightPink"               Click="Button_Click"/>
</Grid>

后台代码

public partial class MainWindow: Window {
	public MainWindow() {
		InitializeComponent();
	}
	private void Button_Click(object sender, RoutedEventArgs e) {
		MessageBox.Show("Hello WPF!");
	}
}

效果如图:

路由事件

事件的路由环境是UI组件树。分为逻辑树(Logical Tree),可视元素树(Visual Tree)。

  • 逻辑树:

逻辑树就是描述WPF界面元素的实际构成,它是由程序在XAML中所有的UI元素组成。最显著的特点就是由布局控件、或者其他常用的控件组成。

  • 可视元素树:

可视树是由界面上可见的元素构成的,这些元素主要是由从Visual或者Visual3D类中派生出来的类。 上面代码中的Window、Grid、StackPanel、TextBox它们本身就包含一些由Visual或者Visual3D类派生出的一些可视树的元素来组成的。

路由事件定义

功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。 实现定义:路由事件是一个 CLR 事件,可以由 RoutedEvent 类的实例提供支持并由WPF 事件系统来处理。

前台代码(简化,去掉了控件背景色)

<Grid>
   <Grid Name="RootSecond"  HorizontalAlignment="Center" ButtonBase.Click="Button_Click">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" MinWidth="100"/>
            <ColumnDefinition Width="*" MinWidth="100"/>
        </Grid.ColumnDefinitions>
        <StackPanel Name="LeftPanel" Grid.Column="0"  HorizontalAlignment="Center" 
                        VerticalAlignment="Center">
           <Button Name="LeftButton" Width="50" Content="Left" Click="LeftButton_Click"/>
        </StackPanel>
        <StackPanel Name="RightPanel" Grid.Column="1" HorizontalAlignment="Center" 
                        VerticalAlignment="Center">
            <Button Name="RightButton" Width="50" Content="Right"/>
        </StackPanel>
    </Grid>
</Grid>

后台代码

private void LeftButton_Click(object sender, RoutedEventArgs e) {
	MessageBox.Show("这是一个点击事件");
}

private void Button_Click(object sender, RoutedEventArgs e) {
	if ("RightButton".Equals((e.OriginalSource as FrameworkElement).Name)) {
		MessageBox.Show("这是一个路由事件");
	}
}

九、WPF命令

WPF命令模型具有4个重要元素:

  • 命令——命令表示一个程序任务,并且可跟踪该任务是否能被执行。然而,命令实际上不包含执行应用程序的代码,真正处理程序在命令目标中。

  • 命令源——命令源触发命令,即命令的发送者。例如Button、MenuItem等控件都是命令源,单击它们都会执行绑定的命令。

  • 命令目标——命令目标是在其中执行命令的元素。如Copy命令可以在TextBox控件中复制文本。

  • 命令绑定——前面说过,命令是不包含执行程序的代码的,真正处理程序存在于命令目标中。那命令是怎样映射到处理程序中的呢?这个过程就是通过命令绑定来完成的,命令绑定完成的就是红娘牵线的作用。

WPF命令模型的核心就在于ICommand接口了,该接口定义命令的工作原理。该接口的定义如下所示:

public interface ICommand
{
    // Events
    event EventHandler CanExecuteChanged;
    // Methods
    //命令是否可使用
    bool CanExecute(object parameter); 
    //命令后续执行的方法
    void Execute(object parameter);
}

代码绑定命令示例:

后端代码

public partial class MainWindow: Window {

	//申明并定义命令
	private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(MainWindow));

	public MainWindow() {
		InitializeComponent();
		//命令初始化
		InitialiizeCommand();
	}
	//初始化命令
	private void InitialiizeCommand() {
		//把命令赋值给命令源(发送者)并指定快捷方键
		this.ClearButton.Command = this.clearCmd;
		this.clearCmd.InputGestures.Add(new KeyGesture(Key.C, ModifierKeys.Alt));

		//指定命令目标
		this.ClearButton.CommandTarget = this.searchValue;

		//创建命令联
		CommandBinding cb = new CommandBinding();
		cb.Command = this.clearCmd; //只关注与clearCmd相关事件
		cb.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);
		cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);

		//把命令关联安置到外围控件上
		this.tabPanel.CommandBindings.Add(cb);
	}

	//当探测命令是否可以执行是,此方法被调用
	void cb_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
		if (string.IsNullOrEmpty(this.searchValue.Text)) {
			e.CanExecute = false;
		} else {
			e.CanExecute = true;
		}
		//避免继续向上传而降低程序性能
		e.Handled = true;
	}

	void cb_Executed(object sender, ExecutedRoutedEventArgs e) {
		this.searchValue.Clear();
		//避免继续向上传而降低程序性能
		e.Handled = true;
	}
}

前端代码

<StackPanel Grid.Row="0" Margin="5" HorizontalAlignment="Left" VerticalAlignment="Center">
	<TabPanel Name="tabPanel">
		<TextBlock Text="用户姓名:" />
		<TextBox Name="searchValue" Width="100" />
		<Button Name="ClearButton" Content="命令方式清除" HorizontalAlignment="Right"
		/>
	</TabPanel>
</StackPanel>

命令传参

前端代码

<StackPanel Grid.Row="0" Margin="5" Width="200" Height="150">
    <TabPanel Name="tabPanel" Width="200" Height="50">
        <TextBlock Text="用户姓名:" />
        <TextBox Name="searchValue" Width="100" />
    </TabPanel>
    <TabPanel Name="tabPane2" Width="200" Height="50">
        <Button  Command="New" Content="命令传参添加" CommandParameter="new"/>
        <Button  Command="New" Content="命令传参添加" CommandParameter="show"/>
    </TabPanel>
</StackPanel>

后端代码

public partial class MainWindow: Window {
	public MainWindow() {
		InitializeComponent();
	}
	//当探测命令是否可以执行是,此方法被调用
	void New_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
		e.CanExecute = true;
		e.Handled = true;
	}
	void New_Executed(object sender, ExecutedRoutedEventArgs e) {
		if (e.Parameter.ToString() == "show") {
			MessageBox.Show("命令方式传参“Show”");
		} else if (e.Parameter.ToString() == "new") {
			this.searchValue.Text += e.Parameter.ToString();
		}
		//避免继续向上传而降低程序性能
		e.Handled = true;
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值