[譯文] GTK 進(jìn)階技術(shù) —— 自定義容器(Container)

原文地址:Advanced GTK Techniques


譯文將 Widget 理解為為“控件”,Container 理解為“容器”,Method 理解為“方法/函數(shù)”。


自定義容器

對(duì)于 GTK 而言,(控件的)空間分配并不是一個(gè)十分困難的任務(wù)。GTK 中的控件會(huì)根據(jù)從屬關(guān)系自下而上給出自身的偏好尺寸,包含它們的容器據(jù)此為子控件做出分配,并計(jì)算出自己的偏好尺寸,然后繼續(xù)向上層反饋。這一過(guò)程通過(guò)調(diào)用 gtk_widget_get_preferred_height()gtk_widget_get_preferred_width() 即可實(shí)現(xiàn);此外,GTK 還提供了其它函數(shù)方便人們通過(guò)獲取的寬度確定高度,亦或通過(guò)高度確定寬度。知曉了各個(gè)控件的偏好尺寸后,容器便會(huì)自上而下開(kāi)始分配工作,通過(guò) gtk_widget_size_allocate() 為其子控件開(kāi)辟空間。由此可知,如果開(kāi)發(fā)者重寫了這個(gè)函數(shù),便可實(shí)現(xiàn)容器對(duì)控件尺寸分配方法的控制。

上述過(guò)程與 GTK 2 的工作原理并不相符,有關(guān) GTK 2 的講解可以查閱網(wǎng)絡(luò)上的其它資源。

在大多數(shù)情形中——比如創(chuàng)建一個(gè)組合控件或者向已有的容器添加功能,你可以通過(guò)編寫一個(gè)容器的子類來(lái)實(shí)現(xiàn)自定義容器的自定義(例如編寫一個(gè) GtkGrid 的子類),這樣就不需要直接面對(duì)如何處理控件尺寸分配或者其它棘手的問(wèn)題,而是通通交給它的父類容器解決。但是如果需要一個(gè)能夠以不同于現(xiàn)有 GTK 容器處理方式的部件時(shí)又該怎么辦?這就需要我們創(chuàng)造一個(gè)新的容器類型,并自行設(shè)計(jì)它的尺寸分配算法。

可惜的是,目前采用這種方法的實(shí)例往往過(guò)于復(fù)雜,不利于教學(xué),因此我們將會(huì)編寫一個(gè)幾乎沒(méi)什么用的容器,它將所有的子控件放置在一個(gè)“表格”中,每個(gè)控件占用一個(gè)格子,頗有種 80 年代影視剪輯中分屏效果的風(fēng)味,就像 Heat of the Moment 一樣。這個(gè)容器會(huì)查找整數(shù) n 使得:

式中 V 表示容器中可見(jiàn)的子控件數(shù)量。這個(gè)容器將會(huì)被分割成 n × n 的表格,從左向右、從上向下依次填充。

頭文件

我們將創(chuàng)建一個(gè)名為 PSquare 的容器,它是 GtkContainer 的子類。PSquare 的頭文件 psquare.h 在之前的教學(xué)中已有涉及,就不在此贅述。頭文件僅會(huì)輸出兩個(gè)函數(shù):p_square_get_type()p_square_new(),代碼中的有趣之處在于重寫 GtkContainer 函數(shù)的部分。

類樣板

我們將簡(jiǎn)單介紹文件 psquare.c 中涉及到 GObject 的知識(shí)點(diǎn)。了解 G_DEFINE_TYPE 宏和 g_type_class_add_private() 的機(jī)制對(duì)我們而言有益無(wú)害,而且并不是每篇教程都會(huì)講這些。

// psquare/psquare.c
G_DEFINE_TYPE(PSquare, p_square, GTK_TYPE_CONTAINER);

G_DEFINE_TYPE 宏非常有用,它可以免除你很多的輸入工作。它為 PSquare 創(chuàng)建了在頭文件中聲明過(guò)的 p_square_get_type() 函數(shù)、定義了名為 p_square_parent_class 的局部變量;此外,還聲明了 p_square_class_init()p_square_init() 兩個(gè)局部函數(shù)。

