跳转至

框架布局

框架布局是 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 主视图(占据剩余空间) mainviewmain

2.2 布局特性

  • 相对停靠:支持通过 dockRelativeTo 属性指定相对于其他窗口的停靠关系
  • 权重分配:支持通过 weight 属性分配剩余空间
  • 边距扩展:支持通过 extend_leftextend_topextend_rightextend_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 的布局流程如下:

  1. 收集子窗口:遍历所有可见的子窗口,检查其停靠模式是否与父布局的 enableDockMode 匹配
  2. 测量子窗口:计算每个子窗口的期望大小
  3. 布局顶部和底部:首先布局顶部和底部的子窗口,调整可用空间
  4. 布局左侧和右侧:然后布局左侧和右侧的子窗口,进一步调整可用空间
  5. 布局主视图:最后将剩余空间分配给主视图

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. 使用场景

框架布局适用于:

  1. 需要将界面划分为多个区域的场景
  2. 类似 IDE、编辑器等复杂界面的布局
  3. 需要固定位置的工具栏、状态栏、侧边栏等
  4. 主内容区域需要根据其他区域的大小自动调整的场景

10. 最佳实践

  1. 合理规划停靠顺序
  2. 通常先设置顶部和底部,再设置左侧和右侧,最后设置主视图
  3. 停靠顺序会影响布局结果

  4. 善用 dockRelativeTo

  5. 当有多个相同方向的停靠窗口时,使用 dockRelativeTo 指定相对关系
  6. 可以创建更复杂的嵌套停靠结构

  7. 灵活使用 size 属性

  8. 对于停靠在边缘的窗口,通常设置一个方向的固定大小,另一个方向填充
  9. 对于主视图,通常设置为填充剩余空间

  10. 合理设置 enableDockMode

  11. 根据需要限制可停靠的方向
  12. 可以避免意外的布局行为

  13. 注意嵌套使用

  14. 框架布局可以嵌套使用
  15. 在主视图区域内可以使用其他布局方式

11. 总结

FrameLayout 是 SOUI 框架中一种强大的布局方式,它提供了类似 MFC CFrameWnd 的停靠布局功能,但更加灵活和易于使用。通过 XML 声明式布局,开发者可以快速构建复杂的界面布局,而不需要编写大量的布局代码。

FrameLayout 的核心价值在于:

  1. 简化布局代码:通过 XML 声明式布局,减少了大量的布局代码
  2. 提高开发效率:快速构建复杂的界面布局,如 IDE、编辑器等
  3. 增强用户体验:支持灵活的布局调整,满足不同用户的需求
  4. 布局持久化:提供布局保存和恢复功能,允许用户自定义布局并保存
  5. 跨平台兼容性:作为 SOUI 框架的一部分,支持跨平台运行

通过合理使用 FrameLayout,开发者可以构建出功能强大、界面美观的应用程序,为用户提供更好的使用体验。