WinForm教程(一)App.config等配置文件

最近又回到用WinForm开发项目,难免少不了跟数据库打交道。平时也会在App.config中写连接字串,但没有仔细深究过,当然除了写数据库字串,肯定还能写别的,我们无论在控制台程序还是窗体程序,都会遇见他,乘代码敲累之际,写篇博客平缓下。

应用程序配置文件是标准的 XML 文件,XML 标记和属性是区分大小写的。它是按需更改,开发人员可以使用配置文件来更改设置,而不必重编译应用程序。配置文件又分:(1)内置配置文件(2)用户自定义配置文件。内置配置文件主要包括App.config、Web.config、Settings.settings( 这个用的不多,操作也很简单,在此不详细叙述)等等。用户自定义配置文件一般是将配置信息放到XML文件或注册表中,配置信息一般包括程序设置,记录运行信息,保存控件的信息(比如位置,样式)。

(一)App.config

一般新建项目都会自动添加配置文件,如果没有可以选中项目,右击添加新建项,选择" 添加应用程序配置文件",即可完成添加,其根节点是固定的configuration。

常用的子节点有:connectionStrings、appSettings、configSections等,操作都略有不同。DotNet直接提供了操作各节点的后台方法,在用到ConfigurationManager时注意要添加System.Configuration.dll程序集的引用。通过下面程序例子,我们会有大致了解。

(1)connectionStrings

一般存放数据库连接字串,如开头所讲,注意后台获取方法。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>  
    <connectionStrings >
        <add name="MystrCon" connectionString="Server=58.210.75.190;Database=jkpt00003;User Id=root;Password=123456;Persist Security Info=True"/>
    </connectionStrings >
</configuration>

//不适用于配置节新增子元素
public static string conStr = System.Configuration.ConfigurationManager.ConnectionStrings["MystrCon"].ConnectionString;//后台获取方法

如果我们进行此connectionStrings节的增删改呢?首先不能通过上述后台ConfigurationManager方法直接赋值写入,会报错该配置是只读的。但肯定有别的方法进行完成诉求,通过Configuration配置对象来实现,请看下面程序。

Configuration config = System.Configuration.ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);        
config.ConnectionStrings.ConnectionStrings["suny"].ConnectionString = "is a suny?";//修改 
config.ConnectionStrings.ConnectionStrings.Add(new ConnectionStringSettings("cloudy", "is cloud"));//增加
//原始或新加子元素都能获取
string name_three = config.ConnectionStrings.ConnectionStrings["cloudy"].ToString();
config.Save(ConfigurationSaveMode.Modified);//只有加保存功能,*.vshost.exe.Config才会作改变
ConfigurationManager.RefreshSection("ConnectionStrings");

(2)appSettings

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
      <add key="connectionstring" value="Data Source=127.0.0.1;initial catalog=Guest;User ID =sa;password=eric19897222008 "/>
    </appSettings>
</configuration>
//private static string SQL_CONN_STR = System.Configuration.ConfigurationSettings.AppSettings["connectionstring"];//已经被废弃
//不能获取配直节新增子元素
private static string SQL_CONN_STR = System.Configuration.ConfigurationManager.AppSettings["connectionstring"];

注意:现在.Net FrameWork 2.0以上框架中已经明确表示此ConfigurationSettings属性已经废弃,建议改为 ConfigurationManager。

当然,也能像connectionStrings一样,也能进行增删改查,请看下面程序。

Configuration config = System.Configuration.ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["foggy"].Value = "is a fog?";//修改子节点
config.AppSettings.Settings.Remove("welcome");//删除子节点              
config.AppSettings.Settings.Add("url", "http://www.fx163.net");//增加子节点        
string get_ui = config.AppSettings.Settings["url"].Value;//获取子节点值,原配置或代码添加配置节子节点都适用   
config.Save(ConfigurationSaveMode.Modified);//只有加保存功能,*.vshost.exe.Config才会作改变
System.Configuration.ConfigurationManager.RefreshSection("AppSettings");

