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 のバンドルリソース操作について理解し、学ぶ必要があります。
バンドルリソース#
bundle#
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に増加します。
もう 1 つの方法は、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)