SDL2基本使用

前言

  在这里记录SDL的环境基本搭建和使用,方便回忆。使用该图形库也是为了方便在没有单片机和显示模块的使用,也能对简单验证些关于图形构建或界面管理的猜想和测试,所以下述不会探讨过于深入的东西。当然,也可以通过SDL官网查看介绍。


下载

在官网看到,SDL2的最新稳定版本为2.30.11,点击跳转到其GitHub下,下载SDL2-devel-2.30.11-mingw.zip

在这里插入图片描述


CLion

这里我是在CLion创建Demo项目,并且将下载好的SDL2-devel-2.30.11-mingw.zip解压后放入,并且编写好CMakeLists.txt,对SDL2进行链接。具体CMake流程可以参考CMake

在这里插入图片描述


SDL2

通过下述的基本使用目录中的教程,以及基本框架让大家了解下SDL2

◇ 基本使用

  教程参考至SDL2官网维基中C++ Programming (thenumb.at),其目录 - SDL2章节下的8个基本教程,下述将基于C语言实现代码,且加上对应的注释。还有就是为了保证代码看起来清晰些,把创建是否成功的状态判断,都去掉了,默认成功创建窗口、表面、图形等对象。然后在没有看到SDL2的交互函数时,暂且先用while(1);,来保持绘制界面。除此之外,除了在第一个案例末尾演示内存释放流程外其它案例,也不在演示释放函数。

