OpenGL學習筆記(十)

龍雲堯個人博客,轉載請註明出處。

CSDN地址:http://blog.csdn.net/michael753951/article/details/72810534

個人blog地址:http://yaoyl.cn/nehexue-xi-bi-ji-shi/


概述

本部分博客將以nehe教程第2課,筆記(三)爲藍本,將Windows中完成的基礎實驗在Ubuntu中進行實現。

【Ubuntu環境配置】中我們已經對Ubuntu中的OpenGL環境進行了配置,並且完成了最基礎的茶壺demo,接下來我們將進行實驗相關的後續開發。

需求分析

因爲實驗中我們需要終端接收到的數據能夠在圖形界面中實時顯示出來,這裏我們使用nehe教程的第二課內容,繪製一個矩形作爲進度條,起始爲0%,最高爲100%。接着我們將讓這個進度條能夠對傳輸過來的信號產生反饋。將整個過程進行拆分,我們可以按照如下步驟進行實現。

  1. 構建一個OpenGL窗口,能夠根據本地按鍵實現進度條控制
  2. 讓OpenGL的窗口能夠接收其他終端發送過來的消息
  3. 讓OpenGL窗口對接收到的信息進行一定的實時反饋(比如進度條變換)

實驗

OpenGL窗口搭建

本次使用的代碼是以nehe教程第二課中,Linux代碼爲藍本,進行修改實現的。【代碼鏈接】

首先我們將窗口顯示中的三角形去掉,留下一個長方形,同時將長方形的右邊兩個點和左邊兩個點重合以做出進度條爲0%的感覺。代碼如下:

進度條代碼

按鈕控制的實現

我在初始化InitGL的時候,將square_len初始化爲0,當有按鍵觸發的時候,square_len++,這樣就能夠完成進度條的前進工作。

在原始代碼中,我們可以看到main函數中,有一個glutKeyboardFunc方法,傳入了keyPressed的地址,在keyPressed中,定義了使用ESC按鈕進行退出的方法。我們將在這裏進行嘗試,試試方向鍵左和方向鍵右能不能讓窗口出現一些反饋。

在經過不短的一段時間的尋找之後,我終於找到了在OpenGL中,各種按鍵的鍵值是在glut.h中預定義好的。

鍵值

參考一片CSDN博客【pengl鍵盤控制一】,我們可以發現在本次程序中,ESC按鍵確實也剛好是27,這是不是也就意味着我們可以直接按照上面的方法進行修改了?首先我們將ESC的宏定義值修改爲102(十進制,對應0x66),嘗試使用左鍵退出窗體程序。

但是很意外的,沒有成功。是不是按鍵本身的鍵值並不是102?

我對代碼進行進一步修改,當有按鍵活動的時候,記錄下來當前按鍵的鍵值,將其存進本地文件中。(親測不能直接printf,因爲根本不會顯示出來,至於原因待會會有解釋)代碼如下:

/* The function called whenever a key is pressed. */
void keyPressed(unsigned char key, int x, int y) {
    /* avoid thrashing this procedure */
    //usleep(100);

    fp = fopen("key_value.txt", "a+"); // a+意味着在文本最後追加
    fprintf(fp, "%d\n", key);
    fclose(fp);

    /* If escape is pressed, kill everything. */
    if (key == ESCAPE) {
        /* shut down our window */
        glutDestroyWindow(window);

        /* exit the program...normal termination. */
        exit(0);
    }
}

嘗試按下F1~F12的按鍵,以及上下左右等按鍵,以及數字按鍵之後,我們發現txt文檔中只記錄下來了數字鍵值,根本沒有其他的鍵值。

爲了解決這個問題,我特地打開了nehe的lesson10的linux代碼(因爲這一課會用到方向鍵進行控制)。發現原來上下左右這類按鍵需要在main函數中使用glutSpecialFunc方法,傳入一個操作函數進行操作。這裏我定義了一個specialKeyPressed方法。在嘗試獲取鍵值,並且成功之後,我開始在這裏進行進度條的控制。

