夜雨聆风学习资料网

ARTICLE · 1156969

【MATLAB】GUI 进阶(一)App Designer 布局管理全解——让界面随窗口自适应

【MATLAB】GUI 进阶(一)App Designer 布局管理全解——让界面随窗口自适应

前篇回顾

上一篇:
【MATLAB】GUI 过渡(一)同一 App,两种写法——纯代码 vs App Designer 逐行对照

【MATLAB】 GUI 过渡(二)两种方式各自深入——补强"短板"的进阶技巧

【MATLAB】 GUI 过渡(三)你该走哪条路?——两种方式能力对比与进阶路线

  入门时我们用 App Designer 拖出过能用的界面,但组件位置都是写死的——窗口一缩放就乱套。进阶第一步,彻底解决布局:学容器组件、理解弹性规则、掌握多页导航和外观定制,最终搭出能随窗口自适应缩放的复杂界面。


前言:为什么布局是进阶的第一课?

入门六搭的 App,每个组件都用固定坐标摆在画布上,有个致命缺点:

窗口默认大小 → 一切正常
窗口被拖大   → 组件挤在左上角,右侧大片空白
窗口被拖小   → 表格溢出窗口,按钮被遮住

真正专业的 App,界面必须能自适应——不管窗口怎么缩放,组件都能合理地重新排列、填充空间。

App Designer 解决这个问题靠的是布局系统:容器组件 + 布局管理器。这一篇,我们把布局系统的每个部分拆开。


第一部分:容器组件(布局的地基)

1.1 什么是容器组件?

容器是"能装其他组件的组件"。它是布局的基础——你不应该把组件直接扔在图窗上,而应该把它们装进容器里,再让容器去安排位置。

1.2 五大容器组件

容器
位置
用途
自动布局能力
Panel(面板)
容器分类下
逻辑分组、加标题
手动摆放内部组件
Tab Group(选项卡组)
容器分类下
分页,每页放一组内容
页面切换
Grid Layout(网格布局)
容器分类下
划成网格,组件自动对齐
✅ 自动适配核心
Button Group(按钮组)
容器分类下
装一组单选按钮
单选互斥
Scrollable Panel(可滚动面板)
容器分类下
内容多放不下时滚动
内容滚动

💡 核心认知:真正实现"窗口缩放自适应"的是 Grid Layout。Panel 和 Tab 主要是"分组 + 导航"的工具。

1.3 组件放进容器的正确姿势

关键点:组件的父对象(Parent)要指向容器,而不是 UIFigure。

在代码里创建组件时,最容易犯的错就是父对象写错:

% ❌ 错误:表格父对象是 UIFigure,不会跟着容器走
app.UITable = uitable(app.UIFigure, 'Data', data);

% ✅ 正确:表格父对象是面板,跟着面板走
app.UITable = uitable(app.Panel, 'Data', data);

在设计视图里怎么放(跟着做一遍):

  1. 1. 左侧 组件库 最下方是 容器 分类(需要往下滚一点才看得到)
  2. 2. 把 面板(Panel) 拖到画布中央——松手前面板内部会高亮,提示"可以放进来"
  3. 3. 再从组件库拖一个 按钮 到面板的内部,而不是面板外面
  4. 4. 看右侧 组件浏览器:放对了的话,按钮会缩进显示在面板下面
App Designer 设计视图四大区域:左组件库 / 中画布 / 右上组件浏览器 / 右下检查器

⚠️ 最常见的失误:按钮看着像是"在面板上",其实拖到了面板外面,父对象还是 UIFigure。
判断依据只有一条——看组件浏览器里的缩进层级,别信眼睛。


第二部分:Grid Layout(网格布局)——自动适配的核心

这是全篇最重点的部分,拆成几个小节。

2.1 Grid Layout 是什么?

Grid Layout 把一块区域划分成网格(行 + 列),然后把组件一个个放进去。每个组件占据一个格子,自动对齐,不需要手调坐标。

Grid Layout(3行 × 3列)
┌─────┬─────┬─────┐
│ A   │ B   │ C   │   ← 第1行
├─────┼─────┼─────┤
│ D   │ E   │ F   │   ← 第2行
├─────┼─────┼─────┤
│ G   │ H   │ I   │   ← 第3行
└─────┴─────┴─────┘

