t e m p o r a l 
 d o o r w a y 

An Aggregate Scroll Panel Component

 

Introduction

One of the most difficult challenges for the component developer lies in creating components that, like TPanel, allow other components to be dropped on them. The reason this is difficult isn't because it is hard, but because it is a) undocumented and b) relies on the largely undocumented VCL component streaming system. This article will take care of (a) and some of (b); for more on (b) see the article on Aggregate Streamable Components.

The component described in this article is a simple one, which allows you to drop other controls onto a scrollable panel. Almost all of the code supports the ability of the component to save which controls belong in it. It is based on the techniques shown in the now out of print "Developing Custom Delphi 3 Components" by Konopka.

The Scroll Panel Component in the IDE

Technique

The key thing to understand is how the streaming system needs to deal with the dropped components.

The VCL system itself normally parents a component to the component on which it is dropped. However, the owner of the component remains the form not the parent. When the form is saved, it is converted to a DFM file format through the process of streaming. A number of complex events occur during streaming, but the most important thing to remember is that only those components owned by the form are streamed into the DFM.

Unfortunately, with regard to the control we are developing, we are parenting dropped components to a child of our component (the drop site). Keep in mind that anything owned by the form can be deleted and can also be modified in the object inspector. To prevent the drop site from being available for deletion (which would be a catastrophe) we can't allow it to be owned by the form - it has to be owned by our component. But since only components owned by the form are streamed, the drop site will not be streamed, its children will, and the resulting DFM will be corrupted.

Getting around the problem requires implementing the following functions...

  • DYNAMIC void __fastcall GetChildren(TGetChildProc theChildDecisionFunction,TComponent *theRoot);

  • void __fastcall DefineProperties(TFiler *theFiler);

  • void __fastcall WriteChildComponentList(TWriter *theWriter);

  • void __fastcall ReadChildComponentList(TReader *theReader);

  • void __fastcall Loaded(void);

These functions are all overrides of functions provided by the streaming system interface of TPersistent.

  • GetChildren() provides an opportunity for a component to identify components which are its children byh any means desired. We use it to pretend that the dropped controls are children of this component so that the streaming system knows they should be "inside" it. We have to do that because the drop site isn't visible to the streaming system.

  • DefineProperties() lets us stream out properties that are not published - in this case, a list of the names of the components which are children of the drop site. We can use that later when the component is streamed in, to fix up the parentage of the child components to the drop site.

  • The WriteChildComponentList() is called by the DefineProperty() call in DefineProperties(). It copies out the content of the list of dropped component names to the stream.

  • The WriteChildComponentList() is called by the DefineProperty() call in DefineProperties(). It copies in the content of the list of dropped component names from the stream.

  • Loaded() is invoked after everything is streamed in and it looks at the list of component names, finds the named components, and reparents them to the drop site.

Let's look at the implementation in detail....

The Header

//---------------------------------------------------------------------------

#ifndef MCScrollPanelH
#define MCScrollPanelH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class PACKAGE TMCScrollPanel : public TPanel
{
   private:

      TStringList *myChildComponentList;

   protected:

      TPanel *myDropSite;
      TScrollBar *myVerticalScrollBar;

      void __fastcall VerticalScrollBarChange(TObject *Sender);

      int __fastcall GetPanelHeight(void){return myDropSite->Height;};

      void __fastcall SetPanelHeight(int thePanelHeight)
      {
         myVerticalScrollBar->Max = thePanelHeight;
         myVerticalScrollBar->PageSize = Height;

         myDropSite->Height = thePanelHeight;
      };

      DYNAMIC void __fastcall GetChildren(TGetChildProc theChildDecisionFunction,TComponent *theRoot);
      void __fastcall DefineProperties(TFiler *theFiler);
      void __fastcall WriteChildComponentList(TWriter *theWriter);
      void __fastcall ReadChildComponentList(TReader *theReader);
      void __fastcall Loaded(void);

   public:

      __fastcall TMCScrollPanel(TComponent* Owner);
      __fastcall ~TMCScrollPanel(void);

   __published:

      __property int PanelHeight = {read=GetPanelHeight,write=SetPanelHeight};
};
//---------------------------------------------------------------------------
#endif

