Introduction
After publishing my article about splitter ActiveX control with MFC I have got some
messages asking about the possibility of implementing the same control with ATL. Quite
recently I had some free time and decided to try myself in ATL. The results of my experiments
are here.
Since ATL does not have any splitter window class like MFC I had to implement some
functionality of such window myself. I did not try to create anything cool so if you
find my implementation ugly I would be happy to get your code. Here I will just provide
some small comments since nothing very important has been added. You might want to
download my previous article from here to get more
comments about the source code.
How to use the control
You can read my previous article mentioned above to see how to use the control in ActiveX
control containers. This version works the same way as the previous one. From the user's
point of view this control just has different property names. The SplitterPercentPosition
property describes the percent (not absolute) position of the splitter bar. Possible
values of this property vary from 0 to 100. The control has been implemented to support
windowless activation, although it still uses hidden window to process WM_TIMER messages.
Why I needed a timer I will describe later. If you know how to avoid the need of the
window, I would be glad if you sent me your solution.
This version supports IPerPropertyBrowsing interface so you can make control bindings
in the Properties window of the container. Although the only container which works ok with
this interface (my own opinion) is VB6 (I did not try VB5).
Implementation
Since I had to draw the splitter window myself I had to port some code from MFC. In
Splitter.cpp file you will find several functions to do some graphics. Several methods
are used to calculate the coordinates of panes when control is resized. There is nothing
tricky in the code and moreover it is not very good.
To track the splitter bar I had to process mouse messages like WM_LBUTTONDOWN,
WM_MOUSEMOVE and WM_LBUTTONUP. To set appropriate mouse cursor when user moves mouse
over the splitter bar I had to process WM_SETCURSOR message. While processing these
messages I did not rely on the parameter describing mouse position. Instead I used my
own method GetMouseCursorPosInContainerCoords to avoid differences when the control
is placed in container, which does not support windowless activation.
Methods like GetExtendedName and GetOtherControlsOnTheContainer were just ported from
the previous version. The differences are minor and not important. There is a very little
difference in the PlaceControlOnPane method. But it is very important since it produces
better results. I should use it in the MFC version too. When changing the position of a
pane control, instead of calling SetObectRects, I use now OnPosRectChange method of the
container's site object. Here is a portion of code, which does this.
void CSplitter::PlaceControlOnPane(const BSTR strControlName,const RECT& rectPane)
{
int nPos=m_arrayControls.IndexOf(strControlName);
if(nPos!=-1)
{
IOleObjectPtr pOleObject=m_arrayControls[nPos].m_pOleObject;
IOleClientSitePtr pSite;
pOleObject->GetClientSite(&pSite);
IOleInPlaceSitePtr pIPSite=pSite;
pIPSite->OnPosRectChange(&rectPane);
SIZE size;
size.cx=rectPane.right-rectPane.left;
size.cy=rectPane.bottom-rectPane.top;
HWND hwndContainer=GetContainerWindow(m_spClientSite);
HDC hdc=::GetDC(hwndContainer);
DPtoHIMETRIC(hdc, &size);
::ReleaseDC(hwndContainer,hdc);
pOleObject->SetExtent(DVASPECT_CONTENT,&size);
}
}
When it is needed to place the child controls into panes - AdjustControlsToPanes is called.
But this method is called indirectly from DelayAdjustControlsToPanes. I needed this because
the control container does not know about my tricks and can perform some changes when the
splitter control has already placed the child controls in its panes. Moreover, in some
situations the child controls might not even exist, for instance when the container is
loading and creating the controls. When reposition is needed we just inform the splitter
control that we want to do the reposition by calling DelayAdjustControlsToPanes. At
appropriate time the reposition will be performed by calling AdjustControlsToPanes. From
experiments I have found that appropriate time can be a processing WM_TIMER message since
it has very little priority and all transition effects are gone. Our splitter control can
be activated without a window, so to process WM_TIMER messages I created a hidden one, which
the control stores in the m_wndHidden member. The DelayAdjustControlsToPanes method does
nothing more than setting timer for the hidden window.
void CSplitter::DelayAdjustControlsToPanes()
{
if(!m_wndHidden.m_hWnd)
{
RECT rect;
rect.left=rect.top=rect.right=rect.bottom=0;
m_wndHidden.Create(GetContainerWindow(m_spClientSite),rect);
}
::SetTimer(m_wndHidden.m_hWnd,1,55,NULL);
}
The hidden window processes WM_TIMER message by calling the actual reposition method.
LRESULT CHiddenWindow::OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
KillTimer(wParam);
if(m_pSplitter)
m_pSplitter->AdjustControlsToPanes();
bHandled=TRUE;
return 0;
}
Having implemented IPerPropertyBrowsing interface, besides improving the binding
capability, I have also solved the problem of passing the list of controls on the form
to the property page, thus making the ugly _pointer property from the previous MFC
version useless.
if(m_nObjects>0)
{
CComQIPtr<IPerPropertyBrowsing, &IID_IPerPropertyBrowsing> pPPB(m_ppUnk[0]);
CALPOLESTR* pCALPOLESTR;
CADWORD* pCADWORD;
pCALPOLESTR=(CALPOLESTR*)CoTaskMemAlloc(sizeof(CALPOLESTR));
pCADWORD=(CADWORD*)CoTaskMemAlloc(sizeof(CADWORD));
if(SUCCEEDED(pPPB->GetPredefinedStrings(DISPID_FIRST_CONTROL_NAME,
pCALPOLESTR,
pCADWORD)))
{
for(int i=0; i<pcalpolestr->cElems; i++)
{
TCHAR* lpstrElem=OLE2T(pCALPOLESTR->pElems[i]);
CoTaskMemFree(pCALPOLESTR->pElems[i]);
SendDlgItemMessage(IDC_COMBO_FIRST,
CB_ADDSTRING,
0,
LPARAM(lpstrElem));
SendDlgItemMessage(IDC_COMBO_SECOND,
CB_ADDSTRING,
0,
LPARAM(lpstrElem));
}
CoTaskMemFree(pCALPOLESTR->pElems);
CoTaskMemFree(pCADWORD->pElems);
}
if(pCADWORD)
CoTaskMemFree(pCADWORD);
if(pCALPOLESTR)
CoTaskMemFree(pCALPOLESTR);
}
</pcalpolestr->
Here is how the interface has been implemented on the control. Notice please that this
implementation is bad, because it does not check any success on memory allocation.
According to the documentation, in any failure I should have freed all successfully
allocated memory and return E_FAIL. I did not do this because it happens VERY rarely
and just to save my time. You might check MFC implementation of this interface for
COleControl class.
STDMETHODIMP CSplitter::GetPredefinedStrings(DISPID dispID, CALPOLESTR *pCaStringsOut,CADWORD *pCaCookiesOut)
{
ATLTRACE2(atlTraceControls, 2,
_T("IPerPropertyBrowsingImpl::GetPredefinedStrings\n"));
if (pCaStringsOut == NULL || pCaCookiesOut == NULL)
return E_POINTER;
pCaStringsOut->cElems = 0;
pCaStringsOut->pElems = NULL;
pCaCookiesOut->cElems = 0;
pCaCookiesOut->pElems = NULL;
if(dispID==DISPID_FIRST_CONTROL_NAME || dispID==DISPID_SECOND_CONTROL_NAME)
{
GetOtherControlsOnTheContainer();
if(m_arrayControls.size()>0)
{
pCaStringsOut->cElems=pCaCookiesOut->cElems=m_arrayControls.size();
pCaStringsOut->pElems=(LPOLESTR*)CoTaskMemAlloc(pCaStringsOut->cElems*sizeof(LPOLESTR));
pCaCookiesOut->pElems=(DWORD*)CoTaskMemAlloc(pCaCookiesOut->cElems*sizeof(DWORD));
for(int i=0; i<m_arrayControls.size(); i++)
{
pCaCookiesOut->pElems[i]=i;
CControlInfo& ci=m_arrayControls[i];
pCaStringsOut->pElems[i]=(LPOLESTR)CoTaskMemAlloc((ci.m_strName.Length()+1)*sizeof(OLECHAR));
wcscpy(pCaStringsOut->pElems[i],ci.m_strName.m_str);
}
}
}
return S_OK;
}
STDMETHODIMP CSplitter::GetPredefinedValue(DISPID dispID, DWORD dwCookie, VARIANT* pVarOut)
{
if(dispID==DISPID_FIRST_CONTROL_NAME || dispID==DISPID_SECOND_CONTROL_NAME)
{
if(dwCookie>=0 && dwCookie<m_arrayControls.size())
{
V_VT(pVarOut)=VT_BSTR;
V_BSTR(pVarOut)=m_arrayControls[dwCookie].m_strName.Copy();
return S_OK;
}
}
return E_FAIL;
}
I hope you will like this control. If you have any improvements please send me you code.