Unity Editor 基础篇(三):自定义窗口 EditorWindow

本文参自:http://mp.weixin.qq.com/s/tMSAIND4Pq0farn2jY8rwg

本文为本人学习上连接的笔记有改动,请点击以上链接查看原文,尊重楼主知识产权。

###Unity Editor自定义窗口

目标:
1.了解一些属性的使用
2.创建一个自定义窗口
最终目标:
利用学到的东西制作自己的工具(自定义的窗口、Inspector、菜单、插件等等)。


最终效果:

这里写图片描述

准备工作:
在之前的项目中,找到 Editor 文件夹,然后创建一个新的 C# 脚本,命名为“MyFirstWindow”,然后双击打开脚本,添加如下代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using UnityEditor.SceneManagement;
using System.IO;

//继承自EditorWindow类
public class MyFirstWindow : EditorWindow 
{
    string bugReporterName = "";
    string description = "";
    GameObject buggyGameObject;

    //利用构造函数来设置窗口名称
    MyFirstWindow()
    {
        this.titleContent = new GUIContent("Bug Reporter");
    }
	
    //添加菜单栏用于打开窗口
    [MenuItem("Tool/Bug Reporter")]
    static void showWindow()
    {
        EditorWindow.GetWindow(typeof(MyFirstWindow));
    }
    void OnGUI()
    {
        GUILayout.BeginVertical();

        //绘制标题
        GUILayout.Space(10);
        GUI.skin.label.fontSize = 24;
        GUI.skin.label.alignment = TextAnchor.MiddleCenter;
        GUILayout.Label("Bug Reporter");

        //绘制文本
        GUILayout.Space(10);
        bugReporterName = EditorGUILayout.TextField("Bug Name",bugReporterName);

        //绘制当前正在编辑的场景
        GUILayout.Space(10);
        GUI.skin.label.fontSize = 12;
        GUI.skin.label.alignment = TextAnchor.UpperLeft;
        GUILayout.Label("Currently Scene:"+EditorSceneManager.GetActiveScene().name);

        //绘制当前时间
        GUILayout.Space(10);
        GUILayout.Label("Time:"+System.DateTime.Now);

        //绘制对象
        GUILayout.Space(10);
        buggyGameObject = (GameObject)EditorGUILayout.ObjectField("Buggy Game Object",buggyGameObject,typeof(GameObject),true);

        //绘制描述文本区域
        GUILayout.Space(10);
        GUILayout.BeginHorizontal();
        GUILayout.Label("Description",GUILayout.MaxWidth(80));
        description = EditorGUILayout.TextArea(description,GUILayout.MaxHeight(75));
        GUILayout.EndHorizontal();

        EditorGUILayout.Space();

        //添加名为"Save Bug"按钮,用于调用SaveBug()函数
        if(GUILayout.Button("Save Bug")){
            SaveBug();
        }

        //添加名为"Save Bug with Screenshot"按钮,用于调用SaveBugWithScreenshot() 函数
        if(GUILayout.Button("Save Bug With Screenshot")){
            SaveBugWithScreenshot();
        }

        GUILayout.EndVertical();
    }

    //用于保存当前信息
    void SaveBug()
    {
        Directory.CreateDirectory("Assets/BugReports/" + bugReporterName);
        StreamWriter sw = new StreamWriter("Assets/BugReports/" + bugReporterName + "/" + bugReporterName + ".txt");
        sw.WriteLine(bugReporterName);
        sw.WriteLine(System.DateTime.Now.ToString());
        sw.WriteLine(EditorSceneManager.GetActiveScene().name);
        sw.WriteLine(description);
        sw.Close();
    }

    void SaveBugWithScreenshot()
    {
        Directory.CreateDirectory("Assets/BugReports/" + bugReporterName);
        StreamWriter sw = new StreamWriter("Assets/BugReports/" + bugReporterName + "/" + bugReporterName + ".txt");
        sw.WriteLine(bugReporterName);
        sw.WriteLine(System.DateTime.Now.ToString());
        sw.WriteLine(EditorSceneManager.GetActiveScene().name);
        sw.WriteLine(description);
        sw.Close();
        Application.CaptureScreenshot("Assets/BugReports/"+bugReporterName+"/"+bugReporterName+"Screenshot.png");
    }
}