(3)configSections(自定义节点)

显然,用上面的两种默认的配置节,能满足基本使用。是否可以自定义配置节呢?当然可以了,我们可以自己定义各元素之间的关系,先看个简单的。

<configuration>
   <configSections>
      <section name="simple"  type="Appconfig.SimpleSection,Appconfig"/>
   </configSections>
   <simple maxValue="20" minValue="1"></simple>
   <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
   </startup>
</configuration>

Xml文件,注意三个点,一是configSections配置节,自定义的一定要写在其外面;二是name是新增节名字,Type是程序集命名空间+类名和空间;三是是必须作为第一个配置节,否则无法获取配置对象实例,前面搞定,我们敲开后台大门,先定义一个配置类。

一般需要做如下三件事:
1. 定义类型从System.Configuration.ConfigurationSection继承;
2. 定义配置类的属性,这些属性需要用ConfigurationProperty特性修饰,并制定属性在配置节中的名称和其他一些限制信息;
3. 通过基类的string索引器实现属性的get ,set;

//新加了个.cs类文件
namespace Appconfig
{
 
    public class SimpleSection : System.Configuration.ConfigurationSection
    {
        [ConfigurationProperty("maxValue", IsRequired = false, DefaultValue = Int32.MaxValue)]
        public int MaxValue
        {
            get
            {
                return (int)base["maxValue"];
            }
            set
            {
                base["maxValue"] = value;
            }
         }
         [ConfigurationProperty("minValue", IsRequired = false, DefaultValue = 1)]
         public int MinValue
            {
                get { return (int)base["minValue"]; }
                set { base["minValue"] = value; }
            }
         [ConfigurationProperty("enabled", IsRequired = false, DefaultValue = true)]
         public bool Enable
         {
             get
             {
                 return (bool)base["enabled"];
             }
             set
             {
                 base["enabled"] = value;
             }
          }
     }
}
//获取配置信息代码
Appconfig.SimpleSection simple = ConfigurationManager.GetSection("simple") as Appconfig.SimpleSection;
MessageBox.Show(simple.MinValue.ToString() + simple.MaxValue.ToString());

我们看个再复杂点的,配置节点自带属性值和子元素:

<configuration>
   <configSections>
      <section  name="complex"  type="Appconfig.ComplexSection,Appconfig"/>
   </configSections>
   <complex height="190">
      <child firstName="James" lastName="Bond"/>
   </complex>
   <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
   </startup>
</configuration>

这个配置的名字是complex,他有一个属性height,他的节点内还有一个child元素这个元素有两个属性firstName和lastName;对于这个内嵌的节点该如何实现呢?首先我们需要定义一个类,要从ConfigurationElement类继承,然后再用和SimpleSection类似的方法定义一些用ConfigurationProperty特性修饰的属性就可以了,当然属性值的get,set也要使用基类的索引器。如下实现:

namespace Appconfig
{
    class ComplexSection : ConfigurationSection 
    {
        [ConfigurationProperty("height", IsRequired = true)] 
        public int Height 
        { 
            get
            { 
                return (int)base["height"]; 
            } 
            set
            { 
                base["height"] = value; 
            } 
         } 
        [ConfigurationProperty("child", IsDefaultCollection = false)] 
        public ChildSection Child 
        { 
            get
            { 
                return (ChildSection)base["child"]; 
            } 
            set
            { 
                base["child"] = value; 
            } 
         } 
     }
    public class ChildSection : ConfigurationElement
    {
        [ConfigurationProperty("firstName", IsRequired = true, IsKey = true)]
        public string FirstName
        {
            get
            {
                return (string)base["firstName"];
            }
            set
            {
                base["firstName"] = value;
            }
        }

        [ConfigurationProperty("lastName", IsRequired = true)]
        public string LastName
        {
            get
            {
                return (string)base["lastName"];
            }
            set
            {
                base["lastName"] = value;
            }
        }
    }
}
 ComplexSection complex = ConfigurationManager.GetSection("complex") as ComplexSection;
 MessageBox.Show("Height" + complex.Height);
 MessageBox.Show("complex firstname+lastname:" + complex.Child.FirstName + complex.Child.LastName);

