Custom Open dialogs, part II

by Damon Chandler

Last month, I showed you how to place a TWinControl-type child window to the right side of an Open dialog’s standard controls. This month, I’ll demonstrate how to use an in-memory dialog box template to move the standard controls. This way you can place multiple TWinControl-type child windows anywhere within the dialog.

Using a dialog template with the standard Open dialog

Recall from last month’s article that an Open dialog box contains a child dialog designed to host additional, user-defined controls. This child dialog has a default width of zero, which was preferable last time because we didn’t use it (i.e., we placed the additional controls directly on the Open dialog box itself, not on its child dialog). That technique was fine for adding controls below or to the right of the Open dialog’s standard controls, but it won’t work if you need to place additional controls above or to the left of the standard controls. Why? Well, last month, when we placed our TWinControl-type window to the right of the Open dialog’s standard controls, we didn’t have to worry about the location of these standard controls—they weren’t in the way. Before you can place additional controls above or to the left of the standard controls however, you’ll need to move the standard controls to make room for the new controls.

There are two ways to move the Open dialog’s standard controls:

·Use a dialog box template.

·Grab a handle to each control, and then use the MoveWindow() or SetWindowPos() API function.

As it turns out, the second approach doesn’t work very well, particularly because the Open dialog’s list view control (i.e., the view that displays the files and folders) is re-created each time the user makes a folder change.

Specifying the dimensions of the Open dialog box

As I mentioned last time, you can use a resource template to specify the size of the Open dialog’s child dialog. The Open dialog will expand its own height by whatever value you specify as the child dialog’s height. For example, here’s a resource script that will expand the Open dialog’s height by 100 vertical dialog units:

OPENDLGTEMPLATE DIALOG 0, 0, 80, 100
STYLE WS_CHILD | WS_CLIPSIBLINGS |
      DS_CONTROL
BEGIN
END

Here, I’ve specified 100 as the child dialog’s height, which implicitly instructs the Open dialog to increase its own height by 100 vertical dialog units. In contrast, although I’ve specified 80 as the child dialog’s width, the Open dialog box won’t adjust its own width by this (or any) amount. This (somewhat odd) behavior is by design; namely, the Open dialog adjusts only its height because, by default, all additional user-defined controls are to be placed below the Open dialog’s standard controls.

Note that, because this resource script defines the template for the child dialog (not the Open dialog), I’ve specified the WS_CHILD and DS_CONTROL styles. In addition, I’ve specified the WS_CLIPSIBLINGS style, which ensures that the child dialog won’t occlude the Open dialog’s standard controls.

To use this resource template, you simply save it to an RC file (e.g., MYOPENDIALOG.RC), add the file to your project, and then assign the child dialog’s identifier (OPENDLGTEMPLATE in this example) to the TOpenDialog::Template property (in a descendant class), like so:

bool __fastcall TMyOpenDialog::Execute()
{
  Template = "OPENDLGTEMPLATE";
  return TOpenDialog::Execute();
}

Figure A depicts the resulting Open dialog box when this template is used.

Figure A

By using a dialog box template to define the Open dialog’s child dialog, you can indirectly specify the height (but not the width) of the Open dialog box.

Specifying the location of the Open dialog’s standard controls

Again, by using a resource template to specify the styles and the height of the child dialog, you can implicitly define the height of the Open dialog box. In order to adjust the width of the Open dialog box—and also move its standard controls—you’ll need to place a special child window on the Open dialog’s child dialog; specifically, you need to add a static control with the identifier stc32 (defined as 1119 in DGLS.H). For example:

#include <dlgs.h>
OPENDLGTEMPLATE DIALOG 0, 0, 80, 100
STYLE WS_CHILD | WS_CLIPSIBLINGS |
      DS_CONTROL
BEGIN
  LTEXT "", stc32, 52, 48, 0, 0,
    NOT WS_VISIBLE | NOT WS_GROUP
END

The LTEXT statement creates a static control whose text is aligned to the left. Here, this static control is given an identifier of stc32, a horizontal position of 52, a vertical position of 48, and a width and height of zero. As depicted in Figure B, the Open dialog will interpret the horizontal and vertical position of this special static control as the desired location of the standard controls. The “stc32” static control is still created, but it’s not positioned at the location specified in the template (52, 48 here). Rather, the “stc32” static control is automatically positioned at the lower-right corner of the Open dialog’s standard controls. You can see this in Figure C, which depicts the resulting Open dialog box when the following resource script is used:

#include <dlgs.h>
OPENDLGTEMPLATE DIALOG 0, 0, 80, 100
STYLE WS_CHILD | WS_CLIPSIBLINGS |
      DS_CONTROL
BEGIN
  LTEXT "STC32", stc32, 52, 48, 0, 0
END

Notice from Figures B and C that creating the “stc32” static control not only allows you to specify the location of the Open dialog’s standard controls, it also instructs the Open dialog to increase its own width by the specified width of the child dialog (80 horizontal dialog units in these examples).

Keep in mind that the coordinates of the “stc32” static control are relative to the client area of the Open dialog’s child dialog, and that they are specified in dialog units (not pixels). To convert from pixels to dialog units, you can use the GetDialogBaseUnits() API function like so:

short Pix2DlgUnitsX(short x)
{
  return (x * 4.0) /
    LOWORD(GetDialogBaseUnits());
}

short Pix2DlgUnitsY(short y)
{
  return (y * 8.0) /
    HIWORD(GetDialogBaseUnits());
}

Figure B

By placing a static control with the identifier stc32 on the Open dialog’s child dialog, you can specify the location of the Open dialog’s standard controls.

Figure C

The “stc32” static control isn’t positioned at the coordinates specified in the resource script; instead, it’s placed at the bottom-left corner of the standard controls.

I’ve just shown you how to use a resource script to define the template that’s used for the Open dialog’s child dialog. Although creating the script isn’t too hard, defining the correct size of the child dialog, and the correct location of the “stc32” static control, can require a bit of trial and error. Remember, the whole point of resizing the Open dialog box and moving its standard controls is to make room for the TWinControl-type VCL controls that will be added to the Open dialog. If you later change the size of one or more of these VCL controls, you’ll have to edit the resource script to adjust the location of the static control and the size of the child dialog—more trial and error. As I’ll discuss next, however, there’s a way around this hassle.

Creating an in-memory dialog template

Instead of using a resource script to define the template that’s used to create the Open dialog’s child dialog, there is an alternative technique: you can use a template that’s stored in global memory. The following function demonstrates how this is done; it creates an in-memory dialog box template with the dimensions specified by the dlg_w and dlg_h parameters and with an “stc32” static control at the location specified by the stc_x and stc_y parameters. Here’s the code:

HGLOBAL CreateChildDlgTemplate(
   short dlg_w, short dlg_h,
   short stc_x, short stc_y,
   DWORD styles = 0,
   DWORD ex_styles = 0)
{
  // create a global memory object
  const HGLOBAL hTemplate =
    GlobalAlloc(GMEM_ZEROINIT, 384);

  // grab a pointer to the memory
  DLGTEMPLATE* pTemplate =
    static_cast<DLGTEMPLATE*>
      (GlobalLock(hTemplate));
  if (!pTemplate) return 0;

  // initialize the child dialog
  pTemplate->style =
    styles | WS_CHILD |
    WS_CLIPSIBLINGS | DS_CONTROL;
  pTemplate->dwExtendedStyle = ex_styles;    
  pTemplate->cdit = 1; // 1 child control
  pTemplate->cx = Pix2DlgUnitsX(dlg_w);
  pTemplate->cy = Pix2DlgUnitsY(dlg_h);

  // grab a pointer to the data for
  // the dialog's menu, class, and
  // title properties
  WORD* pData = reinterpret_cast<WORD*>
    (pTemplate + 1);

  // set no menu, use default class,
  // and no title
  pData += 3;

  // grab a pointer to the data for
  // the "stc32" control properties
  DLGITEMTEMPLATE* pItem =
    reinterpret_cast<DLGITEMTEMPLATE*>
      (pData);
  pItem->style = WS_CHILD | SS_LEFT;
  pItem->id = stc32;
  pItem->x = Pix2DlgUnitsX(stc_x);
  pItem->y = Pix2DlgUnitsY(stc_y);

  // grab a ponter to the data for the
  // static's class and title properties
  pData =
    reinterpret_cast<WORD*>(pItem + 1);
  // set the STATIC class and no title
  pData[0] = 0xFFFF;
  pData[1] = 0x0082;
  pData += 3;

  // clean up
  GlobalUnlock(hTemplate);

  // return a handle to the memory
  return hTemplate;
}

This code first uses the GlobalAlloc() API function to create a global memory object of 384 bytes (large enough to hold our template data). It then grabs a pointer to the underlying memory (by using the GlobalLock() function) and fills it with the data of the template; these data include the following:

1.A DLGTEMPLATE structure that specifies the attributes of the dialog.

2.A variable-length array of identifiers for the dialog’s menu, class, and title.

3.One or more DLGITEMTEMPLATE structures that specify the attributes of the dialog’s child controls; each DLGITEMTEMPLATE data is followed by a variable-length array of identifiers for the child control’s class, title, and creation data.

This is a just brief overview of the layout of an in-memory template. You can read a full description in the section titled “Templates in Memory” in the Platform SDK help files or on MSDN online. (Pay particular attention to the DWORD-alignment requirements of the data structures.)

The dlg_w, dlg_h, stc_x, and stc_y parameters are specified in pixels. The CreateChildDlgTemplate() function uses the previously defined Pix2DlgUnitsX() and Pix2DlgUnitsY() functions to convert these coordinates to dialog units. The styles and ex_styles parameters allow you to specify additional styles and extended styles, respectively, for the dialog.

Using an in-memory dialog template

If successful, the CreateChildDlgTemplate() function will return a handle to the global memory object that holds the template. How do you instruct the Open dialog box to use this in-memory template? Well, the standard approach is to set the OFN_ENABLETEMPLATEHANDLE flag in the OPENFILENAME::Flags data member, and then assign the handle to the in-memory template’s global memory object to the OPENFILENAME::hInstance data member. For example:

void __fastcall TForm1::
  OpenButtonClick(TObject *Sender)
{
  TCHAR filename[MAX_PATH];
  memset(&filename, 0, MAX_PATH);

  // create the in-memory template
  const HGLOBAL hTemplate =
    CreateChildDlgTemplate(
      160, 200, 104, 96);

  OPENFILENAME ofn = {
    OPENFILENAME_SIZE_VERSION_400};
  ofn.hwndOwner = Handle;
  ofn.nMaxFile = MAX_PATH;
  ofn.lpstrFile = filename;

  // assign the handle to the in-memory
  // template to the hInstance member
  ofn.hInstance = hTemplate;

  // add the OFN_ENABLETEMPLATEHANDLE
  // flag to the Flags member
  ofn.Flags =
    OFN_EXPLORER |
    OFN_ENABLETEMPLATEHANDLE;
  
  // display the open dialog box.
  if (GetOpenFileName(&ofn))
  {
    // ...
  }

  // clean up
  GlobalFree(hTemplate);
}

Although the TOpenDialog class provides the Template property (which adds the OFN_ENABLETEMPLATE flag to the OPENFILENAME::Flags data member, and which sets the specified resource identifier to the OPENFILENAME::lpTemplateName data member), there is no TOpenDialog::TemplateHandle property that would make things easy here. Fortunately, the TOpenDialog class does grant access to its internal OPENFILENAME structure, which you can modify in a fashion similar to that shown in this code snippet. Later, I’ll show you how to perform this modification.

By using the CreateChildDlgTemplate() function, you don’t have to worry about creating a resource script; and, you don’t have to hand code the dimensions of the dialog and/or the location of the “stc32” static control. Instead (as I’ll demonstrate shortly), you can compute these values at run time (by querying the size(s) of the VCL control(s) that you’re going to add to the Open dialog) and then simply pass them to the CreateChildDlgTemplate() function.

Extending the TOpenDialogEx class

Last month, we created a TOpenDialog descendant class (TOpenDialogEx) that allowed you place a TWinControl-type VCL window to the right of the Open dialog’s standard controls. Now that we know how to move these standard controls, let’s extend this class to accept four TWinControl-type child windows. As you can see from Listing A, I’ve named these four child windows ChildWinLeft, ChildWinTop, ChildWinRight, and ChildWinBottom; Figure D depicts their layout within the Open dialog box.

Pointers to the four child windows are stored in the ChildWinLeft_, ChildWinTop_, ChildWinRight_, and ChildWinBottom_ members. These members are initialized in the class constructor, like so:

__fastcall TOpenDialogEx::
  TOpenDialogEx(TComponent* Owner)
  : TOpenDialog(Owner),
    ChildWinLeft_(NULL),
    ChildWinTop_(NULL),
    ChildWinRight_(NULL),
    ChildWinBottom_(NULL),
    OldDlgWP_(NULL), NewDlgWP_(NULL)
{
}

Recall from last month that the OldDlgWP_ and NewDlgWP_ members are used to subclass the Open dialog box to support resizing. I’ll return to this issue shortly.

Figure D

The layout of the TOpenDialogEx::ChildWinLeft, ChildWinTop, ChildWinRight, and ChildWinBottom windows.

Making room for the child windows

Last time, we adjusted the width of the Open dialog manually by using the SetWindowPos() API function. This time, we’ll use an in-memory template to adjust the size of the Open dialog and move its standard controls. It’s the job of the TOpenDialogEx::DoCreateTemplate() method to create the in-memory template. Here’s how the method is defined:

HGLOBAL __fastcall TOpenDialogEx::
  DoCreateTemplate()
{
  // compute the desired location of the
  // "stc32" static control and the size
  // of the child dialog
  const short stc_x = ChildWinLeft_ ?
    ChildWinLeft_->Width : 0;
  const short stc_y = ChildWinTop_ ?
    ChildWinTop_->Height : 0;
  const short dlg_w =
    stc_x + (ChildWinRight_ ?
    ChildWinRight_->Width : 0);
  const short dlg_h =
    stc_y + (ChildWinBottom_ ?
    ChildWinBottom_->Height : 0);

  // create the child dialog template
  return CreateChildDlgTemplate(
    dlg_w, dlg_h, stc_x, stc_y);
}

The DoCreateTemplate() method determines which child windows are specified, and then computes the appropriate dimensions of the Open dialog’s child dialog and the appropriate location of the “stc32” static control. These values are then passed to the previously defined CreateChildDlgTemplate() function, which returns a handle to the global memory object that holds the child dialog’s template.

In order to instruct the TOpenDialog class to use our in-memory template, we’ll need to augment the TOpenDialog::DoTaskModal() method. It’s from within this method that we’re granted access to the internal OPENFILENAME structure. Here’s the code:

BOOL __fastcall 
TOpenDialogEx::TaskModalDialog(
  void* DialogFunc, void* DialogData)
{
  // punt, if there's a resource template
  // specified or if there are no child
  // windows to add
  if (Template || !HasChild())
  {
    // show the dialog
    return TOpenDialog::TaskModalDialog(
      DialogFunc, DialogData);
  }

  // create an in-memory template
  const HGLOBAL hTemplate =
    DoCreateTemplate();
  try
  {
    // grab a pointer to the OPENFILENAME
    TOpenFilename* pofn =
      static_cast<TOpenFilename*>
        (DialogData);
        
    // remove the resource template flag
    pofn->Flags &=
      ~OFN_ENABLETEMPLATE;
    // add the in-memory template flag
    pofn->Flags |=
      OFN_ENABLETEMPLATEHANDLE;
    // set the in-memory template handle
    pofn->hInstance = hTemplate;

    // show the dialog
    const BOOL ok =
      TOpenDialog::TaskModalDialog(
        DialogFunc, DialogData);
        
    // clean up
    GlobalFree(hTemplate);
    return ok;
  }
  catch (...)
  {
    // clean up  
    GlobalFree(hTemplate);
    throw;
  }
}

Adding the child windows to the dialog

After the Open dialog is resized, it’s time to add the child windows (ChildWinLeft, ChildWinTop, ChildWinRight, and ChildWinBottom). Whereas last time, we placed the child window (ChildWin) directly on the Open dialog, this time we need to place the child windows on the Open dialog’s child dialog. Again, this step is straightforward—you just use the ParentWindow property like so:

void __fastcall TOpenDialogEx::
  DoShowChildWins()
{
  RECT RDlg;
  const HWND hChildDlg = Handle;
  if (GetClientRect(hChildDlg, &RDlg))
  {
    if (ChildWinLeft_)
    {
      // parent the child to the dialog
      ChildWinLeft_->
        ParentWindow = hChildDlg;

      // position/size the child window
      ChildWinLeft_->Left = 0;
      ChildWinLeft_->Top = 0;
      ChildWinLeft_->Height =
        RDlg.bottom;

      // show the child window
      ChildWinLeft_->Visible = true;
    }
    if (ChildWinTop_)
    {
      // parent the child to the dialog
      ChildWinTop_->
        ParentWindow = hChildDlg;

      // position/size the child window
      ChildWinTop_->Left =
        ChildWinLeft_ ?
        ChildWinLeft_->Width : 0;
      ChildWinTop_->Top = 0;
      ChildWinTop_->Width =
        RDlg.right -
        ChildWinTop_->Left -
        (ChildWinRight_ ?
         ChildWinRight_->Width : 0);

      // show the child window
      ChildWinTop_->Visible = true;
    }
    if (ChildWinRight_)
    {
      // parent the child to the dialog
      ChildWinRight_->
        ParentWindow = hChildDlg;

      // position/size the child window
      ChildWinRight_->Left =
        RDlg.right -
        ChildWinRight_->Width;
      ChildWinRight_->Top = 0;
      ChildWinRight_->Height =
        RDlg.bottom;

      // show the child window
      ChildWinRight_->Visible = true;
    }
    if (ChildWinBottom_)
    {
      // parent the child to the dialog
      ChildWinBottom_->
        ParentWindow = hChildDlg;

      // position/size the child window
      ChildWinBottom_->Left =
         ChildWinLeft_ ?
         ChildWinLeft_->Width : 0;
      ChildWinBottom_->Top =
        RDlg.bottom -
        ChildWinBottom_->Height;
      ChildWinBottom_->Width =
        RDlg.right -
        ChildWinBottom_->Left -
        (ChildWinRight_ ?
         ChildWinRight_->Width : 0);

      // show the child window
      ChildWinBottom_->Visible = true;
    }
  }
}

Recall that the DoShowChildWins() method (which was named DoShowChildWin() last month) is called from within the TOpenDialogEx::DoShow() method (i.e., after the dialog has initialized its standard controls). Here’s how the DoShow() method is defined:

void __fastcall TOpenDialogEx::DoShow()
{
  if (
    HasChild() &&
    !Options.Contains(ofOldStyleDialog))
  {
    // show the child windows
    DoShowChildWins();

    // subclass the dialog
    DoSubclassDialog(true);
  }
  TOpenDialog::DoShow();
}

Before the Open dialog closes, we need to remove the child windows, like so:

void __fastcall TOpenDialogEx::DoClose()
{
   if (ChildWinLeft_)
   {
      // clean up
      ChildWinLeft_->Hide();
      ChildWinLeft_->ParentWindow = 0;
   }
   if (ChildWinTop_)
   {
      // clean up
      ChildWinTop_->Hide();
      ChildWinTop_->ParentWindow = 0;
   }
   if (ChildWinRight_)
   {
      // clean up
      ChildWinRight_->Hide();
      ChildWinRight_->ParentWindow = 0;
   }
   if (ChildWinBottom_)
   {
      // clean up
      ChildWinBottom_->Hide();
      ChildWinBottom_->ParentWindow = 0;
   }
   TOpenDialog::DoClose();
}

Finishing up

Recall from last month that we subclassed the Open dialog in order to resize the child windows whenever the user resizes the Open dialog box. Now that we have four child windows instead of one, the subclass procedure needs to be modified to resize all four child windows. I won’t repeat the code that does the actual subclassing, but here’s the modified subclass procedure:

void __fastcall TOpenDialogEx::
  DialogWndProc(TMessage& Msg)
{
  // call the default window procedure
  const HWND hDlg = GetParent(Handle);  
  Msg.Result = CallWindowProc(
    OldDlgWP_, hDlg,
    Msg.Msg, Msg.WParam, Msg.LParam);

  switch (Msg.Msg)
  {
    case WM_SIZE:
    {
      RECT RDlg;
      if (::GetClientRect(hDlg, &RDlg))
      {
        // resize the child windows
        if (ChildWinLeft_)
        {
          ChildWinLeft_->Height =
            RDlg.bottom;
        }
        if (ChildWinRight_)
        {
          ChildWinRight_->Height =
            RDlg.bottom;
        }
        if (ChildWinTop_)
        {
          ChildWinTop_->Width =
            RDlg.right -
            ChildWinTop_->Left -
            (ChildWinRight_ ?
             ChildWinRight_->Width : 0);
        }
        if (ChildWinBottom_)
        {
          ChildWinBottom_->Width =
            RDlg.right -
            ChildWinBottom_->Left -
            (ChildWinRight_ ?
             ChildWinRight_->Width : 0);
        }
      }
      break;
    }
    case WM_DESTROY:
    {
      // remove the subclass
      DoSubclassDialog(false);
      break;
    }
  }
}

Conclusion

How do you use the new TOpenDialogEx class? Because I provided an example in last month’s article, and because the interface to the class hasn’t changed much, I’ll let the sample code (which is available at www.bridgespublishing.com) do the talking here. If you have further questions on the customizing common dialogs, please fell free to e-mail me at dmc27@cornell.edu.

Listing A: Declaration of the modified TOpenDialogEx class

#include <dlgs.h>
class PACKAGE TOpenDialogEx : public TOpenDialog
{
public:
  // default constructor
  __fastcall TOpenDialogEx(TComponent* Owner);

  // introduced properties
  __property TWinControl* ChildWinLeft =
    {read = ChildWinLeft_,
     write = SetChildWinLeft};
  __property TWinControl* ChildWinTop =
    {read = ChildWinTop_,
     write = SetChildWinTop};
  __property TWinControl* ChildWinRight =
    {read = ChildWinRight_,
     write = SetChildWinRight};
  __property TWinControl* ChildWinBottom =
    {read = ChildWinBottom_,
     write = SetChildWinBottom};

protected:
  // inherited member functions
  DYNAMIC void __fastcall DoShow();
  DYNAMIC void __fastcall DoClose();
  virtual BOOL __fastcall TaskModalDialog
    (void* DialogFunc, void* DialogData);

  // introduced member functions
  virtual void __fastcall DoShowChildWins();
  virtual void __fastcall DoSubclassDialog
    (bool subclass);
  virtual void __fastcall DialogWndProc
    (TMessage& Msg);
  virtual HGLOBAL __fastcall DoCreateTemplate();

private:
  FARPROC OldDlgWP_;
  FARPROC NewDlgWP_;

  TWinControl* ChildWinLeft_;
  TWinControl* ChildWinTop_;
  TWinControl* ChildWinRight_;
  TWinControl* ChildWinBottom_;

  bool __fastcall HasChild()
    {
      return (
        ChildWinLeft_ || ChildWinTop_ ||
        ChildWinRight_ || ChildWinBottom_
        );
    }
  void __fastcall SetChildWinLeft
    (TWinControl* Value)
    {
      if (ChildWinLeft_ != Value)
      {
        ChildWinLeft_ = Value;
        ChildWinLeft_->Parent = NULL;
      }
    }
  void __fastcall SetChildWinTop
    (TWinControl* Value)
    {
      if (ChildWinTop_ != Value)
      {
        ChildWinTop_ = Value;
        ChildWinTop_->Parent = NULL;
      }
    }
  void __fastcall SetChildWinRight
    (TWinControl* Value)
    {
      if (ChildWinRight_ != Value)
      {
        ChildWinRight_ = Value;
        ChildWinRight_->Parent = NULL;
      }
    }
  void __fastcall SetChildWinBottom
    (TWinControl* Value)
    {
      if (ChildWinBottom_ != Value)
      {
        ChildWinBottom_ = Value;
        ChildWinBottom_->Parent = NULL;
      }
    }
};