常用自定义窗口属性:

EditorWindow编辑器窗口

从这个类来创建编辑器窗口。
注意:这是一个编辑器类,如果想使用它你需要把它放到工程目录下的Assets/Editor文件夹下。编辑器类在UnityEditor命名空间下。所以当使用C#脚本时,你需要在脚本前面加上 "using UnityEditor"引用。
传送门:http://www.ceeger.com/Script/EditorWindow/EditorWindow.html

这里写图片描述

这里写图片描述

以上为这个案例中主要用到的几个类。


代码分析:

1、属性:

string bugReporterName=""; //储存记录bug人的名字
string description=""; //用于描述Bug信息
GameObject buggyGameObject; //用于存储Bug对象

首先声明了三个变量。

2.设置窗口的名字:

   //利用构造函数来设置窗口名称
    MyFirstWindow()
    {
        this.titleContent = new GUIContent("Bug Reporter");
    }

如代码注释所示,利用构造函数来设置窗口的名字。比较陌生的是 titleContent属性 和 GUIContent 类,简单了解如下图所示:

GUIContent 界面内容类

这里写图片描述

这个构造函数所产生的作用如下图所示:

这里写图片描述

3.添加菜单栏选项 - 打开窗口:

 //添加菜单栏用于打开窗口
    [MenuItem("Tool/Bug Reporter")]
    static void showWindow()
    {
        EditorWindow.GetWindow(typeof(MyFirstWindow));//可变大小窗口
        //Rect re=new Rect(0,0,500,500);
        //EditorWindow.GetWindowWithRect(typeof(MyFirstWindow),re);//规定大小窗口
    }

这个函数用于在菜单栏上添加一个打开该窗口的的菜单选项。比较陌生的是 [MenuItem()] 属性 和 GetWindow()函数,简单了解如下:

MenuItem菜单项:详解看这里

4.获取窗口

这里写图片描述

1/ 该函数就是用于返回一个窗口对象(就是打开一个窗口)。

2/ utility为false:(不写默认false,unity标准窗口)
这里写图片描述

utility为true:(浮动窗口,无法贴边unity)
这里写图片描述

3/ title不写则为构造函数里的样式,若写则优先使用。

绘制窗口
绘制窗口元素需要在 OnGUI() 函数里面设计,接下来我们一一分解。

5.标题label

GUILayout.Space(10);
GUI.skin.label.fontSize = 24;
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
GUILayout.Label("Bug Reporter");

步骤:
1.GUILayout.Space(10),这个有说过,让两个元素之间空十个像素之间的距离
2.GUI.skin.label.fontSize 、GUI.skin.label.alignment 用于设置标题的字体大小和对齐格式,具体从下图中了解:

这里写图片描述

对于 GUI.skin API 里面没有列出一些关于皮肤的属性,但是大伙们可以通过在Assets 菜单下右键 Create => GUI skin,如下图所示:

这里写图片描述

3.利用 GUILayout.Label() 来绘制标题
这里写图片描述

4.小标题:
EditorGUILayout.LabelField(“Bug:”,EditorStyles.boldLabel);//可选格式如粗体

6.绘制文本TextField

 GUILayout.Space(10);
bugReporterName = EditorGUILayout.TextField("Bug Name", bugReporterName);

效果如下:

这里写图片描述

7.显示当前正在编辑的场景

GUILayout.Space(10);
GUI.skin.label.fontSize = 12;
GUI.skin.label.alignment = TextAnchor.UpperLeft;
GUILayout.Label("Currently Scene:" + EditorSceneManager.GetActiveScene().name);

在这段代码中,比较陌生的也就是 EditorSceneManager.GetActiveScen().name,我们先看下图进行简单的了解:

这里写图片描述

其实就是返回当前编辑的场景信息(也就是返回 Scene 类型参数),然后利用 name 属性获取场景的名字,效果如下:
这里写图片描述

8.显示当前时间

GUILayout.Space(10);
GUILayout.Label("Time:" + System.DateTime.Now);