void specialKeyPressed(int key, int x, int y) {
    //usleep(100);

    /*
    fp = fopen("key_value.txt", "a+");
    fprintf(fp, "%d\n", key);
    fclose(fp);
    */
    switch(key) {
    case GLUT_KEY_LEFT:
        square_len--;
        if(square_len <= 0) square_len = 0;
        break;
    case GLUT_KEY_RIGHT:
        square_len++;
        if(square_len >= 100) square_len = 100;
        break;
    }
}

爲了避免越界,我們需要將square_len控制在0-100之間。同時我們直接使用glut中宏定義的鍵值,進行按鍵判斷(我已經對鍵值進行過測試。發現和宏定義的鍵值確實一致)。

到這裏,我們完成了本次demo的step1,一個使用按鍵進行進度條控制的OpenGL窗口已經構建成功。

在OpenGL創建的控制檯窗口中使用網絡協議傳輸

首先,我們需要知道,在之前的Socket編程中,我們使用的一直都是控制檯窗口程序進行的測試,但是在本次實驗中,我的理想狀態是使用OpenGL建立的窗口作爲server,新建一個控制檯作爲client,然後實驗中使用client對server進行控制。

我先定義了一個tcp_server.h頭文件。

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>

#define MYPORT  8887
#define QUEUE   20

bool tcp_server_init(int &server_sockfd, int &conn) {
    ///定義sockfd
    server_sockfd = socket(AF_INET,SOCK_STREAM, 0);

    ///定義sockaddr_in
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(MYPORT);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    ///bind,成功返回0,出錯返回-1
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1) {
        perror("bind");
        return 0;
    }

    ///listen,成功返回0,出錯返回-1
    if(listen(server_sockfd,QUEUE) == -1) {
        perror("listen");
        return 0;
    }

    ///客戶端套接字
    struct sockaddr_in client_addr;
    socklen_t length = sizeof(client_addr);

    ///成功返回非負描述字,出錯返回-1
    conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
    if(conn<0) {
        perror("connect");
        return 0;
    }
    //printf("before_conn\n");
    return 1;
}

bool tcp_server_close(int &server_sockfd, int &conn) {
    close(conn);
    close(server_sockfd);
    return 1;
}

方法一,DrawGLScene中接收消息

首先我嘗試直接在main函數中沒有進入glutMainLoop之前,建立tcp連接。(tcp_server_init(server_sockfd, conn);方法在上面已經給出來了)然後在DrawGLScene中接受消息,代碼如下。

/* The main drawing function. */
void DrawGLScene() {

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);     // Clear The Screen And The Depth Buffer
    glLoadIdentity();               // Reset The View

    memset(buffer,0,sizeof(buffer));
    int len = recv(conn, buffer, sizeof(buffer),0);
    for(int i = 0; i<len; ++i) {
        if(buffer[0] == 0x66) {
            square_len++;
            if(square_len >= 100) square_len = 100;
        } else if(buffer[0] == 0x64) {
            square_len--;
            if(square_len <= 0) square_len = 0;
        }
    }
    fputs(buffer, stdout);
    send(conn, buffer, len, 0);

    glTranslatef(-1.5f,0.0f,-6.0f);             // Move Right 3 Units

    // draw a square (quadrilateral)
    glBegin(GL_QUADS);              // start drawing a polygon (4 sided)
    glVertex3f(-1.0f, 1.0f, 0.0f);      // Top Left
    glVertex3f(-1.0f+square_len*0.05, 1.0f, 0.0f);      // Top Right
    glVertex3f(-1.0f+square_len*0.05,-1.0f, 0.0f);      // Bottom Right
    glVertex3f(-1.0f,-1.0f, 0.0f);      // Bottom Left
    glEnd();                    // done with the polygon

    // swap buffers to display, since we're double buffered.
    glutSwapBuffers();
}

初次嘗試

運行結果如圖。

初次嘗試-運行