// psquare/psquare.c
static void
p_square_class_init(PSquareClass *klass)
{
    /* Override GtkWidget methods */
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    widget_class->get_preferred_width = p_square_get_preferred_width;
    widget_class->get_preferred_height = p_square_get_preferred_height;
    widget_class->size_allocate = p_square_size_allocate;

    /* Override GtkContainer methods */
    GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
    container_class->child_type = p_square_child_type;
    container_class->add = p_square_add;
    container_class->remove = p_square_remove;
    container_class->forall = p_square_forall;

    /* Add private indirection member */
    g_type_class_add_private(klass, sizeof(PSquarePrivate));
}

在函數(shù) p_square_class_init() 中引入 parent_class 變量是為了避免每次鏈接父類時(shí)都調(diào)用 g_type_class_peek_parent() (常出現(xiàn)于 finalize 過(guò)程中)。你需要為自己類和實(shí)例編寫初始化函數(shù)。

類初始化函數(shù) p_square_class_init 重寫了多個(gè)父類的方法。如果你不熟悉這種 “GObject 式”的實(shí)現(xiàn)過(guò)程,不妨現(xiàn)在就了解一下。我們的測(cè)試程序不會(huì)用到類中的 forallchild_type 方法,但是瀏覽 gtkcontainer.c 后可以看到二者在父類中均被設(shè)置為 NULL ——不折不扣的“虛”函數(shù),所以也需要重寫。

為了找出父類的哪些方法需要被重寫,你不得不瀏覽 GtkContainer 的源代碼,從 API 文檔中并不能很好地做出分辨。

類初始化函數(shù)做的另外一件事是注冊(cè)一個(gè) PSquare 的私有成員,這通常是隱藏類中實(shí)現(xiàn)方法細(xì)節(jié)最有效的方式。代碼中的 g_type_class_add_private() 會(huì)定義私有成員的結(jié)構(gòu),并通知 GObject 為類的每一個(gè)實(shí)例分配私有成員內(nèi)存。這樣一來(lái),每個(gè)實(shí)例就都有了私有成員部分,而你只能通過(guò)編寫接口函數(shù)實(shí)現(xiàn)對(duì)它們的訪問(wèn)。私有結(jié)構(gòu)通過(guò)如下代碼定義:

// psquare/psquare.c
#define P_SQUARE_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), P_SQUARE_TYPE, PSquarePrivate))

typedef struct _PSquarePrivate PSquarePrivate;

struct _PSquarePrivate
{
    GList *children;
};

或許將這些代碼專門放入類似于名為 psquare-private.h 的文件中是個(gè)更好的選擇。這樣,任何包含它的文件將可以訪問(wèn)私有成員,類似于 C++ 中的友元數(shù)據(jù)。如果你編寫的類體積巨大,這么做也有助于明晰結(jié)構(gòu)。

P_SQUARE_PRIVATE() 宏可以返回 PSquare 的私有成員。編寫的例子中僅有一個(gè)私有成員數(shù)據(jù),我們需要自己去維護(hù)它。

譯者注:在很多實(shí)際的 GTK 程序中,更為常見(jiàn)的私有成員實(shí)現(xiàn)方法為在 psquare/psquare.c 中輸入:G_DEFINE_TYPE_WITH_PRIVATE(PSquare, p_square, GTK_TYPE_CONTAINER); 。這將直接聲明一個(gè)帶私有成員 PSquarePrivate 的類,無(wú)需再在類初始化函數(shù)中引入 g_type_class_add_private()。
通過(guò)這一宏定義類后,在函數(shù)中調(diào)取私有成員可直接使用 PSquarePrivate *priv = p_square_get_instance_private() 函數(shù),因此 P_SQUARE_PRIVATE() 也不需要了。

實(shí)例初始化函數(shù) p_square_init() 負(fù)責(zé)將公有和私有成員實(shí)例化:

// psquare/psquare.c
static void
p_square_init(PSquare *square)
{
    gtk_widget_set_has_window(GTK_WIDGET(square), FALSE);

    /* Initialize private members */
    PSquarePrivate *priv = P_SQUARE_PRIVATE(square);
    priv->children = NULL;
}

gtk_widget_set_has_window() 值為 FALSE 意味著 PSquare 不具有 GdkWindow,因此我們不會(huì)自己完成繪制操作,而只是負(fù)責(zé)組織容器的子控件。正如代碼中展示的,私有成員 children 初始化為 NULL——一個(gè)空的 GList。

