WPF#基础

5 篇文章 0 订阅

WPF#基础

1、Wpf中的ResourceDirectory

WPF对象资源的定义和查找
每个WPF界面元素都有一个名为Resource的属性,这个属性继承至FrameworkElement类,其类型为ResourceDictionary。ResourceDictionary能够以键值对的形式存储资源,当要使用到某个资源的时候,使用键值对的形式获取资源对象。在保存资源时,ResourceDictionary视资源对象为Object类型,所以再使用资源时先要对资源对象进行类型转换,XAML编译器能够根据Attribute自动识别资源类型,如果类型不对就会抛出异常,但在C#中检索到资源对象之后,类型转换的事情就只能由我们自己来做了。ResourceDictionary可以存储任意类型的对象。在XAML代码中向Resource添加资源时需要把正确的命名空间引入到XAML代码中.
ResourceDictionary具有一个名为Source的属性,只要把包含资源定义的文件路径赋值给这个属性就一切搞定了!例如把皮肤以资源的形式放在XAML文件中,使用时仅需要将相应的XAML文件添加进项目并使用Source属性进行引用.

静态资源和动态资源

StaticResource(静态资源)
  • 程序的非执行状态
  • 指的是在程序载入内存时对资源的一次性使用,之后就不在访问这个资源了
DynamicResource(动态资源)
  • 程序运行状态
  • 指的是在程序运行过程中仍然会去访问资源

如果确定某些资源只在程序初始化时使用一次、之后不会再改变,就应该使用StaticResource,而程序运行过程中还有可能改变的资源应该以DynamicResource形式来使用。

备注

如果想让外部文件编译进目标成为二进制资源,必须在属性窗口中把文件的BuildAction属性值设为Resource,并不是每种文件都会自动设为Resource,比如图片文件会,MP3文件就不会。一般情况下如果BuildAction属性被设为Resoure,则Copy to Output Directory属性就设为Do not copy;如果不希望以资源的形式使用外部文件,可以把Build Action设为None,而把Copy to Output Directory设为Copy always,另外,Build Action属性的下拉列表里有一个颇具迷惑性的值Embeded Resource,不要选择这个值。

2、ObservableCollection

表示一个动态集合,在添加项,移除项或刷新整个列表时,此集合将提供通知。
ObservableCollection : Collection,INotifyCollectionChanged,INotifyPropertyChanged
在许多情况下,所使用的数据是对象的集合.例如,数据绑定中的一个常见方案是使用ItemsControl(如ListBox,ListView或TreeView)来显示记录的集合.
可以枚举实现 IEnumerable 接口的任何集合。 但是,若要设置动态绑定,以便集合中的插入或删除操作可以自动更新 UI,则该集合必须实现 INotifyCollectionChanged 接口。 此接口公开 CollectionChanged 事件,只要基础集合发生更改,都应该引发该事件。
WPF 提供 ObservableCollection 类,它是实现 INotifyCollectionChanged 接口的数据集合的内置实现。
还有许多情况,我们所使用的数据只是单纯的字段或者属性,此时我们需要为这些字段或属性实现INotifyPropertyChanged接口,实现了该接口,只要字段或属性的发生了改变,就会提供通知机制。

3、BindableCollection

BindableCollection是ObservableCollection的子类。如果在viewmodel中有一个东西的集合,并且想将它用作ItemsSource在View中的东西(并且在将项目添加到该集合中或从该集合中删除时,都会通知View),则使用该类。
额外的功能

  • AddRange,RemoveRange和Refresh方法
  • 是线程安全的

AddRange和RemoveRange允许一次添加一个删除一系列元素,而不必手动遍历每个元素collection.Add(element)(同时为添加的每个元素引发大量事件)。AddRange和RemoveRange只会在每个添加/删除的范围内引发一组事件。
Refresh不会以任何方式修改集合,但是会触发PropertyChanged和CollectionChanged事件,从而向任何UI元素指示集合已被修改,并且他们应该重新加载其数据。
通过将所有操作(添加、删除、清楚、重置等)分派到UI线程来实现线程安全,调度使用Execute.OnUIThreadSync,这意味着:
·这些操作是同步的:被调用的方式在操作完成之前是不会返回的。
·如果已经在UI-Thread上,则它们是免费的,在这种情况下,该操作将同步执行。
·所有PropertyChanged和CollectionChanged事件总是在UI线程上引发的。