拖九个按钮进一个 3×3 的 Grid,什么都不用调,出来就是这样:

3×3 网格里九个按钮自动排布,拖大窗口会等比放大

九个格子的大小完全相同,拖动窗口边框时它们会一起等比缩放——这就是"不用手调坐标"的意思。

2.2 行高与列宽的弹性规则('1x' 详解)

这是 Grid 的灵魂。在检查器里,每行可以设高度,每列可以设宽度,值有三种类型:

值
含义
效果
固定像素
,如 100
固定大小
窗口变化时不变
'fit'
自适应内容
根据内容大小自动调整(R2025b 起叫 fit)
'1x'
 / '2x'
弹性权重
窗口变化时按比例伸缩(核心)

'1x' 的工作原理(举例说明):

假设 Grid 是 2 列,两列宽度分别是 {'1x', 200}:

窗口宽度 600px → 第1列 = 600 - 200 = 400px(弹性列吃掉剩余空间)
窗口宽度 800px → 第1列 = 800 - 200 = 600px(弹性列变大)
第2列永远是固定 200px

在检查器里怎么设:选中画布上的 Grid Layout,右侧检查器就有这两行——

检查器里的 RowHeight 与 ColumnWidth,三行三列全部是 1x

把它们改成 {60, '1x'} 之类的组合,画布上会立刻看到变化,比改代码直观得多。

再看 {'1x', '2x'}(两列都是弹性,权重 1:2):

窗口宽度 600px → 第1列 = 200px,第2列 = 400px(按 1:2 分配)
窗口宽度 900px → 第1列 = 300px,第2列 = 600px(比例不变)

💡 一句话记忆:固定值管住"不该变的",'1x' 管住"该跟着窗口变的"。

⚠️ 版本坑:如果你在别的教程里看到 'auto',那是 R2022a 以前的写法。
R2025b 里写 'auto' 会直接报错:
'RowHeight' 必须为包含关键字 'fit'、数字或后跟 'x' 字符的数字的元胞向量。
记住三个合法值就够了:数字(如 60)、'fit'(贴合内容)、'Nx'(弹性权重)。

2.3 实战:设计一个自适应的典型布局

设计一个常见的"顶部工具栏 + 左侧列表 + 右侧内容"布局:

设计步骤:

  1. 1. 拖入一个 Grid Layout,设成 2行 × 1列
    • • 第1行高度:60(固定,放工具栏)
    • • 第2行高度:'1x'(弹性,放主体)
  2. 2. 在第2行再拖入一个 Grid Layout(1行 × 2列)
    • • 第1列宽:200(固定,放列表)
    • • 第2列宽:'1x'(弹性,放表格)

最终结构:

┌────────────────────────────────────────┐
│ 第1行(高度60,固定)— 工具栏           │
│ [按钮]  [按钮]  [标签...]              │
├────────────────────────────────────────┤
│ 第2行('1x'弹性)                      │
│ ┌──────────┬─────────────────────────┐ │
│ │ 第1列200px │ 第2列'1x'弹性          │ │
│ │ 列表       │ 表格                    │ │
│ └──────────┴─────────────────────────┘ │
└────────────────────────────────────────┘

在 App Designer 里怎么搭(这是本篇最值得动手做一遍的练习):

  1. 1. 新建空白 App,拖一个 Grid Layout 进画布,检查器里设 2 行 × 1 列
  2. 2. RowHeight 填 {60, '1x'}——第 1 行固定 60 放工具栏,第 2 行弹性放主体
  3. 3. 点中第 2 行,再拖一个 Grid Layout 进去(这就是嵌套),设 1 行 × 2 列
  4. 4. ColumnWidth 填 {200, '1x'}——左列固定 200 放列表,右列弹性放表格
  5. 5. 往第 1 行的格子里拖按钮和标签,往内层 Grid 的两个格子里分别拖列表框和表格

搭完运行,是这个样子:

嵌套布局初始状态:顶部工具栏 + 左侧列表 + 右侧表格

点"载入数据"把表格填满:

点"载入数据"后表格填满 5×3 成绩

再点"统计",标签算出平均分:

点"统计"后标签显示平均分 78.3 分

把窗口从 760 拉宽到 1060,注意看什么变了、什么没变:

窗口从 760 拉宽到 1060:工具栏高度、列表宽度不变,表格吃掉多出来的空间

2.4 嵌套 Grid(多层布局)

复杂界面需要嵌套多个 Grid——一个 Grid 的某个格子里,再放一个 Grid。

% ===== 代码中嵌套 Grid =====
% 外层 Grid:2行1列
g1 = uigridlayout(app.UIFigure, [21]);      % 父对象是图窗
g1.RowHeight = {60, '1x'};                   % 第1行固定60,第2行弹性

% 内层 Grid:放在外层 Grid 的第2行格子里,1行2列
g2 = uigridlayout(g1, [12]);                % 父对象是外层 Grid
g2.ColumnWidth = {200, '1x'};                % 第1列固定200,第2列弹性

% 表格放进内层 Grid 的第2列
app.UITable = uitable(g2, 'Data', data);     % 父对象是内层 Grid

💡 嵌套是复杂仪表盘的必经之路。原则是:从外到内、逐层划分,先定整体框架,再细化局部。

2.5 Grid 的合并单元格

有时某个组件想横跨多列。Grid 支持合并单元格(类似 Excel 的合并):

% ===== 让表格横跨 Grid 的第1~2列 =====
app.UITable.Layout.Row    = 2;        % 放在第2行
app.UITable.Layout.Column = [12];    % 从第1列开始,一直跨到第2列

就这两个属性,没有第三个:

Layout 属性
作用
Row
所在行;写成 [1 2] 表示跨第 1~2 行
Column
所在列;写成 [1 2] 表示跨第 1~2 列
标题和状态栏都横跨两列,中间两个按钮各占一列

第三部分:Tab Group(选项卡组)——多页面导航

3.1 什么时候用选项卡?

当一个 App 功能太多、一屏放不下,或者想把"不同功能的界面"分开时,用选项卡组最合适——就像浏览器多标签页。

3.2 设计视图操作

跟着做一遍:

  1. 1. 组件库 → 容器 分类 → 拖 选项卡组(Tab Group) 到画布
  2. 2. 默认自带两个页。在 组件浏览器 里点选某一页,右侧 检查器 改 Title(如"数据""设置")
  3. 3. 加第三个页:选中 Tab Group,在检查器里点 添加选项卡
  4. 4. 放组件前先点中目标页,再把组件拖进该页内部——不先选中就拖,组件会跑到别的页去
  5. 5. 点画布上方的页标签可以来回切,确认每页内容彼此独立

搭完之后,右侧组件浏览器的层级是这样(缩进 = 父子关系):

选项卡组的设计视图,右侧组件浏览器清楚显示 Tab → Tab → 组件的层级

选中选项卡组本身,检查器长这样:

选中选项卡组后的检查器属性

3.3 代码方式动态管理选项卡

% ===== 创建选项卡组 =====
tabGroup = uitabgroup(app.UIFigure, 'Position', [2020600400]);

% ===== 创建两个选项卡页 =====
tab1 = uitab(tabGroup, 'Title', '数据');   % 第1页,标题"数据"
tab2 = uitab(tabGroup, 'Title', '图表');   % 第2页,标题"图表"

% ===== 在"数据"页放表格(父对象是 tab1!)=====
app.UITable = uitable(tab1, 'Data', data);

% ===== 在"图表"页放坐标区(父对象是 tab2)=====
app.UIAxes = uiaxes(tab2);
plot(app.UIAxes, 1:10, rand(1,10));        % 画一条随机曲线

3.4 监听选项卡切换事件

想"切换到某页时刷新内容",监听选项卡组的切换:

% ===== 设置选项卡组的回调 =====
tabGroup.SelectionChangedFcn = @(src, evt) app.onTabChanged(evt);