剛看到這種情況的時候我以爲是程序運行出錯,於是經過很長一段時間的搜索才找到了一種解決辦法,這個辦法我待會會說,這裏先說目前的這個問題的解決辦法。

首先我們要知道,爲什麼會出現這個問題,它其實是TCP_\server_init函數中,執行到conn = accept(server_sockfd, (struct sockaddr*)&client\_addr, &length);的時候,在那裏停止了,結果導致glut沒有繼續繪製窗口,最終造成我們看到的窗口很奇怪。對accept稍作了解便知道,這個方法是提取出所監聽套接字的等待連接隊列中第一個連接請求,創建一個新的套接字,並返回指向該套接字的文件描述符。(參見【socket編程之accept()函數】)這個時候,所以,OpenGL中繪製的窗口會這麼奇怪,其實就只是因爲server在等待客戶端的連接,所以纔會繼續沒有往下執行而已。

我們打開之前的tcp編程中,tcp_client_demo2項目,編譯並且運行,和server成功建立上連接成功,理想中,這個時候應該是沒有問題了,但是運行以後,顯示依然有問題。

問題2

這裏我猜測是因爲recv阻塞了整個進程,造成後續畫筆繪製沒辦法繪製。因爲當我在client中發送一個f之後,server中的窗口就立刻能夠正常移動進度條了。

解決1

最終的代碼如下:

//
// This code was created by Jeff Molofee '99 (ported to Linux/GLUT by Richard Campbell '99)
//
// If you've found this code useful, please let me know.
//
// Visit me at www.demonews.com/hosted/nehe
// (email Richard Campbell at [email protected])
//
#include <GL/glut.h>    // Header File For The GLUT Library
#include <GL/gl.h>  // Header File For The OpenGL32 Library
#include <GL/glu.h> // Header File For The GLu32 Library
#include <unistd.h>     // Header File For sleeping.
#include <stdio.h>
#include "tcp_server.h"

/* ASCII code for the escape key. */
#define ESCAPE 27
#define VK_LEFT 37
#define VK_RIGHT 39

FILE *fp = NULL;//需要注意
int square_len;

/* TCP 鏈接  */
#define BUFFER_SIZE 1024
int server_sockfd, conn;
char buffer[BUFFER_SIZE];
bool server_init_flag;

/* The number of our GLUT window */
int window;

/* A general OpenGL initialization function.  Sets all of the initial parameters. */
void InitGL(int Width, int Height) {        // We call this right after our OpenGL window is created.
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);       // This Will Clear The Background Color To Black
    glClearDepth(1.0);              // Enables Clearing Of The Depth Buffer
    glDepthFunc(GL_LESS);               // The Type Of Depth Test To Do
    glEnable(GL_DEPTH_TEST);            // Enables Depth Testing
    glShadeModel(GL_SMOOTH);            // Enables Smooth Color Shading

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();               // Reset The Projection Matrix

    gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);   // Calculate The Aspect Ratio Of The Window

    glMatrixMode(GL_MODELVIEW);

    square_len = 0;
    server_init_flag = false;
}

/* The function called when our window is resized (which shouldn't happen, because we're fullscreen) */
void ReSizeGLScene(int Width, int Height) {
    if (Height==0)              // Prevent A Divide By Zero If The Window Is Too Small
        Height=1;

    glViewport(0, 0, Width, Height);        // Reset The Current Viewport And Perspective Transformation

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
    glMatrixMode(GL_MODELVIEW);
}

