蛙蛙推荐:蛙蛙Delphi学习经验(第一版)
蛙蛙推荐:蛙蛙Delphi学习经验(第一版)
一、使用ModelMaker设计类,并生成设计文档。
ModelMaker是Delphi7自带的一个UML建模软件,支持和Delphi IDE的双向工程,没用过Rose For Delphi的插件。我直接用的MM6,感觉MM就挺好用的。
你可以在Classes视图里添加类,并添加类的方法、字段、属性、字段,这些操作都很简单,在Classes视图的上边类部分右键点击添加类,然后选中这个类,点击下面的各个添加成员按钮,按向导就可以给类添加成员了。
要想生成代码,请点击Units视图,在Units视图的空白处右键点击添加新的单元,在弹出的Edit Unit对话框里,在Relative Unit file name后面点击Browse按钮选择单元保存的路径,其它参数你看着选择,然后点击OK。然后在Units视图里把刚刚添加的类从Classes not assignet to units拖到新建的单元下面。选中刚刚添加的单元,确保最上面按钮条的开着的小锁按钮处于按下状态(代码生成解锁,如果不选中,则代码处于保护状态,无法生成代码。)点击Units视图上的闪电按钮生成代码。点击菜单Delphi-locate in delphi,就可以在delphi里打开刚刚生成的代码了,你可以在实现部分写你要写的代码,因为mm只给你生成了一个类的架子,我们在代码的接口部分再添加一些字段和方法,以及注释,然后点击菜单ModelMaker-jump to modelmaker,又切换到mm界面了。在Units视图里点击刷新按钮(和浏览器的刷新按钮类似的那个按钮),你会发现刚刚在delphi里添加的字段和方法已经同步到了mm里,然后你再在MM里修改类,再生成代码,你会发现其它部分都生成的很好,就是接口部分你给成员写的注释给没了,这就不爽了,好歹也是你手工写的注释代码,为啥MM给你弄没了捏,一会儿再给大家介绍解决办法。
模型有了,我们还要生成模型的文档,以便和别人交流。在左边点击到单元视图,右边点击到Documentation视图。在左边选中单元,在右边的文档视图的下拉菜单里选择Unit,然后在下面大的文本编辑区域里写关于单元的注释文本。在左边点击类,右边下拉列表里选择Class,添加类的注释,在左边选择类的成员,右边下拉列表里选择Class Member,添加类成员的注释。OK,把右边的下拉列表选到unit,点击下拉列表左边的问号按钮,在弹出的对话框里,选择文档的保存路径,和是否生成私有成员的注释,然后点击OK就生成文档了。打开刚刚生成文档的目录,你会发现一个rtf文件和一个hpj文件,打开rtf文件一看,原来不支持中文,这可咋办,这可咋好,这可咋整,别着急,从网上下载个Microsoft Help WorkShop装上,双击生成的.hpj项目,打开后点击存盘并编译按钮,就生成一个hlp文件了,这不就是你要的文档吗?多好呀,和MSDN似的。.net的ndoc也不过如此嘛。
生成文档有个缺点,我给大家说说哦,你在文档视图里给类添加的描述(One Line)是不是想生成到代码里作为注释以提高代码的可读性呀,我也想呀,可是找了半天帮助也不直到怎么生成。还有,你是不是想在代码里写一定的注释,让mm同步的时候直接同步到文档视图里显示呀,这样你就可以在delphi里写注释,用mm来生成专业文档了,这个应该也能搞成,但我还是没搞成,你写的注释要符合MM的格式,大括号的那种注释。在MM的Project option对话框的Source Doc Generation标签里有生成注释和同步注释的一些设置,然后在帮助里有关于类注释、成员注释的一些语法,你可以看看,没准儿能鼓捣成呢,我主要是没时间琢磨了。其实这两个不算是MM的缺点,只能怪我没研究出来了。还有一个缺点,就是MM在生成文档的时候只能生成一个单元的,我晕,这个问题困扰了我好久,最终在它的网站上下载了一个插件装到MM上才搞定,这插件免费的哦。http://www.modelmakertools.com/modelmaker/plugins.html。下载那个HelpExpert插件,解压后把Expert目录下的OCHelpExpertMM6.dll拷贝到mm6安装目录的Expert目录下,把bin目录下的rtf文件拷贝到mm6安装目录的bin目录下。点击MM的tools菜单的Expert Manager子菜单,在打开的对话框里看看有没有多了一个OCHelpExpertMM6.dll,打上勾,发现tools里多了一个子菜单Geneater help form all units,点击就可以生成所有单元的文档了,cool吧。
MM的基本功能咱就挖掘到这儿吧,另一个常用的功能就是它的UML图视图,在这里你可以把你设计好的类,拖上去,以表示它们之间的关系给你的开发团队成员看。你刚拖上去的类也只显示一个类名,没有成员,你在这个类上点击右键,选择dragram properties,在打开的对话框里,把Class symbol member filter组合框的Project member type filter复选框的勾去掉,然后把custom member type filter下面的勾打上就可以显示成员了,这下你就可以画UML图来表达你的设计了,我想最常用的也是类图和组件图吧,其它的图我没用过。MM的功能还很强大,可以直接根据设计模式生成代码的架构,还有用宏自定义生成代码等功能,等你觉得以上我介绍的功能不够用的时候你可以学习学习,网上mm的教程不多,但也有几个比较好的。比如《ModelMake初探.rar》,比如《MM(ModelMaker)两小时上手指南》,还有大富翁整理的关于MM应用设计模式的一个教程。
总的来说mm对delphi的开发还是很有帮助的,和delphi结合紧密,比visio和vs结合还好,建议delphi的新手学学。老手一般不用这个,直接手工写就可以了,但如果想节省时间的话也可以用用。
二、使用OutputDebugString进行跟踪输出
无论哪一种语言和编辑工具,使用好它的调试功能都是非常必要的。在Delphi里可以使用OutputDebugString API函数输出调试信息,然后在调试运行的时候可以打开view-debug windows-event log菜单,查看你跟踪输出的信息。如果软件编译成二进制执行代码后,可以去微软下载个你debugview(下载地址自己搜索一下吧,微软绝对免费下载),用它去查看调试信息。在用Delphi调试的时候用debugview看不到信息的,因为被delphi的Event log给截获了。DebugView里可以使用命令行来启动,并把获取的信息输出到一个文本文件,还可以用命令行自动运行,每天生成一个日志,具体命令行参数可以看帮助。另外DebugView非常适合监控分布式程序,它有远程监控功能,你可以在你的机器上监控到运行在远程机器上的软件的调试信息,前提是你在那个服务器上用命令行启动一个debugview服务器,这个我没有试过哦,人家帮助里说的,现在DebugView刚出新版本,支持64位系统。有了它,你就可以简化你的日志方案了,你可以轻轻松松了解你的软件的运行状态,根本不用自己鼓捣一套。另外你也可以自己开发一个截获OutputDebugString输出消息的小软件,网上有资料,这样你就可以自定义显示你的调试信息和跟踪信息了。
三、使用断言,给断言处理事件挂接一个处理错误的回调函数。
我们.net程序员都知道,像c#,java这样的语言编译后不是二进制代码,而是可以解释执行的一种东西,这样在我们调试的时候,如果出错了,报的错可以显示错误所在的堆栈,方法,行号(当然,你release编译或者把pdb调试文件去了就没行号了),而Delphi就不行了。如果你在异常处理里想把异常出现的堆栈,方法和行号输出出来简直是不可能的。但有个你变通的方法,就是使用断言,断言在调试状态下可以指出错误行号(其实我编译生成exe后好像也可以输出源文件的行号,我不知道为什么)。我们要先为断言失败事件挂接一个默认执行方法,如下。
procedure AssertErrorHandler(const Message, Filename: string; LineNumber: Integer; ErrorAddr: Pointer);
var
S: String;
begin
S := Format('%s (%s, line %d, address $%x)',
[Message, Filename, LineNumber, Pred(Integer(ErrorAddr))]);
OutputDebugString(PChar(S));
end;
initialization
AssertErrorProc := @AssertErrorHandler
然后你就可以使用断言了, Assert(pam1 <> nil,'参数为空');比如你的方法要传入一个pam1的参数,如果传进来的参数未定义,这个断言就会失败,从而触发断言失败事件,然后你就可以用debugview看到断言失败的行号了。
使用这个方法的不足就是,你只能获取可预期异常的信息,对未知异常无法使用了。
四、带chkbox的树
vcl自带的treeview控件不支持前面带checkbox,我从网上找了一些资料,写了个帮助类,这个帮助器接受sui的tree,要接受普通的tree你稍微修改一下就可以了,我把代码给大家贴一下。
type
/// <remarks>
/// 此类为带checkbox的treeview帮助类,可实现
/// 点击父节点自动选中或者反选子节点,以及获取
/// 选中节点列表等功能
/// </remarks>
/// <example>
/// <code>
/// var
/// th: TTreeHelper;
/// list: TList;
/// i: Integer;
///begin
/// th := TTreeHelper.Create;
/// th.RegTree(tree);
/// list := TList.Create;
/// try
/// th.GetChkedNodes(list);
/// for i := 0 to list.Count - 1 do
/// begin
/// ShowMessage(TTreeNode(list[i]).Text);
/// end;
/// finally
/// list.Free;
/// end;
///end;
/// </code>
/// </example>
TTreeHelper = class(TObject)
private
FTree: TSUITreeView;
public
function GetChkedNodes(nodes:TList):TTreeNodes; //获取选定的节点数组
function IsChecked(Node: TTreeNode): Boolean; //判断某个节点是否选中
procedure AllChildYNChecked(Node: TTreeNode; isChecked: Boolean); //设置一个节点的子节点是否选中
procedure SetChecked(Node: TTreeNode; Checked: Boolean);//设置一个节点是否选中
function isBrotherAllChecked(Node: TTreeNode): Boolean; //判断一个节点的兄弟节点是否选中
procedure SetParentChecked(Node: TTreeNode); //设置一个节点的父节点选中
procedure SetParentNOChecked(Node: TTreeNode);//取消一个节点的父节点选中状态
procedure RegTree(tree: TSUITreeView); //注册一个树,让它有复选框,并挂接mosuedown事件
procedure MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
end;
implementation
{ TTreeHelper }
procedure TTreeHelper.AllChildYNChecked(Node: TTreeNode;
isChecked: Boolean);
var
CurChild: TTreeNode;
begin
if Node.HasChildren then
begin
CurChild:=Node.GetFirstChild;
while CurChild <> nil do
begin
SetChecked(CurChild, isChecked);
if CurChild.HasChildren then
begin
AllChildYNChecked(CurChild, isChecked);
end;
CurChild:=Node.getNextChild(CurChild);
end;
end;
end;
function TTreeHelper.GetChkedNodes(nodes:TList): TTreeNodes;
var
CurItem: TTreeNode;
begin
CurItem := FTree.Items.GetFirstNode;
while CurItem <> nil do
begin
if IsChecked(CurItem) then nodes.Add(CurItem);
CurItem := CurItem.GetNext;
end;
end;
function TTreeHelper.isBrotherAllChecked(Node: TTreeNode): Boolean;
var
CurChild: TTreeNode;
tmpBool: Boolean;
begin
Node := Node.Parent;
if Node.HasChildren then
begin
CurChild:=Node.GetFirstChild;
while CurChild <> nil do
begin
tmpBool := IsChecked(CurChild);
if not tmpBool then
begin
Result := False;
exit;
end;
CurChild:=Node.getNextChild(CurChild);
end;
end;
Result := True;
end;
function TTreeHelper.IsChecked(Node: TTreeNode): Boolean;
var
TvItem :TTVItem;
begin
TvItem.Mask := TVIF_STATE;
TvItem.hItem := Node.ItemId;
TreeView_GetItem(Node.TreeView.Handle, TvItem);
Result := (TvItem.State and TVIS_CHECKED) = TVIS_CHECKED;
end;
procedure TTreeHelper.RegTree(tree: TSUITreeView);
begin
FTree := tree;
setwindowlong(FTree.Handle,GWL_STYLE,getwindowlong(FTree.Handle,GWL_STYLE) or $0100);
FTree.Invalidate;
FTree.OnMouseDown := MouseDown;
end;
procedure TTreeHelper.SetChecked(Node: TTreeNode; Checked: Boolean);
var
TvItem :TTVItem;
begin
FillChar(TvItem, SizeOf(TvItem), 0);
with TvItem do begin
hItem := Node.ItemId;
Mask := TVIF_STATE;
StateMask := TVIS_STATEIMAGEMASK;
if Checked then
TvItem.State :=TVIS_CHECKED
else
TvItem.State :=TVIS_CHECKED shr 1;
TreeView_SetItem(Node.TreeView.Handle, TvItem);
end;
end;
procedure TTreeHelper.SetParentChecked(Node: TTreeNode);
begin
if Node.Parent <> nil then
begin
if isBrotherAllChecked(Node) then
SetChecked(Node.Parent, True);
SetParentChecked(Node.Parent);
end;
end;
procedure TTreeHelper.SetParentNOChecked(Node: TTreeNode);
begin
if Node.Parent <> nil then
begin
SetChecked(Node.Parent, False);
SetParentNOChecked(Node.Parent);
end;
end;
procedure TTreeHelper.MouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
HitTests: THitTests;
tmpNode: TTreeNode;
begin
HitTests := FTree.GetHitTestInfoAt(X, Y);
if htOnStateIcon in HitTests then
begin
tmpNode := FTree.GetNodeAt(x, y);
tmpNode.Selected := True;
if IsChecked(FTree.Selected) then
begin
SetParentChecked(FTree.Selected);
AllChildYNChecked(FTree.Selected, true);
end
else
begin
SetParentNOChecked(FTree.Selected);
AllChildYNChecked(FTree.Selected, False);
end;
end;
end;
end.
五、面向对象和动态类型的运用。
其实Delphi的面向对象特性和动态特性不比c#和java差,我们应该好好应用才是。有这样一个场景,系统里有一些窗口需要动态创建,在show之前呢,要给它设置一个窗口类型,它根据这个类型来设置自身的一些控件是否可见以及其它的一些外观。然后呢要给它传递一些自定义数据和它交互,比如说要传给它一个要删除客户的ID列表。因为是有好多个窗口需要有这种特征,所以我们先提取出一个基类,然后让我们创建的窗口继承它。
type
TCustomWindow = class(tform)
public
WindowsType: Integer; //窗口类型
//自定义数据,主要传递要修改的组或联系人的ID列表,
//一般来说tstrings里只有一项数据,因为不可能同时修改俩联系人
CustomData:TStrings;
end;
然后我们要写个你方法来动态创建窗口
//这个方法先判断要打开的窗口有没有打开,如果有,改变属性并show,如果没有打开
//则动态创建一个,并show出来,让已经打开的窗口前置要给它发送一个windows消息
//参数分别是要打开窗口的实例,类名,窗口标题,自定义类型,自定义数据
procedure showWindow (form:TCustomWindow;clssname:PAnsiChar;title: PAnsiChar;
wintype:Integer;data:TStrings);
var
H: THandle; //窗口句柄
C:TFormClass; //窗口的类型
begin
H := FindWindow(clssname, title); //根据窗口的类名和标题获取句柄
if H > 0 then //判断有没有找到句柄,
begin //如果找到句柄,则给它的自定义属性复制,让其前置
form.WindowsType := wintype;
form.CustomData := data;
SendMessage(H, WM_SYSCOMMAND, SC_RESTORE, 0); //忘了这句是干啥了?可能是恢复存在窗体的
SetForegroundWindow(H); //窗口前置
end
else //如果窗口没有被创建
begin
C:=TFormClass(FindClass(clssname)); //获取这个窗口类的类型信息,前提是要先注册它的类信息
form := TCustomWindow(C.Create(nil)); //根据窗口类型,动态创建一个窗口,并强制转换成CustomWindow
form.WindowsType := wintype; //设置新窗口的属性并show出来
form.CustomData := data;
form.Show;
end;
end;
使用FindClass前要注册相关的类,你新添加一个CustomWindow的子类要注册一下,如下
initialization
RegisterClass(TfrmDelContact);
RegisterClass(TfrmAddContact);
RegisterClass(TfrmAddGroup);
下面写一个显示批量删除联系人的窗体的使用方法
var
delIdList: TStrings; //要删除的联系人列表
begin
delIdList := TStringList.Create;
delIdList.Add('1');
delIdList.Add('2');
showWindow(frmDelContact, 'TfrmDelContact', title, 2, delIdList);
end;
注:TfrmDelContact继承自TCustomWindow,TfrmDelContact= class(TCustomWindow)
看到了吧,Delphi的动态功能还是挺强大的,不比.net的反射功能弱到哪儿去
六、使用自定义异常
我们直到在设计一个系统的时候经常也要对异常进行建模,Delphi也支持自定义异常。合理使用异常可以提高应用系统的健壮性,外界想要直到一个方法的内部情况,可以使用引用参数(输出参数)、返回值、事件(回调函数)、和异常。使用异常和调用者交互可以让交互者更合理的使用你写的方法和接口,让它们在面对你的方法执行出错的时候进行合理的措施。下面举个例子。
1、先来定义异常
EConfigErr = class (Exception) //配置文件异常
public
ErrorCode: Integer;
constructor Create(ErrorNum: Integer);
end;
ENotFindUserDir = class (EConfigErr) //找不到用户配置目录异常
public
constructor Create;
end;
ENotFindUserCfg = class (EConfigErr) //找不到用户配置文件异常
public
constructor Create;
end;
{ EConfigErr }
{
********************************** EConfigErr **********************************
}
constructor EConfigErr.Create(ErrorNum: Integer);
var
Msg: string;
begin
case ErrorNum of
1:
Msg := '找不到用户目录';
2:
Msg := '找不到用户配置文件';
else
Msg := '其它用户配置错误';
end;
inherited Create(Msg);
ErrorCode := ErrorNum;
end;
{ ENotFindUserDir }
{
******************************* ENotFindUserDir ********************************
}
constructor ENotFindUserDir.Create;
begin
inherited Create(1);
end;
{ ENotFindUserCfg }
{
******************************* ENotFindUserCfg ********************************
}
constructor ENotFindUserCfg.Create;
begin
inherited Create(2);
end;
2、适当的时候触发异常
if not DirectoryExists(ExtractFilePath(ParamStr(0))+ user) then
raise ENotFindUserDir.Create;
if not FileExists(ExtractFilePath(ParamStr(0))+ user + '\' +UserIniFileName) then
raise ENotFindUserCfg.Create;
3、捕获异常并处理
try
//操作
except
on E:EConfigErr do
//这里还可以根据E的ErrorNum属性针对异常做出更具体的处理
ShowMessage(e.Message);
else
ShowMessage('其它错误');
end;
showmessage('ok');
七、关于释放对象的最佳实践
delphi没有垃圾回收装置,虽然有人也实现了一些delphi自动回收内存防止内存泄露的机制,可毕竟还是手工释放用完的对象比较好。不直到在delphi里有没有这么一条最佳实践,就是一个对象谁Create就应该谁Free。我晕,我们知道Create(new)只是创建一个对象的一种方式,设计模式里专门有一类模式就是针对对象创建的,一个对象只能通过new创建的话那岂不是很郁闷。我写一个方法要返回一个对象,在方法里create,然后返回给外界,我怎么直到外界什么时候使用完了呀,我如果返回后就free,那不就是返回给外界一个空引用了吗?所以谁create谁free我感觉就不成立。其实在delphi的类库里有好多要返回对象的时候它们是这样做的。在外界生命这个对象,并create,然后把它以参数的方式传递给一个方法,在方法里对这个对象进行操作,比如填充list,改变属性等。然后执行完这个方法后,外界就可以使用这个对象了。当然了,这又符合了谁create谁free的原则,可毕竟还是不太爽,有点麻烦,我想的一种解决方案是对方法进行命名约定,符合这种命名规则的方法,你获取它的返回值,用完后要自己free掉,而不要期待返回它的方法给你free,它怎么直到你什么时候用完呀,我晕。
小节:好久不发帖子了,想不到这么一篇帖子花费了半个下午的时间,差不多三个多小时。学Delphi也是一个偶然的机会,学习时间也不长,总共接触了也刚一个来月的时间,不过也有了点儿收获,所以发出来和大家分享交流一下。另外推荐delphi新手两个教程《DELPHI基础教程.chm》和《Delphi高手突破(官方版).pdf》,我就是靠这两个教程快速入门的,前者是讲语法和IDE基本操作的,可惜是delphi3,后者讲delphi面向对象开发和vcl的。
本人长期神经末梢坏死,并且经常语无伦次,没条没理,讲东西没人能听懂,思维逻辑也和普通人不一样。所以你看不懂我发的帖子就尽情的鄙视我吧,我早就习惯了,不过有任何问题我还是很希望和大家讨论的。