wxPython + pyOpenGL,打造三维数据分析的利器

1. 前言

在三维显示领域,OpenGL 是神一样的存在,其地位就像编程语言里面的 C 一样。基于 OpenGL 衍生出来的分支、派系,林林总总,多如牛毛。Python 旗下,影响较大的三维库有 pyOpenGl / VTK / Mayavi / Vispy 等,它们各自拥有庞大的用户群体。VTK 在医学领域应用广泛,Vispy 在科研领域粉丝众多。VTK 和 Vispy 都是基于 OpenGL 的扩展,Mayavi 则是基于VTK 的,因此很多的医学影像应用都是采用 Python + VTK + ITK + Mayavi 的组合(ITK 是图像处理库,类似于 OpenCV 或 PIL)。

上述三维渲染库,包括 pyOpenGl,都有一个共同的特点,那就是只专注于三维功能的实现,而疏于对 UI 的支持。比 Vispy,虽然支持以 wx 或者 Qt 作为后端,但绑定后端以后,在窗口管理、交互操作等方面还是存在不少问题。pyOpenGl 做得更简单,提供一个 glut 库就算是对 UI 的支持了。

事实上,在复杂的三维展示系统中,UI 的重要性并不亚于 OpenGL。如果能为 OpenGL 找到一位 UI 搭档,必将提高程序的可靠性和可操作性,增强用户感受。wxPython 和 pyOpenGL 就是这样的一对黄金搭档。有诗赞曰:

面壁十年图破壁,宝剑霜刃未曾试。
秋风策马出京师,开启三维新天地。

2. 关于 wxPython

我一直认为,wxPython 是最适合 python 的GUI库,并为此专门写过一篇博文。详情见《wxPython:python首选的GUI库》。这里不再讨论如何使用 wxPython,只贴出几张开发项目的截图,展示一下 wxPython 的风格。

下图为 wxPython + pyOpenGL 开发的项目截图(隐去敏感信息):
在这里插入图片描述下图为界面细节展示(隐去敏感信息):
在这里插入图片描述在这里插入图片描述
下图为 wxPython 的传统风格:
在这里插入图片描述

3. 关于pyOpenGL

pyOpenGL 的入门教程有很多,我也有一篇博文滥竽充数,详见《写给 python 程序员的 OpenGL 教程》。特别提醒一下,这篇博文最后提到顶点缓冲区对象 VBO,并有演示代码。VBO 的概念很重要很重要很重要,只有学会使用 VBO,才能真正进入 OpenGL 的精彩世界。

早期的 OpenGL 使用立即渲染模式(Immediate mode,也就是固定渲染管线),概念清晰易于理解,绘制图形也很方便,但效率太低。从 OpenGL3.2 开始,规范文档开始废弃立即渲染模式,并鼓励开发者在 OpenGL 的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

VBO 是 OpenGL 核心模式的基础。VBO 将顶点数据集存储在 GPU 中,这意味着渲染 VBO 数据会很快。不过,数据从 RAM 传送到 GPU 是有代价的。VBO 虽然在 GPU 上,但并没有使用 GPU 的运算功能。在 VBO 之上,还有 VAO 的概念,即Vertex Array Object,顶点数组对象。这个概念很复杂,我们可以简单把 VAO 理解为 VBO 管理者。由于 VAO 依赖于显卡,通用性较差,我选择绕过它。

说实话,我对 OpenGL 的核心模式了解不多,对于着色器语言 GLSL 更是畏之如虎,对 VBO 的理解也不见得正确。虽然在模型拾取、体数据绘制、三维重建等方面,我的代码跑出来的效果还算差强人意,我仍然觉得我的方法与主流思路不同。很多时候,我喜欢说我的方法是“独辟蹊径”。

下面是我在工作中绘制的一些三维效果图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. 架起沟通 wxPython 和 pyOpenGL 的桥梁

