A Splitter Window Control

Introduction


This article contains updated code for the splitter
window control created by myself several months ago. Also I provide here some
descriptions where and how to use such control. I must admit that the previous
version of the control was rather unreliable and it failed to work correctly in
many cases. This version is much more stable, although I can’t guarantee
that it will work in all conditions you want. In this article I do not try to
show the difference of code between this and the previous version of the
control. I just describe it from the scratch.

 


Where and how to use the control



After the publication of the first version of the
control I received some messages, which told me that some people had not
understood the idea of this component. I was asked questions like:

I have a dialog box derived class in an MFC project. The
class has two members: m_TreeCtrl and m_ListCtrl created with help of the MFC
Class Wizard. The question is how to put the members into the splitter window
control?



Here is my answer. The control I have created is
supposed to be used in other tasks. Actually, to create a splitter window
control to be used in an MFC dialog box is a much easier task (if not take into
account the ability to place ActiveX controls into panes). You can even use
portions of code from my control. You do not have to deal with COM. All you have
to do is to provide a way of letting know the splitter control the window
handles of other controls on the dialog you want to place in panes. When the
user changes the sizes of panes you would call SetWindowPos function.

The control is being described in this article is
supposed to be used in ActiveX control containers such as forms in Visual Basic.
Personally myself I am going to use the control in ActiveX scripting hosts with
forms support. An example if such program is provided as a testing program. The
task of the control is to manipulate other ActiveX controls, placed on the same
container, to be used as panes.


 


Testing



In the Results\Release folder you will find a lot of files. One
of them is “SplitterWindowControl.ocx “ one. Register it.

In the same folder you can also find a testing program
– ATnT. This program
is an example of an ActiveX scripting host created
with help of the HyperHost
library provided by Dundas Software company, plus
one of my own libraries.  In

fact, ATnT is loosely based on
HyperHost’s ATnTLite sample.

To test splitter control with the ATnT program, run the
program first. Invoke the File | Open menu item. Select ATnT.mdb file and press
[Open] button. You will see “Main Form”, on the first page of which
there will be two splitter controls. The first splitter control has a list view
in its left pane and another splitter control in its right pane. The second
splitter control manipulates two other list view controls.

You can also try to open “Splitters.afm”
file with the ATnT program. The form in this file demonstrates two splitter
controls, placed on different pages. You can also notice that the program can
open module files (scripts to control the “Application” object
exposed by the program), plus files of ActiveX document servers registered on
your computer. At the end of the article there is a chapter, which describes
what the ATnT sample is, what it demonstrates and how to obtain its source
code.


 


What is the splitter window control?



I hope you know everything about CSplitterWnd class. I
will not explain here what it is. This is MFC code guru site is not it? He, who
does not know, can always learn it from the documentation. This control has a
splitter window. The splitter window in this control always consists of two
panes (if you improve it to use more and send me your updates I would appreciate
that). The splitter window can have two rows or two columns. The control has 4
properties. It fires no events and has no methods.

Properties:

[id(1)] BSTR strFirstControlName;

[id(2)] BSTR strSecondControlName;


[id(3)] boolean bVertical;


[id(4)] short intFirstPaneSize;


[id(5)] long _pointer;

The strFirstControlName property tells the control the
name of other control to put into the first pane. The strSecondControlName
property tells the control the name of other control to put into the second
pane. If bVertical is TRUE, then we have a splitter with two columns, if it is
FALSE, we have a splitter with two rows. The intFirstPaneSize property contains
width or height of the first pane, depending on the value of the bVertical
property. The pointer property is a read only property, which contains the
pointer to this control. Here is how the pointer is returned:




long
CSplitterWindowControlCtrl::GetPointer()

{



//
TODO: Add your property handler here

return long(this);

}

 

How to use the control



You can place the control on a form. Then place another
two controls to use in panes (see the picture bellow).


Notice that you do not have to align your controls with
the panes. But I do recommend you to put them above the panes. You can not use
windowless controls as panes.



Having put the controls on the form, you must attach
them to the splitter window control. You can do this manually by assigning their
names to the “strFirstControlName” and
“strSecondControlName” properties respectively, or by invoking the
property page for the control.


You can pick the control you need from the comboboxes.

When you switch your form into the user mode the
attached controls will be placed into panes. Do not forget to turn off the
border from the controls your are going to attach to the splitter control. This
will make the picture better.

 


 


Implementation of the control



The idea is a simple one.


  1. We place a splitter window on our control.
  2. Provide an ability to find other controls on the
    form.
  3. When the control container is in the user mode we
    access the “pane” controls by their extended “Name”
    properties and change their position when user changes sizes of panes.

The CSplitterWnd class is very capricious. It was
designed to be used in frames, and to use CView derived classes as panes. To
save my time I had to satisfy it with these conditions. The
RecreateSplitterWindow demonstrates the sequence of code to create the splitter
window. As you can see the control uses a CFrameWnd derived object to cover all
the control’s client area. The splitter window is placed into the frame
one. Two CView derived objects are placed into panes. The view objects do not
have documents. The frame window and the view ones were created just to satisfy
MFC version of the CSplitterWnd class.




void
CSplitterWindowControlCtrl::RecreateSplitterWindow()

{



 if(m_wndFrame.m_hWnd)

 m_wndFrame.DestroyWindow();
 

 CRect rectClient;
 GetClientRect(rectClient);
 m_wndFrame.Create(NULL, NULL, WS_CHILD|WS_VISIBLE,
rectClient, this);

 
 int nRows,nColumns;
 CPoint ptPanes[2];
 CSize sizePanes[2];
 if(m_bVertical)
 {


 nRows=1;
 nColumns=2;
 ptPanes[0]=CPoint(0,0);
 sizePanes[0]=CSize(m_intFirstPaneSize,rectClient.Height());
 ptPanes[1]=CPoint(0,1);
 sizePanes[1]=sizePanes[0];

 }
 else
 {


 nRows=2;
 nColumns=1;
 ptPanes[0]=CPoint(0,0);
 sizePanes[0]=CSize(rectClient.Width(),m_intFirstPaneSize);
 ptPanes[1]=CPoint(1,0);
 sizePanes[1]=sizePanes[0];

}
 
m_pSplitter=new CControlsSplitterWnd;
m_pSplitter->CreateStatic(&m_wndFrame,nRows,nColumns);
 
CCreateContext context;
context.m_pCurrentDoc = NULL;
context.m_pCurrentFrame=&m_wndFrame;
m_pSplitter->CreateView(ptPanes[0].x, ptPanes[0].y,
RUNTIME_CLASS(CSplitterControlView),sizePanes[0],&context);

 m_pSplitter->CreateView(ptPanes[1].x, ptPanes[1].y,
RUNTIME_CLASS(CSplitterControlView),sizePanes[1],&context);

RepositionSplitter();

}

 


Here is the list of window classes participating in the
implementation of the control:

  1. CSplitterWindowControlCtrl – is a COleControl
    derived class, which represents the ActiveX splitter control.
  2. CSplitterControlFrame – is a CFrameWnd
    derived class, instance of which is placed on the
    CSplitterWindowControlCtrl’s one.
  3. CControlsSplitterWnd – is a CSplitterWnd
    derived class. An instance of this class is placed in the client area of the
    CSplitterControlFrame.
  4. CSplitterControlView – this class provides
    views for the splitter window.

 


Now we will learn how to find other controls, placed on
the same form along with our control. We will ask the form to provide us with
this information and store it in a CControlInfoArray.




struct
CControlInfo


{


CString m_strName;

IOleObjectPtr
m_pOleObject;



 

CControlInfo& operator=(const CControlInfo& other)



{


m_strName=other.m_strName;


m_pOleObject=other.m_pOleObject;


return *this;

}

};
 

class
CControlInfoArray : public
CArray<CControlInfo,CControlInfo&>

{


public:


int IndexOf(LPCTSTR lpstrName);

};