还有稍微再复杂一点的情况,我们可能要在配置中配置一组相同类型的节点,也就是一组节点的集合。如下面的配置:

<configuration>
  <configSections>
    <section name="morecomplex" type="Appconfig.MoreComplexSection,Appconfig"/>
  </configSections>
  <morecomplex height="190">
    <child firstName="James" lastName="Bond"/>
    <children>
      <add firstName="Zhao" lastName="yukai"/>
      <add firstName="Lee" lastName="yukai"/>
      <remove firstName="Zhao"/>
    </children>
  </morecomplex>
 <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
 </startup>
</configuration>

请看children节点,它就是一个集合类,在它里面定义了一组add元素(children不是直接'/'结尾,包含两个子元素,见形式),也可以有remove节点把已经添进去的配置去掉。要使用自定义节点集合需要从ConfigurationElementCollection类继承一个自定义类,然后要实现此类GetElementKey(ConfigurationElement element)和ConfigurationElement CreateNewElement()两个方法;为了方便的访问子节点可以在这个类里面定义只读的索引器,请看下面的实现:

namespace Appconfig
{
    class MoreComplexSection : ConfigurationSection 
    {
        [ConfigurationProperty("height", IsRequired = true)]
        public int Height
        {
            get
            {
                return (int)base["height"];
            }
            set
            {
                base["height"] = value;
            }
        }
        [ConfigurationProperty("child", IsDefaultCollection = false)]
        public ChildSection Child
        {
            get
            {
                return (ChildSection)base["child"];
            }
            set
            {
                base["child"] = value;
            }
        }
        [ConfigurationProperty("children", IsDefaultCollection = false)]
        //[ConfigurationCollection(typeof(ChildSection), CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap, RemoveItemName = "remove")]//可加可不加
        public Children Children
        {
            get
            {
                return (Children)base["children"];
            }
            set
            {
                base["children"] = value;
            }
        }
    }
    public class Children : ConfigurationElementCollection
    {
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ChildSection)element).FirstName;//新建节点定义一个Key值,Key同为firstname
        }

        protected override ConfigurationElement CreateNewElement()
        {
            return new ChildSection();
        }

        public ChildSection this[int i]
        {
            get
            {
                return (ChildSection)base.BaseGet(i);
            }
        }

        public ChildSection this[string key]
        {
            get
            {
                return (ChildSection)base.BaseGet(key);
            }
        }
        protected override string ElementName
        {
            get
            {
                return "children";
            }
        }
    }
    public class ChildSection : ConfigurationElement
    {
        [ConfigurationProperty("firstName", IsRequired = true, IsKey = true)]
        public string FirstName
        {
            get
            {
                return (string)base["firstName"];
            }
            set
            {
                base["firstName"] = value;
            }
        }

        [ConfigurationProperty("lastName", IsRequired = true)]
        public string LastName
        {
            get
            {
                return (string)base["lastName"];
            }
            set
            {
                base["lastName"] = value;
            }
        }
    }
}
MoreComplexSection morecomplex = ConfigurationManager.GetSection("morecomplex") as MoreComplexSection;
MessageBox.Show("Height" + morecomplex.Height);
MessageBox.Show("morecomplex child: firstname+lastname:" + morecomplex.Child.FirstName + morecomplex.Child.LastName);
//remove一个元素,所以只有1个
//组元素请参考 https://blog.csdn.net/sweety820/article/details/41695335
MessageBox.Show("morecomplex children: firstname+lastname:" + morecomplex.Children[0].FirstName + morecomplex.Children[0].LastName);

