UI Automation test

Introduction

UI Automation是Microsoft .NET 3.0框架下提供的一种用于自动化测试的技术,是在MSAA基础上建立的,MSAA就是Microsoft Active Accessibility。UI Automation在某些方面超过了MSAA,UI自动化提供了Windows Vista中,微软Windows XP的全部功能,和Windows Server 2003。

在UI Automation中,所有的窗体、控件都表现为一个AutomationElement, AutomationElement 中包含此控件或窗体的属性,在实现自动化的过程中,我们通过其相关属性进行对控件自动化操作。对于UI用户界面来说,所有显示在桌面上的UI,其实际是一个UI Tree,根节点是desktop。我们可以使用UI Spy或者是SPY++来获得Window和Control的相关信息。在UI Automation里,根节点表示为AutomationElemnet.RootElement. 通过根节点,我们可以通过窗体或控件的Process Id、Process Name或者Window Name找到相应的子AutomationElement,例如Dialog、Button、 TextBox、Checkbox等标准控件,通过控件所对应的Pattern进行相关的操作。

UI Automation structure


如下图所示:



 附件: 您所在的用户组无法下载或查看附件



1. 在服务端由UIAutomationProvider.dll和UIAutomationTypes.dll提供。

2. 在客户端由UIAutomationClient.dll和UIAutomationTypes.dll提供。

3. UIAutomationCore.dll为UI自动化的核心部分,负责Server端和Client端的交互。

4. UIAUtomationClientSideProvides.dll为客户端程序提供自动化支持。


Summary

    本文主要简单介绍了UI Automation相关结构以及核心库

 

本文通过一个实例来介绍怎样使用UI Automation实现软件的自动化测试。

1. 首先建立一个待测试的winform程序,即UI Automation的服务端。



 附件: 您所在的用户组无法下载或查看附件


下面是button事件处理程序。

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3.     int i = int.Parse(textBox1.Text);
  4.     int j = int.Parse(textBox2.Text);
  5.     textBox3.Text = (i + j).ToString();
  6. }
复制代码

2. 建立一个测试程序,做UI Automaion的客户端。

