您的位置:首页 > 其它

MVC 详解(例子)

2015-11-10 09:19 609 查看


OpenGL Windows GUI Application

This article is about a MVC (Model-View-Controller) framework to create OpenGL GUI applications on Windows platform. MVC architecture is a common design framework for GUI applications, and is used in many GUI libraries, such as .NET, MFC, Qt, Java, etc. The
major benefits of this MVC framework are the complete separation of system-independent OpenGL calls from Windows system and the universal message router for multiple windows.

Download: glWinSimple.zipglWin.zip
Overview
Example 1: glWinSimple
Create OpenGL window
Separate thread for rendering OpenGL
Example 2: glWin
Message router
More about Controller class


Overview


 
A diagram of MVC design

MVC paradigm is to divide an application into 3 separate components; Model, View and Controller components in order to minimize dependencies between them.

Model component is the brain part of the application, which contains all application data and implementations to tell how the application behaves. More importantly, Model component does not have any reference to View or Controller component,
which means Model component is purely independent. It does not know which Controller or View component is associated with. Model component simply processes the requests from any Controller or View component.

View component is responsible to render the visual contents onto the screen. Also, View module does not have any reference to Controller component (independent on Controller). It only performs rendering process when any Controller component
requests to update the visual. However, View component should reference to a certain Model component, because it must know where to obtain the data from, so, it can render the data on the screen.

Controller component is the bridge between users and the application by receiving and handling all user events, such as keyboard and mouse inputs. This module should know which Model and View component to access. In order to handle a user event,
Controller component requests Model to process the new data, and at the same time, tells View component to update the visuals.


 
A GUI Application: Currency Converter

Here is a very simple scenario. Imagine that you make a currency conversion program from Canadian dollars to US dollars. When a user click "Convert"button, what should your application do?
Controller gets the button click event first.
Controller sends the input value to Model and requests the conversion.
Model converts the input to US dollars and save the result.
Controller requests View to display the result.
View gets the result from Model.
View displays the result on the screen.

The major advantages of MVC design are clarity and modularity. Since MVC paradigm cleanly separates an application into 3 logical components, it is cleaner and easier to understand the role of each component, and to maintain each module separately by multiple
developers. Because of its efficient modularity, the components can be interchangeable and scalable. For example, you can customize the look-and-feel of View component without modifying Model and Controller modules, or can add multiple different views
(table and chart) simultaneously.


Example 1: glWinSimple


 

This is a single window OpenGL application. It does not have GUI controls except mouse interaction. However, this example is better to understand how to implement the MVC design to an OpenGL application. Then, we will discuss with the more complex example in the
following section.


 
MVC diagram of glWinSimple

Get the source and binary (64 bit) file from: glWinSimple.zip 
(Updated: 2014-07-17)

This application consists of 3 separate C++ classes, ModelGLViewGL andControllerGL. For OpenGL application, all system-independent OpenGL commands can be placed in the ModelGL component, so, Model component
itself can be re-usable for other platforms without any modification. Therefore, Model is purely independent on Controller and View modules.

View component is for rendering the visual onto screen. Therefore, all display device properties (rendering context, colour bits, etc) go into this component. Also, system-specific OpenGL commands are placed in this component, such as wglCreateContext() and
wglMakeCurrent(). Again, View component does not reference to Controller (independent on Controller), but may need to reference to Model, for instance, to get Model's data to update View contents.

Controller component is for receiving all user events first, then, updates Model's states, and notifies to View component to render the scene. It has the basic input handling functions for keyboard (escape key) and mouse left/right buttons. Please look at the
the source code in ControllerGL class; keyDown(), lButtonDown(), rButtonDown(), mouseMove(), etc. ControllerGL class is derived from the base class of Controller.cpp. You can simply add event handlers into ControllerGL class if you need to override the default
behaviors.

