TornadoFX编程指南,第7章,布局和菜单

译自《Layouts and Menus

布局和菜单

复杂的UI需要很多控件。 这些控件可能需要使用设置策略(set policies),进行分组,定位并调整大小。 幸运的是,TornadoFX简化了JavaFX自带的许多布局(layouts),并且具有自己的专有Form布局。

TornadoFX还具有类型安全的构建器(type-safe builders),以高度结构化,声明性的方式创建菜单。 使用常规JavaFX代码构建菜单尤其繁琐,而Kotlin在这个部分真的很出色。

布局构建器(Builders for Layouts)

布局(Layouts)将控制分组,并设置有关其大小和定位行为的策略(policies)。 在技​​术上,布局(layouts)本身就是控件,因此您可以在布局中嵌套布局。 这对于构建复杂的UI来说至关重要,而TornadoFX可以通过明显地显示嵌套关系来简化UI代码的维护。

VBox

VBox按照控件在其块中声明的顺序垂直堆叠控件(图7.1)。

vbox {
    button("Button 1").setOnAction {
        println("Button 1 Pressed")
    }
    button("Button 2").setOnAction {
        println("Button 2 Pressed")
    }
}
图7.1

您还可以在子控件的块中调用vboxConstraints()来更改VBox的边距(margin)和垂直增长(vertical growing)行为。

vbox {
    button("Button 1") {
         vboxConstraints {
            marginBottom = 20.0
            vGrow = Priority.ALWAYS
          }
    }
    button("Button 2")
}

您可以用vGrow速记扩展属性(shorthand extension property),而无需调用vboxConstraints()

vbox {
    button("Button 1") {
           vGrow = Priority.ALWAYS
    }
    button("Button 2")
}

HBox

HBox行为几乎与VBox相同,但是按照其块中声明的顺序从左到右水平堆叠所有控件。

hbox {
    button("Button 1").setOnAction {
        println("Button 1 Pressed")
    }
    button("Button 2").setOnAction {
        println("Button 2 Pressed")
    }
}
图7.2

您还可以在子控件的块内调用hboxConstraints()来更改HBox的边距(margin)和横向增长(horizontal growing behaviors)行为。

hbox {
    button("Button 1") {
        hboxConstraints {
                marginRight = 20.0
          hGrow = Priority.ALWAYS
      }
    }
    button("Button 2")
}

您可以使用hGrow缩写扩展属性(shorthand extension property),而不调用hboxConstraints()

hbox {
    button("Button 1") {
          hGrow = Priority.ALWAYS
    }
  button("Button 2")
}

FlowPane

FlowPane控件从左至右布局控件,并在到达边界时将其转到下一行。 例如,假设您添加了100个按钮到FlowPane (图7.3)。你会注意到它只是从左到右布置按钮,当它耗尽空间时,它移动到“下一行”。

flowpane {
   for (i in 1..100) {
        button(i.toString()) {
            setOnAction { println("You pressed button $i") }
        }
   }
}
图7.3

请注意,当您调整窗口大小时, FlowLayout将重新布局按钮,以使它们都可以适合(图7.4)

图7.4

FlowLayout不经常使用,因为处理大量控件通常是简单的,但它可以在某些情况下派上用场,也可以在其他布局中使用。

BorderPane

BorderPane是一个非常有用的布局,将控件分为5个区域: topleftbottomrightcenter 。 可以使用这些区域的两个或更多来来保存控件,很容易地构建许多UI(图7.5)。

borderpane {
    top = label("TOP") {
        useMaxWidth = true
        style {
            backgroundColor = Color.RED
        }
    }

    bottom = label("BOTTOM") {
        useMaxWidth = true
        style {
            backgroundColor = Color.BLUE
        }
    }

    left = label("LEFT") {
        useMaxWidth = true
        style {
            backgroundColor = Color.GREEN
        }
    }

    right = label("RIGHT") {
        useMaxWidth = true
        style {
            backgroundColor = Color.PURPLE
        }
    }

    center = label("CENTER") {
        useMaxWidth = true
        style {
            backgroundColor = Color.YELLOW
        }
    }
}
图7.5

