Delphi的“动态窗体”技术实际应用  
日期:2005年6月1日   作者:On2008   人气:613   查看:[大字体   中字体   小字体]    
在Delphi可视化设计环境中,允许程序员在代码编辑器中以文本的方式浏览和修改DFM文件内容。当用File/Open命令直接打开DFM文件或者选择窗体设计窗口的弹出式菜单上的View   as   Text命令时,就会在编辑器中出现文本形式的信息。在一些资料中将这种文本形式称之为窗体设计脚本。Delphi提供的这种脚本编辑功能是对Delphi可视化设计的一大补充。当然这个脚本编辑能力是有限制的,比方说不能在脚本任意地添加和删除部件,因为代码和DFM脚本是紧密相连的,任意添加和修改会导致不一致性。但在动态生成的DFM文件中,就不存在这一限制。
  实际上,DFM文件内容是二进制数据,它的脚本是经过Delphi开发环境自动转化的,而且Delphi   VCL中的Classes库单元提供了在二进制流中的文件DFM和它的脚本之相互转化的过程。它们是ObjectBinaryToText和ObjectTextToBinary、ObjectResourceToText和ObjectTextToResource。
  ObjectBinaryToText过程将二进制流中存储的部件转化为基于文本的表现形式,这样就可以用文本处理函数进行处理,还可以用文本编辑器进行查找和替代操作,最后可以将文本再转化成二进制流中的部件。
  ObjectTextToBinary过程执行的功能与ObjectBinaryToText相反,将TXT文件转换为二进制流中的部件,而且只要TXT文件内容的书写符合DFM脚本语法,ObjectTextToBinary可将任何程序生成的TXT文件转换为部件,这一功能也为DFM文件的动态生成和编辑奠定了基础。

如何在运行过程中将本窗体保存成一个文本格式的.dfm文件?
zswang(伴水) (2001-11-21   9:52:59)   得0分
function   ComponentToString(Component:   TComponent):   string;
var
BinStream:   TMemoryStream;
StrStream:   TStringStream;
s:   string;
begin
BinStream   :=   TMemoryStream.Create;
try
StrStream   :=   TStringStream.Create(s);
try
BinStream.WriteComponent(Component);
BinStream.Seek(0,   soFromBeginning);
ObjectBinaryToText(BinStream,   StrStream);
StrStream.Seek(0,   soFromBeginning);
Result   :=   StrStream.DataString;
finally
StrStream.Free;
end;
finally
BinStream.Free
end;
end;   {   ComponentToString   }
function   StringToComponent(Value:   string;   Instance:   TComponent):   TComponent;
var
StrStream:   TStringStream;
BinStream:   TMemoryStream;
begin
StrStream   :=   TStringStream.Create(Value);
try
BinStream   :=   TMemoryStream.Create;
try
ObjectTextToBinary(StrStream,   BinStream);
BinStream.Seek(0,   soFromBeginning);
Result   :=   BinStream.ReadComponent(Instance);
finally
BinStream.Free;
end;
finally
StrStream.Free;
end;
end;   {   StringToComponent   }
 
