Golang中的TUI网格/矩阵控件实现

Golang中的TUI网格/矩阵控件实现 我想构建一个带有用户界面的终端/控制台应用程序,并大量使用行和列组成的网格中的多个可选选项。类似矩阵的形状:

±-----------------------------------------+
| [选项11] [选项12] [选项13] |
| [选项21] [选项22] [选项23] |
| [选项31] [选项32] [选项33] |
±-----------------------------------------+

我已经开始尝试使用广泛使用的 Bubbletea 和 LipGloss 包,但它们的表格组件逻辑只能处理光标的行位置,不能处理列位置。

我想问,我是必须自己实现这样一个组件(比如一组多个列表组件),还是已经有现成的解决方案可以使用?


更多关于Golang中的TUI网格/矩阵控件实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

嗯,现在我明白了,lipgloss/table 确实为行和列的参数提供了 StyleFunc 方法。所以之前折腾 bubbles/table 包完全是走错了路……

更多关于Golang中的TUI网格/矩阵控件实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中实现TUI网格/矩阵控件,确实需要自己构建或寻找合适的库。Bubbletea本身不提供直接的网格控件,但可以通过组合现有组件或自定义实现。以下是两种实现方案:

方案一:基于Bubbletea的自定义网格实现

package main

import (
    "fmt"
    "strings"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
)

// 网格项结构
type GridItem struct {
    Text     string
    Selected bool
    Row      int
    Col      int
}

// 网格模型
type GridModel struct {
    items     [][]GridItem
    rows      int
    cols      int
    cursorRow int
    cursorCol int
    width     int
    height    int
}

// 初始化网格
func NewGridModel(rows, cols int) GridModel {
    items := make([][]GridItem, rows)
    for r := 0; r < rows; r++ {
        items[r] = make([]GridItem, cols)
        for c := 0; c < cols; c++ {
            items[r][c] = GridItem{
                Text:     fmt.Sprintf("选项%d%d", r+1, c+1),
                Selected: false,
                Row:      r,
                Col:      c,
            }
        }
    }
    return GridModel{
        items:     items,
        rows:      rows,
        cols:      cols,
        cursorRow: 0,
        cursorCol: 0,
    }
}

// 视图渲染
func (m GridModel) View() string {
    var builder strings.Builder
    
    // 定义样式
    normalStyle := lipgloss.NewStyle().
        Border(lipgloss.RoundedBorder()).
        Padding(0, 1).
        Margin(0, 1)
    
    selectedStyle := normalStyle.Copy().
        Foreground(lipgloss.Color("#FFFFFF")).
        Background(lipgloss.Color("#FF6B6B")).
        BorderForeground(lipgloss.Color("#FF6B6B"))
    
    cursorStyle := normalStyle.Copy().
        BorderForeground(lipgloss.Color("#00FF00")).
        Bold(true)
    
    // 渲染网格
    for r := 0; r < m.rows; r++ {
        for c := 0; c < m.cols; c++ {
            item := m.items[r][c]
            var style lipgloss.Style
            
            switch {
            case r == m.cursorRow && c == m.cursorCol:
                style = cursorStyle
            case item.Selected:
                style = selectedStyle
            default:
                style = normalStyle
            }
            
            builder.WriteString(style.Render(item.Text))
        }
        builder.WriteString("\n")
    }
    
    return builder.String()
}

// 更新逻辑
func (m GridModel) Update(msg tea.Msg) (GridModel, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "up":
            if m.cursorRow > 0 {
                m.cursorRow--
            }
        case "down":
            if m.cursorRow < m.rows-1 {
                m.cursorRow++
            }
        case "left":
            if m.cursorCol > 0 {
                m.cursorCol--
            }
        case "right":
            if m.cursorCol < m.cols-1 {
                m.cursorCol++
            }
        case "enter", " ":
            // 切换选择状态
            m.items[m.cursorRow][m.cursorCol].Selected = 
                !m.items[m.cursorRow][m.cursorCol].Selected
        }
    }
    return m, nil
}

// 主程序
func main() {
    model := NewGridModel(3, 3)
    p := tea.NewProgram(model)
    if _, err := p.Run(); err != nil {
        panic(err)
    }
}

