Introduction
This article shows how you can bring OpenGL to both native and .NET environment.
Background
A few days ago, I wrote a visualization and charting library (called OpenGraph
library) for both native and .NET environment
based on my existing C++ OpenGL code base. I decided to reveal the essential code to the public in the hope that people can benefit from my work.
Using the Code
The demo code consists of four simple projects:
- OpenGraphLib: A C++ OpenGL Windows DLL
- OpenGraphWin32Client: A C++ Win32 OpenGL application
- OpenGraphLib_Cli: A C++/CLI class library that wraps the C++ OpenGL Windows DLL
- OpenGraphWinformDialogClient2010: A C# WinForms OpenGL application
Each project has Debug, Release, Unicode Debug, and Unicode Release configurations. Because of the size of each project and the time
constraint,
I cannot go into too much details describing the code. The following is a very brief description of each project.
Part One – OpenGraphLib
The part is a pure C++ Windows DLL (Unicode compliant). The main interface class is
COpenGraph
, which provides interfaces to setup and use OpenGL functions.
The interface is clean because the actual implementation is done in the CDrawManager
class. Clients (callers) of this code are agnostic about OpenGL.
For example, the native Win32 or MFC clients do not need to include OpenGL library headers.
struct CGLFont2d;
class CRubberBandTracker;
class CDrawManager;
class OPENGRAPHLIB_API COpenGraph
{
public:
COpenGraph(void);
virtual ~COpenGraph(void);
public:
enum {modeNONE = -1,modeTRACK, modeROTATE };
private:
enum {GRID_LIST = 0, NODE_LIST, LINE_LIST, TRIANGLE_LIST, QUAD_LIST, TEXT_LIST,END_LIST};
public:
bool Initialize(HWND hWnd);
void SetMode(int mode);
void SetWindowSize(int cx, int cy);
void Render();
void ForceRender();
void ShowAxes(bool bShow);
BOOL OnLButtonDown(RECT& rect, UINT nFlags, POINT point);
void OnMouseMove(UINT nFlags, POINT point);
HCURSOR GetModeCursor();
void OnSetCursor();
void Rotate(double fRx, double fRy, double fRz);
bool TrackRect(HWND hWnd, POINT point);
RECT GetTrackRect()const; void Clear();
void SelectAll();
void UnSelectAll();
void ReverseSelectAll();
void FreeMemory(void * p);
void FreeMemoryArray(void * p);
CIdentity AddNode(int id,CPointf pt, int nSize, COLORREF color,COLORREF colorSelected,
int nStatus = STATUS_UNSELECTED, LPCTSTR tag = 0, LPCTSTR userData = 0);
CIdentity AddLine(int id,CPointf pt1,CPointf pt2, int nThickness, COLORREF color,
COLORREF colorSelected, int nStatus = STATUS_UNSELECTED,
LPCTSTR tag = 0, LPCTSTR userData = 0 );
private:
CDrawManager* m_pDrawManager;
};
Part Two – OpenGraphWin32Client
This is a Win32 Windows application that uses the Windows native DLL (OpenGraph.dll). The
COpenGraph
object is associated with the main window at InitInstance()
.
Windows messages are handled appropriately in WndProc()
for OpenGL
window size setup, mouse events, and rendering. The WM_ERASEBKGND
message is ignored so we do not
have flickering when the window is being resized. The actual drawing objects are added by some helper functions. They are pretty simple and are not shown here.
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance;
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
m_openGraph.Initialize(hWnd);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
m_openGraph.Render();
EndPaint(hWnd, &ps);
break;
case WM_ERASEBKGND:
break;
case WM_SIZE:
{
m_nCx = LOWORD(lParam );
m_nCy = HIWORD(lParam );
m_openGraph.SetWindowSize(m_nCx, m_nCy);
}
break;
case WM_LBUTTONDOWN:
{
RECT rect;
UINT nFlags = GET_KEYSTATE_WPARAM(wParam);
POINT pt;
pt.x = LOWORD(lParam);
pt.y = HIWORD(lParam);
m_openGraph.OnLButtonDown(rect, nFlags, pt);
}
break;
case WM_MOUSEMOVE:
{
m_openGraph.OnSetCursor(); UINT nFlags = GET_KEYSTATE_WPARAM(wParam);
POINT pt;
pt.x = LOWORD(lParam);
pt.y = HIWORD(lParam);
m_openGraph.OnMouseMove(nFlags, pt);
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Part Three – OpenGraphLib_Cli
The part is a C++/CLI .NET class library (OpenGraph_Cli.dll). It serves as a bridge between the native C++ DLL and the .NET client. For more details about C++/CLI,
please refer to my related CodeProject article entitled “Leveraging your Existing
C++ Code for Use in the .NET Environment”.
Part Four – OpenGraphWinformDialogClient2010
This is WinForms application that displays OpenGL on a user control. It only
interfaces with the C++/CLI .NET class library (OpenGraph_Cli.dll).
Please note that the platform target must be x86.
The user control OpenGLUserControl
is empty and only contains some delegates that will be set by the parent WinForm.
public partial class OpenGLUserControl : UserControl
{
public delegate void OpenGLUserControl_Load_Type(object sender, EventArgs e);
public delegate void OpenGLUserControl_Resize_Type(object sender, EventArgs e);
public delegate void OpenGLUserControl_MouseDown_Type(object sender, MouseEventArgs e);
public delegate void OpenGLUserControl_MouseMove_Type(object sender, MouseEventArgs e);
public delegate void OpenGLUserControl_Paint_Type(object sender, PaintEventArgs e);
public OpenGLUserControl_Load_Type Load_CallBack = null;
public OpenGLUserControl_Resize_Type Resize_CallBack = null;
public OpenGLUserControl_MouseDown_Type MouseDown_CallBack = null;
public OpenGLUserControl_MouseMove_Type MouseMove_CallBack = null;
public OpenGLUserControl_Paint_Type Paint_CallBack = null;
public OpenGLUserControl()
{
InitializeComponent();
}
private void OpenGLUserControl_Load(object sender, EventArgs e)
{
if (Load_CallBack != null)
{
Load_CallBack(sender, e);
}
}
private void OpenGLUserControl_Resize(object sender, EventArgs e)
{
if (Resize_CallBack != null)
{
Resize_CallBack(sender, e);
}
}
private void OpenGLUserControl_MouseDown(object sender, MouseEventArgs e)
{
if (MouseDown_CallBack != null)
{
MouseDown_CallBack(sender, e);
}
}
private void OpenGLUserControl_MouseMove(object sender, MouseEventArgs e)
{
if (MouseMove_CallBack != null)
{
MouseMove_CallBack(sender, e);
}
}
private void OpenGLUserControl_Paint(object sender, PaintEventArgs e)
{
if (Paint_CallBack != null)
{
Paint_CallBack(sender, e);
}
}
}
The WinForm sets the delegates for the user control, sets the appropriate drawing mode, and adds drawing objects.
public partial class Form1 : Form
{
COpenGraph_Cli _openGraph_Cli = new COpenGraph_Cli();
private int m_nCx = 0;
private int m_nCy = 0;
private bool m_bShowAxes = true;
public Form1()
{
InitializeComponent();
openGLUserControl.Load_CallBack = this.Delegate_Load;
openGLUserControl.MouseMove_CallBack = this.Delegate_MouseMove;
openGLUserControl.MouseDown_CallBack = this.Delegate_MouseDown;
openGLUserControl.Paint_CallBack = this.Delegate_Paint;
openGLUserControl.Resize_CallBack = this.Delegate_Resize;
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void Form1_Resize(object sender, EventArgs e)
{
m_nCx = this.openGLUserControl.Width;
m_nCy = this.openGLUserControl.Height;
_openGraph_Cli.SetWindowSize(m_nCx, m_nCy);
_openGraph_Cli.Render();
}
private void Delegate_Load(object sender, EventArgs e)
{
_openGraph_Cli.Initialize(openGLUserControl.Handle);
m_nCx = this.openGLUserControl.Width;
m_nCy = this.openGLUserControl.Height;
_openGraph_Cli.SetWindowSize(m_nCx, m_nCy);
_openGraph_Cli.Render();
_openGraph_Cli.SetMode(COpenGraph_Cli.Mode.modeNONE);
}
private void Delegate_MouseMove(object sender, MouseEventArgs e)
{
_openGraph_Cli.OnSetCursor();
if (e.Button == MouseButtons.Left)
{
CRect_Cli rect = new CRect_Cli();
CPoint_Cli pt = new CPoint_Cli();
pt.X = e.X;
pt.Y = e.Y;
uint nFlags = (int)COpenGraph_Cli.ButtonFlag.flagMK_LBUTTON;
if (_openGraph_Cli != null)
{
_openGraph_Cli.OnMouseMove(nFlags, pt);
}
}
}
private void Delegate_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
CRect_Cli rect = new CRect_Cli();
CPoint_Cli pt = new CPoint_Cli();
pt.X = e.X;
pt.Y = e.Y;
uint nFlags = 0;
if (_openGraph_Cli != null)
{
_openGraph_Cli.OnLButtonDown(ref rect, nFlags, pt);
}
}
}
private void Delegate_Paint(object sender, PaintEventArgs e)
{
if (_openGraph_Cli != null)
{
_openGraph_Cli.Render();
}
}
private void Delegate_Resize(object sender, EventArgs e)
{
if (_openGraph_Cli != null)
{
m_nCx = this.ClientSize.Width;
m_nCy = this.ClientSize.Height - menuStrip1.Height;
_openGraph_Cli.SetWindowSize(m_nCx, m_nCy);
}
}
private void drawNodesToolStripMenuItem_Click(object sender, EventArgs e)
{
int fontId = _openGraph_Cli.AddFont("Times New Roman", 30, 0, 0);
Random random = new Random();
const int nNodes = 400;
const int nBatches = 10;
const int nNodesPerBatch = nNodes / nBatches;
for (int j = 0; j < nBatches; j++)
{
CColor_Cli color = new CColor_Cli((byte)(random.Next(0, 256)),
(byte)(random.Next(0, 256)), (byte)(random.Next(0, 256)));
CColor_Cli colorSelected = new CColor_Cli(255, 0, 0);
List<CPointf_Cli> pPt = new List<CPointf_Cli>();
List<int> pId = new List<int>();
int nSize = (random.Next(0, 256) % 8);
string strTag;
strTag = string.Format("Tag_{0}", j);
for (int i = 0; i < nNodesPerBatch; i++)
{
double x = ((double)random.Next(0, 256) / 256 - 0.5) * 4;
double y = ((double)random.Next(0, 256) / 256 - 0.5) * 4;
double z = ((double)random.Next(0, 256) / 256 - 0.5) * 4;
string strText;
int id = j * nNodesPerBatch + i + 1;
strText = string.Format("{0}", id);
if (i % 2 == 0)
{
_openGraph_Cli.AddText(id, 0, new CPointf_Cli(x, y, z),
strText, new CColor_Cli(125, 123, 0), colorSelected,
CTextAlignment_Cli.ALIGN_LEFT |
CTextAlignment_Cli.ALIGN_BOTTOM, CStatus_Cli.STATUS_UNSELECTED);
}
else
{
_openGraph_Cli.AddText(id, fontId, new CPointf_Cli(x, y, z), strText,
new CColor_Cli(0, 123, 123), colorSelected,
CTextAlignment_Cli.ALIGN_LEFT | CTextAlignment_Cli.ALIGN_BOTTOM,
CStatus_Cli.STATUS_UNSELECTED);
}
pId.Add(id);
pPt.Add(new CPointf_Cli(x, y, z));
}
List<CIdentity_Cli> pIdentity = new List<CIdentity_Cli>();
_openGraph_Cli.AddNodes(ref pIdentity, pId, pPt, nSize, color,
colorSelected, CStatus_Cli.STATUS_UNSELECTED, strTag, null);
}
_openGraph_Cli.AddNode(99999, new CPointf_Cli(), 15, new CColor_Cli(255, 255, 0),
new CColor_Cli(0, 255, 255), 0, null, null);
_openGraph_Cli.AddText(99999, 0, new CPointf_Cli(), "99999",
new CColor_Cli(125, 123, 0), new CColor_Cli(0, 255, 255),
CTextAlignment_Cli.ALIGN_LEFT | CTextAlignment_Cli.ALIGN_BOTTOM,
CStatus_Cli.STATUS_UNSELECTED);
_openGraph_Cli.Render();
}
private void defaultToolStripMenuItem_Click(object sender, EventArgs e)
{
_openGraph_Cli.SetMode(COpenGraph_Cli.Mode.modeNONE);
}
private void trackToolStripMenuItem_Click(object sender, EventArgs e)
{
_openGraph_Cli.SetMode(COpenGraph_Cli.Mode.modeTRACK);
}
private void rotateToolStripMenuItem_Click(object sender, EventArgs e)
{
_openGraph_Cli.SetMode(COpenGraph_Cli.Mode.modeROTATE);
}
private void showAxesToolStripMenuItem_Click(object sender, EventArgs e)
{
m_bShowAxes = !m_bShowAxes;
_openGraph_Cli.ShowAxes(m_bShowAxes);
_openGraph_Cli.Render();
}
}
Windows Presentation Foundation Form
For a WPF form, you can use the same WinForms user control inside the WPF WindowsFormsHost
control. For example:
<Window x:Class="OpenGraphWpfClient.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wf="clr-namespace:OpenGraphWpfClient"
Title="MainWindow" Height="520" Width="934" SizeChanged="Window_SizeChanged">
<Grid>
<WindowsFormsHost Margin="182,49,40,31" Name="windowsFormsHost1">
<wf:OpenGLUserControl Name="openGLUserControl"></wf:OpenGLUserControl>
</WindowsFormsHost>
</Grid>
</Window>
Points of Interest
The code here may be augmented for actual production use. For example, you can add more interactivity to the code by allowing the user to zoom in,
pan, or select individual objects. I leave those features to you to exercise your talents. You can also refer to www.cg-inc.com
for help or more information. Thanks for reading and happy coding!
Junlin Xu is the founder of Computations & Graphics, Inc. (http://www.cg-inc.com). He is the author of Real3D finite element package, SolverBlaze finite element SDK, OpenGraph Library (OpenGL-based visualization and charting SDK for native and .NET environment), and double128 SDK (quad precision floating point math for C++ and .NET).
Junlin has 20+ years software development experiences in various industries. He has skills in Windows desktop and web application development using C++, C++/CLI, C#, Qt, MFC, STL, OpenGL, GLSL, COM/COM+, WinForms, MS SQL, MySQL, ASP.NET and .NET Core, CSS, jQuery and jQuery UI, Autodesk Revit API, Inno Setup. He is also an expert in mathematical, CAD and algorithmic software development.