4、Binding

Text=”{Binding myText}”
1.绑定源,mcl,指的是对象
2.绑定路径,理解为绑定源的某个属性,myClass类型的mytext属性,绑定路径指的是绑定源对象里的某个属性
3.绑定目标对象,TextBox和TextBlock对象
4.绑定目标属性,TextBox和TextBlock对象的Text属性,绑定目标对象一定是依赖项属性
<TextBlock HorizontalAlignment=”left” VerticalAlignment=”Center” Text=”{Binding Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}”>

(1)绑定流方向,绑定流方向是由Binding类对象的Mode属性设置的,它共有四种方向:

  • OneWay,绑定源的属性值可以影响绑定目标属性的值;
  • TwoWay,绑定源的属性值可以影响绑定目标属性的值,同样的绑定目标属性的值改变也会影响绑定源属性的值;
  • OneWayToSource,绑定目标属性的值改变可以影响绑定源属性的值;
  • OneTime,绑定源属性的值在初始化时绑定到绑定目标属性上的值上,后面不再有任何影响。

(2)目标反过来想改变源:也就是方向mode属性是OneWayToSource或者TwoWay,这种情况下还涉及到Binding类型对象的UpdateSourceTrigger属性,它就是表示影响目标的触发方式。UpdateSourceTrigger触发器类型又分为三种:

  • LostFocus,当控件失去焦点时改变源属性值,默认值;
  • PropertyChanged,每当控件的属性值改变都立刻改变源属性值;
  • Explicit,用户自己通过调用UpdateSource方法来改变源属性的值,控制权在用户手上。

(3)源想改变目标,需要实现INotifyPropertyChanged接口。
(4)Binding还支持多级路径。比如想让一个TextBox显示另一个TextBox的文本长度,我们可以写:

<StackPanel>
	<TextBox x:Name="textBox1" BorderBrush="Black" Margin="5">
	<TextBox x:Name="textBox2" Text=“{Binding Path=Text.Length,ElementName=textBox1,Mode=OneWay}”  BorderBrush="Black" Margin="5">
</StackPanel>

等效的C#代码是:

this.textBox2.SetBinding(TextBox.TextProperty,new Binding("Text.Length"){Source=this.textBox1,Mode=BindignMode.OneWay})

(5)“没有Path”的Binding
Binding源本身就是数据且不需要Path来指明。典型的,string、int等基本数据类型就是这样,他们的实例本身就是数据,无法通过它的哪个属性来访问这个数据,这时只需将Path设为“.”即可。

Text="{Binding Path=.,Source={StaticResource Resource=myString}}"

(6)在使用集合类型作为列表控件的ItemSource时一般会考虑使用ObservableCollection代替List,因为ObservableCollection类实现了INotifyCollectionChanged和INotifyPropertyChanged接口,能把集合的变化立刻通知显示它的列表控件,改变就会立刻显现出来。
(7)路由事件(Routed Event)
首先,在创建Binding时要把Binding对象的NotifyOnValidationError属性设为True,这样,当数据校验失败的时候Binding就会像报警器一样发出一个信号,这个信号会以Binding对象的Target为起点在UI元素树上传播。信号每到达一个结点,如果这个结点上设置有对这种信号的侦听器(事件处理器),那么这个侦听器就会被触发用以处理这个信号。信号处理完后,程序员还可以选择是让信号继续向下传播还是就此终止–这就是路由事件,信号在UI元素树上的传递过程就称为路由(Route)。
(8)数据转换
手动写Convert,方法是创建一个类并让这个类实现IValueConverter接口。IValueConverter接口定义如下:

oublic interface IValueConverter
{
	object Convert(object value,Type targetType,object parameter,CaltureInfo culture);	//当数据从Binding的Source流向Target
	object ConvertBack(object value,Type targetType,object parameter,CultureInfo culture);
}

