翻譯:构建基于VCL的应用程序框架

(本文章是翻譯自:http://www.devexpress.com/Support/BestPractices/VCL/SAP/
發表在2004 11,12月開發高手, 但雜誌上有刪節
轉載請與作者聯系)

在这篇文章里,我们将讨论一种通过Borland VCL 更好地构建Windows客户端应用程序的方法。最后,我们将产生一个运行库以及一个能帮我们轻松实现模块和用户界面分离的应用程序的样例

 

本文分为两部分:

一、   在开发简单应用程序框架部分,我们没有采用Developer Express library,所以那些非Developer Express的客户的VCL开发者可以使用;

进一步,采用Developer Express libraries来改善应用程序框架。如果要编译运行这个程序,你必须安装下列的Developer Express libraries





ExpressNavBar Library Ver 1.x

ExpressBars Library Ver 5.x

ExpressQuantumGrid Suite Ver 4.x

ExpressPrinting Library Ver 2.x

本文摘要分类:

n         一种更好的程序设计方法——在你的程序主窗口中使用 Frames(框架)

n         在模块继承中使用Frames(框架)

n         使用原生的VCL Actions

n         要用多长时间才能将一个菜单和工具栏的功能添加到另外一个地方呢?这个程序框架能帮你很快完成,因为你需要做的只是简单的在一个地方修改代码即可

n         在运行时设置 ExpressNavBar 控件

n         在应用程序框架中使用ExpressBars ExpressGrid

n         使用ExpressPrinting 来增加打印功能

 

我们从先一个简单的任务开始,逐步开发我们的应用程序框架,然后,每一步都将加入新功能,这样能让代码的变化尽量保持简单直接。整个过程分为6步,你可以下载和编译每一步的代码。

 

目录

一、   为何要关心应用程序框架?

二、   创建最简单的模块独立应用程序框架;

1.     应用程序界面布局;

2.     Actions 介绍;

三、   使用Developer Express 控件改善应用程序框架;

1.           增加Developer Express Navbar运行库;

2.           增加Developer Express Bars运行库;

3.           创建Developer Express Grid 模块;

4.           为应用程序框架增加打印功能;

 

 

一、  为何我要关心应用程序框架?

Borland公司引进了许多实用的类到VCL 中,从而能够帮助我们快速完善地建立 Windows From 应用程序。那么,为什么我们还要在我们的代码里面多加入一层处理代码呢?在很长时间内我都没有想过这个问题。

 

大概十年前,我加入了一个开发客户销售管理系统的公司,当时,他们用VB+MS SQL开发系统,然而,在我加入公司一月后,Borland公司发布了Delphi1,这是第一个真正面向对象的RAD工具。当公司决定采用Delphi 来开发下一个项目时,我们都感到十分兴奋,我们决定所有模块都共享一个Application,这样能够更好共享彼此的代码。当时公司的大部分人,包括我在内都是刚刚从大学毕业的年轻人,我们投入地写了一段段不同的代码。一切进展得很顺利,直到我们将代码放到一个真实的环境中测试。突然,我们发现Bug修改不如所想的那般容易。修正一个模块Bug的时候,又会在其它模块中产生几个新的bug,开发新的模块,必须花费我们越来越多的时间以保证能与原来的模块兼容,许多模块代码逻辑混合在菜单/工具栏下面,成了我们的噩梦,没有人能够理解那一大堆的“Case/Switch”、加载系统时候的If判断等等类似的操作。为了完成这个项目,我们只能投入大部分自己的私人时间。在项目完成的时候(大约花费了一年时间),每个人都非常疲惫。大部分开发人员离开公司去度假,而且再也没有回来。

 

如果说是因为我们没有使用应用程序框架才导致了那么多问题,那是在撒谎。其实在项目实施过程中存在大量的错误。我估计在我们的开发过程中,几乎犯下了所有可能犯的错误。我们也没有尝试改进我们的代码。“自动测试”——是什么东西?在那个时候,我们根本没有听过它。而且我们根本没有做任何测试。所有人只关心他负责的那部分模块,代码共享简直是乱七八糟。我还可以继续举些例子,但我想你已经明白我要表达什么了。

 

不管怎样,我知道没有使用一个应用程序框架是我们开发中遇到的主要问题之一,而这个问题其实是非常容易解决的。当项目临近结束的时候,我花了点时间检查了大部分模块的代码。我很诧异,每个模块的大部分功能需求相当的简单,即使实现它们的方式五花八门。

 

