CassieD CassieD - 3 months ago 21
C++ Question

Passing messages between an MFC Dialog and an OpenGL control on it's own thread

I'm trying to add an OpenGL control into a MFC dialog. The dialog is launched from a window that is also running OpenGL... which makes it complicated. Since only one OpenGL context can go on a thread at a time, I either have to have the entire dialog or just the control on it's own thread. I opted to attempt the latter, as it would make implementation easier for other dialogs in our project to use as well.

Most of what I know about MFC is spotty, as I've been learning as I go, so apologies if I missed something huge. I could really use some MFC help as to the best way to communicate messages, or set attributes to the control from the dialog. With sheer luck, I seem to have something working.

I'm using OnSize as just an example.I created this one function in the control wrapper class to try to push messages through from the dialog to the control.

void AddOpenGLControl::PostMessageToControl(UINT Msg, WPARAM wParam, LPARAM lParam)
{
if (dialogHandle != NULL)
PostMessage(dialogHandle, Msg, wParam, lParam);
}


But that leaves me trying to simulate fake windows messages, that probably already exist, in the dialog to send to the control.

WPARAM wp = nType;
LPARAM lp = MAKELPARAM(ctrlRect.Width(), ctrlRect.Height());

openGlControlOnThread.PostMessageToControl(WM_SIZE, wp, lp);


I know I'm going to want access to trigger more messages like I would a normal control, and this method will which will get cumbersome fast.

Is there a way to simply pass messages along? It's hard to ask for what I don't know, but I'm thinking inheritance, or a message map in the wrapper class, or pretranslate message, or steal messages from a hidden normal control to send or ... well, I'd take anything that would work across threads.

Here is all the code, to fill in the gaps. I paired the code down to what I could, but I did have to use our message loop (sorry!). Here's my standard launch from the window:

//Main thread (with existing openGL window)
class TCADTLSandbox2
{
public:
virtual bool DoCmd(); // Main Entry Point

};

bool TCADTLSandbox2::DoCmd()
{

TCADTLSandbox2Dialog dialog;

dialog.Create(TCADTLSandbox2Dialog::IDD);
dialog.ShowWindow(SW_SHOW);

// We have to use the main window's accessors to get any messages from the window
WPARAM dialogMessage;
while (!GetUITools()->WaitForCommandMessage("", dialogMessage) || (dialogMessage != TIMsgIDCancel))
{
if (dialogMessage == TIMsgIDOK)
{
break;
}
}

dialog.ShowWindow(SW_HIDE);
dialog.DestroyWindow();

return true;
}


And here's the dialog class. The class member openGlControlOnThread and OnSize() are the two things of interest.

//Dialog
class TCADTLSandbox2Dialog : public CDialog
{
DECLARE_DYNAMIC(TCADTLSandbox2Dialog)

public:
TCADTLSandbox2Dialog(CWnd* pParent = NULL);
virtual ~TCADTLSandbox2Dialog();

enum { IDD = IDD_SANDBOX2 };

protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
DECLARE_MESSAGE_MAP()

private:
AddOpenGLControl openGlControlOnThread;

virtual BOOL OnInitDialog();
afx_msg void OnBnClickedOk();
afx_msg void OnBnClickedCancel();
afx_msg void OnSize(UINT nType, int cx, int cy);
};

IMPLEMENT_DYNAMIC(TCADTLSandbox2Dialog, CDialog)

TCADTLSandbox2Dialog::TCADTLSandbox2Dialog(CWnd* pParent /*=NULL*/): CDialog(TCADTLSandbox2Dialog::IDD, pParent){}

TCADTLSandbox2Dialog::~TCADTLSandbox2Dialog(){}

void TCADTLSandbox2Dialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(TCADTLSandbox2Dialog, CDialog)
ON_BN_CLICKED(IDOK, &TCADTLSandbox2Dialog::OnBnClickedOk)
ON_BN_CLICKED(IDCANCEL, &TCADTLSandbox2Dialog::OnBnClickedCancel)
ON_WM_SIZE()
END_MESSAGE_MAP()


BOOL TCADTLSandbox2Dialog::OnInitDialog()
{
CDialog::OnInitDialog();

CRect rect;
GetDlgItem(IDC_CADTL_SANDBOX_OPENGLTEST)->GetWindowRect(rect);
ScreenToClient(rect);
openGlControlOnThread.Create(rect, this);

initialized = true;
return TRUE;
}

void TCADTLSandbox2Dialog::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);

if (initialized)
{

CRect ctrlRect(0, 0, 0, 0);
GetDlgItem(IDC_CADTL_SANDBOX_OPENGLTEST)->GetWindowRect(ctrlRect);
ScreenToClient(ctrlRect);

//int buttonWidth = ctrlRect.Width();
ctrlRect.left = 20;
ctrlRect.right = cx - 20;
ctrlRect.bottom = cy - 50;

WPARAM wp = nType;
LPARAM lp = MAKELPARAM(ctrlRect.Width(), ctrlRect.Height());

openGlControlOnThread.PostMessageToControl(WM_SIZE, wp, lp);
}
}