Binding对象的Mode属性会影响到这两个方法的调用。如果Mode为TwoWay或Default行为与TwoWay一致则两个方法都有可能被调用;如果Mode为OneWay或Default行为与OneWay一致则只有Convert方法会被调用。

//错误显示转换器
public class PhysicDetailErrorConvert : IValueConverter
{
	public object Convert(object value,Type targetType,object parameter,CaltureInfo culture)
	{
		var isError = (bool)value;
		if(isError)
		{
			return new SolidColorBrush(Color.Red);
		}
		return new SolidColorBrush(Color.Black);
	}
	public object ConvertBack(object value,Type targetType,object parameter,CultureInfo culture)
	{
		throw new NotImplementException();
	}
}
//XAML调用
<Window.Resource>
	<ResourceDirectory>
		<converts:PhysicDetailErrorConvert x:Key="PhysicDetailErrorConvert"/>
	</ResourceDirectory>
</Window.Resource>
<DataGrid.Columns>
	<DataGridTemplateColumn Header="值" Width="*">
		<DataGridTemplateColumn.CellTemplate>
			<DataTemplate>
				<Grid>
					<TextBlock HorizontalAligment="Left" VerticalAligment="Center" Text="{Binding DisplayValue}" Margin="3,0,0,0" Foreground="{Binding IsError ,Converter={StaticResource PhysicDetailErrorConverter}}"/>
				</Grid>
			</DataTemplate>
		</DataGridTemplateColumn.CellTemplate>
	</DataGridTemplateColumn>
</DataGrid.Columns>

(9)MultiBinding(多路Binding)
MultiBinding具有一个名为Bindings的属性,其类型是Collection,通过这个属性MultiBinding把一组Binding对象集合起来,处在这个集合中的Binding对象可以拥有自己的数据校验与转换机制,他们汇集起来的数据将共同决定传往MultiBinding目标的数据。

public class DataPositionSeqConverter : IMultiValueConverter
{
	public object Convert(object[] values,Type targetType,object parameter,CaltureInfo culture)
	{
		if(values[0]!=null)
		{
			...
			return tempStr;
		}
		return string.Empty;
	}
	public object[] ConvertBack(object value,Type[] targetType,object parameter,CultureInfo culture)
	{
		return null;
	}
}

<DataGrid.Columns>
	<DataGridTemplateColumn Header="字节顺序" Width="*">
		<DataGridTemplateColumn.CellTemplate>
			<DataTemplate>
				<TextBlock HorizontalAligment="Left" VerticalAligment="Center" Text="{Binding DisplayValue}" Margin="10,0,0,0" >
					<TextBlock.Text>
						<MultiBinding Convert="{StaticResource DataPositionSeqConverter}" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
							<Binding/>
							<Binding Path="VM.IsByte" RelativeSource={RelativeSource AncestorType=UserControl}>
						</MultiBinding>
					</TextBlock.Text>
				</TextBlock>
			</DataTemplate>
		</DataGridTemplateColumn.CellTemplate>
	</DataGridTemplateColumn>
</DataGrid.Columns>

备注

WPF的核心是数据驱动UI,支撑这个理念的基础就是Data Binding和与之相关的数据校验与转换。在使用Binding时,最重要的事情就是准确的设置它的源和路径。

5、依赖项属性

依赖项属性的用途在于提供一种方法来基于其他输入的值计算属性值。
在wpf体系中,只有定义属性为依赖项属性,这个属性才支持样式设置,数据绑定、继承、动画和默认值。
首先,属性所在的类要直接或间接继承DependencyObject,这个类生成的对象表示一个具有依赖项属性的对象,这些对象,都能享用WPF的属性系统(属性系统主要是计算属性的值,并提供有关值已更改的系统通知)方面的服务。
这个类有两个比较重要的方法,GetValue(返回当前对象依赖项属性的当前有效值)和SetValue(设置依赖项属性的本地值)。
其次,属性对应的字段必须是共有、静态、只读的,类型为DependencyProperty。即public static readonly DependencyProperty字段名,同时字段的命名也有规范,属性名+Property,字段在定义时,通过DependencyProperty.Register来注册属性(只有注册了,才能使用WPF属性系统的服务)。
Register方法有三种重载,如下:

名称说明
Register(string,Type,Type)使用指定的属性名称、属性类型和属性所在对象的类型
Register(string,Type,Type,PropertyMetadata)使用指定的属性名称、属性类型、属性所在对象的类型和属性元数据注册依赖项属性
Register(string,Type,Type,PropertyMetadata,ValidateValueCallback)使用指定的属性名称、属性类型、属性所在对象的类型、属性元数据和属性的值验证回调来注册依赖项属性

在Register中,各个参数解释如下:
String:依赖属性的名称(不加Property,即字段的名字);
Type:属性的类型;
Type:属性所属对象的类型;
PropertyMetadata:依赖项对象的属性元数据,是一个PropertyMetadata类型,可能赋初始值。PropertyMetadata有一个object的构造函数;
ValidateValueCallback:表示用作回调的方法,这个类型是一个委托,用于验证依赖项属性的值的有效性,因为是委托,故它的构造参数为一个方法名。

6、拖放Drop

AllowDrop="True"

Drop通常是指一种数据传输方法,其中涉及使用鼠标(或某些其他定点设备)选择一个或多个对象,将这些对象拖到用户界面(UI)中某个所需的放置目标上,然后放置他们。
拖放操作通常涉及两个方面:拖动源(从中产生拖动对象)和放置目标(接收放置对象)。拖动源和放置目标可以是同一应用程序或不同应用程序中的UI元素。
可以通过拖放操作的对象的类型和数量是完全任意的。例如,文件、文件夹和内容选择是通过拖放操作处理的一些较常见的对象。
在拖放操作期间特定操作是特定于应用程序的,并且通常由上下文确定。例如,默认情况下,将选择的文件从一个文件夹拖到同一设备上的另一个文件夹将移动文件,而将文件从通用命名约定(UFC)共享拖到本地文件夹将默认复制文件。

数据传输

拖放是更通用的数据传输领域的一部分。数据传输包括拖放和复制粘贴操作。拖放操作类似于复制和粘贴或剪切和粘贴操作,该操作用于通过使用系统剪切板将数据从一个对象或应用程序传输到另一个对象或应用程序。两种类型的操作都需要:

  • 提供数据的源对象
  • 一种临时存储传输数据的方法
  • 接收数据的目标对象

在复制和粘贴操作中,系统剪贴板用于临时存储传输的数据。在拖放操作中,DataObject用于存储数据。从概念上讲,数据对象由一对或多对对象组成,这些对象包含实际数据以及相应的数据格式标识符。
拖动源通过调用静态DragDrop.DoDragDrop方法并将传递的数据传递给它来启动拖放操作。如果需要,DoDragDrop方法讲自动讲数据包装在DataObjecr中。为了更好地控制数据格式,可以在数据传递到DoDragDrop中提取数据。

Private void DgPackageParams_OnPreviewMouseMove(Object sender,MouseEventArgs e)
{
	if(e.RightButton == MouseButtonState.Pressed)
	{
		DragDrop.DoDragDrop(DgPackageParams,DgPackageParams.SelectedItems,DragDropEffects.Move);
	}
}

在MouseMove事件处理程序内部,调用DoDragDrop方法以启动拖放操作。所述的DoDragDrop方法有三个参数:

  • dragSource–对作为传输数据源的依赖对象的引用;这通常是MouseMove事件的来源;
  • data-包含传输数据的对象,包装在DataObject中。
  • allowedEffects-其中一个的DragDropEffects指定允许的拖和放操作的效果枚举值。

7、附加

名称描述
MouseDoubleClick当用户点击鼠标按钮两次或更多次时发生
PreviewMouseDoubleClick在用户单击鼠标按钮两次或更多次时发生
SelectionChanged当所选内容发生更改时,将发生此事件。 仅用户交互,还可以通过绑定和其他设置值更改选择
HorizontalScrollBarViaibility设置DataGrid中水平滚动条的可见性
CanUserReorderColumn用户可否对列进行重新排序
TextWrapping获取或设置TextBlock对文本进行换行的方式
BlankWindow.Draggable可拖拽
BlankWindow.Minimize窗体最小化
BlankWindow.Maximize窗体最大化
BlankWindow.Closeable窗体关闭

