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. Themajor 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.zip, glWin.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, ModelGL, ViewGL 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 contextmust 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 solutionsfor 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, theuser 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.
相关文章推荐
- 维度模型数据仓库(十) —— 快照
- poj 2503
- usaco.section1.3.Prime Cryptarithm
- 原生js和jQuery实现页面跳转的学习
- Pixel
- Android资源文件res的使用详解(strings,layout,drawable,arrays等)from http://www.jcodecraeer.com/a/anzhuokaifa/an
- 数据库ORA-01033错误解决办法
- win7与VMware ubuntu虚拟机实现文件共享
- linux sudo 命令
- 如何构建一个小型的Zoomeye—-从技术细节探讨到实现
- Ubuntu 14.04 Apache2 支持中文文件名
- 解决Android Studio Gradle Build Running 特别慢的问题
- coderforce 527APlaying with Paper
- Jquery+Ajax+Json+ashx生成表
- ImageView控件的基本属性
- 对于适配器ArrayAdapter中getView方法重写
- iOS 修改FMdatabase,不使用FMDatabaseQueue支持多线程数据库操作
- swift 快速奔跑的兔几 本节的内容是:SpriteKit第4讲 字体和动画
- [LeetCode]71. Missing Number缺失的数
- [LeetCode]71. Missing Number缺失的数