VSIX:C#项目 重命名所有标识符(Visual Studio扩展开发)

初级代码游戏的专栏介绍与文章目录-CSDN博客

我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。

这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。


        出于某种目的(合法的,真的合法的,合同上明确指出可以这样做),我准备了一个重命名所有标识符的VS扩展,用来把一个C#库改头换面,在简单的测试项目上工作很满意,所有标识符都被准确替换。我还尝试用在C++项目上,问题就比较多了,因为VS并不能准确识别代码,这说明,C#比C++好用太多了。

        当然,最终合同被放弃了,所以这个东西也没派上用场,纯粹成了我的个人练习(因为没有人指派我做这个程序)。

        本文涉及的代码支持2017、2019和2022,为了稳妥起见,项目本身用不同版本的VS创建,实际代码则放在一个共享文件中,只需要在生成的框架之中加入一句调用代码即可。

目录

一、创建项目框架

1.1 新建VS项目

1.2 添加命令

1.3 测试此框架代码

二、引入实际代码

2.1 添加项目文件

2.2 添加依赖项

三、修改代码

四、测试实际效果

4.1 创建测试项目

4.2 在VSIX项目打开测试项目

4.3 效果

五、代码


        

一、创建项目框架

1.1 新建VS项目

        以下均以VS2022社区版为例。

        项目类型过滤选择“扩展”,项目类型为“VSIX Project”。

        如果找不到这个,是因为安装的时候没有装,打开安装程序增加选项即可。

        在开始菜单里面找到“Visual Studio Installer”,修改安装选项:

1.2 添加命令

        创建以后在项目上右键,“添加”-“新建项”:

        “Command”就是一个菜单命令,会出现在VS的“工具”菜单下面。

        添加之后会看到增加了一个文件:Command1.cs,当然如果你改了命令名就是另外一个文件。

        文件不长,直接拉到最后,看最后一个方法的代码:

		/// <summary>
		/// This function is the callback used to execute the command when the menu item is clicked.
		/// See the constructor to see how the menu item is associated with this function using
		/// OleMenuCommandService service and MenuCommand class.
		/// </summary>
		/// <param name="sender">Event sender.</param>
		/// <param name="e">Event args.</param>
		private void Execute(object sender, EventArgs e)
		{
			ThreadHelper.ThrowIfNotOnUIThread();
			string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
			string title = "Command1";

			// Show a message box to prove we were here
			VsShellUtilities.ShowMessageBox(
				this.package,
				message,
				title,
				OLEMSGICON.OLEMSGICON_INFO,
				OLEMSGBUTTON.OLEMSGBUTTON_OK,
				OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
		}

        好简单的,猜也能猜到就是显示一个消息框。

1.3 测试此框架代码

        编译项目,应该没什么问题(全是开发工具生成的代码嘛)。

        调试或者直接运行不调试(“调试菜单”的“开始执行(不调试)”),会打开一个新的VS2022窗口,像普通VS一样,但是已经加载了扩展。

        选择项目,或者不选择项目直接进入。不选择项目直接进入(点击“继续但无需代码”):

        工具菜单下会出现“Invoke Command1”(图中还有另外一个相似菜单,是我的正式项目创建的),点击出现:

        这个框架就算完成了,剩下的就是修改命令代码。

二、引入实际代码

2.1 添加项目文件

        在项目上右键,“添加”-“现有项”,找到实际代码文件添加进来,当然你也可以直接放在项目里面,但是因为VS扩展项目是依赖VS版本的,最好把通用部分独立出来。

        这个位置是在项目之外的,可以由多个项目共享。

2.2 添加依赖项

        文件加进来之后不能编译:

        这是因为缺少依赖项,在“项目”-“引用”上右键,“添加引用”:

        在“程序集”-“扩展”里面找到“Microsoft.VisualStudio.VCCodeModel”,选中,确定。

        然后程序就可以编译了。这个依赖项其实只和C++项目功能有关,删除C++项目相关代码也可以不要这个依赖项。

        如果发生奇怪错误:

严重性	代码	说明	项目	文件	行	禁止显示状态
错误		CreatePkgDef : error : ArgumentException: No Visual Studio registration attribute found in this assembly.
The assembly should contain an instance of the attribute 'Microsoft.VisualStudio.Shell.RegistrationAttribute' defined in assembly 'Microsoft.VisualStudio.Shell.Framework' version '17.0.0.0' 
   在 Microsoft.VisualStudio.Tools.CreatePkgDef.ProcessAssembly(String fileName, Hive hive, PkgDefContext context, Boolean register, RegistrationMode mode) 位置 D:\a\_work\1\s\src\product\vssdk\tools\CreatePkgDef\CreatePkgDef.cs:行号 383
   在 Microsoft.VisualStudio.Tools.CreatePkgDef.DoCreatePkgDef(InputArguments inputArguments) 位置 D:\a\_work\1\s\src\product\vssdk\tools\CreatePkgDef\CreatePkgDef.cs:行号 202
   在 Microsoft.VisualStudio.Tools.CreatePkgDef.Main(String[] arguments) 位置 D:\a\_work\1\s\src\product\vssdk\tools\CreatePkgDef\CreatePkgDef.cs:行号 91	VSIXProject1			

        不要尝试任何解决方案,删除刚才添加的东西也没用,整个过程删掉重来。这可能是VS的BUG。

三、修改代码

        现在我们可以将代码引入,在Command1.cs里面添加如下内容:

//文件头添加对共享代码的引用
using VSIXProjectShare;
using Task = System.Threading.Tasks.Task;

//在类里面添加变量,就近放在构造函数前面好了
		private CommandShare commandshare;

//构造函数最后加上这一句
			commandshare = new CommandShare(this.package);


//Execute最后加一句
			commandshare.Execute();

        最终的Command1.cs是这样的(四处修改在里面已经注明):

using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.ComponentModel.Design;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using VSIXProjectShare;//第一处修改
using Task = System.Threading.Tasks.Task;

namespace VSIXProject1
{
	/// <summary>
	/// Command handler
	/// </summary>
	internal sealed class Command1
	{
		/// <summary>
		/// Command ID.
		/// </summary>
		public const int CommandId = 0x0100;

		/// <summary>
		/// Command menu group (command set GUID).
		/// </summary>
		public static readonly Guid CommandSet = new Guid("16dcb30d-2f74-4781-bde4-c21c60716ac8");

		/// <summary>
		/// VS Package that provides this command, not null.
		/// </summary>
		private readonly AsyncPackage package;

		private CommandShare commandshare;//第二处修改

		/// <summary>
		/// Initializes a new instance of the <see cref="Command1"/> class.
		/// Adds our command handlers for menu (commands must exist in the command table file)
		/// </summary>
		/// <param name="package">Owner package, not null.</param>
		/// <param name="commandService">Command service to add command to, not null.</param>
		private Command1(AsyncPackage package, OleMenuCommandService commandService)
		{
			this.package = package ?? throw new ArgumentNullException(nameof(package));
			commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

			var menuCommandID = new CommandID(CommandSet, CommandId);
			var menuItem = new MenuCommand(this.Execute, menuCommandID);
			commandService.AddCommand(menuItem);

			commandshare = new CommandShare(this.package);//第三处修改
		}

		/// <summary>
		/// Gets the instance of the command.
		/// </summary>
		public static Command1 Instance
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets the service provider from the owner package.
		/// </summary>
		private Microsoft.VisualStudio.Shell.IAsyncServiceProvider ServiceProvider
		{
			get
			{
				return this.package;
			}
		}

		/// <summary>
		/// Initializes the singleton instance of the command.
		/// </summary>
		/// <param name="package">Owner package, not null.</param>
		public static async Task InitializeAsync(AsyncPackage package)
		{
			// Switch to the main thread - the call to AddCommand in Command1's constructor requires
			// the UI thread.
			await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

			OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
			Instance = new Command1(package, commandService);
		}

		/// <summary>
		/// This function is the callback used to execute the command when the menu item is clicked.
		/// See the constructor to see how the menu item is associated with this function using
		/// OleMenuCommandService service and MenuCommand class.
		/// </summary>
		/// <param name="sender">Event sender.</param>
		/// <param name="e">Event args.</param>
		private void Execute(object sender, EventArgs e)
		{
			ThreadHelper.ThrowIfNotOnUIThread();
			string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
			string title = "Command1-d";//修改这里以确认版本

			// Show a message box to prove we were here
			VsShellUtilities.ShowMessageBox(
				this.package,
				message,
				title,
				OLEMSGICON.OLEMSGICON_INFO,
				OLEMSGBUTTON.OLEMSGBUTTON_OK,
				OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
			commandshare.Execute();//第四处修改

		}
	}
}

        然后编译执行(仍然用“继续但无需代码”):

        看,版本正确,这是一个小技巧,debug发生困惑的时候先确认版本,不要编译失败执行旧版本。

        点击【确定】后又弹出新的消息框:

        这就对了,因为没有打开项目,共享代码需要项目来操作。这说明共享代码也正确进去了。后面就可以测试实际效果,共享代码放在本文最后。

四、测试实际效果

4.1 创建测试项目

        创建一个C#项目,一个对话框好了:

        一个对话框,有个静态文本,窗口初始化设置了一下文本。

4.2 在VSIX项目打开测试项目

        先创建好这个项目,然后回到VSIX项目,调试或运行,在新打开的VS启动时选择新建的这个项目,打开后是和普通VS一样操作的,只不过多了扩展菜单项。

        现在从工具菜单执行我们的命令,运行时会在输出窗口输出内容,最后会得到一个消息框:

        这就表示正确完成,提示信息是共享代码最后的版本,就是日期和时间。

        输出窗口输出如下:

        注意:此时修改的文件还没保存,要点击“全部保存”来保存文件。

        然后我们看看效果如何,现重新编译程序确认测试项目是正常的。

4.3 效果

        看看代码变成了什么样:

        Form1.Designer.cs就不贴了。

        看看文件比较:

五、代码

        共享代码在此,文件名CommandShare.cs:

using System;
using System.ComponentModel.Design;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Task = System.Threading.Tasks.Task;

using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.VCCodeModel;
using System.IO;
using Microsoft.Internal.VisualStudio.PlatformUI;


namespace VSIXProjectShare
{
    public sealed class CommandShare
    {
        private readonly AsyncPackage package;

        enum ProjectType { VC,CSharp,OTHER};//项目类型
        ProjectType projectType;
        private Random r ;//随机数
        private string new_name_title;//新名称标题
        private long count = 0;//顺序编号

        public CommandShare(AsyncPackage _package)
        {
            package = _package;

            Log("初始化插件");

            r = new Random();
            new_name_title = "_ASDFGHJKL_" + r.Next().ToString() + "_";
        }

        //显示消息对话框
        private void ShowMessageBox(string title, string message)
        {
            VsShellUtilities.ShowMessageBox(
               package,
               message,
               title,
               OLEMSGICON.OLEMSGICON_INFO,
               OLEMSGBUTTON.OLEMSGBUTTON_OK,
               OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
        }
        //输出日志
        private void Log(string msg)
        {
            Log(0,msg);
        }
        private void Log(int level,string msg)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            IVsOutputWindowPane pane = (IVsOutputWindowPane)Package.GetGlobalService(typeof(SVsGeneralOutputWindowPane));
            int tmp = pane.Activate();
            if (VSConstants.S_OK != tmp)
            {
                ShowMessageBox("注意", "未能激活输出窗口 " + tmp.ToString());
            }
            for (int i = 0; i < level; ++i)
            {
                pane.OutputStringThreadSafe("    ");
            }
            pane.OutputStringThreadSafe(msg + "\r\n");
        }
        private void AddFunction_myToString(int level, VCCodeElements codeElements)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            IVsOutputWindowPane pane = (IVsOutputWindowPane)Package.GetGlobalService(typeof(SVsGeneralOutputWindowPane));

            string px = new string(' ', level * 4);
            string px2 = new string(' ', 4);
            px = level.ToString() + px;

            string fun_name = "myToString";
            string fun_type = "stringstream &";
            vsCMFunction fun_kind = vsCMFunction.vsCMFunctionFunction | vsCMFunction.vsCMFunctionConstant;

            foreach (VCCodeElement element in codeElements)
            {
                Log(px + "Kind " + element.Kind.ToString() + " Name " + element.Name);
                if (0 != element.Children.Count)
                {
                    AddFunction_myToString(level + 1, element.Children as VCCodeElements);
                }

                if (element.Kind == vsCMElement.vsCMElementClass || element.Kind == vsCMElement.vsCMElementStruct)
                {
                    VCCodeElement Found = null;
                    string bodytext = "";

                    //基类必须首先处理
                    foreach (VCCodeElement chileren in element.Children)
                    {
                        if (chileren.Kind == vsCMElement.vsCMElementVCBase)
                        {
                            Log(px + px2 + "基类 " + chileren.Name);
                            bodytext += "\t\t " + chileren.Name + "::myToString(ss) << \" \";\r\n";
                        }
                    }
                    foreach (VCCodeElement chileren in element.Children)
                    {
                        Log(px + px2 + chileren.Name + " Kind " + chileren.Kind);
                        if (chileren.Kind == vsCMElement.vsCMElementVariable)
                        {
                            VCCodeVariable variable = (VCCodeVariable)chileren;
                            Log(px + px2 + "变量 Name " + variable.Name + " TypeString " + variable.TypeString
                                + " StartPoint " + variable.StartPoint.Line + " " + variable.StartPoint.LineCharOffset
                                + " EndPoint " + variable.EndPoint.Line + " " + variable.EndPoint.LineCharOffset);
                            if (variable.TypeString.EndsWith(")"))
                            {
                                bodytext += "\t\t ss << \"函数指针 " + variable.Name + " \" << " + variable.Name + " << \" \";\r\n";
                            }
                            else if (variable.TypeString.EndsWith("]"))
                            {
                                bodytext += "\t\t ss << \"数组 " + variable.Name + " \" << " + variable.Name + " << \" \";\r\n";
                            }
                            else
                            {
                                bodytext += "\t\t Template_" + fun_name + "(" + variable.Name + ", ss) << \" \";\r\n";
                            }
                        }
                        else if (chileren.Kind == vsCMElement.vsCMElementFunction)
                        {
                            if (chileren.Name == fun_name)
                            {
                                Found = chileren;
                                Log(px + px2 + fun_name + " 已存在,重新创建");
                            }
                        }
                    }

                    VCCodeFunction codeFunction;
                    if (element.Kind == vsCMElement.vsCMElementClass)
                    {
                        VCCodeClass codeClass = (VCCodeClass)element;
                        codeClass.RemoveMember(Found);
                        codeFunction = (VCCodeFunction)codeClass.AddFunction(fun_name, fun_kind, fun_type, -1, vsCMAccess.vsCMAccessPublic);
                    }
                    else
                    {
                        VCCodeStruct codeClass = (VCCodeStruct)element;
                        codeClass.RemoveMember(Found);
                        codeFunction = (VCCodeFunction)codeClass.AddFunction(fun_name, fun_kind, fun_type, -1, vsCMAccess.vsCMAccessPublic);
                    }
                    codeFunction.AddParameter("ss", "stringstream &");
                    codeFunction.Comment = "自动生成的代码";

                    bodytext += "\t\t return ss;";
                    codeFunction.BodyText = bodytext;
                }
            }
        }
        private void CSharp_Rename(int level, CodeElements codeElements)
        {
            ThreadHelper.ThrowIfNotOnUIThread();

            foreach (CodeElement _element in codeElements)
            {
                Log(level, "Kind " + _element.Kind.ToString());

                string name = "未知";//Name属性不是每个都有
                if (_element.Kind == vsCMElement.vsCMElementImportStmt)
                {
                    name = "vsCMElementImportStmt";
                }
                else
                {
                    name = _element.Name;//这个竟然不是每个都支持
                }

                CodeElement2 element = (CodeElement2)_element;
                Log(level, "Kind " + element.Kind.ToString() + " Name " + name + " type " + element.GetType().ToString());

                //处理子项
                if (0 != element.Children.Count)
                {
                    CSharp_Rename(level + 1, element.Children);
                }

                bool skip = false;//是否需要跳过
                
                //检查是否已经处理过
                if (name.StartsWith(new_name_title))
                {
                    skip = true;
                }

                if (element.Kind == vsCMElement.vsCMElementVariable)
                {
                    CodeVariable variable = (CodeVariable)element;
                    Log(level + 1, "变量 Name " + variable.Name
                        + " StartPoint " + variable.StartPoint.Line + " " + variable.StartPoint.LineCharOffset
                        + " EndPoint " + variable.EndPoint.Line + " " + variable.EndPoint.LineCharOffset);
                }
                else if (element.Kind == vsCMElement.vsCMElementFunction)
                {
                    Log(level + 1, "函数 " + name);
                    if (name.Equals("Main"))
                    {
                        Log(level + 1, "Main函数(跳过) " + name);
                        skip = true;
                    }
                    if (name.Equals("Dispose"))
                    {
                        Log(level + 1, "Dispose函数(跳过) " + name);
                        skip = true;
                    }
                }
                else if (element.Kind == vsCMElement.vsCMElementNamespace)
                {
                    Log(level + 1, "命名空间 " + name);
                    //skip = true;
                }
                else if (element.Kind == vsCMElement.vsCMElementAttribute)
                {
                    Log(level + 1, "属性(跳过) " + name);
                    skip = true;
                }
                else if (element.Kind == vsCMElement.vsCMElementImportStmt)
                {
                    Log(level + 1, "导入语句(跳过) " + name);
                    skip = true;
                }
                else if (element.Kind == vsCMElement.vsCMElementOther)
                {
                    Log(level + 1, "vsCMElementOther(跳过) " + name);
                    skip = true;
                }
                

                if (!skip)
                {
                    Log(level, "重命名 " + name + "(" + element.Kind.ToString() + ") 为 " + new_name_title + count.ToString());
                    element.RenameSymbol(new_name_title + count.ToString());
                    count++;
                    Log(level, "重命名完成");
                }
            }
        }
        private void ProcessProjectItem(int level, ProjectItem projectItem)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            //项目下的筛选器
            Log(level, "===============目录:" + projectItem.Name + " 项目子项FileCount:" + projectItem.FileCount.ToString());
            for (short i = 0; i < projectItem.FileCount; i++)
            {
                Log(3, "文件名:" + projectItem.FileNames[i]);
            }
            if (projectType == ProjectType.CSharp && projectItem.Name == "Properties")
            {
                Log(3, "C#项目忽略属性目录");
                return;
            }
            if (null != projectItem.FileCodeModel)
            {
                String language = "未知语言";
                switch (projectItem.FileCodeModel.Language)
                {
                    case CodeModelLanguageConstants.vsCMLanguageVC:
                        language = "VC";
                        AddFunction_myToString(5, projectItem.FileCodeModel.CodeElements as VCCodeElements);
                        break;
                    case CodeModelLanguageConstants.vsCMLanguageIDL:
                        language = "IDL";
                        Log(3, "未支持的语言 " + language);
                        break;
                    case CodeModelLanguageConstants.vsCMLanguageVB:
                        language = "VB";
                        Log(3, "未支持的语言 " + language);
                        break;
                    case CodeModelLanguageConstants.vsCMLanguageMC:
                        language = "MC";
                        Log(3, "未支持的语言 " + language);
                        break;
                    case CodeModelLanguageConstants.vsCMLanguageCSharp:
                        language = "CSharp";
                        Log(3, "语言 " + language);
                        if (null == projectItem) Log(3, "语言1" + language);
                        if (null == projectItem.FileCodeModel) Log(3, "语言 2" + language);
                        if (null == projectItem.FileCodeModel.CodeElements) Log(3, "语言 3" + language);
                        Log(3, "语言 " + language);
                        CSharp_Rename(5, projectItem.FileCodeModel.CodeElements);
                        break;
                }

            }
            foreach (ProjectItem current_project_item_item in projectItem.ProjectItems)
            {
                ProcessProjectItem(level + 1, current_project_item_item);
            }
        }

        public void Execute()
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
            string title = "Command1 2023-04-20 1720";

            // Show a message box to prove we were here
            //VsShellUtilities.ShowMessageBox(
            //    this.package,
            //    message,
            //    title,
            //    OLEMSGICON.OLEMSGICON_INFO,
            //    OLEMSGBUTTON.OLEMSGBUTTON_OK,
            //    OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
            Log(title);

            try
            {
                DTE2 dte = (DTE2)Package.GetGlobalService(typeof(SDTE));
                Log("DTE:" + dte.Version);
                Log("DTE:" + dte.Name);
                Log("DTE:" + dte.Edition);
                Log("DTE:" + dte.Mode);

                var solution = dte.Solution;
                var SolutionName = Path.GetFileName(solution.FullName);     //解决方案名称
                var SolutionPath = Path.GetDirectoryName(solution.FullName);//解决方案路径
                Log("解决方案:" + solution.ToString());
                Log("解决方案FileName:" + solution.FileName);
                Log("解决方案FullName:" + solution.FullName);
                Log("解决方案GetFileName:" + SolutionName);
                Log("解决方案GetDirectoryName:" + SolutionPath);
                Log("解决方案Count:" + solution.Count);
                Log("解决方案Projects.Count:" + solution.Projects.Count);

                foreach (Project current_project in solution.Projects)
                {
                    //解决方案下的项目
                    Log(1, "--------------------------Language:" + current_project.CodeModel.Language);
                    if (current_project.CodeModel.Language == "{B5E9BD34-6D3E-4B5D-925E-8A43B79820B4}")
                    {
                        projectType = ProjectType.CSharp;
                    }
                    else if (current_project.CodeModel.Language == "{B5E9BD32-6D3E-4B5D-925E-8A43B79820B4}")
                    {
                        projectType = ProjectType.VC;
                    }
                    else
                    {
                        projectType = ProjectType.OTHER;
                    }
                    Log(1, "--------------------------项目:" + current_project.Name + " 类型 " + projectType + " 项目子项个数:" + current_project.ProjectItems.Count.ToString());

                    foreach (ProjectItem current_project_item in current_project.ProjectItems)
                    {
                        ProcessProjectItem(2, current_project_item);
                    }
                }

                ShowMessageBox(title, "操作完成");
            }
            catch (Exception ex)
            {
                ShowMessageBox("", ex.Message);
            }
        }
    }
}

        这个代码对C#项目执行CSharp_Rename,对C++项目则执行AddFunction_myToString,功能是给所有结构添加toString函数,尚不完善,所以无视即可(其实我最开始折腾VSIX就是为了写这个功能的,我挺希望别人能把这个功能做出来)。

        代码分析看这里:VSIX:C#项目 重命名所有标识符(Visual Studio扩展开发)代码详解-CSDN博客


(这里是结束)

  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值