Go 語言生態中,GUI 一直是短板,更別說跨平臺的 GUI 了。fyne
向前邁了一大步。fyne
是 Go 語言編寫的跨平臺的 UI 庫,它能夠很方便地移植到手機設備上。fyne
使用上很是簡單,同時它還提供fyne
命令打包靜態資源和應用程序。咱們先簡單介紹基本控件和佈局,而後介紹如何發佈一個fyne
應用程序。html
本文代碼使用 Go Modules。git
先初始化:github
$ mkdir fyne && cd fyne $ go mod init github.com/darjun/go-daily-lib/fyne
因爲fyne
包含一些 C/C++ 的代碼,因此須要gcc
編譯工具。在 Linux/Mac OSX 上,gcc
基本是標配,在 windows 上咱們有 3 種方式安裝gcc
工具鏈:golang
本文選擇TDM-GCC
的方式安裝。到https://jmeubank.github.io/tdm-gcc/download/下載安裝程序並安裝。正常狀況下安裝程序會自動設置PATH
路徑。打開命令行,鍵入gcc -v
。若是正常輸出版本信息,說明安裝成功且環境變量設置正確。canvas
安裝fyne
:windows
$ go get -u fyne.io/fyne
到此準備工做已經完成,咱們開始編碼。按照慣例,先以Hello, World程序開始:微信
package main import ( "fyne.io/fyne" "fyne.io/fyne/app" "fyne.io/fyne/widget" ) func main() { myApp := app.New() myWin := myApp.NewWindow("Hello") myWin.SetContent(widget.NewLabel("Hello Fyne!")) myWin.Resize(fyne.NewSize(200, 200)) myWin.ShowAndRun() }
運行結果以下:app
fyne
的使用很簡單。每一個fyne
程序都包括兩個部分,一個是應用程序對象myApp
,經過app.New()
建立。另外一個是窗口對象,經過應用程序對象myApp
來建立myApp.NewWindow("Hello")
。myApp.NewWindow()
方法中傳入的字符串就是窗口標題。框架
fyne
提供了不少經常使用的組件,經過widget.NewXXX()
建立(XXX
爲組件名)。上面示例中,咱們建立了一個Label
控件,而後設置到窗口中。最後,調用myWin.ShowAndRun()
開始運行程序。實際上myWin.ShowAndRun()
等價於ide
myWin.Show() myApp.Run()
myWin.Show()
顯示窗口,myApp.Run()
開啓事件循環。
注意一點,fyne
默認窗口大小是根據內容的寬高來設置的。上面咱們調用myWin.Resize()
手動設置了大小。不然窗口只能放下字符串Hello Fyne!
。
fyne
包結構劃分fyne
將功能劃分到多個子包中:
fyne.io/fyne
:提供全部fyne
應用程序代碼共用的基礎定義,包括數據類型和接口;fyne.io/fyne/app
:提供建立應用程序的 API;fyne.io/fyne/canvas
:提供Fyne
使用的繪製 API;fyne.io/fyne/dialog
:提供對話框組件;fyne.io/fyne/layout
:提供多種界面佈局;fyne.io/fyne/widget
:提供多種組件,fyne
全部的窗體控件和交互元素都在這個子包中。在fyne
應用程序中,全部顯示元素都是繪製在畫布(Canvas
)上的。這些元素都是畫布對象(CanvasObject
)。調用Canvas.SetContent()
方法可設置畫布內容。Canvas
通常和佈局(Layout
)容器(Container
)一塊兒使用。canvas
子包中提供了一些基礎的畫布對象:
package main import ( "image/color" "math/rand" "fyne.io/fyne" "fyne.io/fyne/app" "fyne.io/fyne/canvas" "fyne.io/fyne/layout" "fyne.io/fyne/theme" ) func main() { a := app.New() w := a.NewWindow("Canvas") rect := canvas.NewRectangle(color.White) text := canvas.NewText("Hello Text", color.White) text.Alignment = fyne.TextAlignTrailing text.TextStyle = fyne.TextStyle{Italic: true} line := canvas.NewLine(color.White) line.StrokeWidth = 5 circle := canvas.NewCircle(color.White) circle.StrokeColor = color.Gray{0x99} circle.StrokeWidth = 5 image := canvas.NewImageFromResource(theme.FyneLogo()) image.FillMode = canvas.ImageFillOriginal raster := canvas.NewRasterWithPixels( func(_, _, w, h int) color.Color { return color.RGBA{uint8(rand.Intn(255)), uint8(rand.Intn(255)), uint8(rand.Intn(255)), 0xff} }, ) gradient := canvas.NewHorizontalGradient(color.White, color.Transparent) container := fyne.NewContainerWithLayout( layout.NewGridWrapLayout(fyne.NewSize(150, 150)), rect, text, line, circle, image, raster, gradient)) w.SetContent(container) w.ShowAndRun() }
程序運行結果以下:
canvas.Rectangle
是最簡單的畫布對象了,經過canvas.NewRectangle()
建立,傳入填充顏色。
canvas.Text
是顯示文本的畫布對象,經過canvas.NewText()
建立,傳入文本字符串和顏色。該對象可設置對齊方式和字體樣式。對齊方式經過設置Text
對象的Alignment
字段值,取值有:
TextAlignLeading
:左對齊;TextAlignCenter
:中間對齊;TextAlignTrailing
:右對齊。字體樣式經過設置Text
對象的TextStyle
字段值,TextStyle
是一個結構體:
type TextStyle struct { Bold bool Italic bool Monospace bool }
對應字段設置爲true
將顯示對應的樣式:
Bold
:粗體;Italic
:斜體;Monospace
:系統等寬字體。咱們還能夠經過設置環境變量FYNE_FONT
爲一個.ttf
文件從而使用外部字體。
canvas.Line
是線段,經過canvas.NewLine()
建立,傳入顏色。能夠經過line.StrokeWidth
設置線段寬度。默認狀況下,線段是從父控件或畫布的左上角到右下角的。可經過line.Move()
和line.Resize()
修改位置。
canvas.Circle
是圓形,經過canvas.NewCircle()
建立,傳入顏色。另外經過StrokeColor
和StrokeWidth
設置圓形邊框的顏色和寬度。
canvas.Image
是圖像,能夠經過已加載的程序資源建立(canvas.NewImageFromResource()
),傳入資源對象。或經過文件路徑建立(canvas.NewImageFromFile()
),傳入文件路徑。或經過已構造的image.Image
對象建立(canvas.NewImageFromImage()
)。能夠經過FillMode
設置圖像的填充模式:
ImageFillStretch
:拉伸,填滿空間;ImageFillContain
:保持寬高比;ImageFillOriginal
:保持原始大小,不縮放。下面程序演示了這 3 種建立圖像的方式:
package main import ( "image" "image/color" "fyne.io/fyne" "fyne.io/fyne/app" "fyne.io/fyne/canvas" "fyne.io/fyne/layout" "fyne.io/fyne/theme" ) func main() { a := app.New() w := a.NewWindow("Hello") img1 := canvas.NewImageFromResource(theme.FyneLogo()) img1.FillMode = canvas.ImageFillOriginal img2 := canvas.NewImageFromFile("./luffy.jpg") img2.FillMode = canvas.ImageFillOriginal image := image.NewAlpha(image.Rectangle{image.Point{0, 0}, image.Point{100, 100}}) for i := 0; i < 100; i++ { for j := 0; j < 100; j++ { image.Set(i, j, color.Alpha{uint8(i % 256)}) } } img3 := canvas.NewImageFromImage(image) img3.FillMode = canvas.ImageFillOriginal container := fyne.NewContainerWithLayout( layout.NewGridWrapLayout(fyne.NewSize(150, 150)), img1, img2, img3) w.SetContent(container) w.ShowAndRun() }
theme.FyneLogo()
是 Fyne 圖標資源,luffy.jpg
是磁盤中的文件,最後建立一個image.Image
,從中生成canvas.Image
。
最後一種是梯度漸變效果,有兩種類型canvas.LinearGradient
(線性漸變)和canvas.RadialGradient
(放射漸變),指從一種顏色漸變到另外一種顏色。線性漸變又分爲兩種水平線性漸變和垂直線性漸變,分別經過canvas.NewHorizontalGradient()
和canvas.NewVerticalGradient()
建立。放射漸變經過canvas.NewRadialGradient()
建立。咱們在上面的示例中已經看到了水平線性漸變的效果,接下來一塊兒看看放射漸變的效果:
func main() { a := app.New() w := a.NewWindow("Canvas") gradient := canvas.NewRadialGradient(color.White, color.Transparent) w.SetContent(gradient) w.Resize(fyne.NewSize(200, 200)) w.ShowAndRun() }
運行效果以下:
放射效果就是從中心向周圍漸變。
窗體控件是一個Fyne
應用程序的主要組成部分。它們能適配當前的主題,而且處理與用戶的交互。
標籤(Label
)是最簡單的一個控件了,用於顯示字符串。它有點相似於canvas.Text
,不一樣之處在於Label
能夠處理簡單的格式化,例如\n
:
func main() { myApp := app.New() myWin := myApp.NewWindow("Label") l1 := widget.NewLabel("Name") l2 := widget.NewLabel("da\njun") container := fyne.NewContainerWithLayout(layout.NewVBoxLayout(), l1, l2) myWin.SetContent(container) myWin.Resize(fyne.NewSize(150, 150)) myWin.ShowAndRun() }
第二個widget.Label
中\n
後面的內容會在下一行渲染:
按鈕(Button
)控件讓用戶點擊,給用戶反饋。Button
能夠包含文本,圖標或二者皆有。調用widget.NewButton()
建立一個默認的文本按鈕,傳入文本和一個無參的回調函數。帶圖標的按鈕須要調用widget.NewButtonWithIcon()
,傳入文本和回調參數,還須要一個fyne.Resource
類型的圖標資源:
func main() { myApp := app.New() myWin := myApp.NewWindow("Button") btn1 := widget.NewButton("text button", func() { fmt.Println("text button clicked") }) btn2 := widget.NewButtonWithIcon("icon", theme.HomeIcon(), func() { fmt.Println("icon button clicked") }) container := fyne.NewContainerWithLayout(layout.NewVBoxLayout(), btn1, btn2) myWin.SetContent(container) myWin.Resize(fyne.NewSize(150, 50)) myWin.ShowAndRun() }
上面建立了一個文本按鈕和一個圖標按鈕,theme
子包中包含一些默認的圖標資源,也能夠加載外部的圖標。運行:
點擊按鈕,對應的回調就會被調用,試試看!
盒子控件(Box
)就是一個簡單的水平或垂直的容器。在內部,Box
對子控件採用盒狀佈局(Box Layout
),詳見後文佈局。咱們能夠經過傳入控件對象給widget.NewHBox()
或widget.NewVBox()
建立盒子。或者調用已經建立好的widget.Box
對象的Append()
或Prepend()
向盒子中添加控件。前者在尾部追加,後者在頭部添加。
func main() { myApp := app.New() myWin := myApp.NewWindow("Box") content := widget.NewVBox( widget.NewLabel("The top row of VBox"), widget.NewHBox( widget.NewLabel("Label 1"), widget.NewLabel("Label 2"), ), ) content.Append(widget.NewButton("Append", func() { content.Append(widget.NewLabel("Appended")) })) content.Append(widget.NewButton("Prepend", func() { content.Prepend(widget.NewLabel("Prepended")) })) myWin.SetContent(content) myWin.Resize(fyne.NewSize(150, 150)) myWin.ShowAndRun() }
咱們甚至能夠嵌套widget.Box
控件,這樣就能夠實現比較靈活的佈局。上面的代碼中添加了兩個按鈕,點擊時分別在尾部和頭部添加一個Label
:
輸入框(Entry
)控件用於給用戶輸入簡單的文本內容。調用widget.NewEntry()
便可建立一個輸入框控件。咱們通常保存輸入框控件的引用,以便訪問其Text
字段來獲取內容。註冊OnChanged
回調函數。每當內容有修改時,OnChanged
就會被調用。咱們能夠調用SetReadOnly(true)
設置輸入框的只讀屬性。方法SetPlaceHolder()
用來設置佔位字符串,設置字段Multiline
讓輸入框接受多行文本。另外,咱們可使用NewPasswordEntry()
建立一個密碼輸入框,輸入的文本不會以明文顯示。
func main() { myApp := app.New() myWin := myApp.NewWindow("Entry") nameEntry := widget.NewEntry() nameEntry.SetPlaceHolder("input name") nameEntry.OnChanged = func(content string) { fmt.Println("name:", nameEntry.Text, "entered") } passEntry := widget.NewPasswordEntry() passEntry.SetPlaceHolder("input password") nameBox := widget.NewHBox(widget.NewLabel("Name"), layout.NewSpacer(), nameEntry) passwordBox := widget.NewHBox(widget.NewLabel("Password"), layout.NewSpacer(), passEntry) loginBtn := widget.NewButton("Login", func() { fmt.Println("name:", nameEntry.Text, "password:", passEntry.Text, "login in") }) multiEntry := widget.NewEntry() multiEntry.SetPlaceHolder("please enter\nyour description") multiEntry.MultiLine = true content := widget.NewVBox(nameBox, passwordBox, loginBtn, multiEntry) myWin.SetContent(content) myWin.ShowAndRun() }
這裏咱們實現了一個簡單的登陸界面:
Checkbox/Radio/Select
CheckBox
是簡單的選擇框,每一個選擇是獨立的,例如愛好能夠是足球、籃球,也能夠都是。建立方法widget.NewCheck()
,傳入選項字符串(足球,籃球)和回調函數。回調函數接受一個bool
類型的參數,表示該選項是否選中。
Radio
是單選框,每一個組內只能選擇一個,例如性別,只能是男或女(?)。建立方法widget.NewRadio()
,傳入字符串切片和回調函數做爲參數。回調函數接受一個字符串參數,表示選中的選項。也可使用Selected
字段讀取選中的選項。
Select
是下拉選擇框,點擊時顯示一個下拉菜單,點擊選擇。選項很是多的時候,比較適合用Select
。建立方法widget.NewSelect()
,參數與NewRadio()
徹底相同。
func main() { myApp := app.New() myWin := myApp.NewWindow("Choices") nameEntry := widget.NewEntry() nameEntry.SetPlaceHolder("input name") passEntry := widget.NewPasswordEntry() passEntry.SetPlaceHolder("input password") repeatPassEntry := widget.NewPasswordEntry() repeatPassEntry.SetPlaceHolder("repeat password") nameBox := widget.NewHBox(widget.NewLabel("Name"), layout.NewSpacer(), nameEntry) passwordBox := widget.NewHBox(widget.NewLabel("Password"), layout.NewSpacer(), passEntry) repeatPasswordBox := widget.NewHBox(widget.NewLabel("Repeat Password"), layout.NewSpacer(), repeatPassEntry) sexRadio := widget.NewRadio([]string{"male", "female", "unknown"}, func(value string) { fmt.Println("sex:", value) }) sexBox := widget.NewHBox(widget.NewLabel("Sex"), sexRadio) football := widget.NewCheck("football", func(value bool) { fmt.Println("football:", value) }) basketball := widget.NewCheck("basketball", func(value bool) { fmt.Println("basketball:", value) }) pingpong := widget.NewCheck("pingpong", func(value bool) { fmt.Println("pingpong:", value) }) hobbyBox := widget.NewHBox(widget.NewLabel("Hobby"), football, basketball, pingpong) provinceSelect := widget.NewSelect([]string{"anhui", "zhejiang", "shanghai"}, func(value string) { fmt.Println("province:", value) }) provinceBox := widget.NewHBox(widget.NewLabel("Province"), layout.NewSpacer(), provinceSelect) registerBtn := widget.NewButton("Register", func() { fmt.Println("name:", nameEntry.Text, "password:", passEntry.Text, "register") }) content := widget.NewVBox(nameBox, passwordBox, repeatPasswordBox, sexBox, hobbyBox, provinceBox, registerBtn) myWin.SetContent(content) myWin.ShowAndRun() }
這裏咱們實現了一個簡單的註冊界面:
表單控件(Form
)用於對不少Label
和輸入控件進行佈局。若是指定了OnSubmit
或OnCancel
函數,表單控件會自動添加對應的Button
按鈕。咱們調用widget.NewForm()
傳入一個widget.FormItem
切片建立Form
控件。每一項中一個字符串做爲Label
的文本,一個控件對象。建立好Form
對象以後還能調用其Append(label, widget)
方法添加控件。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Form") nameEntry := widget.NewEntry() passEntry := widget.NewPasswordEntry() form := widget.NewForm( &widget.FormItem{"Name", nameEntry}, &widget.FormItem{"Pass", passEntry}, ) form.OnSubmit = func() { fmt.Println("name:", nameEntry.Text, "pass:", passEntry.Text, "login in") } form.OnCancel = func() { fmt.Println("login canceled") } myWindow.SetContent(form) myWindow.Resize(fyne.NewSize(150, 150)) myWindow.ShowAndRun() }
使用Form
能大大簡化表單的構建,咱們使用Form
從新編寫了上面的登陸界面:
注意Submit
和Cancel
按鈕是自動生成的!
進度條控件(ProgressBar
)用來表示任務的進度,例如文件下載的進度。建立方法widget.NewProgressBar()
,默認最小值爲0.0
,最大值爲1.1
,可經過Min/Max
字段設置。調用SetValue()
方法來控制進度。還有一種進度條是循環動畫,它表示有任務在進行中,並不能表示具體的完成狀況。
func main() { myApp := app.New() myWindow := myApp.NewWindow("ProgressBar") bar1 := widget.NewProgressBar() bar1.Min = 0 bar1.Max = 100 bar2 := widget.NewProgressBarInfinite() go func() { for i := 0; i <= 100; i ++ { time.Sleep(time.Millisecond * 500) bar1.SetValue(float64(i)) } }() content := widget.NewVBox(bar1, bar2) myWindow.SetContent(content) myWindow.Resize(fyne.NewSize(150, 150)) myWindow.ShowAndRun() }
在另外一個 goroutine 中更新進度。效果以下:
標籤容器(TabContainer
)容許用戶在不一樣的內容面板之間切換。標籤能夠是文本或圖標。建立方法widget.NewTabContainer()
,傳入widget.TabItem
做爲參數。widget.TabItem
可經過widget.NewTabItem(label, widget)
建立。標籤還能夠設置位置:
TabLocationBottom
:顯示在底部;TabLocationLeading
:顯示在頂部左邊;TabLocationTrailing
:顯示在頂部右邊。看示例:
func main() { myApp := app.New() myWindow := myApp.NewWindow("TabContainer") nameLabel := widget.NewLabel("Name: dajun") sexLabel := widget.NewLabel("Sex: male") ageLabel := widget.NewLabel("Age: 18") addressLabel := widget.NewLabel("Province: shanghai") addressLabel.Hide() profile := widget.NewVBox(nameLabel, sexLabel, ageLabel, addressLabel) musicRadio := widget.NewRadio([]string{"on", "off"}, func(string) {}) showAddressCheck := widget.NewCheck("show address?", func(value bool) { if !value { addressLabel.Hide() } else { addressLabel.Show() } }) memberTypeSelect := widget.NewSelect([]string{"junior", "senior", "admin"}, func(string) {}) setting := widget.NewForm( &widget.FormItem{"music", musicRadio}, &widget.FormItem{"check", showAddressCheck}, &widget.FormItem{"member type", memberTypeSelect}, ) tabs := widget.NewTabContainer( widget.NewTabItem("Profile", profile), widget.NewTabItem("Setting", setting), ) myWindow.SetContent(tabs) myWindow.Resize(fyne.NewSize(200, 200)) myWindow.ShowAndRun() }
上面代碼編寫了一個簡單的我的信息面板和設置面板,點擊show address?
可切換地址信息是否顯示:
工具欄(Toolbar
)是不少 GUI 應用程序必備的部分。工具欄將經常使用命令用圖標的方式很形象地展現出來,方便使用。建立方法widget.NewToolbar()
,傳入多個widget.ToolbarItem
做爲參數。最常使用的ToolbarItem
有命令(Action
)、分隔符(Separator
)和空白(Spacer
),分別經過widget.NewToolbarItemAction(resource, callback)
/widget.NewToolbarSeparator()
/widget.NewToolbarSpacer()
建立。命令須要指定回調,點擊時觸發。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Toolbar") toolbar := widget.NewToolbar( widget.NewToolbarAction(theme.DocumentCreateIcon(), func() { fmt.Println("New document") }), widget.NewToolbarSeparator(), widget.NewToolbarAction(theme.ContentCutIcon(), func() { fmt.Println("Cut") }), widget.NewToolbarAction(theme.ContentCopyIcon(), func() { fmt.Println("Copy") }), widget.NewToolbarAction(theme.ContentPasteIcon(), func() { fmt.Println("Paste") }), widget.NewToolbarSpacer(), widget.NewToolbarAction(theme.HelpIcon(), func() { log.Println("Display help") }), ) content := fyne.NewContainerWithLayout( layout.NewBorderLayout(toolbar, nil, nil, nil), toolbar, widget.NewLabel(`Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quidem consectetur ipsam nesciunt, quasi sint expedita minus aut, porro iusto magnam ducimus voluptates cum vitae. Vero adipisci earum iure consequatur quidem.`), ) myWindow.SetContent(content) myWindow.ShowAndRun() }
工具欄通常使用BorderLayout
,將工具欄放在其餘任何控件上面,佈局後文會詳述。運行:
標準的 Fyne 控件提供了最小的功能集和定製化以適應大部分的應用場景。有些時候,咱們須要更高級的功能。除了本身編寫控件外,咱們還能夠擴展示有的控件。例如,咱們但願圖標控件widget.Icon
能響應鼠標左鍵、右鍵和雙擊。首先編寫一個構造函數,調用ExtendBaseWidget()
方法得到基礎的控件功能:
type tappableIcon struct { widget.Icon } func newTappableIcon(res fyne.Resource) *tappableIcon { icon := &tappableIcon{} icon.ExtendBaseWidget(icon) icon.SetResource(res) return icon }
而後實現相關的接口:
// src/fyne.io/fyne/canvasobject.go // 鼠標左鍵 type Tappable interface { Tapped(*PointEvent) } // 鼠標右鍵或長按 type SecondaryTappable interface { TappedSecondary(*PointEvent) } // 雙擊 type DoubleTappable interface { DoubleTapped(*PointEvent) }
接口實現:
func (t *tappableIcon) Tapped(e *fyne.PointEvent) { log.Println("I have been left tapped at", e) } func (t *tappableIcon) TappedSecondary(e *fyne.PointEvent) { log.Println("I have been right tapped at", e) } func (t *tappableIcon) DoubleTapped(e *fyne.PointEvent) { log.Println("I have been double tapped at", e) }
最後使用:
func main() { a := app.New() w := a.NewWindow("Tappable") w.SetContent(newTappableIcon(theme.FyneLogo())) w.Resize(fyne.NewSize(200, 200)) w.ShowAndRun() }
運行,點擊圖標控制檯有相應輸出:
2020/06/18 06:44:02 I have been left tapped at &{{110 97} {106 93}} 2020/06/18 06:44:03 I have been left tapped at &{{110 97} {106 93}} 2020/06/18 06:44:05 I have been right tapped at &{{88 102} {84 98}} 2020/06/18 06:44:06 I have been right tapped at &{{88 102} {84 98}} 2020/06/18 06:44:06 I have been left tapped at &{{88 101} {84 97}} 2020/06/18 06:44:07 I have been double tapped at &{{88 101} {84 97}}
輸出的fyne.PointEvent
中有絕對位置(對於窗口左上角)和相對位置(對於容器左上角)。
佈局(Layout
)就是控件如何在界面上顯示,如何排列的。要想界面好看,佈局是必需要掌握的。幾乎全部的 GUI 框架都提供了佈局或相似的接口。實際上,在前面的示例中咱們已經在fyne.NewContainerWithLayout()
函數中使用了佈局。
盒狀佈局(BoxLayout
)是最常使用的一個佈局。它將控件都排在一行或一列。在fyne
中,咱們能夠經過layout.NewHBoxLayout()
建立一個水平盒裝佈局,經過layout.NewVBoxLayout()
建立一個垂直盒裝佈局。水平佈局中的控件都排列在一行中,每一個控件的寬度等於其內容的最小寬度(MinSize().Width
),它們都擁有相同的高度,即全部控件的最大高度(MinSize().Height
)。
垂直佈局中的控件都排列在一列中,每一個控件的高度等於其內容的最小高度,它們都擁有相同的寬度,即全部控件的最大寬度。
通常地,在BoxLayout
中使用layout.NewSpacer()
輔助佈局,它會佔滿剩餘的空間。對於水平盒狀佈局來講,第一個控件前添加一個layout.NewSpacer()
,全部控件右對齊。最後一個控件後添加一個layout.NewSpacer()
,全部控件左對齊。先後都有,那麼控件中間對齊。若是在中間有添加一個layout.NewSpacer()
,那麼其它控件兩邊對齊。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Box Layout") hcontainer1 := fyne.NewContainerWithLayout(layout.NewHBoxLayout(), canvas.NewText("left", color.White), canvas.NewText("right", color.White)) // 左對齊 hcontainer2 := fyne.NewContainerWithLayout(layout.NewHBoxLayout(), layout.NewSpacer(), canvas.NewText("left", color.White), canvas.NewText("right", color.White)) // 右對齊 hcontainer3 := fyne.NewContainerWithLayout(layout.NewHBoxLayout(), canvas.NewText("left", color.White), canvas.NewText("right", color.White), layout.NewSpacer()) // 中間對齊 hcontainer4 := fyne.NewContainerWithLayout(layout.NewHBoxLayout(), layout.NewSpacer(), canvas.NewText("left", color.White), canvas.NewText("right", color.White), layout.NewSpacer()) // 兩邊對齊 hcontainer5 := fyne.NewContainerWithLayout(layout.NewHBoxLayout(), canvas.NewText("left", color.White), layout.NewSpacer(), canvas.NewText("right", color.White)) myWindow.SetContent(fyne.NewContainerWithLayout(layout.NewVBoxLayout(), hcontainer1, hcontainer2, hcontainer3, hcontainer4, hcontainer5)) myWindow.Resize(fyne.NewSize(200, 200)) myWindow.ShowAndRun() }
運行效果:
格子布局(GridLayout
)每一行有固定的列,添加的控件數量超過這個值時,後面的控件將會在新的行顯示。建立方法layout.NewGridLayout(cols)
,傳入每行的列數。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Grid Layout") img1 := canvas.NewImageFromResource(theme.FyneLogo()) img2 := canvas.NewImageFromResource(theme.FyneLogo()) img3 := canvas.NewImageFromResource(theme.FyneLogo()) myWindow.SetContent(fyne.NewContainerWithLayout(layout.NewGridLayout(2), img1, img2, img3)) myWindow.Resize(fyne.NewSize(300, 300)) myWindow.ShowAndRun() }
運行效果:
該佈局有個優點,咱們縮放界面時,控件會自動調整大小。試試看~
GridWrapLayout
是GridLayout
的擴展。GridWrapLayout
建立時會指定一個初始size
,這個size
會應用到全部的子控件上,每一個子控件都保持這個size
。初始,每行一個控件。若是界面大小變化了,這些子控件會從新排列。例如寬度翻倍了,那麼一行就能夠排兩個控件了。有點像流動佈局:
func main() { myApp := app.New() myWindow := myApp.NewWindow("Grid Wrap Layout") img1 := canvas.NewImageFromResource(theme.FyneLogo()) img2 := canvas.NewImageFromResource(theme.FyneLogo()) img3 := canvas.NewImageFromResource(theme.FyneLogo()) myWindow.SetContent( fyne.NewContainerWithLayout( layout.NewGridWrapLayout(fyne.NewSize(150, 150)), img1, img2, img3)) myWindow.ShowAndRun() }
初始:
加大寬度:
再加大寬度:
邊框佈局(BorderLayout
)比較經常使用於構建用戶界面,上面例子中的Toolbar
通常都和BorderLayout
搭配使用。建立方法layout.NewBorderLayout(top, bottom, left, right)
,分別傳入頂部、底部、左側、右側的控件對象。添加到容器中的控件若是是這些邊界對象,則顯示在對應位置,其餘都顯示在中心:
func main() { myApp := app.New() myWindow := myApp.NewWindow("Border Layout") left := canvas.NewText("left", color.White) right := canvas.NewText("right", color.White) top := canvas.NewText("top", color.White) bottom := canvas.NewText("bottom", color.White) content := widget.NewLabel(`Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quidem consectetur ipsam nesciunt, quasi sint expedita minus aut, porro iusto magnam ducimus voluptates cum vitae. Vero adipisci earum iure consequatur quidem.`) container := fyne.NewContainerWithLayout( layout.NewBorderLayout(top, bottom, left, right), top, bottom, left, right, content, ) myWindow.SetContent(container) myWindow.ShowAndRun() }
效果:
表單佈局(FormLayout
)其實就是一個 2 列的GridLayout
,可是針對表單作了一些微調。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Border Layout") nameLabel := canvas.NewText("Name", color.Black) nameValue := canvas.NewText("dajun", color.White) ageLabel := canvas.NewText("Age", color.Black) ageValue := canvas.NewText("18", color.White) container := fyne.NewContainerWithLayout( layout.NewFormLayout(), nameLabel, nameValue, ageLabel, ageValue, ) myWindow.SetContent(container) myWindow.Resize(fyne.NewSize(150, 150)) myWindow.ShowAndRun() }
運行效果:
CenterLayout
將容器內的全部控件顯示在中心位置,按傳入的順序顯示。最後傳入的控件顯示最上層。CenterLayout
中全部控件將保持它們的最小尺寸(大小能容納其內容)。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Center Layout") image := canvas.NewImageFromResource(theme.FyneLogo()) image.FillMode = canvas.ImageFillOriginal text := canvas.NewText("Fyne Logo", color.Black) container := fyne.NewContainerWithLayout( layout.NewCenterLayout(), image, text, ) myWindow.SetContent(container) myWindow.ShowAndRun() }
運行結果:
字符串Fyne Logo
顯示在圖片上層。若是咱們把text
和image
順序對調,字符串將會被圖片擋住,沒法看到。動手試一下~
MaxLayout
與CenterLayout
相似,不一樣之處在於MaxLayout
會讓容器內的元素都顯示爲最大尺寸(等於容器的大小)。細心的朋友可能發現了,在CenterLayout
的示例中。咱們設置了圖片的填充模式爲ImageFillOriginal
。若是不設置填充模式,圖片的默認MinSize
爲(1, 1)
。能夠fmt.Println(image.MinSize())
驗證一下。這樣圖片就不會顯示在界面中。
在MaxLayout
的容器中,咱們不須要這樣處理:
func main() { myApp := app.New() myWindow := myApp.NewWindow("Max Layout") image := canvas.NewImageFromResource(theme.FyneLogo()) text := canvas.NewText("Fyne Logo", color.Black) container := fyne.NewContainerWithLayout( layout.NewMaxLayout(), image, text, ) myWindow.SetContent(container) myWindow.Resize(fyne.Size(200, 200)) myWindow.ShowAndRun() }
運行結果:
注意,canvas.Text
顯示爲左對齊了。若是要居中對齊,設置其Alignment
屬性爲fyne.TextAlignCenter
。
內置佈局在子包layout
中。它們都實現了fyne.Layout
接口:
// src/fyne.io/fyne/layout.go type Layout interface { Layout([]CanvasObject, Size) MinSize(objects []CanvasObject) Size }
要實現自定義的佈局,只須要實現這個接口。下面咱們實現一個臺階(對角)的佈局,好似一個矩陣的對角線,從左上到右下。首先定義一個新的類型。而後實現接口fyne.Layout
的兩個方法:
type diagonal struct { } func (d *diagonal) MinSize(objects []fyne.CanvasObject) fyne.Size { w, h := 0, 0 for _, o := range objects { childSize := o.MinSize() w += childSize.Width h += childSize.Height } return fyne.NewSize(w, h) } func (d *diagonal) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) { pos := fyne.NewPos(0, 0) for _, o := range objects { size := o.MinSize() o.Resize(size) o.Move(pos) pos = pos.Add(fyne.NewPos(size.Width, size.Height)) } }
MinSize()
返回全部子控件的MinSize
之和。Layout()
從左上到右下排列控件。而後是使用:
func main() { myApp := app.New() myWindow := myApp.NewWindow("Diagonal Layout") img1 := canvas.NewImageFromResource(theme.FyneLogo()) img1.FillMode = canvas.ImageFillOriginal img2 := canvas.NewImageFromResource(theme.FyneLogo()) img2.FillMode = canvas.ImageFillOriginal img3 := canvas.NewImageFromResource(theme.FyneLogo()) img3.FillMode = canvas.ImageFillOriginal container := fyne.NewContainerWithLayout( &diagonal{}, img1, img2, img3, ) myWindow.SetContent(container) myWindow.ShowAndRun() }
運行結果:
fyne
提供了一個 Demo,演示了大部分控件和佈局的使用。可以使用下面命令安裝,執行:
$ go get fyne.io/fyne/cmd/fyne_demo $ fyne_demo
效果圖:
fyne
命令fyne
庫爲了方便開發者提供了fyne
命令。fyne
能夠用來將靜態資源打包進可執行程序,還能將整個應用程序打包成可發佈的形式。fyne
命令經過下面命令安裝:
$ go get fyne.io/fyne/cmd/fyne
安裝完成以後fyne
就在$GOPATH/bin
目錄中,將$GOPATH/bin
添加到系統$PATH
中就能夠直接運行fyne
命令了。
其實在前面的示例中咱們已經屢次使用了fyne
內置的靜態資源,使用最多的要屬fyne.FyneLogo()
了。下面咱們有兩個圖片image1.png/image2.jpg
。咱們使用fyne bundle
命令將這兩個圖片打包進代碼:
$ fyne bundle image1.png >> bundled.go $ fyne bundle -append image2.jpg >> bundled.go
第二個命令指定-append
選項表示添加到現有文件中,生成的文件以下:
// bundled.go package main import "fyne.io/fyne" var resourceImage1Png = &fyne.StaticResource{ StaticName: "image1.png", StaticContent: []byte{...}} var resourceImage2Jpg = &fyne.StaticResource{ StaticName: "image2.jpg", StaticContent: []byte{...}}
實際上就是將圖片內容存入一個字節切片中,咱們在代碼中就能夠調用canvas.NewImageFromResource()
,傳入resourceImage1Png
或resourceImage2Jpg
來建立canvas.Image
對象了。
func main() { myApp := app.New() myWindow := myApp.NewWindow("Bundle Resource") img1 := canvas.NewImageFromResource(resourceImage1Png) img1.FillMode = canvas.ImageFillOriginal img2 := canvas.NewImageFromResource(resourceImage2Jpg) img2.FillMode = canvas.ImageFillOriginal img3 := canvas.NewImageFromResource(theme.FyneLogo()) img3.FillMode = canvas.ImageFillOriginal container := fyne.NewContainerWithLayout( layout.NewGridLayout(1), img1, img2, img3, ) myWindow.SetContent(container) myWindow.ShowAndRun() }
運行結果:
注意,因爲如今是兩個文件,不能使用go run main.go
,應該用go run .
。
theme.FyneLogo()
其實是也是提早打包進代碼的,代碼文件是bundled-icons.go
:
// src/fyne.io/fyne/theme/icons.go func FyneLogo() fyne.Resource { return fynelogo } // src/fyne.io/fyne/theme/bundled-icons.go var fynelogo = &fyne.StaticResource{ StaticName: "fyne.png", StaticContent: []byte{}}
發佈圖像應用程序到多個操做系統是很是複雜的任務。圖形界面應用程序一般有圖標和一些元數據。fyne
命令提供了將應用程序發佈到多個平臺的支持。使用fyne package
命令將建立一個可在其它計算機上安裝/運行的應用程序。在 Windows 上,fyne package
會建立一個.exe
文件。在 macOS 上,會建立一個.app
文件。在 Linux 上,會生成一個.tar.xz
文件,可手動安裝。
咱們將上面的應用程序打包成一個exe
文件:
$ fyne package -os windows -icon icon.jpg
上面命令會在同目錄下生成兩個文件bundle.exe
和fyne.syso
,將這兩個文件拷貝到任何目錄或其餘 Windows 計算機均可以經過直接雙擊bundle.exe
運行了。沒有其餘的依賴。
fyne
還支持交叉編譯,能在 windows 上編譯 mac 的應用程序,不過須要安裝額外的工具,感興趣可自行探索。
fyne
提供了豐富的組件和功能,咱們介紹的只是很基礎的一部分,還有剪切板、快捷鍵、滾動條、菜單等等等等內容。fyne
命令實現打包靜態資源和應用程序,很是方便。fyne
還有其餘高級功能留待你們探索、挖掘~
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~