程序的本质是“数据+算法”——用户输入原始数据,算法处理原始数据并得到结果数据。程序可以使用LED阵列、格式字符串、图形化用户界面(Graphic User Interface,GUI)将结果数据显示给用户,其中图形化用户界面最方便、直观。

程序的本质是“数据+算法”——用户输入原始数据,算法处理原始数据并得到结果数据 。程序可以使用LED阵列、格式字符串、图形化用户界面(Graphic User Interface,GUI)将结果数据显示给用户,其中图形化用户界面最方便、直观。
在Windows上实现图形化的界面有多种方法,每种方法又拥有自己的一套开发理念和工具并组成一种方法论,常见的有:

  • Windows APl(Win APl) :调用Windows底层绘图函数,使用C语言,最原始也最基础。
  • Microsoft Foundation Class(MFC) :使用C++语法将原始的Win32API函数封装成控件类。
  • Visual Component Library(VCL) :Delphi和C++Builder使用的与MFC相近的控件类库。
  • Visual Basic+ActiveX控件(VB6) :使用组件化的思想把WinAPI封装成UI控件,以期多语言共用。
  • Java Swing/AWT :Java SDK中用于跨平台开发GUI程序的控件类库。
  • Windows Form :.NET平台上进行GUl开发的老牌劲旅,完全组件化但需要.NET运行时支持。
  • Windows Presentation Foundation(WPF) :后起之秀,使用全新的数据驱动UI的理念。
  • 可以把上述这些方法论分为四代:

  • WinAPI时代 :函数调用+Windows消息处理。
  • 封装时代 :使用面向对象理念把WinAPI封装成类;由来自UI的消息驱动程序处理数据。
  • 组件化时代 :使用面向组件理念在类的基础上封装成组件;消息被封装成事件,变成事件驱动。
  • WPF时代 :在组件化的基础上,使用专门的UI设计语言并引入由数据驱动UI的理念。
  • 注:目前流行使用前端的方式实现GUI,如 Electron NanUI 等。

    WPF中是数据驱动UI,数据是核心、是主动的;UI从属于数据并表达数据、是被动的 。WPF把那些能够展示数据、响应用户操作的UI元素称为 控件(Control) ,控件所展示的数据称之为控件的“数据内容”,控件在响应用户的操作后会执行自己的一些方法或以事件(Event)的形式通知应用程序称之为 控件的“行为”或“算法内容”

    WPF中的控件是个非常抽象的概念—— Control是数据和行为的载体 ,而 无需具有固定的形象 ,如只要是用来显示一个bool类型值并允许用户通过单击来切换true/false/null的UI元素就是一个CheckBox( 关注抽象的数据和行为而不是控件具体的形象 )。

    日常工作中打交道最多的控件无外乎6类,即:

  • 布局控件 :可以容纳多个控件或嵌套其他布局控件,用于在UI上组织和排列控件,如Grid、StackPanel、DockPanel 等控件( 共同的父类是Panel )。
  • 内容控件 :只能容纳一个其他控件或布局控件作为它的内容,如Window、Button等控件(经常需要借助布局控件来规划其内容, 共同父类是ContentControl )。
  • 带标题内容控件 :相当于一个内容控件,但可以加一个标题(Header),标题部分亦可容纳一个控件或布局,GroupBox、Tabltem等是这类控件的典型代表(共同父类是HeaderedContentControl)。
  • 条目控件 :可以显示一列数据,一般情况下这列数据的类型相同,此类控件包括ListBox、ComboBox等( 共同基类是ltemsControl ,在显示集合类型数据方面功能非常强大)。
  • 带标题条目控件 :相当于一个条目控件加上一个标题显示区,Tree Viewltem、Menultem都属于此类控件(往往用于显示层级关系数据,结点显示在其Header区域,子级结点则显示在其条目控件区域, 共同基类是HeaderedltemsControl )。
  • 特殊内容控件 :这类控件相对 比较独立 ,比如TextBox容纳的是字符串、TextBlock可以容纳可自由控制格式的文本、Image容纳图片类型数据。
  • 6类控件的派生关系如下图所示:

    WPF是构建在.NET Framework上的一个子系统,它也是一个用于开发应用程序的框架(Framework),FrameworkElement的Framework指的就是WPF Framework 。而FrameworkElement类在UIElement类的基础上添加了很多专门用于WPF开发的API(比如SetBinding方法),所以从这个类开始才算是进入WPF开发柜架。

    WPF的内容模型

    根据是否可以装载内容、能够装载什么样的内容,WPF的UI元素可以分为如下类型:

    控件的 内容可以直接是数据,也可以是控件 。当控件的内容还是控件的时候就形成了控件的嵌套(UI布局时尤为常见),被嵌套的控件称为子级控件,所以WPF的UI会形成一个树形结构。

  • 逻辑树(Logical Tree) :不考虑控件内部的组成结构,只观察由控件组成的“树”。
  • 可视元素树(Visual Tree) :WPF控件往往是由更基本的控件构成的(控件本身就是一棵树),连控件本身的树也考虑在内,比逻辑树更“繁茂”的树。
  • 控件是内存中的对象,控件的内容也是内存中的对象。 控件通过自己的某个属性引用着作为其内容的对象,这个属性称为内容属性(Content Property) 。“内容属性”是个统称,具体到每种控件上,内容属性都有自己确切的名字——Content、Child、Items或Children。

    控件的内容属性与XAML标签的内容存在一定的对应关系,XAML标签的内容区域专门映射了控件的内容属性,下面从语法和常理来解释一下:

  • 严格按照语法来说, 控件有内容属性,在XAML里就应该能够使用Atribute=Value 或者属性标签的形式来为内容赋值 ,如:
  • <Button Content="OK"/> 
    <!--或-->
    <Button>
      <Button.Content>
        <sys:String>OK</sys:String>
      </Button.Content>
    </Button>
    
  • 按照常理来说,控件对应到XAML文档里就是标签,控件的内容就应该是标签的内容、子级控件就应该是标签的子级元素(简称标签的元素),标签的内容是夹在起始标签和结束标签间的代码,上面的代码可以写成:
  • <Button>
      <sys:String>OK</sys:String>
    </Button>
    

    各类内容模型详解

    把符合某类内容模型的UI元素称为一个族,每个族用它们共同基类来命名

    ContentControl族

    本族元素的特点如下:

  • 均派生自ContentControl类。
  • 它们都是控件(Control)。
  • 内容属性的名称为Content。
  • 只能由单一元素充当其内容
  • “只能由单一元素充当其内容”以Button为例,Buton只能接受一个元素作为它的Content,需要一个带图标、文字的Button时要先用一个可以包含多个元素的布局控件把图片和文字包装起来,再把这个布局控件作为Buton的内容(控件的内容也可以是控件)。

    ContentControl族包含的控件:Button、ButtonBase、CheckBox、ComboBoxItem、ContentControl、Frame、GridViewColumnHeader、GropItem、Label、ListBoxItem、ListViewItem、NavigationWindow、RadioButton、RepeatButton、ScrollViewer、StatusBarItem、ToggleButton、ToolTip、UserControl、Window。

    HeaderedContentControl族

    本族元素的特点如下:

  • 它们都派生自HeaderedContentControl类,HeaderedContentControl是ContentControl类的派生类。
  • 它们都是控件,用于显示带标题的数据。
  • 除了用于显示主体内容的区域外,控件还具有一个显示标题(Header)的区域。
  • 内容属性为Content和Header。
  • 无论是Content 还是Header都只能容纳一个元素作为其内容。
  • HeaderedContentControl族包含的控件:Expender、GroupBox、HeaderedContentControl、TabItem。
    下面演示一个以图标为Header、以文字为主体内容的GroupBox,代码如下:

    <!--GroupBox.Content标签可以省略-->
    <GroupBox Margin="10" BorderBrush="SlateBlue">
        <GroupBox.Header>
            <Image Source="img.jpg" Width="20" Height="20"></Image>
        </GroupBox.Header>
        <GroupBox.Content>
            <TextBlock TextWrapping="WrapWithOverflow" Margin="10" Text="测试内容"></TextBlock>
        </GroupBox.Content>
    </GroupBox>
    

    ItemsControl族

    本族元素的特点如下:

  • 均派生自ItemsControl类。
  • 它们都是控件,用于显示列表化的数据。
  • 内容属性为Items或ItemsSource。
  • 每种ItemsControl都对应有自己的条目容器(Item Container)
  • 本族的包含的控件:Menue、MenuBase、ContextMenu、ComboBox、ItemsControl、ListBox、ListViewe、TabControl、TreeView、Selector、StatusBar,对应的Item Container如下:

    ItemsControl名称 对应的Item container

    下面列出属于ItemsControl族元素和其对应的Item Container有ComboBox——ComboBoxItem,ContextMenu——MenuItem,ListBox——ListBoxItem,ListView——ListViewItem,Menu——MenuItem,StatusBar——StatusBarItem,TabControl——TabItem,TreeView——TreeViewItem.

    ListBox:在XAML中添加数据

    ListBox除了可以显示中规中矩的字符串条目还能够显示更多的元素,如CheckBox、RadioButton、TextBox等,例如下面这段代码:

    <!--ListBoxItem标签已省略-->
    <ListBox x:Name="listbox" Margin="5">
        <CheckBox x:Name="chb1" Content="选择1"/>
        <CheckBox x:Name="chb2" Content="选择2"/>
        <CheckBox x:Name="chb3" Content="选择3"/>
        <CheckBox x:Name="chb4" Content="选择4"/>
        <Button x:Name="btn1" Content="按钮1"/>
        <Button x:Name="btn2" Content="按钮2"/>
        <Button x:Name="btn3" Content="按钮3"/>
    </ListBox>
    

    表面看上去是ListBox直接包含了一些CheckBox和Buton,实际上这些CheckBox和Buton的父级容器是ListBoxItem。为按钮添加如下的事件代码查看父容器:

    private void btn1_Click(object sender, RoutedEventArgs e)
        Button btn = (sender) as Button;
        DependencyObject level1 = VisualTreeHelper.GetParent(btn);
        DependencyObject level2 = VisualTreeHelper.GetParent(level1);
        DependencyObject level3 = VisualTreeHelper.GetParent(level2);
        StringBuilder sbr = new StringBuilder();
        sbr.AppendLine("level1:"+level1.GetType().ToString());
        sbr.AppendLine("level2:" + level2.GetType().ToString());
        sbr.AppendLine("level3:" + level3.GetType().ToString());           
        MessageBox.Show(sbr.ToString());    
    

    ListBox:在代码中添加数据

    除非列表里的元素自始至终都是固定的才使用这种直接把UI元素作为ItemsControl内容的方法,如日期等。大多数情况下,UI上的列表会用于显示动态的后台数据,此时交给ltemsControl的就是程序逻辑中的数据了。在代码中为ListBox添加数据的代码如下:

    <ListBox x:Name="listbox" Margin="5"/> </Grid>
    List<Employee> empList = new List<Employee>()
        new Employee(){Id = 1, Name ="Tim", Age = 30},
        new Employee(){Id = 2, Name="Tom",Age=26},
        new Employee(){Id = 3,Name="Guo",Age=26},
        new Employee(){Id = 4,Name="Yan",Age=25},
        new Employee(){Id = 5,Name="Owen",Age=30},
        new Employee(){Id=6,Name="Victor",Age=30 }
    this.listbox.DisplayMemberPath = "Name"; 
    this.listbox.SelectedValuePath = "Id"; 
    this.listbox.ItemsSource= empList;
    //Employee类
    public class Employee
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    
  • DisplayMemberPath:这个属性告诉ListBox显示每条数据的哪个属性,ListBox会去调用这个属性值的ToString()方法,把得到的字符串放入一个TextBlock(最简单的文本控件),然后再按前面说的办法把TextBlock包装进一个ListBoxItem里。
  • SelectedValuePath:这个属性将与其SelectedValue属性配合使用,当调用SelectedValue属性时ListBox先找到选中的Item所对应的数据对象,然后把SelectedValuePath的值当作数据对象的属性名称并把这个属性的值取出来
  • DisplayMemberPath 和Selected ValuePath 是两个相当简化的属性。DisplayMemberPath只能显示简单的字符串,想用更加复杂的形式显示数据需要使用DataTemplate;SelectedValuePath也只能返回单一的值,如果想进行一些复杂的操作可直接使用ListBox的SelectedItem和SelectedItems属性,这两个属性返回的是数据集合中的对象

    HeaderedItemsControl族

    本族控件*除了具有ItemsControl的特性外,还具显示标题的能力**。本族元素的特点如下:

  • 均派生自HeaderedItemsControl类。
  • 它们都是控件,用于显示列表化的数据,同时可以显示一个标题。
  • 内容属性为Items、ItemsSource和Header。
  • 本族控件只有3个:MenuItem、TreeViewItem、ToolBar

    Decorator族

    本族中的元素是在UI上起装饰效果的,本族元素的特点如下:

  • 均派生自Decorator类。
  • 起UI装饰作用。
  • 内容属性为Child。
  • 只能由单一元素充当内容。
  • 本族元素有:ButtonChrome、ClassicBorderDecorator、ListBoxChrome、SystemDropShadowChrome、Border、InkPresenter、BulletDecorator、Viewbox、AdornerDecorator。

    可以使用Border元素为一些组织在一起的内容加个边框,使用ViewBox元素让组织在一起的内容能够自由缩放

    TextBlock和TextBox

    两个控件最主要的功能是显示文本:

  • TextBlock只能显示文本,不能编辑,又称静态文本,可以使用丰富的印刷级的格式控制标记显示专业的排版效果。由于需要操纵格式它的内容属性是Inlines(印刷中的“行”),同时也保留一个名为Text的属性(当简单地显示一个字符串时可以使用)
  • TextBox则允许用户编辑其中的内容,由于不需要太多的格式显示它的内容是简单的字符串,内容属性为Text
  • TextBlock属于比较底层的控件,因此它的性能要比Label好一些。如果需求只是纯文本的显示,并且不提供Access key的支持,那么TextBlock是个不错的选择。

    Shape族元素

    Shape族元素(只是简单的视觉元素,不是控件)是专门用来在UI上绘制图形的一类元素,本族元素的特点如下:

  • 均派生自Shape类。
  • 用于2D图形绘制。
  • 无内容属性
  • 使用Fill属性设置填充,使用Stroke属性设置边线
  • 注:该族控件一般较少使用,用于自定义控件外观。

    Panel族元素

    所有用于UI布局的元素都属于这一族,本族元素的特点如下:

  • 均派生自Panel抽象类
  • 主要功能是控制UI布局。
  • 内容属性为Children
  • 内容可以是多个元素,Panel元素将控制它们的布局。
  • ItemsControl和Panel元素内容都可以是多个元素,但ItemsControl强调以列表的形式来展现数据而Panel则强调对包含的元素进行布局,所以ItemsControl的内容属性是Items和ItemsSource而Panel的内容属性名为Children。

    本族元素有:Canvas、DockPanel、Grid、TabPanel、ToolBarOverflowPanel、StackPanel、ToolBarPanel、UniformGrid、VirtualizingPanel|VirtualizingStackPanel、WrapPanel。

    UI布局(Layout)

    WPF的布局是依靠各种布局元素实现的。

    WPF中的布局元素有如下几个:

  • Grid:网格,可以自定义行和列并通过行列的数量、行高和列宽来调整控件的布局,近似于HTML中的Table。
  • StackPanel:栈式面板,可将包含的元素在竖直或水平方向上排成一条直线,移除元素时后面的元素会自动向前移动填充空缺
  • Canvas:画布,内部元素可以使用以像素为单位的绝对坐标进行定位,类似于Windows Form编程的布局方式。
  • DockPanel:泊靠式面板,内部元素可以选择泊靠方向,类似于在Windows Form编程中设置控件的Dock属性
  • WrapPanel:自动折行面板,内部元素在排满一行后能够自动折行,类似于HTML中的流式布局。
  • Grid元素会以网格的形式对内容元素们(即它的Children)进行布局。
    Grid的特点如下:

  • 可以定义任意数量的行和列,非常灵活。
  • 行的高度和列的宽度可以使用绝对数值、相对比例或自动调整的方式进行精确设定,并可设置最大和最小值。
  • 内部元素可以设置自己的所在的行和列,还可以设置自己纵向跨几行、横向跨几列。
  • 可以设置Children元素的对齐方向。
  • Grid适用的场合有:

  • UI布局的大框架设计
  • 大量UI元素需要成行或者成列对齐的情况。
  • UI整体尺寸改变时,元素需要保持固有的高度和宽度比例
  • UI后期可能有较大变更或扩展。
  • 定义Grid的行与列

    Grid类具有ColumnDefinitions和RowDefinitions两个属性,分别是ColumnDefinition和RowDefinition的集合,表示Grid定义了多少列、多少行。
    XAML代码如下:

    <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> </Grid>

    C#代码如下:

    //添加列
    grid.ColumnDefinitions.Add(new ColumnDefinition());
    grid.ColumnDefinitions.Add(new ColumnDefinition());
    //添加行
    grid.RowDefinitions.Add(new RowDefinition());
    grid.RowDefinitions.Add(new RowDefinition());
    

    行高和列宽的单位

    计算机图形设计的标准单位是像素(Pixel),所以Grid的宽度和高度单位就是像素。此外,Grid还接受英寸(Inch)、厘米(Centimeter)和点(Point),如下表所示:

    对于Grid的行高和列宽,可以设置三类值:

  • 绝对值:double数值加单位后缀(如上例),一经设定就不会再改变,又称固定值,适用于当控件的宽度和高度不需要改变或者使用空行、空列作为控件间隔时
  • 比例值:double数值后加一个星号(“*”),比例值的最终像素数=比例值的数值/所有比例值的数值和*未被占用空间的像素数,当改变容器的尺寸时使用比例值的行高会保持固有比例,行高和列宽的默认形式就是比例值,没有显式指定行高或列宽时默认值就是1*(1*又可以简写为*)
  • 自动值:字符串Auto,行高或列宽的实际值将由行列内控件的高度和宽度决定,控件会把行列“撑”到合适的宽度和高度,行列中没有控件时行高和列宽均为0
  • 为控件指定行和列遵循的规则

  • 行和列都是从0开始计数
  • 指定一个控件在某行,就为这个控件的标签添加Grid.Row=“行编号“这样一个Attribute,若行编号为0(即控件处于首行)则可省略这个Attribute
  • 指定一个控件在某列,就为此控件添加Grid.Column=”列编号”这样的Attribute,若列编号为0则Attribute可以者略不写
  • 控件需要跨多个行或列,请使用Grid.RowSpan=“行数“和Grid.ColumnSpan=“列数“两个Atribute
  • StackPanel

    StackPanel可以把内部元素在纵向或横向上紧凑排列、形成栈式布局,StackPanel适合的场合有:

  • 同类元素需要紧凑排列(如制作菜单或者列表)。
  • 移除其中的元素后能够自动补缺的布局或者动画。
  • StackPanel使用Orientation、HorizontalAlignment和VerticalAlignment这3个属性来控制内部元素的布局,如下所示:

    |属性名称|可取值|描述|
    |--|--|--|--|
    |Orientation|Horizontal
    Vertical|决定内部元素是横向累积还是纵向累积|
    |HorizontalAlignment|Left
    Center
    Right
    Stretch|决定内部元素水平方向上的对齐方式|
    |VerticalAlignment|Top
    Center
    Bottom
    Stretch|决定内部元素竖直方向上的对齐方式|

    Canvas

    Canvas译成中文就是“画布”,在Canvas里布局就像在画布上画控件一样。使用Canvas布局与在Windows Form窗体上布局基本上是一样的,只是WPF的控件没有Left和Top等属性,当控件被放置在Canvas里时就会被附加上Canvas.X和Canvas.Y属性

    Canvas适用的场合包括:

  • 一经设计基本上不会再有改动的小型布局(如图标)。
  • 艺术性比较强的布局。
  • 需要大量使用横纵坐标进行绝对点定位的布局。
  • <Canvas>
        <TextBlock Text="用户名:" Canvas.Left="12" Canvas.Top="12"/>
        <TextBox Height="23" Width="200" BorderBrush="Black" Canvas.Left="66" Canvas.Top="19"/>
        <TextBlock Text="密码:" Canvas.Left="12" Canvas.Top="40.72" Height="16" Width="36"/>
        <TextBox Height="23" Width="200" BorderBrush="Black" Canvas.Left="66" Canvas.Top="38"/>
        <Button Content="确定" Width="80" Height="22" Canvas.Left="100" Canvas.Top="67"/>
        <Button Content="清除" Width="80" Height="22" Canvas.Left="186" Canvas.Top="67"/>
    </Canvas>
    

    除非你确定这个窗口的布局以后不会改变而且窗体尺寸固定,不然还是用Grid进行布局弹性会更好。

    DockPanel

    DockPanel内的元素会被附加上DockPanel.Dock这个属性,这个属性的数据类型为Dock枚举(可取Left、Top、Right和Bottom四个值)。根据Dock属性值,DockPanel内的元素会向指定方向累积、切分DockPanel内部的剩余可用空间。

    DockPanel还有一个重要属性——bool类型的LastChildFill(默认值是True),当LastChildFill属性的值为True时,DockPanel内最后一个元素的DockPanel.Dock属性值会被忽略,这个元素会把 DockPanel内部所有剩余空间充满

    实际使用如下:

    <DockPanel> <TextBox DockPanel.Dock="Top" Height="25" BorderBrush="Black" /> <TextBox DockPanel.Dock="Left" Width="150" BorderBrush="Black"/> <TextBox BorderBrush="Black"/> </DockPanel> </Grid>

    WrapPanel

    WrapPanel内部采用的是流式布局,使用Orientation属性来控制流延伸的方向,使用HorizontalAlignment和VerticalAlignment 两个属性控制内部控件的对齐。在流延伸的方向上,WrapPanel会排列尽可能多的控件,排不下的控件将会新起一行或一列继续排列。
    实际使用如下:

    <WrapPanel>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
        <Button Width="50" Height="50" Content="OK"/>
    </WrapPanel>
    

    形而上者谓之道,形而下者谓之器。WPF的内部机理可以说是WPF的“道”,动手实践写程序可以说是WPF的“器”。

    WPF控件和布局
    控件与布局(WPF)
    WPF学习二:TextBlock和Label的区别