These 3 objects are created in main(), and then, a single window is created with the reference (pointer) to the ControllerGL object. I used a helper class, Window.cpp to create a window. Notice that the main function remains very simple, and all detailed implementations
are moved to 3 separate components; ModelGL, ViewGL and ControllerGL.
int WINAPI WinMain(...)
{
// instantiate Model and View, so Controller can reference them
ModelGL model;
Win::ViewGL view;   // under "Win" namespace because of Windows specific

// create ControllerGL with ModelGL and ViewGL pointers
Win::ControllerGL glCtrl(&model, &view);

// create a window with given Controller
Win::Window glWin(hInst, L"glWinSimple", 0, &glCtrl);
glWin.setWindowStyle(WS_OVERLAPPEDWINDOW | WS_VISIBLE |
WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
glWin.setClassStyle(CS_OWNDC);
glWin.setWidth(400);
glWin.setHeight(300);

glWin.create();
glWin.show();

// enter main message loop //////////////////////////////////
int exitCode;
exitCode = mainMessageLoop();

return exitCode;
}


Create OpenGL window

Creating an OpenGL window is same as creating other generic windows except OpenGL rendering context (RC). The OpenGL rendering context is a port to link OpenGL to Windows system. All OpenGL commands can pass through this rendering context. The rendering context
must be associated with a device context (DC) which has same pixel format as RC has, so, OpenGL drawing can take place on the device surface.

In your WM_CREATE handler, you can create a RC:
Get the DC of the OpenGL window with GetDC() and the window handle.
Set the desired pixel format with SetPixelFormat() and the DC.
Create new RC with wglCreateContext() and the DC.
Release the DC with ReleaseDC().

In your WM_CLOSE handler, you can delete the RC:
Release the current RC with wglMakeCurrent() and NULL parameter.
Delete the RC with wglDeleteContext().

In your rendering loop, set the rendering context as the current RC with wglMakeCurrent() before calling any OpenGL commands. I use a separate worker thread for the rendering loop. Plese see the following section, "Separate
thread for rendering OpenGL".

Finding a desired pixel format can be done by searching all available formats using DescribePixelFormat(). A standard scoring mechanism to find the best pixel format is described in findPixelFormat() method in ViewGL class.
// find the best pixel format
int findPixelFormat(HDC hdc, int colorBits, int depthBits, int stencilBits)
{
int currMode;                   // pixel format mode ID
int bestMode;                   // return value, best pixel format
int currScore;                  // points of current mode
int bestScore;                  // points of best candidate
PIXELFORMATDESCRIPTOR pfd;

// search the available formats for the best mode
bestMode = 0;
bestScore = 0;
for(currMode = 1;
::DescribePixelFormat(hdc, currMode, sizeof(pfd), &pfd) > 0;
++currMode)
{
// ignore if cannot support opengl
if(!(pfd.dwFlags & PFD_SUPPORT_OPENGL))
continue;

// ignore if cannot render into a window
if(!(pfd.dwFlags & PFD_DRAW_TO_WINDOW))
continue;

// ignore if cannot support rgba mode
if((pfd.iPixelType != PFD_TYPE_RGBA) ||
(pfd.dwFlags & PFD_NEED_PALETTE))
continue;

// ignore if not double buffer
if(!(pfd.dwFlags & PFD_DOUBLEBUFFER))
continue;

// try to find best candidate
currScore = 0;

// colour bits
if(pfd.cColorBits >= colorBits) ++currScore;
if(pfd.cColorBits == colorBits) ++currScore;

// depth bits
if(pfd.cDepthBits >= depthBits) ++currScore;
if(pfd.cDepthBits == depthBits) ++currScore;

// stencil bits
if(pfd.cStencilBits >= stencilBits) ++currScore;
if(pfd.cStencilBits == stencilBits) ++currScore;

// alpha bits
if(pfd.cAlphaBits > 0) ++currScore;

// check if it is best mode so far
if(currScore > bestScore)
{
bestScore = currScore;
bestMode = currMode;
}
}

return bestMode;
}


Separate thread for rendering OpenGL

MS Windows is an event-driven system. An event driven windows application will rest without doing anything if there is no event triggered. However, OpenGL rendering window needs to be constantly updated even if no event coming to the application. One of solutions
for constant updating OpenGL window is using PeekMessage() in the window message loop.
int mainMessageLoop()
{
MSG msg;

while(1)
{
if(::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
if(msg.message == WM_QUIT)
{
break;
}
else
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
else
{
// it shouldn't be here
// because this message loop suppose to handle window messages only.
render();
}
}
return (int)msg.wParam;
}


However, there is a better solution using a separate worker thread for rendering OpenGL. The advantage of multithread is that you can leave the message loop to do its own job, handling only the windows events, and the separate worker thread will handle rendering
OpenGL scene independently. Also, we do not need to expose any OpenGL rendering method in this message loop. So, the main message loop can remain as simple as possible.
int mainMessageLoop()
{
MSG msg;

// loop until WM_QUIT(0) received
while(::GetMessage(&msg, 0, 0, 0) > 0)
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}

return (int)msg.wParam;
}


When the window is created (WM_CREATE event triggered), the ControllerGL object will initialize ModelGL and ViewGL object, then, start a separate worker thread for OpenGL rendering. _beginthreadex() is used to create a worker thread. Please
check ControllerGL::create() and ControllerGL::runThread() for more details.
// handle WM_CREATE
int ControllerGL::create()
{
....
// create a thread for OpenGL rendering
threadHandle = (HANDLE)_beginthreadex(
0, 0,
(unsigned (__stdcall *)(void *))threadFunction,
this, 0, &threadId);
return 0;
}

// route to the worker thread
void ControllerGL::threadFunction(void* param)
{
((ControllerGL*)param)->runThread();
}

// rendering loop
void ControllerGL::runThread()
{
// set the current RC in this thread
::wglMakeCurrent(viewGL->getDC(), viewGL->getRC());

// initialize OpenGL states
modelGL->init();

// rendering loop
while(loopFlag)
{
::Sleep(10);        // yield to other processes or threads

modelGL->draw();
viewGL->swapBuffers();
}

// terminate rendering thread
::wglMakeCurrent(0, 0);     // unset RC
::CloseHandle(threadHandle);
}


Example 2: glWin


 

This example has 3 windows; the OpenGL rendering child window, the other child dialog window containing all controls, and finally, the main window enclosing those 2 child windows. (There are menu bar and status bar in the main window, but we don't count on
them in this article.)

Download the source and 64 bit binary from: glWin.zip 
(Updated: 2014-07-17)


 
MVC diagram of glWin

Since there are 3 windows, 3 Controller objects are required for this application, 1 Controller per each window: ControllerMain, ControllerGL and ControllerFormGL. And, 2 View components, ViewGL and ViewFormGL are required; one for OpenGL rendering, and for
the dialog child window. Because the main window is simply a container, it does not have a View component. Notice that there is only one Model object necessary, and it is referenced by all 3 Controllers and 2 Views.

ViewFormGL is a dialog (or form) window containing controls (buttons, text box, etc). Therefore, all controls are defined in ViewFormGL class. I implemented the classes for the commonly used controls, and they are declared in "Controls.h" file. The currently
supported controls are Button, RadioButton, CheckBox, TextBox, EditBox, ListBox, TrackBar, ComboBox and TreeView.

Here is a scenario what is happening when a user clicks "Animate" button:
ControllerFormGL gets BN_CLICKED event first.
ControllerFormGL sets the animation flag true in ModelGL, so the earth can spin.
ControllerFormGL tells ViewFormGL to change the caption of the button to "Stop".


Message Router

A windows application must provide a pointer to a callback function (window procedure) when you create a window, more correctly when you register a window class (RegisterClass() or RegisterClassEx()). When something happens to your program, for example, the
user clicks a button control, Windows system sends a message to your program, and the message is passed to the window procedure that you specified.

In general, the window procedure looks like this:
LRESULT CALLBACK wndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_COMMAND:
...
break;

case WM_CREATE:
...
break;

// more messages
...

default:
return ::DefWindowProc(hWnd, msg, wParam, lParam);
}