還有一件約定俗成的事情是你需要知道的:編寫的控件通常通過(guò) XXXX_new(p_square_new())函數(shù)產(chǎn)生,其返回值為一個(gè)新的類的實(shí)例,并強(qiáng)制轉(zhuǎn)換為 GtkWidget 格式:

// psquare/psquare.c
GtkWidget *
p_square_new()
{
    return GTK_WIDGET(g_object_new(p_square_get_type(), NULL));
}

重寫 GtkContainer 方法

現(xiàn)在我們介紹如何重寫父類的方法。GtkContainer 是一個(gè)非?;镜念悾虼宋覀兙蛷乃胧?。

child_type 方法用于確定哪些類型的子控件可以被裝入容器中,這一項(xiàng)我們只要返回 GTK_TYPE_WIDGET 就好了。GtkContainerClass 結(jié)構(gòu)中的 forall 指針并未同一個(gè) GtkContainer 方法對(duì)應(yīng),而是在程序運(yùn)行過(guò)程中用于與 gtk_container_forall()gtk_container_foreach() 對(duì)接。這兩個(gè)函數(shù)的前者意味著將對(duì)容器中包括“內(nèi)部”成員在內(nèi)的所有成員執(zhí)行操作(譯者注:“內(nèi)部”成員指并非由用戶添加的控件,而是由容器創(chuàng)建,例如對(duì)話框中自動(dòng)生成的按鈕),后者則會(huì)跳過(guò)“內(nèi)部”成員。forall 函數(shù)中會(huì)讀取一個(gè)是否包括“內(nèi)部”成員的標(biāo)記,我們的 PSquare 沒(méi)有“內(nèi)部”成員,所以直接忽略它即可。

// psquare/psquare.c
static void
p_square_forall(GtkContainer *container, gboolean include_internals, GtkCallback callback, gpointer callback_data)
{
    PSquarePrivate *priv = P_SQUARE_PRIVATE(container);
    g_list_foreach(priv->children, (GFunc)callback, callback_data);
}

下一項(xiàng)工作是實(shí)現(xiàn) addremove 方法。需要注意的是,你并非一定需要重寫這兩個(gè)方法,僅在例如容器需要與父類不同的添加控件方法時(shí)才會(huì)這么做。GtkContainer 類中這兩個(gè)方法默認(rèn)情況下不會(huì)做任何事,而是發(fā)出一個(gè)“方法未被實(shí)現(xiàn)”的警告。

// psquare/psquare.c
static void
p_square_add(GtkContainer *container, GtkWidget *widget)
{
    g_return_if_fail(container || P_IS_SQUARE(container));
    g_return_if_fail(widget || GTK_IS_WIDGET(widget));
    g_return_if_fail(gtk_widget_get_parent(widget) == NULL);

    PSquarePrivate *priv = P_SQUARE_PRIVATE(container);

    /* Add the child to our list of children. 
     * All the real work is done in gtk_widget_set_parent(). */
    priv->children = g_list_append(priv->children, widget);
    gtk_widget_set_parent(widget, GTK_WIDGET(container));

    /* Queue redraw */
    if(gtk_widget_get_visible(widget))
        gtk_widget_queue_resize(GTK_WIDGET(container));
}

你可能會(huì)對(duì) p_square_add 的簡(jiǎn)短感到驚訝,但正如注釋中所述,所有核心的工作都在 gtk_widget_set_parent() 中完成:添加引用計(jì)數(shù)、重繪控件、觸發(fā)相應(yīng)的信號(hào),等等。這之后我們?nèi)孕枰孕欣L制容器。移除子控件的方法與此類似,一切核心工作都在 gtk_widget_unparent() 中完成。

// psquare/psquare.c
static void
p_square_remove(GtkContainer *container, GtkWidget *widget)
{
    g_return_if_fail(container || P_IS_SQUARE(container));
    g_return_if_fail(widget || GTK_IS_WIDGET(widget));

    PSquarePrivate *priv = P_SQUARE_PRIVATE(container);

    /* Remove the child from our list of children. 
     * Again, all the real work is done in gtk_widget_unparent(). */
    GList *link = g_list_find(priv->children, widget);
    if(link) {
        gboolean was_visible = gtk_widget_get_visible(widget);
        gtk_widget_unparent(widget);

        priv->children = g_list_delete_link(priv->children, link);

        /* Queue redraw */
        if(was_visible)
            gtk_widget_queue_resize(GTK_WIDGET(container));
    }
}

