分类: C/C++
2008-05-31 09:31:26
几乎所有正式一点的C++ Builder程序除了主窗体外都还有从属窗体,有时是对话框,有时是无模式窗口。VCL使得创建和显示从属窗体都易如反掌。但不是所有程序都适于采用无模式窗体,有些程序需要在一个主窗体内显示不同的内容。本文讨论如何将一个从属窗体“寄居”于主窗体中,从属窗体看上去是主窗体的一部分,用户甚至不知道一个从窗体正被显示。图A显示了一个主窗体,其客户区是一个从窗体。
理解子/父联系
这类程序的基本思路是让所有从属窗体都作主窗体的子窗体,这种设计在其他框架(如OWL或MFC)中很常见,但在VCL程序中却不常见。VCL不允许简单地指定一下属性就使一个窗体从属于另一窗体,要做到这一点还得付出点小小的劳动。你得告诉Microsoft 从属窗体是主窗体的子对象,在C++ Builder编程中一般趋于认为窗体是窗口,元件是子对象,实际上从的观点来看,窗体和元件都是窗口。可以将任一窗口
(窗体和元件)指定为另一窗口的子对象,只要你暂时跳出VCL圈子。
更好的“鼠夹”
将一个窗体附属于一个主窗体的一个好处是你可以象设计任何其他从属窗体一样设计子窗体,就是说你创建一个新的窗体,在其上添加元件并书写这个窗体的代码。这样使得设计你的子窗体变得容易,并将所有操纵子窗体的代码集中在一个地方。
程序设计范例
先给出一些程序的背景,程序名叫PARENTING,有一个主窗体,主窗体的顶部和底部各有一个工具条(Tool Bar)和状态条(Status bar),除主窗体外,还有两个子窗体,一个叫TTableForm,用栅格显示ANIMAL.DBF数据表,ANIMAL表是C++ Builder带的数据库样本的一个表。另一个子窗体TChartForm用Tchart显示ANIMAL表。(如果你购买的C++ Builder是标准版则没有数据库元件)你可以通过点击菜单项或工具按钮来选择显示表单还是图形窗体,在你作出选择时,活动窗体被摧毁而被选窗体被显示,子窗体在主窗体的工具条下方、状态条上方的客户区显示,而且随主窗体大小变动而随时保持充满客户区。
重载CreateParams()
如前所述,为让主窗体控制从属窗体,需要将主窗体设为从窗体的“父”,这可以通过重载CreateParams()方法来完成。CreateParams()在VCL创建与窗体联系的窗口时调用,CreateParams()的声明如下:
void __fastcall CreateParams(TCreateParams& Params);
CreateParams()的唯一参数是对一个TCreateParams结构体的引用。在VCL中TCreateParams定义如下:
struct TCreateParams
{
char *Caption;
int Style;
int ExStyle;
int X;
int Y;
int Width;
int Height;
HWND WndParent;
void *Param;
tagWNDCLASSA WindowClass;
char WinClassName[64];
};
此结构包含了Windows创建一个窗口所需的所有信息(如果你曾用API进行Windows编程,你一定能意识到TCreateParams的成员对Windows CREATESTRUCT结构的映射)。在重载CreateParams()时,首先调用基类的CreateParams()方法,然后修改TCreateParams结构的个别成员变量。一个重载过的CreateParams()方法看起来大致如下:
void __fastcall TChartForm::CreateParams(TCreateParams& Params)
{
Tform::CreateParams(Params);
Params.Style=WS_CHILD|WS_CLIPSIBLINGS;
Params.WndParent=MainForm->Handle;
Params.X=0;
Params.Y=0;
Params.Width=MainForm->ClientRect.Right;
Params.Height=MainForm->ClientRect.Bottom;
}
程序的关键是设置TCreateParams结构体的Style和WndParent成员,Style设WS_CHILD 和WS_CLIPSIBLINGS窗口式样,WS_CHILD指定窗口为另一窗口的子窗口。根据定义,一个子窗口是没有标题棒的,设计时的标题棒在Windows在运行时创建窗体会被去掉。WS_CLIPSIBLINGS保证主窗口的不同子窗口在窗体绘制时不互相干涉。很明显,一个子窗口必须得有一个父对象,通过将父窗口句柄指定给TCreateParams结构体WndParent 成员就指定了父对象,正如前面代码所示,WndParent成员设置为主窗体的Handle属性。鉴于指定父对象属性相对直接明了,我不在这个主题上深入更多。
设置子窗体的属性
除了CreateParams()方法中的代码外,还得设置一些子窗体的属性,多数属性可以保留缺省值,但AutoScroll属性应设为false,当然,前提是你的窗体要设计为不必卷动的窗体风格。因为子窗口的大小和位置在CreateParams()中设定,所以Position属性可置为poDefault。Caption和BorderIcon属性将被忽略,故而无需指定。确保BorderStyle置为bsSizeable,还有BorderWidth置为0,如果此二属性设成其它值,子窗体与主窗体会不协调。
窗体的其它元件
很多时候,除了从属窗体外,主窗体还包含别的元件,比如工具条和状态条,此时设置TCreateParams结构体的X、Y、Width和Height成员时得考虑到工具条和状态条,子窗体应与顶部的工具条和底部队状态条协调。因此设置TCreateParams结构体的成员的代码应该是:
Params.X=0;
Params.Y=MainForm->ToolBar->Height+1;
Params.Width=MainForm->ClientRect.Right;
Params.Height=(MainForm->StatusBar->Top-1)-Params.Y;
注意Y设为工具条底部加1,子窗体宽度置为主窗体客户区宽度,高度根据子窗体和状态条的顶部计算,基本上为界于工具条底部和状态条底部之间的主窗体客户区。这些就是使从属窗体“寄居”于主窗体的所有要求,你可能还有其它想在子窗体中实现的特征,我将把这些特征的讨论留到后面。
设置主窗体
主窗体也得进行设置以控制其“收留”的子窗体。首先需要将子窗体从自动创建窗体列表中去掉,需要时再创建它们。如果不从列表中去掉,则当你的应用程序启动时它们会自动显示。你还需要一个变量来跟踪当前的活动子窗体,在主窗体的Public区声明如下变量,Tform * ActiveChild; ActiveChild是公有的,因为子窗体要访问这个变量。我马上会演示如何使用这一变量。现在来书写显示子窗体的代码,先看下面程序行,然后我来解释。
void __fastcall TMainForm::Chart1Click(Tobject *Sender)
{
if(ActiveChild)
delete ActiveChild;
TChartForm * form=new TChartForm(this);
ActiveChild=form;
[NextPage]
form->Show();
Chart1->Checked=true;
Table1->Checked=false;
}
此方法是主窗体的菜单项的OnClick处理句柄,猜的没错,这是在显示TChartForm子窗体。首先检查ActiveChild变量是否非0,如果有激活动子窗体ActiveChild将非0。如果ActiveChild不为0,删除此变量关联的指针以摧毁活动子窗体,否则程序会将子窗体一个接一个地堆叠在一起。接着创建一个TChartForm类的实例,将new操作返回到指针赋予ActiveChild变量,这样ActiveChild总是包含一个指向当前子窗体的指针。最后调用Sow()方法显示子窗体。最后两行代码确保代表表单或图形显示的菜单项显示一个选中标记。为完成ActiveChild变量的讨论,我得带你回到子窗体单元一会儿。每个子窗体都含有一个如下的OnClose事件的事件句柄:
void __fastcall TChartForm::FormClose(Tobject *Sender,
TCloseAction &Action)
{
MainForm->ActiveChild=0;
MainForm->Chart1->Checked=false;
Action=caFree;
}
注意当窗体被摧毁时,主窗体的ActiveChild被置为0,并将与子窗体关联的菜单项置为未选中。Action参数置为caFree,以通知VCL释放与此窗体关联的内存。你或许会疑惑为何FormClose句柄包含上面最后两行。答案是每个子窗体都有一个Close按钮用来关闭窗体,如果用Close按钮来关闭窗体,就需要释放内存和uncheck菜单项。
额外特征
例子程序至少还有一个尚未讨论的特征,就是如果子窗体比主窗体大,要重新调整主窗体大小以容纳子窗体。这些语句放在子窗体的CreateParams()方法中。之前我展示过一个简单的CreateParams()例子,但没有放进调整主窗体大小的语句。列表B中有完整的CreateParams()方法,和先前展示唯一不同的是包含以下语句:
if(Width>MainForm->ClientWidth)
MainForm->ClientWidth=Width;
if(Height>(MainForm->StatusBar->Top-MainForm->ToolBar->Height))
MainForm->ClientHeight=Height+
MainForm->ToolBar->Height+
MainForm->StatusBar->Height ;
这几条语句检查子窗体的宽度是否大于主窗体的ClientWidth属性,如果是,主窗体的ClientWidth置为子窗体的宽度。余下的几行作的是同样的事情,只不过针对的是主窗体的客户区高度而已。这些语句的结果是主窗体总是被调整到能完全容纳被显示的子窗体。范例程序还考虑了主窗体调整大小时的情况。如果主窗口大小有变化,子窗体大小也必须变化以进行充满主窗体的客户区。以下语句演示了主窗体的OnResize事件句柄:
void __fastcall TMainForm::FormResize(Tobject *Sender)
{
if(ActiveChild)
{
ActiveChild->Width=ClientRect.Right;
ActiveChild->Height=(MainForm->StatusBar->Top-1)-
ActiveChild->Top;
}
}
这些语句相当直接,无需我逐条解释。注意首先检查ActiveChild变量确保非0(即指向某子窗体),很明显如果当前没有激活任何子窗体,在OnResize中就什么都不用干。其余语句是CreateParams()中看到语句的变种,只是简单地计算子窗体的新尺寸并设置相应的Width和Height属性。
结语
列表A包含了例子程序主窗体的代码。列表B示出了TChartForm单元的源码。头文件都没有给出因为没有什么有意义的语句,也未给出TTableForm单元的语句,因为与ChatForm单元类似。你可以在例子程序。将子窗体“寄居”于主窗体提供了一种清晰的替代MDI的方法,对那些只能以无模式窗体形式向用户显示数据的程序也是一种替代,使用子窗体允许你使用窗体设计器设计你的从窗口,并将操纵子窗体的代码放在一个地方。
列表A:MAINU.CPP
#include
#pragma hdrstop
#include \"MainU.h\"
#include \"ChartU.h\"
#include \"TableU.h\"
#pragma resource \"*.dfm\"
TMainForm *MainForm;
__fastcall TMainForm::TMainForm(Tcomponent* Owner)
: Tform(Owner)
{
//清0以防包含随机数
ActiveChild=0;
//打开数据表
Table->Active=true;
}
void __fastcall TMainForm::Table1Click(Tobject *Sender)
{
if(Table1->Checked) return;
if(ActiveChild){
delete ActiveChild;
ActiveChild=0;
}
TTableForm *form=new TTableForm(this);
//将DBGrid的DBGrid::DataSource属性赋给数据源
form->DBGrid->DataSource=DataSource;
//跟踪活动子窗体
Active=form;
form->Show();
Table1->Checked=true;
Chart1->Checked=false;
}
void __fastcall TMainForm::Chart1Click(Tobject *Sender)
{
if(Chart1->Checked) return;
if(ActiveChild){
delete ActiveChild;
ActiveChild=0;
}
TChartForm *form=new TChartForm(this);
Active=form;
form->Show();
Chart1->Checked=true;
Table1->Checked=false;
}
void __fastcall TMainForm::FormResize(Tobject *Sender)
{
if(ActiveChild){
ActiveChild->Width=ClientRect.Right;
ActiveChild->Height=(MainForm->StatusBar->Top-1)-
ActiveChild->Top;
}
}
列表B:CHARTU.CPP
#include
#pragma hdrstop
#include \"ChartU.h\"
#include \"MainU.h\"
#pragma resource \"*.dfm\"
TChartForm *ChartForm;
__fastcall TChartForm::TChartForm(Tcomponent* Owner)
: Tform(Owner)
{
}
void __fastcall TChartForm::CreateParams(TCreateParams& Params)
{
//调用基类CreateParams方法
Tform::CreateParams(Params);
//子窗口类型
Params.Style=WS_CHILD|WS_CLIPSIBLINGS;
//设置父为主窗体
Params.WndParent=MainForm->Handle;
Params.X=0;
if(Width>MainForm->ClientWidth)
MainForm->ClientWidth=Width;
if(Height>(MainForm->StatusBar->Top-MainForm->ToolBar->Height))
[NextPage]
MainForm->ClientHeight=Height+
MainForm->ToolBar->Height+
MainForm->StatusBar->Height;
Params.Y=MainForm->ToolBar->Height+1;
Params.Width=MainForm->ClientRect.Right;
Params.Height=(MainForm->StatusBar->Top-1)-Params.Y;
}
void __fastcall TChartForm::FormClose(Tobject *Sender,
TCloseAction &Action)
{
MainForm->ActiveChild=0;
MainForm->Chart1->Checked=false;
Action=caFree;
}
void __fastcall TChartForm::CloseBtnClick(Tobject *Sender)
{
Close();
}