% ===== 切换回调方法 =====
functiononTabChanged(app, evt)
% evt.NewValue:新选中的选项卡页
% evt.OldValue:之前选中的选项卡页
if evt.NewValue == app.tab2
% 切换到"图表"页时,重新画图
        app.refreshPlot();
end
end

💡 选项卡切换回调非常适合"懒加载"——切到哪页才加载哪页的数据,避免启动时一次加载所有页面拖慢速度。

运行起来,切到"数据"页是表格:

选项卡"数据"页里放表格

第四部分:Scrollable Panel(可滚动面板)——内容多放不下时

4.1 用途

当内容太多、界面放不下时,用可滚动面板,让用户在面板内滚动查看。

4.2 设计视图

 可滚动面板就是普通面板打开了一个开关:

  1. 1. 组件库 → 容器 → 拖一个 面板(Panel) 到画布
  2. 2. 选中面板,右侧检查器里把 Scrollable 改成 on
  3. 3. 在面板内部再放一个 Grid Layout,行数按内容多少定
  4. 4. 往里放满组件,内容高度一旦超过面板,右边自动冒出滚动条

代码里是同一个意思——先建面板,再把开关打开:

% ===== 创建可滚动面板:uipanel + Scrollable =====
sp = uipanel(app.UIFigure, 'Position', [2020300400]);
sp.Scrollable = 'on';          % ★ 关键就是这一行

% ===== 面板内容用 Grid 布局(放很多组件)=====
g = uigridlayout(sp, [101]);   % 10行1列,内容会撑出滚动条
fori = 1:10
    uilabel(g, 'Text', ['第 ', num2str(i), ' 行']);   % 逐行放标签
end

放好之后,面板在检查器里的样子(注意 Title 我写成了"显示设置(可滚动)"):

面板属性:Title 写着"显示设置(可滚动)"

内容超高时自动出现滚动条:

选项卡"设置"页放可滚动面板,内容超高时自动出滚动条

💡 Scrollable Panel 适合:设置项很多、日志显示、动态添加大量内容(如监控列表)。


第五部分:外观定制——让界面更专业

布局到位后,外观是"专业感"的最后一块拼图。

5.1 全局字体与主题

App Designer 提供了内置主题,让 App 有统一的外观:

主题
风格
Light(默认)
白底深色文字
Dark
深色底浅色文字(适合展示型)
HighContrast
高对比,无障碍

跟着做一遍:

  1. 1. 在 组件浏览器 里点最顶层的 app.UIFigure
  2. 2. 右侧 检查器 里找到 Theme,下拉选 Light / Dark
  3. 3. 画布上的整窗配色立刻跟着变

代码里要在创建图窗时指定。注意 R2025b 的 Theme 是一个对象而不是字符串,事后赋字符串会报错:

% ✅ 创建时指定主题
app.UIFigure = uifigure('Theme', 'dark');

% ❌ 这样写会报错:无法从 GraphicsTheme 转换为 char
% app.UIFigure.Theme = 'dark';

两种主题的实际效果对比:

Light 主题(默认):白底深色文字
Dark 主题:深底浅字,适合投影展示

5.2 常用外观属性速查

属性
作用
BackgroundColor
背景颜色
FontName
 / FontSize / FontColor
字体
FontWeight
(normal/bold)
加粗
HorizontalAlignment
水平对齐(left/center/right)
WordWrap
文字自动换行(label/button)
Visible
显示/隐藏
Enable
可用/禁用(灰色不可点)

这些属性不用背,选中组件后检查器里全都在(这是按钮的属性面板):

选中按钮后检查器显示的外观属性

5.3 状态控制:禁用/隐藏

% ===== 按钮禁用(变灰,不可点)=====
app.RunButton.Enable = 'off';

% ===== 按钮隐藏(完全不显示)=====
app.RunButton.Visible = 'off';

💡 Enable 和 Visible 是实现"界面状态机"的基础——数据没载入时禁用按钮,载入后启用。


第六部分:综合实战——自适应数据仪表盘

把本篇所有布局知识整合成一个能自适应缩放的完整仪表盘。

界面结构设计(用 Grid 分层)

