mingl in action

Introduction

MinGW is a full-featured (and free) port of the GNU Compiler Collection to the Win32 platform. Although built around the venerable GCC codebase, MinGW runs natively under Windows, and generates native Windows executable files. This combination of a powerful well-known compiler applied to an ubiquitous target platform makes MinGW a very useful development tool.

Previous postings by this author have shown how to use MinGW to do Direct3D development and GDI+ development. This latest offering describes how you can use MinGW to develop 3D applications using OpenGL.

While OpenGL is in some sense a competitor to Microsoft's own Direct3D technology, it nevertheless enjoys good hardware support from Windows-compatible 3D devices. The OpenGL API is also an intuitive one with a large development community. The article at hand gives a thorough description of how the author was able to set up a free OpenGL development environment.

OpenGL is a less centralized development project than Direct3D, and as a result the development setup described here must rely on a conglomeration of several auxiliary tools. The OpenGL Utility Toolkit (GLUT) provides the ability to create and manage a viewing window. The OpenGL Extension Wrangler (GLEW) library manages extensions to OpenGL, which exist to provide abstract support for the wide variety of 3D devices (and host platforms) available. (The necessary components of these two libraries are included with the supplied code.) Finally, the work presented here depends on The OpenGL Utility Library (GLU), although this most basic level of abstraction has become part of many default GCC installations, MinGW included.

Finally, a few caveats are in order before getting into details. As in the Direct3D article, many important OpenGL concepts are necessarily left unexplored, in the interest of simplicity and clarity. In particular, a basic hardware setup is assumed. Nothing is done to fail gracefully on lesser hardware (which should be very uncommon), nor is anything specially done to exploit superior hardware. This approach may not be adequate for all applications, but it simplifies things considerably here. A very small number of similar compromises are mentioned in the discussion below. Nevertheless, it is hoped that the code presented here offers a fairly impressive OpenGL demonstration, which also happens to be concise, portable, and fast, even on budget hardware.

Setup

