WIN32 dialog helpers


Resize and ActiveX support for WIN32 dialogs

This article provides a few samples to enable features in WIN32 dialogs such as support for dialog resizing and ActiveX controls. VC++6 / VC++7.x projects are provided.

 

1. The story

As employee in a software company, I have been given the task to add features to an old-fashioned WIN32-based software program. Since last year I have been using almost exclusively latest technologies including .NET, and ironically only to fall into projects dealing with 10 years old technologies. Oh my goddish!

So I have been facing WIN32 dialogs, global callback-based procs that do not natively provide a way to be resized on will, and I find very poor the fact that WIN32 dialogs cannot host ActiveX controls either. All of this seems to me like using prehistoric tool for todays' business needs. And don't get me started on ergonomic UI. These amazing limitations have allowed poor boss technical decisions like using VB instead of C++, opening path for blood.

So it all started by implementing the features as described in the specs, and finally came to the point that the two mentioned constraints on the UI were so lousy that the whole project was looking like a student project. I knew that MFC CDialogs had native ActiveX support so I began searching for ways to fill the gap.

In fact, with MFC, ActiveX support is almost scattered in the entire source code tree. Since it is more than annoying to have to bring all that MFC mess within the app, along with being forced to either statically link with the libraries (huge dlls), or have to distribute it (MFC42 is probably the biggest mess in setup issues one can think of. Thanks MS for upgrading the (likely to be) system locked MFC dlls without changing the name of the dlls.), I have decided to find other ways. One of them was to extract the relevant code from the MFC. I gave up after a few hours, it required such an amazing amount of work that there was no point in doing so, except for sadistic reasons.

The original MSPRESS ActiveX inside out[^] book also gave me no idea about how I could accomplish this simple task. Coincidentally, I stumped on one of Michael Dunn's articles[^] (ATL GUI classes), and found that, after a few minutes browsing the MSDEV ATL source code, that it was providing the blocks I needed to come up with the features I wanted for my WIN32 dialogs. Fortunately, unlike MFC, ATL is statically linked (by default) and is small in size, really small, providing against all odds an incredible framework to start with. Don't be afraid with the ATL acronym, I won't annoy you with COM wrappers. ATL GUI classes is an almost separate code which has enough implementation and simplicity to develop well standing light apps, in no matter of time.

The remainder of this article shows how to bring resizing and ActiveX support to WIN32 dialogs. We are talking general features, and this doesn't preclude the fact that, most of the time, when people want to add ActiveX controls to dialogs, it's often because they want to add the Windows Media player to their app, or the Web browser. Those are things we are going to address too.

 

2. Adding resizing to WIN32 dialogs

Here is the agenda :
  • definition of the feature
  • details over the helper grip object
  • standard ATL dialog object
  • integration of the grip into the ATL dialog
  • initialisation steps
  • we are done!

Adding resizing to WIN32 dialogs is to allow dialogs to be resized using the right-bottom corner grip, along with moving/resizing controls living inside the dialog. Even though this feature is now default with common controls on W2K and higher (for instance the Open file dialog), if you want to provide it, there is more than starting the app on a W2K box, or providing a manifest file. Just to make it clear, the dialog resize feature has to be coded somehow.

People used to deal with global WIN32 dialog callbacks will be shocked with the opportunity they have to bring code which is as small than standard WIN32 dialog wnd procs, along with full object orientation. That makes a great difference in practice. For instance, the resize feature can be added to an ATL dialog just by adding a specialized member to it, the grip object. There is no need to deal with global state variables.

To have a code skeleton to start with, just create a new WIN32 application using MSDEV. Then, add references to the following headers :

// ATL dialogs
#include <atlbase.h>
extern CComModule _Module;
#include <atlwin.h>

// ATL ActiveX support
#include <atlcom.h>
#include <atlhost.h>
Create a new file and paste this code into it :

#include "resource.h"
#include "AboutDialog.h"

CComModule _Module;


int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{

  INITCOMMONCONTROLSEX InitCtrls;
  InitCtrls.dwICC = ICC_LISTVIEW_CLASSES;
  InitCtrls.dwSize = sizeof(INITCOMMONCONTROLSEX);
  BOOL bRet = InitCommonControlsEx(&InitCtrls);


  _Module.Init(NULL, hInstance, &LIBID_ATLLib);


  // show the dialog
  // CSampleDialog dlg; // resizable dialog
  // CSampleAxDialog dlg; // sample dialog
  // CWindowsMediaAxDialog dlg; // windows media player
  // CWebBrowserAxDialog dlg; // web browser
  CAboutDialog dlg;
  dlg.DoModal();


  _Module.Term();

  return 0;
}
And then paste the actual about dialog implementation :
// About dialog : simple implementation
//