在下一次我参与一个类似的项目,我督促大家花几天时间创建了一个非常简单的应用程序框架。这个框架允许:

n         通过一行代码就能增加删除一个模块

n         在模块之间共享函数库

n         统一的 菜单/工具栏 用法

 

 开发这一层代码所花费的时间,在开发新功能时候将被成倍的节省回来。从那时开始,在我开发的大部分程序,都将这个应用程序模块略微修改加进程序中。当我在Developer Express 公司任职的时候,我浏览了其它同事的一些代码,觉得一些实现方法很好,同时也有一部分代码实现得不够理想。在这里的一些项目让我不由地想起了第一次写大型项目的情景。有时,一些同事反对引入继承到模块、菜单/工具栏实现中来。我想这篇文章会带给他们很大的帮助。对于那些已经写了自己的应用程序框架,并且已经成功使用的人,相信也能从本文中得到一些启发和有用的代码。我们Developer Express公司的人很乐于听到通过这篇文章让你的生活更愉快轻松了(我们在Developer Express 工作的一个主要原因就是帮助开发者)

 

二、  创建最简单的模块独立应用程序框架

第一步,我们将创建一个能够独立构建模块的应用程序框架。主窗体并不知道它将显示什么内容,而模块也不清楚它将被显示在哪里。这样允许你在不同程序的不同部分都能使用该模块。而且可以和主程序并行使用测试模块,如此一来,你和你的团队将感到你们的程序非常棒,这很多时候只是一个心理上的感觉而已,但实际却非常有效。

 

1.    应用程序界面布局

MS Outlook 第一次引入了如下的经典SDI应用程序布局:


菜单和工具栏用蓝色部分标注,导航面板用黄色表示,状态栏用绿色表示,而工作区则灰色表示。

    让我们根据这个布局创建一个应用程序,它将包括两个模块:模块1和模块2

 

在这个程序中,我们将使用一个标准的菜单,一个停靠在左边包含一个Listbox控件(用于空出导航区的空间)Panel控件。为了创建工作区(灰色部分),我们将增加一个Panel控件,将它的dock属性设置为充满所有空间。最后,我们在导航区和工作区之间放置一个splitter控件。为了简化这次工作,我们将不在程序中使用任务栏。

 

现在,我们的目标是要创建一个独立功能模块的应用程序框架,开发者用一行代码就能增加或移除其中的每一个模块。

 

基于我们的框架的程序的所有模块都将从TfrmCustomModule 继承(直接或间接的)。而TfrmCustomModule这个类是继承自delphi自带的TFrame 类。主窗体将只是知道TfrmCustomModule类,而对它的子类毫不了解。

 

在当前这一步里,我们将不会放置太多的功能到CustomModule类中,你将会看到只是加了一个onDestroy的事件。在稍后我们注册模块时将需要它。

unit CustomModule;

 

interface

 

uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs;

type

  TfrmCustomModule = class(TFrame)

  private

    FOnDestroy: TNotifyEvent;

  public 

    destructor Destroy; override;

    property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy;

  end;

  TfrmCustomModuleClass = class of TfrmCustomModule;

implementation

{$R *.dfm}

{ TfrmCustomModule } 

destructor TfrmCustomModule.Destroy;

begin 

  if Assigned(OnDestroy) then

    OnDestroy(self);

  inherited

end

 

end

  

在程序启动时就创建所有模块是一个不好的习惯。所以,我们需要创建一个注册模块的单元。命名为 modules.pas 它包含了两个类: TModuleInfo TModuleInfoManager

 

   TModuleInfo 类包含了模块的名字和模块中类相关的属性信息。我们将使用模块的名字来表示其包含功能,当我们需要显示那个模块时,我们将创建对应的模块/框架 类的实例。

 

TModuleInfoManager 类包含一个我们程序中所有注册模块的注册列表你能通过它的 RegisterModule 方法来注册一个新的模块。 ShowModule 方法则将会显示模块到一个指定的windows控件中Count Items 属性则让我们可以检查所有已经注册了的模块。

 

你能使用全局函数 ModuleInfoManager 来访问 TModuleInfoManager 这个对象下面是modules.pas 单元的声明部分。你可以下载 Step1 源程序来参考它的实现部分。

unit Modules;

 

interface 

uses Classes, Controls, CustomModule;

 

type 

 

//关于模块包含的信息

TModuleInfo = class 

private 

  FModuleClass: TfrmCustomModuleClass;

  FModule: TfrmCustomModule;

  FName: string;

  function GetActive: Boolean;