wx.glcanvas.GLCanvas 是 wxPython 为显示 OpenGL 提供的类,顾名思义,我们可以将其理解为 OpenGL 的画板。有了这个画板,我们就可以使用 OpenGL 提供的各种工具在上面绘制各种三维模型了。

下面这段代码,从 wx.glcanvas.GLCanvas 派生了新类 WxGLScene,绑定了鼠标滚轮事件,并以立即渲染模式(Immediate mode)画了两个三角形。受限于篇幅,删去了鼠标拖拽操作,仅保留了滚轮缩放功能。

# -*- coding: utf-8 -*-

import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLU import *

class WxGLScene(glcanvas.GLCanvas):
    """GL场景类"""
    
    def __init__(self, parent, eye=[0,0,5], aim=[0,0,0], up=[0,1,0], view=[-1,1,-1,1,3.5,10]):
        """构造函数
        
        parent      - 父级窗口对象
        eye         - 观察者的位置(默认z轴的正方向)
        up          - 对观察者而言的上方(默认y轴的正方向)
        view        - 视景体
        """
        
        glcanvas.GLCanvas.__init__(self, parent, -1, style=glcanvas.WX_GL_RGBA|glcanvas.WX_GL_DOUBLEBUFFER|glcanvas.WX_GL_DEPTH_SIZE)
        
        self.parent = parent                                    # 父级窗口对象
        self.eye = eye                                          # 观察者的位置
        self.aim = aim                                          # 观察目标(默认在坐标原点)
        self.up = up                                            # 对观察者而言的上方
        self.view = view                                        # 视景体
        
        self.size = self.GetClientSize()                        # OpenGL窗口的大小
        self.context = glcanvas.GLContext(self)                 # OpenGL上下文
        self.zoom = 1.0                                         # 视口缩放因子
        self.mpos = None                                        # 鼠标位置
        self.initGL()                                           # 画布初始化
        
        self.Bind(wx.EVT_SIZE, self.onResize)                   # 绑定窗口尺寸改变事件
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.onErase)        # 绑定背景擦除事件
        self.Bind(wx.EVT_PAINT, self.onPaint)                   # 绑定重绘事件
        
        self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown)            # 绑定鼠标左键按下事件
        self.Bind(wx.EVT_LEFT_UP, self.onLeftUp)                # 绑定鼠标左键弹起事件
        self.Bind(wx.EVT_RIGHT_UP, self.onRightUp)              # 绑定鼠标右键弹起事件
        self.Bind(wx.EVT_MOTION, self.onMouseMotion)            # 绑定鼠标移动事件
        self.Bind(wx.EVT_MOUSEWHEEL, self.onMouseWheel)         # 绑定鼠标滚轮事件
    
    def onResize(self, evt):
        """响应窗口尺寸改变事件"""
        
        if self.context:
            self.SetCurrent(self.context)
            self.size = self.GetClientSize()
            self.Refresh(False)
            
        evt.Skip()
        
    def onErase(self, evt):
        """响应背景擦除事件"""
        
        pass
        
    def onPaint(self, evt):
        """响应重绘事件"""
        
        self.SetCurrent(self.context)
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)    # 清除屏幕及深度缓存
        self.drawGL()                                       # 绘图
        self.SwapBuffers()                                  # 切换缓冲区,以显示绘制内容
        evt.Skip()
        
    def onLeftDown(self, evt):
        """响应鼠标左键按下事件"""
        
        self.CaptureMouse()
        self.mpos = evt.GetPosition()
        
    def onLeftUp(self, evt):
        """响应鼠标左键弹起事件"""
        
        try:
            self.ReleaseMouse()
        except:
            pass

    def onRightUp(self, evt):
        """响应鼠标右键弹起事件"""
        
        pass
        
    def onMouseMotion(self, evt):
        """响应鼠标移动事件"""
        
        if evt.Dragging() and evt.LeftIsDown():
            pos = evt.GetPosition()
            try:
                dx, dy = pos - self.mpos
            except:
                return
            self.mpos = pos
            
            # 限于篇幅省略改变观察者位置的代码
            
            self.Refresh(False)
        
    def onMouseWheel(self, evt):
        """响应鼠标滚轮事件"""
        
        if evt.WheelRotation < 0:
            self.zoom *= 1.1
            if self.zoom > 100:
                self.zoom = 100
        elif evt.WheelRotation > 0:
            self.zoom *= 0.9
            if self.zoom < 0.01:
                self.zoom = 0.01
        
        self.Refresh(False)
        
    def initGL(self):
        """初始化GL"""
        
        self.SetCurrent(self.context)
        
        glClearColor(0,0,0,0)                                       # 设置画布背景色
        glEnable(GL_DEPTH_TEST)                                     # 开启深度测试,实现遮挡关系        
        glDepthFunc(GL_LEQUAL)                                      # 设置深度测试函数
        glShadeModel(GL_SMOOTH)                                     # GL_SMOOTH(光滑着色)/GL_FLAT(恒定着色)
        glEnable(GL_BLEND)                                          # 开启混合        
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)           # 设置混合函数
        glEnable(GL_ALPHA_TEST)                                     # 启用Alpha测试 
        glAlphaFunc(GL_GREATER, 0.05)                               # 设置Alpha测试条件为大于0.05则通过
        glFrontFace(GL_CW)                                          # 设置逆时针索引为正面(GL_CCW/GL_CW)
        glEnable(GL_LINE_SMOOTH)                                    # 开启线段反走样
        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
                
    def drawGL(self):
        """绘制"""
        
        # 清除屏幕及深度缓存
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        
        # 设置视口
        glViewport(0, 0, self.size[0], self.size[1])
        
        # 设置投影(透视投影)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        
        k = self.size[0]/self.size[1]
        if k > 1:
            glFrustum(
                self.zoom*self.view[0]*k, 
                self.zoom*self.view[1]*k, 
                self.zoom*self.view[2], 
                self.zoom*self.view[3], 
                self.view[4], self.view[5]
            )
        else:
            glFrustum(
                self.zoom*self.view[0], 
                self.zoom*self.view[1], 
                self.zoom*self.view[2]/k, 
                self.zoom*self.view[3]/k, 
                self.view[4], self.view[5]
            )
            
        # 设置视点
        gluLookAt(
            self.eye[0], self.eye[1], self.eye[2], 
            self.aim[0], self.aim[1], self.aim[2],
            self.up[0], self.up[1], self.up[2]
        )
            
        # 设置模型视图
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        
        # ---------------------------------------------------------------
        glBegin(GL_LINES)                    # 开始绘制线段(世界坐标系)
        
        # 以红色绘制x轴
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.8, 0.0, 0.0)           # 设置x轴顶点(x轴负方向)
        glVertex3f(0.8, 0.0, 0.0)            # 设置x轴顶点(x轴正方向)
        
        # 以绿色绘制y轴
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.0, -0.8, 0.0)           # 设置y轴顶点(y轴负方向)
        glVertex3f(0.0, 0.8, 0.0)            # 设置y轴顶点(y轴正方向)
        
        # 以蓝色绘制z轴
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, 0.0, -0.8)           # 设置z轴顶点(z轴负方向)
        glVertex3f(0.0, 0.0, 0.8)            # 设置z轴顶点(z轴正方向)
        
        glEnd()                              # 结束绘制线段
        
        # ---------------------------------------------------------------
        glBegin(GL_TRIANGLES)                # 开始绘制三角形(z轴负半区)
        
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.5, -0.366, -0.5)       # 设置三角形顶点
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.5, -0.366, -0.5)        # 设置三角形顶点
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, 0.5, -0.5)           # 设置三角形顶点
        
        glEnd()                              # 结束绘制三角形
        
        # ---------------------------------------------------------------
        glBegin(GL_TRIANGLES)                # 开始绘制三角形(z轴正半区)
        
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.5, 0.5, 0.5)           # 设置三角形顶点
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.5, 0.5, 0.5)            # 设置三角形顶点
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, -0.366, 0.5)         # 设置三角形顶点
        
        glEnd()                              # 结束绘制三角形