void TCADTLSandbox2Dialog::OnBnClickedOk()
{
PostQuitMessage(0);
}

void TCADTLSandbox2Dialog::OnBnClickedCancel()
{
PostQuitMessage(0);
CDialog::OnCancel();
}


And here's where it gets fun. This is what I did to wrap the OpenGL control onto a new thread. PostMessageToControl is what I'm hoping to replace with something simpler.

// Class to kickoff the control thread
class AddOpenGLControl
{
public:
AddOpenGLControl();
~AddOpenGLControl();

void Create(CRect rect, CWnd *parent);

// You can send something along the lines of (WM_DESTROY, NULL, NULL)
void PostMessageToControl(UINT Msg, WPARAM wParam, LPARAM lParam);

private:
void StartThread(CRect rect, CWnd *parent);
std::thread controlThread;
};

// *** The thread class ***

AddOpenGLControl::AddOpenGLControl(){}

AddOpenGLControl::~AddOpenGLControl()
{
WaitForSingleObject(controlThread.native_handle(), INFINITE); // Okay?
controlThread.join(); // Also okay?
}

void AddOpenGLControl::PostMessageToControl(UINT Msg, WPARAM wParam, LPARAM lParam)
{
if (dialogHandle != NULL)
PostMessage(dialogHandle, Msg, wParam, lParam);
}

void AddOpenGLControl::Create(CRect rect, CWnd *parent)
{
std::thread initThread(&AddOpenGLControl::StartThread, this, rect, std::ref(parent));
controlThread = std::move(initThread);
}

void AddOpenGLControl::StartThread(CRect rect, CWnd *parent)
{
COpenGLControl *openGLControl = new COpenGLControl;

openGLControl->Create(rect, parent);
openGLControl->m_unpTimer = openGLControl->SetTimer(1, 1000, 0);

HWND threadHandle = (HWND)openGLControl->GetSafeHwnd();

// This is where I start getting lost. Taken from MSDN
MSG msg;
BOOL bRet;
while ((bRet = GetMessage(&msg, threadHandle, 0, 0)) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

}


And here is the control itself:

// OpenGL Dialog Control thread (now it can have it's own openGL context)
class COpenGLControl : public CWnd
{
public:
UINT_PTR m_unpTimer;

COpenGLControl();
virtual ~COpenGLControl();

void Create(CRect rect, CWnd *parent);
void UpdateOpenGLDispay();

afx_msg void OnSize(UINT nType, int cx, int cy);
void ParentDialogResize(UINT nType, int cx, int cy);

private:
CWnd *hWnd;
HDC hDC;
HGLRC openGLRC;
CString className;

void Initialize();

void DrawCRD(double centerX, double centerY, double centerZ, double crdScale);

afx_msg void OnPaint();
afx_msg void OnDraw(CDC *pDC);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnTimer(UINT_PTR nIDEvent);

DECLARE_MESSAGE_MAP()
};

// *** The actual control ***

COpenGLControl::COpenGLControl()
{
initialized = false;
display = new Display2d;
}

COpenGLControl::~COpenGLControl()
{
CleanUp();
}


BEGIN_MESSAGE_MAP(COpenGLControl, CWnd)
ON_WM_PAINT()
ON_WM_CREATE()
ON_WM_TIMER()
ON_WM_SIZE()
ON_WM_DESTROY()
END_MESSAGE_MAP()

int COpenGLControl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;

Initialize();

return 0;
}

void COpenGLControl::Initialize()
{
// Initial Setup:
//
//Set up the pixal descriptor for using openGL on the graphics card
PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR), // Size of this structure
1, // Version of this structure
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
24, // Want 24bit color
0, 0, 0, 0, 0, 0, // Not used to select mode
0, 0, // Not used to select mode
0, 0, 0, 0, 0, // Not used to select mode
32, // Size of depth buffer
0, // Not used to select mode
0, // Not used to select mode
PFD_MAIN_PLANE, // Draw in main plane
0, // Not used to select mode
0, 0, 0 }; // Not used to select mode


// Store the device context
if (!(hDC = GetDC()->m_hDC)) // From the dialog?
{
AfxMessageBox("Can't Create A GL Device Context.");
//May not be able to unregister class?? May not need to?
return;
}

// Choose a pixel format that best matches that described in pfd
int nPixelFormat; // Pixel format index
nPixelFormat = ChoosePixelFormat(hDC, &pfd);

// Set the pixel format for the device context
if (!SetPixelFormat(hDC, nPixelFormat, &pfd))
AfxMessageBox("Setting pixel format failed");

// Create the rendering context and make it current
if (!(openGLRC = wglCreateContext(hDC)))
{
AfxMessageBox("Can't Create A GL Rendering Context.");
//May not be able to unregister class?? May not need to?
return;
}

