MFC Control in Tk toplevel

http://www.tfh-berlin.de/~sleuthold/files/tcl/MFCinTk.jpg


I saw this implemented in commercial applications, and asked myself: How did that work?

Somebody could ask, why embed a MFC-Control into a Tk-toplevel:

  • divide GUI from core implementation
  • building huge OpenGl or DirectX applications with platform-independent GUI (CAE,CAD,etc.)
  • make use of the impressive Tcl/Tk automation potential, and hide the core implementation
  • etc.

I will show you one complete implementation on the windows platform. It would be great if somebody would do something similar on Unix/Linux(X11).

I used VC++6.0, Tcl/Tk 8.4 and build this successfully on Win98 and WinXP. Maybe MFC and VC++6.0 is out of fashion, but I think it is still a nice topic, and it can result in a nice designed application. This design makes it possible, to build applications for Linux and Windows, with minimum platform-dependence.

Tk frame in MFC CFrameWnd covers the opposite direction. More MFC and less Tk.

Comments are welcome considering implementation-details and discussions on the subject itself (widget embedding).


We end up in MFCinTk.exe, that sources a MFCinTk.tcl.

Features:

  • toplevel contains button, frame -container true and a menu
  • frame will contain one CFrameWnd - Object
  • interface to resize the CFrameWnd connected to any toplevel-resize-operation
  • interface and override of exit to make sure, that the application not crashes on closing via exit command

MFCinTk.tcl

package provide MfCinTk 1.0

namespace eval ::mfcintk {}

# used by C++
proc ::mfcintk::CreateTopLevel { args } {

        set nsPath [namespace current]

        # Toplevel
        variable toplevel_window .

        wm geometry $toplevel_window 400x300+50+50

        # Menu for the look
        set menu [menu ${toplevel_window}menubar]
        $toplevel_window configure -menu $menu
        set fileMenu [menu $menu.fileMenu -tearoff 0]
        $menu add cascade -label "File" -underline 0 -menu $menu.fileMenu 
        $fileMenu add cascade -label "Exit" -underline 0 -command {exit}

        variable mfcContainer [frame ${toplevel_window}mfcContainer -container true]
        set testFrame [frame ${toplevel_window}testFrame]    
        set tkconButton [button $testFrame.tkconButton  \
                -text "tkcon" \
                -width 20 -height 3 -command {source C:/home/MFCinTk/tcl/tkcon.tcl}]

        pack $mfcContainer      -expand true -fill both -padx 5 -pady 10 
        pack $testFrame         -padx 5 -pady 5 -fill x
        pack $tkconButton       -padx 5 -pady 5 -side left

        variable toplevel_id [winfo id $toplevel_window];       # used by C++
        variable mfc_id [winfo id $mfcContainer];               # used by C++

        # Bindings
        eval "bind $mfcContainer <Configure> {+ ${nsPath}::OnGraphicsSize}"
}

proc ::mfcintk::OnGraphicsSize { args } {

        variable mfcContainer
        return [ResizeMfC [winfo width $mfcContainer] [winfo height $mfcContainer]]
}
  • mfcContainer -container true, to enable embedding
  • variable mfcContainer, to access via C++, so the name is fixed
  • variable toplevel_id and variable mfc_id, to access via C++
  • ::mfcintk::CreateTopLevel called from C++ during application run-up (after sourcing the tcl-file)
  • bind to resize the toplevel, ResizeMfC is an interface proc provided by C++ (without that, the MFC Control would not resize)
  • fixed path-name to source tkcon.tcl with the button

Derive CWinApp to host the application/thread/event-loop

MfCinTk.h

#ifndef MFCINTK_H
#define MFCINTK_H

#include "resource.h"

class CMFCinTkApp : public CWinApp
{
public:
        CMFCinTkApp();

        //{{AFX_VIRTUAL(CMFCinTkApp)
        virtual int      ExitInstance( );
        virtual BOOL OnIdle(LONG lCount);
        virtual BOOL InitInstance();
        //}}AFX_VIRTUAL

private:
        bool    Tk_Initialize();
        void    ParseCmdLine(int* argc, char*** argv);
        void    ParseCmdLine(int* argc, char*** argv);
        Tcl_Interp      *interp;
        char            **argv;
        int             argc;

public:
        //{{AFX_MSG(CMFCinTkApp)
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};

//forward declaration
int ResizeObjCmd(ClientData data, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]);
int ExitObjCmd(ClientData data, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]);

#endif // MFCINTK_H 
  • ResizeObjCmd and ExitObjCmd are Tcl-procs

MfCinTk.cpp

#include "stdafx.h"
#include "MFCinTk.h"
#include "MainFrm.h"


BEGIN_MESSAGE_MAP(CMFCinTkApp, CWinApp)
        //{{AFX_MSG_MAP(CMFCinTkApp)
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()


CMFCinTkApp::CMFCinTkApp()
{
}

CMFCinTkApp theApp;

bool CMFCinTkApp::Tk_Initialize()
{
        CinTkApp::Tk_Initialize()
        Tcl_FindExecutable(argv[0]);
        interp = Tcl_CreateInterp(); 

        /*
         * argc, argv0 and argv
         */
        char *args;
        Tcl_DString argString;
        char buf[BUFFER_SPACE];

        args = Tcl_Merge(argc, (CONST char **)argv);
        Tcl_ExternalToUtfDString(NULL, args, -1, &argString);
        Tcl_SetVar(interp, "argv", Tcl_DStringValue(&argString), TCL_GLOBAL_ONLY);
        Tcl_DStringFree(&argString);
        Tcl_Free(args);
        printf(buf, PRINTF_MAX_COUNT, "%d", argc);
        Tcl_ExternalToUtfDString(NULL, argv[0], -1, &argString);
        Tcl_SetVar(interp, "argc", buf, TCL_GLOBAL_ONLY);
        Tcl_SetVar(interp, "argv0", Tcl_DStringValue(&argString), TCL_GLOBAL_ONLY);
        
        /*
         * Init Tcl/Tk
         */
        if (Tcl_Init(interp) == TCL_ERROR) {
                return TCL_ERROR;
        }
        if (Tk_Init(interp) == TCL_ERROR) {
                return TCL_ERROR;
        }

        /*
         * load framework
         */
        if (Tcl_Eval(interp, "source [file join C:/ home MFCinTk tcl MfCinTk.tcl]") != TCL_OK) {
                return TCL_ERROR;
        }
        if (Tcl_Eval(interp, "::mfcintk::CreateTopLevel") != TCL_OK) {
                return TCL_ERROR;
        }
        }
        Tcl_PkgProvide(interp, "mfcintk", "1.0");
        return TCL_OK;
}

