Fyne-Markdown编辑器
Markdown 编辑器
此项目的要求和最终效果
能够支持中文, 程序窗口左半边写 markdown, 右边则显示富文本, 也就是现在我们 Vs code 里面的 Preview(プレビュー)
解决语言问题
Fyne 默认字体不支持中文, 我们可以复制一下下面的测试文本, 基于我们第一个案例, 会发现是乱码。
package main
import (
"fmt"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
a:= app.New()
w:= a.NewWindow("让我们一起来学习Go语言吧!")
w.SetContent(widget.NewLabel("让我们一起来学习Go语言吧!"))
w.ShowAndRun()
fmt.Println("close!")
}
在项目文件夹中(必要文件), 找到字体文件(NotoSansHans-Regular.ttf)或者亦可以用你喜好的字体。将项目文件夹中的theme.go
和util.go
复制到项目目录下。
然后, 在项目中添加如下代码:
customFont := fyne.NewStaticResource("NotoSansHans.ttf", loadFont("NotoSansHans-Regular.ttf"))
a.Settings().SetTheme(&myTheme{font: customFont})
只需要改loadFont
内的内容即可。
实战 Struct 和 Receiver
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
type config struct {
EditWidget *widget.Entry
PreviewWidget *widget.RichText
CurrentFile fyne.URI
MenuItem *fyne.MenuItem
}
正式写代码
container.NewHSplit
对应的文档:
https://docs.fyne.io/api/v2.4/container/split.html
URI URL 辨析
-
URI 是一个用于标识资源的字符串, 它可以是资源的名称、位置或两者的结合。URI 的概念是一个广义的概念, 包含了两类子集:URL 和 URN。
-
URL(统一资源定位符) URL 是 URI 的一个子集, 用于指定资源的位置。URL 不仅标识资源, 还提供了定位资源的方法。URL 通常包括以下部分:
方案(Scheme): 例如 http、https、ftp 等,表示访问资源的协议。 主机(Host): 资源所在的主机名或 IP 地址。 路径(Path): 资源在主机上的具体位置。 查询参数(Query Param): 附加的参数信息,用于传递额外的数据。 片段(Fragment): 资源的一部分,例如网页中的一个锚点。
举一个例子(锚点): https://ja.wikipedia.org/wiki/Go_(プログラミング言語)#interface
案例
https://example.com/path/to/resource?query=param#fragment
URI例子
file:///Users/username/Documents/example.md
代码是最终成品
Filter(筛选器)
代码
var filter = storage.NewExtensionFileFilter([]string{".md", ".MD"})
提示:
func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
表示方法
s[len(s)-len(suffix):]
假设 s 为example.md
放在不同的文件中
目录说明
myapp/
|-- main.go
|-- test.go
main.go
及test.go
第一行
package main
main.go更多代码
func main() {
... ...
}
附注: 只要在同一个包, 顶层变量和函数就可以互相自由访问。
生成一个图标
随意地找一个生成器即可, 格式要求 Icon.png
最终目录
myapp/
|-- main.go
|-- ui.go
|-- config.go
这里只有 Import 相关的内容
config.go
import (
"io"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
)
ui.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
main.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
综合代码
main.go
type config struct {
EditWidget *widget.Entry
PreviewWidget *widget.RichText
CurrentFile fyne.URI
MenuItem *fyne.MenuItem
BaseTitle string
}
var cfg config
var filter = storage.NewExtensionFileFilter([]string{".md", ".MD"})
func main() {
a := app.New()
customFont := fyne.NewStaticResource("NotoSansHans.ttf", loadFont("NotoSansHans-Regular.ttf"))
a.Settings().SetTheme(&myTheme{font: customFont})
w := a.NewWindow("Markdown编辑器")
cfg.BaseTitle = "Markdown编辑器"
edit, preview := cfg.makeUI()
cfg.createMenu(w)
w.SetContent(container.NewHSplit(edit, preview))
w.Resize(fyne.Size{Width: 800, Height: 600})
w.CenterOnScreen()
w.ShowAndRun()
}
ui.go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
func (cfg *config) makeUI() (*widget.Entry, *widget.RichText){
edit := widget.NewMultiLineEntry()
preview := widget.NewRichTextFromMarkdown("")
cfg.EditWidget = edit
cfg.PreviewWidget = preview
edit.OnChanged = preview.ParseMarkdown
return edit, preview
}
func (cfg *config) createMenu(win fyne.Window){
open := fyne.NewMenuItem("打开...", cfg.openFunc(win))
save := fyne.NewMenuItem("保存", cfg.saveFunc(win))
cfg.MenuItem = save
cfg.MenuItem.Disabled = true
saveAs := fyne.NewMenuItem("另存为...", cfg.saveAsFunc(win))
fileMenu := fyne.NewMenu("文件", open, save, saveAs)
menu := fyne.NewMainMenu(fileMenu)
win.SetMainMenu(menu)
}
config.go
package main
import (
"io/ioutil"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
)
func (cfg *config) saveAsFunc(win fyne.Window) func(){
return func(){
saveDialog := dialog.NewFileSave(func(write fyne.URIWriteCloser, err error){
if err != nil{
dialog.ShowError(err, win)
return
}
if write == nil{
return
}
if !strings.HasSuffix(strings.ToLower(write.URI().String()),".md"){
dialog.ShowInformation("错误", "必须是.md扩展名", win)
return
}
write.Write([]uint8(cfg.EditWidget.Text))
cfg.CurrentFile = write.URI()
defer write.Close()
win.SetTitle(cfg.BaseTitle + "-" + write.URI().Name())
cfg.MenuItem.Disabled = false
},win)
saveDialog.SetFileName("未命名.md")
saveDialog.SetFilter(filter)
saveDialog.Show()
}
}
func (cfg *config) openFunc(win fyne.Window) func() {
return func(){
openDialog := dialog.NewFileOpen(func(read fyne.URIReadCloser, err error){
if err!=nil{
dialog.ShowError(err,win)
return
}
if read == nil{
return
}
data, err := io.ReadAll(read)
if err!=nil{
dialog.ShowError(err,win)
return
}
defer read.Close()
cfg.EditWidget.SetText(string(data))
cfg.CurrentFile = read.URI()
win.SetTitle(cfg.BaseTitle + "-" + read.URI().Name())
cfg.MenuItem.Disabled = false
},win)
openDialog.SetFilter(filter)
openDialog.Show()
}
}
func (cfg *config) saveFunc(win fyne.Window) func() {
return func(){
if cfg.CurrentFile != nil{
write, err := storage.Writer(cfg.CurrentFile)
if err != nil{
dialog.ShowError(err, win)
return
}
write.Write([]byte(cfg.EditWidget.Text))
defer write.Close()
}
}
}
测试文件
对应成品代码
package main
import (
"testing"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/test"
)
func Test_makeUI(t *testing.T){
var testCfg config
edit, preview := testCfg.makeUI()
test.Type(edit, "Gofjer")
if preview.String() != "Gofjer"{
t.Error("Failed!!!!")
}
}