這里有一點(diǎn)讓人欣慰:我們不需要編寫 p_square_destroy() 或者 p_square_finalize() 方法,GtkContainer 會(huì)在容器析構(gòu)時(shí)自動(dòng)處理這些。

譯者注:一般而言,在實(shí)例創(chuàng)建過(guò)程中,GtkWidget 通常被 GtkContainer 包含,層層遞進(jìn),GTK 會(huì)自動(dòng)處理它們的釋放過(guò)程,不需要用戶花費(fèi)精力;而對(duì)于用戶在 XxxxPrivate 中添加的其它私有成員,則可能需要手動(dòng)管理內(nèi)存釋放的問(wèn)題。

空間規(guī)劃

我們已經(jīng)完成了一切準(zhǔn)備工作,現(xiàn)在我們開(kāi)始重點(diǎn)部分。我們需要確定容器中每添加一個(gè)格子時(shí)容器的寬度(或高度)需要增加多少。本示例中,格子的寬度與容器中最寬的子控件相等。格子的高度將采用相似的方式確定。此外,我們也需要考慮 GtkContainerborder_width 屬性帶來(lái)的影響。

// psquare/psquare.c
static void p_square_get_preferred_width(GtkWidget *widget, int *minimal, int *natural);
static void p_square_get_preferred_height(GtkWidget *widget, int *minimal, int *natural);

容器的類中有兩個(gè)返回自身尺寸的函數(shù):一個(gè)返回寬度,一個(gè)返回高度。它們的參數(shù)中有兩個(gè)指向整數(shù)的指針,一個(gè)必須填寫尺寸的最小值,另一個(gè)填寫默認(rèn)值。

在本示例中,這兩個(gè)函數(shù)的運(yùn)作方式近乎相同,所以我們將它們封裝到一個(gè)函數(shù)中—— get_size()。這個(gè)函數(shù)有一個(gè)額外的 GtkOrientation 參數(shù),當(dāng)我們需要輸入寬度時(shí),將其設(shè)置為 GTK_ORIENTATION_HORIZONTAL,需要輸入高度時(shí)則設(shè)置為 GTK_ORIENTATION_VERTICAL

// psquare/psquare.c
static void get_size(PSquare *self, GtkOrientation direction, int *minimal, int *natural);

我們調(diào)用兩次 get_size() 函數(shù),分別設(shè)置格子的寬度和高度。隨后,我們計(jì)算容器的尺寸,也就是表格的行數(shù)和列數(shù),由于容器是一個(gè)正方形,所以用一個(gè) n_groups 表示表格的邊長(zhǎng)即可。如果 n_groups 為零,就直接返回。

隨后,我們遍歷各個(gè)子控件,通過(guò) get_group_sizes() 獲取它們的尺寸。容器的最小尺寸和默認(rèn)尺寸都存儲(chǔ)在 GtkRequestedSize 結(jié)構(gòu)中。一旦獲得了這些信息,我們就將它們累加,得到總大小。

// psquare/psquare.c
static void
get_size(PSquare *self, GtkOrientation direction, int *minimal, int *natural)
{
    /* Start with the container's border width */
    unsigned border_width =
        gtk_container_get_border_width(GTK_CONTAINER(self));
    *minimal = *natural = border_width * 2;

    /* Find out how many children there are */
    unsigned n_groups = get_n_columns_and_rows(self);
    if(n_groups == 0)
        return;

    /* Find out how much space they want */
    GtkRequestedSize *sizes = get_group_sizes(self, direction, n_groups);

    /* Add the widths and pass that as the container's width */
    unsigned count;
    for(count = 0; count < n_groups; count++) {
        *minimal += sizes[count].minimum_size;
        *natural += sizes[count].natural_size;
    }

    g_free(sizes);
}

函數(shù) get_n_columns_and_rows() 如下所示。注意容器中可能會(huì)有不可見(jiàn)的子控件,PSquare 并不會(huì)為它們分配位置,所以 n_groups 有可能在還有子控件時(shí)被設(shè)置為零。

// psquare/psquare.c
unsigned
get_n_columns_and_rows(PSquare *self)
{
    PSquarePrivate *priv = P_SQUARE_PRIVATE(self);

    /* Count the visible children */
    unsigned n_visible_children = 0;
    g_list_foreach(priv->children, (GFunc)count_visible_children,
        &n_visible_children);
    if(n_visible_children == 0)
        return 0;

    /* Calculate the number of columns */
    return (unsigned)ceil(sqrt((double)n_visible_children));
}
// psquare/psquare.c
/* Convenience function for counting the number of visible
 * children, for use with g_list_foreach() */
static void
count_visible_children(GtkWidget *widget, unsigned *n_visible_children)
{
    if(gtk_widget_get_visible(widget))
        (*n_visible_children)++;
}

接下來(lái)我們講解 get_group_sizes(),這是空間計(jì)算最為核心的工作。

首先,我們創(chuàng)建一個(gè)數(shù)組來(lái)存儲(chǔ)所有的控件組信息(即列的寬度或行的高度)。在獲取了所有子控件的偏好尺寸后,我們找出每組信息中的最大值。

// psquare/psquare.c
GtkRequestedSize *sizes = g_new0(GtkRequestedSize, n_groups);

隨后我們?cè)俅伪闅v所有子控件,詢問(wèn)它們的偏好尺寸。如果設(shè)置了寬度模式,我們獲取寬度,反之獲取高度。這一過(guò)程中我們引入一個(gè)變量 group_num,它在寬度模式中記錄當(dāng)前子控件的行號(hào),在高度模式中記錄列號(hào)。

隨后我們獲取每組尺寸的最大值:如果子控件的尺寸大于所屬組的值,就把組值替換。完成這些工作后,我們返回 sizes 數(shù)組。

// psquare/psquare.c
unsigned count = 0;
GList *iter;
for(iter = priv->children; iter; iter = g_list_next(iter)) {
    if(!gtk_widget_get_visible(iter->data))
        continue;

    int child_minimal, child_natural;
    unsigned group_num;
    if(direction == GTK_ORIENTATION_HORIZONTAL) {
        gtk_widget_get_preferred_width(iter->data,
            &child_minimal, &child_natural);
        group_num = count % n_groups;
    } else {
        gtk_widget_get_preferred_height(iter->data,
            &child_minimal, &child_natural);
        group_num = count / n_groups;
    }

    sizes[group_num].minimum_size =
        MAX(child_minimal, sizes[group_num].minimum_size);
    sizes[group_num].natural_size =
        MAX(child_natural, sizes[group_num].natural_size);

    count++;
}

空間分配

size_allocate 方法的執(zhí)行與上述過(guò)程相似。它接收一個(gè) GtkAllocation 結(jié)構(gòu)體,其中包含了控件必須具有的尺寸大小信息。

// psquare/psquare.c
static void p_square_size_allocate(GtkWidget *widget, GtkAllocation *allocation);

我們首先需要將尺寸分配信息寫入控件:

// psquare/psquare.c
gtk_widget_set_allocation(widget, allocation);

下一步的工作與 size_request 方法十分相似。首先我們獲取行數(shù)和列數(shù):

// psquare/psquare.c
unsigned n_columns, n_rows;
n_columns = n_rows = get_n_columns_and_rows(P_SQUARE(widget));
if(n_columns == 0)
    return;

n_columnsn_rows 事實(shí)上是相同的,但為了便于理解還是在代碼中區(qū)別對(duì)待。再一次地,如果容器中沒(méi)有可見(jiàn)的子控件,我們就直接返回。

現(xiàn)在我們需要將容器開(kāi)辟的空間分配給它的子控件。我們的策略是在將額外的寬度平均分配到每一列,將額外的高度平均分配到每一行。如果剩余空間過(guò)小,就反過(guò)來(lái)從每行或每列抽取相等的長(zhǎng)度分配給新的控件。首先我們計(jì)算出每列每行空間富余或缺少的長(zhǎng)度,并用兩個(gè)變量來(lái)表示: extra_widthextra_height。它們的初始值為總寬度/高度,隨后將會(huì)減去我們需要的長(zhǎng)度。首先需要減去的是容器的邊界寬度:

// psquare/psquare.c
unsigned border_width =
    gtk_container_get_border_width(GTK_CONTAINER(widget));
int extra_width = allocation->width - 2 * border_width;
int extra_height = allocation->height - 2 * border_width;

隨后我們通過(guò)上面提到的 get_group_sizes() 函數(shù)獲得每列的寬度。通過(guò)計(jì)算,我們?yōu)槊苛刑砑宇~外長(zhǎng)度(這個(gè)值可能是負(fù)數(shù)),得出每列的實(shí)際寬度。這些工作在函數(shù) distribute_extra_space() 中完成,我們將在稍后介紹它。

完成列寬的工作后,我們繼續(xù)計(jì)算每行的高度。二者過(guò)程近乎相同,除了用一個(gè)名為 get_group_sizes_for_sizes() 的函數(shù)替換 get_group_sizes(),它能夠?yàn)橐阎獙挾确峙溥m宜的高度,反之亦然。我們?cè)诖瞬辉俳o出這個(gè)函數(shù)的代碼,它與 get_group_sizes() 的區(qū)別在于將 gtk_widget_get_preferred_height()..._width() 替換成了 gtk_widget_get_preferred_height_for_width()..._width_for_height()。你可以在 psquare.c 文件中查看它的實(shí)現(xiàn)。

// psquare/psquare.c
/* Follow the same procedure as in the size request to get 
 * the ideal sizes of each column */
GtkRequestedSize *widths = get_group_sizes(P_SQUARE(widget),
    GTK_ORIENTATION_HORIZONTAL, n_columns);

/* Distribute the extra space per column (can be negative) */
unsigned count;
for(count = 0; count < n_columns; count++)
    extra_width -= widths[count].minimum_size;
distribute_extra_space(P_SQUARE(widget), widths, extra_width, n_columns);

/* Follow the same procedure for height,
 * now that we know the width */
GtkRequestedSize *heights = get_group_sizes_for_sizes(P_SQUARE(widget),
    GTK_ORIENTATION_VERTICAL, widths, n_rows);

/* Distribute the extra space per row (can be negative) */
for(count = 0; count < n_rows; count++)
    extra_height -= heights[count].minimum_size;
distribute_extra_space(P_SQUARE(widget), heights, extra_height, n_rows);

接下來(lái)的函數(shù)名為 distribute_extra_space(),它將額外空間(可能為負(fù)數(shù))分配給每個(gè)組(即行或列)。GTK 已經(jīng)提供了一個(gè)便捷的函數(shù),可以用于賦予一組控件額外的空間(但必須為非負(fù)數(shù)),以使得每個(gè)控件都能獲取盡可能多的空間。當(dāng)額外空間值非負(fù)時(shí),我們可以直接調(diào)用這個(gè)函數(shù)。如果仍有空間剩余,或者第一個(gè)位置空間不足,我們將會(huì)把富余或短缺的空間均分給組內(nèi)每個(gè)成員。需要注意我們不能把子控件安放在一個(gè)空間小于零的格子中,因此如果出現(xiàn)了這樣的情況,我們需要從其它行列中“挪用”一些像素,直到值達(dá)到零為止。

// psquare/psquare.c
static void
distribute_extra_space(PSquare *self, GtkRequestedSize *sizes,
    int extra_space, unsigned n_groups)
{
    if(extra_space > 0) {
        extra_space = gtk_distribute_natural_allocation(extra_space,
            n_groups, sizes);
    }

    unsigned count;
    int extra_per_group = extra_space / (int)n_groups;

    for(count = 0; count < n_groups; count++) {
        sizes[count].minimum_size += extra_per_group;
        /* If this results in a negative width, redistribute
         * pixels from other nonzero-width columns to this one */
        if(sizes[count].minimum_size < 0) {
            unsigned count2;
            for(count2 = (count + 1) % n_groups;
                sizes[count].minimum_size < 0;
                count2++, count2 %= n_groups)
            {
                if(count2 == count || sizes[count2].minimum_size < 0)
                    continue;
                sizes[count2].minimum_size--;
                sizes[count].minimum_size++;
            }
        }
    }
}

回到本章核心——空間分配。我們?cè)O(shè)立一個(gè)點(diǎn) (x, y) 用于記錄下一個(gè)子控件放置位置的左上點(diǎn)座標(biāo)。需要注意的是 GtkAllocation 結(jié)構(gòu)中的 xy 記錄的是整個(gè)屏幕的座標(biāo)(原作者是這樣認(rèn)為的),而不是從容器的左上角開(kāi)始計(jì)算,所以你需要做出一些調(diào)整。

// psquare/psquare.c
/* Start positioning the items at the container's origin,
 * less the border width */
int x = allocation->x + border_width;
int y = allocation->y + border_width;

隨后我們?cè)俅伪闅v可見(jiàn)的子控件。我們會(huì)在棧中為每個(gè)子控件分配一個(gè)包含了座標(biāo) (x, y) 以及寬高信息的 GtkAllocation 結(jié)構(gòu)體,然后通過(guò) gtk_widget_size_allocate() 函數(shù)應(yīng)用這些設(shè)置。接著,我們需要更新下一個(gè)子控件的座標(biāo),在完成了一行的分配后還需下移一行并返回新行左上角座標(biāo)。

// psquare/psquare.c
count = 0;
GList *iter;
for(iter = priv->children; iter; iter = g_list_next(iter)) {
    if(!gtk_widget_get_visible(iter->data))
        continue;

    /* Give the child its allocation */
    GtkAllocation child_allocation;
    child_allocation.x = x;
    child_allocation.y = y;
    child_allocation.width = widths[count % n_columns].minimum_size;
    child_allocation.height = heights[count / n_columns].minimum_size;
    gtk_widget_size_allocate(iter->data, &child_allocation);

    /* Advance the x coordinate */
    x += child_allocation.width;
    count++;
    /* If we've moved to the next row, return the x coordinate 
     * to the left, and advance the y coordinate */
    if(count % n_columns == 0) {
        x = allocation->x + border_width;
        y += child_allocation.height;
    }
}

范例程序

我們將提供一個(gè)簡(jiǎn)單的范例程序來(lái)展示 PSquare 容器的效果。你可以下載 test-psquare.c 和它的 Makefile 自行編譯。我們首先創(chuàng)建一個(gè) toplevel 窗口,并用 gtk_widget_set_size_request() 設(shè)定它的大小(不過(guò)在程序運(yùn)行中你仍能夠擴(kuò)大窗口)。在一開(kāi)始只有少量控件時(shí),容器中的空間相對(duì)充足;隨著控件越來(lái)越多地?cái)D進(jìn)來(lái),它們就不得不開(kāi)始縮小尺寸以適應(yīng)新的布局。

我們?yōu)榇翱谔砑恿艘粋€(gè)具有新增控件按鈕和移除控件按鈕工具欄,這樣可以手動(dòng)添加控件(gtk_container_add())或者移除最近添加的一個(gè)控件(gtk_container_remove())。

編譯并運(yùn)行程序后,你就可以嘗試添加很多控件。你會(huì)注意到,一個(gè) GtkEntry 會(huì)占用很多寬度,所以包含了 GtkEntry 的列往往會(huì)在空間緊張時(shí)將其它列占用,不過(guò),即使分配了負(fù)的尺寸,理論上你也并不會(huì)收到警告。

練習(xí)

  • 修改 p_square_size_allocate(),使得它在分配空間時(shí)能夠根據(jù)每列占總寬度比例進(jìn)行規(guī)劃。比如,如果空間不足,原先較寬的列將騰出較多的空間,而較窄的列則騰出較少的空間。

  • 修改 PSquare 的子控件排列方法,使得其能從左上角沿順時(shí)針?lè)较蚍胖米涌丶?/p>

  • PSquare 的子控件屬性添加實(shí)現(xiàn)方法。例如,fill-horizontalfill-vertical 屬性,它們用于確定是否要將容器為其分配的空間沿橫向或縱向全部占用。你也可以新建一個(gè)將二者封裝在一起的屬性來(lái)決定是否讓控件占用整個(gè)空間。對(duì)于這個(gè)任務(wù),你可以使用兩個(gè)對(duì)齊選項(xiàng),也可以用一個(gè) GtkAnchorType 來(lái)實(shí)現(xiàn)。


文章許可協(xié)議:Attribution-NonCommercial-ShareAlike 3.0 Unported

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容