学无先后达者为师!
不忘初心,砥砺前行。

【.NET Core 跨平台 GUI 开发】第二篇:Gtk# 布局入门,初识HBox 和 VBox

这是 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 程序只能显示一个部件,那么就太尴尬了,只能包含一个部件的窗口也没有意义。其实我们有很多现成的部件可以解决这个尴尬的问题,常见的比如:VBoxHBoxTable

在 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 设置为“占满”且“填充”,那么画风会是下面这样:

赞(8) 打赏
未经允许不得转载:码农很忙 » 【.NET Core 跨平台 GUI 开发】第二篇:Gtk# 布局入门,初识HBox 和 VBox

评论 抢沙发

给作者买杯咖啡

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册