2024年8月

设计模式是前辈们经过实践验证总结的解决方案,帮助我们构建出更具可维护性、可扩展性和可读性的代码。当然,在面试的过程中,也会或多或少的被问到。那么今天,我们就来看一道设计模式中的常见面试问题:JDK 中都用了哪些设计模式?

我按照大家比较熟悉且好理解的方式,把 JDK 中使用的设计模式总结了一下,如下图所示:
image.png
那么,接下来我们一个个来看。

1.单例模式

单例模式保证一个类只有一个实例,并提供一个全局访问点。

Runtime 类使用了单例模式,如下源码可知:

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();
    private static Version version;
    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class {@code Runtime} are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the {@code Runtime} object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    // 省略其他源码
}

从以上源码可以看出,Runtime 使用的饿汉方式实现了单例模式。

2.工厂模式

工厂模式提供了一种将对象创建的过程封装在一个单独的类中的方法,这个类就是工厂类。

线程池中的所有线程的创建都是通过工厂创建的,使用的就是工厂模式,具体源码如下:

3.代理模式

代理模式是一种为其他对象提供一种代理以控制对这个对象的访问的设计模式。代理对象在客户端和目标对象之间起到中介的作用,并且可以去掉客户不能看到的内容和服务或者添加客户需要的额外服务。

JDK 内置了动态代理的功能,动态代理是代理模式的一种实现,它是由 java.lang.reflect.Proxy 类提供的。

Proxy 使用 Demo 如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 1.接口
interface Subject {
    void doSomething();
}

// 2.目标类(被代理类)
class RealSubject implements Subject {
    @Override
    public void doSomething() {
        System.out.println("RealSubject is doing something");
    }
}

// 3.动态代理类
class DynamicProxyHandler implements InvocationHandler {
    private Object target;
    DynamicProxyHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before calling method");
        Object result = method.invoke(target, args);
        System.out.println("After calling method");
        return result;
    }
}

public class JDKProxyDemo {
    public static void main(String[] args) {
        // 创建真实对象
        Subject realSubject = new RealSubject();
        // 创建动态代理处理器
        InvocationHandler handler = new DynamicProxyHandler(realSubject);
        // 创建代理对象
        Subject proxySubject = (Subject) Proxy.newProxyInstance(
            realSubject.getClass().getClassLoader(),
            realSubject.getClass().getInterfaces(),
            handler);
        // 调用代理对象的方法
        proxySubject.doSomething();
    }
}

4.迭代器模式

迭代器模式能够
提供一种简单的方法来遍历容器中的每个元素
。通过迭代器,用户可以轻松地访问容器中所有的元素,简化了编程过程。

Iterable 就是标准的迭代器模式,Collection 就是 Iterator 的子类,它的使用代码如下:

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorDemo {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Orange");

        // 获取迭代器
        Iterator<String> iterator = list.iterator();

        // 使用迭代器遍历集合
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.println("Fruit: " + fruit);
        }
    }
}

5.模版方法模式

模板方法模式(Template Method Pattern)定义了一个操作中的算法骨架,将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

在 AQS(AbstractQueuedSynchronizer) 中,acquire 方法和 release 方法使用了模板方法模式。

这些方法之所以被认为是模板方法模式,是因为它们定义了一个操作的基本框架或流程,但其中的某些关键步骤被设计为抽象方法,留给子类去具体实现。

以 acquire 方法为例,它大致的流程包括尝试获取资源、如果获取失败则将当前线程加入等待队列、阻塞线程等步骤。但是具体如何判断能否获取资源(通过调用 tryAcquire 方法),以及在获取失败后的一些处理细节,是由子类去实现的,具体源码如下:

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

例如,基于 AQS 实现的 ReentrantLock 中就重写了 tryAcquire 方法,实现源码如下:

6.装饰器模式

装饰器模式是在不修改原对象的基础上,动态地给对象添加额外功能的设计模式。

BufferedInputStream 就是典型装饰器模式,当使用普通的 InputStream 读取数据时,每次可能都会进行实际的 I/O 操作,而 BufferedInputStream 会先将一部分数据读入缓冲区,后续的读取操作可以直接从缓冲区获取,减少了实际的 I/O 次数。

例如以下代码:

InputStream inputStream = new FileInputStream("file.txt");
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);

BufferedInputStream 并没有改变 FileInputStream 的基本结构和接口,只是为其
添加了缓冲的特性

7.策略模式

策略模式定义了一系列可互换的算法,并将每一个算法封装起来,使它们可以互相替换。

Comparator 是策略模式的一个典型例子,Comparator 接口定义了一个比较两个对象的方法 compare(T o1, T o2)。这个接口允许用户定义不同的比较策略,使得我们可以灵活地改变排序或比较逻辑。

例如以下示例代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class StrategyPatternExample {
    static class Person {
        private String name;
        private int age;
        // 忽略 Setter、Getter 等方法
    }
    // 按照年龄升序排列
    static class AgeComparator implements Comparator<Person> {
        @Override
        public int compare(Person p1, Person p2) {
            return Integer.compare(p1.getAge(), p2.getAge());
        }
    }
    // 按照姓名降序排列
    static class NameDescendingComparator implements Comparator<Person> {
        @Override
        public int compare(Person p1, Person p2) {
            return p2.getName().compareTo(p1.getName());
        }
    }
    public static void main(String[] args) {
        ArrayList<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        // 使用年龄升序的策略
        Collections.sort(people, new AgeComparator());

        // 使用姓名降序的策略
        Collections.sort(people, new NameDescendingComparator());
    }
}

8.建造者模式

建造者模式是一种创建型设计模式,用于通过一系列的步骤来创建复杂的对象。它将对象的构建过程与其表示相分离,使得同样的构建过程可以创建不同的表示。

在 JDK 中,使用建造者模式的常见例子是 StringBuilder 和 StringBuffer 类。

虽然这两个类本身不是传统意义上的建造者模式实现(因为建造者模式通常用于构建不同的表示或者不同部分的同一个对象),它们提供了一种链式调用的方式来构建和修改字符串,这在某种程度上体现了建造者模式的思想。

例如以下代码:

public class StringBuilderDemo {  
    public static void main(String[] args) {  
        // 使用 StringBuilder 构建和修改字符串  
        StringBuilder builder = new StringBuilder();  
        builder.append("Hello")  
        .append(", ")  
        .append("world")  
        .append("!")  
        .insert(7, "beautiful ")  
        .deleteCharAt(13);  

        // 输出构建和修改后的字符串  
        System.out.println(builder.toString());  
        // 输出: Hello, beautiful world!  
    }  
}

StringBuilder 通过链式调用 append、insert 和 deleteCharAt 方法来逐步构建和修改字符串。这种方式使得构建和修改字符串的过程更加流畅和易于阅读。

课后思考

Spring 中都用了哪些设计模式?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

1.简介

WireShark的强大之处就在于不用你再做任何配置就可以抓取http或者https的包。今天宏哥主要是讲解和分享如何使用WireShark抓包。

2.运行Wireshark

安装好 Wireshark 以后,就可以运行它来捕获数据包了。方法如下:

1.在 Windows 的“开始”菜单中,单击 Wireshark 菜单,如下图所示:

2.点击启动 Wireshark,如下图所示:

该图为 Wireshark 的主界面,界面中显示了当前可使用的接口,例如,本地连接* 5、本地连接* 6 等。要想捕获数据包,必须选择一个接口,表示捕获该接口上的数据包。

3.捕获设置

小伙伴或者童鞋们可以使用以下任意一种方式启动捕获。

2.1第一种方法

在上图中,选择捕获“本地连接* 5”接口上的数据包。选择“本地连接* 5”选项,然后单击左上角的“开始捕获分组”按钮,将进行捕获网络数据,如下图所示:

图中没有任何信息,表示没有捕获到任何数据包。这是因为目前“本地连接* 5”上没有任何数据。此时wireshark处于抓包状态中。只有在本地计算机上进行一些操作后才会产生一些数据,如浏览网站。如下图所示:

2.2第二种方法

1.选择菜单栏上捕获(Capture) ->选项(Option),弹出捕获选项,如下图所示:

当然也可以点击【捕获选项】的图标一步到位,如下图所示:

2.在捕获选项中:勾选WLAN网卡或者其他网卡(这里需要根据各自电脑网卡使用情况选择,简单的办法可以看使用的IP对应的网卡)。点击开始(Start)。启动抓包。如下图所示:

3.点击开始后,wireshark处于抓包状态中。由于本地计算机在浏览网站等一系列操作时,“以太网”接口的数据将会被 Wireshark 捕获到。捕获的数据包如图所示。图中方框中显示了成功捕获到“以太网”接口上的数据包。如下图所示:

4.Wireshark 将一直捕获“以太网”上的数据。如果不需要再捕获,可以单击左上角的“停止捕获分组”按钮,停止捕获。如下图所示:

2.3第三种方法

1.选中一个网卡,右键点击“Start capture”开始抓包,如下图所示:

3.Wireshark实战抓包

首次捕获都要经历网卡选取、选项配置和启动捕获三个过程,启动的方式不同并不会带来本质区别,都是殊途同归。

宏哥这里以本地计算ping一下百度的域名为例给小伙伴或童鞋们讲解和分享一下抓包过程。

1.通过前边的步骤我们知道Wireshark已经处于抓取“以太网”接口的抓包状态,宏哥这里就直接ping一下百度域名,如下图所示:

2.我们查看Wireshark是否抓取到宏哥ping百度域名的包,通过对照我们可以发现抓取到了,如下图所示:

4.过滤栏设置

我们发现在众多抓取的包查找ping百度的还是特别麻烦的,因为可以通过在过滤栏设置过滤条件进行数据包列表过滤,以免抓取无用包影响查看,这里就以ping baidu.com为例,只过滤百度的ip,设置如下:

ip.addr == 110.242.68.3 and icmp

以上过滤条件说明:表示只显示ICPM协议且源主机IP或者目的主机IP为39.156.69.79的数据包。注意:
协议名称icmp要小写
。这里宏哥简单的介绍一下,后边会进行详细地介绍和讲解。

经过过滤后,我们发现查找到非常简单,因为宏哥ping了两次,因此总共有8条数据。如下图所示:

到此,关于Wireshark抓包流程就大功告成。你学废了吗???Wireshark抓包完成,就这么简单。关于wireshark过滤条件和如何查看数据包中的详细内容在后面介绍。

5.小结

好了,到此宏哥就将使用WireShark抓包讲解和分享完了,是不是很简单了。今天时间也不早了,就到这里!感谢您耐心的阅读~~

第3章:布局

本章目标

  • 理解布局的原则
  • 理解布局的过程
  • 理解布局的容器
  • 掌握各类布局容器的运用

理解 WPF 中的布局

WPF 布局原则

​ WPF 窗口只能包含单个元素。为在WPF 窗口中放置多个元素并创建更贴近实用的用户男面,需要在窗口上放置一个容器,然后在这个容器中添加其他元素。造成这一限制的原因是 Window 类继承自 ContentControl 类,在后续章节中将进一步分析ContentControl类。

布局过程

​ WPF 布局包括两个阶段:测量(measure)阶段和排列(arange)阶段。在测量阶段,容器遍历所有子元素,并询问子元素它们所期望的尺寸。在排列阶段,容器在合适的位置放置子元素。

​ 当然,元素未必总能得到最合适的尺寸—有时容器没有足够大的空间以适应所含的元素。在这种情况下,容器为了适应可视化区域的尺寸,就必须剪裁不能满足要求的元素。在后面可以看到,通常可通过设置最小窗口尺寸来避免这种情况。

注意:

布局容器不能提供任何滚动支持.相反,滚动是由特定的内容控件ScrollViewer—一提供的,ScrollViewer 控件几乎可用于任何地方。

布局容器

​ 所有 WPF 布局容器都是派生自 System.Windows.Controls.Panel 抽象类的面板(见下图)。Panel 类添加了少量成员,包括三个公有属性,下表列出了这三个公有属性的详情。

名称 说明
Background 该属性是用于为面板背景着色的画刷。如果想接收鼠标事件,就必须将该属性设置为非空值(如果想接收鼠标事件,又不希望显示固定颜色的背景,那么只需要将背景色设置为透明即可)
Children 该属性是在面板中存储的条目集合。这是第一级条目—换句话说,这些条目自身也可以包含更多的条目。
IsItemsHost 该属性是一个布尔值,如果面板用于显示与 ItemsControl 控件关联的项(例如,TreeView 控件中的节点或列表框中的列表项),该属性值为 true。在大多数情况下,甚至不需要知道列表控件使用后台面板来管理它所包含的条目的布局。但如果希望创建自定义的列表,以不同方式放置子元素(例如,以平铺方式显示图像的 ListBox控件),该细节就变得很重要了。

​ 就Panel基类本身而言没有什么特别的,但它是其他更多特类的起点。WPF提供了大量可用于安排布局的继承自Panel的类,下表中列出了其中几个最基本的类。与所有 WPF控件和大多数可视化元素一样,这些类位于 System. Windows.Controls 名称空间中。

名称 说明
StackPanel 在水平或垂直的堆栈中放置元素。这个布局容器通常用于更大、更复杂窗口中的一些小区域.
WrapPanel 在一系列可换行的行中放置元素。在水平方向上,WrapPanel 面板从左向右放置条目,然后在随后的行中放置元素。在垂直方向上,WrapPanel 面板在自上而下的列中放置元素,并使用附加的列放置剩余的条目.
DockPanel 根据容器的整个边界调整元素
Grid 根据不可见的表格在行和列中排列元素,这是最灵活、最常用的容器之一.
UnitformGrid 在不可见但是强制所有单元格具有相同尺寸的表中放置元素,这个布局容器不常用.
Canvas 使用固定坐标绝对定位元素。这个布局容器与传统 Windows 窗体应用程序最相似,但没有提供锚定或停靠功能。因此,对于尺寸可变的窗口,该布局容器不是合适的选择。如果选择的话,需要另外做一些工作。

StackPanel 面板

​ StackPanel 面板是最简单的布局容器之一。该面板简单地在单行或单列中以堆栈形式放置其子元素。例如,下面的代码段中,窗口包含4个按钮。

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <StackPanel>
        <Label>A Button Stack</Label>
        <Button>button1</Button>
        <Button>button2</Button>
        <Button>button3</Button>
        <Button>button4</Button>
    </StackPanel>
</Window>

​ 认情况下,StackPane! 面板按自上而下的顺序推列元素,使每个元素的高度适合它的内≥。在这个示例中,这底味老标签和技钮的大小刚好足够适应它们内部包含的文本。所有元素都被拉伸到 SatckPane! 面板的整个宽度,这也是窗口的宽度。如果加宽窗口,StackPanel 面板也会变宽,并且按钮也会拉伸自身以适应变化。

​ 通过设置 Oricntation 属性,StackPanel 面板也可用于水平排列元素:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <StackPanel Orientation="Horizontal">
        <Label>A Button Stack</Label>
        <Button MinWidth="100">button1</Button>
        <Button MinWidth="100">button2</Button>
        <Button MinWidth="100">button3</Button>
        <Button MinWidth="100">button4</Button>
    </StackPanel>
</Window>

布局属性

​ 尽管布局由容器决定,但子元素仍有一定的决定权。实际上,布局面板支持一小组布局属性,以便与子元素结合使用,下表中列出了这些布局属性。

名称 说明
HorizontalAlignment 前水平方向上新额外的空闲时,该属性决定了子元素在布局容器中如何定位。可选用 Center、Left、 Right 或 Stretch 等属性值。
VerticalAlignment 当垂直方向上有额外的空闲时,该属性决定了子元素在布局容中如何定位。可选用 Center、Top、Bottom 或 Stretch 等属性值。
Margin 该属性用于在元素的周围添加一定的空间.Margin 属性是 System.Windows.Thickness结构的一个实例,该结构具有分别用于为顶部、底部、左边和右边添加空间的独立组件。
MinWidth 和 MinHeight 这两个属性用于设置元素的最小尺寸。如果一个元素对于其他布局容器来说太大,该元素将被剪裁以适合容器。
MaxWidth 和 MaxHeight 这两个属性用于设置元素的最大尺寸。如果有更多可以使用的空间,那么在扩展子元素时就不会超出这一限制,即使将 HorizontalAlignment 和 VerticalAlignment 属性设置为 Stretch 也同样如此。
Width 和 Height 这两个属性用于显式地设置元素的尺寸。这一设置会重写为 HorizontalAlignment 和VerticalAlignment 属性设置的 Stretch 值。但不能超出MinWidth、MinHeight、MaxWidth 和 MaxHeight 属性设置的范。

对齐方式

​ 通常,对于 Label 控件,HorizontalAligament 属性的值默认为 Lef:对于 Button 控件,HorzontalAlignment 属性的售默认为 Streteh。这也是为什么每个技钮的宽度被调整为控列的宽度的原因所在。但可以改变这些细节:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <StackPanel>
        <Label HorizontalAlignment="Center">A Button Stack</Label>
        <Button HorizontalAlignment="Left">button1</Button>
        <Button HorizontalAlignment="Right">button2</Button>
        <Button>button3</Button>
        <Button>button4</Button>
    </StackPanel>
</Window>

​ 现在前两个按钮的尺寸是它们应当具有的最小尺寸,并进行了对齐,而底部两个按钮被拉伸至整个 SiackPanel面板的宽度。如果改变窗口的尺寸,就会发现标签保持在中间位置,而前两个按钮分别被粘贴到两边。