您会注意到topbottom区域占据整个水平空间,而leftcenterright必须共享可用的水平空间。 但center有权获得任何额外的可用空间(垂直和水平),使其成为像TableView这样的大型控件的理想选择。 例如,您可以在left区域中垂直堆叠一些按钮,并将TableView放在center区域(图7.6)。

borderpane {
    left = vbox {
        button("REFRESH")
        button("COMMIT")
    }

    center  = tableview<Person> {
        items = listOf(
                Person("Joe Thompson", 33),
                Person("Sam Smith", 29),
                Person("Nancy Reams", 41)
        ).observable()

        column("NAME",Person::name)
        column("AGE",Person::age)
    }
}
图7.6

BorderPane是您可能想要经常使用的布局,因为它简化了许多复杂的UI。top区域通常用于保存MenuBarbottom区域通常保持某种状态栏。 您已经看到center保持焦点控制,如TableViewleftright保持侧面板与任何不适合放在MenuBar中的外围控件(如按钮或工具栏) 。 本节稍后将介绍菜单。

表单生成器

TornadoFX有一个有用的Form控件来处理大量的用户输入。 拥有多个输入字段以获取用户信息是常见的,JavaFX没有内置的解决方案来简化此操作。 为了解决这个问题,TornadoFX有一个构建器来声明具有任意数量字段的Form (图7.7)。

form {
    fieldset("Personal Info") {
        field("First Name") {
            textfield()
        }
        field("Last Name") {
            textfield()
        }
        field("Birthday") {
            datepicker()
        }
    }
    fieldset("Contact") {
        field("Phone") {
            textfield()
        }
        field("Email") {
            textfield()
        }
    }
    button("Commit") {
        action { println("Wrote to database!")}
    }
}
图7.7-1
图7.7-2

是不是很棒? 您可以为每个字段指定一个或多个控件, Form将为您呈现分组和标签。

您也可以选择在输入字段之上布置标签:

fieldset("FieldSet", labelPosition = VERTICAL)

每个field都包含一个内有标签的容器,另一个容器用于在其中添加的输入字段。 默认情况下,输入字段的容器是HBox ,这意味着单个字段中的多个输入将彼此水平相邻布置。 您可以指定一个字段的orientation参数,使其在多个输入之间相互上下排列。 垂直取向的另一个用例是允许输入随着垂直方向的扩展而增长。 这对于在表单中显示TextAreas非常方便:

form {
    fieldset("Feedback Form", labelPosition = VERTICAL) {
        field("Comment", VERTICAL) {
            textarea {
                prefRowCount = 5
                vgrow = Priority.ALWAYS
            }
        }
        buttonbar {
            button("Send")
        }
    }
}
图7.8

上面的示例还使用buttonbar构建器创建一个没有标签的特殊字段,同时保留标签缩进,使按钮在输入框下排列。

您将每个输入绑定到一个模型(model),您可以将控件布局的渲染留给Form。 因此,如果可能,您可能希望在GridPane上使用它,接下来我们将介绍。

Form内嵌套布局(Nesting layouts inside a Form)

您可以使用您选择的任何布局容器来包装fieldetsfields,以创建复杂的表单布局。

form {
    hbox(20) {
        fieldset("Left FieldSet") {
            hbox(20) {
                vbox {
                    field("Field l1a") { textfield() }
                    field("Field l2a") { textfield() }
                }
                vbox {
                    field("Field l1b") { textfield() }
                    field("Field l2b") { textfield() }
                }
            }
        }
        fieldset("Right FieldSet") {
            hbox(20) {
                vbox {
                    field("Field r1a") { textfield() }
                    field("Field r2a") { textfield() }
                }
                vbox {
                    field("Field r1b") { textfield() }
                    field("Field r2b") { textfield() }
                }
            }
        }
    }
}
图7.9

GridPane

