界面布局:Dock面板与多窗口
这是「从零搭建工业控制系统」系列第16篇。后端逻辑聊了十几篇,终于说到前端了。工业系统的界面不像网页那样花哨,但布局复杂度一点不低——十几个面板同屏显示,还要可拖拽、可隐藏、可保存布局。本篇只讲布局,自定义控件下篇再说。
第一次布局的灾难
最初版本的界面我用Grid硬切,三行五列,每个格子塞一个UserControl。看起来整齐,但操作员投诉说面板太小看不清数据,想拖大一点又拖不动。
后来换成了DockPanel + AvalonDock(DockingManager),面板可拖拽、可浮动、可钉住。操作员想看哪个面板就拖大哪个,不想看的折叠起来。布局还能保存,下次启动自动恢复。
整体布局结构
主窗口用Grid分三块:顶部导航栏、中间DockingManager、底部状态栏:

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- 导航栏 -->
<RowDefinition Height="*" /> <!-- DockingManager -->
<RowDefinition Height="Auto" /> <!-- 状态栏 -->
</Grid.RowDefinitions>
<!-- 顶部导航:Logo + 用户信息 + 权限显示 -->
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="控制系统" FontWeight="Bold" />
<TextBlock Text="{Binding PermissionService.UserDisplayText}" />
</StackPanel>
<!-- 中间:可拖拽面板区域 -->
<dock:DockingManager Grid.Row="1">
<!-- 各种可停靠面板 -->
</dock:DockingManager>
<!-- 底部状态栏:连接状态 + 报警状态 -->
<StatusBar Grid.Row="2">
<StatusBarItem Content="{Binding DeviceStatusText}" />
</StatusBar>
</Grid>
Dock面板布局
DockingManager 里放 LayoutDocumentPane 和 LayoutAnchorablePane。Document是标签页式面板,Anchorable是可停靠可隐藏的面板:

<dock:DockingManager>
<dock:LayoutRoot>
<dock:LayoutPanel Orientation="Horizontal">
<!-- 左侧:自动运行面板 -->
<dock:LayoutAnchorablePane DockWidth="300">
<dock:LayoutAnchorable Title="自动运行" ContentId="AutoPanel">
<views:AutoRunView />
</dock:LayoutAnchorable>
</dock:LayoutAnchorablePane>
<!-- 中间:主操作区 -->
<dock:LayoutDocumentPane>
<dock:LayoutDocument Title="设备控制" ContentId="DevicePanel">
<views:DevicePanel />
</dock:LayoutDocument>
</dock:LayoutDocumentPane>
<!-- 右侧:日志面板 -->
<dock:LayoutAnchorablePane DockWidth="400">
<dock:LayoutAnchorable Title="日志" ContentId="LogPanel">
<views:LogView />
</dock:LayoutAnchorable>
</dock:LayoutAnchorablePane>
</dock:LayoutPanel>
</dock:LayoutRoot>
</dock:DockingManager>
权限控制面板显隐
面板的显隐也受权限控制。操作员不该看到的面板直接不创建:
<dock:LayoutAnchorable
Title="原始IO调试"
Visibility="{Binding PermissionService.IsAdmin,
Converter={StaticResource BoolToVisibilityConverter}}">
<views:RawIOPanel />
</dock:LayoutAnchorable>
布局保存与恢复
AvalonDock支持布局序列化。程序关闭时保存,启动时恢复:
// 保存布局
private void SaveLayout()
{
var serializer = new XmlLayoutSerializer(dockingManager);
using var writer = new StreamWriter(layoutFilePath);
serializer.Serialize(writer);
}
// 恢复布局
private void LoadLayout()
{
if (!File.Exists(layoutFilePath)) return;
var serializer = new XmlLayoutSerializer(dockingManager);
serializer.LayoutSerializationCallback += (s, e) =>
{
// 根据ContentId找回ViewModel
e.Content = FindContent(e.Model.ContentId);
};
using var reader = new StreamReader(layoutFilePath);
serializer.Deserialize(reader);
}
踩过的坑:反序列化时如果找不到对应的ContentId,面板会消失。回调函数里要处理找不到的情况,给个默认面板。

ViewModel与View的映射
因为代码混淆,默认的ViewModel自动定位会失效。在 App.xaml.cs 里手动注册映射:
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
ViewModelLocationProvider.Register<Views.DevicePanel, ViewModels.DevicePanelViewModel>();
ViewModelLocationProvider.Register<Views.Dock.LogView, ViewModels.Dock.LogViewModel>();
// ... 其他映射
}
不注册的话,Release版混淆后View找不到对应的ViewModel,界面全是空的。

界面踩坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| Grid硬切 | 面板不能拖拽 | 用DockingManager |
| 混淆后VM丢失 | Release版界面空白 | 手动注册ViewModel映射 |
| 布局恢复失败 | 面板消失 | 处理LayoutSerializationCallback |
| 权限面板可见 | 操作员看到调试面板 | Visibility绑定权限属性 |
本篇小结
| 知识点 | 关键做法 |
|---|---|
| Dock布局 | AvalonDock DockingManager |
| 面板类型 | Document(标签页) + Anchorable(可停靠) |
| 权限控制 | Visibility绑定IsAdmin等属性 |
| 布局序列化 | XmlLayoutSerializer保存/恢复 |
| VM映射 | ViewModelLocationProvider手动注册 |
工业界面的核心不是好看,是信息密度——一屏能看到多少关键数据,能多快找到要操作的按钮。
下期预告
第17篇:自定义控件设计
布局讲完了,下篇讲自定义控件——线圈开关、模拟仪表、DataTemplate选择器,怎么封装可复用的工业UI组件。
:界面布局——Dock面板与多窗口&spm=1001.2101.3001.5002&articleId=164033778&d=1&t=3&u=ac52d93820dc4727af54cab3d15e7794)

被折叠的 条评论
为什么被折叠?