其实网上有很多微软提供的默认接口,如System.Configuration.DictionarySectionHandler、NameValueSectionHandler、SingleTagSectionHandler,去实现单或组元素获取,只需要写类去实现这些接口(Type用的是即是此类),很快就能获取到数据,可见https://blog.csdn.net/lulu_jiang/article/details/6688078https://www.cnblogs.com/caoyc/p/6002702.html两篇博客。当然现在我们用的是非继承接口自定义类,幸运的是,我们也实现了(单/组)元素的获取,前提代码量不小。如果使用系统标准的<add name=" ...", valeu="...">,就可以大大减少我们自定义的代码量,看下面程序:

<configuration>
  <configSections>
    <section name="easy" type="Appconfig.EasySection,Appconfig"/>
  </configSections>
  <easy height="190">
    <NVs>
      <add name="abc" value="123"/>
      <add name="abcd" value="12d3"/>
    </NVs>
  </esay>
 <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
 </startup>
</configuration>
namespace Appconfig
{
    class EasySection : ConfigurationSection 
    {
        [ConfigurationProperty("height", IsRequired = true)]
        public int Height
        {
            get
            {
                return (int)base["height"];
            }
            set
            {
                base["height"] = value;
            }
        }
        [ConfigurationProperty("NVs", IsDefaultCollection = false)]
        public System.Configuration.NameValueConfigurationCollection NVs
        {
            get
            {
                return (NameValueConfigurationCollection)base["NVs"];
            }
            set
            {
                base["NVs"] = value;
            }
        }
    }
}
EasySection Easy= ConfigurationManager.GetSection("easy") as EasySection;
MessageBox.Show("Height" + Easy.Height);
MessageBox.Show(Easy.NVs["abc"].Value);

是不是代码量少了许多?上述基本上可以满足所有的配置需求了。不过还有一点更大但是不复杂的概念,就是sectionGroup。我们可以自定义SectionGroup,然后在sectionGroup中配置多个section;分组对于大的应用程序是很有意义的。如下配置,配置了一个包含simple和一个morecomplex两个section的sectionGroup

<configuration>
  <configSections>
    <sectionGroup type="Appconfig.SampleSectionGroup,Appconfig" name="sampleGroup">
      <section type="Appconfig.SimpleSection,Appconfig" allowDefinition="Everywhere" name="simple" />
      <section type="Appconfig.MoreComplexSection,Appconfig" allowDefinition="Everywhere" name="morecomplex"/>
    </sectionGroup>
  </configSections>
  <sampleGroup>
    <simple maxValue="20" minValue="1">
    </simple>
    <morecomplex height="190">
      <child firstName="James" lastName="Bond"/>
      <children>
        <add firstName="Zhao" lastName="yukai"/>
        <add firstName="Lee" lastName="yukai"/>
        <remove firstName="Zhao"/>
      </children>   
    </morecomplex>
  </sampleGroup>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>
namespace Appconfig
{
    class SampleSectionGroup: System.Configuration.ConfigurationSectionGroup 
    {
        public MoreComplexSection Complex
        {
            get
            {
                return (MoreComplexSection)base.Sections["morecomplex"];
            }
        }

        public SimpleSection Simple
        {
            get
            {
                return (SimpleSection)base.Sections["simple"];
            }
        } 
    }
}

SimpleSection和MoreComplexSection不变,SectionGroup只是做个集合,需要注意的是SectionGroup不能使用如下方法获取ConfigurationManager.GetSection(string),要获得sectionGroup必须通过Configuration类的SectionGroups[string]索引器获得,如下示例代码:

SampleSectionGroup sample = (SampleSectionGroup)ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).SectionGroups["sampleGroup"];
MessageBox.Show(sample.Simple.MinValue.ToString());
MessageBox.Show(sample.Complex.Children[0].LastName);

(二)Web.config

Web.config跟App.config差不多,由于我不了解C#的Web运用,所以没法举例。日后如果学习了这部分做网页知识,肯定会来补上。但是,个人觉得做网页并非是微软系C#的强项,更不提主流开发语言了。

(三)Settings.settings

简而言之,Settings对应App.config中的这两个Section:"configuration-userSettings"与"configuration-applicationSettings",Settings提供了可视化修改App.config中这两个Section的途径,然而Settings与App.config却是独立存在于磁盘上的两个实体。一般情况下,Settings可视化修改,App.config自动修改;反之,App.config修改,会询问Settings可视化是否同步,可同步亦可不同步(没意义),Properties.Settings.Default取值源都是App.config。