To set up the development environment described in this article, it is first necessary to install MinGW. The exact version of MinGW used by the author is available for download, although most other versions will work as well. The rest of the article will be easiest to follow if the default installation paths (e.g. "C:\MinGW\") are used during MinGW installation. Note that the C++ compiler may not be selected for installation by default, but it is required for the code supplied here, as is the C compiler. After installation, Windows may ask whether installation completed successfully, and it is advisable to answer "yes."

As installed, MinGW already contains a basic OpenGL development kit, as exposed by the -lopengl command line parameter. Beyond this, though, it is still necessary to install GLEW and GLUT. In the example at hand, GLEW is compiled into the target executable from a ".c" file. GLEW is thus statically linked into the target executable, and in general the author prefers this approach for its predictability. GLUT, though, is linked dynamically in classic Windows fashion: a stub ".lib" file is referenced during compilation, and this makes calls into a ".dll" file which must be present alongside the demo executable at runtime. (Technically there are other places it can be stored, e.g., under "c:\Windows\System32"). This linking strategy proved expedient because of the large number of source code files provided for GLUT. Unlike GLEW, building GLUT into a project is not necessarily a matter of compiling just one (or even just a few) ".cpp" files.

The file "glew.c" is provided with the source code for this article, since it is essentially a part of the source. In addition to "glew.c", a header ("glew.h") is required to use GLEW. This header should be obtained and placed in the MinGW #include path, e.g., in the "c:\MinGW\include\GL\" folder. The exact versions of this header, and all the headers the author had to add to the "c:\MinGW\include\GL\" folder, are present in a subfolder named "GL" in the source code archive folder provided. These files are available in many other places on the Internet, and most versions of these files should work with the techniques described in this article.

GLUT relies on some headers in "GL" as well, and it also requires a ".dll" (at runtime) and a ".lib" file (to build). So, the necessary ".dll" is provided in the demo (executable) download accompanying this article, and the ".lib" is present in the corresponding source code archive. For maximum convenience, the developer using the demo code should thus combine the two downloads provided into a single folder; otherwise, it will be possible to build the demo from the source download, but the resultant ".exe" will be missing a file and will fail when executed.

Building the Demo

The demonstration executable is built from the Windows "Command Prompt" with the working folder set to the folder containing the provided source code. The GLEW source file name ("glew.c") is passed to the compiler (which is named "c++.exe"), just like the source file name for the demo itself ("mingl.cpp"). The -lopengl and -lglu32 options are used to statically link OpenGL and GLU, respectively, and the -o option is used to name the output file. An example of the full command line is shown below:

c:\mingw\bin\c++ -oc:\mingl\mingl.exe c:\mingl\glew.c
c:\mingl\mingl.cpp c:\mingl\glut32.lib -lopengl32 -lglu32

The source code is assumed to be resident in the folder "c:\mingl\" in this example. The target executable will be named "mingl.exe" and will be present in the "c:\mingl\" folder. However, note that one more issue must be dealt with before the build command shown above will return without error.

GLEW_STATIC and glewExperimental

The boolean variable glewExperimental determines whether GLEW, in "wrangling" extensions, includes extensions deemed to be experimental. This setting defaults to false, which is acceptable for the relatively limited demo offered here. However, it was necessary to deal with one more issue related to glewExperimental before the build script shown above would execute without errors. Specifically, it was necessary to add a #define statement for the constant GLEW_STATIC at the top of this file. This has been done in the supplied copy of the file. Defining this constant causes GLEW to import, export, and link correctly for our application, which uses static linking for GLEW. The alternative is to define GLEW_BUILD, which is appropriate when linking to GLEW dynamically, i.e., using a ".dll" present at runtime.

The Demo Code

The demonstration begins by presenting a stationary solid (cube) as shown in the picture presented at the beginning of the article. This is done in a 1024x768 window. The cube has an identical texture on each of its six faces. When a key (other than ESC) is pressed, the cube begins moving away from the viewer, i.e., moving negatively in the "Z" dimension (the imaginary third dimension extending forward and backward from the monitor). When the cube reaches a certain extreme, it begins moving positively in the "Z" dimension, i.e., back towards the viewer. Then, at a positive "Z" extreme, the cube begins moving away from the viewer again, and this pattern repeats itself until the user exits the program by pressing ESC or closing the window. This is accomplished in just over 300 lines of code (in "mingl.cpp"), a figure which is a bit smaller than the equivalent figure for Direct3D Programming with MinGW.

Some snapshots of the demo program in action are shown beneath this paragraph:

mingl in action

Before going further, a few words are in order about the OpenGL coordinate system. To a great extent, the extremes and proportions of the system are established by setup calls and constants shown shortly below. However, qualitative statements can be made about its three dimensions. The "X" dimension extends left to right on the screen, with coordinates increasing from left to right. The "Y" dimension extends up and down on the screen, with coordinates increasing from bottom to top. Coordinates in the "Z" dimension increase from back (behind the display) to front (closer to the viewer).

Constants

The main source code file for the demo, "mingl.cpp," begins by defining some constants. The first group of constants define the angular velocity of the cube as it spins about each dimension. These values are expressed in degrees per frame. The code for these constants is shown below:

//Cube rotation speed
const GLfloat X_ROTATE_SPEED=0.016f;
const GLfloat Y_ROTATE_SPEED=5.0f;
const GLfloat Z_ROTATE_SPEED=0.0849f;

Next, some constants related to the back-and-forth motion of the cube in the "Z" dimension are defined; these are expressed in terms of the 3D coordinate system defined later, in the setup code:

//Cube movement
const GLfloat Z_MOVE_SPEED=0.125;
const GLfloat CUBE_Z_MAX=25.0;
const GLfloat CUBE_Z_MIN=2.0;

The next set of constants defines the starting angular position of the cube. The cube cannot simply start out non-rotated; this would result in an uninteresting, two-dimensional display. So the cube is slightly skewed in each dimension, in a way that is more pleasing to the eye. Note that these values are expressed in degrees. This section also defines the starting "Z" position of the cube, START_Z_AT; this is set to place the cube close to the viewer, without getting so close that parts are hidden:

//Starting angular position of cube (slight starting skew to make it more interesting)
const GLfloat START_X_ABOUT=20.3f;
const GLfloat START_Y_ABOUT=31.6f;
const GLfloat START_Z_ABOUT=4.9f;
const GLfloat START_Z_AT=4.25f; //Initial position of cube

After this last snippet, some constants related to the cone of visibility or "frustum" (see below) are defined. These include VIEW_FIELD, which is the top-to-bottom angle of the frustum, and the minimum and maximum visible "Z" coordinates (which define the length of the frustum):

const GLfloat VIEW_FIELD=45.0f;
const GLfloat NEAR_Z=0.1f;   //Minimum visible Z coord.
const GLfloat FAR_Z=1000.0f; //Maximum visible Z coord.

The remainder of the constants defined relate to pedestrian details like screen size, character set, and file format:

const GLint WINDOW_WIDTH=1024;
const GLint WINDOW_HEIGHT=768;
const GLint WINDOW_STARTX=20;
const GLint WINDOW_STARTY=20;

const GLint ESCAPE=27; /* ASCII code for the escape key. */
const GLint TEXTURE_SIZE=512;
const GLint MAX_APPERROR=64; //Size of error text
const GLint BMP_HEADER_SIZE=54;

File-Level Variables

A few file-level variables (which are basically globals, with internal linkage) are used. These are defined after the constants. Every effort has been made to minimize this practice, but for a relatively simply demo such as this, it was deemed acceptable for such variables to be used in cases where cross-function communication is necessary. From the top, these variables hold the per-frame angular change amounts (xdelt, xydelt, and zdelt) and the "Z" positional changes (stepdelt), followed by the total rotation of the cube about each axis at any point in time (xrot, yrot, and zrot). Finally, the variable stepback holds the cube's position in the "Z" dimension over time:

static GLfloat xdelt=0.0f,ydelt=0.0f,zdelt=0.0f,stepdelt=0.0f; //Start at rest
static GLfloat xrot=START_X_ABOUT;
static GLfloat yrot=START_Y_ABOUT;
static GLfloat zrot=START_Z_ABOUT;
static GLfloat stepback=START_Z_AT;

These variables all exist because of a need for cross-functional communication. The keypress() function loads the angular change variables with values that start the cube motion once a key is pressed, for instance; the rendering function needs access to these variables as well, and it would be cumbersome (at least in an initial demo) to pass all of these values around.

Two more file-level variables are defined. First, a GLUT-specific window identifier (similar to a Win32 "window handle") is allocated in the file-level scope. This is used by a variety of setup and rendering functions. Then, the main texture object is defined. This is central to many of the functions used for the demo. These last two declarations are shown beneath this paragraph:

/* The number of our GLUT window */
GLuint window;
/*The main demo texture for the cube*/
GLuint mesh;

Entry Point

The main() function of the demo is shown below. This is located at the bottom of the file:

int main(int argc, char **argv)
{
 glutInit(&argc, argv);

 glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);
 glDisable(GLUT_ALPHA);
 glutInitWindowSize(WINDOW_WIDTH,WINDOW_HEIGHT);
 glutInitWindowPosition(WINDOW_STARTX, WINDOW_STARTY);

 window = glutCreateWindow("MinGW / OpenGL");

 glutDisplayFunc(&drawscene);
 glutIdleFunc(&drawscene);
 glutReshapeFunc(&resizer);
 glutKeyboardFunc(&keypress);
 initgl(WINDOW_WIDTH,WINDOW_HEIGHT);
 glewInit();

 makemesh(TEXTURE_SIZE,TEXTURE_SIZE);

 glutMainLoop();

 return 0;
}

This routine begins by initializing GLUT, which (as a general windowing toolkit) expects pointers to the command line parameter data.

The next four statements set up some device basics. RGB colors are used, with no transparency. Double-buffering is enabled so that the display buffer will not need to be written to directly (which would cause flicker). A "depth buffer" is requested; this allows for the 3D engine to selectively omit rendering operations for screen objects which are hidden by closer objects.

After those calls, a series of calls are made into the GLUT functions, each of which accepts the address of a handler function defined elsewhere in the code. The first of these, to glutDisplayFunc(), establishes drawscene() as our frame-drawing function. It is then also set up as the "idle" function by the next call, meaning that continuous animation will occur. The function resizer() is set up to execute if the user resizes the view window (it simply exits, since resizing is not supported) and keypress() is passed to GLUT as the keystroke handler. GLUT takes care of hooking these functions into its messaging system; as window manager, it is in charge of actually creating an OS object capable of hosting the OpenGL scene, and of managing its runtime needs.

After that, the application initialization function initgl() is called. This sets up a number of world-level and device-level parameters, which are discussed further below. Then the GLEW startup function is called, followed by another function (makemesh()) defined elsewhere in the same file and discussed in detail further below. This function sets up the texture applied to the cube.

Finally, the program enters a message loop by calling glutMainLoop(), which will not return until the application is ready to exit. After that, the application returns 0 (success) to the OS.