如果觉得下述讲解的太过累赘的,可以直接看上面的原教程链接。

  1. 创建窗口并显示出来的基本流程:

    调用SDL_Init()初始化SDL2,在通过SDL_CreateWindow()创建窗口对象win,在通过SDL_GetWindowSurface基于窗口对象创建表面对象winSurface。然后将要绘制的矩形通过SDL_FillRect()绘制到表面对象winSurface上,最后通过SDL_UpdateWindowSurface()更新窗口对象win,显示在界面上。在结束时,调用SDL_DestroyWindow()SDL_Quit()完成释放变量和关闭SDL2。

    表面:SDL 将您可以绘制的任何区域(包括加载的图像)抽象为“表面”

    #include <stdio.h>
    #include <stdlib.h>
    #include <SDL.h>
    #include <synchapi.h>
    
    int main(int argc, char* argv[]) {
        SDL_Init( SDL_INIT_EVERYTHING );    // 初始化SDL2所有部分
    
        SDL_Window* win = SDL_CreateWindow( "my window", 100, 100, 640, 480, SDL_WINDOW_SHOWN );    // 创建窗口
        SDL_Surface* winSurface = SDL_GetWindowSurface( win );   // 基于窗口创建“表面”
        SDL_UpdateWindowSurface( win );                          // 更新窗口
        SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 255, 90, 120 ));            // 绘制矩形
        SDL_UpdateWindowSurface( win ); // 更新窗口
        while (1);                              // 卡住界面(保持窗口,)
        SDL_DestroyWindow( win );       // 销毁窗口
        win = NULL; winSurface = NULL;         // 释放变量
        SDL_Quit();                            // 关闭SDL2
    
        return 0;
    }
    

    在这里插入图片描述

  2. 位图显示

    该节主要讲解了位图在SDL的保存、绘制和缩放等。同时相比于第1节教程多了SDL_Rect结构,来控制显示位置。

    #include <SDL.h>
    
    SDL_Window* win;
    SDL_Surface* winSurface;
    SDL_Rect dest;
    SDL_Surface* image1;
    SDL_Surface* image2;
    
    // 显示BMP图片
    void ShowBMP()
    {
        dest.x = 20; dest.y = 20;
        SDL_BlitSurface( image1, NULL, winSurface, &dest );  // 图片1加载到表面
        SDL_UpdateWindowSurface( win ); // 更新窗口
        while (1);                              // 卡住界面(保持窗口)
    }
    // 缩放BMP图片
    void ScaledBMP()
    {
        dest.x = 20; dest.y = 20; dest.w = 200; dest.h = 100;                      // 重新设置SDL_Rect结构对象
        SDL_BlitScaled( image1, NULL, winSurface, &dest );  // 缩放图片
        SDL_UpdateWindowSurface( win ); // 更新窗口
        while (1);                              // 卡住界面(保持窗口)
    }
    // 转换表面
    void ConvertSurface()
    {
        dest.x = 20; dest.y = 20;
        image2 = SDL_ConvertSurface( image1, winSurface->format, 0 ); // 将图片1进行转换
        SDL_BlitSurface( image2, NULL, winSurface, &dest );  // 图片2加载到表面
        SDL_FreeSurface(image1);                                             // 图片1随后释放
        SDL_UpdateWindowSurface( win ); // 更新窗口
        while (1);                              // 卡住界面(保持窗口)
    }
    
    int main(int argc, char* argv[]) {
    
        SDL_Init( SDL_INIT_EVERYTHING );                                                             // 初始化SDL2所有部分
        win = SDL_CreateWindow( "MyWindow", 100, 100, 640, 480, SDL_WINDOW_SHOWN ); // 创建窗口
        winSurface = SDL_GetWindowSurface( win );                                                  // 基于窗口创建“表面”
        image1 = SDL_LoadBMP( "../../../../logo.bmp" );                                                    // 加载BMP图形(相对路径)
    
        // 下述三个案例逐个解开注释查看
        // ShowBMP();
        // ScaledBMP();
        ConvertSurface();
    
        return 0;
    }
    

    在这里插入图片描述

  3. 事件活动

    在教程中该节主要讲解到了事件,如健值的输入事件,关闭事件等。且通过SDL_Event 定义的结构对象,和SDL_PollEvent()函数获取到事件的类型和触发健值等等。还有我怀疑,教程是不是把SDL_PollEvent函数写成SDL_PolLEvent,这压根就没有找到。下述就健值获取事件退出事件鼠标事件列出函数案例,至于官方不推荐的健值轮询,和自定义用户就举例了,有需要的可以调整教程详细观看,还有更多的其它事件可以浏览SDL的文档网站查看。

    #include <SDL.h>
    #include "SDL_events.h"
    
    SDL_Window* win;
    SDL_Surface* winSurface;
    SDL_Event ev;
    
    char running = 1;
    
    // 按键输入验证
    // 简述:通过识别按下的'1'、'2'、'3'键来,切换纯色界面。按下'4'退出案例。
    void KeyInPut()
    {
        running = 1;
        while( running)
        {
            while ( SDL_PollEvent( &ev ) != 0 ) {
                switch (ev.type) {
                    case SDL_KEYDOWN:
                        // 健值分支
                        switch ( ev.key.keysym.sym ) {
                            case SDLK_KP_1:
                                SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 255, 0, 0 ));            // 绘制红色矩形
                                break;
                            case SDLK_KP_2:
                                SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 0, 255, 0 ));            // 绘制绿色矩形
                                break;
                            case SDLK_KP_3:
                                SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 0, 0, 255 ));            // 绘制蓝色矩形
                                break;
                            case SDLK_KP_4:
                                // 退出该案例
                                running = 0;
                                break;
                        }
                        break;
                }
            }
            SDL_UpdateWindowSurface( win ); // 更新窗口
            SDL_Delay(100);
        }
    }
    
    // 窗口退出验证
    // 简述:只有在 SDL_QUIT 类型的事件下,点击'x'关闭窗口,才能得到响应关闭。
    void WinClose()
    {
        running = 1;
        while ( running ) {
            // Event loop
            while ( SDL_PollEvent( &ev ) != 0 ) {
                switch (ev.type) {
                    case SDL_QUIT:
                        running = 0;
                        break;
                }
            }
            SDL_Delay(100);
        }
    }
    
    // 鼠标输入
    // 简述:通过按下鼠标左键、中键、右键切换纯色界面。
    void MouseInput()
    {
        while ( running ) {
            while ( SDL_PollEvent( &ev ) != 0 ) {
                switch (ev.type) {
                    case SDL_MOUSEBUTTONUP:
                        // test button
                        switch ( ev.button.button ) {
                            case SDL_BUTTON_LEFT:   // 鼠标左键
                                SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 255, 255, 0 ));
                                break;
                            case SDL_BUTTON_RIGHT:  // 鼠标右键
                                SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 0, 255, 255 ));
                                break;
                            case SDL_BUTTON_MIDDLE: // 鼠标中键
                                SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 255, 0, 255 ));
                                break;
                        }
                        break;
                }
            }
            SDL_UpdateWindowSurface( win ); // 更新窗口
            SDL_Delay(100);
        }
    }
    
    // 退出后清理释放内存
    void kill() {
        // Free images
        SDL_FreeSurface( winSurface );
        // Quit
        SDL_DestroyWindow( win );
        SDL_Quit();
    }
    
    int main(int argc, char* argv[]) {
    
        SDL_Init( SDL_INIT_EVERYTHING );                                                              // 初始化SDL2所有部分
        win = SDL_CreateWindow( "MyWindow", 100, 100, 640, 480, SDL_WINDOW_SHOWN );  // 创建窗口
        winSurface = SDL_GetWindowSurface( win );                                                   // 基于窗口创建“表面”
        SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 0, 0, 0 )); // 绘制矩形
        SDL_UpdateWindowSurface( win ); // 更新窗口
    
        // KeyInPut();
        // WinClose();
        MouseInput();
    	
        kill(); // 清理释放
        return 0;
    }
    
    

    在这里插入图片描述

  4. 几何渲染

    区别于上述几个案例,在绘制图形时都是使用基于软件或 CPU 渲染。在下述中将会引入到渲染器SDL_Renderer结构来进行渲染绘制,它的渲染速度会得到提高。而且不在采用基于SDL_Surface表面结构的绘制,而是通过操作渲染器SDL_Renderer结构,定位,绘制,更新,来完成帧图。如何更多的绘制,如点、线、面等可以查看函数文档。

    代码案例中,实现的效果和案例1基本相同,被注释掉的是,之前基于表面结构实现的方法。

    SDL_Window* win;
    SDL_Renderer* renderer;
    
    int main(int argc, char* argv[]) {
        SDL_Rect rect;
        rect.x = 20; rect.y = 20; rect.w = 100; rect.h = 100;
    
        SDL_Init( SDL_INIT_EVERYTHING );                                                              // 初始化SDL2所有部分
        // win = SDL_CreateWindow( "MyWindow", 100, 100, 640, 480, SDL_WINDOW_SHOWN );  // 创建窗口
        SDL_CreateWindowAndRenderer( 640, 480, NULL, &win, &renderer );     // 创建窗口和渲染器
        // winSurface = SDL_GetWindowSurface( win );                                                   // 基于窗口创建“表面”
        // SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 0, 0, 0 )); // 绘制矩形
        SDL_SetRenderDrawColor( renderer, 255, 0, 0, 255 );
        SDL_RenderFillRect(renderer, &rect);
        // SDL_UpdateWindowSurface( win ); // 更新窗口
        SDL_RenderPresent( renderer );  // 更新窗口
    
        while (1);  // 卡住界面(保持窗口)
        // 清理释放
        SDL_DestroyRenderer( renderer );
        SDL_DestroyWindow( win );
        return 0;
    }
    
    SDL_Window* win;
    SDL_Renderer* renderer;
    
    int main(int argc, char* argv[]) {
        SDL_Rect rect;
        rect.x = 20; rect.y = 20; rect.w = 100; rect.h = 100;
    
        SDL_Init( SDL_INIT_EVERYTHING );                                                              // 初始化SDL2所有部分
        // win = SDL_CreateWindow( "MyWindow", 100, 100, 640, 480, SDL_WINDOW_SHOWN );  // 创建窗口
        SDL_CreateWindowAndRenderer( 640, 480, NULL, &win, &renderer );     // 创建窗口和渲染器
        // winSurface = SDL_GetWindowSurface( win );                                                   // 基于窗口创建“表面”
        // SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface->format, 0, 0, 0 )); // 绘制矩形
        SDL_SetRenderDrawColor( renderer, 255, 0, 0, 255 );
        SDL_RenderFillRect(renderer, &rect);
        // SDL_UpdateWindowSurface( win ); // 更新窗口
        SDL_RenderPresent( renderer );  // 更新窗口
    
        while (1);  // 卡住界面(保持窗口)
        // 清理释放
        SDL_DestroyRenderer( renderer );
        SDL_DestroyWindow( win );
        return 0;
    }
    

    在这里插入图片描述

  5. 创建纹理

    因为上述讲了渲染可以通过表面结构或渲染器结构,所以有两种。在接下来的代码案例中,将只会介绍渲染器来进行渲染的结构。关于纹理,“纹理是表面的 GPU 渲染等效物”,应该寓意着纹理的渲染是通过GPU,且更高效的。

    下述代码只提供,纹理创建到渲染。像原教程中还举例有,纹理透明度改变,图形翻转以及改变模式之类的暂不列出。

    SDL_Rect rect;
    SDL_Window* win;
    SDL_Surface* image;
    SDL_Texture* texture;
    SDL_Renderer* renderer;
    
    int main(int argc, char* argv[]) {
        SDL_Init( SDL_INIT_EVERYTHING );                                                               // 初始化SDL2所有部分
        SDL_CreateWindowAndRenderer( 640, 480, NULL, &win, &renderer );     // 创建窗口和渲染器
    
        // 获取logo图片,并创建纹理
        image = SDL_LoadBMP( "D:\\Desktop\\MyData\\CLion\\Demo\\logo.bmp" );
        texture = SDL_CreateTextureFromSurface( renderer, image );
        SDL_FreeSurface( image );
        rect.x = 20; rect.y = 20; rect.w = 300; rect.h = 300;
        SDL_RenderCopy( renderer, texture, NULL, &rect );
        SDL_RenderPresent(renderer);
    
        // 清理释放
        SDL_DestroyRenderer( renderer );
        SDL_DestroyWindow( win );
        return 0;
    }
    
    

    在这里插入图片描述

  6. 声音和扩展库

    SDL虽然有着广泛的API,但是部分区域还是要借助扩展库。这里将介绍使用到SDL_ImageSDL_Mixer库。扩展库都需要导入其代码,这里暂时就先不介绍了。

    // SDL_Image
    IMG_Init( IMG_INIT_JPG | IMG_INIT_PNG );	// 初始化
    SDL_Surface* image = IMG_Load("image.png");	// 导入图片
    IMG_Quit();	// 销毁
    
    // SDL_Mixer
    Mix_OpenAudio( 44100, MIX_DEFAULT_FORMAT, 2, 1024 );	// 初始化
    /* 加载音频 */
    Mix_Music* music;
    Mix_Chunk* sound;
    music = Mix_LoadMUS("music.wav");	// 加载音乐
    sound = Mix_LoadWAV("sound.mp3");	// 加载声音
    /* 播放音乐 */
    Mix_PlayMusic( music, -1 );
    /* 播放声音 */
    Mix_PlayChannel( -1, sound, 0 );	
    Mix_Pause( channel );
    SDL_Delay( 1000 );
    Mix_Resume( channel );
    /* 销毁 */
    Mix_FreeChunk( sound );
    Mix_FreeMusic( music );
    Mix_Quit();
    
  7. 文本渲染和输入

    该功能也是基于扩展库,基于SDL_ttf.h

    SDL_Surface* text;
    SDL_Texture* text_texture;
    SDL_Color color = { 0, 0, 0 };
    
    /* 初始化 */
    TTF_Init();
    /* 渲染文本 */
    text = TTF_RenderText_Solid( font, "Hello World!", color );
    text_texture = SDL_CreateTextureFromSurface( renderer, text );
    SDL_Rect dest = { 0, 0, text->w, text->h };
    SDL_RenderCopy( renderer, text_texture, &dest );
    /* 文本输入 */
    SDL_StartTextInput();
    string in;
    bool running = true;
    
    while ( running ) {
        SDL_Event ev;
        while ( SDL_PollEvent( &ev ) ) {
            if ( ev.type == SDL_TEXTINPUTEVENT ) {
                in += ev.text.text;
                // cout << " > " << in << endl;
            } else if ( ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_BACKSPACE && in.size()) {
                in.pop_back();
                // cout << " > " << in << endl;
            } eles if ( ev.type == SDL_QUIT ) {
                running = false;
            }
        }
    }
    
    SDL_StopTextInput();
    /* 关闭 */
    TTF_CloseFont( font );
    TTF_Quit();
    
  8. 计时:帧速率、物理、动画

    该章节讲到的概念案例都较为易懂,所以直接附上教程中的代码。现在先看个大概,可以在使用到的时候在多看看。

    /***** 定时 *****/
    Uint32 ticks = SDL_GetTicks();
    // ...中途操作
    Uint32 end = SDL_GetTicks();
    float secondsElapsed = (end - start) / 1000.0f;
    
    /***** 更高精度计数器 *****/
    Uint64 start = SDL_GetPerformanceCounter();
    // ...中途操作
    Uint64 end = SDL_GetPerformanceCounter();
    float secondsElapsed = (end - start) / (float)SDL_GetPerformanceFrequency();
    
    /***** 更高精度计数器 *****/
    bool running = true;
    while (running) {
    	Uint64 start = SDL_GetPerformanceCounter();
    	// 事件循环
    	// 物理循环
    	// 呈现循环
    	Uint64 end = SDL_GetPerformanceCounter();
    	float elapsed = (end - start) / (float)SDL_GetPerformanceFrequency();
        printf("Current FPS: %s\r\n",to_string(1.0f / elapsed));
    }
    
    /***** 限制FPS速率 *****/
    bool running = true;
    while (running) {
    	
    	Uint64 start = SDL_GetPerformanceCounter();
    	// 事件循环
    	// 物理循环
    	// 呈现循环
    	Uint64 end = SDL_GetPerformanceCounter();
    	float elapsedMS = (end - start) / (float)SDL_GetPerformanceFrequency() * 1000.0f;
    	// 上限为60FPS
    	SDL_Delay(floor(16.666f - elapsedMS));
    
    }
    
    /***** 垂直同步 *****/
    SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED );
    
    /***** 物理 *****/
    bool running;
    Uint32 lastUpdate = SDL_GetTicks();
    
    while (running) {
    	// 事件循环
    	// 物理循环
    	Uint32 current = SDL_GetTicks();
    	// 计算dT(单位:秒)
    	float dT = (current - lastUpdate) / 1000.0f;
    	for ( /* 对象列表 */ ) {
    		object.position += object.velocity * dT;
    	}
    	// 设置更新时间
    	lastUpdate = current;
    	// 呈现循环
    }
    
    /***** 动画 *****/
    float animatedFPS = 24.0f;
    bool running;
    
    while (running) {
    	// 事件循环
    	// 物理循环
    	// 呈现循环
    	Uint32 current = SDL_GetTicks();
    	// 计算dT(单位:秒)
    	for ( /* 对象列表 */ ) {
    		float dT = (current - object.lastUpdate) / 1000.0f;
    
    		int framesToUpdate = floor(dT / (1.0f / animatedFPS));
    		if (framesToUpdate > 0) {
    			object.lastFrame += framesToUpdate;
    			object.lastFrame %= object.numFrames;
    			object.lastUpdate = current;
    		}
    
    		render(object.frames[object.lastFrame]);
    	}
    }
    