return 0;
}


The other benefit of this MVC framework is the dynamic message router, which distributes the messages to the Controller associated with the window handle. So, we can create one window procedure once and reuse it multiple times for all windows that you create.
This technique is inspired by Windows API tutorials from Reliable Software. The basic idea is passing the pointer to the Controller object in lpParam of
CreateWindow() or CreateWindowEx() when you create a window. When WM_NCCREATE message is called, we extract the pointer value fromlpCreateParams and store it as the window's GWL_USERDATA attribute. Later, other message
is triggered, we simply look up the GWL_USERDATA of a window to find out which controller is associated with the window. Since this window procedure will be used for all windows you create, you don't have to worry about rewriting another window procedure any
more.

The actual message router looks like this:
LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
LRESULT returnValue = 0;    // return value

// find controller associated with window handle
static Win::Controller *ctrl;
ctrl = (Controller*)::GetWindowLongPtr(hwnd, GWL_USERDATA);

if(msg == WM_NCCREATE)      // Non-Client Create
{
// WM_NCCREATE message is called before non-client parts(border,
// titlebar, menu,etc) are created. This message comes with a pointer
// to CREATESTRUCT in lParam. The lpCreateParams member of CREATESTRUCT
// actually contains the value of lpPraram of CreateWindowEX().
// First, retrieve the pointrer to the controller specified when
// Win::Window is setup.
ctrl = (Controller*)(((CREATESTRUCT*)lParam)->lpCreateParams);
ctrl->setHandle(hwnd);

// Second, store the pointer to the Controller into GWL_USERDATA,
// so, other messege can be routed to the associated Controller.
::SetWindowLongPtr(hwnd, GWL_USERDATA, (LONG_PTR)ctrl);

return ::DefWindowProc(hwnd, msg, wParam, lParam);
}