┌───────────────────────────────────────────────┐
│ 第1行(Grid,3列)→ 顶部工具栏                  │
│ [载入数据] [统计] [标签:当前文件]              │
├───────────────────────────────────────────────┤
│ 第2行(Grid,2列)→ 主体                       │
│ ┌────────────┬──────────────────────────────┐ │
│ │ 左(200px)  │ 右('1x')                     │ │
│ │ 列表       │ 嵌套Grid(2行1列)           │ │
│ │            │  表格  /  坐标区             │ │
│ └────────────┴──────────────────────────────┘ │
└───────────────────────────────────────────────┘

完整代码(含布局 + 回调 + 自定义方法)

% =====================================================
% 成绩数据仪表盘(自适应布局版)
% 亮点:整个界面用 Grid 分层,窗口缩放时自动适配
% 使用:保存为 DashboardApp.m,命令行输入 DashboardApp 运行
% =====================================================
classdef DashboardApp < matlab.apps.AppBase

% ===== 组件属性区 =====
properties (Access = public)
        UIFigure      matlab.ui.Figure            % 图窗
        LoadButton    matlab.ui.control.Button    % 载入数据按钮
        StatButton    matlab.ui.control.Button    % 统计按钮
        FileLabel     matlab.ui.control.Label     % 文件名标签
        FileListBox   matlab.ui.control.ListBox   % 文件列表
        UITable       matlab.ui.control.Table     % 表格
        UIAxes        matlab.ui.control.UIAxes    % 坐标区
end

% ===== 业务数据属性区 =====
properties (Access = private)
        fileData = {}    % 当前数据(cell 数组)
        fileCols = {}    % 列名
end

% ===== 组件创建与布局 =====
methods (Access = private)

functioncreateComponents(app)
% 创建图窗
            app.UIFigure = uifigure;
            app.UIFigure.Position = [100100800550];

% 外层 Grid:2行1列(工具栏固定60,主体弹性)
            gOuter = uigridlayout(app.UIFigure, [21]);
            gOuter.RowHeight = {60, '1x'};       % 第1行固定60,第2行弹性
            gOuter.Padding = [10101010];       % 四周内边距10像素

% 顶部工具栏 Grid:1行3列
            gToolbar = uigridlayout(gOuter, [13]);
            gToolbar.ColumnWidth = {120, 100, '1x'};
            gToolbar.Padding = [0000];

% 载入数据按钮(父对象是工具栏 Grid)
            app.LoadButton = uibutton(gToolbar, 'push', ...
'Text', '载入数据', ...
'ButtonPushedFcn', @(src,evt) app.loadData());

% 统计按钮
            app.StatButton = uibutton(gToolbar, 'push', ...
'Text', '统计', ...
'ButtonPushedFcn', @(src,evt) app.computeStat());

% 文件名标签(左对齐)
            app.FileLabel = uilabel(gToolbar, ...
'Text', '未载入文件', ...
'HorizontalAlignment', 'left');

% 主体 Grid:1行2列(列表固定200,内容弹性)
            gMain = uigridlayout(gOuter, [12]);
            gMain.ColumnWidth = {200, '1x'};

% 左列:文件列表
            app.FileListBox = uilistbox(gMain, ...
'Items', {'示例数据.xlsx'}, ...
'ValueChangedFcn', @(src,evt) app.onFileSelected());

% 右列:嵌套 Grid,2行1列(表格弹性,坐标区固定200)
            gRight = uigridlayout(gMain, [21]);
            gRight.RowHeight = {'1x', 200};

% 表格和坐标区
            app.UITable = uitable(gRight, 'Data', cell(0,0));
            app.UIAxes = uiaxes(gRight);
end

% ===== 启动初始化 =====
functionstartupFcn(app)
% 表格初始为空
            app.UITable.Data = cell(0, 0);
% 坐标区初始化
            grid(app.UIAxes, 'on');              % 打开网格
            xlabel(app.UIAxes, '姓名');
            ylabel(app.UIAxes, '平均分');
end