回复人: zswang(伴水) (2001-11-21   9:58:13)   得0分
procedure   TForm1.Button2Click(Sender:   TObject);
begin
StringToComponent(
'object   Label1:   TLabel '#13#10   +
'   Left   =   232 '#13#10   +
'   Top   =   56 '#13#10   +
'   Width   =   26 '#13#10   +
'   Height   =   13 '#13#10   +
'   Caption   =   #20320#22909 '#13#10   +
'   Font.Charset   =   GB2312_CHARSET '#13#10   +
'   Font.Color   =   clRed '#13#10   +
'   Font.Height   =   -13 '#13#10   +
'   Font.Name   =   #23435#20307 '#13#10   +
'   Font.Style   =   [] '#13#10   +
'   ParentFont   =   False '#13#10   +
'end '#13#10,   Label1);
end;
//要注册类
==end=================================
好了,理解了上面的这段文字,一些朋友就会自然想到,利用这几个函数应该可以弄出点有用的东西出来,我就弄出了一点应用,并全面应用到了项目中,现在我来给大家完整描述出来:
首先我要求我的程序有如下能力:
1.   我的程序的窗体是可以动态替换的,不用编译Exe,只要替换一个DFM窗体设计脚本就可以了(当然,你可以重新包装一下这个DFM文件,比如换成txt后缀名等)。
2.   我可以预览所有的DFM文件,让它变成实际的Form察看。
不要小看这两点,在很多情况下,这意义非常重大,举几个例子①开发阶段,可以把界面设计和程序设计完全分开,分工进行②现场维护时,有些界面的调整和功能设置不需要再找源代码到Delphi下去编译一遍了,老出差做Mis类的朋友应该能从这点体会出好处③某些功能界面的升级简单了不少,只要让用户下载一个DFM文件覆盖原来的就可以了。
好,不费话了,下面详细说明怎么达到以上两点要求。
显然我们要让一段文本变成一个Form,那么就用这个函数:
function   StringToComponent(Value:   string;   Instance:TComponent):   TComponent;
var
StrStream:TStringStream;
BinStream:   TMemoryStream;
begin
StrStream   :=   TStringStream.Create(Value);
try
BinStream   :=   TMemoryStream.Create;
try
ObjectTextToBinary(StrStream,   BinStream);
BinStream.Seek(0,   soFromBeginning);
Result   :=   BinStream.ReadComponent(Instance);
finally
BinStream.Free;
end;
finally
StrStream.Free;
end;
end;
但是,所有的Class必须是注册过的,例如,如下的Form1FRM.DFM文件
object   Form1:   TForm1
Left   =   222
Top   =   168
Width   =   485
Height   =   290
Caption   =   'Form1 '
Color   =   clBtnFace
Font.Charset   =   DEFAULT_CHARSET
Font.Color   =   clWindowText
Font.Height   =   -11
Font.Name   =   'MS   Sans   Serif '
Font.Style   =   []
OldCreateOrder   =   False
PixelsPerInch   =   96
TextHeight   =   13
object   Panel1:   TPanel
Left   =   0
Top   =   0
Width   =   477
Height   =   33
Align   =   alTop
TabOrder   =   0
object   BitBtn1:   TBitBtn
Left   =   4
Top   =   4
Width   =   75
Height   =   25
Caption   =   'OK '
TabOrder   =   0
end
end
object   Memo1:   TMemo
Left   =   0
Top   =   33
Width   =   477
Height   =   230
Align   =   alClient
TabOrder   =   1
end
end
你应该这么使用,
var   list:TstringList;form:TForm

list.Lines.LoadFromFile(‘Form1FRM.DFM’);
RegisterClass(TForm1);
RegisterClass(TPanel);
RegisterClass(TBitBtn);
RegisterClass(TMemo);
form   :=   StringToComponent(list.Lines.Text,nil);
form.ShowModal();

这样就能显示出一个窗体了。
但是这有个问题,Delphi自带的VCL控件是固定的,用RegisterClass(…)注册一遍没有问题,可TForm1不是,如果连TForm1都要注册的话,就无法达成第2点要求。我们可以变通一下,因为所有的Form都是从Tform继承的,所以,应该都可以用注册Tform来取代,因此,有了下面这样一个函数:
function   LoadTextForm(FileName:String):TForm;
var
list:TStrings;
FirstLine:String;
iPos   :   Integer;
Form   :   TForm;
begin
Result   :=   nil;
if   FileExists(FileName)=False   then
Exit;
Form   :=   TForm.Create(Application);
list   :=   TStringList.Create;
try
list.LoadFromFile(FileName);
if   list.Count=0   then
Exit;
FirstLine   :=   list[0];
iPos   :=   Pos( ':   ',FirstLine);
if   iPos   =   0   then   //找不到 ':   ',格式不对
Exit;
list[0]:=Copy(FirstLine,1,iPos)+ '   TForm ';
DeleteErrorLines(list);
StringToComponent(list.Text,Form);
Result   :=   Form;
except
Form.Free;
Result   :=   nil;
end;
list.Free;
end;
原理就是读入DFM文件后把窗体的类别偷换成Tform。其中还有一个函数:
procedure   DeleteErrorLines(list:TStrings);
var
i:Integer;
line:String;
begin
if   list.Count=0   then
Exit;

i:=0;
while   i <list.Count   do
begin
line   :=   Trim(list[i]);
if   Copy(line,1,2)= 'On '   then
list.Delete(i)
else
Inc(i);
end;
end;
这个函数是把凡是含有“On”开头的行删除,应为在Delphi中,所有控件的事件都是以“On”开头,删除了这样的行,就能保证StringToComponent(list.Text,Form);不出错,用以上的两个函数就可以写一个DFM窗体察看器了,到目前为止,我还没有搜到哪个人发布了DFM窗体察看器。这样我们就完成了第2个要求。

对我有用[0]丢个板砖[0]引用举报管理TOP精华推荐:想做一个所见即所得的html编辑器 谁有这方面技术资料 谢谢了

xthmpro_cn
([可人])

等 级:

#5楼 得分:0回复于:2005-06-27 08:57:54实际应用中,一个窗体几乎肯定会有事件处理函数,所以我们要达成第1个要求。我这儿提供了两个方案,各有优缺点:
方案一:
程序员在开发时,在窗体的FormCreate(…)中,用LoadTextForm(…)生成窗体文件,然后把窗体上的控件全部移到本窗体上,最后查找窗体上的控件,动态设置事件处理函数。这个方法要求有一套好的控件命名规则,而且开发比较烦琐,享受不到Delphi的IDE所见即所得,自动生成事件关联代码的好处了。不过对Form文件的制作人员限制很小,他们可以直接用Delphi来制作窗体。
方案二:
用这个函数
procedure   ReadForm(aFrom   :   TComponent;aFileName   :string= ' ');
var
FrmStrings   :   TStrings;
begin
RegisterClass(TPersistentClass(aFrom.ClassType));
FrmStrings:=TStringlist.Create   ;
try
if   trim(aFileName)= ' '   then   FrmStrings.LoadFromFile(   gsPathInfo+ '\ '+aFrom.Name+ '.txt ')
else   FrmStrings.LoadFromFile(aFileName);
while   aFrom.ComponentCount> 0   do   aFrom.Components[0].Destroy   ;
aFrom:=StringToComponent(FrmStrings.Text,aFrom)
finally
FrmStrings.Free;
end;
UnRegisterClass(TPersistentClass(aFrom.ClassType));
end;
在FormCreate中调用ReadForm(self,…)。
这个方案没有第一个方案的限制,但是要求开发人员必须先完成一个完整的Form文件交给Form文件制作人员,   Form文件的制作人员不能修改控件的name,不能添加或删除控件,而且必须保留开发人员给定所有事件处理函数,不能修改函数名。不过很多问题可以写一个Form编辑器来保证不出问题。
具体代码就不写了。
我想,肯定还有跟好的方案来解决动态窗体的问题,希望大家讨论。
(以上代码使用Delphi6编写)
最后,我给出一个我实际项目中的有关动态窗体的函数的Unit
{*****************************************
模块编号:J001DfmFunc
模块名称:Dfm窗体函数集单元
作者:刘爱军
建立日期:2002年12月2日
最后修改日期:
说明:本Unit包含了一些函数,用于根据Delphi窗体文件格式的文件动态创建窗体
*******************************************}

unit   J001DfmFunc;

interface

uses
Windows,   Messages,   SysUtils,   Variants,   Classes,   Graphics,   Controls,   Forms,
Dialogs,   ExtCtrls,   DBCtrls,   Grids,   DBGrids,   Buttons,   StdCtrls,
ComCtrls,dbcgrids,   buttonComps,Tabs,QryGlobal;

type
TAllComponentClass   =   Array   of   TPersistentClass;

procedure   InitClassType(ClassArray:TAllComponentClass);

function   ComponentToString(Component:   TComponent):   string;
function   StringToComponent(Value:   string;   Instance:TComponent):   TComponent;
procedure   RegisterAllClasses(aAllCmpClass:TAllComponentClass);
procedure   UnRegisterAllClasses(aAllCmpClass:TAllComponentClass);
function   GetObjectString(list:TStrings;BegLine:Integer=0;TypeString:string= ' '):string;
function   LoadTextForm(FileName:String):TForm;
function   LoadTextForm2(FileName:String;out   ErrMsg:string):TForm;
procedure   DeleteErrorLines(list:TStrings);
procedure   ReadForm(aFrom   :   TComponent;aFileName   :string= ' ');
const
RegisteredCompoentClassCount   =   32;//数组大小

var
AllCmpClass   :   TAllComponentClass;   //存放控件类

implementation

//初始化可以解析的类,可随需要增加
procedure   InitClassType(ClassArray:TAllComponentClass);
begin
SetLength(AllCmpClass,RegisteredCompoentClassCount);
AllCmpClass[0]   :=   TForm;
AllCmpClass[1]   :=   TGroupBox;
AllCmpClass[2]   :=   TPanel;
AllCmpClass[3]   :=   TScrollBox;
AllCmpClass[4]   :=   TLabel;
AllCmpClass[5]   :=   TButton;
AllCmpClass[6]   :=   TBitBtn;
AllCmpClass[7]   :=   TSpeedButton;
AllCmpClass[8]   :=   TStringGrid;
AllCmpClass[9]   :=   TImage;
AllCmpClass[10]   :=   TBevel;
AllCmpClass[11]   :=   TStaticText;
AllCmpClass[12]   :=   TTabControl;
AllCmpClass[13]   :=   TPageControl;
AllCmpClass[14]   :=   TTabSheet;
AllCmpClass[15]   :=   TDBNavigator;
AllCmpClass[16]   :=   TDBText;
AllCmpClass[17]   :=   TDBEdit;
AllCmpClass[18]   :=   TDBMemo;
AllCmpClass[19]   :=   TDBGrid;
AllCmpClass[20]   :=   TDBCtrlGrid;
AllCmpClass[21]   :=   TMemo;
AllCmpClass[22]   :=   TSplitter;
AllCmpClass[23]   :=   TCheckBox;
AllCmpClass[24]   :=   TEdit;
AllCmpClass[25]   :=   TListBox;
AllCmpClass[26]   :=   TComboBox;
AllCmpClass[27]   :=   TDateTimePicker;
AllCmpClass[28]   :=   TImageButton;
AllCmpClass[29]   :=   TTabSet;
AllCmpClass[30]   :=   TTreeView;
AllCmpClass[31]   :=   TListView;

end;
 
 
对我有用[0]丢个板砖[0]引用举报管理TOP精华推荐:这样算不算线程死锁状态

xthmpro_cn
([可人])

等 级:

#6楼 得分:0回复于:2005-06-27 08:58:14procedure   RegisterAllClasses(aAllCmpClass:TAllComponentClass);
var
i:Integer;
begin
for   i:=0   to   RegisteredCompoentClassCount-1   do
RegisterClass(aAllCmpClass[i]);
end;

procedure   UnRegisterAllClasses(aAllCmpClass:TAllComponentClass);
var
i:Integer;
begin
for   i:=0   to   RegisteredCompoentClassCount-1   do
UnRegisterClass(aAllCmpClass[i]);
end;

function   ComponentToString(Component:   TComponent):   string;
var
BinStream:TMemoryStream;
StrStream:   TStringStream;
s:   string;
begin
BinStream   :=   TMemoryStream.Create;
try
StrStream   :=   TStringStream.Create(s);
try
BinStream.WriteComponent(Component);
BinStream.Seek(0,   soFromBeginning);
ObjectBinaryToText(BinStream,   StrStream);
StrStream.Seek(0,   soFromBeginning);
Result:=   StrStream.DataString;
finally
StrStream.Free;

end;
finally
BinStream.Free
end;
end;

function   StringToComponent(Value:   string;   Instance:TComponent):   TComponent;
var
StrStream:TStringStream;
BinStream:   TMemoryStream;
begin
StrStream   :=   TStringStream.Create(Value);
try
BinStream   :=   TMemoryStream.Create;
try
ObjectTextToBinary(StrStream,   BinStream);
BinStream.Seek(0,   soFromBeginning);
Result   :=   BinStream.ReadComponent(Instance);

finally
BinStream.Free;
end;
finally
StrStream.Free;
end;
end;

function   GetObjectString(list:TStrings;BegLine:Integer=0;TypeString:string= ' '):string;
var
i,iBegCount,iEndCount:Integer;
ObjString,Line,ClassStr:String;
begin
iBegCount:=0;
iEndCount:=0;
ClassStr   :=   Trim(UpperCase(TypeString));
for   i:=BegLine   to   list.Count-1   do
begin
line   :=   UpperCase(list[i]);
if   Pos( 'OBJECT ',line)> 0   then
begin
if   (TypeString= ' ')   or   (Pos( ':   '+ClassStr,line)> 0)   then
Inc(iBegCount);
end
else   if   (iBegCount> iEndCount)   and   (trim(line)= 'END ')   then
Inc(iEndCount);

if   iBegCount> 0   then
Result   :=   Result   +   list[i]   +   #13#10;

if   (iBegCount> 0)   and   (iBegCount=iEndCount)   then
Exit;
end;
end;

procedure   DeleteErrorLines(list:TStrings);
var
i:Integer;
line:String;
begin
if   list.Count=0   then
Exit;

i:=0;
while   i <list.Count   do
begin
line   :=   Trim(list[i]);
if   Copy(line,1,2)= 'On '   then
list.Delete(i)
else
Inc(i);
end;
end;
procedure   ReadForm(aFrom   :   TComponent;aFileName   :string= ' ');
var
FrmStrings   :   TStrings;
begin
RegisterClass(TPersistentClass(aFrom.ClassType));
FrmStrings:=TStringlist.Create   ;
try
if   trim(aFileName)= ' '   then   FrmStrings.LoadFromFile(   gsPathInfo+ '\ '+aFrom.Name+ '.txt ')
else   FrmStrings.LoadFromFile(aFileName);
while   aFrom.ComponentCount> 0   do   aFrom.Components[0].Destroy   ;
aFrom:=StringToComponent(FrmStrings.Text,aFrom)
finally
FrmStrings.Free;
end;
UnRegisterClass(TPersistentClass(aFrom.ClassType));
end;
function   LoadTextForm(FileName:String):TForm;
var
list:TStrings;
FirstLine:String;
iPos   :   Integer;
Form   :   TForm;
begin
Result   :=   nil;

if   FileExists(FileName)=False   then
Exit;

Form   :=   TForm.Create(Application);
list   :=   TStringList.Create;
try
list.LoadFromFile(FileName);
if   list.Count=0   then
Exit;

FirstLine   :=   list[0];
iPos   :=   Pos( ':   ',FirstLine);
if   iPos   =   0   then   //找不到 ':   ',格式不对
Exit;

list[0]:=Copy(FirstLine,1,iPos)+ '   TForm ';

DeleteErrorLines(list);

StringToComponent(list.Text,Form);
Result   :=   Form;
except
Form.Free;
Result   :=   nil;
end;
list.Free;
end;
function   LoadTextForm2(FileName:String;out   ErrMsg:string):TForm;
var
list:TStrings;
FirstLine:String;
iPos   :   Integer;
Form   :   TForm;
begin
Result   :=   nil;

if   FileExists(FileName)=False   then
begin
ErrMsg   :=   '无效的文件名! ';
Exit;
end;

Form   :=   TForm.Create(Application);
list   :=   TStringList.Create;
try
list.LoadFromFile(FileName);
if   list.Count=0   then
Exit;

FirstLine   :=   list[0];
iPos   :=   Pos( ':   ',FirstLine);
if   iPos   =   0   then   //找不到 ':   ',格式不对
begin
ErrMsg   :=   '找不到 ' ':   ' ',文件格式不对 ';
Exit;
end;

list[0]:=Copy(FirstLine,1,iPos)+ '   TForm ';

DeleteErrorLines(list);

StringToComponent(list.Text,Form);
Result   :=   Form;
except
on   e:exception   do
begin
Form.Free;
Result   :=   nil;
ErrMsg   :=   '读入文件错误: '+e.Message;
end;
end;
list.Free;
end;

initialization
begin
InitClassType(AllCmpClass);
RegisterAllClasses(AllCmpClass);
end;
finalization
UnRegisterAllClasses(AllCmpClass);
end.

Delphi的“动态窗体”技术实际应用[网络摘抄]相关推荐

  1. Web应用工作原理、动态网页技术

    我们知道应用程序有两种模式,C/S模式和B/S模式.C/S模式是客户端/服务器模式,这类 应用程序一般独立的运行.B/S模式是浏览器/服务器模型,需要借助浏览器来运行. web应用程序一般就是B/S模 ...

  2. 2018-2019-2 20165312《网络攻防技术》Exp7 网络欺诈防范

    2018-2019-2 20165312<网络攻防技术>Exp7 网络欺诈防范 目录 一.相关知识点总结 二.实验内容 三.实验步骤 四.实验总结及问题回答 五.实验中遇到的问题及解决方法 ...

  3. 浅析IRF虚拟化技术增强企业网络架构的弹性

    浅析IRF虚拟化技术增强企业网络架构的弹性  [摘要]随着"云"时代到来和各种虚拟化技术日趋成熟,对传统企业网络架构提出新挑战.例如:在不破坏企业原有网络架构和资产投入情况下,可以 ...

  4. Web技术的发展 网络发展简介(三)

    在上一篇文章中,对TCP/IP通信协议进行了简单的介绍 通信协议是通信的理论基石,计算机.操作系统以及各种网络设备对通信的支持是计算机网络通信的物质基础 而web服务则是运行于应用层,借助于应用层的协 ...

  5. 流媒体技术在宽带网络的应用与发展

    流媒体技术在宽带网络的应用与发展 作者/来源:中国电信 梁晓辉 游志胜 摘 要 文章介绍了流媒体的概念与特点,深入分析了流媒体系统及其关键技术,研究了流媒体技术在宽带网络中的应用及在国内外的发展情况, ...

  6. 知识追踪之动态键值对记忆网络 Dynamic Key-Value Memory Networks for Knowledge Tracing

    文章目录 简介 动态键值对记忆网络 DKVMN原理图详细介绍 简介 本文分三个部分介绍: 动态键值对记忆网络的详细介绍以及如何将记忆增强神经网络(MANN)引入到知识追踪领域. DKVMN模型是如何进 ...

  7. 所谓的CDN动态加速技术

    以前说CDN的优势是其在网络边缘缓存了用户请求的内容,离用户近,从而保证用户的访问效果:但是动态网页由于是源站动态生成的内容,CDN的边缘节点无法存储用户请求的内容,请求到了边缘节点之后还得回源,传统 ...

  8. 物联网技术NB-IOT的网络和应用场景

    物联网技术NB-IOT的网络和应用场景 NB-IoT的全称是Narrow Band-Internet of Things,是基于蜂窝网络的窄带物联网技术,聚焦于低功耗广域网,支持物联网设备在广域网的蜂 ...

  9. 虚拟化技术在企业网络中的应用

    2019-11-试题二 虚拟化技术在企业网络中的应用 虚拟化技术已经广泛应用于各类应用中,结合自己参与设计的系统加以评估,写出一篇有自己特色的论文.请围绕"虚拟化技术在企业网络中的应用&qu ...

最新文章

  1. Spring WebClient vs. RestTemplate
  2. linux使用yum本地源
  3. 南京秦淮灯会亮灯迎春 明城墙内外“飘”年味
  4. 锦州中学高考2021成绩查询,锦州高中成绩排名2021,锦州中考分数线排行榜
  5. 深度学习如何均衡精度、内存、计算和通信开销?
  6. Go学习笔记(一)windows下的Go 语言环境安装,并运行第一个Hello World程序
  7. Matlab滤波函数
  8. python对文件操作方法是_Python文件操作
  9. 期刊检索级别简单介绍
  10. Apache详细的安装和配置
  11. c语言3f,C语言%.4f与%3f代表是什么意思?
  12. python教你用骰子拼图
  13. ORACLE数据库练习题整理(2)
  14. html5中表单属性值_如何在HTML表单中使用Autocompletetype属性
  15. (一)改掉这些坏习惯,还怕写不出健壮的代码?
  16. ARMv8 Cortex-a 编程向导手册学习_2.ARMv8-A 寄存器
  17. python绘制缓和曲线_CAD中缓和曲线的画法 - AutoCAD基础应用 - CAD论坛 - 明经CAD社区 - Powered by Discuz!...
  18. 大厂 Java 后端经典面试题:Redis 为什么这么快?
  19. 狗子课堂 二 虚拟机配置
  20. 23 20210525+0529直播 企业微信接口测试实战1+2

热门文章

  1. rpm(centos)软件包管理工具
  2. Python之路:线程池
  3. Androidclient与服务端交互之登陆演示样例
  4. 【转】VS.NET2003 调试无法启动
  5. 模拟用户登陆Web应用
  6. 面试中常见的查找与排序
  7. go/git设置及取消代理方法
  8. Number Theory Problem(The 2016 ACM-ICPC Asia China-Final Contest 找规律)
  9. 【python 3.6】python读取json数据存入MySQL(一)
  10. IDEA開發 java web 初步