Setting有4个属性Name(命名),Type(类型),Value(值)和Scope(作用域),前面三个不难理解,重点讲一下Scope属性。Scope属性有两个值Application,User。这两者区别,简单来说,用户作用域设置在运行时为读/写,并且可在代码中对其值进行更改和保存。应用程序作用域设置在运行时为只读。虽然可以读取,但是不能对其进行写入。具有应用程序作用域的设置只能在设计时或通过手动修设置文件进行更改。

//father的scope为用户,son为程序
//Properties.Settings.Default.daughter = 17;//应用程序作用域设置在运行时为只读,会报错
//Properties.Settings.Default.son = "xiaoliu";
MessageBox.Show(Properties.Settings.Default.father);//用户程序作用域可读
Properties.Settings.Default.father = "liu";//用户程序作用域可写
//Properties.Settings.Default.Save();
MessageBox.Show(Properties.Settings.Default.father);

上面程序是基本的读写操作,我们重点来说说程序运行到Properties.Settings.Default.Save()这个函数。应用程序作用域总是从App.config读取,在程序中也不能修改,所以改动只要改App.config或者Settings.settings可视化修改,但用户作用域就比较麻烦了。范围为"用户"的属性 Settings 在第一次运行时会读取App.config里的初始值(系统里找不到),放入内存(可修改),但是一旦调用Save方法后,Settings里"用户"范围的属性就会保存在系统里面,类似WebForm里的Cookies一样,从此以后,读取都会从系统里保存的值里读取,手工修改App.config里的"用户"范围的属性不会影响到这些属性,但是在调用Reset方法时会从新从App.config里获取"用户"范围的属性写入到系统中 ,也就是说,settings里"用户"范围的属性总共有3个地方存储。

1、App.config配置文件——程序第一次运行,之前程序未调用save(),在系统中找不到就会从这里获取;

2、操作系统——当Settings调用save()保存后;

3、内存——程序启动后,实例化settings对象,该实例在创建的时候从系统(如果系统中没有就如前面提到的从配置文件中获取)读取在内存中(过渡作用);

确实我们会发现,一旦保存之后,无论修改哪里,都会读取的是保存的那个值。不过,可以使用方法让App.config、系统、内存这3个位置之间值的关系互相影响。

new() 构造函数,"用户"属性先检查系统,如果没有值,再从配置文件中读取到内存。

Properties.Settings setting = new Properties.Settings();
MessageBox.Show(setting.father);

Save(),将Settings"用户”范围的属性写入到系统中, settings实例后,在程序中被赋予新值,如果想保存这些值以便在程序下一次运行时使用,就可以调用Save保存。无论是new()对象还是Properties.Settings.Default.xx获取,程序运行都是内存取值,内存首选系统,最后App.Config。

Reload(),从系统或App.config中读取已保存的值刷新当前内存里"用户"范围的属性。先检查系统,当系统中没有保存时再从App.config中获取。

private void button1_Click(object sender, EventArgs e)
{              
    Properties.Settings.Default.father = "yes";
    Properties.Settings.Default.Save();            
    Properties.Settings.Default.father = "st";
    //Properties.Settings.Default.Reload();//这句加和不加出现完全不同结果
    Properties.Settings setting = new Properties.Settings();
    MessageBox.Show(Properties.Settings.Default.father);
    MessageBox.Show(Properties.Settings.Default.father);
}

Reset(),清空系统中保存的Settings,并且把App.config中对应属性存入系统中。

//App.config
<userSettings>
    <settings.Properties.Settings>     
        <setting name="father" serializeAs="String">
            <value>sun</value>
        </setting>
    </settings.Properties.Settings>