if (!wglMakeCurrent(hDC, openGLRC))
AfxMessageBox("WGLFailed");


// Set color to use when clearing the background.
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClearDepth(1.0f);

// Turn on backface culling
glFrontFace(GL_CCW);
glCullFace(GL_BACK);

// Turn on depth testing
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);

// Enable alpha blending
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

UpdateOpenGLDispay();

initialized = true;
}

void COpenGLControl::Create(CRect rect, CWnd *parent)
{
className = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW | CS_OWNDC, NULL, (HBRUSH)GetStockObject(BLACK_BRUSH), NULL);

CreateEx(0, className, "OpenGL", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, rect, parent, 0);

hWnd = parent;
}

void COpenGLControl::OnPaint()
{
// Attempt to help with flickering
UpdateOpenGLDispay();
CPaintDC dc(this);
ValidateRect(NULL);
}

void COpenGLControl::OnDraw(CDC *pDC)
{
UpdateOpenGLDispay();
}

void COpenGLControl::UpdateOpenGLDispay()
{
DrawCRD(); // ... just draw something
SwapBuffers(hDC);
}

void COpenGLControl::DrawCRD(double centerX, double centerY, double centerZ, double crdScale)
{
// X CRD
glColor4f(1.0f, 0.0f, 0.0f, 1);//R
glBegin(GL_LINES);
glVertex3f(centerX, centerY, centerZ);
glVertex3f(centerX + 1 * crdScale, centerY, centerZ);
glEnd();

// Y CRD
glColor4f(0.0f, 0.0f, 1.0f, 1);//B
glBegin(GL_LINES);
glVertex3f(centerX, centerY, centerZ);
glVertex3f(centerX, centerY + 1 * crdScale, centerZ);
glEnd();

// Z CRD
glColor4f(0.0f, 1.0f, 0.0f, 1);//G
glBegin(GL_LINES);
glVertex3f(centerX, centerY, centerZ);
glVertex3f(centerX, centerY, centerZ + 1 * crdScale);
glEnd();
}

void COpenGLControl::OnTimer(UINT_PTR nIDEvent)
{
switch (nIDEvent)
{
case 1:
UpdateOpenGLDispay();
break;
}

CWnd::OnTimer(nIDEvent);
}

void COpenGLControl::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);

if (0 >= cx || 0 >= cy || nType == SIZE_MINIMIZED) return;

if (nType == SIZE_RESTORED)
ParentDialogResize(nType, cx, cy);
}

void COpenGLControl::ParentDialogResize(UINT nType, int cx, int cy)
{
if (initialized)
{
if (0 >= cx || 0 >= cy || nType == SIZE_MINIMIZED) return;
this->SetWindowPos(NULL, 20, 20, cx, cy, SWP_SHOWWINDOW); // fixed for this example
}
}

void COpenGLControl::OnDestroy()
{
CWnd::OnDestroy();
PostQuitMessage(0);
}

void COpenGLControl::CleanUp()
{

if (!wglDeleteContext(openGLRC))
AfxMessageBox("Deleting OpenGL Rendering Context failed");

if (!wglMakeCurrent(hDC, NULL))
AfxMessageBox("Removing current DC Failed");

//ReleaseDC(hDC); // The internets say the device context should automatically be deleted since it's CS_OWNDC

}

// Sorry for the heap of code. Or is it stack?


Thanks!

Answer

Seems I had missed a few huge aspects of MFC. If anyone follows a similar path, it would be good to go through the process of subclassing a regular control before attempting to create an opengl control.

There are several ways to do this. If I use OnWndMsg or similarly WindowProc I can get to the message while it's still in the wParam/lParam state, and pass it along.

BOOL TCADTLSandbox2Dialog::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) 
{  
  //This is one way to just pass the message along.  For some cases, we may need to check the mouse position as well. 
  if (message == WM_MOUSEWHEEL) 
  { 
    openGlControlOnThread.PostMessageToControl(message, wParam, lParam); 
    return true; 
  } 
  if (wParam == WM_LBUTTONDOWN) 
  { 
    openGlControlOnThread.PostMessageToControl(message, wParam, lParam); 
    return true; 
  } 
  return CDialog::OnWndMsg(message, wParam, lParam, pResult); 
}

Also, it turns out that PreTranslateMessage was an option I could use, though I didn't see how at the time.

BOOL TCADTLSandbox2Dialog::PreTranslateMessage(MSG* pMsg)
{
  // Send the message to the control (the dialog gets it by default)
  if (pMsg->message == WM_MOUSEWHEEL)
  {
      openGlControlOnThread.PostMessageToControl(pMsg->message, pMsg->wParam, pMsg->lParam);
      return true;
  }

  return MCDialog::PreTranslateMessage(pMsg);
}

Hope this helps anyone else out there!