WxGLScene 类的使用示例:

#-*- coding: utf-8 -*-

import wx
from scene import *

APP_TITLE = u'架起沟通 wxPython 和 pyOpenGL 的桥梁'

class mainFrame(wx.Frame):
    """程序主窗口类,继承自wx.Frame"""
    
    def __init__(self):
        """构造函数"""
        
        wx.Frame.__init__(self, None, -1, APP_TITLE, style=wx.DEFAULT_FRAME_STYLE)
        
        self.SetBackgroundColour(wx.Colour(224, 224, 224))
        self.SetSize((800, 600))
        self.Center()
        
        self.scene = WxGLScene(self)
        
class mainApp(wx.App):
    def OnInit(self):
        self.SetAppName(APP_TITLE)
        self.Frame = mainFrame()
        self.Frame.Show()
        return True

if __name__ == "__main__":
    app = mainApp()
    app.MainLoop()

界面效果如下:
在这里插入图片描述

5. 场景、视区和模型

OpenGL 允许用户使用 glViewport() 命令设置多个视口,这意味着我们可以在显示屏幕上分割出多个显示区域,这些区域可以相互重叠,在逻辑上是完全独立的。我们可以将 WxGLScene 称作场景(scene),由 glViewport() 命令创建的视口称为视区(region),拥有相同名字的三维部件定义为模型(model)。一个场景可以添加多个视区,一个视区可以创建多个模型。