/* The main drawing function. */
void DrawGLScene() {

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);     // Clear The Screen And The Depth Buffer
    glLoadIdentity();               // Reset The View

    memset(buffer,0,sizeof(buffer));
    int len = recv(conn, buffer, sizeof(buffer),0);
    for(int i = 0; i<len; ++i) {
        if(buffer[0] == 0x66) {
            square_len++;
            if(square_len >= 100) square_len = 100;
        } else if(buffer[0] == 0x64) {
            square_len--;
            if(square_len <= 0) square_len = 0;
        }
    }
    fputs(buffer, stdout);
    send(conn, buffer, len, 0);

    glTranslatef(-1.5f,0.0f,-6.0f);             // Move Right 3 Units

    // draw a square (quadrilateral)
    glBegin(GL_QUADS);              // start drawing a polygon (4 sided)
    glVertex3f(-1.0f, 1.0f, 0.0f);      // Top Left
    glVertex3f(-1.0f+square_len*0.05, 1.0f, 0.0f);      // Top Right
    glVertex3f(-1.0f+square_len*0.05,-1.0f, 0.0f);      // Bottom Right
    glVertex3f(-1.0f,-1.0f, 0.0f);      // Bottom Left
    glEnd();                    // done with the polygon

    // swap buffers to display, since we're double buffered.
    glutSwapBuffers();
}

/* The function called whenever a key is pressed. */
void keyPressed(unsigned char key, int x, int y) {

    /* If escape is pressed, kill everything. */
    if (key == ESCAPE) {
        /* shut down our window */
        glutDestroyWindow(window);

        if(!tcp_server_close(server_sockfd, conn)) {
            exit(1);
        }
        /* exit the program...normal termination. */
        exit(0);
    }
}

void specialKeyPressed(int key, int x, int y) {
    //usleep(100);
    switch(key) {
    case GLUT_KEY_LEFT:
        square_len--;
        if(square_len <= 0) square_len = 0;
        break;
    case GLUT_KEY_RIGHT:
        square_len++;
        if(square_len >= 100) square_len = 100;
        break;
    }
}

int main(int argc, char **argv) {


    /* Initialize GLUT state - glut will take any command line arguments that pertain to it or
       X Windows - look at its documentation at http://reality.sgi.com/mjk/spec3/spec3.html */
    glutInit(&argc, argv);

    /* Select type of Display mode:
       Double buffer
       RGBA color
       Alpha components supported
       Depth buffer */
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);

    /* get a 640 x 480 window */
    glutInitWindowSize(640, 480);

    /* the window starts at the upper left corner of the screen */
    glutInitWindowPosition(0, 0);

    /* Open a window */
    window = glutCreateWindow("我的第一個長方形進度條demo");

    /* Register the function to do all our OpenGL drawing. */
    glutDisplayFunc(&DrawGLScene);

    // 全屏
    /* Go fullscreen.  This is the soonest we could possibly go fullscreen. */
    //glutFullScreen();

    /* Even if there are no events, redraw our gl scene. */
    glutIdleFunc(&DrawGLScene);

    /* Register the function called when our window is resized. */
    glutReshapeFunc(&ReSizeGLScene);

    /* Register the function called when the keyboard is pressed. */
    glutKeyboardFunc(&keyPressed);

    /* Register the function called when special keys (arrows, page down, etc) are pressed. */
    glutSpecialFunc(&specialKeyPressed);

    /* Initialize our window. */
    InitGL(640, 480);

    // 嘗試在glutMainLoop之外建立tcp連接
    tcp_server_init(server_sockfd, conn);

    /* Start Event Processing Engine */
    glutMainLoop();

    return 1;
}

方法二,在空閒回調函數中接受消息

使用空閒回調函數glutIdleFunc。參考博客【Idle回調函數的使用】(原出處未知),以及一篇很有用的博客【OpenGL下圖形的交互控制[轉]】

在nehe的所有教程中,圖像的轉變均是在DrawGLScene實現的,這種方法在單純的圖像變換,不存在任何等待的時候,是沒有問題的。但是一旦需要等待的時候,就會出現之前截圖中那樣,圖片繪製上出現問題,畫面顯示會很不流暢,我剛開始接觸的時候也以爲是自己的代碼寫的有問題。爲了解決這個問題,我們可以試試其他的方法。

在博客中我們也知道,一般更新場景數據的時候,使用的就是Idle Callback。剛好符合我們的需求。下面我將說明一下代碼的編寫。