The Implementation

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "MCScrollPanel.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//

static inline void ValidCtrCheck(TMCScrollPanel *)
{
   new TMCScrollPanel(NULL);
}
//---------------------------------------------------------------------------
void __fastcall TMCScrollPanel::VerticalScrollBarChange(TObject *Sender)
{
   myDropSite->Top = -myVerticalScrollBar->Position;
}
//---------------------------------------------------------------------------
__fastcall TMCScrollPanel::TMCScrollPanel(TComponent* Owner)
   : TPanel(Owner)
{
   myChildComponentList = new TStringList;

   Parent = dynamic_cast<TWinControl *>(Owner); // Dimensions for subcomponents don't work correctly unless this is done
   
   Left = 0;
   Top = 0;
   Width = 320;
   Height = 240;
   TabOrder = 0;

   myVerticalScrollBar = new TScrollBar(this);
   myVerticalScrollBar->Parent = this;

      myVerticalScrollBar->Kind = sbVertical;
      myVerticalScrollBar->Align = alRight;
      myVerticalScrollBar->OnChange = VerticalScrollBarChange;
      myVerticalScrollBar->PageSize = Height;

   myDropSite = new TPanel(this);
   myDropSite->Parent = this;

      myDropSite->Width = ClientWidth - myVerticalScrollBar->Width - 1;
      myDropSite->Height = 240;
      myDropSite->Caption = "";
}
//---------------------------------------------------------------------------
__fastcall TMCScrollPanel::~TMCScrollPanel(void)
{
   delete myChildComponentList;
}
//---------------------------------------------------------------------------
void __fastcall TMCScrollPanel::Loaded(void)
{
   for (int Index = 0; Index < myChildComponentList->Count; Index++)
   {
      TComponent *Child = GetParentForm(this)->FindComponent(myChildComponentList->Strings[Index]);
      if (Child != NULL) ((TControl *) Child)->Parent = myDropSite;
   };
}
//---------------------------------------------------------------------------
void __fastcall TMCScrollPanel::GetChildren(TGetChildProc theChildDecisionFunction, TComponent *theRoot)
{
   TPanel::GetChildren(theChildDecisionFunction,theRoot);
   TCustomForm *Form = GetParentForm(this);

   for (int Index = 0; Index < myDropSite->ControlCount; Index++)
   {
      TControl *Control = myDropSite->Controls[Index];
      if (Control->Owner == Form) theChildDecisionFunction(Control);
   };
}
//---------------------------------------------------------------------------
void __fastcall TMCScrollPanel::DefineProperties(TFiler *theFiler)
{
   theFiler->DefineProperty("ChildComponentList",ReadChildComponentList,WriteChildComponentList,true);
}
//---------------------------------------------------------------------------
void __fastcall TMCScrollPanel::ReadChildComponentList(TReader *theReader)
{
   myChildComponentList->Clear();

   theReader->ReadListBegin();

      while (!theReader->EndOfList())
      {
         myChildComponentList->Add(theReader->ReadIdent());
      };

   theReader->ReadListEnd();
}
//---------------------------------------------------------------------------
void __fastcall TMCScrollPanel::WriteChildComponentList(TWriter *theWriter)
{
   theWriter->WriteListBegin();

      for (int Index = 0; Index < myDropSite->ControlCount; Index++)
      {
         theWriter->WriteIdent(myDropSite->Controls[Index]->Name);
      };

   theWriter->WriteListEnd();
}
//---------------------------------------------------------------------------
namespace Mcscrollpanel
{
   void __fastcall PACKAGE Register()
   {
      TComponentClass classes[1] = {__classid(TMCScrollPanel)};
      RegisterComponents("MCTestComponent", classes, 0);
   }
}
//---------------------------------------------------------------------------

Conclusion

Follow the model above, and it's not that hard to make a "panel-like" component. Enjoy it! Extend it!

Copyright © 2004 by Mark Cashman (unless otherwise indicated), All Rights Reserved