ARTICLE · 1156982
【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. 左侧 组件库 最下方是 容器 分类(需要往下滚一点才看得到) 2. 把 面板(Panel) 拖到画布中央——松手前面板内部会高亮,提示"可以放进来" 3. 再从组件库拖一个 按钮 到面板的内部,而不是面板外面 4. 看右侧 组件浏览器:放对了的话,按钮会缩进显示在面板下面

⚠️ 最常见的失误:按钮看着像是"在面板上",其实拖到了面板外面,父对象还是 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,什么都不用调,出来就是这样:

九个格子的大小完全相同,拖动窗口边框时它们会一起等比缩放——这就是"不用手调坐标"的意思。
2.2 行高与列宽的弹性规则('1x' 详解)
这是 Grid 的灵魂。在检查器里,每行可以设高度,每列可以设宽度,值有三种类型:
固定像素100 | ||
'fit' | fit) | |
'1x''2x' |
'1x' 的工作原理(举例说明):
假设 Grid 是 2 列,两列宽度分别是 {'1x', 200}:
窗口宽度 600px → 第1列 = 600 - 200 = 400px(弹性列吃掉剩余空间)
窗口宽度 800px → 第1列 = 800 - 200 = 600px(弹性列变大)
第2列永远是固定 200px在检查器里怎么设:选中画布上的 Grid Layout,右侧检查器就有这两行——

把它们改成 {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. 拖入一个 Grid Layout,设成 2行 × 1列 • 第1行高度: 60(固定,放工具栏)• 第2行高度: '1x'(弹性,放主体)2. 在第2行再拖入一个 Grid Layout(1行 × 2列) • 第1列宽: 200(固定,放列表)• 第2列宽: '1x'(弹性,放表格)
最终结构:
┌────────────────────────────────────────┐
│ 第1行(高度60,固定)— 工具栏 │
│ [按钮] [按钮] [标签...] │
├────────────────────────────────────────┤
│ 第2行('1x'弹性) │
│ ┌──────────┬─────────────────────────┐ │
│ │ 第1列200px │ 第2列'1x'弹性 │ │
│ │ 列表 │ 表格 │ │
│ └──────────┴─────────────────────────┘ │
└────────────────────────────────────────┘在 App Designer 里怎么搭(这是本篇最值得动手做一遍的练习):
1. 新建空白 App,拖一个 Grid Layout 进画布,检查器里设 2 行 × 1 列 2. RowHeight填{60, '1x'}——第 1 行固定 60 放工具栏,第 2 行弹性放主体3. 点中第 2 行,再拖一个 Grid Layout 进去(这就是嵌套),设 1 行 × 2 列 4. ColumnWidth填{200, '1x'}——左列固定 200 放列表,右列弹性放表格5. 往第 1 行的格子里拖按钮和标签,往内层 Grid 的两个格子里分别拖列表框和表格
搭完运行,是这个样子:

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

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

把窗口从 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列就这两个属性,没有第三个:
Row | [1 2] 表示跨第 1~2 行 |
Column | [1 2] 表示跨第 1~2 列 |

第三部分:Tab Group(选项卡组)——多页面导航
3.1 什么时候用选项卡?
当一个 App 功能太多、一屏放不下,或者想把"不同功能的界面"分开时,用选项卡组最合适——就像浏览器多标签页。
3.2 设计视图操作
跟着做一遍:
1. 组件库 → 容器 分类 → 拖 选项卡组(Tab Group) 到画布 2. 默认自带两个页。在 组件浏览器 里点选某一页,右侧 检查器 改 Title(如"数据""设置")3. 加第三个页:选中 Tab Group,在检查器里点 添加选项卡 4. 放组件前先点中目标页,再把组件拖进该页内部——不先选中就拖,组件会跑到别的页去 5. 点画布上方的页标签可以来回切,确认每页内容彼此独立
搭完之后,右侧组件浏览器的层级是这样(缩进 = 父子关系):

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

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. 组件库 → 容器 → 拖一个 面板(Panel) 到画布 2. 选中面板,右侧检查器里把 Scrollable改成on3. 在面板内部再放一个 Grid Layout,行数按内容多少定 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 我写成了"显示设置(可滚动)"):

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

💡 Scrollable Panel 适合:设置项很多、日志显示、动态添加大量内容(如监控列表)。
第五部分:外观定制——让界面更专业
布局到位后,外观是"专业感"的最后一块拼图。
5.1 全局字体与主题
App Designer 提供了内置主题,让 App 有统一的外观:
跟着做一遍:
1. 在 组件浏览器 里点最顶层的 app.UIFigure2. 右侧 检查器 里找到 Theme,下拉选 Light / Dark3. 画布上的整窗配色立刻跟着变
代码里要在创建图窗时指定。注意 R2025b 的 Theme 是一个对象而不是字符串,事后赋字符串会报错:
% ✅ 创建时指定主题
app.UIFigure = uifigure('Theme', 'dark');
% ❌ 这样写会报错:无法从 GraphicsTheme 转换为 char
% app.UIFigure.Theme = 'dark';两种主题的实际效果对比:


5.2 常用外观属性速查
BackgroundColor | |
FontNameFontSize / FontColor | |
FontWeight | |
HorizontalAlignment | |
WordWrap | |
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' 吃掉所有多出来的空间。
第七部分:本篇知识点串联
| 容器组件 | ||
| Grid Layout | '1x' | |
| 嵌套 Grid | ||
| 合并单元格 | Layout.Row/Column = [起 止] | |
| Tab Group | uitabgroupuitab | |
| Scrollable | uipanelScrollable='on' | |
| 外观定制 |
🏠 小任务
把本篇的仪表盘改造成带选项卡的版本:把"表格"放到"数据"页,"柱状图"放到"图表"页,并在切换到"图表"页时自动重画柱状图。(提示:用 SelectionChangedFcn,参考本篇 3.4 节。)
📢 下篇预告
下一篇进阶课程,我们进入 App Designer 面向对象编程进阶——搞懂 classdef、属性权限与类型约束、get/set 方法、依赖属性、事件监听,以及如何用类继承让多个 App 复用公共代码。
如果这篇布局课对你有帮助,欢迎转发~ 🔄