您好,欢迎来到UU财经。
搜索
您的当前位置:首页用组件开发一致的界面

用组件开发一致的界面

来源:UU财经




11章用组件开发一致的界面

什么能算是好的界面,标准是非常主观的。许多商业应用程序都跟随着WinTel标准:灰色的按钮和控件,白色的背景。对商务程序来说,这可能是个不错的主意,因为通过多年的熟悉使得这个界面在某种程度上较为舒服,但这是个好的界面吗?AlanCooperVisualBasic之父,他建议“通过坚持使他们(MicrosoftApple)各自的开发者群体遵守既定的方针,他们偷偷摸摸地阻止了来自应用者群体的革新。”[Cooper,212]Cooper认为,“我并不鼓吹忽略界面风格方面的指导,从而导致界面出现混乱。我仅仅认为应该像参议员看待说客那样来看待对界面风格的指导,而绝不能像司机服从于交警那样。立法者知道说客想要削减某项经费,
但说客并非来自于持有客观态度的第三方。”[Cooper212] 在所有的条件下都是最好的界面可能并不存在,即使在一定的条件下,界面的设计仍然是高度主观的。如果你能开发出像图11.1所示的新RealPlayer那样的界面,而且符合你的目的,那就很好了。如果你不擅长创建独一无二或非常有趣的图形用户界面,而且并没

的而言,也许较为熟悉的风格可以避免使用方面的障碍。
有雇佣图形设计者的预算,那么可能会开发出与WinTel风格类似的应用程序。对于商业目

www.taodocs.co

11.1 RealPlayer 8使用了一些漂亮的图形按钮,并进行了视觉人类

工程学方面的尝试。还可以选用卡通标志和斑马条纹等外表

只有一个问题不是主观的,它也是本章的主题,那就是界面应该是一致、连贯、完全的。不一致、不连贯、不完全,不考虑界面的风格对用户来说是不可容忍的。第11章示范
了一些技术,可用于简化开发并确保一致性,包括如何使用定制组件、组件模板和窗体继承,以提供一致、连贯而完全的应用程序。



11 章用组件开发一致的界面

265

11.1 定制组件

创建定制组件很有趣,而且定制组件也很有用。首先,显而易见的理由是可以重用已有的对象,并封装新的或增强的特性;其次,它可以提供一致的效用。无须绘制组件时保证相同的尺寸、风格、字体、颜色或措辞,可以对组件进行定制以确保这些目标。

11.1.1 定制组件的三个C

定制组件的三个C是一致性、连贯性和完备性。一致性意味着组件在你的应用程序和其他地方的行为是一致的。