class CAboutDialog : public CDialogImpl<CAboutDialog>
{
public:
  enum { IDD = IDD_ABOUT };

  BEGIN_MSG_MAP(CAboutDialog)
    MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
    MESSAGE_HANDLER(WM_CLOSE, OnClose)
    COMMAND_ID_HANDLER(IDOK, OnOK)
    COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
  END_MSG_MAP()

  LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
  {
    CenterWindow();
    return TRUE;    // let the system set the focus
  }

  LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
  {
    EndDialog(IDCANCEL);
    return 0;
  }

  LRESULT OnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
  {
    EndDialog(IDOK);
    return 0;
  }

  LRESULT OnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
  {
    EndDialog(IDCANCEL);
    return 0;
  }

};

As you can see, the code above is going to load the IDD_ABOUT dialog resource when it starts. Just make sure you have such a dialog ready. The global DialogProc callback is hidden, which is fine.

If you need to process some dialog messages, just add an entry to the message map macro, and provide a handler to it. In case you are interested in some notification messages instead, use the NOTIFY_HANDLER(control_id, notification, func) macro. Look atlwin.h for further info.

Now that we have the dialog framework to play with, let's clone this code, call the dialog CSampleDialog and simply add the grip object as a member of the class. Just like this :

class CSampleDialog : public CDialogImpl<CSampleDialog>
{
protected:
  CResizableGrip m_grip;
  ...
};

The resizable grip is the resulting code from Paolo Messina's excellent article about MFC's CResizableDialog[^]. I have extracted the only relevant portion from this code, added my own features, and then made it MFC-free so it ended as a reusable object without any run-time dependency. The resizable grip is a stripped diagonal-looking scrollbar standing in the right-bottom corner of the dialog. It mimics a standard resize grip. That said, what we do in the dialog is create an instance of a grip, and then pass references to all dialog controls to it, along with custom resizing rules. Resizing rules describe what is to be done with a given control when the dialog is resized. Namely, should it be moved, resized, both, or even remain as is (default rule) ? The grip provides a simple API for this. Here is how we use it in our dialog :

LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
 
  m_grip.InitGrip( m_hWnd );
  m_grip.ShowSizeGrip();

  // allow treeview x-y resize when the dialog is resized
  HWND hTreeView = GetDlgItem(IDC_TREE1);
  {
    CResizableControl *pListDynamics = m_grip.AddDynamicControls();
    if (pListDynamics)
    {
      pListDynamics->Init(hTreeView);
      pListDynamics->AllowResizeOnResize();
    }
  }

  // allow OK button x-only move (no resize) when the dialog is resized 
  HWND hOk = GetDlgItem(IDOK);
  {
    CResizableControl *pListDynamics = m_grip.AddDynamicControls();
    if (pListDynamics)
    {
      pListDynamics->Init(hOk);
      pListDynamics->AllowMoveXOnResize();
    }
  }

  ...

  return TRUE; // let the system set the focus
}

Since those initialisation steps are clear code patterns, I am going to introduce helping macros for this purpose :

#define RX 1
#define RY 2
#define RXY RX | RY
#define MX 4
#define MY 8
#define MXY MX | MY

#define BEGIN_SIZINGRULES(grip, hParent) \
  grip.InitGrip( hParent ); \
  grip.ShowSizeGrip();

#define ADDRULE(grip, item, rule) \
  { \
    HWND hObject##item = GetDlgItem( item ); \
    if ( hObject##item ) \
    { \
      CResizableControl *pListDynamics = grip.AddDynamicControls(); \
      if (pListDynamics) \
      { \
        pListDynamics->Init(hObject##item); \
        if ((rule)&RX) pListDynamics->AllowResizeXOnResize(); \
        if ((rule)&RY) pListDynamics->AllowResizeYOnResize(); \
        if ((rule)&MX) pListDynamics->AllowMoveXOnResize(); \
        if ((rule)&MY) pListDynamics->AllowMoveYOnResize(); \
      } \
    } \
  }

#define END_SIZINGRULES

#define DORESIZE(grip) \
  if (grip.GetSafeHwnd()) \
  { \
    grip.UpdateGripPos(); \
    grip.MoveAndResize(); \
  }

#define MINMAX(x,y) \
  LPRECT pRect = (LPRECT) lParam; \
  \
  int nWidth = pRect->right - pRect->left; \
  if (nWidthright = pRect->left + x; \
  \
  int nHeight = pRect->bottom - pRect->top; \
  if (nHeightbottom = pRect->top + y;

Thanks to the macros, the code above simplifies as :

LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{

  BEGIN_SIZINGRULES(m_grip, m_hWnd)
    ADDRULE(m_grip, IDC_TREE1, RXY)
    ADDRULE(m_grip, IDOK, MX)
  END_SIZINGRULES

  ...

  return TRUE; // let the system set the focus
}

Then, we override the WM_SIZE and WM_SIZING message handlers to provide actual support for the feature. Namely, WM_SIZE is sent by Windows whenever the dialog is being resized. WM_SIZING is also sent by Windows and provides a unique opportunity to resize the passed bounding rect on will, allowing us to apply predefined min/max rules. A sample code is as follows :


BEGIN_MSG_MAP(CSampleDialog)
  ...
  MESSAGE_HANDLER(WM_SIZE, OnSize)
  MESSAGE_HANDLER(WM_SIZING, OnSizing)
  ...
END_MSG_MAP()

// called by framework while resizing
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
  int cx, cy;
  cx = LOWORD(lParam);
  cy = HIWORD(lParam);

  if (m_grip.GetSafeHwnd())
  {
    m_grip.UpdateGripPos();
    m_grip.MoveAndResize();
  }

  // you can use the following macro instead of the code above :
  // DORESIZE(m_grip)

  return 0;
}

// called by framework while resizing, to allow min/max bound adjustement
LRESULT OnSizing(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
  LPRECT pRect = (LPRECT) lParam;

  // min width is 150 pixels
  int nWidth = pRect->right - pRect->left;
  if ( nWidth < 150 ) pRect->right = pRect->left + 150;

  // min height is 150 pixels
  int nHeight = pRect->bottom - pRect->top;
  if ( nHeight < 150 ) pRect->bottom = pRect->top + 150;

  // you can use the following macro instead of the code above :
  // MINMAX(150,150)

  return 0;
}

The min/max bounding rect is of course highly correlated to the layout and to what controls the dialog is made of. Which means that the OnSizing() actual implementation is likely to be different for each dialog, unlike OnSize()'s.

Before we compile the code, we still need to do a few things :

  • add the border resizing style to the dialog resource (Dialog properties, Styles tab, Border dropdown)
  • add the clip children style
  • in fact, those styles are automatically forced at run-time by the grip object, so if I get into some of those details, it's because I want you to be able to build the process from scratch. Forcing the styles at run-time goes with code like this :
        // force dialog styles
        ::SetWindowLong(hParent, GWL_STYLE, 
          ::GetWindowLong(hParent, GWL_STYLE) | WS_THICKFRAME | WS_CLIPCHILDREN);
    	
  • add a maximize box, to support double-clicks in the dialog title (the grip is removed when we are in zoomed mode)
  • remember to initialize the common controls by code in case the dialog declares one or more common controls (and we need to link with the comctl32.lib library as well).

When compiled and run, here is what we get :


A resizable dialog from a generic framework

 

3. Adding ActiveX support to WIN32 dialogs

In case you didn't know, ActiveX controls are not supported in WIN32 dialogs. If you try to drop say, the Web browser, onto a WIN32 dialog, then the class wizard aborts the process. In fact, ActiveX support requires a OLE control container to be internally implemented, something WIN32 dialogs are lacking.

Fortunately the MS guys went such that they provide an CAxDialogImpl ATL class for us. This class is also a root WTL class, but the good news is the Windows Template Library is not required. The only thing we have to do from the code posted above is to replace CDialogImpl by CAxDialogImpl.

We still need to solve the ActiveX insertion issue, since the class wizard does not allow us to do so. There are several to solve it :

  • While editing the dialog, right-click on it, select "Insert ActiveX control", and choose one.
  • Create a fake MFC dialog-based application, add one or more ActiveX controls to a dialog, then import the resulting .rc file into your app. You are done.
  • Manually grab the CONTROL tags from the .rc file

This is enough to play with the ActiveX control, but what now if we, like in most use cases, we need to communicate with it other than with the UI-based property pages, and actually either :

  • call one or more methods, or get one or more property values
  • subscribe events

The dispatch driver wrapper provided by the MFC class wizard is of no help since, like already said, it requires a great portion of MFC source code within your app. What we are going to do first is import the ActiveX type-library so that we have the public API to play with. The imported type-library is reflected by a pair of files and are dynamically generated at compile-time, in the Debug folder, for instance msdxm.tlh and msdxm.tli. We are going to address two use cases :

  1. playing a Windows Media player video file
  2. navigating a URL using the web browser

Playing with the Windows Media player

Let's import the Windows Media player type-library (smart pointer wrappers) with this code :

// put the following line in your precompiled headers 
#import "c:\winnt\system32\msdxm.ocx"

Doing so, we are provided with the entire object model in the Debug\msdxm.tlh + msdxm.tli files. It's such that if you take a few minutes to browse it (I know this is rude!), you'll figure out there are coclasses and interfaces. Coclasses are entry points when we need to create new instances, while interfaces are binders we can rely on. On the other hand, the ATL CAxDialogImpl inherits lower level classes, one of which exposes the GetDlgControl(int nID, REFIID iid, /*out*/void** ppDispatch) accessor, giving us a handy way to bind the interfaces with the running player instance (inserted in the dialog's .rc file as described above).

The code which does the binding is then straight forward. To set a given video filename to play, just do the following, for instance in the dialog's OnInitDialog() implementation :

// Windows media player specific code
MediaPlayer::IMediaPlayerPtr pMediaPlayer = NULL;
HRESULT hr = GetDlgControl(IDC_MEDIAPLAYER1, 
                           __uuidof(MediaPlayer::IMediaPlayer), 
                           (void**)&pMediaPlayer);

pMediaPlayer->FileName =  _bstr_t("e:\\videos\\01.avi");
pMediaPlayer->Play();
That's all. The working code is provided in WindowsMediaAxDialog.h.

 

Navigating a URL using the web browser

Using the web browser consistently is like for any other ActiveX control. But, because it's interesting to show how to get notified of navigation events, we are going to see how this works.

Just like the Windows Media player, insert this line of code to import the Web browser type-libraries :

// put the following line in your precompiled headers 
#pragma warning( disable : 4192 )
#import "c:\winnt\system32\shdocvw.dll" // web browser control
#import "c:\winnt\system32\mshtml.tlb" // web browser dom

To navigate an url, just add this code (for instance in OnInitDialog()'s dialog method) :

// Web browser specific code
SHDocVw::IWebBrowserAppPtr pWebBrowser = NULL;
HRESULT hr = GetDlgControl(IDC_EXPLORER1, 
                           __uuidof(SHDocVw::IWebBrowserAppPtr), 
                           (void**)&pWebBrowser);

pWebBrowser->Navigate( _bstr_t("http://www.codeproject.com") );

So far so good. Now we'd like to get notified of navigation events such like when the web page is transferred from the web and rendered. We need to subscribe to the web browser event source (in fact there are two, DWebBrowserEvents and DWebBrowserEvents2 for versioning reasons). In order to do this, we have to enumerate IConnectionPoint interfaces (which are the technical names of event sources), and call advise() on it. That said, we could use ATL EventSink macros to make our lives easier. But just for the beauty of doing it "by hand", let's do it with real code :

// subscribe the web browse event source
LPCONNECTIONPOINTCONTAINER pCPC = NULL;
LPCONNECTIONPOINT pCP = NULL;
pWebBrowser->QueryInterface(IID_IConnectionPointContainer, (LPVOID*)&pCPC);
pCPC->FindConnectionPoint(__uuidof(SHDocVw::DWebBrowserEventsPtr), &pCP);
DWORD dwCookie;
pCP->Advise((LPUNKNOWN)&m_events, &dwCookie);

m_events is a class member which implements the DWebBrowserEvents interface, a IDispatch interface :

class CWebBrowserAxDialog : public CAxDialogImpl
{
protected:
  DWebBrowserEventsImpl m_events;
  ...
}

class DWebBrowserEventsImpl : public DWebBrowserEvents
{

	// IUnknown methods
    STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppv);
    STDMETHOD_(ULONG, AddRef)();
    STDMETHOD_(ULONG, Release)();

	// IDispatch methods
	STDMETHOD(GetTypeInfoCount)(UINT* pctinfo);
	STDMETHOD(GetTypeInfo)(UINT iTInfo,
            LCID lcid,
            ITypeInfo** ppTInfo);
	STDMETHOD(GetIDsOfNames)(REFIID riid,
            LPOLESTR* rgszNames,
            UINT cNames,
            LCID lcid,
            DISPID* rgDispId);
	STDMETHOD(Invoke)(DISPID dispIdMember,
            REFIID riid,
            LCID lcid,
            WORD wFlags,
            DISPPARAMS __RPC_FAR *pDispParams,
            VARIANT __RPC_FAR *pVarResult,
            EXCEPINFO __RPC_FAR *pExcepInfo,
            UINT __RPC_FAR *puArgErr);

    // events
    HRESULT BeforeNavigate (
        _bstr_t URL,
        long Flags,
        _bstr_t TargetFrameName,
        VARIANT * PostData,
        _bstr_t Headers,
        VARIANT_BOOL * Cancel );

    HRESULT NavigateComplete ( _bstr_t URL );
    HRESULT StatusTextChange ( _bstr_t Text );
    HRESULT ProgressChange (
        long Progress,
        long ProgressMax );
    HRESULT DownloadComplete();
    HRESULT CommandStateChange (
        long Command,
        VARIANT_BOOL Enable );
    HRESULT DownloadBegin ();
    HRESULT NewWindow (
        _bstr_t URL,
        long Flags,
        _bstr_t TargetFrameName,
        VARIANT * PostData,
        _bstr_t Headers,
        VARIANT_BOOL * Processed );
    HRESULT TitleChange ( _bstr_t Text );
    HRESULT FrameBeforeNavigate (
        _bstr_t URL,
        long Flags,
        _bstr_t TargetFrameName,
        VARIANT * PostData,
        _bstr_t Headers,
        VARIANT_BOOL * Cancel );
    HRESULT FrameNavigateComplete (
        _bstr_t URL );
    HRESULT FrameNewWindow (
        _bstr_t URL,
        long Flags,
        _bstr_t TargetFrameName,
        VARIANT * PostData,
        _bstr_t Headers,
        VARIANT_BOOL * Processed );
    HRESULT Quit (
        VARIANT_BOOL * Cancel );
    HRESULT WindowMove ( );
    HRESULT WindowResize ( );
    HRESULT WindowActivate ( );
    HRESULT PropertyChange (
        _bstr_t Property );

	// members
	CWebBrowserAxDialog *m_cpParent; // any time a IWebBrowser instance is needed

public:
	void SetParent(CWebBrowserAxDialog *pParent) { m_cpParent = pParent; }
};

What remains is the implementation of the IDispatch::Invoke() which is really the entry point for all subscribed events. By contract, it's our job in the Invoke implementation to dispatch the event as an appropriate method call. In the example, we have only implemented the dispatch of the OnBeforeNavigate event, which is one of the main events programmers are interested about. Code for the implementation is as follows :

HRESULT __stdcall DWebBrowserEventsImpl::Invoke(DISPID dispIdMember,
            REFIID riid,
            LCID lcid,
            WORD wFlags,
            DISPPARAMS __RPC_FAR *pDispParams,
            VARIANT __RPC_FAR *pVarResult,
            EXCEPINFO __RPC_FAR *pExcepInfo,
            UINT __RPC_FAR *puArgErr)
{ 
  // proces OnBeforeNavigate
  if (dispIdMember == DISPID_BEFORENAVIGATE)
  {
    // call BeforeNavigate
    // (parameters are on stack, thus on reverse order)
    BeforeNavigate( /*url*/ _bstr_t( pDispParams->rgvarg[5].bstrVal ),
                    0,
                    _bstr_t( pDispParams->rgvarg[3].bstrVal ),
                    NULL,
                    _bstr_t(""),
                    NULL);
  }
  else if (dispIdMember == DISPID_NAVIGATECOMPLETE)
  {
    NavigateComplete( _bstr_t( pDispParams->rgvarg[0].bstrVal ) );
  }
  else
  {
     ... // implement all event handlers of interest to you
  }

  return NOERROR;
}

That's all. The working code is provided in WebBrowserAxDialog.h and WebBrowserAxDialog.cpp.

Last but not least, here is how the get the current html document : (made possible by importing the mshtml type-library)

HRESULT DWebBrowserEventsImpl::NavigateComplete ( _bstr_t URL ) 
{ 
  SHDocVw::IWebBrowserAppPtr pWebBrowser = NULL;
  HRESULT hr = m_cpParent->GetDlgControl(IDC_EXPLORER1, 
                               __uuidof(SHDocVw::IWebBrowserAppPtr), 
                              (void**)&pWebBrowser);

  // get the html document
  MSHTML::IHTMLDocument2Ptr doc( pWebBrowser->Document );
  MSHTML::IHTMLElementPtr htmlbody( doc->body );

  BSTR content = NULL;
  htmlbody->get_innerHTML(&content);
  _bstr_t bcontent(content);

  return S_OK; 
}

As a side note, there is an amazing article[^] by Dino Esposito developers often refer to when they are trying to integrate the web browser into MFC dialogs. Dino has a hard time trying to fit a CHtmlView class (CView derived class) as if it was a mere dialog control. In the end, this works, although this requires a lot of overhead, including degrading the MFC document/view model.

 

Update history

April 12 - initial release
April 19 - update :
  • removed flickering (WS_CLIPCHILDREN style)
  • added helper macros
  • added ATL7 support

 

 

Stéphane Rodriguez - April 19, 2003.

 


Home
Blog