[译文] GTK 进阶技术 —— 自定义容器(Container)

原文地址:Advanced GTK Techniques


译文将 Widget 理解为为“控件”,Container 理解为“容器”,Method 理解为“方法/函数”。


自定义容器

对于 GTK 而言,(控件的)空间分配并不是一个十分困难的任务。GTK 中的控件会根据从属关系自下而上给出自身的偏好尺寸,包含它们的容器据此为子控件做出分配,并计算出自己的偏好尺寸,然后继续向上层反馈。这一过程通过调用 gtk_widget_get_preferred_height()gtk_widget_get_preferred_width() 即可实现;此外,GTK 还提供了其它函数方便人们通过获取的宽度确定高度,亦或通过高度确定宽度。知晓了各个控件的偏好尺寸后,容器便会自上而下开始分配工作,通过 gtk_widget_size_allocate() 为其子控件开辟空间。由此可知,如果开发者重写了这个函数,便可实现容器对控件尺寸分配方法的控制。

上述过程与 GTK 2 的工作原理并不相符,有关 GTK 2 的讲解可以查阅网络上的其它资源。

在大多数情形中——比如创建一个组合控件或者向已有的容器添加功能,你可以通过编写一个容器的子类来实现自定义容器的自定义(例如编写一个 GtkGrid 的子类),这样就不需要直接面对如何处理控件尺寸分配或者其它棘手的问题,而是通通交给它的父类容器解决。但是如果需要一个能够以不同于现有 GTK 容器处理方式的部件时又该怎么办?这就需要我们创造一个新的容器类型,并自行设计它的尺寸分配算法。

可惜的是,目前采用这种方法的实例往往过于复杂,不利于教学,因此我们将会编写一个几乎没什么用的容器,它将所有的子控件放置在一个“表格”中,每个控件占用一个格子,颇有种 80 年代影视剪辑中分屏效果的风味,就像 Heat of the Moment 一样。这个容器会查找整数 n 使得:

式中 V 表示容器中可见的子控件数量。这个容器将会被分割成 n × n 的表格,从左向右、从上向下依次填充。

头文件

我们将创建一个名为 PSquare 的容器,它是 GtkContainer 的子类。PSquare 的头文件 psquare.h 在之前的教学中已有涉及,就不在此赘述。头文件仅会输出两个函数:p_square_get_type()p_square_new(),代码中的有趣之处在于重写 GtkContainer 函数的部分。

类样板

我们将简单介绍文件 psquare.c 中涉及到 GObject 的知识点。了解 G_DEFINE_TYPE 宏和 g_type_class_add_private() 的机制对我们而言有益无害,而且并不是每篇教程都会讲这些。

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

G_DEFINE_TYPE 宏非常有用,它可以免除你很多的输入工作。它为 PSquare 创建了在头文件中声明过的 p_square_get_type() 函数、定义了名为 p_square_parent_class 的局部变量;此外,还声明了 p_square_class_init()p_square_init() 两个局部函数。

// 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));
}

在函数 p_square_class_init() 中引入 parent_class 变量是为了避免每次链接父类时都调用 g_type_class_peek_parent() (常出现于 finalize 过程中)。你需要为自己类和实例编写初始化函数。

类初始化函数 p_square_class_init 重写了多个父类的方法。如果你不熟悉这种 “GObject 式”的实现过程,不妨现在就了解一下。我们的测试程序不会用到类中的 forallchild_type 方法,但是浏览 gtkcontainer.c 后可以看到二者在父类中均被设置为 NULL ——不折不扣的“虚”函数,所以也需要重写。

为了找出父类的哪些方法需要被重写,你不得不浏览 GtkContainer 的源代码,从 API 文档中并不能很好地做出分辨。

类初始化函数做的另外一件事是注册一个 PSquare 的私有成员,这通常是隐藏类中实现方法细节最有效的方式。代码中的 g_type_class_add_private() 会定义私有成员的结构,并通知 GObject 为类的每一个实例分配私有成员内存。这样一来,每个实例就都有了私有成员部分,而你只能通过编写接口函数实现对它们的访问。私有结构通过如下代码定义:

// 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 的文件中是个更好的选择。这样,任何包含它的文件将可以访问私有成员,类似于 C++ 中的友元数据。如果你编写的类体积巨大,这么做也有助于明晰结构。

P_SQUARE_PRIVATE() 宏可以返回 PSquare 的私有成员。编写的例子中仅有一个私有成员数据,我们需要自己去维护它。

译者注:在很多实际的 GTK 程序中,更为常见的私有成员实现方法为在 psquare/psquare.c 中输入:G_DEFINE_TYPE_WITH_PRIVATE(PSquare, p_square, GTK_TYPE_CONTAINER); 。这将直接声明一个带私有成员 PSquarePrivate 的类,无需再在类初始化函数中引入 g_type_class_add_private()
通过这一宏定义类后,在函数中调取私有成员可直接使用 PSquarePrivate *priv = p_square_get_instance_private() 函数,因此 P_SQUARE_PRIVATE() 也不需要了。