protected 

 //创建模块实例

  procedure CreateModule;

   //销毁模块实例

  procedure DestroyModule;

public 

  constructor Create(const AName: string; AModuleClass: TfrmCustomModuleClass);

  destructor Destroy; override;

   //隐藏模块

  procedure Hide;

   //显示模块到指定的控件中

  procedure Show(AParent: TWinControl);

  //如果当然模块是激活的就返回 true

  property Active: Boolean read GetActive;

  property Module: TfrmCustomModule read FModule;

  property Name: string read FName;

end

 

//管理模块的类信息

TModuleInfoManager = class 

private 

  FModuleList: TList;

  FActiveModuleInfo: TModuleInfo;

  function GetCount: Integer;

  function GetItem(Index: Integer): TModuleInfo;

public 

  constructor Create;

  destructor Destroy; override;

 

  //通过模块的名字返回对应模块的信息

  function GetModuleInfoByName(const AName: string): TModuleInfo;

  //注册模块到管理器

  procedure RegisterModule(const AName: string; AModuleClass: TfrmCustomModuleClass);

  //显示模块到指定控件中 

  procedure ShowModule(const AName: string; AParent: TWinControl);

  //当前激活模块的信息

  property ActiveModuleInfo: TModuleInfo read FActiveModuleInfo;

  //返回当前已注册模块的数量

  property Count: Integer read GetCount;

  property Items[Index: Integer]: TModuleInfo read GetItem; default;

end;

 

//返回全局TModuleInfoManager对象的实例

function ModuleInfoManager: TModuleInfoManager;

 

现在我们需要设置我们的菜单系统和导航控件,让最终用户可以操纵我们的模块

constructor TfrmMain.Create(AOwner: TComponent);

begin 

  inherited Create(AOwner);

  //安装菜单和导航控件

  RegisterModules;

  //在启动时显示第一个模块

  if ModuleInfoManager.Count > 0 then 

    ShowModule(ModuleInfoManager[0].Name);

end;

 

procedure TfrmMain.RegisterModules;

var

  I: Integer;

  AMenuItem: TMenuItem;

begin 

  //遍历所有模块

  for I := 0 to ModuleInfoManager.Count - 1 do 

  begin

   //将每一项增加到列表框中

    lblNavigation.Items.Add(ModuleInfoManager[I].Name);

    // 增加子菜单项

    AMenuItem := TMenuItem.Create(self);

    mView.Add(AMenuItem); 

    AMenuItem.Caption := ModuleInfoManager[I].Name;

    //使用 tag 来识别各个模块

    AMenuItem.Tag := I;

    AMenuItem.OnClick := mViewClick;

  end;

end;

 

procedure TfrmMain.ShowModule(const AName: string);

begin 

  //在显示模块时,锁住当前主窗口的刷新

  LockWindowUpdate(Handle);

  try 

    ModuleInfoManager.ShowModule(AName, pnlWorkingArea);

  finally 

    //刷新主窗口

    LockWindowUpdate(0);

    RedrawWindow(Handle, nil, 0, RDW_ERASE or RDW_FRAME or RDW_INVALIDATE or RDW_ALLCHILDREN);

  end;

end;

 

procedure TfrmMain.lblNavigationClick(Sender: TObject);

begin 

  if lblNavigation.ItemIndex < 0 then exit;

  //显示模块

  ShowModule(lblNavigation.Items[lblNavigation.ItemIndex]);

end;

 

procedure TfrmMain.mViewClick(Sender: TObject);

begin 

//显示模块 

  ShowModule(lblNavigation.Items[TMenuItem(Sender).Tag]);

end;

 

最后一步是创建一个新模块并且注册到我们的应用程序框架中

unit module1;

 

interface 

uses 

   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

   Dialogs, custommodule, StdCtrls, modules;

 

type 

  //新模块必须从custom module类继承

  TfrmModule1 = class(TfrmCustomModule)

    Label1: TLabel;

  private 

  public 

  end;

 

implementation 

{$R *.dfm} 

 

initialization 

  //在应用程序框架中注册模块包含的类

  ModuleInfoManager.RegisterModule('Module1', TfrmModule1);

End.

 

摘要

如您所见,我们仅仅用了少量代码就完成了我们的创建一个模块独立的应用程序框架的目标。


