App Inventor 2 动态创建组件与布局教程

« 返回首页

一、什么是动态创建组件?

在 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 团队开发并托管。

导入步骤:

  1. 打开 MIT App Inventor 2 编辑器
  2. 点击菜单栏 Extensions(扩展)
  3. 选择 Import extension(导入扩展)
  4. 在弹出的 URL 输入框中输入扩展的 .aix 文件地址,或从本地选择 .aix 文件
  5. 导入成功后,在组件面板的 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)
        }
      }
    }
  }
}

重要提醒:当你在遍历过程中删除组件时,建议从后往前遍历,或者先收集要删除的组件再统一删除,以避免索引错乱的问题。


九、实战案例:动态问卷调查

让我们将以上知识综合起来,构建一个完整的动态问卷调查应用。

应用功能

用户创建一个问卷,可以:

  1. 动态添加不同类型的问题(单选、多选、文本回答)
  2. 每个问题由标签 + 输入控件组成
  3. 提交时收集所有答案

设计视图布局

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 社区 交流讨论。

文档反馈