每次寫 OpenGL 都要重寫像素格式、RC 建立、訊息迴路……為何不封裝成 **ATL COM 控件**?拖拉上表單、處理 OnRender 事件即可專注繪圖邏輯。本文手把手帶你在 VC6/VC7 建立 OpenGL_ATL.ocx,支援事件回調、雙緩衝、自動清理。
OpenGL_ATL → **Dynamic Link Library** → FinishMyControl (Short Name 同) → **Options** 勾選:
#include
#include
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")
class ATL_NO_VTABLE CMyControl :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IPersistStreamInitImpl,
public IOleControlImpl,
public IOleObjectImpl,
public IOleInPlaceActiveObjectImpl,
public IViewObjectExImpl,
public IOleInPlaceObjectWindowlessImpl,
public ISupportErrorInfo,
public IConnectionPointContainerImpl,
public IPersistStorageImpl,
public IPersistPropertyBagImpl,
public IQuickActivateImpl,
public IDataObjectImpl,
public IProvideClassInfo2Impl<&CLSID_MyControl, NULL, &LIBID_OpenGL_ATLLib>,
public CComControl
{
HGLRC m_hRC; // OpenGL Rendering Context
// ...
BOOL CMyControl::SetupPixelFormat(HDC hdc) {
static PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR), 1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA, 24, // 24-bit 色深
0,0,0,0,0,0, 0,0, // color bits ignored
0,0,0,0, 32, // 32-bit Z-buffer
0,0, PFD_MAIN_PLANE, 0, 0,0,0
};
int pf = ChoosePixelFormat(hdc, &pfd);
if (!pf) return FALSE;
return SetPixelFormat(hdc, pf, &pfd);
}
// 訊息對應表
BEGIN_MSG_MAP(CMyControl)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
CHAIN_MSG_MAP(CComControl)
END_MSG_MAP()
LRESULT CMyControl::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
HDC hdc = GetDC();
RECT rc; GetClientRect(&rc);
if (SetupPixelFormat(hdc)) {
m_hRC = wglCreateContext(hdc);
wglMakeCurrent(hdc, m_hRC);
// 初始化 OpenGL 狀態 (glClearColor, glEnable GL_DEPTH_TEST 等)
}
ReleaseDC(hdc);
return 0;
}
LRESULT CMyControl::OnDestroy(UINT, WPARAM, LPARAM, BOOL&) {
wglMakeCurrent(NULL, NULL);
if (m_hRC) { wglDeleteContext(m_hRC); m_hRC = NULL; }
return 0;
}
ClassView → 展開 IMyControlEvents (連接點介面) → 右鍵 Add Method → 名稱 OnRender,參數:
[id(1)] HRESULT OnRender([in] long right, [in] long left, [in] long bottom, [in] long top);
編譯 IDL (Build → Compile OpenGL_ATL.idl) → 回到 CMyControl 實作 → 點擊 **Connection Points** → 勾選 IMyControlEvents → OK (ATL 自動生成 Fire_OnRender)
HRESULT CMyControl::OnDraw(ATL_DRAWINFO& di) {
HDC hdc = di.hdcDraw;
RECT& rc = *(RECT*)di.prcBounds;
wglMakeCurrent(hdc, m_hRC);
glViewport(0, 0, rc.right - rc.left, rc.bottom - rc.top);
glClearColor(1.0f, 0.0f, 0.0f, 1.0f); // 紅色背景測試
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// ★ 關鍵:觸發事件,讓容器/使用者注入繪圖代碼
Fire_OnRender(rc.right, rc.left, rc.bottom, rc.top);
// VC7+ 可直接呼叫 OnRender(...)
glFinish();
SwapBuffers(wglGetCurrentDC());
return S_OK;
}
MyControl Class → 拖到表單
// VB6 範例
Private Sub MyControl1_OnRender(ByVal right As Long, ByVal left As Long, _
ByVal bottom As Long, ByVal top As Long)
' 在此處撰寫 OpenGL 繪圖代碼
glBegin(GL_TRIANGLES)
glColor3f(0,1,0); glVertex3f(-0.5, -0.5, 0)
glColor3f(0,0,1); glVertex3f( 0.5, -0.5, 0)
glColor3f(1,1,0); glVertex3f( 0.0, 0.5, 0)
glEnd()
End Sub
// VC++ 對話框類
void COpenGLTestDlg::OnRenderMyControl(long right, long left, long bottom, long top) {
// 同樣在這裡寫 gl* 指令
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 控件區域全黑/白 | 像素格式不支援 / RC 未建立 | 檢查 ChoosePixelFormat 回傳值、確認 wglCreateContext 成功 |
| 事件不觸發 | 未勾選 Connection Points / IDL 未編譯 | 重新編譯 IDL、確認 Fire_OnRender 存在於 .h |
| 調整大小後畫面錯亂 | 未在 OnDraw 呼叫 glViewport | 每幀根據 di.prcBounds 設定 viewport |
| 記憶體洩漏 | WM_DESTROY 未釋放 RC | 務必 wglDeleteContext 並設 NULL |
| 多執行緒存取衝突 | OpenGL RC 綁定執行緒 | ATL 控件預設 SingleThreadModel,所有呼叫自動封送至 UI 執行緒 |
幾百行 ATL 模板代碼,換來一個 **「拖拉即用、事件驅動、雙緩衝無閃爍」** 的 OpenGL 控件。這就是 COM 元件化的威力:**將繁瑣的平台細節封裝在二進位介面後,讓使用者專注領域邏輯**。下次專案需要 3D 視覺化,直接丟上這個 OCX,連 VB6、C#、網頁都能輕鬆嵌入——**一次封裝,到處複用**。