这段代码主要就是利用 System.DateTime.Now 获取当前时间,然后通过 GUILayout.Label() 把当前时间显示出来,对于 System.DateTime.Now 可参考我另一篇关于时间的博客。http://blog.csdn.net/qq_33337811/article/details/54669494

这里写图片描述

9.绘制对象槽

GUILayout.Space(10);
buggyGameObject = (GameObject)EditorGUILayout.ObjectField("Buggy Game Object", buggyGameObject, typeof(GameObject), true);

看一下API:
EditorGUILayout.ObjectField物理字段

描述:制作一个物体字段。可以拖拽或物体拾取器选择物体

static object ObjectField(Object obj,Type objType,bool allowSceneObjects,…)
static object ObjectField(string label,Object obj,Type objType,bool allowSceneObjects,…)
static object ObjectField(GUIContent label,Object obj,Type objType,bool allowSceneObjects,…)

参数:
label:字段前面可选标签
obj:字段显示的物体
objType:物体的类型
allowSceneObjects:是否允许指定场景中的物体
options:额外布局属性的可选列表

返回:用户设置的物体 Object类型

这里写图片描述

10.绘制描述文本区域

GUILayout.Space(10);
GUILayout.BeginHorizontal();
GUILayout.Label("Description", GUILayout.MaxWidth(80));
description = EditorGUILayout.TextArea(description, GUILayout.MaxHeight(75));
GUILayout.EndHorizontal();

直接上效果:

这里写图片描述

11.绘制按钮

 if (GUILayout.Button("Save Bug"))
        {
            SaveBug();
        }

看一下API:

这里写图片描述

其实很简单,不外乎就是添加一个按钮呗。在我们的代码中,用了一个 if 判断语句来判断,当我们点击该按钮时所触发的事件(该函数的返回值是一个 bool 类型,直接上效果图:

这里写图片描述

12.SaveBug() 函数

Directory.CreateDirectory("Assets/BugReports/" + bugReporterName);
StreamWriter sw = new StreamWriter("Assets/BugReports/" + bugReporterName + "/" + bugReporterName + ".txt");
sw.WriteLine(bugReporterName);
sw.WriteLine(System.DateTime.Now.ToString());
sw.WriteLine(EditorSceneManager.GetActiveScene().name);
sw.WriteLine(description);
sw.Close();

其实这个函数所做的事情也很简单,就是把我们设置好的一些参数保存到一个文本文件(.txt文件)上,仅此而已。

步骤如下:
1.第一行,利用 Directory 类创建一个目录
2.创建一个写入流类(StreamWriter)
3.然后把设置好的各个参数写入文件中

然后就完成了!

这里写图片描述


补充一些本案例里没有的点:

1.Toggle开关按钮、BeginToggleGroup开关区域

bool showBtn = true;
void OnGUI()
    {
        showBtn = EditorGUILayout.Toggle("Show Button",showBtn);
        if(showBtn){  //开关点开
            if(GUILayout.Button("Close")){ //绘制按钮
                this.Close(); //关闭面板
            }
        }
      }

效果:

这里写图片描述

开关组控制一个区域:

private bool groupEnabled; //区域开关
void OnGUI(){
groupEnabled = EditorGUILayout.BeginToggleGroup("Optional Settings", groupEnabled);
---
EditorGUILayout.EndToggleGroup();}

2.SelectableLabel 可选择标签(通常用于显示只读信息,可以被复制粘贴)

string text="hiahia";
    void OnGUI()
    {
        EditorGUILayout.SelectableLabel(text); //文本:可以选择然后复制粘贴
    }

效果:
这里写图片描述

3.PasswordField 密码字段

//创建密码字段并可视化在密码字段有什么键入。
	string text = "Some text here";
	bool showBtn = true;
	void OnGUI() {
		text = EditorGUILayout.PasswordField("Password:",text);
        showBtn = EditorGUILayout.Toggle("Show Button", showBtn);
        if (showBtn)
        {
            EditorGUILayout.LabelField("mima:", text);
        }
	}
}

效果:

这里写图片描述

4.整数字段 IntField :返回整数,由用户输入的值
浮点数字段 FloatField :返回小数,由用户输入的值

 int clones= 1;
	void OnGUI() {
		clones= EditorGUILayout.IntField("Number of clones:", clones);
}

这里写图片描述

5.Slider 滑动条
IntSlider 整数滑动条
MinMaxSlider 最小最大滑动条

Slider(float leftValue,float rightValue,GUILayoutOption[] options)
Slider(string label,float leftValue,float rightValue,GUILayoutOption[] options)
Slider(GUIContent label,float value,float leftValue,float rightValue,GUILayoutOption[] options)

//参数:label开关按钮前的可选标签
//leftValue滑动条最左边的值
//rightValue滑动条最右边的值 options。。。
//返回:float,由用户设置的值

    float scale = 0.0f; 
    void OnGUI()
    {
        scale = EditorGUILayout.Slider(scale,1,100);
    }
//随机放置选择的物体在最小最大滑动条之间
    float  minVal = -10.0f;
    float minLimit = -20.0f;
    float maxVal = 10.0f;
    float maxLimit = 20.0f;
    void OnGUI()
    {
        EditorGUILayout.LabelField("Min Val:", minVal.ToString());
        EditorGUILayout.LabelField("Max Val:", maxVal.ToString());
        EditorGUILayout.MinMaxSlider(ref minVal,ref  maxVal, minLimit, maxLimit);

    }

这里写图片描述

这里写图片描述

6.Popup弹出选择菜单

Popup(int selectedIndex,string[] displayOptions,GUILayoutOption[] paroptions) Popup(int selectedIndex,string[] displayOptions,GUIStyle style,GUILayoutOption[] paroptions)
Popup(string label,int selectedIndex,string[] displayOptions,GUILayoutOption[] paroptions) Popup(GUIContent label,int selectedIndex,string[] displayOptions,GUILayoutOption[] paroptions)。。。。
//参数:label字段前面可选标签
selectedIndex字段选项的索引
displayedOptions弹出菜单选项的数组 style可选样式 options。。
//返回:int,用户选择的选项索引

 string[] options = { "Cube","Sphere","Plane"};
    int index = 0;
    void OnGUI()
    {
        index = EditorGUILayout.Popup(index, options);
    }

这里写图片描述

7.EnumPopup 枚举弹出选择菜单(效果同上)

//返回System.Enum,用户选择的枚举选项。

enum OPTIONS
{
    CUBE = 0,
    SPHERE = 1,
    PLANE = 2
}
public class myEditor3 : EditorWindow {
    OPTIONS op=OPTIONS.CUBE;
     [MenuItem("cayman/tempShow")]
    static void newWelcome()
    {
        EditorWindow.GetWindow(typeof(myEditor3), true, "Eam");
    }
    void OnGUI()
    {
       op = (OPTIONS)EditorGUILayout.EnumPopup("Primitive to create:", op)  ;
    }
	}

8.Toolbar工具栏

int m_SelectedPage=0;
string[] m_ButtonStr=new string[4]{"Combine Animation","Check Part","Create RootMotion","CheckunUsedPrefab"};
void OnGUI(){
	m_SelectedPage=GUILayout.Toolbar(m_SelectedPage,m_ButtonStr,GUILayout.Height(25));
}

效果:
这里写图片描述

9.ColorField 颜色字段
ColorField (string label,Color value,…)
//参数:label字段前面的可选标签 value编辑的值
//返回:Color,由用户输入的值

  Color matColor = Color.white;
    void OnGUI()
    {
        matColor = EditorGUILayout.ColorField("New Color", matColor);

    }

这里写图片描述

10.Vector2Field 二维向量字段 Vector3Field 三维向量字段(略,同2维)

Vector2Field (string label,Vector2 value,GUILayoutOption[] options)
//参数:label字段前面的可选标签 value编辑的值 options…
//返回:Vector2,由用户输入的值

 float distance = 0;
    Vector2 p1, p2;
    void OnGUI()
    {
        p1 = EditorGUILayout.Vector2Field("Point 1:", p1);
        p2 = EditorGUILayout.Vector2Field("Point 2:", p2);
        EditorGUILayout.LabelField("Distance:", distance.ToString());
    }
    void OnInspectorUpdate() //面板刷新
    {
        distance = Vector2.Distance(p1, p2);
        this.Repaint();
    }