% ===== 载入数据 =====
functionloadData(app)
% 生成示例数据(简化演示)
            app.fileCols = {'姓名', '语文', '数学', '英语'};
            app.fileData = {'张三', 88, 92, 78;
'李四', 75, 88, 90;
'王五', 95, 70, 85;
'赵六', 82, 91, 76};
% 填入表格
            app.UITable.ColumnName = app.fileCols;
            app.UITable.Data = app.fileData;
% 更新标签
            app.FileLabel.Text = '示例数据.xlsx(已载入)';
end

% ===== 列表选中 =====
functiononFileSelected(app)
% 简化:选中就重新载入
            app.loadData();
end

% ===== 统计:调用自定义方法 =====
functioncomputeStat(app)
% 如果还没载入数据,提示
ifisempty(app.fileData)
                app.FileLabel.Text = '请先载入数据';
return;
end
% 调用自定义方法算平均分
            avgs = app.computeAverage(app.fileData);
% 清空坐标区并画柱状图
            cla(app.UIAxes);
            names = cell(size(avgs,1), 1);       % 预分配姓名cell
for r = 1:size(avgs,1)
                names{r} = app.fileData{r,1};    % 取第1列姓名
end
            bar(app.UIAxes, 1:length(avgs), avgs);   % 画柱状图
            set(app.UIAxes, 'XTick', 1:length(avgs), ...
'XTickLabel', names);            % 设置横轴标签
            app.FileLabel.Text = ['平均分已计算(共', ...
                num2str(length(avgs)), '人)'];
end

% ===== 自定义方法:算平均分 =====
functionavgs = computeAverage(app, data)
% 输入:data 是 cell 数组(第1列姓名,其余分数)
% 输出:avgs 是每人平均分向量
            n = size(data, 1);                   % 人数(行数)
            avgs = zeros(n, 1);                  % 预分配
for r = 1:n
                total = 0;                       % 累加总分
for c = 2:size(data, 2)          % 从第2列起
                    val = data{r, c};
if isnumeric(val)            % 数字直接加
                        total = total + val;
else% 文本转数字再加
                        total = total + str2double(val);
end
end
                avgs(r) = total / (size(data, 2) - 1);  % 平均分
end
end

% ===== 关闭清理 =====
functiondelete(app)
            delete(app.UIFigure);                % 删除图窗
end
end
end

运行与验证

初始打开时,表格是空的:

综合实战仪表盘的初始状态

点"载入数据",表格和列表一起填上:

点"载入数据":表格和列表同时填上

点"统计",坐标区画出四个人的平均分柱状图:

点"统计":坐标区画出每人平均分柱状图

把窗口缩到很小——各区域按比例压缩,但谁都没被挤没:

窗口缩到很小时,各区域按比例压缩但仍完整

再拉大——表格和坐标区同步变大,工具栏高度和列表宽度纹丝不动:

窗口拉大后表格和坐标区同步放大,工具栏与列表宽度不变

这就是 Grid 弹性布局的功劳:固定值管住不该变的,'1x' 吃掉所有多出来的空间。


第七部分:本篇知识点串联

知识点
核心
一句话记住
容器组件
Panel/Tab/Grid/ButtonGroup/Scrollable
组件装进容器,别扔在图窗上
Grid Layout
弹性 '1x'
固定值管不变,'1x'管自适应
嵌套 Grid
从外到内逐层划分
复杂仪表盘靠嵌套实现
合并单元格Layout.Row/Column = [起 止]
让组件横跨多行/列
Tab Groupuitabgroup
 + uitab
多页面导航,懒加载
Scrollableuipanel
 + Scrollable='on'
内容多时滚动
外观定制
Theme、字体、Enable/Visible
统一外观 + 状态控制

🏠 小任务

把本篇的仪表盘改造成带选项卡的版本:把"表格"放到"数据"页,"柱状图"放到"图表"页,并在切换到"图表"页时自动重画柱状图。(提示:用 SelectionChangedFcn,参考本篇 3.4 节。)


📢 下篇预告

下一篇进阶课程,我们进入 App Designer 面向对象编程进阶——搞懂 classdef、属性权限与类型约束、get/set 方法、依赖属性、事件监听,以及如何用类继承让多个 App 复用公共代码。

如果这篇布局课对你有帮助,欢迎转发~ 🔄


相关学习资料