以曲面模型为例,函数原型如下:

def drawSurface(self, name, v, c=None, t=None, texture=None, method='Q', mode=None, display=True, pick=False):
    """绘制曲面
    
    name        - 模型名
    v           - 顶点坐标集,numpy.ndarray类型,shape=(cols,3)
    c           - 顶点的颜色集,numpy.ndarray类型,shape=(3|4,)|(cols,3|4)
    t           - 顶点的纹理坐标集,numpy.ndarray类型,shape=(cols,2)
    texture     - 2D纹理对象
    method      - 绘制方法
                    'Q'         - 四边形
                                    0--3 4--7
                                    |  | |  |
                                    1--2 5--6
                    'T'         - 三角形
                                    0--2 3--5
                                     \/   \/
                                      1    4
                    'Q+'        - 边靠边的连续四边形
                                   0--2--4
                                   |  |  |
                                   1--3--5
                    'T+'        - 边靠边的连续三角形
                                   0--2--4
                                    \/_\/_\
                                     1  3  5
                    'F'         - 扇形
                    'P'         - 多边形
    mode        - 显示模式
                    None        - 使用当前设置
                    'FCBC'      - 前后面填充颜色FCBC
                    'FLBL'      - 前后面显示线条FLBL
                    'FCBL'      - 前面填充颜色,后面显示线条FCBL
                    'FLBC'      - 前面显示线条,后面填充颜色FLBC
    display     - 是否显示
    pick        - 是否可以被拾取
    """

生成曲面模型顶点集、索引集的函数原型如下:

def _createSurface(self, v, c, t):
    """生成曲面的顶点集、索引集、顶点数组类型
    
    v           - 顶点坐标集,numpy.ndarray类型,shape=(clos,3)
    c           - 顶点的颜色集,None或numpy.ndarray类型,shape=(3|4,)|(cols,3|4)
    t           - 顶点的纹理坐标集,None或numpy.ndarray类型,shape=(cols,2)
    """

创建 VBO 和 EBO 的方法如下:

def _createVBO(self, vertices):
    """创建顶点缓冲区对象"""
    
    id = uuid.uuid1().hex
    buff = vbo.VBO(vertices)
    self.buffers.update({id: buff})
    
    return id
    
def _createEBO(self, indices):
    """创建索引缓冲区对象"""
    
    id = uuid.uuid1().hex
    buff = vbo.VBO(indices, target=GL_ELEMENT_ARRAY_BUFFER)
    self.buffers.update({id: buff})
    
    return id

