Go 言語の Fyne UI フレームワークの上級編、カスタムテーマ設定。
- 参考コース:Go 言語 + Fyne 迅速入門チュートリアル
- 参考文書:Fyne.io
- ノートリポジトリ:InkkaPlum チャンネルの Golang および Fyne フレームワークチュートリアルのコードとテキスト版チュートリアル。
前言#
前回のノートでは、Fyne フレームワークを使用して簡易的な Markdown エディタを迅速に作成する方法を学びました。その中でいくつかの問題が提起されました:
- Fyne はホットリロードをサポートしていない
- カスタム中国語フォントを使用するには、
theme.go
とutil.go
を作成する必要がある
以下では、同様に Markdown エディタの例を通じてこれらの問題を一つずつ解決していきます。
ホットリロード#
Air ツールを使用して Fyne のホットリロード機能を実現できます。このツールの開発者は、Gin
がリアルタイムリロード機能を欠いているために開発したもので、Fyne の学習者の問題も同時に解決しました。
GitHub プロジェクトのアドレス:air: ☁️ Go アプリのライブリロード
設定は非常に簡単です:
Air ツールをインストールします。
go install github.com/air-verse/air@latest
プロジェクトディレクトリを開き、以下のコマンドを実行します。これにより、デフォルト設定の .air.toml
構成ファイルが現在のディレクトリに初期化されます。
air init
その後は、追加のパラメータなしで air
コマンドを実行するだけで、 .air.toml
ファイルの設定を使用できます。
air
air
コマンドを実行してプロジェクトを起動することで、プロジェクトはホットリロードをサポートするようになります。
カスタムテーマ#
テーマインターフェース#
Fyne のテーマは実際にはインターフェースfyne.Theme
です:
type Theme interface {
Color(ThemeColorName, ThemeVariant) color.Color // 色
Font(TextStyle) Resource // フォント
Icon(ThemeIconName) Resource // アイコン
Size(ThemeSizeName) float32 // サイズ
}
カスタムテーマを実装するには、カスタムテーマを定義し、このインターフェース内のすべての関数を実装する必要があります。
例えば、私はカスタムテーマmyTheme
を定義しました:
type myTheme struct {}
ここで、公式文書では、コンパイルエラーが発生した場合にエラーの位置をより明確にするために、実装したインターフェースをアサーションすることが推奨されています。以下のコードでアサーションを実現します:
var _ fyne.Theme = (*myTheme)(nil)
理解を助けるための例を挙げます:上記のアサーションコードを書いた後、インターフェースのすべての関数を実装していないためにプログラムがエラーを報告した場合、プログラムは直接theme.go
内のエラーを特定します。例えば、以下のエラーメッセージ:
.\theme.go:29:20: cannot use (*myTheme)(nil) (value of type *myTheme) as fyne.Theme value in variable declaration: *myTheme does not implement fyne.Theme (missing method Size)
しかし、上記のアサーションコードを書かなかった場合、インターフェースを実装していないためにプログラムがエラーを報告した場合、プログラムはmain.go
内にエラーを特定するかもしれませんが、エラーの位置が不明瞭で、実際のエラー原因を見つけるのが難しくなります。例えば、以下のエラーメッセージ:
.\main.go:16:24: cannot use &myTheme{…} (value of type *myTheme) as fyne.Theme value in argument to a.Settings().SetTheme: *myTheme does not implement fyne.Theme (missing method Size)
したがって、ここでアサーションコードを使用する利点は、コードの堅牢性と保守性を向上させることです。
カスタムカラー#
次に、カスタムテーマを実装したい場合は、新しいtheme.go
ファイルを作成し、その中で Fyne が定義した Theme インターフェースを実装します。
例えば、色をカスタマイズしたい場合は、Color
関数を実装します:
// 色パッケージをインポートします。ここでは "image/color" からインポートする必要があります
import "image/color"
// fyne.Themeインターフェースを実装していると仮定したmyThemeという名前の構造体を定義します
type myTheme struct {
// ここにテーマに関連する他のフィールドを定義できます
}
// Colorメソッドはfyne.Themeインターフェース内のColorメソッドを実装しています
// nameは色の名前、variantはテーマのバリアントです
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
// 色の名前が背景色かどうかを確認します
if name == theme.ColorNameBackground {
// 明るいテーマの場合
if variant == theme.VariantLight {
// 背景色として白を返します
return color.RGBA{R: 254, G: 200, B: 216, A: 0xff}
}
// それ以外は暗いテーマで、背景色として黒を返します
return color.RGBA{R: 149, G: 125, B: 173, A: 0xff}
}
// 他の色の名前については、デフォルトテーマのColorメソッドを呼び出します
return theme.DefaultTheme().Color(name, variant)
}
もちろん、前述のように、カスタムテーマを有効にするには、myTheme
がTheme
のすべてのメソッドを実装する必要があります。したがって、テーマの色を設定するだけでも、他のメソッドを実装する必要があります。変更が必要ない場合は、デフォルト値を返すことができますが、メソッドは実装されている必要があります!
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
func (m myTheme) Size(name fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(name)
}
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
return theme.DefaultTheme().Font(style)
}
最後に、main.go
に以下のコードを追加することで、カスタムテーマをプログラムに導入できます。
app.Settings().SetTheme(&myTheme{})
ここでは入力ボックス
theme.ColorNameInputBackground
の色を設定していないため、デフォルトの白黒の色が表示されています。
以上がカスタム背景色を設定するためのコードとその効果です。他のウィジェットの色を同時に設定したい場合は、Go 言語のswitch
文を利用して逐次判断し、色を設定できます。
// Colorメソッドはfyne.Themeインターフェース内のColorメソッドを実装しています
// mはmyTheme構造体へのポインタを指し、メソッドがmyThemeの状態を変更できるようにします
// nは色の名前、vはテーマのバリアントです
func (m *myTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
switch n {
case theme.ColorNameBackground: // 背景色
return color.RGBA{190, 233, 255, 1}
case theme.ColorNameButton: // ボタンの色
return color.RGBA{0, 122, 255, 255}
case theme.ColorNameDisabledButton: // 無効ボタンの色
return color.RGBA{142, 142, 147, 255}
case theme.ColorNameHover: // ホバー時の色
return color.RGBA{230, 230, 230, 255}
case theme.ColorNameFocus: // フォーカス時の色
return color.RGBA{255, 165, 0, 255}
case theme.ColorNameShadow: // シャドウの色
return color.RGBA{0, 0, 0, 50}
default: // 他にマッチしないウィジェットはデフォルトテーマの色を設定
return theme.DefaultTheme().Color(n, v)
}
}
同様に、フォント、アイコン、サイズをカスタマイズするには、同じ手順を利用し、対応するメソッドを実装すればよいです。
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {……}
func (m myTheme) Size(name fyne.ThemeSizeName) float32 {……}
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {……}
ただし、カスタムフォントやアイコンを設定する前に、Fyne のバンドルリソース操作について理解し、学ぶ必要があります。
バンドルリソース#
バンドル#
Go ベースのアプリケーションは通常、単一のバイナリ実行ファイルとして構築されるため、配布が非常に便利です。Fyne アプリケーションも同様です。
しかし、不幸なことに GUI アプリケーションは通常、ユーザーインターフェースを表示するために追加のリソースが必要です。例えば、画像、フォント、音声などです。この課題に対処するために、Go アプリケーションは資産をバイナリファイル自体にバンドルできます。
Fyne ツールキットはfyne bundle
を使用することを好みます。なぜなら、さまざまな利点があるからです。以下で探ります。
アプリケーションに画像をバンドルして、プログラムが実行時に使用できるようにするには、以下のコマンドを実行してリソースをバンドルします:
fyne bundle -o bundled.go image.png
上記のコードは、フォルダ内にbundled.go
ファイルを生成し、その中にバンドルした画像のバイナリデータが直接含まれます。これにより、プログラム内で画像を使用でき、画像を持ち歩く必要がなくなります。
実際には、すべてのバイナリ情報がコード変数内に既に含まれています。例えば、下記のbundled.go
ファイル内のStaticContent
には、画像image.png
のすべてのバイナリデータが格納されています。
var resourceImagePng = &fyne.StaticResource{
StaticName: "image.png",
StaticContent: []byte{...}
}
ただし、注意が必要なのは、fyne bundle -o bundled.go image.png
を繰り返し実行すると、互いに上書きされることです。複数のリソースをバンドルしたい場合は、-append
を使用してリソースを追加できます:
fyne bundle -o bundled.go image1.png
fyne bundle -o bundled.go -append image2.png
次に、これらのバンドルされたリソースをプログラム内で使用できます。例えば、キャンバス上に画像をロードするには:
img := canvas.NewImageFromResource(resourceImagePng)
ここでのresourceImagePng
リソース名にはルールがあります。例えば、image1.png
とimage2.png
の 2 つの画像をバンドルした場合、それぞれのリソース名はresourceImage1Png
とresourceImage2Png
になります。
デフォルトの命名規則は、ファイル名の最初の文字は必ず大文字、ファイルの拡張子の最初の文字も必ず大文字で、他はそのまま保持されます。つまり、
resource<Name><Ext>
です。
カスタムアイコン#
Fyne のバンドルリソース操作を理解した上で、前のセクションに続いて、バンドルされたリソースを使用してアイコンをカスタマイズできます。
例えば、icon.jpeg
画像をバンドルし、デフォルトのホームアイコンを置き換えたい場合は、theme.go
内のIcon
メソッドの実装コードを変更するだけです:
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
if name == theme.IconNameHome {
return resourceIconJpeg
}
return theme.DefaultTheme().Icon(name)
}
これにより、デフォルトのホームアイコンを自分の画像に置き換えることができます。コード内でも同様にホームアイコンを使用できます。例えば、以下のようにホームアイコンtheme.HomeIcon()
を使用してアイコンボタンを作成します。
widget.NewButtonWithIcon("Home", theme.HomeIcon(), func() {})
Fyne にはいくつかの一般的なアイコンが組み込まれています。テーマアイコン | Fyne.ioで全アイコンを確認できます。
カスタムフォント#
カスタムフォントについても同様の方法が適用できます。fyne bundle
を使用してフォントファイルをbundle.go
にバンドルし、theme.go
内でFont
メソッドを実装します。
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
return resourceNotoSansHansRegularTtf
}
main.go
でテーマを設定します:
app.Settings().SetTheme(&myTheme{})
これでフォントの導入が完了しますが、この方法には欠点もあります。つまり、ファイルサイズが非常に大きくなります!NotoSansHans-Regular.ttf
フォントのサイズは8.5 MBですが、bundle.go
ファイルを導入すると、bundle.go
のファイルサイズは21.3 MBになります。
もう一つの方法は、theme.go
内のmyTheme
構造体にフォントフィールドを追加することです:
type myTheme struct {
font fyne.Resource
}
次に、Font メソッドを実装します。ここでは、構造体内の Font フィールドのデータを返すだけで済みます。
func (m *myTheme) Font(s fyne.TextStyle) fyne.Resource {
return m.font
}
フォントデータを読み込む具体的な手順は、新しいutil.go
ファイルを作成するか、theme.go
内に直接書くことができます。その中で実装します:
package main
import (
"os" // ファイル操作のためにosパッケージをインポート
"log" // ログ記録のためにlogパッケージをインポート
)
// loadFont関数は指定されたパスのフォントファイルを読み込むためのものです
// 引数fontPathはフォントファイルのパス、返り値はフォントファイルのバイトスライスです
func loadFont(fontPath string) []byte {
// os.ReadFileを使用してフォントファイルを読み込みます
// fontDataには読み込まれたバイトデータが格納され、errには発生した可能性のあるエラーが格納されます
fontData, err := os.ReadFile(fontPath)
if err != nil {
// フォントファイルの読み込み中にエラーが発生した場合、エラー情報を記録し、プログラムを終了します
log.Fatalf("フォントファイルを読み込めません: %v", err)
}
// 読み込んだフォントファイルデータを返します
return fontData
}
最後に、main.go
でこのフォントを導入できます。具体的な方法は以下の通りです:
// loadFont関数を使用して指定されたパスのフォントファイルを読み込み、静的リソースとして作成します
customFont := fyne.NewStaticResource("NotoSansHans.ttf", loadFont("NotoSansHans-Regular.ttf"))
// アプリケーションのテーマをカスタムテーマに設定し、読み込んだフォントをそのテーマに適用します
// myThemeはカスタムテーマの構造体で、フォント情報を含んでいます
app.Settings().SetTheme(&myTheme{font: customFont})
これが前回のチュートリアルで中国語フォントを導入する方法です。この方法ではフォントファイルをプログラムにバンドルせず、プログラム実行時にフォントファイルを読み込むため、このカスタムフォントの方法ではフォントファイルを一緒に配布する必要があり、あまり便利ではありません。
同様の方法で、フォントの太字、斜体、等幅体の表示スタイルをそれぞれ設定できます:
type myTheme struct {
regular fyne.Resource
bold fyne.Resource
italic fyne.Resource
monospace fyne.Resource
}
func (m *myTheme) Font(s fyne.TextStyle) fyne.Resource {
if s.Monospace {
return m.monospace
}
if s.Bold {
return m.bold
}
if s.Italic {
return m.italic
}
return m.regular
}
型の考え方を利用して、さらに高次で包括的な設定方法もありますが、同様にフォントファイルを別途持ち歩く必要があります。
theme.go
type myTheme struct {
regular, bold, italic, boldItalic, monospace fyne.Resource
}
func (t *myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(name, variant)
}
func (t *myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
func (m *myTheme) Font(style fyne.TextStyle) fyne.Resource {
if style.Monospace {
return m.monospace
}
if style.Bold {
if style.Italic {
return m.boldItalic
}
return m.bold
}
if style.Italic {
return m.italic
}
return m.regular
}
func (m *myTheme) Size(name fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(name)
}
func (t *myTheme) SetFonts(regularFontPath string, monoFontPath string) {
t.regular = theme.TextFont()
t.bold = theme.TextBoldFont()
t.italic = theme.TextItalicFont()
t.boldItalic = theme.TextBoldItalicFont()
t.monospace = theme.TextMonospaceFont()
if regularFontPath != "" {
t.regular = loadCustomFont(regularFontPath, "Regular", t.regular)
t.bold = loadCustomFont(regularFontPath, "Bold", t.bold)
t.italic = loadCustomFont(regularFontPath, "Italic", t.italic)
t.boldItalic = loadCustomFont(regularFontPath, "BoldItalic", t.boldItalic)
}
if monoFontPath != "" {
t.monospace = loadCustomFont(monoFontPath, "Regular", t.monospace)
} else {
t.monospace = t.regular
}
}
func loadCustomFont(env, variant string, fallback fyne.Resource) fyne.Resource {
variantPath := strings.Replace(env, "Regular", variant, -1)
res, err := fyne.LoadResourceFromPath(variantPath)
if err != nil {
fyne.LogError("指定されたフォントの読み込み中にエラーが発生しました", err)
return fallback
}
return res
}
main.go
// テーマを設定します
t := &myTheme{}
t.SetFonts("./assets/font/Consolas-with-Yahei Regular Nerd Font.ttf", "")
// 注意: "./assets/font"ディレクトリには4つのファイルがあります:
// Consolas-with-Yahei Bold Nerd Font.ttf
// Consolas-with-Yahei BoldItalic Nerd Font.ttf
// Consolas-with-Yahei Italic Nerd Font.ttf
// Consolas-with-Yahei Regular Nerd Font.ttf
app.Settings().SetTheme(t)