BOOL CMFCinTkApp::InitInstance()
{

        SetRegistryKey(_T("Local AppWizard-Generated Applications"));

        /*
         * Process the command line options 
         */
        ParseCmdLine(&argc,&argv);

        /*
         * Init Tcl 
         */
        if (Tk_Initialize() != TCL_OK) {
                MessageBox(NULL, "Tcl/Tk Init error", NULL, MB_ICONERROR);
                return FALSE;
        }
        }
        /*
         * Get the Tk Container Path and build the custom 
         * Mfc TkFrame into that Container
         *              Wnd                      - is the CWnd Object from the container
         *              id_container - returned by [winfo id graphicsContainer] 
         */
        char* Path = Tcl_GetVar(interp, "::mfcintk::mfcContainer", TCL_LEAVE_ERR_MSG);
        
        Tcl_Obj* id_container_obj = Tcl_GetVar2Ex(interp, "::mfcintk::mfc_id", NULL, TCL_LEAVE_ERR_MSG);
        int id_container;
        Tcl_GetIntFromObj(interp,id_container_obj,&id_container);
        
        Tcl_Obj* id_toplevel_obj = Tcl_GetVar2Ex(interp, "::mfcintk::toplevel_id", NULL, 
                                                  TCL_LEAVE_ERR_MSG);
        int id_toplevel;
        Tcl_GetIntFromObj(interp,id_toplevel_obj,&id_toplevel);

        Tk_Window const tkmainwin = Tk_MainWindow(interp);
        if (tkmainwin==NULL)
                        return FALSE;

        Tk_Window const tkwin = Path==NULL
                        ? tkmainwin
                        : Tk_NameToWindow(interp,Path,tkmainwin);
        if (tkwin==NULL)
                        return FALSE;
        Tk_MakeWindowExist(tkwin);
        Window const window = Tk_WindowId(tkwin);
        if (window==NULL)
                        return FALSE;

        HWND const hwnd = Tk_GetHWND(window);
        if (hwnd==NULL)
                        return FALSE;

        CWnd *const Wnd = CWnd::FromHandle(hwnd);
        if (Wnd==NULL)
                   return FALSE;
                   return FALSE;
        /*
         * Build the TkFrame MfC Object
         */
        m_pMainWnd = new CMainFrame(Wnd, id_container);
        m_pMainWnd->ShowWindow(m_nCmdShow);
        m_pMainWnd->UpdateWindow();
        Tcl_CreateObjCommand(interp, "::mfcintk::ResizeMfC", ResizeObjCmd, (ClientData)m_pMainWnd, (Tcl_CmdDeleteProc*)NULL);
        Tcl_CreateObjCommand(interp, "::exit", ExitObjCmd, (ClientData)m_pMainWnd, (Tcl_CmdDeleteProc*)NULL);

        return TRUE;
}

int CMFCinTkApp::ExitInstance( )
{
        if (interp != NULL) {
                Tcl_DeleteInterp(interp);
                Tcl_Finalize();
        }
        CWinApp::ExitInstance();

        return 0;
}

BOOL CMFCinTkApp::OnIdle(LONG lCount)
{

        while(Tcl_DoOneEvent(TCL_ALL_EVENTS | TCL_DONT_WAIT))
        {
        }
        return 0;
}

void CMFCinTkApp::ParseCmdLine(int* argcPtr, char*** argvPtr)
{
/*
 * tclAppInit.c --
 *
 *      Provides a default version of the main program and Tcl_AppInit
 *      procedure for Tcl applications (without Tk).  Note that this
 *      program must be built in Win32 console mode to work properly.
 *
 * Copyright (c) 1996-1997 by Sun Microsystems, Inc.
 * Copyright (c) 1998-1999 by Scriptics Corporation.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: 15857,v 1.23 2006-11-09 07:00:12 jcw Exp $

static void
setargv(argcPtr, argvPtr)
        int *argcPtr;           Filled with number of argument strings.
        char ***argvPtr;        Filled with argument strings (malloc'd). )

Code from tcl-distribution
*/

        char *cmdLine, *p, *arg, *argSpace;
        char **argv;
        int argc, size, inquote, copy, slashes;

        /*
         * Set argv[0] to *.exe
         */
        char buffer[MAX_PATH+1];
        GetModuleFileName(m_hInstance, buffer, sizeof(buffer));

        for (p = buffer; *p != '\0'; p++) {
                if (*p == '\\') {
                        *p = '/';
                }
        }
        }
        for (p = buffer; ; p++) {
                if (*p == '\0') {
                        *p = ' ';

                        /*
                         * Append the command line options
                         */
                        for (int i = 0; ; i++) {
                                if ( m_lpCmdLine[i] == '\0')
                                        break;
                                p++;
                                *p = m_lpCmdLine[i];
                        }
                        p++;
                        *p = '\0';
                        break;
                        break;
                }
                }
        }

        cmdLine = buffer;
        cmdLine = buffer;
        /*
         * Precompute an overly pessimistic guess at the number of arguments
         * in the command line by counting non-space spans.
         */

        size = 2;
        for (p = cmdLine; *p != '\0'; p++) {
        if ((*p == ' ') || (*p == '\t')) {      /* INTL: ISO space. */
                size++;
                while ((*p == ' ') || (*p == '\t')) { /* INTL: ISO space. */
                p++;
                }
                if (*p == '\0') {
                break;
                }
        }
        }
        argSpace = (char *) Tcl_Alloc(
                (unsigned) (size * sizeof(char *) + strlen(cmdLine) + 1));
        argv = (char **) argSpace;
        argSpace += size * sizeof(char *);
        size--;

        p = cmdLine;
        for (argc = 0; argc < size; argc++) {
        argv[argc] = arg = argSpace;
        while ((*p == ' ') || (*p == '\t')) {   /* INTL: ISO space. */
                p++;
        }
        if (*p == '\0') {
                break;
        }

        inquote = 0;
        slashes = 0;
        while (1) {
                copy = 1;
                while (*p == '\\') {
                slashes++;
                p++;
                }
                if (*p == '"') {
                if ((slashes & 1) == 0) {
                        copy = 0;
                        if ((inquote) && (p[1] == '"')) {
                        p++;
                        copy = 1;
                        } else {
                        inquote = !inquote;
                        }
                                }
                                slashes >>= 1;
                        }

                        while (slashes) {
                *arg = '\\';
                arg++;
                slashes--;
                }

                if ((*p == '\0')
                        || (!inquote && ((*p == ' ') || (*p == '\t')))) { /* INTL: ISO space. */
                break;
                }
                if (copy != 0) {
                *arg = *p;
                arg++;
                }
                p++;
                }
        *arg = '\0';
        argSpace = arg + 1;
        }
        argv[argc] = NULL;


        *argcPtr = argc;
        *argvPtr = argv;

} 

int ExitObjCmd(ClientData data, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])
{
        CMainFrame* frame = reinterpret_cast<CMainFrame*>(data);
        CMainFrame* frame = reinterpret_cast<CMainFrame*>(data);
        if (objc == 1) {
                 == 1) {
                frame->DestroyWindow();
                Tcl_Obj *resultPtr;
                resultPtr = Tcl_GetObjResult(interp);
                Tcl_SetIntObj(resultPtr, 1);
                return TCL_OK;
        } else {
                Tcl_WrongNumArgs(interp, 1, objv, "");
                return TCL_ERROR;
        }
}

int ResizeObjCmd(ClientData data, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])
{
        zeObjCmd(ClientData data, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])
        CMainFrame* frame = reinterpret_cast<CMainFrame*>(data);

        if (objc == 1) {
                CRect rect;
                frame->GetWindowRect(&rect);

                Tcl_Obj *listPtr;

                listPtr = Tcl_NewListObj(0, NULL);
                
                if (Tcl_ListObjAppendElement(interp, listPtr, Tcl_NewLongObj(rect.right)) != TCL_OK)
                        return TCL_ERROR;
                if (Tcl_ListObjAppendElement(interp, listPtr, Tcl_NewLongObj(rect.bottom)) != TCL_OK)
                        return TCL_ERROR;

                Tcl_SetObjResult(interp, listPtr);
                return TCL_OK;
        }

        int width,height;
        if (objc != 3) {
                Tcl_WrongNumArgs(interp, 1, objv, "width height");
                return TCL_ERROR;
        }
        }
        if (Tcl_GetIntFromObj(interp, objv[1], &width) != 
                        TCL_OK) {
                return TCL_ERROR;
        }
        if (Tcl_GetIntFromObj(interp, objv[2], &height) != 
                        TCL_OK) {
                return TCL_ERROR;
        }
        }
        frame->Resize(width,height);

        Tcl_Obj *resultPtr;
        resultPtr = Tcl_GetObjResult(interp);
        Tcl_SetIntObj(resultPtr, 1);
        return TCL_OK;
}
  • on load framework you see first, that I source the tcl-file with fixed path-name
  • then ::mfcintk::CreateTopLevel is a proc defined in the tcl-file, and sourced via C++
  • CMFCinTkApp::ParseCmdLine is a long member-function and off topic, but maybe useful for somebody
  • then you find the two Tcl-Obj-Cmd's