6. 三维重建的实例

手头有 109 张头部 CT 的断层扫描图片,我打算用这些图片尝试头部的三维重建。基础工作之一,就是要把这些图片数据读出来,组织成一个三维的数据结构(实际上是四维的,因为每个像素有 RGBA 四个通道)。
在这里插入图片描述
这个数据结构,自然是 numpy 的 ndarray 对象,读取图像文件我习惯使用 PIL。因此,需要导入两个模块:

import numpy as np
from PIL import Image

接下来,我用一行代码就把 109 张图片读到了一个 109x256x256x4 的 numpy 数组中,耗时 172 毫秒:

data = np.stack([np.array(Image.open('head%d.png'%i)) for i in range(109)], axis=0)

三维重建代码如下:

# -*- coding: utf-8 -*-

import numpy as np
from PIL import Image
import wx
import win32api
import sys, os

from wxgl.scene import *
from wxgl.colormap import *

FONT_FILE = r"C:\Windows\Fonts\simfang.ttf"
APP_TITLE = u'CT断层扫描三维重建工具'
APP_ICON = 'res/head.ico'

class mainFrame(wx.Frame):
    '''程序主窗口类,继承自wx.Frame'''
    
    def __init__(self):
        '''构造函数'''
        
        wx.Frame.__init__(self, None, -1, APP_TITLE, style=wx.DEFAULT_FRAME_STYLE)
        
        self.SetBackgroundColour(wx.Colour(224, 224, 224))
        self.SetSize((800, 600))
        self.Center()
        
        # 以下代码处理图标
        if hasattr(sys, "frozen") and getattr(sys, "frozen") == "windows_exe":
            exeName = win32api.GetModuleFileName(win32api.GetModuleHandle(None))
            icon = wx.Icon(exeName, wx.BITMAP_TYPE_ICO)
        else :
            icon = wx.Icon(APP_ICON, wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)
        
        
        self.scene = WxGLScene(self, FONT_FILE, bg=[1,1,1,1])
        #self.scene.setView([-1,1,-1,1,2,500])
        #self.scene.setPosture(elevation=30, azimuth=-45, save=True)
        self.master = self.scene.addRegion((0,0,1,1))
        
        # 读取109张头部CT的断层扫描图片
        data = np.stack([np.array(Image.open('res/head%d.png'%i)) for i in range(109)], axis=0)
        
        # 三维重建(本质上是体数据绘制)
        self.master.drawVolume('volume', data/255.0, method='Q', smooth=False)
        self.master.update()
        
class mainApp(wx.App):
    def OnInit(self):
        self.SetAppName(APP_TITLE)
        self.Frame = mainFrame()
        self.Frame.Show()
        return True

if __name__ == "__main__":
    app = mainApp()
    app.MainLoop()

三维重建后的效果如下图:
在这里插入图片描述

7. 后记

原本打算好好写一写纹理、拾取、体数据绘制的,结果写完第4章的时候,就已经感觉写不动了。这个题目实在太大,不是一篇一两万字的博文就可以说清楚的。如果不是因为有朋友在我的博客上留言说,想了解三维重构,本文也许会止于第4章。第5章简单展示了我的创作思路,只是为了讲解第6章的三维重建——事实上,也没有说明白。

行文至此,深为没有掰扯清楚关键问题而满怀愧疚。如果对这个话题感兴趣,请直接联系我吧:xufive@sdysit.com