// check NULL pointer, because GWL_USERDATA is initially 0, and
// we store a valid pointer value when WM_NCCREATE is called.
if(!ctrl)
return ::DefWindowProc(hwnd, msg, wParam, lParam);

// route messages to the associated controller
switch(msg)
{
case WM_CREATE:
returnValue = ctrl->create();
break;

// more messages
...

default:
returnValue = ::DefWindowProc(hwnd, msg, wParam, lParam);
}

return returnValue;
}


And, you create 3 windows like the following in the main function. Notice that we provide the pointer to Controller object when we initialize the parameters of a window. This pointer value will be stored in GWL_USERDATA of each window, then, the message router
will dynamically distribute the window events to the given Controller object.
// create the main window
Win::ControllerMain mainCtrl;
Win::Window mainWin(hInst, "glWinApp", 0, &mainCtrl);
mainWin.create();

// create the OpenGL rendering window
Win::ControllerGL glCtrl(&modelGL, &viewGL);
Win::Window glWin(hInst, L"WindowGL", mainWin.getHandle(), &glCtrl);
glWin.create();

// create the dialog window containing controls
Win::ControllerFormGL formCtrl(&modelGL, &viewFormGL);
Win::DialogWindow glDialog(hInst, IDD_FORMVIEW, mainWin.getHandle(), &formCtrl);
glDialog.create();


There is a small difference for the dialog window procedure. We store the pointer of the Controller toGWL_USERDATA when WM_INITDIALOG message is called, instead of WM_NCCREATE. Please check procedure.cpp for more details.


More about Controller class

This MVC framework provides a base Controller class, which is the default event handler. Indeed, it does nothing and returns 0. To do some meaningful tasks when a message comes, you need to create a class derived from the base class, and override (rewrite)
the virtual functions. The names of the virtual functions in Controller base class are same as message IDs without prefix, for example, lButtonDown() for WM_LBUTTONDOWN message.

For the above glWin program, ControllerMain, ControllerGL, and ControllerFormGL are derived from the Controller base class.

ControllerMain is responsible to handle WM_CLOSE message to quit the program when the user click close button or select "Exit" from main menu.

ControllerGL simply handles mouse interactions for the camera manipulations (zoom and rotate the camera). And, it creates a worker thread for OpenGL renderer when WM_CREATE message is arrived.

ControllerFormGL manages all control interfaces.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: