前面学习了如何用Fyne 简单搭建一个GUI框架
而本网站的博客是使用Markdown进行创作的,理所当然的要写一个 基于Fyne的Markdown编辑器
程序框架搭建
main.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package main
import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" "io" "time" )
type config struct { Edit *widget.Entry Preview *widget.RichText CurrentFile fyne.URI MenuItem *fyne.MenuItem }
var cfg config
func main() { a := app.New() w := a.NewWindow("Markdown编辑器")
w.SetContent(container.NewHSplit(cfg.makeUI())) cfg.createMenu(w) w.Resize(fyne.NewSize(800, 600)) w.CenterOnScreen() w.ShowAndRun() }
func (cfg *config) makeUI() (*widget.Entry, *widget.RichText) { edit := widget.NewMultiLineEntry() preview := widget.NewRichTextFromMarkdown("")
edit.OnChanged = preview.ParseMarkdown
cfg.Edit = edit cfg.Preview = preview return edit, preview } func (cfg *config) createMenu(win fyne.Window) {
}
|
在这里,使用了一个自定义的config结构,里面存储了会用到的一些属性,如输入的文本,输出渲染完成的Markdown文本,已经保存文件路径等。
使用了container.NewHSplit
新容器,它以垂直的分割线左右平分页面。
在makeUI
中,将Edit
和Preview
等组件与cfg关联起来,而edit.OnChanged = preview.ParseMarkdown
则是在编辑区发生变动的时候调用Markdown富文本渲染。
创建菜单