添加引用:UIAutomationClient.dll 和 UIAutomationTypes.dll

  1.   1using System;
  2.   2using System.Diagnostics;
  3.   3using System.Threading;
  4.   4using System.Windows.Automation.Provider;
  5.   5using System.Windows.Automation.Text;
  6.   6using System.Windows.Automation;
  7.   7
  8.   8namespace UIAutomationTest
  9.   9{
  10. 10    class Program
  11. 11    {
  12. 12        static void Main(string[] args)
  13. 13        {
  14. 14            try
  15. 15            {
  16. 16                Console.WriteLine("/nBegin WinForm UIAutomation test run/n");
  17. 17                // launch Form1 application
  18. 18                // get refernce to main Form control
  19. 19                // get references to user controls
  20. 20                // manipulate application
  21. 21                // check resulting state and determine pass/fail
  22. 22
  23. 23                Console.WriteLine("/nBegin WinForm UIAutomation test run/n");
  24. 24                Console.WriteLine("Launching WinFormTest application");
  25. 25                //启动被测试的程序
  26. 26                Process p = Process.Start(@"E:/Project/WinFormTest/WinFormTest/bin/Debug/WinFormTest.exe");
  27. 27
  28. 28                //自动化根元素
  29. 29                AutomationElement aeDeskTop = AutomationElement.RootElement;
  30. 30
  31. 31                Thread.Sleep(2000);
  32. 32                AutomationElement aeForm = AutomationElement.FromHandle(p.MainWindowHandle);
  33. 33                //获得对主窗体对象的引用,该对象实际上就是 Form1 应用程序(方法一)
  34. 34                //if (null == aeForm)
  35. 35                //{
  36. 36                //    Console.WriteLine("Can not find the WinFormTest from.");
  37. 37                //}
  38. 38
  39. 39                //获得对主窗体对象的引用,该对象实际上就是 Form1 应用程序(方法二)
  40. 40                int numWaits = 0;
  41. 41                do
  42. 42                {
  43. 43                    Console.WriteLine("Looking for WinFormTest……");
  44. 44                    //查找第一个自动化元素
  45. 45                    aeForm = aeDeskTop.FindFirst(TreeScope.Children, new PropertyCondition(
  46. 46                        AutomationElement.NameProperty, "Form1"));
  47. 47                    ++numWaits;
  48. 48                    Thread.Sleep(100);
  49. 49                } while (null == aeForm && numWaits < 50);
  50. 50                if (null == aeForm)
  51. 51                    throw new NullReferenceException("Failed to find WinFormTest.");
  52. 52                else
  53. 53                    Console.WriteLine("Found it!");
  54. 54
  55. 55                Console.WriteLine("Finding all user controls");
  56. 56                //找到第一次出现的Button控件
  57. 57                AutomationElement aeButton = aeForm.FindFirst(TreeScope.Children,
  58. 58                  new PropertyCondition(AutomationElement.NameProperty, "button1"));
  59. 59
  60. 60                //找到所有的TextBox控件
  61. 61                AutomationElementCollection aeAllTextBoxes = aeForm.FindAll(TreeScope.Children,
  62. 62                    new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
  63. 63
  64. 64                // 控件初始化的顺序是先初始化后添加到控件
  65. 65                // this.Controls.Add(this.textBox3);                 
  66. 66                // this.Controls.Add(this.textBox2);
  67. 67                // this.Controls.Add(this.textBox1);
  68. 68
  69. 69                AutomationElement aeTextBox1 = aeAllTextBoxes[2];
  70. 70                AutomationElement aeTextBox2 = aeAllTextBoxes[1];
  71. 71                AutomationElement aeTextBox3 = aeAllTextBoxes[0];
  72. 72
  73. 73                Console.WriteLine("Settiing input to '30'");
  74. 74                //通过ValuePattern设置TextBox1的值
  75. 75                ValuePattern vpTextBox1 = (ValuePattern)aeTextBox1.GetCurrentPattern(ValuePattern.Pattern);
  76. 76                vpTextBox1.SetValue("30");
  77. 77                Console.WriteLine("Settiing input to '50'");
  78. 78                //通过ValuePattern设置TextBox2的值
  79. 79                ValuePattern vpTextBox2 = (ValuePattern)aeTextBox2.GetCurrentPattern(ValuePattern.Pattern);
  80. 80                vpTextBox2.SetValue("50");
  81. 81                Thread.Sleep(1500);
  82. 82                Console.WriteLine("Clickinig on button1 Button.");
  83. 83                //通过InvokePattern模拟点击按钮
  84. 84                InvokePattern ipClickButton1 = (InvokePattern)aeButton.GetCurrentPattern(InvokePattern.Pattern);
  85. 85                ipClickButton1.Invoke();
  86. 86                Thread.Sleep(1500);
  87. 87
  88. 88                //验证计算的结果与预期的结果是否相符合
  89. 89                Console.WriteLine("Checking textBox3 for '80'");
  90. 90                TextPattern tpTextBox3 = (TextPattern)aeTextBox3.GetCurrentPattern(TextPattern.Pattern);
  91. 91                string result = tpTextBox3.DocumentRange.GetText(-1);//获取textbox3中的值
  92. 92                //获取textbox3中的值
  93. 93                //string result = (string)aeTextBox2.GetCurrentPropertyValue(ValuePattern.ValueProperty);
  94. 94                if ("80" == result)
  95. 95                {
  96. 96                    Console.WriteLine("Found it.");
  97. 97                    Console.WriteLine("TTest scenario: *PASS*");
  98. 98                }
  99. 99                else
  100. 100                {
  101. 101                    Console.WriteLine("Did not find it.");
  102. 102                    Console.WriteLine("Test scenario: *FAIL*");
  103. 103                }
  104. 104
  105. 105                Console.WriteLine("Close application in 5 seconds.");
  106. 106                Thread.Sleep(5000);
  107. 107                //实现关闭被测试程序
  108. 108                WindowPattern wpCloseForm = (WindowPattern)aeForm.GetCurrentPattern(WindowPattern.Pattern);
  109. 109                wpCloseForm.Close();
  110. 110
  111. 111                Console.WriteLine("/nEnd test run/n");
  112. 112            }
  113. 113            catch (Exception ex)
  114. 114            {
  115. 115                Console.WriteLine("Fatal error: " + ex.Message);
  116. 116            }
  117. 117        }
  118. 118    }
  119. 119}
  120. 120
复制代码

文/开着拖拉机

MS提供的控件Pattern
 
DockPattern                                ExpandCollapsePattern

GridPattern                                  GridItemPattern

InvokePattern                              MultipleViewPattern

RangeValuePattern                      ScrollPattern

ScrollItemPattern                        SelectionPattern

SelectionItemPattern                  TablePattern

TableItemPattern                        TextPattern

TogglePattern                            TransformPattern

ValuePattern                              WindowPattern

 

Chapter 3  UI Automation中的几个重要属性

Control Tree of the AutomationElement


在UI Automation控件树中,根节点为Desktop window, 其他运行在用户桌面的窗体都作为Desktop window的子节点。

如下图所示:
           

 附件: 您所在的用户组无法下载或查看附件

Desktop window可通过AutomationElement.RootElement属性获取,子节点中的窗体或对话框可通过

AutomationElement.RootElement.FindAll(TreeScope.Descendants, condition)



AutomationElement.RootElement.FindFirt(TreeScope.Descendants, condition)来获取.


AutomationElement property

在UI Automation中有如下几个重要属性:

1.        AutomationIdProperty: 通过AutomationId来查找AutomationElement。

2.        NameProperty: 通过控件的Name属性来查找AutomationElement。

3.        ControlType: 通过控件的类型来查找AutomationElement

4.        AutomationId: 唯一地标识 自动化元素,将其与同级相区分。

5.        Name:  WPF 按钮的 Content 属性、Win32 按钮的 Caption 属性以及 HTML 图像的 ALT 属性都映射到 UI 自动化视图中的同一个属性 Name。

注:PropertyCondition类是用来对相关属性进行条件匹配,在控件树中查找控件时,可以通过最佳匹配来找到相应的控件。

如下代码列出了使用不同的属性来构建PropertyCondition,通过PropertyCondition来查找控件树中的控件.

  1. public class PropertyConditions
  2.     {
  3.         static PropertyCondition propertyCondition;
  4.         /// <summary>
  5.         /// Create PropertyCondition by AutomationId
  6.         /// </summary>
  7.         /// <param name="automationId">Control AutomationId</param>
  8.         /// <returns>Return PropertyCondition instance</returns>
  9.         public static PropertyCondition GetAutomationIdProperty(object automationId)
  10.         {
  11.             propertyCondition = new PropertyCondition(AutomationElement.AutomationIdProperty, automationId);
  12.             return propertyCondition;
  13.         }
  14.         /// <summary>
  15.         ///
  16.         /// </summary>
  17.         /// <param name="controlType"></param>
  18.         /// <returns></returns>
  19.         public static PropertyCondition GetControlTypeProperty(object controlType)
  20.         {
  21.             propertyCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, controlType);
  22.             return propertyCondition;
  23.         }
  24.         /// <summary>
  25.         ///
  26.         /// </summary>
  27.         /// <param name="controlName"></param>
  28.         /// <returns></returns>
  29.         public static PropertyCondition GetNameProperty(object controlName)
  30.         {
  31.             propertyCondition = new PropertyCondition(AutomationElement.NameProperty, controlName);
  32.             return propertyCondition;
  33.         }
  34.         /// <summary>
  35.         /// Find element by specific PropertyCondition
  36.         /// </summary>
  37.         /// <param name="condition">PropertyCondition instance</param>
  38.         /// <returns>Target automation element</returns>
  39.         public static AutomationElement FindElement(PropertyCondition condition)
  40.         {
  41.             return AutomationElement.RootElement.FindFirst(TreeScope.Descendants, condition);
  42.         }

DockPattern

DockPattern用于操作可停靠容器控件,我们最熟悉的VS2005/2008中的ToolBox,Solution Explorer都可以设置不同的DockPosition, 但是目前并不支持DockPattern,所以无法做为实例来讲。使用DockPattern的前提为控件支持DockPattern。 DockPattern中的DockPosition有六个枚举变量,即Bottom、Left、Right、Top、Fill和None。如果控件支持DockPattern, 则可以获取相对应的DockPosition以及设置控件的DockPosition。

如下代码是获取控件的DockPattern、获取控件当前的DockPosition以及设置控件的DockPosition。

  1. #region DockPattern helper
  2.         /// <summary>
  3.         /// Get DockPattern
  4.         /// </summary>
  5.         /// <param name="element">AutomationElement instance</param>
  6.         /// <returns>DockPattern instance</returns>
  7.         public static DockPattern GetDockPattern(AutomationElement element)
  8.         {
  9.             object currentPattern;
  10.             if (!element.TryGetCurrentPattern(DockPattern.Pattern, out currentPattern))
  11.             {
  12.                 throw new Exception(string.Format("Element with AutomationId '{0}' and Name '{1}' does not support the DockPattern.",
  13.                     element.Current.AutomationId, element.Current.Name));
  14.             }
  15.             return currentPattern as DockPattern;
  16.         }
  17.         /// <summary>
  18.         /// Get DockPosition
  19.         /// </summary>
  20.         /// <param name="element">AutomationElement instance</param>
  21.         /// <returns>DockPosition instance</returns>
  22.         public static DockPosition GetDockPosition(AutomationElement element)
  23.         {
  24.             return GetDockPattern(element).Current.DockPosition;
  25.         }
  26.         /// <summary>
  27.         /// Set DockPosition
  28.         /// </summary>
  29.         /// <param name="element">AutomationElement instance</param>
  30.         public static void SetDockPattern(AutomationElement element, DockPosition dockPosition)
  31.         {
  32.             GetDockPattern(element).SetDockPosition(dockPosition);
  33.         }
  34.         #endregion

表示以可视方式进行展开(以显示内容)和折叠(以隐藏内容)的控件。例如ComboBox控件支持ExpandCollapsePattern。

ExpandCollapsePattern有两个主要方法:

Expand()方法:隐藏 AutomationElement 的全部子代节点、控件或内容。

Collapse()方法:显示 AutomationElement 的全部子节点、控件或内容。

      以下代码是用ExpandCollapsePattern来测试ComboBox控件的Expand和Collapse。

  1. using System;
  2. using System.Text;
  3. using System.Diagnostics;
  4. using System.Threading;
  5. using System.Windows.Automation;
  6. namespace UIATest
  7. {
  8.     class Program
  9.     {
  10.         static void Main(string[] args)
  11.         {
  12.             Process process = Process.Start(@"F:/CSharpDotNet/AutomationTest/ATP/WpfApp/bin/Debug/WpfApp.exe");
  13.             int processId = process.Id;
  14.             AutomationElement element = FindElementById(processId, "comboBox1");
  15.             ExpandCollapsePattern currentPattern = GetExpandCollapsePattern(element);
  16.             currentPattern.Expand();
  17.             Thread.Sleep(1000);
  18.             currentPattern.Collapse();
  19.         }
  20.         /** <summary>
  21.         /// Get the automation elemention of current form.
  22.         /// </summary>
  23.         /// <param name="processId">Process Id</param>
  24.         /// <returns>Target element</returns>
  25.         public static AutomationElement FindWindowByProcessId(int processId)
  26.         {
  27.             AutomationElement targetWindow = null;
  28.             int count = 0;
  29.             try
  30.             {
  31.                 Process p = Process.GetProcessById(processId);
  32.                 targetWindow = AutomationElement.FromHandle(p.MainWindowHandle);
  33.                 return targetWindow;
  34.             }
  35.             catch (Exception ex)
  36.             {
  37.                 count++;
  38.                 StringBuilder sb = new StringBuilder();
  39.                 string message = sb.AppendLine(string.Format("Target window is not existing.try #{0}", count)).ToString();
  40.                 if (count > 5)
  41.                 {
  42.                     throw new InvalidProgramException(message, ex);
  43.                 }
  44.                 else
  45.                 {
  46.                     return FindWindowByProcessId(processId);
  47.                 }
  48.             }
  49.         }
  50.         /** <summary>
  51.         /// Get the automation element by automation Id.
  52.         /// </summary>
  53.         /// <param name="windowName">Window name</param>
  54.         /// <param name="automationId">Control automation Id</param>
  55.         /// <returns>Automatin element searched by automation Id</returns>
  56.         public static AutomationElement FindElementById(int processId, string automationId)
  57.         {
  58.             AutomationElement aeForm = FindWindowByProcessId(processId);
  59.             AutomationElement tarFindElement = aeForm.FindFirst(TreeScope.Descendants,
  60.             new PropertyCondition(AutomationElement.AutomationIdProperty, automationId));
  61.             return tarFindElement;
  62.         }
  63.         ExpandCollapsePattern helper#region ExpandCollapsePattern helper
  64.         /** <summary>
  65.         /// Get ExpandCollapsePattern
  66.         /// </summary>
  67.         /// <param name="element">AutomationElement instance</param>
  68.         /// <returns>ExpandCollapsePattern instance</returns>
  69.         public static ExpandCollapsePattern GetExpandCollapsePattern(AutomationElement element)
  70.         {
  71.             object currentPattern;
  72.             if (!element.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out currentPattern))
  73.             {
  74.                 throw new Exception(string.Format("Element with AutomationId '{0}' and Name '{1}' does not support the ExpandCollapsePattern.",
  75.                     element.Current.AutomationId, element.Current.Name));
  76.             }
  77.             return currentPattern as ExpandCollapsePattern;
  78.         }
  79.         #endregion
  80.     }
  81. }
复制代码

以下代码为被测程序的xaml文件:

  1. 1<Window x:Class="WpfApp.Window1"
  2. 2    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. 3    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. 4    Title="Window1" Height="219" Width="353">
  5. 5    <Grid>
  6. 6        <ComboBox Name="comboBox1" Height="23" VerticalAlignment="Top" Margin="94,58,0,0" HorizontalAlignment="Left" Width="119">
  7. 7            <ComboBoxItem>kaden</ComboBoxItem>
  8. 8            <ComboBoxItem>sam</ComboBoxItem>
  9. 9        </ComboBox>
  10. 10    </Grid>
  11. 11</Window>
  12. 12
复制代码

Summary

本文主要是对ExpandCollapsePattern 做简单的介绍,并使用ExpandCollapsePattern来操作ComboBox控件,对ComboBox进行Expand和Collapse操作。

 

 

 

InvokePattern

InvokePattern是UIA中最常用的Pattern之一,WPF和Winform中的button控件都支持InvokePattern。

对InvokePattern的Invoke()方法的调用应立即返回,没有出现阻止情况。但是,此行为完全依赖于 Microsoft UI 自动化提供程序实现。在调用 Invoke() 会引起阻止问题(如Winform中的模式对话框,但是WPF中的对话框的处理方式和winform不同,所以可以使用Invoke()方法来操作WPF中的模式对话框,因为WPF中的模式对话框不会出现阻止的问题)的情况下,要调用此方法,则需要另起线程来操作。

  1. using System;
  2. using System.Text;
  3. using System.Diagnostics;
  4. using System.Threading;
  5. using System.Windows.Automation;
  6. namespace UIATest
  7. {
  8.     class Program
  9.     {
  10.         static void Main(string[] args)
  11.         {
  12.             Process process = Process.Start(@"F:/CSharpDotNet/AutomationTest/ATP/WpfApp/bin/Debug/WpfApp.exe");
  13.             int processId = process.Id;
  14.             AutomationElement element = FindElementById(processId, "button1");
  15.             InvokePattern currentPattern = GetInvokePattern(element);
  16.             currentPattern.Invoke();
  17.         }
  18.         /// <summary>
  19.         /// Get the automation elemention of current form.
  20.         /// </summary>
  21.         /// <param name="processId">Process Id</param>
  22.         /// <returns>Target element</returns>
  23.         public static AutomationElement FindWindowByProcessId(int processId)
  24.         {
  25.             AutomationElement targetWindow = null;
  26.             int count = 0;
  27.             try
  28.             {
  29.                 Process p = Process.GetProcessById(processId);
  30.                 targetWindow = AutomationElement.FromHandle(p.MainWindowHandle);
  31.                 return targetWindow;
  32.             }
  33.             catch (Exception ex)
  34.             {
  35.                 count++;
  36.                 StringBuilder sb = new StringBuilder();
  37.                 string message = sb.AppendLine(string.Format("Target window is not existing.try #{0}", count)).ToString();
  38.                 if (count > 5)
  39.                 {
  40.                     throw new InvalidProgramException(message, ex);
  41.                 }
  42.                 else
  43.                 {
  44.                     return FindWindowByProcessId(processId);
  45.                 }
  46.             }
  47.         }
  48.         /// <summary>
  49.         /// Get the automation element by automation Id.
  50.         /// </summary>
  51.         /// <param name="windowName">Window name</param>
  52.         /// <param name="automationId">Control automation Id</param>
  53.         /// <returns>Automatin element searched by automation Id</returns>
  54.         public static AutomationElement FindElementById(int processId, string automationId)
  55.         {
  56.             AutomationElement aeForm = FindWindowByProcessId(processId);
  57.             AutomationElement tarFindElement = aeForm.FindFirst(TreeScope.Descendants,
  58.             new PropertyCondition(AutomationElement.AutomationIdProperty, automationId));
  59.             return tarFindElement;
  60.         }
  61.         #region InvokePattern helper
  62.         /// <summary>
  63.         /// Get InvokePattern
  64.         /// </summary>
  65.         /// <param name="element">AutomationElement instance</param>
  66.         /// <returns>InvokePattern instance</returns>
  67.         public static InvokePattern GetInvokePattern(AutomationElement element)
  68.         {
  69.             object currentPattern;
  70.             if (!element.TryGetCurrentPattern(InvokePattern.Pattern, out currentPattern))
  71.             {
  72.                 throw new Exception(string.Format("Element with AutomationId '{0}' and Name '{1}' does not support the InvokePattern.",
  73.                     element.Current.AutomationId, element.Current.Name));
  74.             }
  75.             return currentPattern as InvokePattern;
  76.         }
  77.         #endregion
  78.     }
  79. }

 

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
UiAutomation 集成到 Android 应用中,需要在应用中使用 Android Instrumentation 框架来创建一个 Instrumentation 实例,并使用这个实例来获取 UiAutomation 对象。下面是基本的步骤: 1. 在应用的 build.gradle 文件中添加以下依赖: ``` androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3' ``` 2. 在应用中创建一个 Instrumentation 类,在类中创建 Instrumentation 实例并获取 UiAutomation 对象: ``` import android.app.Instrumentation; import android.os.Build; import android.support.test.InstrumentationRegistry; import android.support.test.uiautomator.UiAutomation; public class MyInstrumentation extends Instrumentation { public UiAutomation uiAutomation; @Override public void onCreate(Bundle arguments) { super.onCreate(arguments); uiAutomation = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 ? getUiAutomation(FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES) : null; } @Override public void onDestroy() { super.onDestroy(); if (uiAutomation != null) { uiAutomation.disconnect(); } } } ``` 3. 在应用中使用 InstrumentationRegistry 获取 Instrumentation 实例并获取 UiAutomation 对象: ``` import android.support.test.InstrumentationRegistry; public class MyActivity extends Activity { private UiAutomation uiAutomation; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MyInstrumentation instrumentation = (MyInstrumentation) InstrumentationRegistry.getInstrumentation(); uiAutomation = instrumentation.uiAutomation; } @Override protected void onDestroy() { super.onDestroy(); if (uiAutomation != null) { uiAutomation.disconnect(); } } } ``` 4. 最后,在应用中使用 UiAutomation 对象来执行 UI 自动化测试。 需要注意的是,使用 UiAutomation 可能会影响应用的性能和稳定性,因此需要谨慎使用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值