当然,这个应用程序框架并没拥有很多的特性,在实际环境中使用的时候,还必须扩充它的功能。例如在我写过的大多数程序中,必须给模块提供安全认证的功能。根据用户权限来决定每个最终用户能访问或者不能访问特定的模块。 在模块的注册部分, 能很简单的引入这个功能。 在这里,基模块没有实现任何功能,但实际中这也并不常见。在大多数实际案例中,你必须在基模块中就直接引入一些特定的功能。在这里,你只要记住,你在基模块引入的任何功能,都将自动引入到其它的子模块中。 所以你必须事先规划好模块的继承方案。 例如CustomModule -> CustomDBModule -> CustomGridModule… 等等当然每个新的模块都会增加新的功能。

1.        Actions 介绍

在上一步,我们实现了一个允许创建独立模块的简单应用程序框架,现在我们将考虑如何新增功能到模块中,首要问题是,我们要解决如何在主窗体UI和业务逻辑之间添加一个新的层(layer)

 

换言之,我们希望在主窗体上有一个菜单和工具栏,菜单里的项目和工具栏上的按钮的VisibleEnable以及其它属性必须能够反映当前显示模块的业务逻辑状态,菜单项目和工具栏项目并不知道当前显示模块的细节,而模块也根本不知道菜单和工具栏的存在。我们期望能够改变界面上不同的UI控件而不需要修改模块中的代码。如:把标准菜单替改成Developer Express ExpressBar,反之亦然。同时我们还希望能够在没有创建程序主窗口的情况下使用测试引擎来测试模块的功能

 

基本上,我们需要在UI和业务逻辑代码之间引入一个层(layer)或多个层(layer)的代码, 我们把它叫做Actions(layer)

 

VCL自己有一个原生的Action,但是在这里,我们似乎不能直接使用它,因为它会破环我们应用程序中模块的独立性。然而,我们可以扩展VCL Actions 来实现我们需要的功能

 

通过VCL Actoins 来实现Action模块非常简单,首先,我们创建一个DataMoudle,拖一个TActionManager 控件到它上面,在TActionManager Excute事件下面写代码同时在DataMoudle 中也添加几行代码。要添加新的Action,你只需要在ActionManager 控件中新建一个Action就可,然后要绑定的UI控件的Action属性设置为对应的action组件就可。 注意,你使用的UI对象必须支持Actions。当然, 标准的VCLDeveloper Express的所有控件都支持Action技术。

unit dmActions;

 

interface 

uses 

   SysUtils, Classes, AppEvnts, XPStyleActnCtrls, ActnList, ActnMan;

 

type 

  TdmAppActions = class(TDataModule)

    ActionManager: TActionManager;

    Action1: TAction;

    Action2: TAction;

    Action3: TAction;

    procedure ActionManagerExecute(Action: TBasicAction;

      var Handled: Boolean);

  private 

    function GetActionCount: Integer;

    function GetAction(Index: Integer): TBasicAction;

   // 如果不指定Action事件,绑定的UI控件将是Disable状态

   // 最简单的解决方法就是给它付一个假的事件

      procedure DoFakeVCLAction(Sender: TObject);

    procedure FakeVCLActions;

  public 

    constructor Create(AOwner: TComponent); override;

   //返回 action 的数目

    property ActionCount: Integer read GetActionCount;

    property Actions[Index: Integer]: TBasicAction read GetAction;

  end;

 

var 

  dmAppActions: TdmAppActions;

//返回Actions类的全局实例

function AppActions: TdmAppActions;

 

implementation 

uses Forms, Modules;

{$R *.dfm} 

 

//返回Actions类的全局实例

function AppActions: TdmAppActions;

begin 

  if(dmAppActions = nil) then 

    dmAppActions := TdmAppActions.Create(Application);

  Result := dmAppActions;

end;

 

{ TdmAppActions } 

constructor TdmAppActions.Create(AOwner: TComponent);

begin 

  inherited Create(AOwner);

  FakeVCLActions;

end;

 

procedure TdmAppActions.FakeVCLActions;

var 

  I: Integer;

begin 

  for I := 0 to ActionCount - 1 do 

    Actions[I].OnExecute := DoFakeVCLAction;

end;

 

procedure TdmAppActions.DoFakeVCLAction(Sender: TObject);

begin 

//不执行任何代码

end;

 

function TdmAppActions.GetActionCount: Integer;

begin 

  Result := ActionManager.ActionCount;

end;

 

function TdmAppActions.GetAction(Index: Integer): TBasicAction;

begin 

  Result := ActionManager.Actions[Index];

end;

 

//处理ActionManager控件的Execute 事件

procedure TdmAppActions.ActionManagerExecute(Action: TBasicAction;

  var Handled: Boolean);

begin 

  //调用当前显示模块的ExecuteAction方法

  if (ModuleInfoManager.ActiveModuleInfo <> nil) then 

    Handled := ModuleInfoManager.ActiveModuleInfo.Module.ExecuteAction(Action);

end;

 

end.

 

最后 我们必须增加一些功能到CustomModule类。为了注册支持Actions的功能,你需要调用RegisterAction方法,RegisterAction方法覆盖了内部默认的RegisterAction方法。调用IsActionSupported方法将会返回是否是支持Action。 我们将覆盖改写UpdateActionsState方法来改变action EnabledisDown 属性。

下面是CustomModule.pas单元的声明部分:

unit CustomModule;

 

interface 

uses 

   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

   Dialogs, ActnList;

 

type 

  TActionNotification = procedure(Action: TBasicAction) of object;

 

  TfrmCustomModule = class(TFrame)

  private 

   //支持actions的列表

    FSupportedActionList: TList;

    FOnDestroy: TNotifyEvent;

  //返回Action的事件

    function GetNotificationByAction(Action: TBasicAction): TActionNotification;

  protected 

    //注册支持的action

    procedure RegisterAction(const Action: TBasicAction; ANotification: TActionNotification);

    //继承这个类的子类必须重写覆盖掉这个方法以便可注册支持的actions 

    procedure RegisterActions; virtual;

  public 

    constructor Create(AOwner: TComponent); override;

    destructor Destroy; override;

   //重写父类TfrmaeExecuteAction行为

    function ExecuteAction(Action: TBasicAction): Boolean; override;

    //如果当前模块支持action则返回 true

    function IsActionSupported(Action: TBasicAction): Boolean;

    //在模块被激活时,所有支持actions将显示出来,而不支持的则隐藏起来

    procedure UpdateActionsVisibility; virtual;

    //更新action状态

    procedure UpdateActionsState; virtual;

    property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy;

  end;

 

  TfrmCustomModuleClass = class of TfrmCustomModule;

 

下面是一个在TfrmCustomModule 子类中使用Actions的简单例子:

unit module1;

 

interface 

uses 

   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

   Dialogs, custommodule, StdCtrls, modules;

 

type 

  //新模快必须从定制模块中继承

  TfrmModule1 = class(TfrmCustomModule)

    Label1: TLabel;

    CheckBox1: TCheckBox;

    Label2: TLabel;

    Edit1: TEdit;

    procedure CheckBox1Click(Sender: TObject);

  private 

   //处理Action1

    procedure DoAction1(Action: TBasicAction);

  protected 

    //注册支持的actions

    procedure RegisterActions; override;

  public 

   // 更新模块状态 

    procedure UpdateActionsState; override;

  end;

 

implementation 

uses dmActions;

{$R *.dfm} 

 

{ TfrmModule1 } 

procedure TfrmModule1.RegisterActions;

begin 

  inherited RegisterActions;

  //注册Action1到支持actions列表

  RegisterAction(AppActions.Action1, DoAction1);

end;

 

procedure TfrmModule1.UpdateActionsState;

begin 

  inherited UpdateActionsState;

  //如果checkbo1没有选中,则让action1为可操作

  AppActions.Action1.Enabled := not CheckBox1.Checked;

end;

 

//显示Action1被执行的次数到Edit1

procedure TfrmModule1.DoAction1(Action: TBasicAction);

begin 

  Edit1.Text := IntToStr(StrToInt(Edit1.Text) + 1);

end;

 

procedure TfrmModule1.CheckBox1Click(Sender: TObject);

begin 

  UpdateActionsState;

end;

 

initialization 

  // 在我们的应用程序框架中注册对应的类 

  ModuleInfoManager.RegisterModule('Module1', TfrmModule1, 0, 0);

End.

 

摘要


 


一、  使用Developer Express 控件改善应用程序框架

 

1.        增加Developer Express Navbar

在上一步,我们创建了第一版本的应用程序框架,看来它运行得很好。然而如果现在就给我们的老板或者客户演示,他们会嘲笑我们的简陋。在一个潮流的应用程序中采用ListBox作为导航条并不是一个最好的选择,用户希望拥有一个和当前技术发展水平相当的UI外表。Developer提供的ExpressNavBar控件提供十多种不同的显示风格,将让你的应用程序拥有一个全新时髦的外表。

 

NavBar非常容易使用,可以帮助你在设计期内简化控件设计工作,但是不幸的是,主模块不知道我们要引入到系统的其它模块的细节(而且我们希望只是简单地增加/移除一行代码就可控制模块),所以我们必须放弃使用IDE拖拉控件这种设计习惯,改用代码来完成所有事情。

 

首先,我们得给应用程序框架引入新的特性。NavBar控件是以类别(categories)来区分的。所以,我们必须在我们的模块注册类中引入类别(categories)。此外,为了彻底改善应用程序的外观,我们还想在Navbar控件表示的菜单项和群组中加入图片。所以我们还必须在注册类中引入 Image属性。

 

下面是我们必须在Modules.pas单元引入的改变:

增加TCategoryInfo

//类别包含的信息

TCategoryInfo = class

private 

  FName: string;

  FImageIndex: Integer;

  function GetIndex: Integer;

public 

  constructor Create(AName: string; AImageIndex: Integer);

  property Index: Integer read GetIndex;

  property ImageIndex: Integer read FImageIndex;

  property Name: string read FName;

end;

 

ModuleInfo类增加CategoryImageIndex属性。 这样做我们能够保存category所属的模块的信息,同时还保存NavBar控件显示图片对应的索引(index), 下面是TModuleInfo类改变的地方:

//模块中包含的信息

TModuleInfo = class 

private 

  FCategory: TCategoryInfo;

  FImageIndex: Integer;

public 

  constructor Create(const AName: string;

                     AModuleClass: TfrmCustomModuleClass;

                     ACategory: TCategoryInfo;

                     AImageIndex: Integer = -1);

        

  property Category: TCategoryInfo read FCategory;

  property ImageIndex: Integer read FImageIndex;

end;

    

增加CategoryCountCategories属性和AddCategoryGetCategoryByName方法到TModuleInfoManager类中,这样我们能够将类别(categories)增加到我们的框架中,并在需要的时候找回它们。下一步我们会用到这个功能。

//管理模块信息的类 

TModuleInfoManager = class 

private 

        

  FCategoryList: TList;

  function GetCategoryCount: Integer;

  function GetCategory(Index: Integer): TCategoryInfo;

public 

  //增加新的类别

  procedure AddCategory(const AName: string; ImageIndex: Integer);

  通过名字返回 CategoryInfo 对象

  function GetCategoryByName(const Name: string): TCategoryInfo;

  //注册模块到管理器中

  procedure RegisterModule(const AName: string; AModuleClass: TfrmCustomModuleClass;

    ACategory: TCategoryInfo = nil; AImageIndex: Integer = -1);

  //返回类别的数量

  property CategoryCount: Integer read GetCategoryCount;

  property Categories[Index: Integer]: TCategoryInfo read GetCategory;

end;

 

下一步要做的就是在我们的应用程序框架中通过模块的注册来创建NavBar控件组别(groups),条目(items)和链接(links)。我们必须改变主窗体单元的RegisterModules方法。

procedure TfrmMain.RegisterModules;

var 

  I: Integer;

  ANavBarGroup: TdxNavBarGroup;

  ANavBarItem: TdxNavBarItem;

begin 

  //遍历所有的类别

  for I := 0 to ModuleInfoManager.CategoryCount - 1 do 

  begin 

   增加NavBar组别

    ANavBarGroup := NavBar.Groups.Add;

    //设置NavBar组别的标题

    ANavBarGroup.Caption := ModuleInfoManager.Categories[I].Name;

   //设置NavBar组别德图片

    ANavBarGroup.LargeImageIndex := ModuleInfoManager.Categories[I].ImageIndex;

   //显示图片到NavBar组别中

    ANavBarGroup.UseSmallImages := False;

  end;

  //遍历所有模块

  for I := 0 to ModuleInfoManager.Count - 1 do

  begin 

   //增加新的项目到navBar

    ANavBarItem := NavBar.Items.Add;

   //设置NavBar项目的标题

    ANavBarItem.Caption := ModuleInfoManager[I].Name;

   //设置NavBar项目的图片索引

    ANavBarItem.SmallImageIndex := ModuleInfoManager[I].ImageIndex;

   使用tag来标示模块

    ANavBarItem.Tag := I;

   // 增加项目到对应的navBar组别中

    NavBar.Groups[ModuleInfoManager[I].Category.Index].CreateLink(ANavBarItem);

  end;

end;

 

下面是Navbar控件对应单击事件的处理代码:

procedure TfrmMain.NavBarLinkClick(Sender: TObject;

  ALink: TdxNavBarItemLink);

begin 

  //显示指定模块

  ShowModule(ModuleInfoManager.Items[ALink.Item.Tag].Name);

end;

 

最后一步是注册Categories 到应用程序框架中。可以这样做,比如,在主窗口的initialzation部分注册:

initialization 

  ModuleInfoManager.AddCategory('Category 1', 0);

 

摘要

使用了Developer Express NavBar控件作为导航条后,我们的程序看起来比以前美观多了。


1.        增加Developer Express Bars

现在是用其它东西来替代老式的标准菜单和工具栏的时候了。因为我们定义了Actions层,所以这不是什么大问题。不管我们选择哪个控件,我们只需要修改住窗体即可。下面我将展示如何移植使用Developer Express ExpressBars控件。

 

现在的框架中使用了VCL Actions技术,所以,在改用其它的菜单、工具栏系统之前,你必须检查新的控件是否支持VCL Actions 技术。ExpressBars 支持VCL Actions

 

拖放一个TdxBarManager控件到主窗口上,使用TdxBarConverter来将原来的标准主菜单代替成ExpressBars的主菜单。

 

模块之间使用TdxBarListItem类来互相沟通。我们需要修改RegisterModules方法。

procedure TfrmMain.RegisterModules;

var 

  I: Integer;

begin 

//遍历所有模块 

  for I := 0 to ModuleInfoManager.Count - 1 do 

  begin 

   //增加项目到Bar列表中

    barListItem.Items.Add(ModuleInfoManager[I].Name)

  end;

end;

 

下面是BarListItemClick事件的代码:

procedure TfrmMain.barListItemClick(Sender: TObject);

begin 

  //显示代码 

  ShowModule(ModuleInfoManager.Items[barListItem.ItemIndex]);

end;

 

最后一步是根据我们的需要来创建工具栏和上面的按钮。在工具栏上放置按钮以及在设计环境中绑定到相应的VCL Actions。过程就是这样。

 

摘要

如您所见,工作很简单,我们只是修改了主窗体模块中的代码就完成了需要的改动。你在自己程序中是如何移植菜单和工具栏到其它风格的控件呢?我猜这肯定是一个痛苦的过程。


1.        创建Developer Express Grid 模块

我们通过采用XtraNavBar XtraBars来替换标准控件的方法,大大改善了我们应用程序的外观。现在该是我们考虑根据模块内容来改进框架的时候了。在你的应用程序里面,你的“最终”模块不一定是直接从CustomerModule继承。大部分的程序一般都会有几个包含很多对象,记录的列表。 通常我们用网格(grid)来表示这些记录, 而这部分可能是程序中重要的组成部分。你必须编写Grid的处理代码,举例来说:根据定制窗体的需要显示或隐藏相应的Grid栏目, 等等。当然,如果要对每个包含grid的模块都编写这样的代码,那太没意思了。我们将创建一个CustomGridModule模块。 它将包含Developer Express ExpressQuantumGrid。 就像grid Action一样,我们将引入一个导出Action。 尽管我们在基模块就加入了导出action的支持,但这些action在默认状态是被禁止的。所以,实际上继承的Grid模块必须覆盖重写这些方法来重新使它们变为可用。

 

为了引入Grid模块,我们需要在主窗体,Action数据模块中作些修改,并且创建一个新的模块: CustomGridModule它将继承自 CustomModule

 

我们必须修改主窗体和Action数据模块,给它们增加Actions ExpressBars项目,并且链接 Action到对应的ExpressBars项目,就如我们之前做的。过程几乎完全一样。

 

一件更有趣的任务是给CustomModule增加导出Actions的支持。导出Actions对所有的模块是可见的,但默认是禁用的,为了使它们可用,继承的模块必须覆写两个方法: SupportedExportTypes DoExport。下面是在CustomModule实现导出Action支持的代码:

  TExportType = (etHTML, etXML, etXLS, etText);

  TExportTypes = set of TExportType;

  TfrmCustomModule = class(TFrame)

  protected 

    //为了注册actions支持,子类必须覆盖这个方法

    procedure RegisterActions; virtual;

//根据需要导出的类型做对应的导出

    procedure DoExport(AExportType: TExportType; const AFileName: string); virtual;

    //返回相应支持的导出类型

    function SupportedExportTypes: TExportTypes; virtual;

  end;

 

CustomGridModule中实现导出Actions是相当简单的:

procedure TfrmCustomGridModule.DoExport(AExportType: TExportType; const AFileName: string);