The CControlInfo structure stores the extended name
property of a control (the one, which is given to the control by the container)
and the IOleObject interface of the control. The GetOtherControlsOnTheContainer
function fills a CControlInfoArray with the information about controls.
Let’s see how does the thing work.

  1. When a container places an object onto itself, it
    gives the one different site interfaces, through which the object can
    communicate with the container. In our case IOleClientSite is used. There
    are other interfaces IOleControlSite, IOleControlSiteWindowless,
    IBoundObjectSite etc.
  2. Any good container implements IOleContainer
    interface, through which we can get IUnknown pointers to the objects the
    container contains.
  3. If the container provides extended properties and
    events it does it by adding an “extender” COM object, which
    aggregates the control. The IDispatch interface to the
    “extender” object is provided through, the corresponding to the
    object, site interface.
  4. It seems to be a standard that containers
    distinguish the controls by the unique strings they associate with each
    control. The string is accessible through the “Name” extended
    property. Using the IDispatch of the “extender” object we can
    obtain values of the extended properties.

 




 

void
CSplitterWindowControlCtrl::GetOtherControlsOnTheContainer(






CControlInfoArray&
arrayControls)


{

if(!m_pClientSite)

return;
 


IOleContainerPtr pOleContainer;
IEnumUnknownPtr pEnumUnk;

 

if(FAILED(m_pClientSite->GetContainer(&pOleContainer)))


return;


 

if(FAILED(pOleContainer->EnumObjects(OLECONTF_EMBEDDINGS|











OLECONTF_OTHERS,



&pEnumUnk)))


{

return;

}
 

IUnknownPtr pUnk;
HRESULT hr = E_FAIL;

 

while(pEnumUnk->Next(1,&pUnk,NULL) == S_OK)
{


IOleObjectPtr pOleObject;
hr =
pUnk->QueryInterface(IID_IOleObject,(void**)&pOleObject);

if(SUCCEEDED(hr))
{

//ActiveX control
IOleClientSitePtr pClientSite;
pOleObject->GetClientSite(&pClientSite);

 



IDispatchPtr pAmbientDispatch;
pClientSite->QueryInterface(IID_IDispatch,







(void**)&pAmbientDispatch);



 

if(pAmbientDispatch)
{

CControlInfo info;
info.m_strName=GetExtendedName(pAmbientDispatch);
if(!info.m_strName.IsEmpty())
{

info.m_pOleObject=pOleObject;
arrayControls.Add(info);

}

}

}
pUnk=NULL;

}

}
 
 
 
CString CSplitterWindowControlCtrl::GetExtendedName(IDispatchPtr
pDispatch)

{



COleVariant var;
DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};
 
HRESULT hr = E_FAIL;
hr=pDispatch->Invoke(DISPID_AMBIENT_DISPLAYNAME,





IID_NULL,


LOCALE_USER_DEFAULT,

DISPATCH_PROPERTYGET | DISPATCH_METHOD,


&dispparamsNoArgs,

&var,


NULL,


NULL);


 

if(FAILED(hr))
{


DISPID dispID=0;
LPWSTR lpName[1] = { (WCHAR *)L"Name" };


hr = pDispatch->GetIDsOfNames(IID_NULL,







lpName,


1,

LOCALE_SYSTEM_DEFAULT,


&dispID);


 

if(SUCCEEDED(hr))

{


hr=pDispatch->Invoke(dispID,




IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET |
DISPATCH_METHOD,

&dispparamsNoArgs,
&var,
NULL,
NULL);


}

}
 
if(FAILED(hr))

return _T("");

return CString(var.bstrVal);

}

 


Having implemented the feature of getting an array of
controls, placed on the from, we can easily find the IOleObject interface to the
control if we know it’s name.

When user works with the splitter the RecalcLayout
method is called. We tell the parent control (as you remember the splitter
window is placed on a frame and the frame is placed on our splitter control
window) to delay control reposition. It means that in a few milliseconds the
CControlsSplitterWnd::AdjustControlsToPanes() method will be called.




void
CControlsSplitterWnd::RecalcLayout()

{



CSplitterWnd::RecalcLayout();
GetParentActiveXControl()->DelayControlsReposition();


}

 


The AdjustControlsToPanes method first detects if the
container is in the user mode. If it is, it obtains information about other
controls placed on the same container and invokes a method to reposition the
“pane” controls.




void
CControlsSplitterWnd::AdjustControlsToPanes()

{



CSplitterWindowControlCtrl* pControl=
GetParentActiveXControl();

pControl->m_intFirstPaneSize=pControl->GetFirstPaneSize();
if(!pControl->AmbientUserMode())

return;
 

CControlInfoArray arrayControls;
pControl->GetOtherControlsOnTheContainer(arrayControls);
 
CWnd* pWndPane1=GetPane(0,0);
CWnd* pWndPane2=NULL;
if(pControl->m_bVertical)

pWndPane2=GetPane(0,1);

else

pWndPane2=GetPane(1,0);
 

if(!pWndPane1 || !pWndPane2)

return;
 

PlaceControlOnPane(pControl->m_strFirstControlName,pWndPane1,arrayControls);
PlaceControlOnPane(pControl->m_strSecondControlName,




pWndPane2,
arrayControls);


}

The purpose of the PlaceControlOnPane method is to
adjust the position of the control to the position of the pane in the splitter
window. The method obtains IOleObject interface by the “Name”
extended property, obtains other interfaces needed to do the task and does the
reposition. The reposition process consists of three parts:

  1. First we determine if the “pane”
    control supports IOleInPlaceObject interface. Having this interface we
    obtain the window handle of the “pane” control. Since our
    splitter control uses a lot of windows we can not use the windowless
    controls as panes. Since the control container knows nothing about all this
    tricks and not ready for them, there are cases when our control exists but
    the attached controls do not. In that cases we just postpone the call to the
    AdjustControlsToPanes function.
  2. We obtain client coordinates of the pane window and
    convert them into another ones to call SetObjectRects and SetExtent for the
    control used as pane.
  3. We change extents of the “pane”
    control. We do this twice. First we set extents to zero, because if we do
    not do this the call to SetObjectRects can lead to some “dirty
    places” because the container will not expect this call and will not
    invalidate some portions of its client area. Then we call SetObjectRects
    method to change the position of the control. And finally call SetExtents
    with correct extent of the control.



void
CControlsSplitterWnd::PlaceControlOnPane(LPCTSTR lpstrControlName,









CWnd* pWndPane,
CControlInfoArray&
arrayControls)

{



int nPos=arrayControls.IndexOf(lpstrControlName);
if(nPos!=-1)
{


IOleInPlaceObjectPtr pOleInPlaceObject;
IOleObjectPtr
pOleObject=arrayControls[nPos].m_pOleObject;

pOleObject->QueryInterface(IID_IOleInPlaceObject,





(void**)&pOleInPlaceObject);

 
if(pOleInPlaceObject)
{


HWND hwndControl=NULL;
pOleInPlaceObject->GetWindow(&hwndControl);
if(::IsWindow(hwndControl))
{


CRect rectClient;
pWndPane->GetClientRect(rectClient);
 
//Convert coordinates to the
container’s ones

CRect rectObject(rectClient);
pWndPane->ClientToScreen(rectObject);
IOleClientSitePtr pClientSite;
pOleObject->GetClientSite(&pClientSite);
 
CWnd wndContainer;
wndContainer.m_hWnd=GetContainerWindow(pClientSite);
 
wndContainer.ScreenToClient(rectObject);
wndContainer.m_hWnd=NULL;
 
CSize size;
pOleObject->SetExtent(DVASPECT_CONTENT,&size);
pOleInPlaceObject->SetObjectRects(rectObject,rectObject);
size=rectObject.Size();
CClientDC dc(this);
dc.DPtoHIMETRIC(&size);
pOleObject->SetExtent(DVASPECT_CONTENT,&size);


}
else

GetParentActiveXControl()->DelayControlsReposition();


}

}

}
 

And now the most weird thing. It was not difficult to
make a property page for the control. But to fill comboboxes on the page with
the controls to attach, I needed pointer to the control. Both the control and
the property page are placed in the same DLL, so I can use the pointer without
restrictions. But how can I obtain the pointer? The property page seems can edit
many controls at the same time, but I never saw that any container used this
possibility. Containers use their own property sheets when editing selections.
From within the COlePropertyPage class I can get the array of IDispatch
interfaces to the controls being edited by the page. I can not use
CcmdTarget::FromIDispatch to get the pointer to the control. This method returns
NULL. I failed to find any other quick solution than making the
“_pointer” property of the splitter control.




BOOL
CSplitterWindowControlPropPage::OnInitDialog()

{

COlePropertyPage::OnInitDialog();

 


//
TODO: Add extra initialization here

long pointer=0;
GetPropText(_T("_pointer"),&pointer);
CCmdTarget* pCmdTarget=(CCmdTarget*)pointer;
if(pCmdTarget->IsKindOf(RUNTIME_CLASS(CSplitterWindowControlCtrl)))
{


CSplitterWindowControlCtrl*
pControl=(CSplitterWindowControlCtrl*)pCmdTarget;
 
CControlInfoArray arrayControls;
pControl->GetOtherControlsOnTheContainer(arrayControls);
for(int i=0; i<arrayControls.GetSize(); i++)
{


m_cbxFirstControlName.AddString(arrayControls[i].m_strName);
m_cbxSecondControlName.AddString(arrayControls[i].m_strName);


}

}
 
return FALSE;


}

 


Usual CSplitterWnd class uses CView objects as panes and
these objects are child windows of the splitter window. In our case controls are
not child windows and if we leave the splitter window unchanged we will not be
able to see the tracking line when we change the size of panes. To fix this we
override OnInverTracker for our custom splitter window.




void
CControlsSplitterWnd::OnInvertTracker(const CRect& rect)

{



ASSERT_VALID(this);
ASSERT(!rect.IsRectEmpty());
//ASSERT((GetStyle() & WS_CLIPCHILDREN) == 0);
 
CRect rectContainer(rect);
ClientToScreen(rectContainer);
CWnd wndContainer;
IOleClientSitePtr
pOleClientSite=GetParentActiveXControl()->GetClientSite();

wndContainer.m_hWnd=GetContainerWindow(pOleClientSite);
wndContainer.ScreenToClient(rectContainer);
wndContainer.ScreenToClient(rectContainer);
DWORD dwOldStyle=wndContainer.GetStyle();
::SetWindowLong(wndContainer.m_hWnd,



GWL_STYLE,
dwOldStyle&~WS_CLIPCHILDREN);

 
//
pat-blt without clip children on

CDC* pDC = wndContainer.GetDC();
//
invert the brush pattern (looks just like frame window sizing)

CBrush* pBrush = CDC::GetHalftoneBrush();
HBRUSH hOldBrush = NULL;
if
(pBrush != NULL)


hOldBrush = (HBRUSH)SelectObject(pDC->m_hDC,
pBrush->m_hObject);

 

pDC->PatBlt(rectContainer.left,




rectContainer.top,
rectContainer.Width(),

rectContainer.Height(),

PATINVERT);
 

if
(hOldBrush != NULL)


SelectObject(pDC->m_hDC, hOldBrush);
 

ReleaseDC(pDC);
::SetWindowLong(wndContainer.m_hWnd,GWL_STYLE,dwOldStyle);
wndContainer.m_hWnd=NULL;

}

 


 


ATnT sample



The ATnT sample is a demo program for the HyperHost MFC
extension library created by Dundas Software. It demonstrates a very simple
ActiveX scripting host capable to store forms and script modules in a database.
The database for the program stores information about people, phone numbers used
by the people, phone calls made by the people and how the services are paid. The
sample opens the database, reads the script modules stored in it, runs the
scripts. The scripts define the behavior of the sample.

Libraries used by the ATnT program:

  1. HyperHost (http://www.dundas.com/hyperhost) – provides classes to create
    ActiveX scripting hosts. Contains code for IActiveScriptSite, work with
    Microsoft Script Debugger, script editor with syntax highlighting and
    IntelliSense, work with Microsoft Forms ActiveX designer, plus a lot of code
    to solve many COM tasks in MFC programs.
  2. Ultimate Toolbox (http://www.dundas.com/UTB/default.htm) – set of classes to help MFC
    developers to create robust desktop applications.
  3. ActiveXDocumentContainer – a little bit
    improved code published by myself in Internet. Used to create ActiveX
    document containers in MFC programs with Visual Studio 5. MFC version
    shipped with Visual Studio 6 has its own set of classes to create document
    containers.

In my life I have created two big applications, which
use the script hosting capabilities. Both programs are desktop applications to
access some databases (SQL Server 6.5 and Microsoft Access database). Since the
development of databases and the client programs I did myself I was free in
choosing the way the client applications should be implemented.

My task was not just implementation of the solutions for
some current problems. My applications had to be ready to solve other future
problems. For instance if you are designing application for accountants and want
to sell it to many companies, your program must be ready for all accounting
rules used by the companies. If you are sure that in the companies, which you
want to sell your program to, some power users know VBScript or JScript
languages you might want to choose the ActiveX script hosting technology. In
this case you would build your program as a collection of some COM objects and
expose the ones to the scripting engines. Power users would be able with help of
scripts to modify the behavior of your program if it needs modification. If the
set of objects, exposed by your program, is powerful – the limits of abilities
of your product will be hard to calculate.

Personally myself, I used to associate a group of script
modules with a group of users of my program. When a user logs into the database,
my client programs used to load the scripts and run them. Scripts used to
manipulate the exposed COM objects of my application. In this case the same C++
code was used to provide different program behavior for different groups of
people.

ATnT is a sample of such programs. Of course it is
greatly simplified. It does not understand user groups. To see how it works just
hold the Ctrl key pressed while opening the ATnT.mdb file. Instead of loading
the "Main Form" you will see a project window, which contains all
forms and modules stored in the database. These forms and modules are not the
ones, put by Access. The forms and modules have been created with the ATnT
itself. The picture below demonstrates the project window.


By clicking an item in the tree you can open a form or
module. You can try to play with the program by editing forms and providing some
other code for the Application module. You can also test how the sample tries to
combine the abilities of the script hosting and document containment by pressing
the [W] and [X] buttons on the toolbar.

I will not explain here all details of the application.
If you have some questions concerning the application you might want to drop me
a message. I will just provide some notes.

Form notes:

  1. In the script modules for forms the main object is
    HostForm. It has all other controls placed on the form (buttons, list views)
    as properties.
  2. UserForm – is the container itself. It represents
    the form’s window.
  3. You can access the Application object from the form
    module. You might want to do this to open another form or to get reference
    to already open form or to access the database.

 


Application notes:

  1. Application is the main object. You can open forms,
    ActiveX documents and access the database using this object.
  2. PhoneCallRegistrar is an object, which exposes
    functionality of a device, registering phone calls, to scripts.
    PhoneCallRegistrar is a dynamic property of the Application object. If the
    application had some more devices we could expose them to scripts as
    properties of the Application object and these properties could be able to
    fire events (samples: Thermometer_OnTemperatureChanged,
    Barometer_OnPressureChanged).

In modules you can use the "$$Typelib:"
keyword in comments to let the program know that you want to use constants and
enums from a type library.

Example:

‘$$TypeLib: Excel.Sheet


‘$$TypeLib: COMCTL.ListViewCtrl.1



You can place these comments anywhere you want.

 


Well, the most things are described. If you have any
ideas, information, code snippets use
andrew@skif.net.
If you have questions about COM use the same address. If you are interested in
Dundas products, you might want to go http://www.dundas.com



Regards,


Andrew

Download source – 454 KB

Date Last Updated: March 24, 1999

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read