框架布局¶
框架布局是 SOUI 中的提供的一种类似于 MFC 的 CFrameWnd 布局方式,支持将子窗口停靠在容器的不同位置。
1. 概述¶
框架布局(FrameLayout)是 SOUI 框架中一种强大的布局方式,类似于 MFC 的 CFrameWnd 布局机制。它允许将子窗口停靠在父窗口的不同位置(如顶部、底部、左侧、右侧),并自动调整剩余空间给主视图。
通过在容器窗口上设置 layout="frame" 或 layout="frameLayout" 来启用框架布局。
2. 核心功能¶
2.1 停靠位置支持¶
FrameLayout 支持以下停靠位置:
| 停靠位置 | 描述 | XML 属性值 |
|---|---|---|
| DockNone | 无停靠 | none |
| DockLeft | 左侧停靠 | left |
| DockTop | 顶部停靠 | top |
| DockRight | 右侧停靠 | right |
| DockBottom | 底部停靠 | bottom |
| DockMainView | 主视图(占据剩余空间) | mainview 或 main |
2.2 布局特性¶
- 相对停靠:支持通过
dockRelativeTo属性指定相对于其他窗口的停靠关系 - 权重分配:支持通过
weight属性分配剩余空间 - 边距扩展:支持通过
extend_left、extend_top、extend_right、extend_bottom属性设置扩展边距 - 对齐方式:支持通过
gravity属性设置对齐方式 - 停靠模式限制:支持通过
enableDockMode属性限制允许的停靠模式
3. 布局属性¶
3.1 容器属性¶
- layout: 指定布局类型
frame: 框架布局-
frameLayout: 框架布局(完整名称) -
enableDockMode: 指定停靠模式
none: 禁用停靠模式left: 仅启用左侧停靠top: 仅启用顶部停靠right: 仅启用右侧停靠bottom: 仅启用底部停靠any: 启用所有方向的停靠
3.2 子窗口属性¶
- size: 指定窗口大小
-1: 包裹内容 (wrap_content)-
-2: 填充父窗口 (match_parent) -
dock: 指定停靠位置
none: 不停靠,使用默认布局left: 停靠在左侧top: 停靠在顶部right: 停靠在右侧bottom: 停靠在底部mainview: 停靠在主视图区域-
main: 停靠在主视图区域(简写) -
dockRelativeTo: 指定相对停靠的窗口 ID
-
用于指定当前窗口相对于哪个窗口进行停靠
-
weight: 按比例分配剩余空间
-
layout_gravity: 指定对齐方式
-
extend: 设置外边距,格式:"left,top,right,bottom"
- 也可单独设置:extend_left,extend_top,extend_right,extend_bottom
4. 实现原理¶
4.1 布局流程¶
FrameLayout 的布局流程如下:
- 收集子窗口:遍历所有可见的子窗口,检查其停靠模式是否与父布局的
enableDockMode匹配 - 测量子窗口:计算每个子窗口的期望大小
- 布局顶部和底部:首先布局顶部和底部的子窗口,调整可用空间
- 布局左侧和右侧:然后布局左侧和右侧的子窗口,进一步调整可用空间
- 布局主视图:最后将剩余空间分配给主视图
4.2 关键实现代码¶
4.2.1 布局子窗口的核心方法¶
void SFrameLayout::LayoutChildren(IWindow *pParent)
{
SList<ChildInfo> lstChildren;
CollectChildren(pParent, lstChildren);
CRect rcParent;
pParent->GetChildrenLayoutRect(&rcParent);
CRect rcAvailable = rcParent;
ChildInfo *pMainViewInfo = NULL;
SList<ChildInfo *> lstLeft, lstTop, lstRight, lstBottom;
// 分类子窗口到不同的停靠列表
SPOSITION pos = lstChildren.GetHeadPosition();
while (pos)
{
ChildInfo &info = lstChildren.GetNext(pos);
if (info.pParam->dockPos == DockMainView)
{
pMainViewInfo = &info;
}
else if (info.pParam->dockPos == DockLeft)
{
lstLeft.AddTail(&info);
}
else if (info.pParam->dockPos == DockTop)
{
lstTop.AddTail(&info);
}
else if (info.pParam->dockPos == DockRight)
{
lstRight.AddTail(&info);
}
else if (info.pParam->dockPos == DockBottom)
{
lstBottom.AddTail(&info);
}
}
// 布局顶部和底部
LayoutDockTopBottom(pParent, lstTop, rcAvailable, TRUE, rcParent);
LayoutDockTopBottom(pParent, lstBottom, rcAvailable, FALSE, rcParent);
// 布局左侧和右侧
LayoutDockLeftRight(pParent, lstLeft, rcAvailable, TRUE, rcParent);
LayoutDockLeftRight(pParent, lstRight, rcAvailable, FALSE, rcParent);
// 布局主视图
if (pMainViewInfo)
{
int nScale = pMainViewInfo->pWnd->GetScale();
CRect rcExtend;
CalcExtendRect(*pMainViewInfo, rcExtend, nScale);
CRect rcMainView;
rcMainView.left = rcAvailable.left + rcExtend.left;
rcMainView.top = rcAvailable.top + rcExtend.top;
rcMainView.right = rcAvailable.right - rcExtend.right;
rcMainView.bottom = rcAvailable.bottom - rcExtend.bottom;
((SWindow *)pMainViewInfo->pWnd)->OnRelayout(rcMainView);
}
}
4.2.2 顶部和底部布局实现¶
LayoutDockTopBottom 方法负责布局顶部和底部的子窗口,支持相对停靠关系和权重分配。
4.2.3 左侧和右侧布局实现¶
LayoutDockLeftRight 方法负责布局左侧和右侧的子窗口,同样支持相对停靠关系和权重分配。
5. 代码示例¶
5.1 基本框架布局¶
<window layout="frame" size="400,300" colorBkgnd="#cccccc">
<!-- 顶部停靠 -->
<window dock="top" size="-1,50" colorBkgnd="#ff0000">
<text>顶部标题栏</text>
</window>
<!-- 左侧停靠 -->
<window dock="left" size="100,-1" colorBkgnd="#00ff00">
<text>左侧导航</text>
</window>
<!-- 右侧停靠 -->
<window dock="right" size="100,-1" colorBkgnd="#0000ff">
<text>右侧面板</text>
</window>
<!-- 底部停靠 -->
<window dock="bottom" size="-1,30" colorBkgnd="#ffff00">
<text>底部状态栏</text>
</window>
<!-- 主视图区域 -->
<window dock="main" colorBkgnd="#888888">
<text>主内容区域</text>
</window>
</window>
5.2 带相对停靠的框架布局¶
<window layout="frame" size="400,300" colorBkgnd="#cccccc">
<!-- 顶部主工具栏 -->
<window id="toolbar" dock="top" size="-1,40" colorBkgnd="#ff0000">
<text>主工具栏</text>
</window>
<!-- 顶部辅助工具栏,相对于主工具栏 -->
<window dock="top" dockRelativeTo="toolbar" size="-1,30" colorBkgnd="#ff6666">
<text>辅助工具栏</text>
</window>
<!-- 左侧面板 -->
<window dock="left" size="120,-1" colorBkgnd="#00ff00">
<text>左侧面板</text>
</window>
<!-- 主内容区域 -->
<window dock="main" colorBkgnd="#888888">
<text>主内容区域</text>
</window>
</window>
5.3 启用特定停靠模式的框架布局¶
<window layout="frame" size="400,300" colorBkgnd="#cccccc" enableDockMode="left,top,right">
<!-- 顶部停靠 -->
<window dock="top" size="-1,40" colorBkgnd="#ff0000">
<text>顶部工具栏</text>
</window>
<!-- 左侧停靠 -->
<window dock="left" size="100,-1" colorBkgnd="#00ff00">
<text>左侧面板</text>
</window>
<!-- 右侧停靠 -->
<window dock="right" size="100,-1" colorBkgnd="#0000ff">
<text>右侧面板</text>
</window>
<!-- 主内容区域 -->
<window dock="main" colorBkgnd="#888888">
<text>主内容区域</text>
</window>
</window>
5.4 典型 IDE 界面布局示例¶
以下是 uieditor 中使用 FrameLayout 的完整示例:
<window size="-2,0" weight="1" layout="frame" enableDockMode="any">
<!--menu bar-->
<menubar name="main_menu" size="-2,36" useMenuEx="1" dock="top">
<!-- 菜单内容 -->
</menubar>
<toolbar name="tb_main" dock="top" size="0,40" weight="1" skin="skin_item_bk" colorText="@color/white">
<!-- 工具栏内容 -->
</toolbar>
<!--main designer-->
<splitcol name="NAME_UIDESIGNER_split_col" dock="mainview" sepSkin="" sepSize="6">
<pane idealSize="380" minSize="30" priority="1" clipClient="1">
<include src="layout:xml_mainwnd_left" />
</pane>
<pane idealSize="800" minSize="30" priority="0" clipClient="1">
<include src="layout:xml_uidesigner_main" />
</pane>
</splitcol>
<dockbar width="300" dock="right" name="property_panel_dock" text="@string/property" resizable="1" margin="2">
<include src="layout:property_panel" />
</dockbar>
<window name="wnd_status" size="-2,25" dock="bottom" margin="2" colorBorder="@color/border" layout="hbox" gravity="center">
<text name="txt_status" text="@string/idlemsg" />
</window>
</window>
布局效果说明:
上述布局实现了一个典型的 IDE 界面布局:
- 顶部:菜单栏和工具栏
- 右侧:属性面板
- 底部:状态栏
- 中间:主设计区域(使用 splitcol 分割为左右两部分)
6. 高级用法¶
6.1 相对停靠¶
通过 dockRelativeTo 属性指定相对于其他窗口的停靠关系:
<window name="toolbar1" dock="top" size="-2,30" />
<window name="toolbar2" dock="top" size="-2,30" dockRelativeTo="toolbar1" />
6.2 权重分配¶
通过 weight 属性分配剩余空间:
<window dock="left" width="100" weight="0" />
<window dock="left" width="0" weight="1" />
6.3 停靠模式限制¶
通过 enableDockMode 属性限制允许的停靠模式:
<window layout="frame" enableDockMode="left,right,top,bottom">
<!-- 只能停靠在左、右、上、下,不能设置为 mainview -->
</window>
7. 与 MFC CFrameWnd 的对比¶
| 特性 | SOUI FrameLayout | MFC CFrameWnd |
|---|---|---|
| 布局方式 | XML 声明式布局 | 代码式布局 |
| 停靠位置 | left, top, right, bottom, mainview | left, top, right, bottom, client |
| 相对停靠 | 支持(dockRelativeTo) | 支持(SetBarStyle) |
| 权重分配 | 支持(weight) | 有限支持 |
| 边距扩展 | 支持(extend_*) | 支持(SetBarWidth) |
| 灵活性 | 更高,支持动态调整 | 较低,需要手动代码调整 |
| 布局保存 | 支持(SaveLayout) | 需要手动实现 |
| 布局恢复 | 支持(RestoreLayout) | 需要手动实现 |
8. 布局保存与恢复功能¶
FrameLayout 提供了完整的布局保存和恢复功能,允许用户自定义布局并保存到配置文件,下次启动时自动恢复。
8.1 API 接口¶
class SFrameLayout {
public:
// 保存布局到数据结构
BOOL SaveLayout(IWindow *pParent, SArray<FrameLayoutItemInfo> &lstItems) const;
// 从数据结构恢复布局
BOOL RestoreLayout(IWindow *pParent, const SArray<FrameLayoutItemInfo> &lstItems);
};
8.2 数据结构¶
struct FrameLayoutItemInfo : public SFrameLayoutParamStruct
{
SStringW strName; // 窗口名称
BOOL bVisible; // 是否可见
};
8.3 保存布局到文件¶
// 获取 FrameLayout 布局对象
SFrameLayout *pLayout = (SFrameLayout *)pFrameWindow->GetLayout();
// 保存布局到数据结构
SArray<FrameLayoutItemInfo> lstItems;
pLayout->SaveLayout(pFrameWindow, lstItems);
// 自定义保存逻辑 - 这里示例使用简单的二进制格式
FILE *fp = _wfopen(L"layout_config.bin", L"wb");
if (fp) {
// 写入项目数量
int nCount = lstItems.GetCount();
fwrite(&nCount, sizeof(int), 1, fp);
// 写入每个项目的信息
for (int i = 0; i < nCount; i++) {
const FrameLayoutItemInfo &item = lstItems[i];
// 写入窗口名称
int nNameLen = item.strName.GetLength();
fwrite(&nNameLen, sizeof(int), 1, fp);
fwrite(item.strName.c_str(), sizeof(wchar_t), nNameLen, fp);
// 写入其他属性
fwrite(&item.dockPos, sizeof(DockPosition), 1, fp);
fwrite(&item.width, sizeof(SLayoutSize), 1, fp);
fwrite(&item.height, sizeof(SLayoutSize), 1, fp);
fwrite(&item.weight, sizeof(float), 1, fp);
fwrite(&item.bVisible, sizeof(BOOL), 1, fp);
}
fclose(fp);
}
8.4 从文件恢复布局¶
// 获取 FrameLayout 布局对象
SFrameLayout *pLayout = (SFrameLayout *)pFrameWindow->GetLayout();
// 自定义加载逻辑
SArray<FrameLayoutItemInfo> lstItems;
FILE *fp = _wfopen(L"layout_config.bin", L"rb");
if (fp) {
// 读取项目数量
int nCount;
fread(&nCount, sizeof(int), 1, fp);
// 读取每个项目的信息
for (int i = 0; i < nCount; i++) {
FrameLayoutItemInfo item;
// 读取窗口名称
int nNameLen;
fread(&nNameLen, sizeof(int), 1, fp);
wchar_t *szName = new wchar_t[nNameLen + 1];
fread(szName, sizeof(wchar_t), nNameLen, fp);
szName[nNameLen] = 0;
item.strName = szName;
delete[] szName;
// 读取其他属性
fread(&item.dockPos, sizeof(DockPosition), 1, fp);
fread(&item.width, sizeof(SLayoutSize), 1, fp);
fread(&item.height, sizeof(SLayoutSize), 1, fp);
fread(&item.weight, sizeof(float), 1, fp);
fread(&item.bVisible, sizeof(BOOL), 1, fp);
lstItems.Add(item);
}
fclose(fp);
// 恢复布局
pLayout->RestoreLayout(pFrameWindow, lstItems);
}
8.5 高级用法¶
动态调整布局¶
// 获取当前布局
SArray<FrameLayoutItemInfo> lstItems;
pLayout->SaveLayout(pFrameWindow, lstItems);
// 修改某个窗口的停靠位置
for (int i = 0; i < lstItems.GetCount(); i++) {
if (lstItems[i].strName == L"property_panel_dock") {
lstItems[i].dockPos = DockLeft; // 将属性面板从右侧移到左侧
lstItems[i].width.fSize = 250; // 调整宽度
}
}
// 应用新布局
pLayout->RestoreLayout(pFrameWindow, lstItems);
布局预设¶
// 保存多个布局预设
void SaveLayoutPreset(const SStringW &strPresetName) {
SArray<FrameLayoutItemInfo> lstItems;
pLayout->SaveLayout(pFrameWindow, lstItems);
SStringW strFileName;
strFileName.Format(L"presets\\%s.bin", strPresetName.c_str());
// 自定义保存逻辑
FILE *fp = _wfopen(strFileName, L"wb");
if (fp) {
// 写入项目数量
int nCount = lstItems.GetCount();
fwrite(&nCount, sizeof(int), 1, fp);
// 写入每个项目的信息
for (int i = 0; i < nCount; i++) {
const FrameLayoutItemInfo &item = lstItems[i];
// 写入窗口名称
int nNameLen = item.strName.GetLength();
fwrite(&nNameLen, sizeof(int), 1, fp);
fwrite(item.strName.c_str(), sizeof(wchar_t), nNameLen, fp);
// 写入其他属性
fwrite(&item.dockPos, sizeof(DockPosition), 1, fp);
fwrite(&item.width, sizeof(SLayoutSize), 1, fp);
fwrite(&item.height, sizeof(SLayoutSize), 1, fp);
fwrite(&item.weight, sizeof(float), 1, fp);
fwrite(&item.bVisible, sizeof(BOOL), 1, fp);
}
fclose(fp);
}
}
// 加载布局预设
void LoadLayoutPreset(const SStringW &strPresetName) {
SStringW strFileName;
strFileName.Format(L"presets\\%s.bin", strPresetName.c_str());
// 自定义加载逻辑
SArray<FrameLayoutItemInfo> lstItems;
FILE *fp = _wfopen(strFileName, L"rb");
if (fp) {
// 读取项目数量
int nCount;
fread(&nCount, sizeof(int), 1, fp);
// 读取每个项目的信息
for (int i = 0; i < nCount; i++) {
FrameLayoutItemInfo item;
// 读取窗口名称
int nNameLen;
fread(&nNameLen, sizeof(int), 1, fp);
wchar_t *szName = new wchar_t[nNameLen + 1];
fread(szName, sizeof(wchar_t), nNameLen, fp);
szName[nNameLen] = 0;
item.strName = szName;
delete[] szName;
// 读取其他属性
fread(&item.dockPos, sizeof(DockPosition), 1, fp);
fread(&item.width, sizeof(SLayoutSize), 1, fp);
fread(&item.height, sizeof(SLayoutSize), 1, fp);
fread(&item.weight, sizeof(float), 1, fp);
fread(&item.bVisible, sizeof(BOOL), 1, fp);
lstItems.Add(item);
}
fclose(fp);
// 恢复布局
pLayout->RestoreLayout(pFrameWindow, lstItems);
}
}
9. 使用场景¶
框架布局适用于:
- 需要将界面划分为多个区域的场景
- 类似 IDE、编辑器等复杂界面的布局
- 需要固定位置的工具栏、状态栏、侧边栏等
- 主内容区域需要根据其他区域的大小自动调整的场景
10. 最佳实践¶
- 合理规划停靠顺序
- 通常先设置顶部和底部,再设置左侧和右侧,最后设置主视图
-
停靠顺序会影响布局结果
-
善用 dockRelativeTo
- 当有多个相同方向的停靠窗口时,使用 dockRelativeTo 指定相对关系
-
可以创建更复杂的嵌套停靠结构
-
灵活使用 size 属性
- 对于停靠在边缘的窗口,通常设置一个方向的固定大小,另一个方向填充
-
对于主视图,通常设置为填充剩余空间
-
合理设置 enableDockMode
- 根据需要限制可停靠的方向
-
可以避免意外的布局行为
-
注意嵌套使用
- 框架布局可以嵌套使用
- 在主视图区域内可以使用其他布局方式
11. 总结¶
FrameLayout 是 SOUI 框架中一种强大的布局方式,它提供了类似 MFC CFrameWnd 的停靠布局功能,但更加灵活和易于使用。通过 XML 声明式布局,开发者可以快速构建复杂的界面布局,而不需要编写大量的布局代码。
FrameLayout 的核心价值在于:
- 简化布局代码:通过 XML 声明式布局,减少了大量的布局代码
- 提高开发效率:快速构建复杂的界面布局,如 IDE、编辑器等
- 增强用户体验:支持灵活的布局调整,满足不同用户的需求
- 布局持久化:提供布局保存和恢复功能,允许用户自定义布局并保存
- 跨平台兼容性:作为 SOUI 框架的一部分,支持跨平台运行
通过合理使用 FrameLayout,开发者可以构建出功能强大、界面美观的应用程序,为用户提供更好的使用体验。