begin 

  case AExportType of 

    etHTML: ExportGrid4ToHTML(AFileName,Grid);

    etXML: ExportGrid4ToXML(AFileName, Grid);

    etXLS: ExportGrid4ToExcel(AFileName, Grid);

    etText: ExportGrid4ToText(AFileName, Grid);

  end; 

end;

 

function TfrmCustomGridModule.SupportedExportTypes: TExportTypes;

begin 

  Result := [etHTML, etXML, etXLS, etText];

end;

 

为了实现Grid Actions,将使用TcxGridOperationHelper类,你能在随产品发布的cxGridUIHelper.pas文件中找到它。它为不同的视图实现了标准的网格操作。

constructor TfrmCustomGridModule.Create(AOwner: TComponent);

begin 

  inherited Create(AOwner);

  FGridOperationHelper := TcxGridOperationHelper.Create(self);

  FGridOperationHelper.Grid := Grid;

  FGridOperationHelper.OnUpdateOperations := DoGridUpdateOperations;

  FGridOperationHelper.OnCustomizationFormVisibleChanged := DoGridUpdateOperations;

end;

 

procedure TfrmCustomGridModule.RegisterActions;

begin 

  inherited RegisterActions;

  RegisterAction(AppActions.actionGridGrouping, DoActionGridGrouping);

        

  RegisterAction(AppActions.actionGridColumnsCustomization, DoActionGridColumnsCustomization);

end;

 

procedure TfrmCustomGridModule.UpdateActionsState;

begin 

  inherited UpdateActionsState;

  AppActions.actionGridGrouping.Enabled := FGridOperationHelper.IsOperationEnabled[GROP_SHOWGROUPINGPANEL];

  AppActions.actionGridGrouping.Checked := FGridOperationHelper.IsOperationShowing[GROP_SHOWGROUPINGPANEL];

  AppActions.actionGridColumnsCustomization.Enabled := FGridOperationHelper.IsOperationEnabled[GROP_SHOWCOLUMNCUSTOMIZING];

  AppActions.actionGridColumnsCustomization.Checked := FGridOperationHelper.IsOperationShowing[GROP_SHOWCOLUMNCUSTOMIZING];

end;

 

function TfrmCustomGridModule.FocusedView: TcxCustomGridView;

begin 

  Result := Grid.FocusedView;

end;

 

procedure TfrmCustomGridModule.DoActionGridGrouping(Action: TBasicAction);

begin 

  FGridOperationHelper.DoShowGroupingPanel(not FGridOperationHelper.IsGroupingPanelShowing);

end;

 

procedure TfrmCustomGridModule.DoActionGridColumnsCustomization(Action: TBasicAction);

begin 

  FGridOperationHelper.DoShowColumnCustomizing(not FGridOperationHelper.IsColumnsCustomizingShowing);

end;

 

procedure TfrmCustomGridModule.DoGridUpdateOperations(Sender: TObject);

begin 

  UpdateActionsState;

end;

 

摘要

在这步,我们在应用程序框架中创建一个基础列表模块


它包含了从父网格模块继承的额外模块。你必须在你的开发环境安装Developer Express ExpressNavBar控件, ExpressBars ExpressQuantumGrid支持库才能正常的编译和运行这个程序。

 

1.        使用ExpressPrinting 来增加打印功能

最后一个要加到我们的应用程序框架的是打印功能。我们编写导出Actions一样,我们给基模块引入和实现打印Actions

 

在增加打印支持ActionsCustomModule后,我们多了三个额外的虚拟保护方法: HasPrinting DoPrint DoPreview

 

这些方法将在CustomGridModule中被覆盖以增加打印ExpressQuantumGrid的功能。从ExpressPrinting面板拖一个TdxComponentPrinter控件到CustomGridModule然后为在模块中的Grid创建一个报表链接。有了这个控件,给增加CustomGridModule打印支持是一个相当简单的任务。

//如果模块支持打印就返回true

function TfrmCustomGridModule.HasPrinting: Boolean;

begin 

  Result := True;

end;

 

procedure TfrmCustomGridModule.DoPrint;

begin 

  printerLinkGrid.Print(False, nil);

end;

 

procedure TfrmCustomGridModule.DoPreview;

begin 

  printerLinkGrid.Preview(True);

end;

 

摘要

在这一步,我们给应用程序框架引入了打印支持并且在Grid模块的基类实现。你必须在你的工作环境中安装Developer Express ExpressNavBar控件、ExpresPrinting ExpressBars ExpressQuantumGrid支持库才可以编译和运行这个演示程序(demo)

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值