</userSettings>
private void button1_Click(object sender, EventArgs e)
{              
     Properties.Settings.Default.father = "yes";
     Properties.Settings.Default.Save();           
     //Properties.Settings.Default.Reset();//这句加和不加完全出现不同结果      
     Properties.Settings setting = new Properties.Settings();
     MessageBox.Show(Properties.Settings.Default.father);
     MessageBox.Show(Properties.Settings.Default.father);
}

Upgrade(),当程序安装多个时,从最新的一个程序所保存在系统的值读取出来并刷新内存里"用户"范围的属性。

至于下面说的*.exe.config*.vshost.exe.config,Settings.settings完全不具备,既无法实现*.exe运行读取,也不会在调试期间*.vshost.exe.config发生变化。

总结

我们可以发现,在项目debug调试下,会在其目录生成app.config,*.exe.config 和 *.vshost.exe.config。它们三者的功能及存在意义都不同(Settings.settings除外)。

app.config 作为开发时配置参数的文件,可以在vs开发时右键属性设置是否复制到可执行程序同目录下(默认始终不复制)。在不复制的情况下,凡进行编译操作在生成可执行程序时会copy该文件为*.exe.config文件和*.vshost.exe.config文件。除非编写程序修改它,一旦生成可执行程序后,没有存在意义。

*.exe.config exe.config文件为程序实际运行时(直接运行.exe程序)直接操作的配置文件,它极大地提高程序的灵活性。例如当程序发布了,我们又想改配置字段。我们不需要去源码中改app.config文件,然后重新编译一遍,这样笨拙繁琐,完全抛去了*.exe.config设计初衷。我们直接修改它,就可以了,执行文件运行只跟它打交道,跟app.config及*.vshost.exe.config都没关系。

*.vshost.exe.config 从名字我们可以看出它跟宿主程序有关系,跟调试有关系,事实上确实如此。该文件主要是在vs调试运行时起作用。在调试运行程序伊始,*vshost.exe.config先copy *.exe.config内容,覆盖*.vshost.exe.config,然后运行程序时操作的配置文件为*.vshost.exe.config,代码对配置文件增删改,都会在这里变动。在调试程序结束时 *.vshost.exe.config再次copy *.exe.config覆盖*.vshost.exe.config,因为exe.config仅为执行文件运行操作,调试不会起任何变动,所以*.vshost.exe.config的内容又变回跟*.exe.config一样。

实际上发布release版本的exe程序时,*.vshost.exe.config 和app.config(如果copy了的话)可以不要的,但是*.exe.config文件必须有,它是要被执行文件读取的,后两者在发布后没有存在的意义。

  • 3
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WinForm中读取Oracle配置文件,可以按照以下步骤进行: 1. 首先,确保已经正确安装了Oracle客户端,并配置好了环境变量。这样才能在WinForm中正确访问Oracle数据库。 2. 在WinForm的项目中添加一个配置文件,可以命名为“App.config”,这个文件将用于存储Oracle数据库的连接信息。 3. 在配置文件中添加以下代码,用于配置Oracle数据库的连接信息: ```xml <configuration> <appSettings> <add key="OracleConnectionString" value="Data Source=数据库地址;User ID=用户名;Password=密码;"/> </appSettings> </configuration> ``` 其中,将“数据库地址”替换为实际的Oracle数据库地址、“用户名”替换为要连接的数据库的用户名、“密码”替换为对应的密码。 4. 在WinForm中的代码中,可以通过以下方法读取配置文件中的连接信息: ```csharp string connectionString = ConfigurationManager.AppSettings["OracleConnectionString"]; ``` 这样就可以获取到配置文件中存储的Oracle数据库的连接字符串。 5. 使用获取到的连接字符串进行数据库操作,例如连接数据库、执行SQL语句等。 需要注意的是,配置文件中的连接信息可以根据实际情况进行修改,以适应不同的Oracle数据库连接需求。另外,在代码中访问配置文件需要引用`System.Configuration`命名空间。 以上就是在WinForm中读取Oracle配置文件的基本步骤。通过配置文件,在不同环境下可以方便地修改连接信息,使得程序更加灵活和易于维护。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值