注意:

​ SiackPanel面板也有自己的HorizontalAlignment 和 VerticalAlignment 属性。这两个属性职认都被设置为 Stretch,所以 StackPanel 面板完全充满它的容器、在这个示例中,这意味着stackPanel西板充滿整个窗口。如果使用不同设置,StackPanel 面板的尺寸将足够宽以容纳最宽的控件。

边距

​ 在SackPane!示例中,在当前情况下存在一个明显的问题。设计良好的窗口不只是包含元素—还应当在元素之间包貪一定的额外空间。为了添加额外的空间并使StaokPanel 面板示例中的按钮不那么紧密,可为控件设置边距。

​ 当设置边距时,可为所有边设置相同的宽度,如下所示:

<Button Margin="5">button3</Button>

​ 相应地,也可为控件的每个边以左、上、右、下的顺序设置不同的边距:

<Button Margin="5,10,5,10">Button 3</Button>

​ 在代码中,使用 Thickness 结构来设置边距:

btn. Margin = new Thickness (5) ;

​ 为得到正确的控件边距,需要采用一些艺术手歐,因內需要考慮相邻控件边距改置的相互影响。例如,如果两个技钮堆在一起,位于最高处的技钮的底部边距设置为5,而下面技钮的顶部边距也设置为5,那么在这两个按钮之间就有10个单位的空间。

​ 理想情况是,能尽可能始终如一地保持不同的边距设置,避免为不同的边设置不同的值。
例如,在StackPanel示例中,为按钮和面板本身使用相同的边距是比较合适的,如下所示:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <StackPanel Margin="5">
        
        <Label Margin="3" HorizontalAlignment="Center">A Button Stack</Label>
        <Button Margin="3" HorizontalAlignment="Left">button1</Button>
        <Button Margin="3" HorizontalAlignment="Right">button2</Button>
        <Button Margin="3">button3</Button>
        <Button Margin="3">button4</Button>
        
    </StackPanel>
</Window>

尺寸设置

​ 最后,每个元素都提供了 Height 和 Width 属性,用于显式地指定元素大小。但这种设置一般不是一个好主意。相反,如有必要,应当使用最大尺寸和最小尺寸属性,将控件限制在正确范围内。

提示:

在WPF中显式地设置尺寸之前一定要三思。在良好的布局设计中,不必显式地设置尺寸.
如果确实添加了尺寸信息,那就冒险创建了一种更不稳定的布局,这种布局不能适应变化(例如,不能适应不同的语言和不同的窗口尺寸),而且可能剪裁您的内容.

​ 例如,您可能决定拉伸 StackPanel 容器中的按钮,使其适合 StackPanel,但其宽度不能超过200单位,也不能小于100单位(默认情况下,最初按钮的最小宽度是75单位)。

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <StackPanel Margin="5">
        
        <Label Margin="3" HorizontalAlignment="Center">A Button Stack</Label>
        <Button Margin="3" MaxWidth="200" MinWidth="100">button1</Button>
        <Button Margin="3" MaxWidth="200" MinWidth="100">button2</Button>
        <Button Margin="3" MaxWidth="200" MinWidth="100">button3</Button>
        <Button Margin="3">button4</Button>
        
    </StackPanel>
</Window>

​ 当 StackPanel 调整按钮的尺寸时,需要考虑以下几部分信息:

  • 最小尺寸: 每个按钮的尺寸始终不能小于最小尺寸。
  • 最大尺寸: 每个按钮的尺寸始终不能超过最大尺寸(除非执行错误操作,使最大尺寸比最小尺寸还小)
  • 内容: 如果按钮中的内容需要更大的宽度,StackPanel 容器会尝试扩展技钮(可以通过检查 DesiredSized 属性确定所需的按钮大小,该属性返回最小宽度或内容的宽度,返回两者中较大的那个)。
  • 容器尺寸: 如果最小宽度大于 StackPanel 面板的宽度,按钮的一部分将被剪裁掉。否则,不允许按钮比 StackPanel 面板更宽,即使不能适合按钮表面的所有文本也同样如此。
  • 水平对齐方式: 因为默认情况下按钮的 HorizontalAlignment 属性值设置为Stretch,所以 StackPanel 面板将尝试放大按钮以占满 StackPanel 面板的整个宽度。

Border 控件

​ Border控件不是布局面板,而是非常便于使用的元素,经常与布局面板一起使用。所以,在继续介绍其他布局面板之前,现在先介绍一下 Border 控件是有意义的。

​ Border 类非常简单。它只能包含一段嵌套内容(通常是布局面板),并为其添加背景或在其周围添加边框。为了深入地理解Border 控件,只需要掌握下表中列出的属性就可以了。

名称 说明
Barckground 使用Brush 对象设置边框中所有内容后面的背景。可使用固定颜色背景,也可使用其他更特殊的背景.
BorderBush和BorderThickness 使用Brush 对象设置位于Border 对象边缘的边框的颜色,并设置边框的宽度。为显示边框,必须设置这两个属性.
CornerRadius 该属性可使边框具有雅致的圆角。ComerRadius 的值越大,圆角效果就越明显.
Padding 该属性在边框和内部的内容之间添加空间(与此相对,Margin 属性在边框之外添加空间).

​ 下面是一个具有轻微圆角效果的简单边框,该边框位于一组按钮的周围,这组按钮包含在一个StackPanel 面板中:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Border Margin="5" Padding="5" Background="LightYellow"
         BorderBrush="SteelBlue" BorderThickness="3,3,3,5" CornerRadius="3"
         VerticalAlignment="Top">

        <StackPanel >
            <Button Margin="3">button1</Button>
            <Button Margin="3">button2</Button>
            <Button Margin="3">button3</Button>
        </StackPanel>

    </Border>
</Window>

WrapPanel 和 DockPanel 面板

WrapPanel 面板

​ WrapPanel 面板在可能的空间中,以一次一行或一列的方式布置控件。默认情况下,WrapPanel.Orientation 属性设置为 Horizontal;控件从左向右进行排列,再在下一行中排列。但可将 WrapPanel.Orientation 属性设置为 Vertical,从而在多个列中放置元素。

提示:

与StackPanel 面板类似,WrapPanel 面板实际上主要用来控制用户界面中一小部分的布局细节,并非用于控制整个窗口布局。例如,可能使用 WrapPanel面板以类似工具栏控件的方式将所有按钮保持在一起。

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <WrapPanel Margin="3">
        <Button VerticalAlignment="Top">Button1</Button>
        <Button MinHeight="60">Button2</Button>
        <Button VerticalAlignment="Bottom">Button3</Button>
        <Button>Button4</Button>
        <Button VerticalAlignment="Center">Button5</Button>
    </WrapPanel>

</Window>

注意:

WrapPanel 面板是唯一一个不能通过灵活使用Grid 面板代替的面板。

DockPanel 面板

​ DockPanel 面板是更有趣的布局选项。它沿着一条外边缘来拉伸所包含的控件。理解该面板最简便的方法是,考虑一下位于许多 Windows 应用程序窗口顶部的工具栏。这些工具栏停靠到窗口顶部。与 StackPanel 面板类似,被停靠的元素选择它们布局的一个方面。例如,如果将一个按钮停靠在DockPanel 面板的顶部,该按钮会被拉伸至 DockPanel 面板的整个宽度,但根据内容和 MinHfeight 属性为其设置所需的高度。而如果将一个按钮停靠到容器左边,该技钮的高度将被拉伸以适应容器的高度,而其宽度可以根据需要自由增加。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <!--LastChildFill:最后一个子元素填充-->
    <DockPanel LastChildFill="True">
        <Button DockPanel.Dock="Top">上</Button>
        <Button DockPanel.Dock="Bottom">下</Button>
        <Button DockPanel.Dock="Left">左</Button>
        <Button DockPanel.Dock="Right">右</Button>
        <Button>中间</Button>
    </DockPanel>

</Window>

案例2:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <!--
    DockPanel.Dock:停靠方向
    LastChildFill:最后一个子元素填充
    HorizontalAlignment:水平对齐方式
    MinWidth:最小宽度
    -->
    <DockPanel LastChildFill="True">
        <Button DockPanel.Dock="Top">上-1</Button>
        <Button DockPanel.Dock="Top" HorizontalAlignment="Center" MinWidth="200">上-2</Button>
        <Button DockPanel.Dock="Top" HorizontalAlignment="Left" MinWidth="200">上-3</Button>
        <Button DockPanel.Dock="Bottom">下</Button>
        <Button DockPanel.Dock="Left">左</Button>
        <Button DockPanel.Dock="Right">右</Button>
        <Button>中间</Button>
    </DockPanel>

</Window>

嵌套布局容器

