常用控件:从基础到熟练
上一篇把窗口分成了状态、曲线、配方、报警。这一篇把占位换成真正的控件。数据先写死在 XAML 里。Binding 是下一篇,Command 是第 5 篇。
熟练的标准:配方区能选配方、改上下限;报警区是一张只能看、不能改的表;说得清显示用TextBlock,输入用TextBox,一组记录用ListBox还是DataGrid。
1. 基础:文本、输入、按钮
TextBlock:只显示
状态、温度、标签都用它。操作员不能在上面打字。
<TextBlockText="25.0"FontSize="20"VerticalAlignment="Center"/>要显示的是Text。不要为了显示一个数去用只读TextBox,只读输入框仍然像能编辑。
TextBox:给操作员改的值
配方上下限用它。
<TextBoxText="30"Width="120"HorizontalAlignment="Left"/>不设Width且放在会拉伸的格子里时,输入框会拉满整列。配方区这样是合适的。状态栏里不要放会拉满的输入框。
上限、下限是单行数值。AcceptsReturn默认就是 false,回车不会在框里换行。不要为了配方输入去打开它。报警备注那种需要多行的文本才设AcceptsReturn="True",并加上TextWrapping="Wrap"。数字是否合法留给转换器,这一篇先允许框里出现任意字符。
Text在这一篇是写死的字。下一篇改成{Binding HighLimit}之后,界面和数据才连得上。
Button:点一下做一件事
<ButtonContent="启动"Padding="12,4"/>按钮上的字是Content,不是Text。Content也可以是别的元素,例如一个圆点加文字。现在先写字符串。Button、ListBoxItem、ComboBoxItem都是ContentControl,所以内容都放在Content上,可以是文字,也可以是更复杂的界面。模板那篇再讲这块内部怎么画。
Click能进 code-behind。本篇若要确认按钮是活的,可以临时在Click里改某个TextBlock的Text。启动设备、写寄存器不要写在Click里,那是 Command 那篇的事。
三个控件的属性不要混:
| 控件 | 字写在哪 | 谁来改 |
|---|---|---|
| TextBlock | Text | 程序 |
| TextBox | Text | 操作员,稍后由绑定写回 |
| Button | Content | 一般不改 |
2. 熟练:选择一项,和列出很多行
ComboBox:多套配方里选一套
<ComboBoxSelectedIndex="0"HorizontalAlignment="Stretch"><ComboBoxItemContent="默认配方"/><ComboBoxItemContent="高温"/><ComboBoxItemContent="低温"/></ComboBox>SelectedIndex="0"是第一项。操作员展开后只能选中一项。上位机里它代表「当前编辑的是哪套配方」,不是用来显示报警的。
IsEditable默认是 false,操作员不能在框里打出列表中没有的名字。配方选择保持默认。设成 true 时,框变成可输入,容易写出一套不存在的配方。
项很多、或项是后面从 SQLite 读出来的,就不要在 XAML 里逐条写ComboBoxItem。下一篇起改为ItemsSource绑定到集合。
ListBox:一列记录,突出当前选中
适合先把报警「列出来」。每项这一篇先是纯文本。
<ListBox><ListBoxItemContent="12:01 温度超上限 31.2"/><ListBoxItemContent="12:04 已恢复"/></ListBox>点某一行,它成为选中项。SelectionMode默认是Single,报警表保持单选即可。ListBox自己带滚动,不必再包一层ScrollViewer。前提是父布局给了它一块有限高度,例如 Grid 里固定 180 的那一行。放进竖直的 StackPanel 时,可用高度是无穷的,列表会按全部内容一直变高,滚动条不会出现。这和布局篇里 StackPanel 不把剩余空间分给子元素是同一件事。
ListBox的一项后期会换成时间、内容、数值三列,那是数据模板那篇。这一篇用一行字就能练习选中。
DataGrid:已经知道列的表
报警最终更适合表,因为列是固定的:时间、内容、当时的值。
<DataGridAutoGenerateColumns="False"IsReadOnly="True"CanUserAddRows="False"HeadersVisibility="Column"GridLinesVisibility="Horizontal"><DataGrid.Columns><DataGridTextColumnHeader="时间"Binding="{Binding Time}"Width="160"/><DataGridTextColumnHeader="内容"Binding="{Binding Message}"Width="*"/><DataGridTextColumnHeader="值"Binding="{Binding Value}"Width="80"/></DataGrid.Columns></DataGrid>这一篇先认识这几个开关。列上的Binding只是列定义。没有ItemsSource时不会生成行,所以表现在是空的。行数据下一篇再接。
ListBox和DataGrid都自带滚动,同样要求父布局限制高度。报警表放在 Grid 的固定高度行里。不要放进竖直 StackPanel,否则表会无限变高,内部滚动不会出现。HeadersVisibility="Column"去掉最左边的行头;有的主题下单元格左侧仍会留一条细缝,不影响列内容。
| 属性 | 作用 |
|---|---|
AutoGenerateColumns="False" | 列由自己写,不按对象属性自动长出一堆列 |
IsReadOnly="True" | 报警是记录,不允许在格子里改字 |
CanUserAddRows="False" | 底部不留一个空白新行 |
HeadersVisibility="Column" | 只要列表头,不要最左边的行头 |
Width="*" | 这一列吃掉剩余宽度,和 Grid 的*同一思路 |
怎么选:
- 配方名称这种「从几项里挑一个」用
ComboBox。 - 先看选中效果、一项就一句话,用
ListBox。 - 时间、内容、数值这种多列记录,用
DataGrid,并且只读。
不要用DataGrid做主界面布局。它是表,不是面板。
选中项
ListBox、ComboBox、DataGrid都有SelectedIndex和SelectedItem。
SelectedIndex是第几项,从 0 开始。没有选中时是-1。SelectedItem是那一项本身。静态 XAML 里它是ListBoxItem或ComboBoxItem。绑到集合之后,它是集合里的那个对象,例如一条报警。
本篇在设计器或运行界面里点一行,能看到该项高亮,就说明选中在工作。下一篇把SelectedItem绑到 ViewModel,用来删除这一条。
多选删除
报警偶尔要一次删掉几条。SelectionMode还有Multiple和Extended。Extended可以用 Ctrl 和 Shift 连续选,和资源管理器一样,报警表用这个。默认Single保持不变,直到真的要多选。
<DataGridSelectionMode="Extended"/>SelectedItem仍然只表示最后点中的那一条。多选的集合是SelectedItems。它不是依赖属性,不能写{Binding SelectedItems}。删除前在SelectionChanged里把当前选中抄进 ViewModel 的一份列表,命令删的是这份列表,不是控件上的属性。
privatevoidOnAlarmSelectionChanged(objectsender,SelectionChangedEventArgse){if(DataContextisnotDeviceViewModelvm)return;vm.ReplaceSelection(((DataGrid)sender).SelectedItems.Cast<AlarmRow>());}没有选中时列表为空,删除命令直接返回。不要用SelectedIndex != -1代替这份列表。
数字过滤和红框校验见08-值转换器.md。表的行虚拟化,以及行样式和数据模板怎么分工,见07-数据模板.md和06-资源与样式.md。在线、运行那种状态灯不是TextBlock也不是Ellipse堆在窗口上,从10-依赖属性.md的StatusLamp做起。
3. 后期应用:放回四块骨架
曲线区仍用Border占位。其余三块换成控件。状态栏的「启动」「停止」先只是按钮,不写Click。
<StackPanelGrid.Row="0"Grid.ColumnSpan="2"Orientation="Horizontal"Margin="12,8"><TextBlockText="离线"VerticalAlignment="Center"/><TextBlockText="待机"Margin="16,0,0,0"VerticalAlignment="Center"/><TextBlockText="25.0"Margin="16,0,0,0"FontSize="20"VerticalAlignment="Center"/><ButtonContent="启动"Margin="16,0,0,0"Padding="12,4"/><ButtonContent="停止"Margin="8,0,0,0"Padding="12,4"/></StackPanel><BorderGrid.Row="1"Grid.Column="1"Margin="6,0,12,8"Padding="12"><StackPanel><TextBlockText="配方"FontSize="16"/><ComboBoxMargin="0,12,0,0"SelectedIndex="0"><ComboBoxItemContent="默认配方"/><ComboBoxItemContent="高温"/><ComboBoxItemContent="低温"/></ComboBox><TextBlockText="上限"Margin="0,12,0,4"/><TextBoxText="30"/><TextBlockText="下限"Margin="0,12,0,4"/><TextBoxText="10"/><ButtonContent="保存"Margin="0,12,0,0"Padding="12,4"HorizontalAlignment="Left"/></StackPanel></Border><DataGridGrid.Row="2"Grid.ColumnSpan="2"Margin="12,0,12,12"AutoGenerateColumns="False"IsReadOnly="True"CanUserAddRows="False"HeadersVisibility="Column"><DataGrid.Columns><DataGridTextColumnHeader="时间"Binding="{Binding Time}"Width="160"/><DataGridTextColumnHeader="内容"Binding="{Binding Message}"Width="*"/><DataGridTextColumnHeader="值"Binding="{Binding Value}"Width="80"/></DataGrid.Columns></DataGrid>运行后应看到:状态下拉不动、温度是一块文本;配方能切换三项、上下限能打字;报警表只有表头,没有空行可编辑。表是空的是对的,行要等变更通知那篇往集合里加。
若想在这一篇就看到报警文字,同一格先改用ListBox加两条ListBoxItem。换成DataGrid时删掉这些静态项。
按钮变灰这一篇可以写IsEnabled="False"看效果,然后删掉。真正的灰亮跟着待机、运行、离线走,用 Command 的CanExecute,不要在界面上写死。
4. 自测
- 温度数值为什么用
TextBlock,上限为什么用TextBox? Button上的字写在Text还是Content?- 三套配方切换用
ListBox、ComboBox还是DataGrid? - 报警表为什么要
IsReadOnly和CanUserAddRows="False"? - 没有任何选中时,
SelectedIndex是多少?
参考答案:
- 温度是程序显示给操作员看的。上限是操作员要修改并保存的设定。
Content。ComboBox。它表示当前这一套,不占一大块列表,也不需要多列。- 报警是历史记录。只读避免改字,关掉新增行避免底部多出一个空行。
-1。
5. 练习
- 用第 3 节替换主窗口里的状态栏、配方区和报警区。运行,切换配方,在上下限里打字。
- 把报警区临时换成带两条
ListBoxItem的ListBox,点其中一行。再换回DataGrid。 - 给「启动」加上
IsEnabled="False",确认它变灰且点不到。确认完删掉这个属性。
面试可能提问
温度用 TextBlock,上限用 TextBox,依据是什么?
答:温度是程序显示给操作员的。上限是操作员要改、并且会写回配方的。只读的数不要放进 TextBox。报警记录为什么用只读 DataGrid,而不让操作员在格子里改?
答:报警是已经发生的记录。IsReadOnly禁止改字,CanUserAddRows="False"去掉底部空行。列用AutoGenerateColumns="False"自己定,避免按对象属性长出多余列。三套配方的切换为什么用 ComboBox?
答:操作员只需要选定当前这一套。ListBox 占一块列表,DataGrid 适合多列记录,都不适合「从几项里挑一个」。SelectedIndex为 -1 表示什么?删除当前报警前要先看什么?
答:表示没有选中项。删除前看SelectedItem是不是 null。不要只判断SelectedIndex != -1就去取第 0 项。两种判断大多同步,业务代码以SelectedItem这份对象为准,没有选中就是 null。