实例初始化函数 p_square_init() 负责将公有和私有成员实例化:

// 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,因此我们不会自己完成绘制操作,而只是负责组织容器的子控件。正如代码中展示的,私有成员 children 初始化为 NULL——一个空的 GList

还有一件约定俗成的事情是你需要知道的:编写的控件通常通过 XXXX_new(p_square_new())函数产生,其返回值为一个新的类的实例,并强制转换为 GtkWidget 格式:

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

重写 GtkContainer 方法

现在我们介绍如何重写父类的方法。GtkContainer 是一个非常基本的类,因此我们就从它入手。

child_type 方法用于确定哪些类型的子控件可以被装入容器中,这一项我们只要返回 GTK_TYPE_WIDGET 就好了。GtkContainerClass 结构中的 forall 指针并未同一个 GtkContainer 方法对应,而是在程序运行过程中用于与 gtk_container_forall()gtk_container_foreach() 对接。这两个函数的前者意味着将对容器中包括“内部”成员在内的所有成员执行操作(译者注:“内部”成员指并非由用户添加的控件,而是由容器创建,例如对话框中自动生成的按钮),后者则会跳过“内部”成员。forall 函数中会读取一个是否包括“内部”成员的标记,我们的 PSquare 没有“内部”成员,所以直接忽略它即可。

// 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);
}

下一项工作是实现 addremove 方法。需要注意的是,你并非一定需要重写这两个方法,仅在例如容器需要与父类不同的添加控件方法时才会这么做。GtkContainer 类中这两个方法默认情况下不会做任何事,而是发出一个“方法未被实现”的警告。

// 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));
}

你可能会对 p_square_add 的简短感到惊讶,但正如注释中所述,所有核心的工作都在 gtk_widget_set_parent() 中完成:添加引用计数、重绘控件、触发相应的信号,等等。这之后我们仍需要自行绘制容器。移除子控件的方法与此类似,一切核心工作都在 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));
    }
}

这里有一点让人欣慰:我们不需要编写 p_square_destroy() 或者 p_square_finalize() 方法,GtkContainer 会在容器析构时自动处理这些。

译者注:一般而言,在实例创建过程中,GtkWidget 通常被 GtkContainer 包含,层层递进,GTK 会自动处理它们的释放过程,不需要用户花费精力;而对于用户在 XxxxPrivate 中添加的其它私有成员,则可能需要手动管理内存释放的问题。

空间规划

我们已经完成了一切准备工作,现在我们开始重点部分。我们需要确定容器中每添加一个格子时容器的宽度(或高度)需要增加多少。本示例中,格子的宽度与容器中最宽的子控件相等。格子的高度将采用相似的方式确定。此外,我们也需要考虑 GtkContainerborder_width 属性带来的影响。

// 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);

容器的类中有两个返回自身尺寸的函数:一个返回宽度,一个返回高度。它们的参数中有两个指向整数的指针,一个必须填写尺寸的最小值,另一个填写默认值。

在本示例中,这两个函数的运作方式近乎相同,所以我们将它们封装到一个函数中—— get_size()。这个函数有一个额外的 GtkOrientation 参数,当我们需要输入宽度时,将其设置为 GTK_ORIENTATION_HORIZONTAL,需要输入高度时则设置为 GTK_ORIENTATION_VERTICAL

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

我们调用两次 get_size() 函数,分别设置格子的宽度和高度。随后,我们计算容器的尺寸,也就是表格的行数和列数,由于容器是一个正方形,所以用一个 n_groups 表示表格的边长即可。如果 n_groups 为零,就直接返回。

随后,我们遍历各个子控件,通过 get_group_sizes() 获取它们的尺寸。容器的最小尺寸和默认尺寸都存储在 GtkRequestedSize 结构中。一旦获得了这些信息,我们就将它们累加,得到总大小。

// 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);
}

函数 get_n_columns_and_rows() 如下所示。注意容器中可能会有不可见的子控件,PSquare 并不会为它们分配位置,所以 n_groups 有可能在还有子控件时被设置为零。

// 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)++;
}

接下来我们讲解 get_group_sizes(),这是空间计算最为核心的工作。

首先,我们创建一个数组来存储所有的控件组信息(即列的宽度或行的高度)。在获取了所有子控件的偏好尺寸后,我们找出每组信息中的最大值。

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

随后我们再次遍历所有子控件,询问它们的偏好尺寸。如果设置了宽度模式,我们获取宽度,反之获取高度。这一过程中我们引入一个变量 group_num,它在宽度模式中记录当前子控件的行号,在高度模式中记录列号。

随后我们获取每组尺寸的最大值:如果子控件的尺寸大于所属组的值,就把组值替换。完成这些工作后,我们返回 sizes 数组。

