Go 每日一庫之 fyne

簡介

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

安裝fynewindows

$ 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全部的窗體控件和交互元素都在這個子包中。

Canvas

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()建立,傳入顏色。另外經過StrokeColorStrokeWidth設置圓形邊框的顏色和寬度。

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()
}
複製代碼

運行效果以下:

放射效果就是從中心向周圍漸變。

Widget

窗體控件是一個Fyne應用程序的主要組成部分。它們能適配當前的主題,而且處理與用戶的交互。

Label

標籤(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)控件讓用戶點擊,給用戶反饋。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對子控件採用盒狀佈局(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

輸入框(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

表單控件(Form)用於對不少Label和輸入控件進行佈局。若是指定了OnSubmitOnCancel函數,表單控件會自動添加對應的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從新編寫了上面的登陸界面:

注意SubmitCancel按鈕是自動生成的!

ProgressBar

進度條控件(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

標籤容器(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

工具欄(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

佈局(Layout)就是控件如何在界面上顯示,如何排列的。要想界面好看,佈局是必需要掌握的。幾乎全部的 GUI 框架都提供了佈局或相似的接口。實際上,在前面的示例中咱們已經在fyne.NewContainerWithLayout()函數中使用了佈局。

BoxLayout

盒狀佈局(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

格子布局(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

GridWrapLayoutGridLayout的擴展。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

邊框佈局(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

表單佈局(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將容器內的全部控件顯示在中心位置,按傳入的順序顯示。最後傳入的控件顯示最上層。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顯示在圖片上層。若是咱們把textimage順序對調,字符串將會被圖片擋住,沒法看到。動手試一下~

MaxLayout

MaxLayoutCenterLayout相似,不一樣之處在於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

內置佈局在子包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

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(),傳入resourceImage1PngresourceImage2Jpg來建立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.exefyne.syso,將這兩個文件拷貝到任何目錄或其餘 Windows 計算機均可以經過直接雙擊bundle.exe運行了。沒有其餘的依賴。

fyne還支持交叉編譯,能在 windows 上編譯 mac 的應用程序,不過須要安裝額外的工具,感興趣可自行探索。

總結

fyne提供了豐富的組件和功能,咱們介紹的只是很基礎的一部分,還有剪切板、快捷鍵、滾動條、菜單等等等等內容。fyne命令實現打包靜態資源和應用程序,很是方便。fyne還有其餘高級功能留待你們探索、挖掘~

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. fyne GitHub:github.com/fyne-io/fyn…
  2. fyne 官網:fyne.io/
  3. fyne 官方入門教程:developer.fyne.io/tour/introd…
  4. Go 每日一庫 GitHub:github.com/darjun/go-d…

個人博客:darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索