首先在main函數中已經定義好的部分回調函數後面加上一行空閒回調函數。

    //tcp_server_init(server_sockfd, conn);
    glutIdleFunc(&IdleFun);  // idle 回調函數

有趣的是,我們發現,DrawGLScene方法也是在空閒回調函數中執行的。不過在main函數中出現兩個DrawGLScene函數的時候,對程序的執行並不影響。

接下來我們定義IdleFun

void IdleFun() { // 回調函數,在控制檯中的一些操作,需要在本部分進行控制
    //printf("test\n");
    if(!server_init_flag) {
        //square_len++;
        //if(square_len >= 100) square_len = 0;
        //printf("init\n");
        if(tcp_server_init(server_sockfd, conn)) printf("success\n");
        else printf("false\n");
        server_init_flag = true;
        glutPostRedisplay();
    } else {
        memset(buffer,0,sizeof(buffer));
        int len = recv(conn, buffer, sizeof(buffer),0);
        for(int i = 0; i<len; ++i) {
            if(buffer[0] == 0x66) {
                square_len++;
                if(square_len >= 100) square_len = 100;
            } else if(buffer[0] == 0x64) {
                square_len--;
                if(square_len <= 0) square_len = 0;
            }
        }
        fputs(buffer, stdout);
        send(conn, buffer, len, 0);
    glutPostRedisplay();
    }
}

爲了避免反覆創建tcp連接,我們使用一個全局bool變量來標誌是否已經創建連接。並且我們在刷新完場景數據之後,一定要調用glutPostRedisplay刷新當前屏幕,否則當前屏幕不會自動刷新,你也將看不到場景變化。具體參考可見【[譯]GLUT教程 - glutPostRedisplay函數】。(這是一篇很好的博客)最終的參考代碼如下。


#include <GL/glut.h>    // Header File For The GLUT Library
#include <GL/gl.h>  // Header File For The OpenGL32 Library
#include <GL/glu.h> // Header File For The GLu32 Library
#include <unistd.h>     // Header File For sleeping.
#include <stdio.h>
#include "tcp_server.h"

/* ASCII code for the escape key. */
#define ESCAPE 27
#define VK_LEFT 37
#define VK_RIGHT 39

FILE *fp = NULL;//需要注意
int square_len;

/* TCP 鏈接  */
#define BUFFER_SIZE 1024
int server_sockfd, conn;
char buffer[BUFFER_SIZE];
bool server_init_flag;

/* The number of our GLUT window */
int window;

/* A general OpenGL initialization function.  Sets all of the initial parameters. */
void InitGL(int Width, int Height) {        // We call this right after our OpenGL window is created.
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);       // This Will Clear The Background Color To Black
    glClearDepth(1.0);              // Enables Clearing Of The Depth Buffer
    glDepthFunc(GL_LESS);               // The Type Of Depth Test To Do
    glEnable(GL_DEPTH_TEST);            // Enables Depth Testing
    glShadeModel(GL_SMOOTH);            // Enables Smooth Color Shading

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();               // Reset The Projection Matrix

    gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);   // Calculate The Aspect Ratio Of The Window

    glMatrixMode(GL_MODELVIEW);

    square_len = 0;
    server_init_flag = false;
    //if(!tcp_server_init(server_sockfd, conn)) exit(1);
}

/* The function called when our window is resized (which shouldn't happen, because we're fullscreen) */
void ReSizeGLScene(int Width, int Height) {
    if (Height==0)              // Prevent A Divide By Zero If The Window Is Too Small
        Height=1;

    glViewport(0, 0, Width, Height);        // Reset The Current Viewport And Perspective Transformation

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
    glMatrixMode(GL_MODELVIEW);
}