如果你想对控件的布局进行细致的管理, GridPane会给你很多的。 当然,它需要更多的配置和代码样板。 在继续使用GridPane之前,您可能需要考虑使用为您抽象了布局配置的Form或其他布局。

使用GridPane的一种方法是声明每row的内容。 对于任何给定的Node您可以调用其gridpaneConstraints来配置该Node的各种GridPane行为,例如margincolumnSpan (图7.10)

 gridpane {
     row {
         button("North") {
             useMaxWidth = true
             gridpaneConstraints {
                 marginBottom = 10.0
                 columnSpan = 2
             }
         }
     }
    row {
        button("West")
        button("East")
    }
    row {
        button("South") {
            useMaxWidth = true
            gridpaneConstraints {
                marginTop = 10.0
                columnSpan = 2
            }
        }
    }
}
图7.11

请注意,在每行之间,如果在其gridpaneConstraints内分别为“North”和“South”按钮的marginBottommarginTop声明了每行之间的距离为10.0 。

或者,您可以显式指定每个Node的列/行索引位置,而不是声明每row的控件。 这将完成我们之前建立的精确布局,但是使用列/行索引来规范。 它有点冗长,但它可以更加明确地控制控件的位置。

gridpane {
     button("North") {
         useMaxWidth = true
         gridpaneConstraints {
             columnRowIndex(0,0)
             marginBottom = 10.0
             columnSpan = 2
         }
     }
    button("West").gridpaneConstraints {
        columnRowIndex(0,1)
    }
    button("East").gridpaneConstraints {
        columnRowIndex(1,1)
    }

    button("South") {
        useMaxWidth = true
        gridpaneConstraints {
            columnRowIndex(0,2)
            marginTop = 10.0
            columnSpan = 2
        }
    }
}

这些都是您可以在给定Node上修改的gridpaneConstraints属性。 一些表示为可以赋值的简单属性,而其他属性可以通过函数赋值。

属性 描述
columnIndex:Int 给定控件的列索引
rowIndex:Int 给定控件的行索引
columnRowIndex(columnIndex:Int,rowIndex:Int) 指定行和列索引
columnSpan:Int 控件占用的列数
rowSpan:Int 控制占用的行数
hGrow:Priority 水平增长优先
vGrow:Priority 垂直成长优先
vhGrow:Priority 为vGrow和hGrow指定相同的优先级
fillHeight:Boolean 设置Node是否填充其区域的高度
fillWidth:Boolean 设置Node是否填充其区域的宽度
fillHeightWidth:Boolean 设置Node是否填充高度和宽度的区域
hAlignment:HPos 水平对齐政策
vAlignment:VPos 垂直对齐策略
margin:Int Node所有四边的边距
marginBottom:Int Node底部的边距
marginTop:Int Node顶端的边距
marginLeft:Int Node左侧的左边距
marginRight:Int Node右侧的右边距
marginLeftRight:Int Node的右边距和左边距
marginTopBottom:Int Node的顶部和底部边距

另外,如果需要配置ColumnConstraints,可以在GridPane本身的GridPane Node上调用gridpaneColumnConstraints ,也可以调用constraintsForColumn(columnIndex)

gridpane {
    row {
        button("Left") {
            gridpaneColumnConstraints {
                percentWidth = 25.0
            }
        }

        button("Middle")
        button("Right")
    }
    constraintsForColumn(1).percentWidth = 50.0
}

StackPane

一个StackPane是一个布局,您将不太经常使用。 对于您添加的每个控件,它将逐字地堆叠在一起(literally stack them ),而不是像VBox,但是字面上覆盖它们(literally overlay them)。

例如,您可以创建一个“BOTTOM” Button并在其顶部放置一个“TOP” Button 。 您声明控件的顺序将以相同的顺序从底部到顶部添加它们(图7.10)。

class MyView: View() {

    override val root =  stackpane {
        button("BOTTOM") {
           useMaxHeight = true
           useMaxWidth = true
           style {
               backgroundColor += Color.AQUAMARINE
               fontSize = 40.0.px
           }
        }

        button("TOP") {
            style {
                backgroundColor += Color.WHITE
            }
        }
    }
}
图7.11

TabPane

TabPane创建一个用“tab”分隔的不同屏幕的UI。 这允许通过点击相应的选项卡快速轻松地切换不同的屏幕(图7.11)。 您可以声明一个tabpane(),然后根据需要声明尽可能多的tab()实例。 对于每个tab()函数,通过Tab的名称和父Node控件来填充它。

 tabpane {
    tab("Screen 1", VBox()) {
        button("Button 1")
        button("Button 2")
    }
    tab("Screen 2", HBox()) {
        button("Button 3")
        button("Button 4")
    }
}
图7.12

TabePane是分隔屏幕并组织大量控件的有效工具。 语法有些简洁,足以在tab()块中声明像TableView这样的复杂控件(图7.13)。

tabpane {
  tab("Screen 1", VBox()) {
      button("Button 1")
      button("Button 2")
  }
  tab("Screen 2", HBox()) {
      tableview<Person> {

          items = listOf(
              Person(1,"Samantha Stuart",LocalDate.of(1981,12,4)),
              Person(2,"Tom Marks",LocalDate.of(2001,1,23)),
              Person(3,"Stuart Gills",LocalDate.of(1989,5,23)),
              Person(3,"Nicole Williams",LocalDate.of(1998,8,11))
          ).observable()

          column("ID",Person::id)
          column("Name", Person::name)
          column("Birthday", Person::birthday)
          column("Age",Person::age)
      }
  }
}
图7.13

像许多构建器一样, TabPane有几个属性可以调整其选项卡的行为。 例如,您可以调用tabClosingPolicy来去掉选项卡上的“X”按钮,从而无法关闭。

class MyView: View() {
    override val root =  tabpane {
        tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE

        tab("Screen 1", VBox()) {
            button("Button 1")
            button("Button 2")
        }
        tab("Screen 2", HBox()) {
            button("Button 3")
            button("Button 4")
        }
    }
}

菜单构建器

以严格面向对象的方式构建菜单可能很麻烦。 但是使用类型安全的构建器,Kotlin的函数结构可以直观地声明嵌套的菜单层次结构。

MenuBar,Menu和MenuItem

使用导航菜单在用户界面上保留大量命令并不常见。 例如, BorderPanetop区域通常是MenuBar所在的地方。 在那里可以轻松添加菜单和子菜单(图7.12)。

menubar {
   menu("File") {
       menu("Connect") {
           item("Facebook")
           item("Twitter")
       }
       item("Save")
       item("Quit")
   }
   menu("Edit") {
       item("Copy")
       item("Paste")
   }
}
图7.14

您还可以选择提供键盘快捷键,图形以及每个item()action函数参数,以指定选定操作时的动作(图7.14)。

menubar {
     menu("File") {
         menu("Connect") {
             item("Facebook", graphic = fbIcon).action { println("Connecting Facebook!") }
             item("Twitter", graphic = twIcon).action { println("Connecting Twitter!") }
         }
         item("Save","Shortcut+S").action {
             println("Saving!")
         }
         menu("Quit","Shortcut+Q").action {
             println("Quitting!")
         }
     }
     menu("Edit") {
         item("Copy","Shortcut+C").action {
             println("Copying!")
         }
         item("Paste","Shortcut+V").action {
             println("Pasting!")
         }
     }
 }

分隔线(Separators)

您可以在Menu的两个items之间声明一个separator()来创建一个分隔线。 这有助于给Menu分组命令并将它们分开(图7.15)。

 menu("File") {
     menu("Connect") {
         item("Facebook")
         item("Twitter")
     }
     separator()
     item("Save","Shortcut+S") {
         println("Saving!")
     }
     item("Quit","Shortcut+Q") {
         println("Quitting!")
     }
 }
图7.15

上下文菜单(ContextMenu)

JavaFX中的大多数控件都有一个contextMenu属性,您可以在其中指定ContextMenu实例。 这是一个在右键单击控件时弹出的Menu

一个ContextMenu有函数可以添加MenuMenuItem实例,就像MenuBar一样 。 例如,将一个ContextMenu添加到TableView<Person>是有帮助的,并提供要在表格记录上完成的命令(图7.16)。 有一个名为contextmenu的构建器将构建一个ContextMenu并将其赋值给控件的contextMenu属性。

tableview(persons) {
     column("ID", Person::id)
     column("Name", Person::name)
     column("Birthday", Person::birthday)
     column("Age", Person::age)

     contextmenu {
         item("Send Email").action {
             selectedItem?.apply { println("Sending Email to $name") }
         }
         item("Change Status").action {
             selectedItem?.apply { println("Changing Status for $name") }
         }
     }
 }
图7.16

注意还有可用的RadioMenuItemCheckMenuItem这些MenuItem变体。

当菜单被选为op块参数时,menuitem构建器采取动作来执行。 不幸的是,这破坏了其他构建器,其中op块对构建器创建的元素进行操作。 因此,引入item构建器作为替代,您可以在item本身上操作,因此您必须调用setOnAction来赋值动作。menuitem构建器没有被弃用,因为它以比item构建器更简洁的方式解决了常见情况。

ListMenu

TornadoFX带有一个列表菜单(ListMenu),其行为和看起来更像是一个典型的基于ul/li的HTML5菜单。

以下代码示例显示如何使用构建器模式的ListMenu

listmenu(theme = "blue") {
    item(text = "Contacts", graphic = Styles.contactsIcon()) {
        // Marks this item as active.
        activeItem = this
        whenSelected { /* Do some action */ }
    }
    item(text = "Projects", graphic = Styles.projectsIcon())
    item(text = "Settings", graphic = Styles.settingsIcon())
}

以下属性可用于配置ListMenu :

Css属性(Css Properties)

伪类(Pseudo Classes)

看看ListMenu的默认样式表。

项目(Item)

item构建器允许以非常方便的方式为ListMenu创建items。 支持以下语法:

item("SomeText", graphic = SomeNode, tag = SomeObject) {
    // Marks this item as active.
    activeItem = this

    // Do some action when selected
    whenSelected { /* Action */ }
}

填充父容器(Filling the parent container)

useMaxWidth属性可用于水平填充父容器。 useMaxHeight属性将垂直填充父容器。 这些属性实际上适用于所有节点,但对ListMenu特别有用。

Squeezebox

JavaFX具有手风琴(Accordion)控件,可让您将一组TilePanes组合在一起,形成手风琴控件(accordion of controls)。 JavaFX手风琴(Accordion)只允许您一次打开单个手风琴折叠(a single accordion fold),并且还有一些其他缺点。 为了解决这个问题,TornadoFX附带了SqueezeBox组件,其行为看起来非常类似于手风琴(Accordion),同时提供了一些增强功能。

squeezebox {
    fold("Customer Editor", expanded = true) {
        form {
            fieldset("Customer Details") {
                field("Name") { textfield() }
                field("Password") { textfield() }
            }
        }
    }
    fold("Some other editor", expanded = true) {
        stackpane {
            label("Nothing here")
        }
    }
}
图7.17

一个Squeezebox显示两个折叠,两者都默认扩展。

您可以通过将multiselect = false传递给构建器构造函数,使SqueezeBox仅允许在任何给定时间展开单个折叠。

您可以选择通过单击标题窗格右侧的十字架(clicking a cross in the right corner of the title pane)而允许折叠成为可关闭的(allow folds to be closable)。 您可以通过将closeable = true传递给fold构建器,从而以每折为单位启用关闭按钮(enable the close buttons on a per fold basis)。

squeezebox {
    fold("Customer Editor", expanded = true, closeable = true) {
        form {
            fieldset("Customer Details") {
                field("Name") { textfield() }
                field("Password") { textfield() }
            }
        }
    }
    fold("Some other editor", closeable = true) {
        stackpane {
            label("Nothing here")
        }
    }
}
图7.18

这个SqueezeBox有可关闭的折叠(closeable folds)。

closeable属性当然可以结合expanded

SqueezeBoxAccordion之间的另一个重要区别就是分配空间(distributes overflowing space)的方式。 手风琴(Accordion)将垂直延伸以填充其父容器,并将当前打开的任何折叠推至底部。 如果父容器非常大,这将创建一个不自然的查看视图。 在这方面,挤压框(SqueezeBox)可能默认就是您想要的,但您可以添加fillHeight = true以获得类似于Accordion的外观。

您可以像您一样创建一个TitlePane样式一样来创建SqueezeBox样式。 关闭按钮有一个名为close-buttoncss类,容器有一个名为squeeze-boxcss类。

Drawer

抽屉(Drawer)是一个非常像TabPane的导航组件,但它在父容器的任一侧的垂直或水平放置的按钮栏中组织每个抽屉项目。 它类似于许多流行的业务应用程序和IDE中发现的工具抽屉(tool drawers)。 当选择项目时,项目的内容将显示在跨越控件的高度或宽度的内容区域中的按钮旁边或上方/下方,以及内容的首选宽度或高度,具体取决于是否将其停靠在父级的垂直或水平方面。 在多重选择(multiselect)模式下,您甚至可以同时打开多个抽屉物品,让它们共享它们之间的空间。 它们将始终按照相应按钮的顺序打开。

class DrawerView : View("TornadoFX Info Browser") {
    override val root = drawer {
        item("Screencasts", expanded = true) {
            webview {
                prefWidth = 470.0
                engine.userAgent = iPhoneUserAgent
                engine.load(TornadoFXScreencastsURI)
            }
        }
        item("Links") {
            listview(links) {
                cellFormat { link ->
                    graphic = hyperlink(link.name) {
                        setOnAction {
                            hostServices.showDocument(link.uri)
                        }
                    }
                }
            }
        }
        item("People") {
            tableview(people) {
                column("Name", Person::name)
                column("Nick", Person::nick)
            }
        }
   }

   class Link(val name: String, val uri: String)
   class Person(val name: String, val nick: String)

   // Sample data variables left out (iPhoneUserAgent, TornadoFXScreencastsURI, people and links)
}
图7.19

抽屉可以配置为显示右侧的按钮,您可以选择同时支持打开多个抽屉物品。 当以多重选择模式运行时,内容上方会出现一个标题,这将有助于区分内容区域中的项目。 您可以使用布尔的showHeader参数控制标题外观。 当启用多重选择时,它将默认为true,否则为false。

drawer(side = Side.RIGHT, multiselect = true) {
    // Everything else is identical
}
图7.20

带有右侧按钮的抽屉,多选模式和标题窗格。

当抽屉被添加到某物的旁边时,您可以选择抽屉的内容区域是否应替换其旁边的节点(默认)或浮动。 floatingContent属性默认为false,导致Drawer替换其旁边的内容。

您可以使用DrawermaxContentSizefixedContentSize属性进一步控制内容区域的大小。 根据dockingSide,这些属性将限制内容区域的宽度或高度。

Workspace功能内置支持抽屉控件。 任何WorkspaceleftDrawerrightDrawerbottomDrawer属性将允许您将抽屉项目bottomDrawer在其中。 在“工作区(Workspace)”一章中了解更多信息。

转换可观察列表项并绑定到布局(Converting observable list items and binding to layouts)

TODO

总结

到目前为止,您应该拥有能力可以使用布局,标签窗格以及其他控件来管理控件。 将这些与数据控件结合使用,您应该可以在一小部分时间内转换UI。

当涉及到构建器时,您已经达到了顶峰(top of the peak),并拥有所需要的所有成效。 剩下的所有内容都是图表和形状(charts and shapes),我们将在接下来的两章中介绍。

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

推荐阅读更多精彩内容