Implementação de GUI em Go Tcl/Tk sem CGo (modernc.org/tk9.0): empacotamento de widgets e renderização de imagens

Python possui bibliotecas de GUI integradas por padrão, como o Tkinter. Recentemente, foi desenvolvida a CGo-Free, Cross Platform Tk 라이브러리, que permite usar Tcl/Tk também na linguagem Go. Hoje, vamos examinar seus fundamentos de uso.
Criando um Hello, Tk
Vamos começar com um exemplo simples de 'Hello, TK!'.
1package main
2
3import tk "modernc.org/tk9.0"
4
5func main() {
6 tk.Pack(
7 tk.TButton(
8 tk.Txt("Hello, TK!"),
9 tk.Command(func() {
10 tk.Destroy(tk.App)
11 })),
12 tk.Ipadx(10), tk.Ipady(5), tk.Padx(15), tk.Pady(10),
13 )
14 tk.App.Wait()
15}

Vamos examinar detalhadamente o código de exemplo acima e seu resultado de execução.
Para quem já tem experiência com Tk em Python, a estrutura em que widgets são empacotados dentro de uma janela ou diretamente sob ela será familiar. Dependendo do tipo de widget, rótulos e outros elementos são incluídos dentro dele.
Ipadx e Ipady são abreviações de Internal padding, que ajustam o espaçamento interno dos widgets. Neste exemplo, o espaçamento do botão é ajustado.
Esta biblioteca possui a estrutura Window, e a variável App gerencia a janela de nível superior. Ela já está predefinida dentro da biblioteca. Assim, a função tk.App.Destroy(), que encerra tk.App.Wait(), exerce o papel de fechar a janela de nível superior.
Agora, vamos examinar alguns exemplos presentes na pasta _examples do GitLab.
Processando arquivos SVG
A seguir, um exemplo que exibe um arquivo SVG em um widget de rótulo.
1package main
2
3import . "modernc.org/tk9.0"
4
5// https://en.wikipedia.org/wiki/SVG
6const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
7<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
8<svg width="391" height="391" viewBox="-70.5 -70.5 391 391" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9<rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
10<g opacity="0.8">
11 <rect x="25" y="25" width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
12 <circle cx="125" cy="125" r="75" fill="orange" />
13 <polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
14 <line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
15</g>
16</svg>`
17
18func main() {
19 Pack(Label(Image(NewPhoto(Data(svg)))),
20 TExit(),
21 Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
22 App.Center().Wait()
23}

O processo de tratamento de SVG nesta biblioteca é o seguinte:
- Lê o conteúdo do arquivo SVG como uma string (ou o inclui diretamente, como no exemplo acima).
- Passa esse conteúdo para a função Data, convertendo-o em uma string contendo opções (opção -data).
- Os bytes convertidos são passados para a função NewPhoto, que retorna um ponteiro para a estrutura Img, representando uma imagem Tcl/Tk.
- Ao passar pela função Image, o ponteiro da estrutura Img é convertido em uma string à qual é adicionada a opção -Image.
- A conversão para uma string contendo o valor RAW da estrutura se deve à criação do widget Label.
Arquivos ICO e PNG também são processados de maneira semelhante.
Processando arquivos PNG
1package main
2
3import _ "embed"
4import . "modernc.org/tk9.0"
5
6//go:embed gopher.png
7var gopher []byte
8
9func main() {
10 Pack(Label(Image(NewPhoto(Data(gopher)))),
11 TExit(),
12 Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
13 App.Center().Wait()
14}

O processo de tratamento de arquivos PNG é o seguinte:
- Converte o gopher.png embutido em um tipo string contendo opções.
- Converte para o tipo *Img por meio da função NewPhoto.
- Após passar pela função Image e ser convertido em uma string RAW, é criado como um widget de rótulo.
Arquivos ICO são processados do mesmo modo, e a única diferença em relação ao formato SVG está no modo como a função Data trata internamente os dados.
Vamos examinar a natureza da "string contendo opções" mencionada acima:
1type rawOption string
A string contendo opções mencionada anteriormente é simplesmente uma string formatada.
1func (w *Window) optionString(_ *Window) string {
2 return w.String()
3}
O método optionString é um método do ponteiro Window e retorna uma string.
Por fim, vamos examinar brevemente a estrutura interna da função Data:
1func Data(val any) Opt {
2 switch x := val.(type) {
3 case []byte:
4 switch {
5 case bytes.HasPrefix(x, pngSig):
6 // ok
7 case bytes.HasPrefix(x, icoSig):
8 b := bytes.NewBuffer(x)
9 img, err := ico.Decode(bytes.NewReader(x))
10 if err != nil {
11 fail(err)
12 return rawOption("")
13 }
14
15 b.Reset()
16 if err := png.Encode(b, img); err != nil {
17 fail(err)
18 return rawOption("")
19 }
20
21 val = b.Bytes()
22 }
23 }
24 return rawOption(fmt.Sprintf(`-data %s`, optionString(val)))
25}
Pelo código, nota-se que, no caso de arquivos ICO ou PNG, é necessário um processo de codificação/decodificação. Nos demais casos, não há conversão especial: apenas a opção -data é acrescentada à string convertida em bytes, indicando que se trata do resultado da função Data.
Carregando imagens com um widget de menu
Se adicionarmos um widget de menu aos exemplos de carregamento de PNG e ICO que praticamos anteriormente, poderemos criar um aplicativo que exibe a imagem desejada.
Primeiro, vamos examinar um exemplo simples de widget de menu.
1package main
2
3import (
4 "fmt"
5 . "modernc.org/tk9.0"
6 "runtime"
7)
8
9func main() {
10 menubar := Menu()
11
12 fileMenu := menubar.Menu()
13 fileMenu.AddCommand(Lbl("New"), Underline(0), Accelerator("Ctrl+N"))
14 fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func() { GetOpenFile() }))
15 Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(1) }))
16 fileMenu.AddCommand(Lbl("Save As..."), Underline(5))
17 fileMenu.AddSeparator()
18 fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
19 Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(4) }))
20 menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
21
22 App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
23 App.Configure(Mnu(menubar), Width("8c"), Height("6c")).Wait()
24}

Neste exemplo, experimentamos uma barra de menus e a criação de menus, destaque de caracteres, a opção Command, a vinculação de atalhos de teclado e a definição do tamanho inicial da janela da aplicação.
Agora, vamos modificar a função Command especificada em GetOpenFile para criar um programa que carrega e exibe imagens.
O planejamento para escrever o programa é o seguinte:
- Restringir a abertura apenas a arquivos PNG e ICO
- Processar o arquivo selecionado na caixa de diálogo de arquivos
- Implementar um widget para exibir a imagem
O código que reflete esse planejamento é o seguinte:
1package main
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "runtime"
8 "strings"
9
10 . "modernc.org/tk9.0"
11)
12
13func handleFileOpen() {
14 s := GetOpenFile()
15 for _, photo := range s {
16 formatCheck := strings.Split(photo, ".")
17 format := formatCheck[len(formatCheck)-1]
18
19 if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
20 picFile, err := os.Open(photo)
21 if err != nil {
22 log.Println("Error while opening photo, error is: ", err)
23 }
24
25 pictureRawData := make([]byte, 10000*10000)
26 picFile.Read(pictureRawData)
27
28 imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
29 Pack(imageLabel,
30 TExit(),
31 Padx("1m"), Pady("2m"), Ipadx("1m"), Ipady("1m"))
32 }
33 picFile.Close()
34 }
35}
36
37func main() {
38 menubar := Menu()
39
40 fileMenu := menubar.Menu()
41 fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(handleFileOpen))
42 Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
43 fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
44 Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
45 menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
46
47 App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
48 App.Configure(Mnu(menubar), Width("10c"), Height("10c")).Wait()
49}

O código acima funciona da seguinte maneira:
- Verifica a extensão do arquivo com as funções de comparação de strings do pacote strings
- Abre, lê e fecha o arquivo usando o pacote os
- A imagem lida é exibida em um widget de rótulo
Conclusão
Neste texto, utilizando a biblioteca Tcl/Tk da linguagem Go, abordamos os seguintes conteúdos:
- Estrutura básica de aplicações GUI
- Processamento de diversos formatos de imagem, como SVG, PNG e ICO
- Empacotamento de widgets e gerenciamento de layout
- Estrutura de processamento de dados de imagem
- Vinculação de atalhos de teclado e comandos de widgets
Ao usar a linguagem Go em conjunto com Tcl/Tk, é possível criar programas GUI simples e, ao mesmo tempo, práticos. Com base nisso, recomendo que você se desafie no desenvolvimento de aplicações GUI mais complexas.