TornadoFX編程指南,第7章,布局和菜單

譯自《Layouts and Menus

布局和菜單

復雜的UI需要很多控件。 這些控件可能需要使用設置策略(set policies),進行分組,定位并調(diào)整大小。 幸運的是,TornadoFX簡化了JavaFX自帶的許多布局(layouts),并且具有自己的專有Form布局。

TornadoFX還具有類型安全的構(gòu)建器(type-safe builders),以高度結(jié)構(gòu)化,聲明性的方式創(chuàng)建菜單。 使用常規(guī)JavaFX代碼構(gòu)建菜單尤其繁瑣,而Kotlin在這個部分真的很出色。

布局構(gòu)建器(Builders for Layouts)

布局(Layouts)將控制分組,并設置有關(guān)其大小和定位行為的策略(policies)。 在技??術(shù)上,布局(layouts)本身就是控件,因此您可以在布局中嵌套布局。 這對于構(gòu)建復雜的UI來說至關(guān)重要,而TornadoFX可以通過明顯地顯示嵌套關(guān)系來簡化UI代碼的維護。

VBox

VBox按照控件在其塊中聲明的順序垂直堆疊控件(圖7.1)。

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

您還可以在子控件的塊中調(diào)用vboxConstraints()來更改VBox的邊距(margin)和垂直增長(vertical growing)行為。

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

您可以用vGrow速記擴展屬性(shorthand extension property),而無需調(diào)用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

您還可以在子控件的塊內(nèi)調(diào)用hboxConstraints()來更改HBox的邊距(margin)和橫向增長(horizontal growing behaviors)行為。

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

您可以使用hGrow縮寫擴展屬性(shorthand extension property),而不調(diào)用hboxConstraints() 。

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

FlowPane

FlowPane控件從左至右布局控件,并在到達邊界時將其轉(zhuǎn)到下一行。 例如,假設您添加了100個按鈕到FlowPane (圖7.3)。你會注意到它只是從左到右布置按鈕,當它耗盡空間時,它移動到“下一行”。

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

請注意,當您調(diào)整窗口大小時, FlowLayout將重新布局按鈕,以使它們都可以適合(圖7.4)

圖7.4

FlowLayout不經(jīng)常使用,因為處理大量控件通常是簡單的,但它可以在某些情況下派上用場,也可以在其他布局中使用。

BorderPane

BorderPane是一個非常有用的布局,將控件分為5個區(qū)域: top , leftbottom , rightcenter 。 可以使用這些區(qū)域的兩個或更多來來保存控件,很容易地構(gòu)建許多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區(qū)域占據(jù)整個水平空間,而left , center , right必須共享可用的水平空間。 但center有權(quán)獲得任何額外的可用空間(垂直和水平),使其成為像TableView這樣的大型控件的理想選擇。 例如,您可以在left區(qū)域中垂直堆疊一些按鈕,并將TableView放在center區(qū)域(圖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是您可能想要經(jīng)常使用的布局,因為它簡化了許多復雜的UI。top區(qū)域通常用于保存MenuBar , bottom區(qū)域通常保持某種狀態(tài)欄。 您已經(jīng)看到center保持焦點控制,如TableView , leftright保持側(cè)面板與任何不適合放在MenuBar中的外圍控件(如按鈕或工具欄) 。 本節(jié)稍后將介紹菜單。

表單生成器

TornadoFX有一個有用的Form控件來處理大量的用戶輸入。 擁有多個輸入字段以獲取用戶信息是常見的,JavaFX沒有內(nèi)置的解決方案來簡化此操作。 為了解決這個問題,TornadoFX有一個構(gòu)建器來聲明具有任意數(shù)量字段的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將為您呈現(xiàn)分組和標簽。

您也可以選擇在輸入字段之上布置標簽:

fieldset("FieldSet", labelPosition = VERTICAL)

每個field都包含一個內(nèi)有標簽的容器,另一個容器用于在其中添加的輸入字段。 默認情況下,輸入字段的容器是HBox ,這意味著單個字段中的多個輸入將彼此水平相鄰布置。 您可以指定一個字段的orientation參數(shù),使其在多個輸入之間相互上下排列。 垂直取向的另一個用例是允許輸入隨著垂直方向的擴展而增長。 這對于在表單中顯示TextAreas非常方便:

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

上面的示例還使用buttonbar構(gòu)建器創(chuàng)建一個沒有標簽的特殊字段,同時保留標簽縮進,使按鈕在輸入框下排列。

您將每個輸入綁定到一個模型(model),您可以將控件布局的渲染留給Form。 因此,如果可能,您可能希望在GridPane上使用它,接下來我們將介紹。

Form內(nèi)嵌套布局(Nesting layouts inside a Form)

您可以使用您選擇的任何布局容器來包裝fieldetsfields,以創(chuàng)建復雜的表單布局。

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會給你很多的。 當然,它需要更多的配置和代碼樣板。 在繼續(xù)使用GridPane之前,您可能需要考慮使用為您抽象了布局配置的Form或其他布局。

使用GridPane的一種方法是聲明每row的內(nèi)容。 對于任何給定的Node您可以調(diào)用其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內(nèi)分別為“North”和“South”按鈕的marginBottommarginTop聲明了每行之間的距離為10.0 。

或者,您可以顯式指定每個Node的列/行索引位置,而不是聲明每row的控件。 這將完成我們之前建立的精確布局,但是使用列/行索引來規(guī)范。 它有點冗長,但它可以更加明確地控制控件的位置。

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屬性。 一些表示為可以賦值的簡單屬性,而其他屬性可以通過函數(shù)賦值。

屬性 描述
columnIndex:Int 給定控件的列索引
rowIndex:Int 給定控件的行索引
columnRowIndex(columnIndex:Int,rowIndex:Int) 指定行和列索引
columnSpan:Int 控件占用的列數(shù)
rowSpan:Int 控制占用的行數(shù)
hGrow:Priority 水平增長優(yōu)先
vGrow:Priority 垂直成長優(yōu)先
vhGrow:Priority 為vGrow和hGrow指定相同的優(yōu)先級
fillHeight:Boolean 設置Node是否填充其區(qū)域的高度
fillWidth:Boolean 設置Node是否填充其區(qū)域的寬度
fillHeightWidth:Boolean 設置Node是否填充高度和寬度的區(qū)域
hAlignment:HPos 水平對齊政策
vAlignment:VPos 垂直對齊策略
margin:Int Node所有四邊的邊距
marginBottom:Int Node底部的邊距
marginTop:Int Node頂端的邊距
marginLeft:Int Node左側(cè)的左邊距
marginRight:Int Node右側(cè)的右邊距
marginLeftRight:Int Node的右邊距和左邊距
marginTopBottom:Int Node的頂部和底部邊距

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

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

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

StackPane

一個StackPane是一個布局,您將不太經(jīng)常使用。 對于您添加的每個控件,它將逐字地堆疊在一起(literally stack them ),而不是像VBox,但是字面上覆蓋它們(literally overlay them)。

例如,您可以創(chuàng)建一個“BOTTOM” Button并在其頂部放置一個“TOP” Button 。 您聲明控件的順序?qū)⒁韵嗤捻樞驈牡撞康巾敳刻砑铀鼈儯▓D7.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創(chuàng)建一個用“tab”分隔的不同屏幕的UI。 這允許通過點擊相應的選項卡快速輕松地切換不同的屏幕(圖7.11)。 您可以聲明一個tabpane(),然后根據(jù)需要聲明盡可能多的tab()實例。 對于每個tab()函數(shù),通過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

像許多構(gòu)建器一樣, TabPane有幾個屬性可以調(diào)整其選項卡的行為。 例如,您可以調(diào)用tabClosingPolicy來去掉選項卡上的“X”按鈕,從而無法關(guān)閉。

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")
        }
    }
}

菜單構(gòu)建器

以嚴格面向?qū)ο蟮姆绞綐?gòu)建菜單可能很麻煩。 但是使用類型安全的構(gòu)建器,Kotlin的函數(shù)結(jié)構(gòu)可以直觀地聲明嵌套的菜單層次結(jié)構(gòu)。

MenuBar,Menu和MenuItem

使用導航菜單在用戶界面上保留大量命令并不常見。 例如, BorderPanetop區(qū)域通常是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函數(shù)參數(shù),以指定選定操作時的動作(圖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()來創(chuàng)建一個分隔線。 這有助于給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中的大多數(shù)控件都有一個contextMenu屬性,您可以在其中指定ContextMenu實例。 這是一個在右鍵單擊控件時彈出的Menu。

一個ContextMenu有函數(shù)可以添加MenuMenuItem實例,就像MenuBar一樣 。 例如,將一個ContextMenu添加到TableView<Person>是有幫助的,并提供要在表格記錄上完成的命令(圖7.16)。 有一個名為contextmenu的構(gòu)建器將構(gòu)建一個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塊參數(shù)時,menuitem構(gòu)建器采取動作來執(zhí)行。 不幸的是,這破壞了其他構(gòu)建器,其中op塊對構(gòu)建器創(chuàng)建的元素進行操作。 因此,引入item構(gòu)建器作為替代,您可以在item本身上操作,因此您必須調(diào)用setOnAction來賦值動作。menuitem構(gòu)建器沒有被棄用,因為它以比item構(gòu)建器更簡潔的方式解決了常見情況。

ListMenu

TornadoFX帶有一個列表菜單(ListMenu),其行為和看起來更像是一個典型的基于ul/li的HTML5菜單。

以下代碼示例顯示如何使用構(gòu)建器模式的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構(gòu)建器允許以非常方便的方式為ListMenu創(chuàng)建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屬性將垂直填充父容器。 這些屬性實際上適用于所有節(jié)點,但對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傳遞給構(gòu)建器構(gòu)造函數(shù),使SqueezeBox僅允許在任何給定時間展開單個折疊。

您可以選擇通過單擊標題窗格右側(cè)的十字架(clicking a cross in the right corner of the title pane)而允許折疊成為可關(guān)閉的(allow folds to be closable)。 您可以通過將closeable = true傳遞給fold構(gòu)建器,從而以每折為單位啟用關(guān)閉按鈕(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有可關(guān)閉的折疊(closeable folds)。

closeable屬性當然可以結(jié)合expanded。

SqueezeBoxAccordion之間的另一個重要區(qū)別就是分配空間(distributes overflowing space)的方式。 手風琴(Accordion)將垂直延伸以填充其父容器,并將當前打開的任何折疊推至底部。 如果父容器非常大,這將創(chuàng)建一個不自然的查看視圖。 在這方面,擠壓框(SqueezeBox)可能默認就是您想要的,但您可以添加fillHeight = true以獲得類似于Accordion的外觀。

您可以像您一樣創(chuàng)建一個TitlePane樣式一樣來創(chuàng)建SqueezeBox樣式。 關(guān)閉按鈕有一個名為close-buttoncss類,容器有一個名為squeeze-boxcss類。

Drawer

抽屜(Drawer)是一個非常像TabPane的導航組件,但它在父容器的任一側(cè)的垂直或水平放置的按鈕欄中組織每個抽屜項目。 它類似于許多流行的業(yè)務應用程序和IDE中發(fā)現(xiàn)的工具抽屜(tool drawers)。 當選擇項目時,項目的內(nèi)容將顯示在跨越控件的高度或?qū)挾鹊膬?nèi)容區(qū)域中的按鈕旁邊或上方/下方,以及內(nèi)容的首選寬度或高度,具體取決于是否將其??吭诟讣壍拇怪被蛩椒矫妗?在多重選擇(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

抽屜可以配置為顯示右側(cè)的按鈕,您可以選擇同時支持打開多個抽屜物品。 當以多重選擇模式運行時,內(nèi)容上方會出現(xiàn)一個標題,這將有助于區(qū)分內(nèi)容區(qū)域中的項目。 您可以使用布爾的showHeader參數(shù)控制標題外觀。 當啟用多重選擇時,它將默認為true,否則為false。

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

帶有右側(cè)按鈕的抽屜,多選模式和標題窗格。

當抽屜被添加到某物的旁邊時,您可以選擇抽屜的內(nèi)容區(qū)域是否應替換其旁邊的節(jié)點(默認)或浮動。 floatingContent屬性默認為false,導致Drawer替換其旁邊的內(nèi)容。

您可以使用DrawermaxContentSizefixedContentSize屬性進一步控制內(nèi)容區(qū)域的大小。 根據(jù)dockingSide,這些屬性將限制內(nèi)容區(qū)域的寬度或高度。

Workspace功能內(nèi)置支持抽屜控件。 任何WorkspaceleftDrawer, rightDrawerbottomDrawer屬性將允許您將抽屜項目bottomDrawer在其中。 在“工作區(qū)(Workspace)”一章中了解更多信息。

轉(zhuǎn)換可觀察列表項并綁定到布局(Converting observable list items and binding to layouts)

TODO

總結(jié)

到目前為止,您應該擁有能力可以使用布局,標簽窗格以及其他控件來管理控件。 將這些與數(shù)據(jù)控件結(jié)合使用,您應該可以在一小部分時間內(nèi)轉(zhuǎn)換UI。

當涉及到構(gòu)建器時,您已經(jīng)達到了頂峰(top of the peak),并擁有所需要的所有成效。 剩下的所有內(nèi)容都是圖表和形狀(charts and shapes),我們將在接下來的兩章中介紹。

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

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

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