​ 很少单独使用 StackPanel、WrapPanel 和 DockPanel 面板。相反,它们通常用来设置一部分用户界面的布局。例如,可使用 DockPanel 面板在窗口的合适区域放置不同的 StackPanel 和WrapPane! 面板容器。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <!--
    LastChildFill:最后一个子元素填充
    DockPanel.Dock:停靠方向
    HorizontalAlignment:水平对齐方式
    Orientation:排列方向(水平/垂直)
    Margin:外边距
    Padding:内边距
    -->
    <DockPanel LastChildFill="True">
        <StackPanel DockPanel.Dock="Bottom" HorizontalAlignment="Right" Orientation="Horizontal">
            <Button Margin="10,10,2,10" Padding="3">OK</Button>
            <Button Margin="2,10,10,10" Padding="3">Cancel</Button>
        </StackPanel>
        <TextBox DockPanel.Dock="Top">这是一个文本框</TextBox>
    </DockPanel>

</Window>

提示:

​ 如果有一棵茂密的嵌套元素树,很可能看不到整个结构。Visual Studio 提供了一个方使的功能,用于显示一棵表示各个元素的树,并允许您通过逐步单击进入希望查看(或修改)的元素。
这一功能是指 Document Outline 窗口,可通过选择 View | Other Windows | Document Outline 菜单项来显示该窗口。

Grid 面板

​ Grid 面板是WPF 中功能最强大的布局容器。很多使用其他布局控件能完成的功能,用Grid面板也能实现。Grid 面板也是将窗口分割成(可使用其他面板进行管理的)更小区域的理想工具。实际上,由于 Grid 面板十分有用,因此在 Visual Studio 中为窗口添加新的XAML 文档时,会自动添加 Grid 标签作为顶级容器,并嵌套在 Window 根元素中。

​ Grid 面板将元素分隔到不可见的行列网格中。尽管可在一个单元格中放置多个元素(这时这些元素会相互重叠),但在每个单元格中只放置一个元素通常更合理。当然,在Grid 单元格中的元素本身也可能是另一个容器,该容器组织它所包含的一组控件。

提示:

​ 尽管Grid面板被设计成不可见的,但可将 Grid.ShowGridlines 属性设置汐 true,从而更清晰她观察Gird 面板。这一特性并不是真正试图美化窗口,反而是为了方便调试,设计该特性旨在帮助理解 Grid 面板如何将其自身分制成多个较小的区域。这一特性十分童要,因为通过该特性可准确控制 Grid 面板如何选择列宽和行高。

​ 需要两个步骤来创建基于 Crd 面板的布局。首先,选择希望使用的行和列的数厭。然后,为每个包含的元素指定恰当的行和列,从而在合适的位置放置元素。

​ Grid 面板通过使用对象填充 Grid.ColumnDefinitions和 Grid. Row Definiti ons 集合来创建网格和行。例如,如果确定需要两行和三列,可添加以下标签:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid ShowGridLines="True">

        <!--行-->
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!--列-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
    </Grid>

</Window>

​ 为在单元格中放置各个元素,需要使用 Row 和Columm 附加属性。这两个属性的值都是从0开始的索引数。例如,以下标记演示了如何创建Grid面板,并使用按钮填充Grid 面板的部分单元格。

​ 此处存在例外情况。如果不指定 Grid Row 属性,Grid 面板会假定该属性的值为0。对于Crid. Column 属性也是如此。因此,在 Grid 面板的第一个单元格中放置元素时可不指定这两个属性。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid ShowGridLines="True">

        <!--行-->
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!--列-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Button Grid.Row="0" Grid.Column="0">button1-1</Button>
        <Button Grid.Row="0" Grid.Column="1">button1-2</Button>
        <Button Grid.Row="1" Grid.Column="1">button2-2</Button>
        <Button Grid.Row="1" Grid.Column="2">button2-3</Button>
    </Grid>

</Window>

调整行和列

​ Grid 面板支持以下三种设置尺寸的方式:

  • 绝对设置尺寸方式: 使用设备无关单位准确地设置尺寸。这是最无用的策略,因为这种策略不够灵活,难以适应内容大小和容器大小的改变,而且难以处理本地化。

  • 自动设置尺寸方式: 每行和每列的尺寸刚好满足需要。这是最有用的尺寸设置方式。

  • 按比例设置尺寸方式。按比例将空间分割到一组行和列中。这是对所有行和列的标准设置。

​ 可通过将ColumnDefinition 对象的Width 属性或 RowDefinition 对象的 Height 属性设置为数值来确定尺寸设置方式。例如,下面的代码显示了如何设置100 设备无关单位的绝对宽度。

<ColumnDefinition Width="100"></ColumnDefinition>

为使用自动尺寸设置方式,可使用Auto值:

<ColumnDefinition Width="Auto"></ColumnDefinition>

最后,为了使用按比例尺寸设置方式,需要使用*号:

<ColumnDefinition Width="*"></ColumnDefinition>

​ 如果希望不均匀地分割剩余空间,可指定权重,权重必须放在星号之前。例如,如果有两行是按比例设置尺寸,并希望第一行的高度是第二行高度的一半,那么可以使用如下设置来分配剩余空间:

<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="2*"></RowDefinition>

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid ShowGridLines="True">

        <!--行-->
        <Grid.RowDefinitions>
            <RowDefinition  Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!--
        HorizontalAlignment:水平对齐方式
        Orientation:排列方向
        Margin:外边距
        -->
        <TextBox Margin="10" Grid.Row="0">这是一个文本框</TextBox>
        <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
            <Button Margin="10,10,2,10">OK</Button>
            <Button Margin="2,10,10,10">Cancel</Button>
        </StackPanel>
    </Grid>

</Window>

跨越行和列

​ 您已经看到如何使用 Row 和 Calum 附加属性在单元格中放置元素。还可以使用另外两个附加属性使元素跨越多个单元格,这两个附加属性是 RowSpan和ColurmSpan。这两个属性使用元素将会占有的行数和列数进行设置。

​ 例如,下面的按钮将占据第一行中的第一个和第二个单元格的所有空间:

<Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2">跨行</Button>

​ 下面的代码通过跨越两列和两行,拉伸按钮使其占据所有4个单元格:

<Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2">跨行并跨列</Button>

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid UseLayoutRounding="True" >

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <TextBox Margin="10" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            这是一个文本框
        </TextBox>
        <Button Margin="10,10,2,10" Padding="3" Grid.Row="1" Grid.Column="1">OK</Button>
        <Button Margin="2,10,10,10" Padding="3" Grid.Row="1" Grid.Column="2">Cancel</Button>
       
    </Grid>

</Window>

分割窗口

​ 每个 Windows 用户都见过分割条一能将窗口的一部分与另一部分分离的可拖动分割器,
例如,当使用Windows 资源管理器时,会看到一系列立件实(在左边)和一系列文件(在右边)、可拖动它们之间的分割条来确定每部分占据窗口的比例。

​ 理解如何使用 GridSplitter 类,从而得到所期望的效果需要一定的经验。下面列出几条指导原则:

  • GrdSpliter 对象必须放在Grid 单元格中。可与已经存在的内容一并放到单元格中,这时循要调整边距设置,使它们不相互重叠。更好的方法是预留一列或一行专门用于放置 Gridspliter对象,并将预留行或列的Heigh 或 Width 属性的值设置为 Auto。
  • Gridspliter 对象总是改变整行或整列的尺寸(而非改变单个单元格的尺寸)。为使Cridspliter对象的外观和行为保持一致,需要拉伸Gridsplitter 对象使其穿越整行或整列,而不是将其限制在单元格中。为此,可使用前面介绍过的RowSpan或ColumnSpan 属性。
  • 最初,GridSpliter 对象很小不易看见。为了使其更可用,需要为其设置最小尺寸。对于竖直分割条,需要将 VericalAlignment 属性设置为stretch(使分割条填满区域的整个高度),并将 Width 设置为固定值(如10个设备无关单位)。对于水平分割条,需要设置 HorizontalAlignmeat 属性来拉伸,并将 Height 属性设置为固定值。
  • Gridspliter 对齐方式还决定了分割条是水平的(用于改变行的尺寸)还是竖直的(用于改变列的尺寸)。对于水平分割条,需要将 VerticalAlignment 属性设置为Center(这也是默认值),以指明拖动分割条改变上面行和下面行的尺寸。对于竖直分割条,需要将 HorizontalAlignment 属性设置为 Center,以改变分割条两侧列的尺寸。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid UseLayoutRounding="True" >

        <!--行设置-->
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        
        <!--列设置-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="100"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition MinWidth="50"/>
        </Grid.ColumnDefinitions>

        <!--按钮-->
        <Button Grid.Row="0" Grid.Column="0" Margin="3">left-1</Button>
        <Button Grid.Row="0" Grid.Column="2" Margin="3">right-1</Button>
        <Button Grid.Row="1" Grid.Column="0" Margin="3">left-2</Button>
        <Button Grid.Row="1" Grid.Column="2" Margin="3">right-1</Button>

        <!--分割线-->
        <!--ShowsPreview:是否显示预览-->
        <GridSplitter Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Width="3" VerticalAlignment="Stretch" HorizontalAlignment="Center" ShowsPreview="False"></GridSplitter>
    </Grid>