这里写图片描述

11.TagField 标签字段 LayerField层字段

// TagField(string label,string tag,GUIStyle style,GUILayoutOption[] paramsOptions)…
//参数:label字段前面的可选标签 tag显示字段的标签 。。
//返回:string,用户选择的标签

2/ LayerField(string label,int layer,GUIStyle style,GUILayoutOption[] paramsOptions)…
参数:label字段前面的可选标签 layer显示在该字段的层。。
//返回:int,用户选择的层

 string tagStr = "";
    int selectedLayer=0;
    void OnGUI()
    {  //为游戏物体设置
        tagStr = EditorGUILayout.TagField("Tag for Objects:", tagStr);
        if (GUILayout.Button("Set Tag!"))
            SetTags();
        if(GUILayout.Button("Set Layer!"))
            SetLayer();
    }
    void SetTags() {
		foreach(GameObject go in Selection.gameObjects)
			go.tag = tagStr;
	}
     void SetLayer() {
		foreach(GameObject go in Selection.gameObjects)
			go.laye = tagStr;
	}

这里写图片描述

12…IntPopup 整数弹出选择菜单

IntPopup(string label,int selectedValue,string[] displayOptions,int[] optionValues,GUIStyle style,GUILayoutOption[] paramsOptions)…
//参数:label字段前面的可选标签 selectedValue字段选项的索引 displayOptions弹出菜单項数组 optionValues每个选项带有值的数组。。
//返回:int,用户选择的选项的值

 int selectedSize = 1;
    string[] names = { "Normal","Double","Quadruple"};
    int[] sizes = { 1,2,4};
    void OnGUI()
    {
        selectedSize = EditorGUILayout.IntPopup("Resize Scale: ", selectedSize, names, sizes);
        if (GUILayout.Button("Scale"))
            ReScale();
    }
    void ReScale()
    {
        if (Selection.activeTransform)
            Selection.activeTransform.localScale =new Vector3(selectedSize, selectedSize, selectedSize);
        else Debug.LogError("No Object selected, please select an object to scale.");
    }

这里写图片描述

13.打开保存位置文件夹

GUILayout.Label ("Save Path", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextField(path,GUILayout.ExpandWidth(false));
if(GUILayout.Button("Browse",GUILayout.ExpandWidth(false)))
            path = EditorUtility.SaveFolderPanel("Path to Save Images",path,Application.dataPath);   //打开保存文件夹面板
EditorGUILayout.EndHorizontal();

这里写图片描述

这里写图片描述

14.bool Foldout(bool value, string label)折叠标签;
//制作一个左侧带有箭头的折叠标签

15.滑动区域 BeginScrollView
选择网格 SelectionGrid

BeginScrollView滑动区域开始
参数(vector2 位置,总是显示水平滑竿,总是显示垂直滑竿,格式…)
//中间的东西在滑动区域显示,可加{}或不加
GUILayout.EndScrollView(); 滑动结束

SelectionGrid(int 选择的索引,sting[] 显示文字数组,xCount,格式)

Vector2 v2 = new Vector2(0,0);
Int32 v = 0;
string[] str = { "Message1", "Message2", "Message3", "Message4" };

GUIStyle textStyle = new GUIStyle("textfield");
GUIStyle buttonStyle = new GUIStyle("button");
textStyle.active = buttonStyle.active;
textStyle.onNormal = buttonStyle.onNormal;

v2 = GUILayout.BeginScrollView(v2, true, true, GUILayout.Width(300), GUILayout.Height(100));
{
    v = GUILayout.SelectionGrid(v,str,1,textStyle);
}
GUILayout.EndScrollView();

效果:

这里写图片描述

滑动区域还可以:

Vector2 scrollPosition;
     using (var svs = new EditorGUILayout.ScrollViewScope(scrollPosition))
        {
            scrollPosition = svs.scrollPosition;

          //。。。

        }

16.控制区域GetControlRect

//通过拖拽获取文件路径
string path;
Rect rect;
 void OnGUI()
    {
        EditorGUILayout.LabelField("路径");
        //获得一个长300的框  
        rect = EditorGUILayout.GetControlRect(GUILayout.Width(300));
        //将上面的框作为文本输入框  
        path = EditorGUI.TextField(rect, path);

        //如果鼠标正在拖拽中或拖拽结束时,并且鼠标所在位置在文本输入框内  
        if ((Event.current.type == EventType.DragUpdated
          || Event.current.type == EventType.DragExited)
          && rect.Contains(Event.current.mousePosition))
        {
            //改变鼠标的外表  
            DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
            if (DragAndDrop.paths != null && DragAndDrop.paths.Length > 0)
            {
                path = DragAndDrop.paths[0];
            }
        }
    }

这里写图片描述

17.Box绘制

这里写图片描述

效果:
这里写图片描述

18.Tips:
1/ 打开一个通知栏
this.ShowNotification(new GUIContent(“This is a Notification”));

2/ 关闭通知栏
this.RemoveNotification();

3/

//更新
	void Update()
	{
 
	}
 
	void OnFocus()
	{
		Debug.Log("当窗口获得焦点时调用一次");
	}
 
	void OnLostFocus()
	{
		Debug.Log("当窗口丢失焦点时调用一次");
	}
 
	void OnHierarchyChange()
	{
		Debug.Log("当Hierarchy视图中的任何对象发生改变时调用一次");
	}
 
	void OnProjectChange()
	{
		Debug.Log("当Project视图中的资源发生改变时调用一次");
	}
 
	void OnInspectorUpdate()
	{
	   //Debug.Log("窗口面板的更新");
	   //这里开启窗口的重绘,不然窗口信息不会刷新
	   this.Repaint();
	}
 
	void OnSelectionChange()
	{
		//当窗口出去开启状态,并且在Hierarchy视图中选择某游戏对象时调用
		foreach(Transform t in Selection.transforms)
		{
			//有可能是多选,这里开启一个循环打印选中游戏对象的名称
			Debug.Log("OnSelectionChange" + t.name);
		}
	}
 
	void OnDestroy()
	{
		Debug.Log("当窗口关闭时调用");
	}

4/ 关闭面板
this.Close();

5/很多都和上篇文章一样的,如:
EditorGUILayout.HelpBox(“The default mode”,MessageType.None);//帮助信息

6/ 一些格式:
label可以用字体:EditorStyles.boldLabel
按钮什么的可以限制大小:
GUILayout.MaxWidth(160),
GUILayout.MinHeight(60),
GUILayout.ExpandWidth(false)

19.不可操作的 灰阶的区域

				EditorGUI.BeginDisabledGroup(bool变量);
                if (GUILayout.Button("Editor"))
                {
                    if (!AcquireSceneObjects())
                        EditorWindow.GetWindow<ArtistToolsWindow>().ShowNotification(new GUIContent("Failed to acquire scene objects, please confirm the root object."));
                }
                EditorGUI.EndDisabledGroup();

20.按钮用指定图

string icon = '\u2261'.ToString();
                if (GUILayout.Button(icon, EditorStyles.miniButtonMid, GUILayout.MaxWidth(20.0f)))
                {
                  
                }

21.颜色

		Color bak = GUI.color;
        GUI.color = Color.red;
        EditorGUILayout.LabelField("Step 1.");
        GUI.color = bak;

22.导出unitypackage包

string tempstr = EditorUtility.SaveFilePanel("Export to", "", "", "unitypackage");
        if (!string.IsNullOrEmpty(tempstr))
        {
            string[] paths = new string[results.Count];
            for(int i=0;i< results.Count;i++)
            {
                paths[i] = AssetDatabase.GetAssetPath(results[i]);
            }
            AssetDatabase.ExportPackage(paths, tempstr, ExportPackageOptions.Recurse);
        }

文末:再次声明请尊重楼主版权:
http://mp.weixin.qq.com/s/tMSAIND4Pq0farn2jY8rwg

  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jack Yan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值