一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序
本节内容是《一步一步学习使用LiveBindings(14)》中天气预报小程序的进一步优化。虽然编写代码创建TListView的列表项可以提供较大的灵活民生,但是造成代码复杂性增加,而且可重用性较弱。
注意:更理想的自定义列表项的的方法是为 TListView 组件编写自定义样式;将组件放入一个包中,安装到 IDE 中,然后从对象检查器窗口使用它。这样就可以在多个项目中重复的使用。
这一节将介绍如下的几个内容:
- 创建一个Delphi包,在Delphi包中创建自定义列表项。
- 使用自定义列表项进行数据绑定
- 将天气预报数据保存到本地内存表,通过LiveBindings进行显示。
1. 自定义列表项的整体设计思路
本节将创建一个自定义的FireMonkey列表项(TListView)外观类,主要用于显示天气预报信息,包含最低温度和最高温度的特殊显示效果。它的核心设计思路是:
继承体系:继承自TPresetItemObjects,这是FMX框架中预定义列表项外观的基类
定制化显示:在标准列表项基础上增加了两个温度显示字段(最低温度和最高温度)
响应式布局:根据可用空间自动调整布局(当空间不足时隐藏最低温度)
数据绑定支持:为LiveBindings提供数据成员支持。
整体效果如下所示:

2. 新建一个Delphi包,添加自定义列表项单元
1. 单击主菜单中的 File > New > Package ,创建一个新的包,另存为比如MyListViewItemAppearance这样的包名,然后在包中添加一个新的Unit,示例命名为DelphiCookbookListViewAppearanceU.pas。
对于程序员来说,从头开始写一个类,需要添加诸多的uses引用,一步步写好继承基类有点繁琐,无论是新手还是老手。有个模板可以参照和拷贝都是很有必要的。
注意:Delphi本身提供了几个案例,其中的ListViewMultiDetailAppearance案例非常接近于本案例的需求,本节将介绍的案例见下面的路径。
C:\Users\Public\Documents\Embarcadero\Studio\23.0\Samples\Object Pascal\Multi-Device Samples\User Interface\ListView\ListViewMultiDetailAppearance
建议打开这个案例,拷贝其中的代码,通过修改实现自己的需求。
在Delphi的FireMonkey框架中,从TPresetItemObjects继承创建自定义列表项外观时,需要重点实现以下几个核心方法和功能:
2.1 必须重载的关键方法
1. DefaultHeight: Integer
function TMyCustomAppearance.DefaultHeight: Integer;
begin
Result := MY_DEFAULT_HEIGHT; // 返回自定义的默认高度
end;
功能:定义列表项的默认高度,当未明确设置ItemHeight时使用。
2. GetGroupClass: TGroupClass
function TMyCustomAppearance.GetGroupClass: TPresetItemObjects.TGroupClass;
begin
Result := TMyCustomAppearance; // 返回当前类类型
end;
功能:返回当前类的类型,用于外观对象分组管理。
3. UpdateSizes(const FinalSize: TSizeF)
procedure TMyCustomAppearance.UpdateSizes(const FinalSize: TSizeF);
begin
BeginUpdate;
try
inherited;
// 自定义布局逻辑
Text.InternalWidth := FinalSize.Width * 0.6;
Detail.PlaceOffset.X := FinalSize.Width * 0.6;
finally
EndUpdate;
end;
end;
功能:根据最终尺寸调整子对象的布局和大小,实现响应式布局。
4. 构造函数 Create(const Owner: TControl)
constructor TMyCustomAppearance.Create(const Owner: TControl);
begin
inherited;
// 创建并初始化自定义外观对象
FMyObject := TTextObjectAppearance.Create;
FMyObject.Name := 'MyObject';
// ...其他初始化
AddObject(FMyObject, True);
end;
功能:创建和初始化所有自定义的外观对象。
5. 析构函数 Destroy
destructor TMyCustomAppearance.Destroy;
begin
FMyObject.Free; // 释放自定义对象
inherited;
end;
功能:清理创建的自定义对象。
2.2 主要的功能实现
除了重载方法之外,主要的自定义实现要点如下:
1. 自定义对象管理
声明私有字段保存自定义对象引用
private
FMyObject: TTextObjectAppearance;
FMyImage: TImageObjectAppearance;
在published部分公开自定义的属性,以便设计时可以编辑。
published
property MyObject: TTextObjectAppearance read FMyObject write SetMyObject;
property MyImage: TImageObjectAppearance read FMyImage write SetMyImage;
2. 对象初始化模板
推荐使用初始化模板避免重复代码:
procedure InitTextObject(AObject: TTextObjectAppearance);
begin
AObject.OnChange := ItemPropertyChange;
AObject.DefaultValues.Align := TListItemAlign.Leading;
// ...其他默认设置
AObject.RestoreDefaults;
AObject.Owner := Self;
end;
3. LiveBindings支持
为每个自定义对象设置DataMembers:
FMyObject.DataMembers := TObjectAppearance.TDataMembers.Create(
TObjectAppearance.TDataMember.Create(
'MyObject',
Format('Data["%s"]', ['myobject'])
)
);
TDelphiCookbookAppearanceNames自定义类的完整代码如下所示:
unit DelphiCookbookListViewAppearanceU;
interface
uses System.Types, FMX.ListView, FMX.ListView.Types, FMX.ListView.Appearances,
System.Classes, System.SysUtils,
FMX.Types, FMX.Controls, System.UITypes, FMX.MobilePreview;
type
// 定义DelphiCookbook的外观名称常量类
TDelphiCookbookAppearanceNames = class
public const
ListItem = 'DelphiCookbookWeatherAppearance'; // 列表项外观名称
MinTemp = 'mintemp'; // 最低温度字段名称
MaxTemp = 'maxtemp'; // 最高温度字段名称
end;
implementation
type
// 自定义列表项外观类,继承自TPresetItemObjects
TDelphiCookbookItemAppearance = class(TPresetItemObjects)
public const
DEFAULT_HEIGHT = 40; // 默认列表项高度
private
FMinTemp: TTextObjectAppearance; // 最低温度文本对象
FMaxTemp: TTextObjectAppearance; // 最高温度文本对象
procedure SetMinTemp(const Value: TTextObjectAppearance); // 设置最低温度属性
procedure SetMaxTemp(const Value: TTextObjectAppearance); // 设置最高温度属性
protected
function DefaultHeight: Integer; override; // 获取默认高度
procedure UpdateSizes(const FinalSize: TSizeF); override; // 更新尺寸
function GetGroupClass: TPresetItemObjects.TGroupClass; override; // 获取组类
public
constructor Create(const Owner: TControl); override; // 构造函数
destructor Destroy; override; // 析构函数
published
property Accessory; // 继承的附件属性
property Text; // 继承的文本属性
//自定义的属性
property MinTemp: TTextObjectAppearance read FMinTemp write SetMinTemp; // 最低温度属性
property MaxTemp: TTextObjectAppearance read FMaxTemp write SetMaxTemp; // 最高温度属性
end;
const
MIN_TEMP_MEMBER = 'MinTemp'; // 最低温度成员名称
MAX_TEMP_MEMBER = 'MaxTemp'; // 最高温度成员名称
// 构造函数实现
constructor TDelphiCookbookItemAppearance.Create(const Owner: TControl);
var
LInitTextObject: TProc<TTextObjectAppearance>; // 初始化文本对象的匿名方法
begin
inherited;
// 定义初始化文本对象的匿名方法
LInitTextObject := procedure(pTextObject: TTextObjectAppearance)
begin
pTextObject.OnChange := ItemPropertyChange; // 设置变更事件为基类的ItemPropertyChange事件
pTextObject.DefaultValues.Align := TListItemAlign.Leading; // 默认左对齐
pTextObject.DefaultValues.VertAlign := TListItemAlign.Center; // 默认垂直居中
pTextObject.DefaultValues.TextVertAlign := TTextAlign.Center; // 文本垂直居中
pTextObject.DefaultValues.TextAlign := TTextAlign.Trailing; // 文本右对齐
pTextObject.DefaultValues.PlaceOffset.Y := 0; // Y偏移量为0
pTextObject.DefaultValues.PlaceOffset.X := 0; // X偏移量为0
pTextObject.DefaultValues.Width := 80; // 默认宽度80
pTextObject.DefaultValues.Visible := True; // 默认可见
pTextObject.RestoreDefaults; // 恢复默认值
pTextObject.Owner := Self; // 设置所有者
end;
// 创建并初始化最低温度文本对象
FMinTemp := TTextObjectAppearance.Create;
FMinTemp.Name := TDelphiCookbookAppearanceNames.MinTemp; // 设置名称
FMinTemp.DefaultValues.TextColor := TAlphaColorRec.Blue; // 设置蓝色文本
LInitTextObject(FMinTemp); // 调用初始化方法
// 创建并初始化最高温度文本对象
FMaxTemp := TTextObjectAppearance.Create;
FMaxTemp.Name := TDelphiCookbookAppearanceNames.MaxTemp; // 设置名称
FMaxTemp.DefaultValues.TextColor := TAlphaColorRec.Red; // 设置红色文本
LInitTextObject(FMaxTemp); // 调用初始化方法
// 定义最低温度的LiveBindings数据成员
FMinTemp.DataMembers := TObjectAppearance.TDataMembers.Create
(TObjectAppearance.TDataMember.Create(MIN_TEMP_MEMBER,
// 用于LiveBindings显示的表达式
Format('Data["%s"]', [TDelphiCookbookAppearanceNames.MinTemp])));
// 从TListViewItem访问值的表达式
// 定义最高温度的LiveBindings数据成员
FMaxTemp.DataMembers := TObjectAppearance.TDataMembers.Create
(TObjectAppearance.TDataMember.Create(MAX_TEMP_MEMBER,
// 用于LiveBindings显示的表达式
Format('Data["%s"]', [TDelphiCookbookAppearanceNames.MaxTemp])));
// 从TListViewItem访问值的表达式
// 添加外观对象到列表项
AddObject(Text, True); // 添加文本对象
AddObject(MinTemp, True); // 添加最低温度对象
AddObject(MaxTemp, True); // 添加最高温度对象
end;
// 获取默认高度
function TDelphiCookbookItemAppearance.DefaultHeight: Integer;
begin
Result := DEFAULT_HEIGHT; // 返回常量定义的默认高度
end;
// 析构函数实现
destructor TDelphiCookbookItemAppearance.Destroy;
begin
FMinTemp.Free; // 释放最低温度对象
FMaxTemp.Free; // 释放最高温度对象
inherited; // 调用父类析构函数
end;
// 设置最低温度属性
procedure TDelphiCookbookItemAppearance.SetMinTemp
(const Value: TTextObjectAppearance);
begin
FMinTemp.Assign(Value); // 赋值最低温度对象
end;
// 设置最高温度属性
procedure TDelphiCookbookItemAppearance.SetMaxTemp
(const Value: TTextObjectAppearance);
begin
FMaxTemp.Assign(Value); // 赋值最高温度对象
end;
// 获取组类
function TDelphiCookbookItemAppearance.GetGroupClass
: TPresetItemObjects.TGroupClass;
begin
Result := TDelphiCookbookItemAppearance; // 返回当前类类型
end;
// 更新尺寸方法
procedure TDelphiCookbookItemAppearance.UpdateSizes(const FinalSize: TSizeF);
var
LColWidth: Extended; // 列宽度
LFullWidth: Boolean; // 是否全宽标志
begin
BeginUpdate; // 开始更新
try
inherited; // 调用父类方法
LColWidth := FinalSize.Width / 12; // 计算每列宽度(将总宽度分为12列)
LFullWidth := LColWidth * 4 >= MinTemp.Width; // 判断是否有足够空间显示最低温度
if LFullWidth then // 如果有足够空间
begin
MinTemp.Visible := True; // 显示最低温度
Text.InternalWidth := LColWidth * 6; // 设置文本宽度为6列
MinTemp.PlaceOffset.X := LColWidth * 6; // 设置最低温度X偏移
MinTemp.InternalWidth := LColWidth * 2; // 设置最低温度宽度为2列
MaxTemp.PlaceOffset.X := LColWidth * 9; // 设置最高温度X偏移
MaxTemp.InternalWidth := LColWidth * 2; // 设置最高温度宽度为2列
end
else // 如果空间不足
begin
MinTemp.Visible := False; // 隐藏最低温度
Text.InternalWidth := LColWidth * 8; // 设置文本宽度为8列
MaxTemp.PlaceOffset.X := LColWidth * 8; // 设置最高温度X偏移
MaxTemp.InternalWidth := LColWidth * 4; // 设置最高温度宽度为4列
end;
finally
EndUpdate; // 结束更新
end;
end;
const
sThisUnit = 'DelphiCookbookListViewAppearanceU'; // 当前单元名称常量
initialization
// 注册自定义外观
TAppearancesRegistry.RegisterAppearance(TDelphiCookbookItemAppearance,
TDelphiCookbookAppearanceNames.ListItem, [TRegisterAppearanceOption.Item],
sThisUnit);
finalization
// 注销自定义外观
TAppearancesRegistry.UnregisterAppearances
(TArray<TItemAppearanceObjectsClass>.Create(TDelphiCookbookItemAppearance));
end.
类创建完成后,还需要在initialization和finalization添加注册与取消注册代码,以便可以在Delphi对象检查器面板上发现。
将上面的代码与Delphi自带的示例MultiDetailAppearanceU.pas进行比较,可见代码上的发现诸多相似之处。
下面是TDelphiCookbookItemAppearance的类图

下面是MultiDetailAppearanceU.pas中的类:

可以看到TMultiDetailItemAppearance和TDelphiCookbookItemAppearance重载了相同的方法,TMultiDetailItemAppearance包含了更多的自定义的属性。
在自定义列表项代码骨架搭建后,你就可以右击Package项目,选择“Install”菜单项,将项目安装到Delphi中。然后新建一个FMX测试项目,在测试项目的加持下实现调试。

3. 在FMX程序中使用自定义列表项
在安装好后,可以为TListView指定自定义的列表项,如下图所示:

可以切换到设计模式,查看自定义列的设计效果。

现在,自定义的列表项还支持数据绑定。在这个例子中,添加了一个TFDMemTable控件,并且在Fields Editor中添加了4个永久性字段,如下所示:

day和description是string类型的字段,mintemp和maxtemp是float类型的字段,并指定DisplayFormat为#0.00°,显示2位小数位的度数值。
现在按钮单击事件处理代码如下:
procedure TMainForm.btnGetForecastsClick(Sender: TObject);
begin
// 清空ListView1中的项目(当前被注释掉)
// ListView1.Items.Clear;
// 设置REST请求参数:将城市和国家编辑框内容用逗号连接作为country参数值
RESTRequest1.Params.ParameterByName('country').Value :=
String.Join(',', [EditCity.Text, EditCountry.Text]);
// 设置REST请求参数:语言参数
RESTRequest1.Params.ParameterByName('lang').Value := Lang;
// 显示并启用加载指示器
AniIndicator1.Visible := True;
AniIndicator1.Enabled := True;
// 禁用获取预报按钮,防止重复请求
btnGetForecasts.Enabled := False;
// 异步执行REST请求
RESTRequest1.ExecuteAsync(
procedure
var
LForecastDateTime: TDateTime; // 预报日期时间
LJValue: TJSONValue; // JSON值对象
LJObj, LMainForecast, LForecastItem, LJObjCity: TJSONObject; // 各层JSON对象
LJArrWeather, LJArrForecasts: TJSONArray; // JSON数组
LTempMin, LTempMax: Double; // 最低和最高温度
LDay, LWeatherDescription, LAppRespCode: string; // 日期、天气描述、响应码
begin
// 将响应内容转换为JSON对象
LJObj := RESTRequest1.Response.JSONValue as TJSONObject;
// 检查错误响应
// 获取响应状态码
LAppRespCode := LJObj.GetValue('cod').Value;
// 处理404错误(城市未找到)
if LAppRespCode.Equals('404') then
begin
lblInfo.Text := '没有找到城市信息';
Exit; // 退出处理过程
end;
// 处理非200的成功响应
if not LAppRespCode.Equals('200') then
begin
lblInfo.Text := 'Error ' + LAppRespCode;
Exit; // 退出处理过程
end;
// 准备内存表接收数据
FDMemTable1.EmptyView; // 清空内存表视图
FDMemTable1.DisableControls; // 禁用控件刷新,提高批量操作性能
try
// 解析预报数据数组
LJArrForecasts := LJObj.GetValue('list') as TJSONArray;
// 遍历每个预报项
for LJValue in LJArrForecasts do
begin
// 将当前JSON值转换为对象
LForecastItem := LJValue as TJSONObject;
// 解析预报时间戳(Unix时间戳转换为Delphi的TDateTime)
LForecastDateTime := UnixToDateTime((LForecastItem.GetValue('dt')
as TJSONNumber).AsInt64);
// 获取主要天气信息对象
LMainForecast := LForecastItem.GetValue('main') as TJSONObject;
// 解析最低温度
LTempMin := (LMainForecast.GetValue('temp_min')
as TJSONNumber).AsDouble;
// 解析最高温度
LTempMax := (LMainForecast.GetValue('temp_max')
as TJSONNumber).AsDouble;
// 获取天气描述数组
LJArrWeather := LForecastItem.GetValue('weather') as TJSONArray;
// 获取第一个天气项的描述
LWeatherDescription := TJSONObject(LJArrWeather.Items[0])
.GetValue('description').Value;
// 格式化日期显示(星期几 日 月 年)
LDay := FormatDateTime('ddd d mmm yyyy', DateOf(LForecastDateTime));
// 将数据添加到内存表
FDMemTable1.Append; // 添加新记录
FDMemTable1day.AsString := LDay; // 设置日期字段
FDMemTable1description.Value := FormatDateTime('HH',
LForecastDateTime) + ' ' + LWeatherDescription; // 时间+天气描述
FDMemTable1mintemp.Value := LTempMin; // 设置最低温度
FDMemTable1maxtemp.Value := LTempMax; // 设置最高温度
FDMemTable1.Post; // 提交记录
end; // 结束遍历
finally
// 确保以下操作无论是否发生异常都会执行
FDMemTable1.EnableControls; // 重新启用控件刷新
BindSourceDB1.ResetNeeded; // 通知绑定组件数据已更新
FDMemTable1.First; // 定位到第一条记录
end;
// 解析城市信息
LJObjCity := LJObj.GetValue('city') as TJSONObject;
// 更新界面显示城市和国家信息
lblInfo.Text := LJObjCity.GetValue('name').Value + ', ' +
LJObjCity.GetValue('country').Value;
// 隐藏并禁用加载指示器
AniIndicator1.Visible := False;
AniIndicator1.Enabled := False;
// 重新启用获取预报按钮
btnGetForecasts.Enabled := True;
end);
end;
在单击事件处理中,解析JSON数据后,现在将数据直接添加到了内存表中,然后调用BindSourceDB1.ResetNeeded通知绑定数据已经更新,刷新控件的显示。
现在单击事件处理代码并不负责ListView的显示工作,而是交给LiveBindings实现了这一切,下图是LiveBindings Designer上添加的绑定效果:

最后运行一下,看看效果是否如预期:

效果非常好,并且只要安装好了这个包,以后在很多地方者可以重用这个自定义的样式,实在是太方便。
总结
这一节的内容并不复杂,但是非常实用。通过本课的学习,可以了解到:
- 如何设计自定义列。
- 实现自定义列的一般方法。
- 如何在应用程序中使用自定义列。
通过本课的学习,相信你也可以设计出更加美观的自定义列表项。
一步一步学习使用LiveBindings(15)TListView进阶使用(3),创建自定义的列表项打造天气预报程序的更多相关文章
- 每日学习心得:SharePoint 2013 自定义列表项添加Callout菜单项、文档关注、SharePoint服务端对象模型查询
前言: 前一段时间一直都比较忙,没有什么时间进行总结,刚好节前项目上线,同时趁着放假可以好好的对之前遇到的一些问题进行总结.主要内容有使用SharePoint服务端对象模型进行查询.为SharePoi ...
- 12.Linux软件安装 (一步一步学习大数据系列之 Linux)
1.如何上传安装包到服务器 有三种方式: 1.1使用图形化工具,如: filezilla 如何使用FileZilla上传和下载文件 1.2使用 sftp 工具: 在 windows下使用CRT 软件 ...
- (转) 一步一步学习ASP.NET 5 (四)- ASP.NET MVC 6四大特性
转发:微软MVP 卢建晖 的文章,希望对大家有帮助.原文:http://blog.csdn.net/kinfey/article/details/44459625 编者语 : 昨晚写好的文章居然csd ...
- (转) 一步一步学习ASP.NET 5 (二)- 通过命令行和sublime创建项目
转发:微软MVP 卢建晖 的文章,希望对大家有帮助. 注:昨天转发之后很多朋友指出了vNext的命名问题,原文作者已经做出了修改,后面的标题都适用 asp.net 5这个名称. 编者语 : 昨天发了第 ...
- 一步一步学习SignalR进行实时通信_1_简单介绍
一步一步学习SignalR进行实时通信\_1_简单介绍 SignalR 一步一步学习SignalR进行实时通信_1_简单介绍 前言 SignalR介绍 支持的平台 相关说明 OWIN 结束语 参考文献 ...
- 一步一步学习SignalR进行实时通信_8_案例2
原文:一步一步学习SignalR进行实时通信_8_案例2 一步一步学习SignalR进行实时通信\_8_案例2 SignalR 一步一步学习SignalR进行实时通信_8_案例2 前言 配置Hub 建 ...
- 一步一步学习SignalR进行实时通信_9_托管在非Web应用程序
原文:一步一步学习SignalR进行实时通信_9_托管在非Web应用程序 一步一步学习SignalR进行实时通信\_9_托管在非Web应用程序 一步一步学习SignalR进行实时通信_9_托管在非We ...
- 一步一步学习SignalR进行实时通信_7_非代理
原文:一步一步学习SignalR进行实时通信_7_非代理 一步一步学习SignalR进行实时通信\_7_非代理 SignalR 一步一步学习SignalR进行实时通信_7_非代理 前言 代理与非代理 ...
- 一步一步学习SignalR进行实时通信_5_Hub
原文:一步一步学习SignalR进行实时通信_5_Hub 一步一步学习SignalR进行实时通信\_5_Hub SignalR 一步一步学习SignalR进行实时通信_5_Hub 前言 Hub命名规则 ...
- 一步一步学习SignalR进行实时通信_6_案例
原文:一步一步学习SignalR进行实时通信_6_案例 一步一步学习SignalR进行实时通信\_6_案例1 一步一步学习SignalR进行实时通信_6_案例1 前言 类的定义 各块功能 后台 上线 ...
随机推荐
- 推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化 ...
- 化学数据分析AI实验室?ChatMoney帮你打造
本文由 ChatMoney团队出品 AI确实是个好东西,但AI到底是有什么用?其实很多人都没搞明白.AI如果在某个行业用的好,是能带来很大经济价值的.就拿AI在化学应用来说,AI在化工领域上的应用和化 ...
- 阿里微服务解决方案-Alibaba Cloud之集成Nacos(服务注册与发现)(三)
一.集成 Nacos(服务注册与发现) 1.1 下载 Nacos Nacos下载地址 1.2 下载后解压到本地 1.3 启动 Nacos 启动成功界面 输入 http://127.0.0.1:8848 ...
- UFT 对时间的处理
1. 当前时间的后n天 2. 当前时间的前n天 3. 当前时间 eg:
- UFT RegExp
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(13)
1.问题描述: 推送通知里的skills标签有什么用?不填写似乎不影响推送,以及推送的点击跳转操作 解决方案: 鸿蒙系统的推送通知中的skills标签主要用于指定接收推送的应用程序所支持的能力(Ski ...
- DBA备库工具:Oracle环境中表空间全自动扩容
我们的文章会在微信公众号IT民工的龙马人生和博客网站( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢! 由于博客中有大量代码,通过页面浏览效 ...
- 如何识别SQL Server中需要添加索引的查询
引言 在数据库性能优化中,索引是提升查询速度最有效的手段之一.然而,不恰当的索引会降低写操作性能并增加存储开销.作为DBA,我们经常面临这样的挑战:如何精准定位哪些查询真正需要添加索引? 本文将分享几 ...
- java--Struts框架基础
基于mvc模式的应用框架之struts Struts就是基于mvc模式的框架! (struts其实也是servlet封装,提高开发效率!) Struts开发步骤: 1. web项目,引入struts ...
- Excel加载宏.xla文件的使用方法
将文件保存到本地的一个位置. 2.打开任意的excel表格,并按照如下的图片所示的步骤操作: 1)点击development(开发工具)add-ins浏览,然后找到刚刚文件的保存位置并选择文件(如果没 ...