</Window>

​ 上面的标记还包含了一处额外的细节。在声明GridSplitter 对象时,将 ShowsPreview 属性设置为 false .因此,当把分割线从一边拖到另一边时,会立即改变列的尺寸。但是如果将 ShowsPreview 属性设置为 true,当拖动分割条时就会看到一个灰色的阴影跟随鼠标指针,用于显示将在何处进行分割。并且直到释放了鼠标键之后列的尺寸才改变。如果GridSplitter 对象获得了焦点,可可以使用箭头改变相应的尺寸。

​ 如果希望分割条以更大的幅度(如每次10个单位)进行移动,可调整DragIncrement 属性。

提示:

​ 可以改变 GridSplitter 对象的填充方式,使其不只是具有阴影的灰色矩形。技巧是使用Background 属性应用填充,该属性接受简单的颜色或更复杂的画刷。

​ Grid 面板通常包含多个 GridSpliter 对象。然而,可以在一个 Grid 面板中嵌套另一个Grid面板;而且,如果确实在 Grid 面板中嵌套了Grid 面板,那么每个Grid 面板可以有自己的GridSplitter对象。这样就可以创建被分割成两部分(如左边窗格和右边窗格)的窗口,然后将这些区域(如右边的窗格)进一步分成更多的部分(例如,可调整大小的上下两部分)。

案例2:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid UseLayoutRounding="True" >

        
        <!--列设置:3列-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="100"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition MinWidth="50"/>
        </Grid.ColumnDefinitions>

        <!--左侧Grid-->
        <Grid Grid.Column="0">
            
            <!--行设置:2行-->
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>

            <Button Margin="3" Grid.Row="0">Top Left</Button>
            <Button Margin="3" Grid.Row="1">Bottom Left</Button>
            
        </Grid>
        
        <!--分割线-->
        <!--ShowsPreview:是否显示预览-->
        <GridSplitter Grid.Column="1" Grid.RowSpan="2" Width="3" VerticalAlignment="Stretch" HorizontalAlignment="Center" ShowsPreview="False"></GridSplitter>

        <!--右侧Grid-->
        <Grid Grid.Column="2">

            <!--行设置:3行-->
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>

            <Button Margin="3" Grid.Row="0">Top Right</Button>
            <Button Margin="3" Grid.Row="2">Bottom Right</Button>

            <GridSplitter Grid.Row="1" Height="3" VerticalAlignment="Center" HorizontalAlignment="Stretch" ShowsPreview="False"></GridSplitter>

        </Grid>
    </Grid>

</Window>

共享尺寸组

​ 正如在前面看到的,Grid 面板包含一个行列集合,可以明确地按比例确定行和列的尺寸,或根据其子元素的尺寸确定行和列的尺寸。还有另一种确定一行或一列尺寸的方法—与其他行或列的尺寸相匹配。这是通过称为“共享尺寸组”(Shared size groups)的特性实现的。

​ 共享尺寸组的目标是保持用户界面独立部分的一致性。例如,可能希望改变一列的尺寸以适应其内容,并改变另一列的尺寸使其与前面一列改变后的尺寸相匹配。然而,共享尺寸组的真正优点是使独立的Grid 控件具有相同的比例。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid Grid.IsSharedSizeScope="True" Margin="3">
        
        <!--设置行:3行-->
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        
        <!--上方Grid-->
        <Grid Margin="3" Background="LightYellow" ShowGridLines="True">
            
            <!--设置列:3列-->
            <!--SharedSizeGroup:共享尺寸组-->
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto" SharedSizeGroup="TextLabel"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Label Margin="5" Grid.Column="0">这是一个长的文本</Label>
            <Label Margin="5" Grid.Column="1">其他文本</Label>
            <TextBox Margin="5" Grid.Column="2">这是一个文本框</TextBox>
        </Grid>
        
        <!--中间标签-->
        <Label Grid.Row="1">这是中间的标签</Label>

        <!--下方Grid-->
        <Grid Grid.Row="2" Margin="3" Background="LightCoral" ShowGridLines="True">

            <!--设置列:2列-->
            <!--SharedSizeGroup:共享尺寸组-->
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto" SharedSizeGroup="TextLabel"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Label Margin="5" Grid.Column="0">短的</Label>
            <TextBox Margin="5" Grid.Column="1">这是一个文本框</TextBox>
           
        </Grid>
    </Grid>

</Window>

UnitformGrid 面板

​ 有一种网格不遵循前面讨论的所有原则--UniformGrid 面板。与Grid 面板不同,UaifommCrid 面板不需要(甚至不支持)预先定义的列和行。相反,通过简单地设置Rows 和Colurs 属性来设置其尺寸。每个单元格始终具有相同的大小,因为可用的空间被均分。最后,元素根据定义的顺序被放置到适当的单元格中。UniformGrid 面板中没有 Row和 Column 附加属性,也没有空白单元格。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <!--
    Rows:行数
    Columns:列数
    -->
    <UniformGrid Rows="2" Columns="2">
        <Button>Top Left</Button>
        <Button>Top Right</Button>
        <Button>Buttom Left</Button>
        <Button>Buttom Right</Button>
    </UniformGrid>

</Window>

Canvas 面板

​ Canvas 面板允许使用精确的坐标放潭元案,如果设计数据驱动的富窗体和标准对话框,这并非好的选择;但如果需要构建其他一些不同的内容(例如,为图形工具创建绘图表面),Canvas 面板可能是个有用的工具。Canvas面板还是最轻量级的布局容器。这是因为 Canvas面板没有包含任何复杂的布局逻辑,用以改变其子元素的首选尺寸。Canvas 面板只是在指定的位置放置其子元素,并且其子元素具有所希望的精确尺寸。

​ 为在 Canvas 面板中定位元素,需要设置 Canvas.Left 和 Canvas,Top 附加属性。Canvas.Left 属性设置元素左边和 Canvas 面板左边之间的单位数,Canvas.Top 属性设置子元素顶边和Canvas 面板顶边之间的单位数。同样,这些数值也是以设备无关单位设置的,当将系统DPI设置为 96 dpi 时,设备无关单位恰好等于通常的像素。

注意:

​ 另外,可使用 Canvas.Right 属性而不是 Canvas.Lef 属性来确定元素和 Canvas 面板右边缘间的距离;可使用 Canvas.Bottom 属性而不是 Canvas.Top 属性来确定元素和 Canvas 面板底部边缘的距离。不能同时使用 Canvas.Right 和 Canvas. Left 属性,也不能同时使用 Canvas.Top 和Canvas.Bottom 属性。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <!--
    Canvas.Left: 左边距
    Canvas.Top: 上边距
    Width: 宽度
    Height: 高度
    -->
    <Canvas>
        <Button Canvas.Left="10" Canvas.Top="10">(10,10)</Button>
        <Button Canvas.Left="120" Canvas.Top="30">(120,30)</Button>
        <Button Canvas.Left="60" Canvas.Top="80" Width="50" Height="50">(60,80)</Button>
        <Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50">(70,120)</Button>
    </Canvas>

</Window>

Z 轴坐标

​ 如果 Canvas 面板中有多个互相重叠的元素,可通过设置Canvas ZIndex 附加属性来控制它们的层叠方式。

​ 添加的所有元素通常都具有相同的 Zlndex 值—0。如果元素具有相同的 ZIndex 值,就按它们在Canvas.Children 集合中的顺序进行显示,这个顺序依赖于元素在 XAMIL 标记中定义的顺序。

lnkCanvas 元素

​ WPF 还提供了 InkCanvas 元素,它与 Canvas 面板在某些方面是类似的(而在其他方面却完全不同)。和 Canvas 面板一样,InkCanvas 元素定义了4个附加属性(Top、Left、Bottom 和 Righn),可将这4个附加属性应用于子元素,以根据坐标进行定位。然而,基本的内容区别很大—零际上,InkCanvas 类不是派生自 Canvas类,甚至也不是派生自 Pane! 基类,而是直接派生自FrameworkElement类。

​ InkCanvas元素的主要目的是用于接收手写笔输入。手写笔是一种在平板 PC 中使用的类似于钢笔的输入设备,然而,InkCanvas 元素同时也可使用鼠标进行工作,就像使用手写笔一样。因此,用户可使用鼠标在 InkCanvas 元素上绘制线条,或者选择以及操作 InkCanvas 中的元素。

