- 一、什么是动态创建组件?
- 二、方案对比:如何实现动态创建?
- 三、Dynamic Components 扩展介绍
- 四、基础操作:创建按钮和标签
- 五、动态组件的事件处理
- 六、动态创建布局容器
- 七、循环创建列表项
- 八、动态删除组件
- 九、实战案例:动态问卷调查
- 十、常见问题与注意事项
- 十一、扩展阅读与资源
一、什么是动态创建组件?
在 App Inventor 2 中,通常情况下你需要先在设计视图中拖放组件(按钮、标签、文本框等),然后在逻辑视图中编写它们的交互逻辑。这种方式称为静态布局——组件在应用启动时就已经确定。
然而,在很多实际场景中,你无法提前预知界面上需要多少个组件,或者需要根据用户的操作即时生成新的界面元素。这时就需要动态创建组件——在程序运行时,通过代码块来创建新的组件实例,并添加到界面中。
典型适用场景
| 场景 | 说明 |
|---|---|
| 待办事项列表 | 用户每添加一个任务,就动态创建一个新的复选框+标签行 |
| 动态表单 | 根据用户选择的选项,生成不同数量的输入框 |
| 购物车 | 每件商品对应一个动态创建的商品卡片 |
| 聊天消息流 | 每条新消息都是一个动态创建的布局块 |
| 成绩单/报表 | 根据数据条目动态生成行和列的网格 |
| 图片轮播 | 根据服务器返回的图片URL列表,动态创建多个图片组件 |
二、方案对比:如何实现动态创建?
在 App Inventor 2 中,实现动态创建组件主要有以下几种方案:
方案一:使用 MIT 官方 Dynamic Components 扩展
官方扩展,由 MIT 的 CML(Center for Mobile Learning)团队维护。这是目前最推荐的方式。
- 优点:官方维护、功能完整、支持事件绑定、支持多种组件类型
- 缺点:需要导入 .aix 扩展文件
- 适用:大多数动态创建场景
方案二:使用 Taifun 的扩展(TaifunDW)
社区开发者 Taifun 提供的扩展,功能同样强大。
- 优点:社区成熟、文档丰富
- 缺点:第三方扩展,依赖个人维护
- 适用:需要额外扩展功能时
方案三:预置可见组件 + 数据源控制
不真正动态创建组件,而是预先在设计中放置足够数量的组件(如标签、布局),通过改变它们的 Visible 属性和文本内容来模拟动态效果。
- 优点:无需任何扩展,纯原生功能
- 缺点:组件数量固定、不够灵活、代码冗长
- 适用:只需要少量变化且组件数量可预估的场景
方案四:使用ListView的动态条目
利用 ListView 组件的 Elements 属性,通过后端数据驱动列表显示。
- 优点:自带滚动、无需扩展、性能好
- 缺点:只能显示列表,无法自定义复杂布局
- 适用:简单的文本列表场景
推荐方案
对于大多数情况,推荐使用 MIT 官方 Dynamic Components 扩展。它功能完善、由 MIT 团队维护,后续升级有保障。
三、Dynamic Components 扩展介绍
3.1 下载与导入
下载地址:在 MIT App Inventor 的扩展管理页面中搜索 “DynamicComponents1” 即可找到。扩展由 MIT CML 团队开发并托管。
导入步骤:
- 打开 MIT App Inventor 2 编辑器
- 点击菜单栏 Extensions(扩展)
- 选择 Import extension(导入扩展)
- 在弹出的 URL 输入框中输入扩展的 .aix 文件地址,或从本地选择 .aix 文件
- 导入成功后,在组件面板的 Extension(扩展) 下会出现 DynamicComponents1 组件
3.2 核心方法概览
DynamicComponents1 扩展提供了以下几类方法:
组件创建类:
| 方法 | 说明 |
|---|---|
CreateComponent(componentType, componentName) |
创建一个指定类型的组件 |
MakeLayout(componentType, componentName) |
创建一个布局容器 |
CreateComponentInLayout(componentType, componentName, layout) |
在指定布局内创建组件 |
属性设置类:
| 方法 | 说明 |
|---|---|
SetPropertyOfComponent(component, propertyName, propertyValue) |
设置组件的属性 |
GetPropertyOfComponent(component, propertyName) |
获取组件的属性值 |
事件绑定类:
| 方法 | 说明 |
|---|---|
AddComponentToLayout(component, layout) |
将组件添加到布局中 |
ComponentClicked(component) |
当动态组件被点击时触发的事件 |
ComponentLongClicked(component) |
当动态组件被长按时触发的事件 |
ComponentGotFocus(component) |
当动态组件获得焦点时触发的事件 |
ComponentLostFocus(component) |
当动态组件失去焦点时触发的事件 |
组件管理类:
| 方法 | 说明 |
|---|---|
DeleteComponent(component) |
删除动态创建的组件 |
DeleteAllComponentsInLayout(layout) |
删除布局中的所有动态组件 |
Clear() |
清除所有动态组件 |
GetComponents() |
获取所有动态组件的列表 |
3.3 支持的组件类型
DynamicComponents1 支持以下组件类型的动态创建:
- 基础组件:Button(按钮)、Label(标签)、TextBox(文本框)、PasswordTextBox(密码框)、Image(图片)
- 容器组件:HorizontalArrangement(水平布局)、VerticalArrangement(垂直布局)、TableArrangement(表格布局)
- 交互组件:CheckBox(复选框)、Switch(开关)、Slider(滑块)、ListPicker(列表选择器)
- 媒体组件:Image(图片)
四、基础操作:创建按钮和标签
4.1 动态创建一个按钮
假设你想在用户点击”添加按钮”后,动态创建一个新的按钮并添加到屏幕上。
步骤 1:准备布局
在设计视图中放置:
- 一个 Button,名为
AddButton,文字设为”添加按钮” - 一个 VerticalArrangement,名为
MainLayout,作为动态组件的容器
步骤 2:编写逻辑
when AddButton.Click() {
// 创建一个新的按钮组件
global newButton = DynamicComponents1.CreateComponent("Button", join("btn_", Clock1.Now()))
// 设置新按钮的属性
DynamicComponents1.SetPropertyOfComponent(newButton, "Text", "我是动态按钮")
DynamicComponents1.SetPropertyOfComponent(newButton, "Width", -2) // (表示自动适配内容)
DynamicComponents1.SetPropertyOfComponent(newButton, "Height", -2)
// 将按钮添加到布局中
DynamicComponents1.AddComponentToLayout(newButton, MainLayout)
}
4.2 动态创建标签和文本框
when btnAddField.Click() {
// 创建一个水平布局来容纳 标签 + 输入框
global rowLayout = DynamicComponents1.MakeLayout("HorizontalArrangement", join("row_", 全局计数变量))
// 设置行布局的属性(宽度填充、高度自动)
DynamicComponents1.SetPropertyOfComponent(rowLayout, "Width", -1) // (-1 表示填充父容器)
DynamicComponents1.SetPropertyOfComponent(rowLayout, "Height", -2)
// 在行布局中创建标签
global label = DynamicComponents1.CreateComponentInLayout("Label", join("lbl_", 全局计数变量), rowLayout)
DynamicComponents1.SetPropertyOfComponent(label, "Text", join("字段 ", 全局计数变量))
// 在行布局中创建文本框
global textBox = DynamicComponents1.CreateComponentInLayout("TextBox", join("txt_", 全局计数变量), rowLayout)
DynamicComponents1.SetPropertyOfComponent(textBox, "Width", -1)
// 将行布局添加到主容器
DynamicComponents1.AddComponentToLayout(rowLayout, MainLayout)
// 计数加1
global 全局计数变量 = 全局计数变量 + 1
}
提示:组件的
Width和Height属性值遵循 App Inventor 2 的约定:
-1= 填充父容器(Fill parent)-2= 自动适配内容(Automatic)- 其他正值 = 以像素为单位的固定尺寸
五、动态组件的事件处理
动态创建的组件需要绑定事件,这是 DynamicComponents1 扩展的核心功能之一。
5.1 点击事件
// 按钮创建后,当用户点击它时触发
when DynamicComponents1.ComponentClicked(clickedComponent) {
// 获取被点击组件的属性
global componentText = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Text")
// 执行对应的操作
Notifier1.ShowAlert(join("你点击了:", componentText))
}
5.2 长按事件
when DynamicComponents1.ComponentLongClicked(longClickedComponent) {
// 长按时删除该组件
DynamicComponents1.DeleteComponent(longClickedComponent)
}
5.3 焦点事件(适用于文本框)
when DynamicComponents1.ComponentGotFocus(focusedComponent) {
// 获取组件类型
global compType = DynamicComponents1.GetPropertyOfComponent(focusedComponent, "Type")
if (compType == "TextBox") {
Notifier1.ShowAlert("文本框获得了焦点")
}
}
5.4 使用组件名称来区分不同组件
在实际开发中,你通常需要根据组件的名称(在创建时设定的唯一标识)来执行不同的逻辑。
when DynamicComponents1.ComponentClicked(clickedComponent) {
global compName = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Name")
// 根据名称前缀判断
if (starts_with(compName, "btn_")) {
Notifier1.ShowAlert("按钮被点击")
} else if (starts_with(compName, "lbl_")) {
Notifier1.ShowAlert("标签被点击")
} else if (starts_with(compName, "chk_")) {
// 处理复选框
global isChecked = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Checked")
Notifier1.ShowAlert(join("复选框状态:", isChecked))
}
}
六、动态创建布局容器
布局容器的动态创建是实现复杂动态界面的基础。
6.1 创建不同类型的布局
// 创建垂直布局(从上到下排列)
when btnCreateVertical.Click() {
global verticalLayout = DynamicComponents1.MakeLayout("VerticalArrangement", "myVerticalLayout")
DynamicComponents1.SetPropertyOfComponent(verticalLayout, "Width", -1)
DynamicComponents1.SetPropertyOfComponent(verticalLayout, "Height", -2)
}
// 创建水平布局(从左到右排列)
when btnCreateHorizontal.Click() {
global horizontalLayout = DynamicComponents1.MakeLayout("HorizontalArrangement", "myHorizontalLayout")
// 设置子组件的对齐方式
DynamicComponents1.SetPropertyOfComponent(horizontalLayout, "AlignVertical", 1) // (1 = 居中对齐)
}
// 创建表格布局
when btnCreateTable.Click() {
global tableLayout = DynamicComponents1.MakeLayout("TableArrangement", "myTableLayout")
DynamicComponents1.SetPropertyOfComponent(tableLayout, "Columns", 3)
DynamicComponents1.SetPropertyOfComponent(tableLayout, "Rows", 4)
DynamicComponents1.SetPropertyOfComponent(tableLayout, "Width", -1)
}
6.2 嵌套布局
通过布局嵌套,可以创建更复杂的界面结构。
when Screen1.Initialize() {
// 先创建一个垂直布局作为最外层容器
global outerLayout = DynamicComponents1.MakeLayout("VerticalArrangement", "outerLayout")
DynamicComponents1.SetPropertyOfComponent(outerLayout, "Width", -1)
// 在垂直布局中创建一个水平布局行
global row1 = DynamicComponents1.CreateComponentInLayout("HorizontalArrangement", "row1", outerLayout)
// 在水平布局中添加两个按钮
global btnA = DynamicComponents1.CreateComponentInLayout("Button", "btnA", row1)
DynamicComponents1.SetPropertyOfComponent(btnA, "Text", "按钮A")
global btnB = DynamicComponents1.CreateComponentInLayout("Button", "btnB", row1)
DynamicComponents1.SetPropertyOfComponent(btnB, "Text", "按钮B")
// 再创建第二行
global row2 = DynamicComponents1.CreateComponentInLayout("HorizontalArrangement", "row2", outerLayout)
global descLabel = DynamicComponents1.CreateComponentInLayout("Label", "descLabel", row2)
DynamicComponents1.SetPropertyOfComponent(descLabel, "Text", "这是第二行的描述")
// 将整个外层布局添加到主界面
DynamicComponents1.AddComponentToLayout(outerLayout, MainLayout)
}
七、循环创建列表项
最常见的动态创建场景是生成列表。以下示例展示了如何根据数据列表动态生成一组卡片式的布局。
// 假设我们有一个待办事项列表
global todoItems = ["买牛奶", "交房租", "复习数学", "锻炼身体"]
when Screen1.Initialize() {
// 遍历列表,为每项生成一个卡片
for item in todoItems {
创建任务卡片(item)
}
}
procedure 创建任务卡片(item) {
// 创建一个水平布局作为卡片容器
global cardLayout = DynamicComponents1.MakeLayout("HorizontalArrangement", join("card_", item))
DynamicComponents1.SetPropertyOfComponent(cardLayout, "Width", -1)
DynamicComponents1.SetPropertyOfComponent(cardLayout, "Height", 50)
DynamicComponents1.SetPropertyOfComponent(cardLayout, "BackgroundColor", &HFFE8E8E8) // 浅灰色背景
// 在卡片中创建复选框
global checkbox = DynamicComponents1.CreateComponentInLayout("CheckBox", join("chk_", item), cardLayout)
DynamicComponents1.SetPropertyOfComponent(checkbox, "Width", -2)
// 在卡片中创建标签显示任务描述
global label = DynamicComponents1.CreateComponentInLayout("Label", join("lbl_", item), cardLayout)
DynamicComponents1.SetPropertyOfComponent(label, "Text", item)
DynamicComponents1.SetPropertyOfComponent(label, "Width", -1) // 填充剩余空间
// 在卡片中创建删除按钮
global deleteBtn = DynamicComponents1.CreateComponentInLayout("Button", join("del_", item), cardLayout)
DynamicComponents1.SetPropertyOfComponent(deleteBtn, "Text", "✕")
DynamicComponents1.SetPropertyOfComponent(deleteBtn, "Width", -2)
DynamicComponents1.SetPropertyOfComponent(deleteBtn, "BackgroundColor", &HFFFF4444) // 红色
DynamicComponents1.SetPropertyOfComponent(deleteBtn, "TextColor", &HFFFFFFFF) // 白色
// 将卡片添加到主布局
DynamicComponents1.AddComponentToLayout(cardLayout, TaskListLayout)
}
处理列表项的点击事件
// 复选框勾选 - 标记任务完成
when DynamicComponents1.ComponentClicked(clickedComponent) {
global compName = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Name")
// 判断是不是复选框
if (starts_with(compName, "chk_")) {
global isChecked = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Checked")
// 获取对应的标签
global taskName = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Text") // (这是复选框的标签文字)
if (isChecked == true) {
Notifier1.ShowAlert(join("√ 已完成:", taskName))
} else {
Notifier1.ShowAlert(join("○ 未完成:", taskName))
}
} else if (starts_with(compName, "del_")) {
// 获取被删除组件的父布局
global parentLayout = DynamicComponents1.GetPropertyOfComponent(clickedComponent, "Parent")
// 删除整个卡片(父布局)
DynamicComponents1.DeleteComponent(parentLayout)
}
}
八、动态删除组件
8.1 删除单个组件
when btnDeleteComponent.Click() {
// 删除最后添加的那个组件
global allComponents = DynamicComponents1.GetComponents()
// 获取组件总数
global count = lengthOfList(allComponents)
if (count > 0) {
// 获取最后一个组件
global lastComponent = selectListItem(allComponents, count)
// 删除它
DynamicComponents1.DeleteComponent(lastComponent)
} else {
Notifier1.ShowAlert("没有可删除的组件")
}
}
8.2 清空布局
when 按钮_清空布局.Click() {
// 删除 MainLayout 中的所有动态组件
DynamicComponents1.DeleteAllComponentsInLayout(MainLayout)
}
8.3 根据条件删除
when btnDeleteChecked.Click() {
global allComponents = DynamicComponents1.GetComponents()
// 遍历所有组件
for component in allComponents {
global compType = DynamicComponents1.GetPropertyOfComponent(component, "Type")
// 如果是复选框,检查是否勾选
if (compType == "CheckBox") {
global isChecked = DynamicComponents1.GetPropertyOfComponent(component, "Checked")
if (isChecked == true) {
global parentLayout = DynamicComponents1.GetPropertyOfComponent(component, "Parent")
// 删除父布局(整个卡片)
if (parentLayout != "") {
DynamicComponents1.DeleteComponent(parentLayout)
}
}
}
}
}
重要提醒:当你在遍历过程中删除组件时,建议从后往前遍历,或者先收集要删除的组件再统一删除,以避免索引错乱的问题。
九、实战案例:动态问卷调查
让我们将以上知识综合起来,构建一个完整的动态问卷调查应用。
应用功能
用户创建一个问卷,可以:
- 动态添加不同类型的问题(单选、多选、文本回答)
- 每个问题由标签 + 输入控件组成
- 提交时收集所有答案
设计视图布局
Screen1
├── 标签:lblTitle("动态问卷调查")
├── 水平布局:ToolbarLayout
│ ├── 按钮:btnAddText("添加文本题")
│ ├── 按钮:btnAddRadio("添加单选题")
│ └── 按钮:btnAddCheck("添加多选题")
├── 垂直布局:QuestionLayout(动态组件的容器)
├── 水平布局:ActionLayout
│ ├── 按钮:btnSubmit("提交问卷")
│ └── 按钮:btnReset("重置")
└── 标签:lblResult(显示提交结果)
核心逻辑实现
// 全局变量
global questionCount = 0
global questions = []
// 添加文本题
when btnAddText.Click() {
创建问题卡片("text")
}
// 添加单选题
when btnAddRadio.Click() {
创建问题卡片("radio")
}
// 添加多选题
when btnAddCheck.Click() {
创建问题卡片("checkbox")
}
procedure 创建问题卡片(问题类型) {
global qNum = questionCount + 1
global qCard = DynamicComponents1.MakeLayout("VerticalArrangement", join("qCard_", qNum))
DynamicComponents1.SetPropertyOfComponent(qCard, "Width", -1)
DynamicComponents1.SetPropertyOfComponent(qCard, "Height", -2)
// ---- 第一行:问题标题 ----
global titleRow = DynamicComponents1.CreateComponentInLayout("HorizontalArrangement", join("qTitleRow_", qNum), qCard)
global numLabel = DynamicComponents1.CreateComponentInLayout("Label", join("qNumLabel_", qNum), titleRow)
DynamicComponents1.SetPropertyOfComponent(numLabel, "Text", join("问题", qNum, ":"))
DynamicComponents1.SetPropertyOfComponent(numLabel, "FontBold", true)
global titleInput = DynamicComponents1.CreateComponentInLayout("TextBox", join("qTitleInput_", qNum), titleRow)
DynamicComponents1.SetPropertyOfComponent(titleInput, "Hint", "请输入问题...")
DynamicComponents1.SetPropertyOfComponent(titleInput, "Width", -1)
DynamicComponents1.SetPropertyOfComponent(titleInput, "Height", -2)
// ---- 第二行:选项区域 ----
global optionsRow = DynamicComponents1.CreateComponentInLayout("VerticalArrangement", join("qOptionsRow_", qNum), qCard)
if (问题类型 == "text") {
global answerBox = DynamicComponents1.CreateComponentInLayout("TextBox", join("qAnswer_", qNum), optionsRow)
DynamicComponents1.SetPropertyOfComponent(answerBox, "Hint", "请输入您的回答...")
DynamicComponents1.SetPropertyOfComponent(answerBox, "Width", -1)
DynamicComponents1.SetPropertyOfComponent(answerBox, "Height", 100)
DynamicComponents1.SetPropertyOfComponent(answerBox, "Multiline", true)
} else if (问题类型 == "radio") {
for i in [1, 2, 3] {
global radioBtn = DynamicComponents1.CreateComponentInLayout("Button", join("radioBtn_", qNum, "_", i), optionsRow)
DynamicComponents1.SetPropertyOfComponent(radioBtn, "Text", join("选项", i))
DynamicComponents1.SetPropertyOfComponent(radioBtn, "Width", -2)
}
} else if (问题类型 == "checkbox") {
for i in [1, 2, 3] {
global checkBox = DynamicComponents1.CreateComponentInLayout("CheckBox", join("checkBtn_", qNum, "_", i), optionsRow)
DynamicComponents1.SetPropertyOfComponent(checkBox, "Text", join("选项", i))
}
}
DynamicComponents1.AddComponentToLayout(qCard, QuestionLayout)
global questionCount = questionCount + 1
}
when btnSubmit.Click() {
global resultText = "问卷结果:"
lblResult.Text = resultText
Notifier1.ShowAlert("问卷提交成功!")
}
when btnReset.Click() {
DynamicComponents1.DeleteAllComponentsInLayout(QuestionLayout)
global questionCount = 0
global questions = []
lblResult.Text = ""
}
十、常见问题与注意事项
10.1 组件名称必须唯一
动态创建组件时,每个组件都需要一个唯一的名称。如果名称重复,后创建的组件会覆盖之前的组件。
正确做法:使用计数器或时间戳拼接组件名称。
// 使用全局计数器
global 组件名称 = join("btn_", 全局计数器)
global 全局计数器 = 全局计数器 + 1
// 或使用时间戳
global 组件名称 = join("btn_", Clock1.Now())
10.2 不能在设计视图中直接引用动态组件
因为动态组件是在运行时创建的,设计视图中不存在这些组件。你不能像对待普通组件那样直接拖放代码块来引用它们,而必须通过 DynamicComponents1 的方法来操作。
10.3 性能考虑
- 避免一次性创建大量组件(例如超过100个)
- 如果需要大量列表项,考虑使用 ListView 配合列表数据
- 销毁不再需要的组件(使用
DeleteComponent)释放内存 - 嵌套布局不要太深,一般 3-4 层即可
10.4 组件生命周期
- 动态创建的组件在应用重启后消失(除非你使用 TinyDB 保存状态)
- 动态组件的父布局被删除时,其所有子组件也会被自动删除
- 如果你需要在下次启动时恢复界面,需要保存组件信息并在 Screen1.Initialize 中重新创建
10.5 与 Screen1 中的现有组件交互
动态组件可以与设计视图中已有的组件正常交互。例如,动态组件点击后可以更新静态标签的文本,反之亦然。
when DynamicComponents1.ComponentClicked(clickedComponent) {
// 更新设计视图中的静态标签
StaticLabel.Text = "你点击了一个动态组件"
}
10.6 如何获取动态组件中的输入值
对于动态创建的 TextBox,你可以通过 GetPropertyOfComponent 来获取其输入内容。
DynamicComponents1.GetPropertyOfComponent(dynamicTextBox, "Text")
global userInput = 返回值
对于动态创建的 CheckBox:
DynamicComponents1.GetPropertyOfComponent(dynamicCheckBox, "Checked")
global isChecked = 返回值
10.7 使用组件名称列表来管理
为了方便管理大量动态组件,建议使用列表来记录所有创建的组件引用。
// 初始化
global dynamicButtons = []
// 每次创建时记录
when AddButton.Click() {
// 创建按钮...
addItemsTo(dynamicButtons, newButton)
}
// 遍历所有动态按钮
when btnListAll.Click() {
global index = 1
for btn in dynamicButtons {
global btnText = DynamicComponents1.GetPropertyOfComponent(btn, "Text")
Notifier1.ShowAlert(join("按钮", index, ":", btnText))
global index = index + 1
}
}
十一、扩展阅读与资源
官方资源
- MIT App Inventor 扩展中心:https://ai2.appinventor.mit.edu/reference/other/extensions.html
- MIT CML 扩展 GitHub 仓库:https://github.com/mit-cml/mit-cml-extensions
- MIT App Inventor 社区:https://community.appinventor.mit.edu
社区资源
- Pura Vida Apps 教程(Taifun 扩展):https://puravidaapps.com
- App Inventor 中文网:https://www.appinventor.cn
相关扩展
- TaifunDW(Taifun’s Dynamic Views):与 DynamicComponents1 类似的第三方扩展
- TaifunListView:如果只需动态列表功能,考虑使用此扩展
版权声明:本文为 App Inventor 2 中文网原创教程,转载请注明出处。文中涉及的 MIT App Inventor 及 DynamicComponents1 扩展版权归 MIT Center for Mobile Learning 所有。欢迎在 MIT App Inventor 社区 交流讨论。
扫码添加客服咨询