Derive CFrameWnd, the MFC Control

MainFrame.h

#ifndef MAINFRM_H
#define MAINFRM_H

class CMainFrame : public CFrameWnd
{
public:
        CMainFrame(CWnd *const Wnd, const int id);
protected: 
        DECLARE_DYNAMIC(CMainFrame)


public:
        //{{AFX_VIRTUAL(CMainFrame)
        //}}AFX_VIRTUAL

public:
        void    Resize(int width, int height);
        virtual ~CMainFrame();

protected:
        //{{AFX_MSG(CMainFrame)
        afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
        afx_msg void OnSize(UINT nType, int cx, int cy);
        afx_msg void OnPaint();
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};

#endif // MAINFRM_H
  • OnSize and OnPaint are neccessary
  • OnSetCursor is practical, to test if the control is well embedded

MainFrame.cpp

#include "stdafx.h"
#include "MFCinTk.h"
#include "MainFrm.h"

IMPLEMENT_DYNAMIC(CMainFrame, CFrameWnd)

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
        //{{AFX_MSG_MAP(CMainFrame)
        ON_WM_SETCURSOR()
        ON_WM_SIZE()
        ON_WM_PAINT()
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()


CMainFrame::CMainFrame(CWnd *const Wnd, const int id)
{

        CreateEx(WS_EX_CLIENTEDGE,
                        AfxRegisterWndClass(0, ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_WINDOWFRAME+1)),
                        _T("MfC in Tk!"), WS_CHILD,
                        CRect(0,0,400,400), Wnd, id);

}

CMainFrame::~CMainFrame()
{
}

void CMainFrame::Resize(int width, int height)
{
        MoveWindow( 0, 0, width, height);
        OnSize(NULL,width,height);
}

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

void CMainFrame::OnPaint() 
{
        CPaintDC dc(this);
        CRect rect;
        GetClientRect(&rect);

        dc.TextOut(rect.right/2-50,rect.bottom/2,"MFC Control");

        rect.left = 10;
        rect.top = 10;
        rect.bottom = rect.bottom-10;
        rect.right = rect.right-10;

        CBrush brush;
        brush.CreateSolidBrush(RGB(0,0,255));
        dc.FrameRect(&rect,&brush);
}

BOOL CMainFrame::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 
{
        SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));

        return CWnd::OnSetCursor(pWnd, nHitTest, message);
}
  • straight forward implemantation, nearly pure C++ - MFC

stdafx.h

#ifndef STDAFX_H
#define STDAFX_H

#define VC_EXTRALEAN            arly pure C++ - MFC
#include <afxwin.h>

#define NO_CONST
#include <tk.h>
#include <tkPlatDecls.h>

#define PRINTF_MAX_COUNT        1024
#define BUFFER_SPACE            1024

#endif // STDAFX_H

The implementation is straight forward, but it took some time for me to make it work. And I'm not so familar with MFC. I did not cut any code out, so you can see everything, e.g., the command-line proccessing.

As a side note, I have no Tcl installation on my machine. So consider to define tcl_library, and look for some fixed path-names in the code.

  1. During runup the application init's Tcl and Tk
  2. The toplevel pops up after Tk_Init
  3. Than we source the tcl-file with the UI
  4. Now we build a CWnd-child-object into the frame with container -true
  5. Finished

You can find a similar thing here: Using MFC Controls as Tk widgets