​ InkCanvas 元素实际上包含两个子内容集合。一个是为人熟知的Children集合,它保存任意元素,就像 Canvas 面板一样。每个子元素可根据Top、Lef、Bottom 和 Right属性进行定位。另一个是Strokes 集合,它保存 System.Windows.Ink.Stroke 对象,该对象表示用户在 InkCanvas。元素上绘制的图形输入。用户绘制的每条直线或曲线都变成独立的 Stroke 对象。得益于这两个集合,可使用 InkCanvas 让用户使用存储在 Strokes 集合中的笔画(stroke)为保存在Children 集合中的内容添加注释。

​ 根据为 EditingMode 属性设置的值,可以采用截然不同的方式使用 InkCanvas 元素。下表列出了所有选项:

名称 说明
Ink InkCanvas 元素允许用户绘制批注,这是默认模式。当用户用鼠标或手写笔绘图时,会绘制笔画.
GestureOnly InkCanvas 元素不允许用户绘制笔画批注,但会关注预先定义的特定姿势(例如在某个方向拖动手写笔或涂画内容)•能识别的姿势的完整列表由 System.Windows. Ink.Application Gesture 枚举给出.
InkAndGesture InkCanvas元素允许用户绘制笔画批注,也可以识别预先定义的姿势.
EraseByStroke 当单击笔画时,InkCanvas 元素会擦除笔画。如果用户使用手写笔,可使用手写笔的底端切换到该模式(可使用只读的 ActiveEditingMode 属性确定当前编辑模式,也可通过改变EditingModelinverted 属性来改变手写笔的底端使用的工作模式)
EraseByPoint 当单击笔画时,InkCanvas 元素会擦除笔画中被单击的部分(笔画上的一个点)
Select InkCanvas 面板允许用户选择保存在Children集合中的元素。要选择一个元素,用户必须单击该元素或拖动“套索”选择该元素。一旦选择一个元素,就可以移动该元素、改变其尺寸或将其删除
None InkCanvas 元素忽略鼠标和手写笔输入。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <InkCanvas Name="inkCanvas" Background="LightYellow" EditingMode="Ink">
            
    </InkCanvas>

</Window>

​ inkCanvas 元素会引发多种事件,当编辑模式改变时会引发 ActiveEditingModeCbanged 事件,在 GestureOnly 或 InkAndGesture 模式下删除姿势时会引发Gesture 事件,绘制完笔画时会引发SrokeColleoted 事件,擦除笔画时会引发 StokeErasing 事件和StrokeErased 事件,在 Select 模式下选择元素或改变元素时会引发 SelectionChanging 事件、SelectionChanged 事件、SelectionMoving 事件、SelectionMoved 事件、SelectionResizing 事件和 SelectionResized 事件。其中,名称以 “ing”结尾的事件表示动作将要发生,但可以通过设置 EventATgs 对象的Cancel属性取消事件。

​ 在Select 模式下,InkCanvas 元素可为拖动以及操作内容提供功能强大的设计界面。下图显示了 InkCanvas 元素中的一个按钮控件,左图中显示的是该按钮被选中的情况,而右图中显示的是选中该按钮后,改变其位置和尺寸的情况。

案例2:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <InkCanvas Name="inkCanvas" Background="LightYellow" EditingMode="Select">

        <Button InkCanvas.Top="20" InkCanvas.Left="50">hello</Button>
        
    </InkCanvas>

</Window>

布局示例

列设置

​ 布局容器(如Grid面板)使得窗口创建整个布局结构变得非常容易。例如下图中显示的窗口及设置。该窗口在一个表格结构中排列各个组件--标签、文本框以及按钮。

案例1:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid Margin="3,3,10,3" ShowGridLines="False">
        
        <!--行设置-->
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <!--列设置-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0" Margin="3" VerticalAlignment="Center">Home:</Label>
        <TextBox Grid.Row="0" Grid.Column="1" Margin="3" Height="auto" VerticalAlignment="Center">c:\</TextBox>
        <Button Grid.Row="0" Grid.Column="2" Margin="3" Padding="2">Browser</Button>

        <Label Grid.Row="1" Grid.Column="0" Margin="3" VerticalAlignment="Center">Network:</Label>
        <TextBox Grid.Row="1" Grid.Column="1" Margin="3" Height="auto" VerticalAlignment="Center">e:\work</TextBox>
        <Button Grid.Row="1" Grid.Column="2" Margin="3" Padding="2">Browser</Button>

        <Label Grid.Row="2" Grid.Column="0" Margin="3" VerticalAlignment="Center">Web:</Label>
        <TextBox Grid.Row="2" Grid.Column="1" Margin="3" Height="auto" VerticalAlignment="Center">c:\</TextBox>
        <Button Grid.Row="2" Grid.Column="2" Margin="3" Padding="2">Browser</Button>


        <Label Grid.Row="3" Grid.Column="0" Margin="3" VerticalAlignment="Center">Secondary:</Label>
        <TextBox Grid.Row="3" Grid.Column="1" Margin="3" Height="auto" VerticalAlignment="Center">c:\</TextBox>
        <Button Grid.Row="3" Grid.Column="2" Margin="3" Padding="2">Browser</Button>
    </Grid>

</Window>

动态内容

​ 在以下示例中,用户界面可选择短文本和长文本。当使用长文本时,包含文本的按钮会自动改变其尺寸,而其它内容也会相应的调整位置。并且因为改变了尺寸的按钮共享同一布局容器,所以整个用户界面都会改变尺寸。最终的结果是所有按钮保持一致的尺寸---最大按钮的尺寸。

案例1:

xaml代码:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">

    <Grid>
        
        <!--行设置-->
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <!--列设置-->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Row="0" Grid.Column="0">
            <Button x:Name="cmdPrev" Margin="10,10,10,3">Prev</Button>
            <Button x:Name="cmdNext" Margin="10,3,10,3">Next</Button>
            <CheckBox x:Name="chkLong" Margin="10,10,10,10" Unchecked="chkLong_Unchecked" Checked="chkLong_Checked" >Show Long Text</CheckBox>
        </StackPanel>

        <TextBox Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Margin="0,10,10,10" TextWrapping="WrapWithOverflow">
            Computer viruses are artificially created programs that are destructive, infectious, and latent, causing damage to computer information or systems. It does not exist independently, but is hidden within other executable programs. After being infected with a virus in a computer, it can affect the running speed of the machine, and in severe cases, it can cause system crashes and damage; Therefore, viruses cause significant losses to users, and typically, these destructive programs are referred to as computer viruses
        </TextBox>

        <Button Grid.Row="1" Grid.Column="0" Name="cmdClose" Margin="10,3,10,10">Close</Button>
    </Grid>
</Window>

c#代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// <summary>
    /// Window1.xaml 的交互逻辑
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
       
        private void chkLong_Checked(object sender, RoutedEventArgs e)
        {
            this.cmdPrev.Content = "<--Go to the previous Window";
            this.cmdNext.Content = "Go to Next Window-->";
        }

        private void chkLong_Unchecked(object sender, RoutedEventArgs e)
        {
            this.cmdPrev.Content = "Prev";
            this.cmdNext.Content = "Next";
        }
    }
}

组合式用户界面

​ 许多布局容器(如 StackPanel 面板、DockPanc!面板以及 WrapPanel 面板)可以采用灵活多变的柔性方式非常得体地将内容安排到可用窗口空间中。该方法的优点是,它允许创建真正的组合式界面。换句话说,可在用户界面中希望显示的恰当部分插入不同的面板,而保留用户界面的其他部分。整个应用程序本身可以相应地改变界面,这与Web 门户站点有类似之处。

​ 下图展示了一个组合式用户界面,在一个WrapPanel 面板中放置几个独立的面板。用户可以通过窗口顶部的复选框,选择显示这些面板中的哪些面板。