The function initgl() is the first of the non-library functions called by main(). Its code is shown below:

void initgl(GLint width, GLint height)
{
 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 glClearDepth(1.0);
 glEnable(GL_DEPTH_TEST);
 glShadeModel(GL_SMOOTH);

 glMatrixMode(GL_PROJECTION);
 gluPerspective(VIEW_FIELD,(GLfloat)width/(GLfloat)height,NEAR_Z,FAR_Z);
 glMatrixMode(GL_MODELVIEW);
}

The first two calls clear the screen and depth buffer. Then, a call necessary for depth buffering is made, followed by a call to turn on device-level pixel smoothing (anti-aliasing).

Then, the final three calls shown above are used to set up the perspective system of the scene. These three calls typify the architecture of OpenGL in several ways. First, OpenGL is perhaps best modeled as a state machine, and this is evident above. Second, the interface used to transition between states is based on function calls. Above, a call into a simple function is first used to place OpenGL into a mode (GL_PROJECTION) in which matrix operations affect the projection matrix. Then, a GLU utility function (gluPerspective()) is called which does some matrix operations. Finally, OpenGL is taken out of this special mode and placed back into the default mode (GL_MODELVIEW), which is used to move and render objects in 3D space.

The function gluPerspective() accepts a variety of parameters about the cone-like "frustum" (a truncated rectangular pyramid) that comprises visible 3D space at any point in time. First, the vertical angle between the top and bottom of the frustum is passed; this is 45 degrees here (VIEW_FIELD), which is adequate for the entire actual movement of the demo's single polyhedron. Then, the aspect ratio of the scene is passed (1024x768). Finally, the near and far plane coordinates in the "Z" dimension are passed; these serve to place the top and bottom of the frustum (if one were to stand it pyramid-style on its largest rectangular surface). These numbers are in many ways arbitrary; any number of value combinations could conceivably be made to work. In our case, it is simply necessary to size extremes such as these so that the demo cube will remain visible at all times.

Texturing

In addition to initgl(), the main() function calls the function makemesh(), which sets up the demo cube's texture. This is loaded from the file "mesh512.bmp" at runtime. The beginning of the code for makemesh() is shown below:

void makemesh(GLint xSize,GLint ySize)
{
 FILE * file;

 BYTE * texdata = (BYTE*) malloc( xSize * ySize * 3 ); //3 is {R,G,B}

 file = fopen(TEXTURE_FILE, "rb" );

 fseek(file,BMP_HEADER_SIZE,SEEK_CUR);
 fread( texdata, xSize * ySize * 3, 1, file );
 fclose( file ); 

This first section uses the C Standard Library file I/O functions to load the contents of the texture bitmap file. A 54-byte header is skipped. The next section of the function is shown below:

glEnable( GL_TEXTURE_2D );
char* colorBits = new char[ xSize * ySize * 3];
for(GLint a=0; a<xSize * ySize * 3; ++a) colorBits[a]=0xFF; 

First, the code shown above enables the use of two-dimensional texturing. Then, a byte array sufficient for holding a 24-bit version of the (512x512) texture bitmap is constructed, and initialized to plain white. The code continues as shown below:

glGenTextures(1,&mesh);
glBindTexture(GL_TEXTURE_2D,mesh);

glTexImage2D(GL_TEXTURE_2D,0 ,3 , xSize,
             ySize, 0 , GL_RGB, GL_UNSIGNED_BYTE, colorBits);
app_assert_success("post0_image");

The first three calls basically serve to create an OpenGL texture and set it up. The plain white appearance bitmap just created is used for the initial texture appearance. Finally, the application level function app_assert_success() is called. This is a wrapper for glGetError(), a function which the OpenGL documentation recommends calling at least once per frame (this requirement is exceeded by the code provided). The string parameter passed to app_assert_success() is displayed to the end user; the particular values used for this purpose are somewhat cryptic, but are unique and thus aid in error reporting. Calls to app_assert_success() are sprinkled throughout the remainder of the code presented, before and after key operations.

The makemesh() function continues as shown below:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR);
app_assert_success("pre_getview");