◇ 基本框架

下面是我认为,最简易的基本框架,初始化、显示窗口、渲染、以及绘制。变量名称不用管,忘记干啥的了。

#include <SDL.h>
#include <synchapi.h>
#include <stdio.h>

// 全局声明--渲染器
SDL_Renderer* GlobalDebug_Renderer;
SDL_Window* window;

void Window_DebugGuiStart()
{
    SDL_Init(SDL_INIT_VIDEO);

    window = SDL_CreateWindow(
            "SDL Simple Line Drawing",
            SDL_WINDOWPOS_UNDEFINED,
            SDL_WINDOWPOS_UNDEFINED,
            128, 64,
            0
    );


    GlobalDebug_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

    // 设置绘制颜色(这里是黑色)
    SDL_SetRenderDrawColor(GlobalDebug_Renderer, 0, 0, 0, 255);
    SDL_RenderClear(GlobalDebug_Renderer);
}

void Window_DebugGuiRefresh()
{
    // 更新窗口以显示绘制的内容
    SDL_RenderPresent(GlobalDebug_Renderer);
}

void Window_DebugGuiEnd()
{
    // 保持窗口
    char running = 1;
    SDL_Event event;
    while (running) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = 0;
            }
        }
    }

    // 清理
    SDL_DestroyRenderer(GlobalDebug_Renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}