8、Grid

<Grid ShowGridLines='True'>
	<Grid.RowDefinitions>
		<RowDefinition Width="25"/>
		<RowDefinition Width="4"/>
		<RowDefinition Width="1*"/>
		<RowDefinition Width="*"/>
		<RowDefinition/>
	</Grid.RowDefinitions>
</Grid>

对于Grid的行高和列宽,可以设置三类值:

  • 绝对值:double数值加单位后缀
  • 比例值:double数值后加一个星号(*)
  • 自动值:字符串Auto

10、属性(Property)

C#语言规定:对类有意义的字段和方法用static关键字来修饰,称为静态成员,通过类名加访问修饰符(即“.”操作符)可以访问他们;对类的实例有意义的字段和方法不加static关键字,称为非静态成员或实例成员。
从语义方面来看,静态成员与非静态成员有着很好的对称性,但从程序在内存中的结构来看,这种对称就被打破了。静态字段在内存中只有一个拷贝,非静态字段则是每个实例拥有一个拷贝,无论方法是否为静态的,在内存中只会有一个拷贝,区别只是你能通过类名来访问存放指令的内存还是通过实例名来访问存放指令的内存。

class Human
{
	private int age;
	public void SetAge(int value)
	{
		this.age = value;
	}
	public void GetAge()
	{
		return this.age;
	}
}

当.NET Framework推出时,微软更进一步把Get/Set这对方法合并成为属性(Property)。使用属性时,格式上很像使用非private字段,保证了语义上的顺畅,同时又不失Get/Set方法的安全性,代码变得更加紧凑,自动提示菜单也短了很多,可谓一举多得。
使用属性,Human类可以改写成这样:

class Human
{
	private int age;
	public int Age
	{
		get{return this.age;}
		set{this.age=value}
	}
}

这种.NET Framework中的属性又称为CLR属性(CLR,Commom Language Runtime)。我们既可以说CLR属性是private字段的安全访问包装,也可以说一个private字段在后台支持(back)一个CLR属性。
C#代码中的属性的编译结果是两个方法。

9、模板(Template)

WPF中的Template分为两大类:

  • ControlTemplate是算法内容的表现形式,一个控件怎么祖师其内部结构才能让它更符合业务逻辑、让用户操作起来更舒服就是由它来控制的。控件的外衣。
  • DataTemplate是数据内容的表现形式,一条数据显示成什么样子,是简单的文本还是直观的图形动画就由它来决定。数据的外衣。

9.1、DataTemplate

数据模板类
DataTemplate常用的地方有3处,分别是:

  • ContentControl的ContentTemplate属性,相当于给ContentControl的内容穿衣服。
  • ItemsControl的ItemTemplate属性,相当于给ItemsControl的数据条目穿衣服。
  • GridViewColumn的CellTemplate属性,相当于给GridViewColumn单元格里的数据穿衣服。

内联的DataTemplate

<ListBox Width="400" Margin="10"
         ItemsSource="{Binding Source={StaticResource myTodoList}}">
   <ListBox.ItemTemplate>
     <DataTemplate>
       <StackPanel>
         <TextBlock Text="{Binding Path=TaskName}" />
         <TextBlock Text="{Binding Path=Description}"/>
         <TextBlock Text="{Binding Path=Priority}"/>
       </StackPanel>
     </DataTemplate>
   </ListBox.ItemTemplate>
 </ListBox>

资源部分中定义DataTemplate

<Window.Resources>
	<DataTemplate x:Key="myTaskTemplate">
	  <StackPanel>
	    <TextBlock Text="{Binding Path=TaskName}" />
	    <TextBlock Text="{Binding Path=Description}"/>
	    <TextBlock Text="{Binding Path=Priority}"/>
	  </StackPanel>
	</DataTemplate>
</Window.Resources>

现在可以使用MyTaskTemplate用作资源

<ListBox Width="400" Margin="10"
         ItemsSource="{Binding Source={StaticResource myTodoList}}"
         ItemTemplate="{StaticResource myTaskTemplate}"/>

备注