These calls configure aspects of OpenGL texturing. First, simple linear magnification and reduction of the texture is specified. This is fast and accurate, and exhibits an acceptable level of aliasing in testing. Then the program checks for errors once more, before continuing thus:

//Save viewport and set up new one
GLint viewport[4]; //4 is {X,Y,Width,Height}
glGetIntegerv(GL_VIEWPORT,(GLint*)viewport);

app_assert_success("pre_view");
glViewport(0,0,xSize,ySize);
app_assert_success("post0_view");

//Clear target and depth buffers
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

glPushMatrix();   //Duplicates MODELVIEW stack top
glLoadIdentity(); //Replace new top with {1}

app_assert_success("ogl_mvx");

glDrawPixels(xSize,ySize,GL_BGR, GL_UNSIGNED_BYTE,texdata);

This section of code actually draws the bitmap data loaded from the texture file onto the (previously white) texture. To do this requires us to first set up a "viewport" equal in size to the texture. The previous viewport (a part of our 3D rendering setup) is saved in an array first. Then, the code pushes an identity matrix onto the main transformation stack (i.e., the GL_MODELVIEW stack) because we do not want the data loaded in from the file shifted or skewed in any way. Again, the existing matrix at the top of this stack is saved first, in a stack slot from which it will be recovered later. Finally, glDrawPixels() is used to draw the necessary pixels. The drawing is done into the target buffer. The next section of code transfers this data into the texture itself. First, it removes the identity matrix at the top of the GL_MODELVIEW stack, restoring this stack to its previous state:

app_assert_success("pre_copytext");
glPopMatrix();
app_assert_success("copytext2");
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0,0, xSize, ySize, 0);
app_assert_success("post_copy");

Finally, the viewport is restored, and some dynamic data is freed:

 //Restore viewport
 glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);

 app_assert_success("ogl_mm1");
 delete[] colorBits;
 free(texdata);
}

Rendering Function

The actual work of rendering each frame is handled by drawscene(), which GLUT calls repeatedly as needed. This function begins as shown below:

void drawscene()
{
 glBindTexture(GL_TEXTURE_2D,mesh);
 glLoadIdentity(); // make sure we're no longer rotated.

This snippet begins by selecting the mesh texture created earlier. Then the identity matrix is loaded into the GL_MODELVIEW stack, as a basis for a series of transformations about to occur.

The first of these transformations locates the cube in the "Z" dimension, based on the variable stepback. This variable is incremented by the constant stepdelt, and also must be reversed once it reaches an extreme point. The code that handles these things is shown below:

glTranslatef(0.0f,0.0f,-stepback);
if(stepback>CUBE_Z_MAX || stepback<CUBE_Z_MIN) stepdelt*=-1.0;
stepback+=stepdelt;

Next, some transformations are placed atop the GL_MODELVIEW stack. These are intended to rotate the cube about all three axes, according to the variables xrot, yrot, and zrot. These are running totals similar in nature to stepdelt, except that these three variables are angles expressed in degrees. OpenGL provides the function glRotatef() to effect such rotations:

glRotatef(xrot,1.0f,0.0f,0.0f); // Rotate about X axis
glRotatef(yrot,0.0f,1.0f,0.0f);
glRotatef(zrot,0.0f,0.0f,1.0f); 

Finally, with the texture applied and the necessary transformations and translations in place, the code can move on to the actual rendering of the cube. The code to draw the first face of the cube is shown below, along with some necessary preliminary calls:

// Clear screen and depth buffer
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

// Draw the cube (6 faces)
glBegin(GL_QUADS);

// Top of cube
glTexCoord2d(1.0,1.0);
glVertex3f( 1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Top)
glTexCoord2d(0.0,1.0);
glVertex3f(-1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Top)
glTexCoord2d(0.0,0.0);
glVertex3f(-1.0f, 1.0f, 1.0f); // Bottom Left Of The Quad (Top)
glTexCoord2d(1.0,0.0);
glVertex3f( 1.0f, 1.0f, 1.0f); // Bottom Right Of The Quad (Top)

Above, the target and depth buffers are first cleared, to give a clean starting point for rendering. Then, OpenGL is placed in GL_QUAD mode, in which four-sided polygons are drawn. Finally, the four corner coordinates (or vertices) of the face are drawn using glVertex3f(), which receives the X/Y/Z coordinate of each vertex in three floats, and glTexCoord2d(), which is used to pin the texture down to the face at each vertex.

The parameters to glVertex3f() are expressed using the 3D coordinate system set up in the initialization code already shown. The cube is 2 x 2 x 2 in volume. In the code shown above, the first vertex is the back right-hand vertex of the top face. This is located at -1 in the "Z" dimension (i.e., at the cube's back), at +1 in the "X" dimension (i.e., at the right side) and at +1 in the "Y" dimension, i.e., at the cube top. It is really not difficult to come up with the coordinates for each face; simply hold one coordinate constant (the "Y" dimension at 1, in this case, since this is the top face) and make vertices with every combination of 1 and -1 for the other two coordinates. This results in four vertices per face, which is appropriate.

The glTexCoord2d() function uses a 0.0 to 1.0 coordinate system; for example, the first call, glTexCoord2d(1.0,1.0), ties the first vertex to the top right-hand corner of the texture. Because both the faces and the texture are squares, each vertex will tie to a texture coordinate, resulting in simple 0 and 1 texture coordinate values. In determining texture coordinates, a helpful mental exercise is to consider the cube if it were rotated to place the relevant face at the cube front. This allows for the determination of a "top right" vertex, a "bottom left" vertex, and so on. This is the convention followed by the comments associated with each vertex.

The rest of the code to render the cube follows the same basic pattern. After that code, the rendering function concludes as shown below:

 glEnd(); // Done Drawing The Cube
 xrot+=xdelt;
 yrot+=ydelt;
 zrot+=zdelt;
 glutSwapBuffers();
}

The call to glEnd() ends the quadrilateral process begun with the last call to glBegin(). Then, a slight change is added to each rotation variable. Finally, the rendering process is completed by swapping the "back buffer" to which drawing has been applied into the display buffer.

Conclusion

MinGW and OpenGL combine to form a powerful and economical 3D development setup. Compared to Direct3D, OpenGL is seen here to be a bit more straightforward. Its simple state-based / function call architecture is generally simpler than the COM interface used by Direct3D. Inevitably, there are trade-offs inherent to this simplicity, but these do not seem all that onerous. In real-time applications that tend to dominate the entire computer, such as games, the lack of something like COM is often not a big problem anyway.

Both Direct3D and OpenGL exhibit features designed to deal with the fast pace of change evident in the marketplace for 3D hardware. GLEW is OpenGL's answer to this question (or at least it is the most prominent such technology) and it is an admirably simple one, mostly fitting into a single "C" file.

The inherent simplicity of the OpenGL design is a benefit that is mitigated somewhat by the fragmented nature of OpenGL development, with its many similar-sounding but important libraries (such as GLEW, GLU, and GLUT). Direct3D does not suffer from this problem; Microsoft makes a self-contained SDK for Direct3D, and in fact for the entire DirectX suite. (MinGW is also compatible with that SDK.) Portions of this article have been devoted to sorting through these issues, and it is hoped that the result will prove helpful.

History

This is the second major version of the article. The article and code have been updated to correct a problem with the cube coordinates, which caused one face's texture to be shown inverted.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"