一致性(Consistency

组件每次都表现出相同的行为和初始状态,才能提供一致性。对组件的行为或状态进行一次编程,则所有的组件实例都具有一致的外观和行为。

一致性并不追求数量,注意到这一点是很重要的。定制组件无须进行大量的修改,即可提供一致性。即使组件只是重载了缺省的大小或形状,创建一个定制组件也可确保一致性。有两个直接的方法可以做到这一点。您可以子类化所有的需要微小修改的组件然后再

连贯性(Coherency
安装;或者快速地创建组件模板,这更容易一些(参考11.2节“创建组件模板”)。

www.taodocs.co完备性(Completeness

不一致、不完全的应用程序看起来是不合逻辑且不正确的,这样必定是不完备的。如果应用程序不被用户群体所接受,也不能说是完备的。完备性度量了应用程序是否执行了所要求的任务、结果是否正确、应用程序是否具有合理的容错级别。

如果程序给出正确却不合时宜的回答,也是不完备的。而迅速的提供错误的结果,仍然是错误的。如果程序的行为毫无规律、不一致、或不合逻辑,那么该程序是失败的。即使程序有相应的用户群体,仍然可能失败,因为用户群体可以拒绝使用该程序,或恶意共谋使用该程序提供错误的或不合适宜的结果。

为什么组件帮助你走向胜利

组件是对象。每个对象都属于某个类。这意味着有一组代码需要测试、调试和扩展。如果一个类已经是完美的,那么每个实例都不会出错。这样如果类满足了3C标准,那么
类的每个实例都会满足该标准。




266

Delphi 6 应用开发指南

注意:“大而复杂的软件系统需要设计师,以便开发者能够朝着共同的目标前

进。”[Jacobsen,Booch,andRumbaugh62]。设计师是这样的人,他形成解决方案的概念并向程序员说清设计意图。即便开始时的进度比通常慢,也要把事情做正确,这将会节省大量金钱和思考的时间,防止在最后才发现出轨。

没有经验、缺乏技术的管理者可能认为编写组件接近于消磨时间,但这是面向对象的程序设计。以非面向对象的方法去使用面向对象工具是一个错误。使用Delphi编写结构化程序可以很快地到达beta版,这在短期内常常会使管理者高兴,但可能使得处于beta版的时间较长。您的程序可能永远都脱离不了beta版。迅速得到错误的答案,仍然是错误的。

无论是否能确认管理层会花大笔金钱来确保成功,都可以采取一些防御措施。从许多

功能正确的组件来创建程序,可以尽可能少写代码而又能提高程序的正确性。

11.1.2 重分解

重分解是采取小的增量式改变的过程。组件可以一步就写出来,创建全新而独一无二

的东西,这样做代价昂贵、风险较大而且浪费时间;或者我们可以采取小的步骤,分层实

现各种能力,这样就不那么昂贵,风险较低而且快速。设计师的关键作用之一——找到

么很清楚,许多情况下最好的选择就是对组件进行小的修改,将增量式的改变分层添加到

已有的组件中。

为示范进行这种小的修改所需代码的合理数量,设计了下面的组件:
和减少冒险。如果没有设计师,必须由程序员来完成该工作。管理者喜欢快速而廉价。那

www.taodocs.co//Copyright (c) 2000. All Rights Reserved.

//by Software Conceptions, Inc. Okemos, MI USA (800) 471-50 //Written by Paul Kimmel
interface
uses
Windows,Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls,DBCtrls;

type
TNavigatorButtonSet= ( nbsFull, nbsPartial ); TDBShortNavigator = class(TDBNavigator)

private
{ Private declarations }
FButtonSet : TNavigatorButtonSet;



protected procedure SetButtonSet(const Value: TNavigatorButtonSet);



11 章用组件开发一致的界面

267

{Protected declarations }
public
{Public declarations }
published
{Published declarations }
propertyButtonSet : TNavigatorButtonSet read FButtonSet write SetButtonSet;
end;
procedureRegister;
implementation

procedureRegister;
begin
RegisterComponents('PKTools',[TDBShortNavigator]); end;

{TDBShortNavigator }
procedureTDBShortNavigator.SetButtonSet(const Value:

FULL_SET = [nbFirst, nbPrior, nbNext, nbLast, nbInsert, nbDelete,nbEdit, nbPost, nbCancel, nbRefresh];TNavigatorButtonSet);
const

www.taodocs.coif(FButtonSet = Value ) then exit;
FButtonSet:= Value;
VisibleButtons:= SETS[FButtonSet];
end;
end.

TDBShortNavigator继承了TDBNavigator,添加了一个ButtonSet特性。将该特性在nbsFullnbsPartial之间切换,即可显示所有的导航按钮或仅仅显示基本的四个按钮(如图11.2所示)。


11.2 TDBShortNavigator可以快速地在显示部分或全部导航按钮之间切换



268

Delphi 6 应用开发指南

很显然该组件并没有多少代码。而这正是我们所需要的。小的改变快速、便宜而且可靠。然而,有人可能认为这是不重要或不相关的。由混沌理论可知,即使蛾的翅膀振动一下也可能影响到很远的地方。那么我们可以考虑蛾困在Mark型计算机的中继转换开关中的情况。据说是COBOL的发明者GraceHopper杜撰了bug这个词,现在整个工业界都在使用它,连世界历史上最重大的媒体事件之一Y2K问题也是它的标志。

并不是说上述的导航器子类在历史上也能有这样幸运的角色;轶事能被记录本来就是戏剧性的。小的事件能够发挥值得记载的作用,并具有相当的影响。千里之行,始于足下,高级的系统正是由小块的优质代码所组成的。

11.1.3 小的改变有什么好处

除了本节开始所描述的好处之外,还包括快速、廉价、可靠等等,这些好处都是由小的、增量式的改变得到的。TDBShortNavigator这样的组件有利于代码的收敛。收敛是指所有该算法的代码都聚集在一起;如果没有最好的代码,那么一个实例的代码是次好的,代码多于一个实例是较差的。发散是指出现算法的多个副本;这是最坏的情况。

当子类化TDBNavigator这样的组件来进行小的改变时,可以促进代码的收敛。改变可 因此只有一个代码段需要测试、调试和扩展。见按钮数量的所有代码都包含在同一个地方。

如果要为按钮定义三个状态,可以在同一地方快速而有效地修改代码。

注意:您可能听说过比其他的程序员多产一个数量级的程序员。也就是他个

人的产量是其他人的十倍。这怎么可能呢?很显然一个人不可能比程序员的
www.taodocs.co

我们提到过,修改并不重要。重要的是修改表示了什么。像TDBShortNavigator这样的组件就表明了编写内聚代码的倾向。这种累积效应往往分布在程序员的职业生涯或工程的生命周期中。

11.1.4 采取好的策略

编写收敛的代码,或编写的代码只具有算法的单一副本,这是一个策略。这可能是成为高产开发者的最佳途径之一。有两个习惯可促进采用该策略,并逐渐使之成为一种第二天性。首先:考虑多次修改你的代码。我们知道谚语“天才是1%的灵感加99%的汗水”,这意味着当一个人思考解决方案时,只有出现了非常好的机会,灵感才能发挥作用。其次:当发现重复出现的代码时,立即把涉及到的算法编写为过程。

随着实践的进行,这种迭代式的修改会变得更加自然,如果随时进行修改,也更容易 发挥作用。反之,如果等到程序完成之后才进行修改工作,可能会遇到困难。管理者和其他程序员可能不想立即投入很多时间进行修改,而这时代码已经相互纠缠在一起以至于小的修改也可能引起代码的混乱。



11 章用组件开发一致的界面

269

11.1.5 组件化

我们就继续讨论上一节的问题,如果你发现自己正在编写处理组件内部数据的代码,那么最好子类化组件以封装新的行为。组件化的规则是:如果代码涉及到组件内部的数据,例如组件所拥有的对象列表,那么代码实际上描述了组件的行为。对象的行为就是方法。

通过将外部的、隐式的算法提升为方法,可以使代码在类的层次上趋向于收敛。类不一定是组件;任何表示类行为的代码都应该作为方法合并到类中。

有三个地方可以方便地重用代码。可以子类化包括组件在内的任何类,以重用代码。可以定义组件模板,这是Delphi新近添加的功能,利用该方法可以很容易地在一个或多个组件中重用新的行为;而且能够创建窗体或框架模板,这样就可以重用整个窗体或框架及其所包含的组件。

11.2 创建组件模板

首先将组件添加到数据模块或窗体,设置它们的特性,创建事件处理程序,并编写代码。然后选定一个或多个组件,从Component菜单里选择CreateComponent Template菜单项。所有被选中的组件、事件处理程序、以及相关的代码,都添加到了VCL面板的Template

码在内的各个组件。将模板添加到窗体或数据模块后,可以修改其位置、大小和特性等,就像是分别添加的一样。
属性页上。选择该组件模板并将其拖动到任意的窗体或数据模块上,即可重新创建包括代

www.taodocs.co就是其图标必须是24×24像素位图,与其他组件的图标类似。

11.3 Component Template Information对话框

11.2.1 定义组件模板



编写基本的组件相当容易。创建组件模板甚至更容易。定义组件模板的步骤与创建窗体基本相同。增加组件,修改其特性,并编写事件特性。像其他单元一样,对窗体(或数



270

Delphi 6 应用开发指南

据模块)进行单元测试。工作完成后,就可以创建组件模板,选择组件,并将其添加到组

件模板。

注意:Delphi组件模板存储在DelphiBin目录下的二进制文件Delphi.dct

中。Windows错误地把该文件和FoxProDataBase Container 文件类型关联在一起。

考虑第3章的Edit菜单。您可以花费一些时间来查找所有的Windows消息,并在应用程序中对Edit菜单编写SendMessage代码;而下一次在另一个程序里创建Edit菜单时,您还得做同样的事情。

从菜单资源模板重新创建菜单

您可以选择使用InsertTemplate对话框中的Edit菜单,如图11.4所示。使用MenuDesigner上下文菜单添加一个菜单的步骤如下:


www.taodocs.co

11.4 Insert Template 对话框可以从存储的

资源中重新创建菜单。但并不包括代码

1.从VCLStandard属性页上选择MainMenu组件。

2.双击MainMenu图标向窗体添加菜单。

3.右击MainMenu组件,显示TMainMenu组件的组件编辑器菜单,并选择MenuDesigner菜单项(如图11.5所示)。这样可以打开菜单编辑器,如图11.4的背景所示。

4.右击鼠标,打开菜单编辑器的上下文菜单,并选择InsertFrom Template菜单项,

5.从Insert Template 对话框中(如图11.4所示),双击Edit菜单模板,以添加编辑 如图11.6所示。

菜单。



11 章用组件开发一致的界面

271

这就是所有的工作。上述五个步骤将把TMenuItem组件添加到窗体类定义的开头。使用菜单资源模板需要对OnClick事件方法重新编写代码。更好的方法是使用组件模板,这可以包括代码。

11.5 点击TMainMenu组件的组件编辑器菜单上的MenuDesigner菜单项


www.taodocs.co

11.6 菜单设计器的上下文菜单,对于管理菜单资源模板很方便

创建并安装菜单组件模板

组件模板是Delphi新增的功能。使用菜单资源模板存储菜单必须重新编写代码,而使用组件模板则不必如此。还使用上一节提到的Edit菜单,它是在第3章中实现的。我们现在把整个菜单和代码都保存为组件模板。

1.对Edit菜单编写代码(使用第3章的例子,或步骤后列出的代码)使控件呈现正 确的行为。

2.用代码测试Edit菜单后,选择包含Edit菜单的TMainMenu 组件并点击Component | Create Component Template 菜单项。
procedureTForm1.Edit1Click(Sender: TObject);
begin



272

Delphi 6 应用开发指南

//CanUndo test
Undo1.Enabled:= Boolean(SendMessage(
Screen.ActiveControl.Handle,
EM_CANUNDO,0, 0 ));
end;

procedureTForm1.Copy1Click(Sender: TObject);
begin
//Copy menu
SendMessage(Screen.ActiveControl.Handle, WM_COPY, 0, 0 ); end;

procedureTForm1.Cut1Click(Sender: TObject);
begin
//Cut menu
SendMessage(Screen.ActiveControl.Handle, WM_CUT, 0, 0 ); end;

// Paste menu SendMessage( Screen.ActiveControl.Handle, WM_PASTE, 0,0 );
procedure TForm1.Paste1Click(Sender: TObject);
begin

www.taodocs.co//select all text
SendMessage(Screen.ActiveControl.Handle, EM_SETSEL, 0, -1 );
end;

procedureTForm1.Undo1Click(Sender: TObject);
begin
//Undo menu
SendMessage(Screen.ActiveControl.Handle, WM_UNDO, 0, 0 ); end;

该代码与第3章中的类似。因此我们不再对细节进行重复。现在我们已经有了Edit菜单的组件模板,每次需要菜单、相应的菜单项和代码时就可以使用该模板。

使用组件模板菜单

现在已经有了 Edit菜单模板,它可以像其他的组件一样使用。要使用组件模板,从

Template属性页上选择对应的模板,像其他菜单一样拖动到窗体上。菜单组件及其拥有的TMenuItems、以前写过的代码都可以添加到任何窗体上,而无需测试,也不会产生任何混



11 章用组件开发一致的界面

273

乱的情况。

扩展组件模板

可以把组件模板看作组件类。当创建组件模板时(如本节的前半部分的Edit菜单),不要删除它,当需要新的行动时,可以对该模板进行扩展。添加新的行为可以扩展已有的

组件,最后我们有原来的和新的组件模板。

假设现在有Edit菜单,要对其定义Find菜单项。使用组件面板的Dialogs属性页上的TFindDialog组件。按照下列步骤,即可添加Find功能并创建新的组件模板。

1.把在本节开头保存的模板拖动到任意的窗体上。

2.从Dialogs属性页上拖动TFindDialog组件到同一个窗体。

3.在窗体上,点击EditFind菜单项,并添加代码FindDialogl.Execute

4.为TFindDialog.OnFind事件添加事件方法,并向事件处理程序添加一些代码以提醒

用户实现查找行为。

5.选择TMainMenu组件和FindDialog组件并单击Component| Create Component Template菜单项,把合并的控件和代码添加到template属性页。TFindDialog组件可能的代码如下。

begin FindDialog1.Execute; procedure TForm1.Find1Click(Sender:TObject);

www.taodocs.co 0);
end;

警告:当存储模板组件时,确保使用惟一的名字。如果你使用已有的名字,

那么Delphi将提示你是否替换已有的模板,包括代码在内。

这就是所需要的工作。通过将代码分层添加到模板中,可以对完整的组件群体和提供

功能的代码创建精致的接口。

然而使用组件面板也有一些缺点。组件面板并非真正的组件;它们只是写入到二进制文件Delphi.dct中的文本。它们提供了方便,但却放弃了灵活性。回忆对SelectAll 菜单项的前一个实现。第3章开始部分列出的代码中显示的ToDo表明,事件方法需要进行修改才能对其他类型控件做出合适的响应。在SelectAll当前的实现中,是无法响应TComboBox之类的控件的。不幸的是,如果你返回来完成SelestAll 行为,可以创建新的模板。但这对
包含菜单和Find对话框的复合模板没有影响。原来模板不会有什么改变,衍生的模板将继续使用SelectAll的旧版本。



274

Delphi 6 应用开发指南

如果模板代码能够继承,而且可以自动更新有依赖关系的面板,那就太好了,但这并不是面向对象的继承,而是一种新型的资源流化机制。如果需要继承,那就要创建新的类并进行子类化。但组件模板仍然是一种流行的方法,只会变得更好而已。对于可视化的建立多数或全部组件这个目标来说,我们已经不远了。

删除组件模板

ComponentTemplates 包含在Delphi.dct文件中。该文件不像其他的组件库,它并非软件包的库文件。因此无法像管理其他组件一样管理组件目标。为删除组件目标,需要在组件面板上找到对应的位置,并组件面板上下文菜单中点击Properties菜单项。参考图11.7,从右边的组件列表里选取要删除的模板组件,然后点击Delete按钮。


www.taodocs.co

11.7 Palette Properties对话框可用于删除组件模板或隐藏组件

提示:你不会意外地从图11.7所示的PaletteProperties 对话框中删除组件。

如果选定了某个组件,Add按钮右侧将出现一个Hide按钮;而如果组件实际上是一个模板,那么同一位置就出现Delete按钮。

组件模板不同于组件,使用图11.7所示的PaletteProperties对话框只能隐藏组件。如果你删除组件模板,也会删除所有与之相关联的代码。把组件模板中的代码保存到外部文件是个好主意,以防止意外删除。

关于组件模板的最后一个问题。组件模板方便且易于使用,但并非实际的组件。以Edit 菜单为例,模板比简单的资源菜单使用起来更加方便。虽然可以创建精巧的模板,但从长远看来,将复杂的代码和相互交织的组件关系封装到类的话将更为可取。




11 章用组件开发一致的界面

275

11.3 窗体模板与窗体继承

与组件模板相比,窗体模板出现得更早一些。我们知道窗体是由TComponent子类化而来,但如果将窗体直接安装到VCL中,可能会出现不正确的行为;因此在Delphi的早期版本里发明了窗体模板来解决这个问题(更多的信息请参见第10章的对话框组件部分)。

很快发现,窗体一般带有许多代码,而且大多数应用程序里会重复出现许多类型的窗体,很显然的一个例子就是About对话框。几乎所有应用程序都有该对话框。但只是意外DFM文件与VCL无法很好的协作,就需要开发者为每个应用程序绘制一个About框并且添加必要的代码来显示该窗体吗?答案是,你不必如此。最后,我们可以把窗体添加到存储库中,存储库中的窗体可以直接使用、继承和复制。

注意:在技术上,与窗体继承相关的最大的困难可能就是,如何使DFM流机

制正确地工作。为了深入了解DFM流化机制,看第10章的“对象流化与窗体继承”一节。窗体继承机制现在已经工作得很好了。

对于开发者来说,所有这些意味着又添加了一个强有力的方法来重用整个程序。下面的方法都可以促进代码与界面设计的重用,包括保存菜单资源,创建组件模板,将窗体添

11.3.1 创建窗体模板 加到存储库并重用;当然最强大的方法是创建新类。

www.taodocs.co

为实验创建窗体模板的过程,我们将包含TMainMenuTFindDialog的组件模板添加到空白窗体上(如果你在上一节中没有创建组件模板而还想继续的话,只能先把TMainMenuTFindDialog组件拖动到空白窗体上)。按照下列步骤,可以将窗体添加到存储库(见图11.8)。





276

Delphi 6 应用开发指南

11.8 MainForm模板,其中包含TMainMenuTFindDialog组件

1.右击要添加为模板的窗体,以显示窗体设计器上下文菜单。

2.选择Addto Repository菜单项。

3.填写AddTo Repository对话框,需要为模板窗体提供标题和描述(见图11.8), 并选择要加入的属性页,输入作者信息和图标。

件。
4.点击OK按钮。如果尚未保存文件,在添加到存储库之前Delphi将提示你保存文

Newwww.taodocs.co


11.9 NewItems 对话框选择与目标最为接近的窗体目标



11 章用组件开发一致的界面

277

存储库的维护

大多数人都有一个用于存放物品的地方。存储库就是个存放物品的地方。最后您可能需要对存储库中的目标重新进行组织或删除某些模板。Tools菜单有一个Repository菜单项,可以打开ObjectRepository对话框(见图11.10)。对话框的左侧列出了所有可以修改的Repository属性页,包括Forms属性页,其中有用户定义窗体模板的。像NewActiveX等属性页是不可修改的。

要增加、删除、重新命名符合条件的属性页,只需选择相应的属性页并点击AddPageDeletePageRenamePage中某个合适的按钮。要从Objects列表中编辑或删除某个模板,在左侧选择包含该模板的页并在右侧的Objects列表中点击相应的模板。例如要删除上一节定义的FormMain模板,首先点击左侧Pages列表框中的Forms项。所有的窗体模板将在右侧的Objects列表框中列出。找到MainForm模板,单击以选取它并点击DeleteObject按钮(如图11.10所示,选定了MainForm窗体)。


www.taodocs.co

11.10 Object Repository 对话框

NEWFORM也可以在ObjectRepository中选择缺省的NewForm复选框,用于表示在Delphi中点击File| New | Form菜单项时将创建哪个窗体。缺省情况下并未选定NewForm复选框,但很容易就可以将FormMain作为默认的新窗体(选取窗体的指令,请参见前面的章节)。选取窗体后,选定图11.10所示的NewForm复选框并点击OK按钮。

MAINFORM当创建新的可执行应用程序时——例如当Delphi启动时——缺省情况下主窗体是空白窗体。另外,你还可以在Delphi中点击Tools| Repository菜单项打开

Form 复选框(如图11.10所示)。这样,每次创建新应用程序时,将使用该窗体作为缺省
ObjectRepository 对话框。当存储库被打开后,选取所需的新窗体作为主窗体并选定Main

的主窗体。 向存储库添加工程



278

Delphi 6 应用开发指南

可以将整个的工程添加到存储库。完整的工程由工程中的DPR文件、所有的源代码、窗体和数据模块组成。如果要把一个或多个窗体定义为工程模板,可以使用Project菜单将工程添加到存储库。要向存储库添加工程,首先选择要添加工程,然后点击Project| Add to Repository菜单项。这时将显示Addto Repository 对话框,它与窗体模板的情形类似(见图11.8),由于将添加所有窗体,所以不需要选择添加哪个窗体。

将工程添加到存储库后,点击File| New | Other菜单项,然后从NewItems对话框中选择所需的工程,即可基于已有的工程启动一个新的工程。可用于放置工程模板的好地方是Projects属性页,尽管在添加工程时也可以创建新的属性页。

提示:如果要把缺省的工程从标准的可执行程序改变为存储库中的某个工程,

那么可以从Tools 菜单中打开对象存储库,并将相应的工程设置为新的缺省

工程。选定一个工程后,Objects列表框下将出现NewProject 复选框。选定该复选框,则对应的工程将成为新的缺省工程。

当从NewItems对话框中选择一个工程模板时,Delphi将提示您为该工程输入路径,这时Delphi相应创建该工程中所有的文件。要避免选择创建工程模板时的原始路径;如果这样做你将覆盖原来的存储库文件。也可以从ObjectRepository 对话框中删除工程模板。

11.3.2 使用模板窗体 细节请参考前面,标题为“存储库的维护”一节。

www.taodocs.co 如果选择Inherit,将继承模板窗体,这里的继承指的是面向对象的意义。对窗体在存储库中版本的改变将反映到子窗体。考虑到主窗体含有Edit菜单和Find对话框。如果使用

某个主窗体模板创建新的窗体,新窗体将子类化那个窗体。以后,再选择该主窗体模板以及Use选项,添加一个Replace对话框,则所有的子窗体在下次编译时将自动具有Replace行为。

DFM文件的内容可以看出原始窗体、副本窗体、以及使用模板窗体创建的子窗体之间的不同。下面的片断来自三个的DFM文件,分别演示了基于UseInheritCopy方式创建窗体时数据写入DFM文件的方式。这三个窗体都与窗体类TFormMain有关。

objectFormMain: TFormMain
Left= 435
Top= 254

Width = 418
Height = 320



Caption = 'Application Title Here'
上面列出的是实际的窗体。当在New Items 对话框的Forms 属性页中选择Use 时,将



11 章用组件开发一致的界面

279

得到模板窗体。

objectFormMain1: TFormMain1
Left= 435
Top= 254
Width= 418
Height= 320
Caption= 'Application Title Here'

当选择Copy时,得到的是TFormMain1,看上去像是TFormMain的子类,但实际上该窗体是从TForm子类化而来。所拥有的组件都被复制并流化到DFM文件中,但在模板和窗体副本之间不存在更进一步的关系。

inheritedFormMain4: TFormMain4
Caption= 'FormMain4'
PixelsPerInch= 120
TextHeight= 16
end

上面列出的DFM文件是从TFormMain继承时创建的。与前面的两个DFM文件不同,

信息。

当要对所有的祖先都作出持久性的改变时,可修改原始窗体。若希望父窗体与子窗体
第一个词不是object,而是inherited。这是个约定,用于表示应从祖先窗体读取额外的流化

www.taodocs.co的任务看起来也会更加连贯。在应用程序被认为具有完备性之前,它首先必须是一致和连

贯的。另外,完备性还需要程序能够合乎用户的需求。

11.4 静态与动态的组件用法

当把窗体添加到应用程序时,将把一行代码加入到DPR文件(工程源文件)中,以便在程序启动时自动创建窗体。

Application.CreateForm(TForm1,Form1);

对于Visual Basic程序员或尚未完全掌握对象及动态对象创建的用法的新程序员来说,这可以使得添加和使用窗体更为容易。对外行人来说,这实际上使得DelphiVisual Basic看起来很相似:创建窗体、运行程序、调用showshowmodal,然后用户就可以使用窗体了。当在窗体上绘制组件时,也是同样。好像是有魔力一样,它们在运行时出现时,

Delphi Visual Basic之间的相似性在这里结束了。与VisualBasic 相比,Object
自然的就具有设计时的状态以及任何由自己编写的代码所定义的行为。



280

Delphi 6 应用开发指南

PascalDelphi的语言)与C++更为相似。DelphiVisualBasic一样易于使用,并具有C++的强大功能。这意味着新的程序员也能立即开始,而经验丰富的老手则能够创建非常复杂的应用程序。

Delphi自动创建窗体或在运行时自动创建组件时,其行为是一致的。而且,已经对流化机制编码,知道如何解析组件引用、从DFM文件里读出属性、构造对象。当Delphi自动创建窗体和组件时,与手工使用代码来创建的方式是一致的。

11.4.1 动态创建窗体

到现在为止,您可能已经熟悉如何绘制程序所拥有的窗体了。有些程序员允许自动创建所有的窗体,但这会使可执行文件变得相当大并降低程序的启动速度。大多数用户通常只使用某些核心功能,从来或很少使用其他功能。最终结果是用户为一些从不使用的功能付出了性能方面的代价。

好一些的方法是在需要的时候才创建窗体、数据模块以及加载库。这实际上是通过推迟堆内存的分配以及窗体流数据的读取,以便降低程序启动时一次性创建窗体的成本,该技术有个名字,可以称之为惰性实例化(lazyinstancing)。

ProjectOptions对话框的Forms属性页中,将窗体和数据模块从autocreate栏移动到available栏,即可推迟窗体的创建。虽然在启动时性能有所提高,但以后在运行时动态

创建窗体比一次性创建所有的窗体更为可取。有两种技术可用于创建惰性实例,分别讨论如下。
创建窗体是要付出代价的;除非该功能对时间要求非常高或经常使用,否则在需要时动态

内存。www.taodocs.co Form:= TForm.Create(self);
try
if(Form.ShowModal = mrOK ) then
//doe here
finally
Form.Free;
end;

如果把窗体作为模式对话框来显示,该技术工作得很好,因为在调用ShowModal时代码的运行是同步的。只要关闭窗体,内存也就被释放了。另外,您也可以显示窗体并让窗体在关闭时自行释放内存。

TForm.Create(Self).Show;

被创建的窗体需要一个OnClose事件处理程序,以便进行一些设置,这样当窗体关闭
时它可以将自身从内存中释放出去。

procedure TForm1.FormClose(Sender: TObject; var Action:



11 章用组件开发一致的界面

281

TCloseAction);
begin
Action:= caFree;
end;

假设类为TForm1,那么OnClose事件方法可以像上面这样编写。TCloseAction对象caFree将释放分配给窗体的内存。

惰性实例化

懒惰实例的关键在于,从外观看来对象仿佛已经存在了,但实际上仅在最后的可能时刻才创建对象。考虑到窗体,这还是相对较为容易的。将Delphi自动添加的变量声明使用同名函数来替换,该函数返回窗体的实例。该函数返回在堆上分配的窗体,当窗体关闭时

将释放所分配的内存。调用者使用一个本地的窗体变量,外部代码不能访问。如果窗体变

量为空,将创建对象;总是返回对象的引用。

interface
...

var
Form1: TForm1;

使用函数来替换在所有新窗体中找到的上述代码片段。从外部看来,它们是相同的。
implementation

www.taodocs.co FForm: TForm2;

functionForm2 : TForm2;
begin
if(FForm = Nil ) then
FForm:= TForm2.Create(Application.MainForm); result:= FForm;
end;

procedureTForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin

Action := caFree; FForm := Nil;



end;







282

Delphi 6 应用开发指南

initialization
FForm:= Nil;
end.

接口部分的变量已经使用外观和行为都与变量类似的函数进行了替换。该函数使用本地变量FForm。如果FForm变量为空(在initialization部分已将其设置为空),那么将创建窗体。FormClose事件方法将Action设置为caFree,使得窗体可以被释放,而FForm引用将设置为Nil

代码比“显式构造”一节所示的简单的动态创建窗体要复杂一些,但使用该实例是非

常容易的。

Form2.ShowModal;

Form2.Show;

使用该技术有一个潜在的危险。如果使用Form2函数以外的其他变量引用创建了另一个窗体实例,那么在FormClose事件中的FForm:= Nil语句将导致分配给FForm的内存出现泄漏。为了避免这种情况,可以修改FormClose事件方法,对FFormSelf进行比较。

现在FForm 的引用不会被不注意地弄混淆了。if(FForm = Self ) then FForm := Nil;

冗余。实际上,无需在设计时创建新窗体,也能够创建非常复杂的窗体。下面的代码示范www.taodocs.co

了称为TDBFormWizard的组件。这个组件实例化了一个窗体,其中包括一个TDBNavigator组件,一个Close按钮,并对数据集中的每个TField都使用一个DBEdit域来表示。

注意:DelphiC++Builder VisualBasic 之前——在最近的十年里

——大多数应用程序都必须用这种方法创建。新技术代表了一个向前的飞跃;

但如果想要完全动态的窗体还需要一些时间。

unitUDBFormWizard;
//UDBFormWizard.pas - Creates a formless data edit form on the fly //Copyright (c) 2000. All Rights Reserved.

// Written by Paul Kimmel // by Software Conceptions, Inc. Okemos, MIUSA (800) 471-50

interface



11 章用组件开发一致的界面

283

uses
Windows,Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls,DBCtrls, ExtCtrls, DB, DBGrids, DBTables;

type
TDBFormWizard= class(TComponent)
private
{Private declarations }
Form: TForm;
TopPanel,BottomPanel : TPanel;
Navigator: TDBNavigator;
ScrollBox: TScrollBox;
CloseButton: TButton;
FDataSet: TDataSet;
FDataSource: TDataSource;
FTitle: TCaption;
FunctionLargestLabelWidth( const DataSet : TDataset ) :

protected procedure Notification(AComponent : TComponent;Integer;
procedure CloseClick(Sender : TObject );

www.taodocs.coprocedureSetTitle( const Value : TCaption );
public
{Public declarations }
functionExecute : Boolean;
published
propertyTitle : TCaption read FTitle write SetTitle;
propertyDataSet : TDataSet read FDataSet write FDataSet;
propertyDataSource : TDataSource read FDataSource write
FDataSource;
end;

procedureRegister;

implementation { TDBFormWizard }
function TDBFormWizard.Execute: Boolean; begin



284

Delphi 6 应用开发指南

Form:= TForm.Create(Screen.ActiveForm); try
Form.Caption:= FTitle;
Form.SetBounds(382,223, 487, 386 ); AddFields(DataSet );
result:= Form.ShowModal = mrOK;
finally
Form.Free;
end;
end;

procedureTDBFormWizard.InitializeBasicForm; begin
TopPanel:= TPanel.Create(Form);
withTopPanel do
begin
Name:= 'TopPanel';
Caption:= EmptyStr;

Width := Form.ClientWidth; Height := 50; Parent := Form;
Align:= alTop;

www.taodocs.cobegin
Parent:= TopPanel;
Caption:= '&Close';
SetBounds(TopPanel.Width - Width - 20,
TopPanel.Height- Height - 20, Width, Height);
Anchors:= [akRight, akBottom];
OnClick:= CloseClick;
Name:= 'ButtonClose';
end;

Navigator := TDbNavigator.Create(TopPanel);
with Navigator do
begin
Name := 'Navigator'; Parent := TopPanel;
SetBounds( 10, 10, Width, Height ); DataSource :=FDataSource;



11 章用组件开发一致的界面

285

ShowHint:= True;
end;

BottomPanel:= TPanel.Create(Form);
withBottomPanel do
begin
Name:= 'BottomPanel';
Caption:= EmptyStr;
Parent:= Form;
Align:= alClient;
BevelInner:= bvLowered;
BorderWidth:= 4;
TabOrder:= 1;
end;

ScrollBox:= TScrollBox.Create(BottomPanel); with ScrollBox do
begin

Align := alClient; AutoScroll := True; Name := 'ScrollBox';
Parent:= BottomPanel;

www.taodocs.co procedure TDBFormWizard.AddFields( const DataSet :TDataSet );
var
LabelWidth : Integer;
ALabel :TLabel;
DBEdit : TDbEdit;
I : Integer;
begin
FDataSet := DataSet;
SetDataSource( FDataSet );
InitializeBasicForm;
// in characters
LabelWidth:= LargestLabelWidth( FDataSet );
for I := 0 toFDataSet.FieldCount - 1 do
begin
ALabel :=TLabel.Create(ScrollBox);
ALabel.Parent := ScrollBox; ALabel.SetBounds( 10, 10 + (I*26), ALabel.Width, ALabel.Height



286

Delphi 6 应用开发指南

);
ALabel.Caption:= FDataSet.Fields[I].DisplayLabel +':'; ALabel.Width:= LabelWidth;
ALabel.Alignment:= taRightJustify;
DBEdit:= TDBEdit.Create(ScrollBox);
DBEdit.Parent:= ScrollBox;
DBEdit.SetBounds(ALabel.Width + 14, 6 + (26 * I), DBEdit.Width,DBEdit.Height);
DBEdit.DataSource:= FDatasource;
DBEdit.DataField:= FDataSet.Fields[I].FieldName;
//Used M arbitrarily because I liked the result
DBEdit.Width:= FDataSet.Fields[I].DisplayWidth *
Form.Canvas.TextWidth('M' );
DBEdit.ReadOnly:= FDataSet.Fields[I].ReadOnly;
end;
end;

procedureTDBFormWizard.CloseClick(Sender: TObject);

end; begin
Form.Close;

www.taodocs.coTextMetrics: TTextMetric;
begin
result:= 0;
forI := 0 to DataSet.FieldCount - 1 do
if(Length(Dataset.Fields[I].DisplayLabel) > result ) then
result:= Length(Dataset.Fields[I].DisplayLabel);

if( GetTextMetrics( Form.Canvas.Handle, TextMetrics )) then result:= (TextMetrics.tmAveCharWidth +
TextMetrics.tmMaxCharWidth)div 2 * result
else
result := 120;
end;
procedureTDBFormWizard.SetDataSource(const Value: TDataSet); begin FDatasource := TDataSource.Create(Form);



11 章用组件开发一致的界面

287

FDataSource.DataSet:= Value;
end;

procedureTDBFormWizard.SetTitle(const Value: TCaption); begin
FTitle:= Value;
end;

procedureTDBFormWizard.Notification(AComponent: TComponent; Operation:TOperation);
begin
inherited;
if(Operation = opRemove ) then
if(AComponent = FDataSet ) then
FDataset:= Nil
elseif( AComponent = FDataSource ) then
FDataSource:= Nil;
end;

begin RegisterComponents( 'PKTools', [TDBFormWizard] );procedureRegister;

www.taodocs.co创建所有的数据库组件时生成的。基本的窗体在InitializeBasicForm方法里创建的。图11.11显示了将DataSet设置为DBDEMOS中的biolife表时所创建的组件。





288

Delphi 6 应用开发指南

11.11 TDBFormWizard生成的窗体

虽然窗体不具有什么创造力,它完全有能力管理一个数据集。将一个查询赋值给DataSet特性,窗体将生成一个只读的多表窗体。对这个组件进行一些修改,很容易使得

TDBGrid,或者,可能 AddFields可以使用每个TField DataType特性来确定创建哪种数据库控件。
生成的窗体更为灵活。例如:可以允许用户在动态数据库控件中进行选择,如使用

www.taodocs.co 11.5 所有者绘图组件

扩展控件的外观可以提供定制的功能。有Windows句柄或能够接收WM_PAINT消息的可视化组件都是可以子类化的,可以定义Paint方法来创建调整好的可视效果。但有几个控件已经预定义了相应的特性和事件方法,可用于定制其外观。例如:TListView有一个布尔值特性OwnerDraw。如果OwnerDrawTrue,将调用OnAdvancedCustomDrawOnAdvancedCustomDrawItemOnAdvancedDrawCustomDrawSubItemOnDrawItem事件方法,使得可以定制组件的绘制方法。

下面的示例代码演示了一个扩展的TStringGrid组件,该组件使用可视化效果来表示被选中的栏目;你可能会使用该效果来表示TDBGridTStringGrid中的排序栏。

TExStringGrid = class(TStringGrid) private



{ Private declarations }
FColumnIndex : Integer; procedure DrawFixedColumnCell( ACol, ARow : Integer );



11 章用组件开发一致的界面

2

protected
{Protected declarations }
procedureMouseDown(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); override;
procedureDrawCell(ACol, ARow: Longint; ARect: TRect;
AState:TGridDrawState); override;
procedurePaint; override;
public
propertyColumnIndex : Integer read FColumnIndex;
end;

TExStringGrid组件由TStringGrid子类化而来。它重载了MouseDownDrawCellPaint方法,并引入了私有方法DrawFixedColumn,以及字段FColumnIndexDrawFixedColumn方法对选中的栏目进行特定的绘制,该栏目是由FColumnIndex表示的。

代码是通过使得某个固定行上被选取的栏目单元失效而实现的。代码的可视化效果请参考图11.12


www.taodocs.co var
Rect : TRect;
begin
if(not Ctl3D ) then exit;
Rect := CellRect(ACol, ARow);
DrawEdge(Canvas.Handle, Rect, EDGE_SUNKEN, BF_TOPLEFT);
end;

procedure TExStringGrid.DrawCell(ACol, ARow: Integer; ARect: TRect;
AState: TGridDrawState);
begin
inherited;
if(gdFixed in AState ) and (ARow < FixedRows) and (ACol= FColumnIndex) then
DrawFixedColumnCell( FColumnIndex, ARow);
end;



290

Delphi 6 应用开发指南

procedureTExStringGrid.MouseDown(Button: TMouseButton; Shift: TShiftState;
X,Y: Integer);
var
ACol,ARow : Integer;
OldColumnIndex: Integer;
begin
inherited;
MouseToCell(X, Y, ACol, ARow );
if( ARow >= FixedRows ) or ( ARow < 0 ) then exit;
OldColumnIndex:= FColumnIndex;
FColumnIndex:= ACol;
InvalidateCell(OldColumnIndex, ARow );
InvalidateCell(FColumnIndex, ARow );
end;

procedureTExStringGrid.Paint;

end; DrawFixedColumnCell( FColumnIndex, 0 );begin
inherited;

经被选取。MouseDown过程跟踪被选取的栏目单元,当选取新的栏目时,该过程使得旧的www.taodocs.co

和新的被选取的栏目单元都失效,这样将可以对其重新进行绘制。Paint方法确保每次网格重新绘制时都创建所需的可视化效果。

11.5.1 定制网格绘制

像本章开头所说明的,可以使用特性和事件来为组件创建定制的可视化效果。本节开头使用了TExStringGrid子类来演示栏目被选取时的效果。可以将同样的代码放置到字符串网格组件的事件中,以创建相似的可视化结果。

procedure TForm1.StringGrid1MouseDown(Sender: TObject;
Button:TMouseButton; Shift: TShiftState; X, Y: Integer); var
ACol,ARow : Integer;
begin
StringGrid1.MouseToCell(X, Y,ACol, ARow);
if ( ARow >= StringGrid1.FixedRows ) or ( ARow< 0 ) then exit; FColumnIndex := ACol;



11 章用组件开发一致的界面

291

StringGrid1.Invalidate;
end;

procedureTForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;Rect: TRect; State: TGridDrawState);
begin
if(gdFixed in State ) and (ARow < StringGrid1.FixedRows) and (ACol= FColumnIndex) and (StringGrid1.Ctl3D) then
begin
Rect:= StringGrid1.CellRect(ACol, ARow);
DrawEdge(StringGrid1.Canvas.Handle,Rect, EDGE_SUNKEN,
BF_TOPLEFT);
end;
end;

当鼠标单击TStringGrid时,MouseDown事件方法将存储栏目索引并使得网格失效。Invalidate确保网格被重新绘制。DrawCell事件方法确保单元是固定的,而且行和列都对应于固定栏目单元,并且使用与DrawEdge同样的API方法来创建可视化效果。

剩下的惟一问题就是,究竟是子类化字符串网格组件,还是使用TStringGrid的事件方

那么,当组件完成后,您可能需要子类化该组件并添加新的行为。从语义上看来,上面显

示的效果属于新的网格组件的行为。另外,如果组件并未暴露创建效果所需的必要方法,
法较为合适。如果你正在创建新组件的原型,使用具有事件方法的已有组件较为容易一些。

www.taodocs.co11.5.2 所有者绘图TMainMenu组件

有时候,预测定制绘图可能会比较困难。例如:字符串外观能预期并跟踪需要绘制哪个栏目单元;因此子类化字符串网格组件是可能的。另外,我们来考虑TMainMenu组件。我们使用帮助文档中的提议,假设要使用颜色编码的菜单,它显示颜色而不是文本,或同时显示颜色和文本。在设计时我们不可能知道需要哪些菜单项;这使得子类化TMainMenu非常困难。同样,因为TMenuItem是由TMainMenu使用的,TMenuItem的子类将被TMainMenu忽略。在这种环境下,不得不使用特性和事件来创建效果。

使用颜色进行编码的菜单

下列步骤摘自TMenu.OwnerDraw帮助文档中的建议,说明了如何使用颜色编码的菜单

项来对菜单项的绘图进行定制。

1.创建新的应用程序。 2.在主窗体上画一个TMainMenu组件。

3.添加一个名为Color的菜单项以及一个名为BackGround的子菜单。
4.向BackGround添加三个子菜单,名字分别是GrayWhiteAqua(见图11.13)。



292

Delphi 6 应用开发指南

5.向这三个使用颜色命名的菜单项的Tag特性添加TColor值。这些颜色定义在graphics.pas单元中,分别是clGray=TColor($808080)clAqua= TColor($FFFF00),以及clWhite=TColor($FFFFFF);直接将颜色的十六进制的数字值从graphics.pas单元复制并粘贴到每个菜单项的Tag特性中。

6.在ObjectInspector 里,将TMainMenuOwnerDraw特性设置为True
7.创建OnClickOnDrawItem事件方法,并将每个颜色菜单项的OnClick OnDrawItem事件特性都分派到单个事件方法(例如:Gray1.OnClickWhite1.OnClick Aqua1.OnClick都指向同一OnClick方法;OnDrawItem特性也是同样)。

8.用下面的示例代码定义两个事件方法(在示例代码中,首先对Gray菜单项进行处 理,因此命名如下)。

procedureTForm1.Gray1DrawItem(Sender: TObject; ACanvas: TCanvas;ARect: TRect; Selected: Boolean);
begin
ACanvas.Brush.Color:= TMenuItem(Sender).Tag;
ACanvas.FillRect(ARect );
end;

begin Color := TMenuItem(Sender).Tag;procedureTForm1.Gray1Click(Sender: TObject);

www.taodocs.co使用颜色进行编码的菜单和文本

使用文本和颜色编码值的代码更加复杂。要显示文本并重新设置画刷需要一些必要的改变,代码如下列出。该代码使用了新的TBrushReCall类,并将TMenuItem类型的Sender对象强制转换为其平凡子类TDummyMenuItem,以便调用TMenuItem中保护权限的绘图方法来绘制标签。

11.13 在使用颜色进行编码的菜单项中,使用

OwnerDraw 特性和DrawItem事件



11 章用组件开发一致的界面

293

type
TDummyMenuItem= class(TMenuItem);
procedureTForm1.Gray1DrawItem(Sender: TObject; ACanvas: TCanvas; ARect:TRect; Selected: Boolean);
var
Recall: TBrushRecall;
CopyRect: TRect;
begin
Recall:= TBrushRecall.Create(ACanvas.Brush);
try
ACanvas.Brush.Color:= TMenuItem(Sender).Tag;
CopyRect:= ARect;
ARect.Right:= 20;
ACanvas.FillRect(ARect );
ACanvas.Brush.Assign(Recall.Reference );
finally
Recall.Free;
end;

end; DoDrawText( ACanvas, Caption, CopyRect, Selected,0);CopyRect.Left := 22;
with TDummyMenuItem(Sender) do

www.taodocs.cobegin
Width:= Width + 20;
end;

声明TDummyTMenuItem就是为了可以访问TMenuItem中保护权限的方法(过一会儿

我们继续讨论)。新的DrawItem事件处理程序创建一个TBrushRecall对象。在显示颜色块之后,Brush刚好在Finally块之前被恢复,而TBrushRecall对象则在finally块中被释放。TRect记录的副本被保存到CopyRect,这里调整了CopyRectLeft字段以适应颜色矩形所需的空间。

注意:可以将子类转换为超类,反之则不然。如果B定义为A的子类,那么

一个B 类的对象也是A类的对象,但一个A 类的对象并非B类的对象。这样 (B AsA)是有效的,但(A AsB)是无效的。把对象向子类型转换是非法的。
这里使用保护权限的方法DoDrawText,以避免复制TMenuItem的代码。由于Sender TMenuItem类型,需要强制将Sender转换为TDummyMenuItem对象。实际上Sender



294

Delphi 6 应用开发指南

一个TMenuItem祖先对象,因此使用as操作(SenderAs TDummyMenuItem)是无效的。最后定义了OnMeasureItem事件处理程序,用来把每个用颜色编码的菜单宽度增加20个像素,以便为颜色块矩形提供额外的空间。

11.6 小结

11章示范了组件编写的技术,说明了如何创建组件、窗体和工程模板。模板可以重用代码和可视化的设计工作,它也提供了一个方便的方法,可用来直接启动一个一致的应用程序。应用程序内部的一致性使应用程序看上去更加专业和连贯,也更容易使用。要达到完备性、一致性和连贯性是必须的成分。不一致的应用程序可能难于使用而且其行为毫无规律;也就是,它们的行为可能是不连贯的。

本章也示范了如何创建动态窗体,这样可以避免创建静态的、非常冗余的窗体,并使得应用程序的内存占用较为合理。将模板、动态窗体、定制组件以及增强的组件绘制方法联合起来使用,您的应用程序就可以给用户提供专业化而与众不同的体验。


www.taodocs.co



Copyright © 2019- uude.cn 版权所有

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务