### 回答1: SoapUI是一款功能强大的工具,可以用来测试和调用Web服务接口。使用SoapUI调用Web服务接口需要先创建一个项目,然后添加一个接口,接着添加一个操作,最后配置请求和响应参数即可完成调用。在调用过程中,可以通过SoapUI提供的各种功能来进行测试和调试,例如断言、日志记录、性能测试等。总之,SoapUI是一款非常实用的工具,可以帮助开发人员快速、准确地测试和调用Web服务接口。 ### 回答2: SoapUI是一种用于测试Web服务的开源工具。它可以通过简单而强大的用户界面帮助开发人员和测试人员创建,维护和执行自动化API测试。SoapUI支持不同类型的Web服务标准,包括SOAP,REST和HTTP等。本文将重点介绍如何使用SoapUI调用Web服务。 首先,在SoapUI中创建新的项目: 1. 打开SoapUI并在左侧面板中选择“新建项目”。 2. 输入项目名称或任何项目相关的信息,例如项目描述和组织名称,并单击“确定”创建新项目。 3. 在创建新项目时,会自动创建一个新的测试套件,以便您可以添加测试用例和测试步骤。 4. 在测试套件上右键单击并选择“新建测试用例”。输入测试用例名称和任何相关信息,并单击“确定”。 5. 在测试用例上右键单击并选择“新建测试步骤”。在“测试步骤”下拉列表中选择“SOAP请求”(如果您要测试的是SOAP服务)并单击“确定”。 6. 在“SOAP请求”页面中,输入Web服务的地址和命名空间,并选择要调用的操作。您可以使用WSDL链接直接从Web浏览器中获取这些信息或手动输入它们来调用Web服务。 7. 在“请求窗口”中,在SOAP消息正文中定义请求内容。请注意,SOAP方法和输入参数将自动生成,并与您在步骤6中选择的操作相关联。 8. 单击“运行”以开始测试。 9. SoapUI将向Web服务发送请求并将响应显示在“响应窗口”中。在此处观察结果。 10. 您还可以在SoapUI中添加测试脚本和检查点,以确保API是否按预期运行。这可以通过Groovy脚本完成。 综上,SoapUI的使用非常简单,按照上述步骤操作,即可完成调用Web服务并对其进行测试。在测试过程中,您可以随时添加测试用例和测试步骤,并使用集成的测试报告查看测试结果。这使得SoapUI成为一种功能强大且易于使用的工具,可以帮助您快速检测和修复Web服务中的错误。 ### 回答3: SoapUI是一款非常流行的API测试工具,特别适用于测试Web服务或SOAP/REST API的测试。 首先,启动SoapUI并创建一个新项目。在项目中,添加一个新的测试套件,并添加一个新的测试用例。在测试用例中,我们添加一个步骤——"WebService请求"。 在"WebService请求"步骤中,我们需要设置请求URL和请求方法。在SOAP中,我们通常使用POST方法进行请求,因为SOAP消息通常以XML格式进行传输。因此,在我们的测试用例中,我们需要设置一个POST请求并提供请求URL。 在请求头部,我们需要指定该请求要使用的HTTP头部信息,如Content-Type、Accept等。在请求主体中,我们需要提供对应的SOAP操作信息。 在SoapUI中,我们可以提供一个WSDL(WSDL是Web Services Description Language的缩写,是用于描述Web服务的一种语言)文件,从而自动生成对应的SOAP消息体。我们可以通过在请求主体中单击右键并选择"Generate"来自动生成SOAP消息体。 在SOAP请求中,我们需要为每个操作提供一个操作名称、命名空间和请求格式。这使得SOAP消息可以与WSDL文档相匹配,并针对每个SOAP操作提供所需的输入和输出参数。 在构建SOAP请求时,我们还可以在请求主体中提供SOAP Header以及SOAP Envelope。将数据封装在SOAP Envelope中,以确保数据传输始终具有统一的格式和标准。 完成SOAP请求的设置后,我们可以执行该测试用例,并在测试面板中查看结果。此结果将显示请求的响应数据、状态代码等。 SoapUI还提供了其他有用的功能,如自动生成测试报告和执行自动化测试等。 总之,在SoapUI调用Web Service接口非常简单。我们只需定义请求URL、请求主体、操作名称等关键属性即可。这使得我们可以更快、更有效地测试我们的Web服务,并保证服务的质量达到预期。
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天元浪子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值