/* The main drawing function. */
void DrawGLScene() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);     // Clear The Screen And The Depth Buffer
    glLoadIdentity();               // Reset The View

    glTranslatef(-1.5f,0.0f,-6.0f);             // Move Right 3 Units

    // draw a square (quadrilateral)
    glBegin(GL_QUADS);              // start drawing a polygon (4 sided)
    glVertex3f(-1.0f, 1.0f, 0.0f);      // Top Left
    glVertex3f(-1.0f+square_len*0.05, 1.0f, 0.0f);      // Top Right
    glVertex3f(-1.0f+square_len*0.05,-1.0f, 0.0f);      // Bottom Right
    glVertex3f(-1.0f,-1.0f, 0.0f);      // Bottom Left
    glEnd();                    // done with the polygon

    // swap buffers to display, since we're double buffered.
    glutSwapBuffers();
}

/* The function called whenever a key is pressed. */
void keyPressed(unsigned char key, int x, int y) {
    /* avoid thrashing this procedure */
    //usleep(100);

    /* If escape is pressed, kill everything. */
    if (key == ESCAPE) {
        /* shut down our window */
        glutDestroyWindow(window);

        if(!tcp_server_close(server_sockfd, conn)) {
            exit(1);
        }
        /* exit the program...normal termination. */
        exit(0);
    }
}

void specialKeyPressed(int key, int x, int y) {
    //usleep(100);

    switch(key) {
    case GLUT_KEY_LEFT:
        square_len--;
        if(square_len <= 0) square_len = 0;
        break;
    case GLUT_KEY_RIGHT:
        square_len++;
        if(square_len >= 100) square_len = 100;
        break;
    }
}

void IdleFun() { // 回調函數,在控制檯中的一些操作,需要在本部分進行控制
    //printf("test\n");
    if(!server_init_flag) {
        //square_len++;
        //if(square_len >= 100) square_len = 0;
        //printf("init\n");
        if(tcp_server_init(server_sockfd, conn)) printf("success\n");
        else printf("false\n");
        server_init_flag = true;
        glutPostRedisplay();
    } else {
        memset(buffer,0,sizeof(buffer));
        int len = recv(conn, buffer, sizeof(buffer),0);
        for(int i = 0; i<len; ++i) {
            if(buffer[0] == 0x66) {
                square_len++;
                if(square_len >= 100) square_len = 100;
            } else if(buffer[0] == 0x64) {
                square_len--;
                if(square_len <= 0) square_len = 0;
            }
        }
        fputs(buffer, stdout);
        send(conn, buffer, len, 0);
    }
    glutPostRedisplay();
}

int main(int argc, char **argv) {

    /* Initialize GLUT state - glut will take any command line arguments that pertain to it or
       X Windows - look at its documentation at http://reality.sgi.com/mjk/spec3/spec3.html */
    glutInit(&argc, argv);

    /* Select type of Display mode:
       Double buffer
       RGBA color
       Alpha components supported
       Depth buffer */
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);

    /* get a 640 x 480 window */
    glutInitWindowSize(640, 480);

    /* the window starts at the upper left corner of the screen */
    glutInitWindowPosition(0, 0);

    /* Open a window */
    window = glutCreateWindow("我的第一個長方形進度條demo");

    /* Register the function to do all our OpenGL drawing. */
    glutDisplayFunc(&DrawGLScene);

    // 全屏
    /* Go fullscreen.  This is the soonest we could possibly go fullscreen. */
    //glutFullScreen();

    /* Even if there are no events, redraw our gl scene. */
    glutIdleFunc(&DrawGLScene);

    /* Register the function called when our window is resized. */
    glutReshapeFunc(&ReSizeGLScene);

    /* Register the function called when the keyboard is pressed. */
    glutKeyboardFunc(&keyPressed);

    //tcp_server_init(server_sockfd, conn);
    glutIdleFunc(&IdleFun);  // idle 回調函數

    /* Register the function called when special keys (arrows, page down, etc) are pressed. */
    glutSpecialFunc(&specialKeyPressed);

    /* Initialize our window. */
    InitGL(640, 480);

    /* Start Event Processing Engine */
    glutMainLoop();

    return 1;
}

完成這一步之後,基本上你就能夠完成一個能夠通過網絡通信控制窗口界面的小demo了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章