// 调试毫秒级延时
void Window_DebugGuiDelayMs(unsigned int ms)
{
    Sleep(ms);
}
// 绘制点
SDL_SetRenderDrawColor(GlobalDebug_Renderer, 255, 255, 255, 255);
SDL_SetRenderDrawColor(GlobalDebug_Renderer, 0, 0, 0, 255);

问题记录

1.编译文件后报错,如下图所示

这种情况多半还是,CMakeLists.txt写错了。

在这里插入图片描述

2.编译成功,运行基本案例返回异常

这种情况,是缺少文件,把SDL2.dll放到cmake-build-debug编译后的文件夹下,即可,忘记哪看的解决方案了。

在这里插入图片描述


资料

原版教程用到的扩展库,和目前SDL最新稳定版的包

文件数量
SDL2-devel-2.30.11-mingw.zip1
SDL2_image-devel-2.8.4-mingw.zip1
SDL2_mixer-devel-2.7.2-mingw.zip1
SDL2_ttf-devel-2.22.0-mingw.zip1
SDL2.dll1

链接: https://pan.baidu.com/s/1T0-MWNgjsXIXz-Fanov5gA  提取码: db2w

### 实现 SDL2 基本操作 为了实现 SDL2基本操作,通常会经历以下几个方面的工作: #### 初始化 SDL 子系统并检测错误 在任何 SDL 应用程序中,第一步通常是初始化所需的子系统。对于大多数应用来说,`SDL_INIT_EVERYTHING`标志就足够了。如果初始化失败,则返回 `-1` 表明存在错误。 ```c if (SDL_Init(SDL_INIT_EVERYTHING) == -1) { return -1; } ``` 这段代码尝试初始化所有的 SDL 子系统,并检查是否有任何错误发生[^1]。 #### 创建窗口 一旦成功初始化了 SDL,下一步就是创建一个窗口对象。这可以通过调用 `SDL_CreateWindow()` 函数完成,此函数接收多个参数来定义新窗口的属性,比如位置、大小以及显示模式等特性。 ```c SDL_Window* window = SDL_CreateWindow( "My Game", // 窗口标题 SDL_WINDOWPOS_UNDEFINED, // 初始 X 位置 SDL_WINDOWPOS_UNDEFINED, // 初始 Y 位置 800, // 宽度 600, // 高度 SDL_WINDOW_SHOWN // 窗口标记 ); if (!window) { SDL_Log("Could not create window: %s\n", SDL_GetError()); SDL_Quit(); return -1; } ``` 上述 C 语言片段展示了如何创建一个名为"My Game"的新窗口实例;如果创建过程中出现问题,则记录日志信息并退出程序[^2]。 #### 渲染循环与事件处理 为了让应用程序保持响应状态,在主循环内持续监听来自用户的输入和其他类型的事件是非常重要的。同时也要定期更新屏幕上的图像数据以反映最新的游戏场景变化情况。 ```c bool running = true; SDL_Event event; while(running){ while(SDL_PollEvent(&event)){ switch(event.type){ case SDL_QUIT: running = false; break; default: break; } } // 更新逻辑... // 绘制帧... } // 关闭资源和关闭 SDL SDL_DestroyWindow(window); SDL_Quit(); ``` 这里展示了一个典型的渲染循环结构,其中包含了对 `SDL_QUIT` 类型事件的支持以便允许用户通过点击关闭按钮终止程序执行流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值