// 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 方法的执行与上述过程相似。它接收一个 GtkAllocation 结构体,其中包含了控件必须具有的尺寸大小信息。

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

我们首先需要将尺寸分配信息写入控件:

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

下一步的工作与 size_request 方法十分相似。首先我们获取行数和列数:

// 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 事实上是相同的,但为了便于理解还是在代码中区别对待。再一次地,如果容器中没有可见的子控件,我们就直接返回。

现在我们需要将容器开辟的空间分配给它的子控件。我们的策略是在将额外的宽度平均分配到每一列,将额外的高度平均分配到每一行。如果剩余空间过小,就反过来从每行或每列抽取相等的长度分配给新的控件。首先我们计算出每列每行空间富余或缺少的长度,并用两个变量来表示: extra_widthextra_height。它们的初始值为总宽度/高度,随后将会减去我们需要的长度。首先需要减去的是容器的边界宽度:

// 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;

随后我们通过上面提到的 get_group_sizes() 函数获得每列的宽度。通过计算,我们为每列添加额外长度(这个值可能是负数),得出每列的实际宽度。这些工作在函数 distribute_extra_space() 中完成,我们将在稍后介绍它。

完成列宽的工作后,我们继续计算每行的高度。二者过程近乎相同,除了用一个名为 get_group_sizes_for_sizes() 的函数替换 get_group_sizes(),它能够为已知宽度分配适宜的高度,反之亦然。我们在此不再给出这个函数的代码,它与 get_group_sizes() 的区别在于将 gtk_widget_get_preferred_height()..._width() 替换成了 gtk_widget_get_preferred_height_for_width()..._width_for_height()。你可以在 psquare.c 文件中查看它的实现。

// 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);

接下来的函数名为 distribute_extra_space(),它将额外空间(可能为负数)分配给每个组(即行或列)。GTK 已经提供了一个便捷的函数,可以用于赋予一组控件额外的空间(但必须为非负数),以使得每个控件都能获取尽可能多的空间。当额外空间值非负时,我们可以直接调用这个函数。如果仍有空间剩余,或者第一个位置空间不足,我们将会把富余或短缺的空间均分给组内每个成员。需要注意我们不能把子控件安放在一个空间小于零的格子中,因此如果出现了这样的情况,我们需要从其它行列中“挪用”一些像素,直到值达到零为止。

// 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++;
            }
        }
    }
}

回到本章核心——空间分配。我们设立一个点 (x, y) 用于记录下一个子控件放置位置的左上点座标。需要注意的是 GtkAllocation 结构中的 xy 记录的是整个屏幕的座标(原作者是这样认为的),而不是从容器的左上角开始计算,所以你需要做出一些调整。

// 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;

随后我们再次遍历可见的子控件。我们会在栈中为每个子控件分配一个包含了座标 (x, y) 以及宽高信息的 GtkAllocation 结构体,然后通过 gtk_widget_size_allocate() 函数应用这些设置。接着,我们需要更新下一个子控件的座标,在完成了一行的分配后还需下移一行并返回新行左上角座标。

// 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;
    }
}

范例程序

我们将提供一个简单的范例程序来展示 PSquare 容器的效果。你可以下载 test-psquare.c 和它的 Makefile 自行编译。我们首先创建一个 toplevel 窗口,并用 gtk_widget_set_size_request() 设定它的大小(不过在程序运行中你仍能够扩大窗口)。在一开始只有少量控件时,容器中的空间相对充足;随着控件越来越多地挤进来,它们就不得不开始缩小尺寸以适应新的布局。

我们为窗口添加了一个具有新增控件按钮和移除控件按钮工具栏,这样可以手动添加控件(gtk_container_add())或者移除最近添加的一个控件(gtk_container_remove())。

编译并运行程序后,你就可以尝试添加很多控件。你会注意到,一个 GtkEntry 会占用很多宽度,所以包含了 GtkEntry 的列往往会在空间紧张时将其它列占用,不过,即使分配了负的尺寸,理论上你也并不会收到警告。

练习

  • 修改 p_square_size_allocate(),使得它在分配空间时能够根据每列占总宽度比例进行规划。比如,如果空间不足,原先较宽的列将腾出较多的空间,而较窄的列则腾出较少的空间。

  • 修改 PSquare 的子控件排列方法,使得其能从左上角沿顺时针方向放置子控件。

  • PSquare 的子控件属性添加实现方法。例如,fill-horizontalfill-vertical 属性,它们用于确定是否要将容器为其分配的空间沿横向或纵向全部占用。你也可以新建一个将二者封装在一起的属性来决定是否让控件占用整个空间。对于这个任务,你可以使用两个对齐选项,也可以用一个 GtkAnchorType 来实现。


文章许可协议:Attribution-NonCommercial-ShareAlike 3.0 Unported

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容