这是 Gtk# 系列博文的第二篇。在上一篇博文《编写你的第一个 Gtk# 应用》中,我们提到“一个 Gtk.Window 只能直接包含一个部件”。这意味着,在不做其他额外操作的情况下,如果你向一个 GtkWindow 中添加了一个 GtkLabel (就像上一篇博文中的 Hello World一样)那么你将不能再添加一个按钮进去。
如过你尝试这么做,你会发现按钮并不会显示在窗体上,同时在控制台会输出一个警告:尝试将一个 GtkButton 类型的部件添加到 GtkWindow 中,但是 GtkWindow 作为 GtkBin 的子类型每次只能容纳一个部件,它现在已经容纳了一个 GtkLabel 类型的部件。
Attempting to add a widget with type GtkButton to a GtkWindow, but as a GtkBin subclass a GtkWindow can only contain one widget at a time; it already contains a widget of type GtkLabel 。
如果我们的 GUI 程序只能显示一个部件,那么就太尴尬了,只能包含一个部件的窗口也没有意义。其实我们有很多现成的部件可以解决这个尴尬的问题,常见的比如:VBox、HBox 和 Table 。
在 WinForm 开发中,我们使用坐标来定位控件。在 Gtk# 中当然也支持使用这种方式布局控件,但是首选的方式还是使用盒子(Box)。盒子是不可见的容器部件,他们有两种形式:水平(HBox)和垂直(VBox)。你可以把所有的部件(Widget)想象成一个又一个的盒子,然后他们整齐的排列好,塞满一个更大的盒子,而这个更大的盒子外面也可以有盒子。这种布局方式与屏幕无关且能更好的支持国际化。
1、准备项目
和上一篇博文类似,创建一个名为“Gtk.Layouts”的 .NET Core 控制台应用程序并引入 GtkSharp 组件后,在 Program.cs 文件中键入以下代码:
using System; namespace Gtk.Layouts { class Program { static void Main(string[] args) { Application.Init(); var win = new Window("Gtk.Layouts"); win.SetDefaultSize(300, 300); win.WindowPosition = WindowPosition.Center; win.DeleteEvent += (s, e) => { Application.Quit(); }; win.ShowAll(); Application.Run(); } } }
以上代码在运行后会在屏幕中心展示一个标题为“Gtk.Layouts”大小是 300* 300 的空白 GtkWindow 。
2、HBox 和 VBox
HBox 容器中所有的控件都会水平排列在一行上,现在把 HBox 添加到窗体中,新建一个 InitializeWindow 方法,把窗体的初始化代码放到这个方法里:
using System; namespace Gtk.Layouts { class Program { static void Main(string[] args) { Application.Init(); var win = new Window("Gtk.Layouts"); win.SetDefaultSize(300, 300); win.WindowPosition = WindowPosition.Center; win.DeleteEvent += (s, e) => { Application.Quit(); }; InitializeWindow(win); win.ShowAll(); Application.Run(); } private static void InitializeWindow(Window window) { var hBox = new HBox(); window.Add(hBox); } } }
程序运行起来之后,窗体似乎没什么变化?别紧张,前面说过盒子是不可见的容器,并不会被直接展示出来,现在添加一个按钮进去看看:
private static void InitializeWindow(Window window) { var hBox = new HBox(); window.Add(hBox); hBox.Add(new Button("我是按钮")); }
加入按钮以后:
按钮被展示了出来,而且充满了整个窗体。目前的效果和直接在 GtkWindows 下新增 GtkButton 差不多,再多加几个按钮看看:
private static void InitializeWindow(Window window) { var hBox = new HBox(); window.Add(hBox); for (int i = 0; i < 4; i++) { hBox.Add(new Button($"我是按钮{i + 1}号")); } }
我们使用 for 循环添加的 4 个按钮被整齐的展示在了窗体上,每个按钮的宽度和高度都相同,而且整个 GtkWindow 的宽度变长了。回想一下上面提到过的:整齐和塞满。GtkWindow 可以根据其内部控件的需要拓展自身的大小,最大宽度不能超过 32767 像素。如果尝试调整窗体的大小会怎么样?我帮你尝试了一下,调整到最小尺寸时是这个模样:
从 Widget 中派生的 Button 继承了 Widget 的 SetSizeRequest 方法,这个方法可以设置部件所需的布局大小。在实际实践中,通常也不会使用 HBox 的 Add 方法来添加子部件,而是采用功能更加强大的 PackStart 或 PackEnd 方法。与 Add 方法相比,这两个方法提供了更丰富的控制。
PackStart:
public void PackStart(Widget child, bool expand, bool fill, uint padding)
PackEnd:
public void PackEnd(Widget child, bool expand, bool fill, uint padding)
改动一下代码,看看会发生什么事情:
private static void InitializeWindow(Window window) { var hBox = new HBox(); window.Add(hBox); for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox.PackStart(btn, false, false, 3); } }
以上代码设置了按钮的大小,并且规定了内边距:
可以看到按钮的宽度设置 padding 都生效了,按钮变得宽窄不一且与容器和临近元素拉开了距离。那么 PackStart 和 PackEnd 有什么区别呢?为了可以更清楚的解释这个问题,需要引入 VBox 。VBox 和 HBox 相似,两者最大的区别就是 VBox 的子部件是从上到下排列的,HBox 是从左到右。
试试使用 VBox 把窗体分为上下两个部分,并对比一下 PackStar 和 PackEnd 有什么区别:
private static void InitializeWindow(Window window) { var vBox = new VBox(); window.Add(vBox); var hBox1 = new HBox(); var hBox2 = new HBox(); vBox.PackStart(hBox1, false, false, 5); vBox.PackStart(hBox2, false, false, 5); for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox1.PackStart(btn, false, false, 3); } for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox2.PackEnd(btn, false, false, 3); } }
以上的代码创建了一个 VBox 并添加到了 Window 中,之后新建了两个 HBox 分别命名为 hBox1 和 hBox2 并添加到了 VBox 中,这个很像我们在组装鞋架,现在,这个鞋架现在有两层。两个 for 循环分别向 hBox1 和 hBox2 中添加了 4 个按钮,按钮添加的顺序和文字均相同,唯一的区别是分别调用了 PackStart 和 PackEnd 两种不同的方法:
程序运行后可以看到,第一行按钮的显示顺序和第二行是相反的。
如何理解这个事情呢?想象一下你现在要把鞋子放在鞋架的其中一层上,并且你只从剩余空间的左边或者右边开始摆放鞋子,那么 PackStart 相当于把这个鞋子摆在了剩余空间的最左边,而 PackEnd 则相当于摆放在最右边。VBox 中对应的方法也类似,只是将方向换成了上和下,验证一下看看:
private static void InitializeWindow(Window window) { var vBox = new VBox(); window.Add(vBox); var hBox1 = new HBox(); var hBox2 = new HBox(); vBox.PackStart(hBox1, false, false, 5); vBox.PackStart(hBox2, false, false, 5); for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox1.PackStart(btn, false, false, 3); } for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox2.PackEnd(btn, false, false, 3); } var btnBtm = new Button("Bottom"); btnBtm.SetSizeRequest(50, 50); vBox.PackEnd(btnBtm,false,false,5); }
以上代码新创建了一个名为 btnBtm 的按钮,并通过 PackEnd 方法将其添加到了 VBox 中:
可以看到,名为 btnBtm 的按钮确实被放在了剩余空间的底部。
3、expand 和 fill 参数
PackStart 和 PackEnd 方法还有两个参数,分别为 expand 和 fill 。
expand 就是当 Box 给我们的 Widget 分配了额外的空间后,我们的 Widget 会占住这个空间,不会让给别人。
fill 就是当 expand 为 TRUE 的时候,我们不仅占用 Box 给我们分配的空间,而且会把自己的界面扩大到这个空间上。
所以,简单来说,expand = TRUE, fill = FALSE 就是占住空间但是控件本身大小不变;两个都是TRUE,就是不仅占住空间而且控件也会变得和这个空间一样大;expand = FALSE,fill就没了意义。
GtkHBox 中只要 expand 是TRUE,那么,水平方向上一定 fill,所以 fill 参数此时只影响垂直上是否 fill 。
GtkVBox 中只要 expand 是TRUE,那么,垂直方向上一定 fill,所以 fill 参数此时只影响水平上是否 fill 。
以上关于 expand 和 fill 参数的解释引用自:Super的博客 中的博文 GTK Box(hbox&vbox)的expand和fill两个属性的实践理解 。是否有些难以理解?言语都很苍白,幸好我们可以用代码说话。
现在,可以将窗体宽度调大一些,比如 500*300 :
win.SetDefaultSize(500, 300);
窗体变大后,我们的 HBox 中出现了空白:
目前代码中 expand 和 fill 参数均为 false 所以并不会出现“占满”和“填充”的现象。当 expand = false 时,fill 参数没有意义,那么我们就可以尝试下,当 expand = true 时,fill 分别为 true 和 false 时在 HBox 下会是什么效果:
private static void InitializeWindow(Window window) { var vBox = new VBox(); window.Add(vBox); var hBox1 = new HBox(); var hBox2 = new HBox(); vBox.PackStart(hBox1, false, false, 5); vBox.PackStart(hBox2, false, false, 5); for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox1.PackStart(btn, true, true, 3); } for (int i = 0; i < 4; i++) { var btn = new Button($"{i + 1}"); btn.SetSizeRequest(30 * (i + 1), 50); hBox2.PackEnd(btn, true, false, 3); } var btnBtm = new Button("Bottom"); btnBtm.SetSizeRequest(50, 50); vBox.PackEnd(btnBtm, false, false, 5); }
代码中将第一行的按钮设置为:沾满并填充,将第二行的按钮设置为:沾满不填充。
可以看到,“占满”并“填充”的按钮将所有的空白区域全部占领了,而不填充的按钮则会在分配给他的空白区域中居中。如果尝试将 btnBtm 设置为“占满”且“填充”,那么画风会是下面这样: