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
的方式安裝。到jmeubank.github.io/tdm-gcc/dow…下載安裝程序並安裝。正常狀況下安裝程序會自動設置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😄
個人博客:darjun.github.io
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~