视图代码:

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="700">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        
        <StackPanel Grid.Row="0"  Margin="3" Background="LightYellow" Orientation="Horizontal">
            <CheckBox Name="chb1" IsChecked="True" Click="CheckBox_Click">Panel1</CheckBox>
            <CheckBox Name="chb2" IsChecked="True" Click="CheckBox_Click">Panel2</CheckBox>
            <CheckBox Name="chb3" IsChecked="True" Click="CheckBox_Click">Panel3</CheckBox>
            <CheckBox Name="chb4" IsChecked="True" Click="CheckBox_Click">Panel4</CheckBox>
        </StackPanel>
        
        <WrapPanel Name="container" Grid.Row="1"  Background="LightBlue">

            <Grid MinWidth="200" Name="pnl1">
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <Button Grid.Row="0" Grid.Column="0" MinWidth="50" MinHeight="50" Margin="10">1</Button>
                <Button Grid.Row="0" Grid.Column="1" MinWidth="50" MinHeight="50" Margin="10">2</Button>
                <Button Grid.Row="1" Grid.Column="0" MinWidth="50" MinHeight="50" Margin="10">3</Button>
                <Button Grid.Row="1" Grid.Column="1" MinWidth="50" MinHeight="50" Margin="10">4</Button>
            </Grid>
            
            <TabControl MinWidth="400" Name="pnl2">
                <TabItem Header="Page1" VerticalAlignment="Center" HorizontalAlignment="Center">
                    <Label>内容1</Label>
                </TabItem>
                <TabItem Header="Page2">
                    <Label>内容2</Label>
                </TabItem>
            </TabControl>

            <StackPanel Width="200" Name="pnl3">
                <Label>计算机病毒是人为制造的,有破坏性,又有传染性和潜伏性的,对计算机信息或系统起破坏作用的程序。</Label>
                <Button Width="80" Margin="5,40,5,5">Ok</Button>
                <Button Width="80" Margin="5,5,5,5">Cancel</Button>
            </StackPanel>

            <Grid MinWidth="200" Name="pnl4">
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <Button Grid.Row="0" Grid.Column="0" MinWidth="50" MinHeight="50" Margin="10">1</Button>
                <Button Grid.Row="0" Grid.Column="1" MinWidth="50" MinHeight="50" Margin="10">2</Button>
                <Button Grid.Row="1" Grid.Column="0" MinWidth="50" MinHeight="50" Margin="10">3</Button>
                <Button Grid.Row="1" Grid.Column="1" MinWidth="50" MinHeight="50" Margin="10">4</Button>
            </Grid>

        </WrapPanel>
    </Grid>
   
</Window>

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// <summary>
    /// Window1.xaml 的交互逻辑
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void CheckBox_Click(object sender, RoutedEventArgs e)
        {
            CheckBox chb = sender as CheckBox;
            string index = chb.Name.Substring(3,1);

            // 在适当的位置调用FindName方法
            FrameworkElement rootElement = this.FindName("container") as FrameworkElement;
            FrameworkElement control = LogicalTreeHelper.FindLogicalNode(rootElement, "pnl"+index) as FrameworkElement;

            if (chb.IsChecked==true)
            {
               control.Visibility = Visibility.Visible;               
            }
            else
            {
                control.Visibility = Visibility.Collapsed;                
            }
        }
    }
}

​ Visibility 属性是 UIElement 基类的一部分,因此放置于WPF窗口中的任何内容都支持该属性。该属性可使用三个值,它们来自 System.Windows.Visibility 枚举,如下表所示:

说明
Visibility 元素在窗口中正常显示
Collapsed 元素不显示,也不占用任何空间。
Hidden 元素不显示,但仍为其保留空间。

本章小结

​ 本章详细介绍了 WPF布局模型,并讨论了如何以堆栈、网格以及其他排列方式放置元素可使用嵌套的布局容器组合创建更复杂的布局,可结合使用 GridSplitter 对象创建可变的分割窗口。本章一直非常关注这一巨大变化的原因—WPF 布局模型在保持、加强以及本地化用户界面方面所具有的优点。

​ 布局内容远不止这些,接下来的几章还将列举更多使用布局容器组织元素分组的示例,还将学习允许在窗口中排列内容的几个附加功能:

  • 特殊容器
    :可以使用 ScrdlIViewer、Tabltem 以及 Expander 控件滚动内容、将内容放到单独的选项卡中以及折叠内容。与布局面板不同,这些容器只能包含单一内容。不过,可以很容易地组合使用这些容器和布局面板,以便准确实现所需的效果。第6 章将尝试使用这些容器。
  • Viewbox
    :需要一种方法来改变图形内容(如图像和矢量图形)的尺寸吗?Viewbox 是另一种特殊容器,可帮助您解决这一问题,而且 Viewbox 控件内置了缩放功能。
  • 文本布局
    :WPF 新增了用于确定大块格式化文本布局的新工具。可使用浮动图形和列表,并且可以使用分页、分列以及更复杂、更智能的換行功能来获得非常完美的结果。

课后作业

image

三年前,我们
开源了
Quickwit,一个面向大规模数据集的分布式搜索引擎。我们的目标很宏大:创建一种全新的全文搜索引擎,其成本效率比 Elasticsearch 高十倍,配置和管理显著更简单,并且能够扩展到 PB 级别的数据。

虽然我们知道 Quickwit 的潜力,但我们通常测试的数据量不超过
100TB
,索引吞吐量不超过
1GB/s
。我们缺乏现实世界中的数据集和计算资源来测试 Quickwit 在多 PB 规模下的表现。

直到六个月前,情况发生了变化。币安(全球领先的加密货币交易所)的两位工程师发现了 Quickwit 并开始尝试使用它。仅仅几个月内,他们实现了我们梦寐以求的目标:成功地将多个 PB 级别的 Elasticsearch 集群迁移到 Quickwit,取得了显著的成绩,包括:

  • 将索引扩展到了
    每天 1.6 PB
  • 运行了一个处理
    100 PB 日志
    的搜索集群。
  • 每年节省数百万美元
    ,通过
    降低 80% 的计算成本

    减少 20 倍的存储成本
    (对于相同的保留期)。

image

在这篇博客文章中,我将与大家分享币安是如何构建 PB 级别的日志服务以及如何克服将 Quickwit 扩展到多 PB 规模时遇到的挑战。

币安面临的挑战

作为全球领先的加密货币交易所,币安处理着巨大的交易量,每笔交易都会产生对安全、合规性和运营洞察至关重要的日志。这导致了大约每秒处理
2100万
条日志记录,相当于每秒
18.5GB
,或每天
1.6PB
的日志量。

为了管理如此庞大的数据量,币安之前依赖于 20 个 Elasticsearch 集群。大约 600 个 Vector Pod 从不同的 Kafka 主题拉取日志并进行处理,然后再推送到 Elasticsearch 中。

image

然而,这种设置在几个关键领域未能满足币安的需求:

  • 运维复杂性
    :管理众多 Elasticsearch 集群变得越来越具有挑战性和耗时。
  • 有限的保留期限
    :币安仅保留大部分日志几天的时间。他们的目标是将此期限延长至数月,这意味着需要存储和管理
    100PB
    的日志,这对于他们的 Elasticsearch 设置来说成本高昂且复杂。
  • 有限的可靠性
    :为了限制基础设施成本,高吞吐量的 Elasticsearch 集群被配置为没有复制,这损害了持久性和可用性。

团队知道他们需要彻底改变才能满足日益增长的日志管理、保留和分析需求。

为什么 Quickwit 几乎是完美的选择

当币安的工程师们发现 Quickwit 时,他们很快意识到它相比现有的设置提供了几个关键优势:

  • 原生 Kafka 集成
    :它允许直接从 Kafka 中摄入日志,具备恰好一次(exactly-once)语义,提供了巨大的运维好处。具体而言,您可以拆除集群,在一分钟内重新创建,不会丢失任何数据,准备好以每天
    1.6PB
    的速度摄入或搜索 PB 级别的数据,并且可以根据临时峰值上下调整规模。
  • 对象存储作为主要存储
    :所有索引的数据都保留在对象存储上,消除了在集群侧配置和管理存储的需求。
  • 更好的数据压缩
    :Quickwit 通常比 Elasticsearch 实现好两倍的数据压缩,进一步减少了索引的存储占用。

然而,没有任何用户将 Quickwit 扩展到多 PB 级别,任何工程师都知道将系统扩展 10 倍或 100 倍可能会暴露出意想不到的问题。但这并没有阻止他们,他们准备迎接这一挑战!

image

每天 1.6PB 的索引扩展

借助 Kafka 数据源,币安迅速扩大了索引规模。在 Quickwit 概念验证的一个月后,他们已经达到了每秒几 GB 的索引速度。

这种快速进展很大程度上归功于 Quickwit 与 Kafka 的工作方式:Quickwit 利用 Kafka 的消费者组将工作负载分布在多个Pod 之间。每个 Pod 索引 Kafka 分区的一个子集,并使用最新的偏移量更新元存储,确保恰好一次(exactly-once)语义。这种设置使得 Quickwit 的索引器无状态:您可以拆除整个集群并重新启动,索引器会像什么都没有发生一样从中断的地方继续运行。

然而,币安的规模揭示了两个主要问题:

  • 集群稳定性问题
    :几个月前,Quickwit 的 gossip 协议(称为
    Chitchat
    )在数百个 Pod 的情况下遇到了困难:一些索引器会离开集群然后重新加入,导致索引吞吐量不稳定。
  • 不均衡的工作负载分配
    :币安为其日志使用了多个 Quickwit 索引,索引吞吐量各不相同。有些索引的吞吐量高达每秒几 GB,而其他索引只有每秒几 MB。Quickwit 的放置算法无法均匀分布工作负载。这是一个已知的问题
    issue
    ,我们将在今年晚些时候解决这个问题。

为了解决这些限制,币安为每个高吞吐量的 topic 部署了单独的索引集群,并为较小的主题保留了一个集群。由于索引器无状态,隔离每个高吞吐量集群并未带来运维负担。此外,所有 Vector Pod 都被移除,因为币安直接在 Quickwit 中使用了 Vector 转换。

image

经过几个月的迁移和优化后,币安最终实现了 1.6 PB 的索引吞吐量,使用了 10 个 Quickwit 索引集群,共 700 个 Pod 请求大约 2800 个 vCPU 和 6 TB 内存,平均每个 vCPU 达到 6.6 MB/s。在一个特定的高吞吐量 Kafka topic 上,这个数字上升到了每个 vCPU 11 MB/s。

接下来的挑战是:扩展搜索!

用于 100PB 日志的单一搜索集群

现在 Quickwit 已经能够高效地每天索引
1.6PB
的数据,挑战转向了搜索 PB 级别的日志。如果有 10 个集群,币安通常需要为每个集群部署搜索 Pod,这削弱了 Quickwit 的一个优势:汇聚搜索资源以访问所有索引共享的对象存储。

为了避免这个陷阱,币安的工程师们设计了一个巧妙的解决方案:他们通过将每个索引集群元存储中的所有元数据复制到一个PostgreSQL 数据库中,创建了一个统一的元存储。这个统一的元存储使得部署一个独特的集中式搜索集群成为可能,该集群能够搜索所有索引!

image

目前,币安管理着一个适度规模的搜索集群,包含 30 个搜索 Pod,每个 Pod 请求 40 个 vCPU 和
100GB
内存。举个例子,您只需要 5 个搜索器(8 个 vCPU,6GB 内存请求)就能在
400TB
的日志中找到所需的信息。币安运行这类查询针对的是PB级别的数据,同时还运行聚合查询,因此需要更高的资源请求。

总结

总体而言,币安迁移到 Quickwit 是一次巨大的成功,并带来了几个实质性的益处:

  • 与 Elasticsearch 相比,计算资源减少了
    80%
  • 对于相同的保留期限,存储成本降低了
    20
    倍。
  • 在基础设施成本和维护操作方面都是经济可行的大规模日志管理解决方案。
  • 配置微调最少,一旦确定了正确的 Pod 数量和资源,就能够高效工作。
  • 根据日志类型,将日志保留期限增加到一个月或几个月,提高了内部故障排查的能力。

总之,币安从 Elasticsearch 迁移到 Quickwit 是币安和 Quickwit 工程师之间激动人心的 6 个月合作经历,我们为此感到非常自豪。我们已经计划了数据压缩、多集群支持以及更好地与 Kafka 数据源分配工作负载等方面的改进。

更多

前言

今天给大家推荐一个超实用的开源项目《.NET 7 + Vue 权限管理系统 小白快速上手》,
DncZeus
的愿景就是做一个.NET 领域小白也能上手的简易、通用的后台权限管理模板系统基础框架。

不管你是技术小白还是技术大佬或者是不懂前端Vue 的新手,这个项目可以快速上手让我们从0到1,搭建自己的通用后台权限管理系统,掌握后台权限管理系统的搭建技巧以及系统基础框架。

它不仅涵盖了从环境搭建到核心功能实现的全过程,而且特别注重让初学者也能轻松上手。 无论你是希望通过实战来加深对新技术的理解,还是想要为自己的项目升级权限管理功能,让这个项目成为大家的好帮手。期待我们都能够从小白变大佬!

项目介绍

DncZeus 是一个基于 .NET 7 和 Vue.js 的前后端分离的通用后台管理系统框架。

后端使用 .NET 7 和 Entity Framework Core 构建,前端则采用了流行的 iView UI 框架配合 Vue.js。

该项目实现了前后端的动态权限管理和控制以及基于 JWT 的用户令牌认证机制,从而确保前后端交互流畅。

请注意:DncZeus 不是一个完整的业务系统,但它提供了大多数业务系统所需的开发场景,帮助 .NET 开发者快速构建出交互良好、体验优秀且功能丰富的单页应用程序 (SPA)。

项目特点

  • 技术栈
    :后端使用 .NET 7 + EF Core 构建,前端采用基于 Vue.js 的 iView (iview-admin) 进行前后端分离开发。
  • 新手友好
    :设计考虑新手上手简易,代码逻辑清晰。
  • 权限管理
    :实现通用后台权限管理,精确到页面访问和操作按钮的控制。

项目技术

  • .NET 7
  • ASP.NET Core WebAPI
  • JWT 令牌认证
  • AutoMapper
  • Entity Framework Core 7
  • .NET 7 依赖注入
  • Swagger UI
  • Vue.js (ES6 语法)
  • iView (基于 Vue.js 的 UI 框架)

环境工具

1、Node.js (同时安装 npm 前端包管理工具)

2、Visual Studio 2022

3、VS Code 或者其他前端开发工具

4、git 管理工具

5、MySQL、PostgreSQL 或 SQL Server (SQL Server 2012+)

适合人群

了解 DncZeus 所需的知识

DncZeus
让初级 .NET 开发者也能轻松上手,因此后端项目并未涉及复杂的架构和封装,代码逻辑直观易懂。

为了更好地熟悉和运用
DncZeus
,你需要了解以下技术:

  • .NET 7
    :确保你能看懂并理解后端的实现和工作方式。
  • Vue.js
    :前端实现的基础。
  • iView
    :基于 Vue.js 的 UI 框架,
    DncZeus
    的前端 UI 交互正是基于此框架实现。

如果你对这些技术还不熟悉,建议先学习一些基础知识再使用
DncZeus

以下是学习这些技术的官方资源:

下载项目

1、Git工具下载

首先请确保本地开发环境已安装了Git管理工具,然后在需要存放本项目的目录

打开Git 命令行工具
Git Bash Here
,在命令行中输入如下命令:

git clone https://github.com/lampo1024/DncZeus.git

以上命令就把
DncZeus
的源代码拉取到你的本地开发机上。

2、手动下载

如果你不愿意使用Git管理工具下载
DncZeus
的源代码,也可以在
Github
手动下载。

打开地址
https://github.com/lampo1024/DncZeus
,找到页面中"Code" 的按钮点击,然后在弹出的对话框中点击"Download ZIP" 按钮,即可下载
DncZeus
的源代码,具体如下图所示:

安装依赖

1、前端项目

安装前端依赖

1、使用 Git 管理工具,无需退出当前工具,进入
DncZeus
的前端项目目录:

cd DncZeus/DncZeus.App

2、如果你是手动下载的源代码,请在该目录下打开命令行工具。

3、在命令行中输入以下命令来安装前端依赖包:

npm install

或者使用简写命令:

npm i

2、后端项目

配置数据库连接

1、在 Visual Studio 中打开解决方案
DncZeus.sln

2、根据你的开发环境(默认示例为 SQL Server Localdb),修改配置文件
appsettings.json
中的数据库连接字符串。

示例默认连接字符串为:

"ConnectionStrings": {"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=DncZeus;Trusted_Connection=True;MultipleActiveResultSets=true"}

初始化系统数据

1、打开项目根目录中的
Scripts
文件夹。

2、执行与你的数据库类型对应的脚本文件以初始化系统数据。

从 v2.1.0 版本开始,DncZeus 支持 MySQL、PostgreSQL 和 MSSQL 三种数据库类型!

你可以根据需求选择适合自己的数据库。

至此,所有的准备工作已经完成。

现在,你可以开始体验 DncZeus 框架了!

启动项目

1、
启动后端服务

使用 Visual Studio 打开
DncZeus
根目录中的解决方案文件
DncZeus.sln
。(也可以使用 VS Code 进行 .NET 7 的开发。)

设置
DncZeus.API
项目为默认启动项并运行此项目。

浏览器中打开地址:
http://localhost:xxxx/swagger
,即可查看已实现的后端 API 接口服务。

2、
启动前端服务

在命令行中进入到
DncZeus
的前端项目目录
DncZeus.App

运行如下命令以启动前端项目服务:

npm run dev

成功运行后,前端项目服务会在浏览器中自动打开地址
http://localhost:xxxx

项目演示

1、登录信息

  • 超级管理员用户名

    administrator
  • 普通管理员用户名

    admin
  • 密码

    111111
  • 体验地址
    :https://dnczeus.codedefault.com

尝试使用不同的用户名登录系统,体验不同角色的菜单权限差异。

注意
:这是一个个人项目,体验服务器配置较低,请轻度使用,感谢您的理解和支持!

国内镜像地址
:https://gitee.com/rector/DncZeus

2、项目效果

登录页面

系统首页

用户权限

消息中心

项目地址

Gitee:https://gitee.com/rector/DncZeus

Github:https://github.com/lampo1024/DncZeus

总结

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!