可以使用DataTemplate来指定数据对象的可视化。当您将ItemsControl(例如ListBox)绑定到整个集合时,DataTemplate对象特别有用。没有特定的说明,列表框将显示集合中对象的字符串表示形式。在这种情况下,您可以使用DataTemplate定义数据对象的外观。DataTemplate的内容成为数据对象的视觉结构。

ItemsControl可以包含多个项,可以包含字符串或完全用户自己定义的类对象,甚至可以包含Button按钮、StackPanel控件,它是ListBox,ListView,TreeView等其他控件的父类,然而ItemsControl也可以直接作为WPF的控件使用。

<ItemsControl BorderBrush="Red" BorderThickness="3" Cursor="Hand" Name="icl" ItemsSource="{Binding strlist}">

public partical class MainWindow:Window
{
	public List<string> strList { get; set; } = new List<string>;
	public MainWindow()
	{
		IntializeComponent();
		DataContext = this;	//需指定上下文,不然绑定无效
		strlist.Add("...");
		strlist.Add("...");
		strlist.Add("...");
	}
}


string s = "Hello";
Binding b = new Binding();
b.Mode = BindingMode.OneTime;
b.Source = s;

MyText.SetBinding(TextBlock.TextPrperty,b);
如果是通过Path来指定绑定源,那么后面必须跟着的是一个属性对象;
如果是通过source指定绑定源,则可以不是属性,可以是任意对象。

9.2、ControlTemplate

实际项目中,ControlTemplate主要有两大用武之地:

  • 通过更换ControlTemplate改变空间外观,使之具有更优的用户体验及外观。
  • 借助ControlTemplate,程序员与设计师可以并行工作,程序员可以先用WPF标准控件进行编程,等设计师的工作完成后,只需要把新的ControlTemplate应用到程序中就可以了。

9.3 DataTemplate与ControlTemplate的关系与应用

9.3.1 DataTemplate与ControlTemplate的关系

决定控件外观的是ControlTemplate,决定数据外观的是DataTemplate,他们正是Control类的Template和ContentTemplate两个属性的值。
由ControlTemplate生成的控件树其树根就是ControlTemplate的目标控件,此模板化控件的Template属性值就是这个ControlTemplate实例;与之相仿,由DataTemplate生成的控件树其树根是一个ContentPresenter控件,此模板化控件的ContentTemplate属性值就是这个DataTemplate实例。因为ContentPresenter控件是ControlTemplate控件树上的一个结点,所以DataTemplate控件树就是ControlTemplate控件树的一棵子树。

9.3.2 DataTemplate与ControlTemplate的应用

WPF准备了TreeView和MenuItem控件用来显示层级数据。能够帮助层级控件显示层级数据的模板是HierarchicalDataTemplate。

10、Style

构成Style最重要的两个元素是Setter和Trigger,Setter类帮助我们设置控件的静态外观风格,Trigger类则帮助我们设置控件的行为风格。

Style中的Setter

设置器,属性值。

<Style TargetType="TextBlock">
	<Setter Property="FontSize" Value="24"/>
	<Setter Property="TextDecorations" Value="Underline"/>
	<Setter Property="FontStyle" Value="Italic"/>
	<Setter Property="Template">
		<Setter.Value>
			<ControlTemplate TargetType="{x:Type DataGridCell}">
				<Border BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True" BorderBrush="Transparent">
					<ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
				</Border>
			</ControlTemplate>
		</Setter.Value>
	</Setter>
</Style>

如果想设置控件的ControlTemplate,只需要把Setter的Property设为Template并为Value提供一个ControlTemplate对象即可。

Style中的Trigger

触发器。

<Trigger Property="IsChecked" Value="true">
	<Setter Property="FontSize" Value="24"/>
	<Setter Property="TextDecorations" Value="Underline"/>
	<Setter Property="FontStyle" Value="Italic"/>
</Trigger>
<MultiTrigger>
	<MultiTrigger.Conditions>
		<Condition Property="IsSelected" Value="true"/>
		<Condition Property="Selector.IsSelectionActive" Value="false"/>
	</MultiTrigger.Conditions>
	<Setter Property="FontSize" Value="24"/>
	<Setter Property="TextDecorations" Value="Underline"/>
</MultiTrigger>
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值