main.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| func (cfg *config) createMenu(win fyne.Window) { open := fyne.NewMenuItem("Open", cfg.open(win)) save := fyne.NewMenuItem("save", cfg.save(win)) cfg.MenuItem = save save.Disabled = true
saveAs := fyne.NewMenuItem("save as", cfg.saveAsFunc(win))
file := fyne.NewMenu("File", open, save, saveAs) menu := fyne.NewMainMenu(file) win.SetMainMenu(menu) }
func (cfg *config) save(win fyne.Window) func() {
}
func (cfg *config) open(win fyne.Window) func() {
}
func (cfg *config) saveAsFunc(win fyne.Window) func() {
}
|
这里将save 赋值给cfg是为了保证 没有打开文件和另存为文件前,用户无法进行保存操作,防止不可预知的错误出现 。
后续会通过cfg来设置允许保存。
通过回调函数分离实现功能。
实现对应功能
main.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| func (cfg *config) save(win fyne.Window) func() { return func() { writer, err := storage.Writer(cfg.CurrentFile) if err != nil { dialog.ShowError(err, win) return } if writer == nil { return } defer writer.Close()
writer.Write([]byte(cfg.Edit.Text)) } }
func (cfg *config) open(win fyne.Window) func() { return func() { openDialog := dialog.NewFileOpen(func(closer fyne.URIReadCloser, err error) { if err != nil { dialog.ShowError(err, win) return } if closer == nil { return } defer closer.Close()
data, err := io.ReadAll(closer) if err != nil { dialog.ShowError(err, win) return }
cfg.Edit.SetText(string(data))
cfg.CurrentFile = closer.URI()
win.SetTitle("Markdown编辑器-" + closer.URI().Name())
cfg.MenuItem.Disabled = false
}, win)
openDialog.Show() } }
func (cfg *config) saveAsFunc(win fyne.Window) func() { return func() { saveDialog := dialog.NewFileSave(func(closer fyne.URIWriteCloser, err error) {
if err != nil { dialog.ShowError(err, win) return } if closer == nil { return } defer closer.Close() closer.Write([]uint8(cfg.Edit.Text))
cfg.CurrentFile = closer.URI()
win.SetTitle("Markdown编辑器-" + closer.URI().Name())
cfg.MenuItem.Disabled = false
}, win) saveDialog.SetFileName("未命名-" + time.DateOnly + ".md") saveDialog.Show() } }
|
至此,基础的Markdown编辑器 的功能就完成了。
但是打开的文件不只有Markdown怎么办呢
为解决这个问题,可以使用storage.NewExtensionFileFilter
来过滤掉不需要的文件。
main.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ... var cfg config + var filter = storage.NewExtensionFileFilter([]string{".md", ".MD"})
func main() { ... } ... func (cfg *config) open(win fyne.Window) func() { return func() { ... + openDialog.SetFilter(filter)
openDialog.Show() } }
func (cfg *config) saveAsFunc(win fyne.Window) func() { return func() { ... + saveDialog.SetFilter(filter)
saveDialog.SetFileName("未命名-" + time.DateOnly + ".md") saveDialog.Show() } }
|
虽然目前已经大体上完成,但还是有一些Bug
- 可能保存的文件类型错误(缺省文件类型)
- 都在一个文件夹内太过臃肿
对保存文件的类型进行检查
在saveAsFunc的方法内部添加一下语句:
main.go1 2 3 4 5 6 7 8 9 10 11
| ... defer closer.Close()
+if !strings.HasSuffix(strings.ToLower(closer.URI().String()), ".md") { + dialog.ShowInformation("文件类型错误", "必须是 .md 或 .MD", win) + return +}
closer.Write([]uint8(cfg.Edit.Text)) ...
|
分开存放在不同的文件下,便于管理
此次实操,我就将其分为
注意,Golang虽然没有public,protected,private这样的修饰符,但能够通过更改首字母的大小写来更改可见性。
若首字母是大写字母,则表示可以被其他包通过 import 来访问,相当于公开;而小写字母的话,则表示在本包内使用,相当于私有。
因此,在本节内容的分开存放源代码并不影响其变量和struct等的可见性,直接复制粘贴即可。因为他们都在 package main
下面。
最后的源码
main.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package main
import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" )
type config struct { Edit *widget.Entry Preview *widget.RichText CurrentFile fyne.URI MenuItem *fyne.MenuItem }
var cfg config
var filter = storage.NewExtensionFileFilter([]string{".md", ".MD"})
func main() { a := app.New() w := a.NewWindow("Markdown编辑器")
w.SetContent(container.NewHSplit(cfg.makeUI())) cfg.createMenu(w) w.Resize(fyne.NewSize(800, 600)) w.CenterOnScreen() w.ShowAndRun() }
|
ui.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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("")
edit.OnChanged = preview.ParseMarkdown
cfg.Edit = edit cfg.Preview = preview return edit, preview }
func (cfg *config) createMenu(win fyne.Window) { open := fyne.NewMenuItem("Open", cfg.open(win)) save := fyne.NewMenuItem("save", cfg.save(win)) cfg.MenuItem = save save.Disabled = true
saveAs := fyne.NewMenuItem("save as", cfg.saveAsFunc(win))
file := fyne.NewMenu("File", open, save, saveAs)
menu := fyne.NewMainMenu(file) win.SetMainMenu(menu) }
|
config.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| package main
import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "io" "strings" "time" )
func (cfg *config) save(win fyne.Window) func() { return func() { writer, err := storage.Writer(cfg.CurrentFile) if err != nil { dialog.ShowError(err, win) return } if writer == nil { return } defer writer.Close()
writer.Write([]byte(cfg.Edit.Text)) } }
func (cfg *config) open(win fyne.Window) func() { return func() { openDialog := dialog.NewFileOpen(func(closer fyne.URIReadCloser, err error) { if err != nil { dialog.ShowError(err, win) return } if closer == nil { return } defer closer.Close()
data, err := io.ReadAll(closer) if err != nil { dialog.ShowError(err, win) return }
cfg.Edit.SetText(string(data))
cfg.CurrentFile = closer.URI()
win.SetTitle("Markdown编辑器-" + closer.URI().Name())
cfg.MenuItem.Disabled = false
}, win) openDialog.SetFilter(filter)
openDialog.Show() } }
func (cfg *config) saveAsFunc(win fyne.Window) func() { return func() { saveDialog := dialog.NewFileSave(func(closer fyne.URIWriteCloser, err error) {
if err != nil { dialog.ShowError(err, win) return } if closer == nil { return } defer closer.Close()
if !strings.HasSuffix(strings.ToLower(closer.URI().String()), ".md") { dialog.ShowInformation("文件类型错误", "必须是 .md 或 .MD", win) return }
closer.Write([]uint8(cfg.Edit.Text))
cfg.CurrentFile = closer.URI()
win.SetTitle("Markdown编辑器-" + closer.URI().Name())
cfg.MenuItem.Disabled = false
}, win)
saveDialog.SetFilter(filter)
saveDialog.SetFileName("未命名-" + time.DateOnly + ".md") saveDialog.Show() } }
|
打包
接下来使用下面的命令进行打包即可随时随地使用了。
1
| $ fyne package -os windows -icon lnpbqc.png
|