方案二:使用tview库的网格实现

package main

import (
    "github.com/rivo/tview"
)

func main() {
    app := tview.NewApplication()
    
    // 创建网格
    grid := tview.NewGrid().
        SetRows(3, 3, 3).
        SetColumns(10, 10, 10).
        SetBorders(true)
    
    // 创建可选择的按钮
    buttons := make([][]*tview.Button, 3)
    for row := 0; row < 3; row++ {
        buttons[row] = make([]*tview.Button, 3)
        for col := 0; col < 3; col++ {
            text := fmt.Sprintf("选项%d%d", row+1, col+1)
            button := tview.NewButton(text).
                SetSelectedFunc(func() {
                    // 处理选择逻辑
                    app.Stop()
                })
            
            buttons[row][col] = button
            grid.AddItem(button, row, col, 1, 1, 0, 0, true)
        }
    }
    
    // 设置网格导航
    for row := 0; row < 3; row++ {
        for col := 0; col < 3; col++ {
            // 设置导航关系
            if col > 0 {
                buttons[row][col].SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                    if event.Key() == tcell.KeyLeft {
                        app.SetFocus(buttons[row][col-1])
                        return nil
                    }
                    return event
                })
            }
            if col < 2 {
                buttons[row][col].SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                    if event.Key() == tcell.KeyRight {
                        app.SetFocus(buttons[row][col+1])
                        return nil
                    }
                    return event
                })
            }
        }
    }
    
    if err := app.SetRoot(grid, true).Run(); err != nil {
        panic(err)
    }
}

方案三:增强型网格组件(支持多选)

type EnhancedGrid struct {
    cells      [][]*Cell
    rows, cols int
    focusRow   int
    focusCol   int
    selected   map[[2]int]bool
}

type Cell struct {
    content string
    focused bool
    selected bool
}

func NewEnhancedGrid(rows, cols int) *EnhancedGrid {
    cells := make([][]*Cell, rows)
    for i := range cells {
        cells[i] = make([]*Cell, cols)
        for j := range cells[i] {
            cells[i][j] = &Cell{
                content: fmt.Sprintf("选项%d%d", i+1, j+1),
            }
        }
    }
    
    return &EnhancedGrid{
        cells:    cells,
        rows:     rows,
        cols:     cols,
        selected: make(map[[2]int]bool),
    }
}

func (g *EnhancedGrid) MoveFocus(direction string) {
    switch direction {
    case "up":
        if g.focusRow > 0 {
            g.cells[g.focusRow][g.focusCol].focused = false
            g.focusRow--
            g.cells[g.focusRow][g.focusCol].focused = true
        }
    case "down":
        if g.focusRow < g.rows-1 {
            g.cells[g.focusRow][g.focusCol].focused = false
            g.focusRow++
            g.cells[g.focusRow][g.focusCol].focused = true
        }
    case "left":
        if g.focusCol > 0 {
            g.cells[g.focusRow][g.focusCol].focused = false
            g.focusCol--
            g.cells[g.focusRow][g.focusCol].focused = true
        }
    case "right":
        if g.focusCol < g.cols-1 {
            g.cells[g.focusRow][g.focusCol].focused = false
            g.focusCol++
            g.cells[g.focusRow][g.focusCol].focused = true
        }
    }
}

func (g *EnhancedGrid) ToggleSelection() {
    key := [2]int{g.focusRow, g.focusCol}
    if g.selected[key] {
        delete(g.selected, key)
        g.cells[g.focusRow][g.focusCol].selected = false
    } else {
        g.selected[key] = true
        g.cells[g.focusRow][g.focusCol].selected = true
    }
}

func (g *EnhancedGrid) GetSelected() []string {
    var selected []string
    for key := range g.selected {
        selected = append(selected, g.cells[key[0]][key[1]].content)
    }
    return selected
}

这些实现提供了完整的行列导航、选择状态管理和视觉反馈。第一种方案使用纯Bubbletea,第二种方案使用tview库,第三种方案展示了更复杂的网格逻辑。根据具体需求选择合适的实现方式。

回到顶部