Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 12 из 25 ~18 мин +50 XP

Вкладки и навигация

Приложение из одного экрана — это калькулятор. Всё остальное живёт в навигации: разделы настроек, панели редактора, документы. Запрос «fyne вкладки» — это пара виджетов widget.AppTabs для разделов приложения и widget.DocTabs для документных панелей с крестиками, плюс контейнер SplitContainer, который делит окно на регулируемые половины. Вкладки fyne — не самая хитрая тема курса: API компактный, и ты уже знаешь всё, что вокруг него, — компоновку четвёртого урока, списки десятого и события шестого. Запрос «навигация fyne» закрывается именно этими тремя инструментами. Соберём вкладки-разделы с нижней панелью, документную панель с перехватом закрытия и рабочее место из дерева файлов и редактора — скелет финального менеджера задач.

Содержание урока · 10 разделов

В каждом десктопном приложении рано или поздно наступает момент, когда VBox уже не справляется: экранов стало много. Fyne даёт три инструмента навигации, и у каждого своя роль. widget.AppTabs — классические вкладки-разделы: сверху или снизу полоса с заголовками, клик переключает содержимое; так делают экраны «Заметки / Архив / Настройки». widget.DocTabs — документные вкладки из редакторов кода: каждая вкладка с крестиком, вкладки можно открывать и закрывать на лету. Контейнер container.SplitContainer — это не вкладки, а делёж площади: две панели и перетаскиваемая разделительная полоса между ними, знакомая тебе по HSplit из десятого урока, где слева был список, справа — детали. Все три инструмента комбинируются: вкладки внутри половины Split, Split внутри вкладки. Поисковый запрос «fyne вкладки» и «fyne tabs» ведут ровно сюда — к паре AppTabs/DocTabs и TabItem, их элементу. Начнём с разделов: три вкладки для будущих экранов менеджера задач.

AppTabs: разделы приложения

GO
package main

import (
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Менеджер задач")

        inbox := widget.NewLabel("Список задач — из десятого урока")
        editor := widget.NewMultiLineEntry()
        editor.SetPlaceHolder("Новая задача...")
        stats := widget.NewLabel("Таблица статистики — из одиннадцатого урока")

        tabs := widget.NewAppTabs(
                widget.NewTabItem("Задачи", inbox),
                widget.NewTabItem("Быстрый ввод", editor),
                widget.NewTabItem("Статистика", stats),
        )
        tabs.SetLocation(widget.TabLocationBottom)
        tabs.OnChanged = func(item *widget.TabItem) {
                window.SetTitle("Менеджер задач — " + item.Text)
        }

        window.SetContent(tabs)
        window.ShowAndRun()
}

widget.NewAppTabs принимает готовые TabItem: каждый — заголовок и содержимое, созданные через widget.NewTabItem. AppTabs сам является виджетом, поэтому его можно отдать SetContent напрямую — а можно вложить в Border или Split. SetLocation уводит полосу вкладок вниз, как в мобильных приложениях. OnChanged получает элемент вкладки — здесь он синхронизирует заголовок окна с открытым разделом.

Какой аргумент получает колбэк tabs.OnChanged?

TabItem: анатомия вкладки#

Элемент вкладки — маленькая структура из трёх полей: Text, Icon и Content. Фабрики widget.NewTabItem(заголовок, содержимое) и widget.NewTabItemWithIcon(заголовок, иконка, содержимое) создают её; иконка — это fyne.Resource, обычно theme.*Icon() или свой ресурс из шестнадцатого урока. Содержимое — обычный CanvasObject: один виджет или контейнер хоть со всей компоновкой курса внутри. Важное следствие: содержимое вкладки создаётся в момент сборки TabItem и живёт в списках Items всё время работы приложения — скрытая вкладка не удаляется, а просто не показывается. Для тяжёлых экранов это повод строить их лениво: создаешь вкладку с заглушкой, в OnChanged при первом открытии наполняешь содержимое данными. Заголовок меняется и после создания: item.Text = «Архив (12)» плюс tabs.Refresh() — полоса перерисуется. Пара DisableItem и EnableItem выключает и включает вкладку: неактивный раздел виден в полосе, но не кликается; есть и варианты по индексу — DisableIndex и EnableIndex. Программное переключение — Select(item) и SelectIndex(i): с точки зрения событий это тот же путь, что клик пользователя, до последнего колбэка.

Сработает ли OnChanged без клика?

Прочитайте код и выберите, что он выведет:

tabs := widget.NewAppTabs(
        widget.NewTabItem("Задачи", inbox),
        widget.NewTabItem("Настройки", settings),
)
tabs.OnChanged = func(item *widget.TabItem) {
        fmt.Println("открыта вкладка: " + item.Text)
}

tabs.SelectIndex(1)
// что напечатается?

TabLocation: где стоит полоса вкладок#

Полоса вкладок настраивается полем Location — значением типа widget.TabLocation. Их четыре: TabLocationTop — полоса сверху, значение по умолчанию; TabLocationBottom — снизу, приём из мобильных интерфейсов, где большим пальцем дотянуться до нижнего края проще; TabLocationLeading — боковая полоса слева с вертикальными заголовками; TabLocationRight и TabLocationLeft — устаревшие синонимы Leading, оставшиеся от старых версий: компилятор напомнит, что пользоваться ими не стоит. Задать можно двумя способами: полем tabs.Location = widget.TabLocationBottom с последующим Refresh или методом tabs.SetLocation(widget.TabLocationBottom), который сам дёргает перерисовку — каноничное правило сеттеров из второго и третьего уроков в действии. В боковой раскладке ведут себя вкладки с длинными заголовками: полоса забирает ширину под самый длинный заголовок, так что «Быстрый ввод» лучше сократить. Комбинируется Location свободно: боковые вкладки слева плюс Split справа — готовый скелет панели администратора; нижняя полоса плюс Border сверху — мобильный лэйаут, который почти без правок переживёт сборку под Android из двадцать второго урока.

Нужно перенести полосу вкладок вниз окна. Какой вызов правильный и почему?

DocTabs: документные вкладки#

DocTabs — родной брат AppTabs с одной судьбоносной разницей: у каждой вкладки появляется крестик, и пользователь может закрывать документы. API смены вкладок тот же — Items, Select, OnChanged, Location, — добавляются два колбэка на закрытие. OnClosed вызывается уже после того, как вкладка удалена из Items, — тут чистят за ней ресурсы. Главная кнопка — CloseIntercept: если он задан, крестик больше не удаляет вкладку сам, а вызывает твою функцию, и решение остаётся за тобой — удалить вкладку вызовом tabs.Remove(item), показать подтверждение или отказать. Это вкладочный близнец SetCloseIntercept у окна из второго урока: перехватчик полностью берёт ответственность на себя. Канонический сценарий — несохранённый документ: помечай изменённые вкладки точкой или звёздочкой в заголовке, а в перехватчике либо закрывай сохранённое, либо предлагай сохранить перед выходом; сам инструмент подтверждения — тема следующего урока, там же вернёмся к этому сценарию с диалогом. Важно: без CloseIntercept DocTabs удаляет вкладку сразу и безвозвратно — Content не сохраняется нигде, данные живут только в твоих структурах.

Перехват закрытия несохранённой вкладки

GO
package main

import (
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Заметки")

        status := widget.NewLabel("Открыто 2 документа")

        docs := widget.NewDocTabs(
                widget.NewTabItem("shopping.txt", widget.NewLabel("молоко, хлеб")),
                widget.NewTabItem("ideas.txt", widget.NewLabel("свой виджет...")),
        )

        docs.CloseIntercept = func(item *widget.TabItem) {
                if item.Text == "ideas.txt" {
                        item.Text = "ideas.txt ●"
                        docs.Refresh()
                        status.SetText("ideas.txt не сохранён — закрытие отклонено")
                        return
                }
                docs.Remove(item)
        }
        docs.OnClosed = func(item *widget.TabItem) {
                status.SetText("Закрыто: " + item.Text)
        }

        window.SetContent(container.NewBorder(nil, status, nil, nil, docs))
        window.ShowAndRun()
}

CloseIntercept получил вкладку и решил её судьбу: ideas.txt помечен точкой в заголовке и остался открыт — без вызова docs.Remove вкладка не удаляется; shopping.txt удаляется явно. После закрытия сработает OnClosed и обновит статус. Заметь: смена item.Text требует docs.Refresh — поле заголовка само себя не перерисовывает.

CloseIntercept задан, но ничего не удаляет. Что произойдёт по клику на крестик?

SplitContainer: регулируемый делёж#

Вкладки переключают экраны, SplitContainer делит площадь. container.NewHSplit(левая, правая) и container.NewVSplit(верхняя, нижняя) возвращают контейнер *container.Split с полем Offset — относительной позицией разделителя от 0.0 до 1.0. Offset 0.25 означает: левая панель получает четверть площади, разделитель стоит там же, где ты его перетащил мышью. Пользователь двигает полосу сам, программа может задать стартовую позицию — присвой split.Offset = 0.3 до показа окна или вызови split.SetOffset(0.3); снова правило сеттеров: запись в поле без Refresh молчит, если окно уже показано. В каноничном сочетании с коллекциями из двух прошлых уроков Split — это рабочее место: слева дерево файлов или список задач, справа редактор; именно так устроен финальный проект. Смена панелей местами делается просто перестановкой аргументов. Вложение — свободное: DocTabs в правой половине, HSplit внутри вкладки — глубина дерева контейнеров ограничена только здравым смыслом; канон компоновки из четвёртого урока здесь работает без оговорок, потому что Split — обычный контейнер, требующий от детей MinSize и отдающий им весь остаток. Отдельный бонус — запоминание раскладки: пользователь передвинул разделитель — сохрани Offset в preferences из девятнадцатого урока и верни при следующем запуске, интерфейс станет «своим».

Рабочее место: дерево файлов и вкладки документов

GO
package main

import (
        "fyne.io/fyne/v2/app"
        "fyne.io/fyne/v2/container"
        "fyne.io/fyne/v2/widget"
)

func main() {
        myApp := app.New()
        window := myApp.NewWindow("Редактор")

        files := widget.NewTreeWithStrings(map[string][]string{
                "":            {"ui", "store", "main.go"},
                "ui":          {"tabs.go", "list.go"},
                "tabs.go":     nil,
                "list.go":     nil,
                "store":       {"notes.go"},
                "notes.go":    nil,
                "main.go":     nil,
        })

        editor := widget.NewMultiLineEntry()
        editor.SetPlaceHolder("Текст файла...")

        docs := widget.NewDocTabs(
                widget.NewTabItem("main.go", editor),
        )
        docs.SetLocation(widget.TabLocationBottom)

        split := container.NewHSplit(files, docs)
        split.Offset = 0.25

        window.SetContent(split)
        window.ShowAndRun()
}

Три инструмента урока в одном окне: дерево файлов из одиннадцатого урока слева, DocTabs с документами справа, полоса вкладок снизу, Split делит площадь — Offset 0.25 отдаёт четверть дереву. Пользователь может перетащить разделитель и растянуть любую половину: каркас переложит MinSize обоих детей и разделит остаток по Offset.

Куда встанет разделитель

Прочитайте код и выберите, что он выведет:

split := container.NewHSplit(leftPanel, rightPanel)
split.Offset = 0.9
window.SetContent(split)
window.ShowAndRun()
// где окажется разделительная полоса?

Собери вкладки настроек

Восстанови четыре пропуска: конструктор вкладок, фабрика элемента, программное переключение и расположение полосы.

tabs := widget.( widget.("Задачи", inbox), widget.NewTabItem("Настройки", settings), ) tabs.(1) // открыть вкладку «Настройки» из кода tabs.SetLocation(widget.) // полоса вкладок — снизу окна

Путь переключения вкладки

Расставь по порядку, что происходит между кликом по вкладке и колбэком OnChanged.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Каталог API вкладок и навигации#

APIЧто делаетКогда звать
widget.NewAppTabs(items...)вкладки-разделы приложенияодин раз при сборке интерфейса
widget.NewDocTabs(items...)документные вкладки с крестикамипанель открытых документов
NewTabItem(text, content)элемент вкладки: заголовок и содержимоепри создании и Append
Items / Append / Remove / RemoveIndexсписок вкладок и его правкадинамические документы
Select(item) / SelectIndex(i)программное переключение, тот же путь, что кликоткрытие раздела из логики
CurrentTab / CurrentTabIndexчто открыто сейчасв колбэках и перед переключением
OnChanged func(*TabItem)факт смены вкладкистатусная строка, ленивое наполнение
Location / SetLocationTop, Bottom, Leading или Trailingмобильный лэйаут и боковые панели
CloseIntercept / OnClosed (DocTabs)перехват закрытия и факт после негонесохранённые документы
container.NewHSplit / NewVSplit + Offsetрегулируемый делёж площадирабочее место из двух панелей

Сколько вкладок и что внутри#

Практика интерфейсов, а не API: AppTabs хорош на трёх-семи разделах — полоса из двадцати заголовков превращается во вторую навигацию и перестаёт помогать. Заголовки пиши короткими существительными и не пиши их заглавными — полоса сама выделит активную вкладку. Содержимое вкладок держи лёгким на старте: как сказано выше, всё живёт в памяти; таблица на сто тысяч строк, спрятанная в невидимой вкладке, всё равно отняла бы время сборки — наполняй такие экраны лениво при первом OnChanged. DocTabs любит динамические заголовки: имя файла плюс точка-маркер изменённости читается мгновенно. Offset подбирается под доминирующую панель: у редактора 0.75, у дерева файлов 0.2-0.3. И помни про минимальные размеры: спрячь в панель виджет без MinSize — пользователь сожмёт её в ноль; защита — MinSize собственного виджета из восьмого урока или отступ-минимум в контейнере. На мобильной сборке нижняя полоса вкладок и вертикальный Split дают почти нативное поведение — проверяется запуском go run -tags mobile из первого урока. Напоследок замечание про параллель: полоса вкладок — не замена поиску и не каталог: если разделов много, но все они узкие, дерево из одиннадцатого урока внутри боковой панели Leading покажет структуру глубже, чем плоский ряд заголовков, а вкладки оставь документам и настройкам.

Что дальше: диалоги и подтверждения#

Навигация собрана, но в сценарии DocTabs осталась дыра: перехватчик закрытия умеет только отклонять или помечать вкладку, а настоящий вопрос «документ не сохранён — закрыть?» задаёт диалог. Тринадцатый урок закрывает её: dialog.ShowConfirm, уведомления, файловый выбор и модальность — там же вернёмся к SetCloseIntercept окна из второго урока и сделаем честный выход с подтверждением. Материал этого урока опирается на компоновку четвёртого, списки десятого и дерево одиннадцатого. Полный маршрут из двадцати пяти уроков — на странице курса Fyne. Домашнее задание соберёт многоэкранное приложение с боковой навигацией и документными вкладками — скелет, в который лягут и диалоги следующего урока, и настройки из девятнадцатого.

Термины урока

Термин

widget.AppTabs

нажмите, чтобы перевернуть
Определение

Вкладки-разделы приложения: Items []*TabItem, Select/SelectIndex, CurrentTab/CurrentTabIndex, OnChanged func(*TabItem), Location полосы. Элементы — widget.NewTabItem.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный экран

HTML-модель виджет-дерева fyne
Вкладкиfyne · мок

Первая вкладка

Журнал событий

Экран собран: «Вкладки»

Коротко: что запомнить

  • AppTabs — вкладки-разделы: NewTabItem для элементов, Select и SelectIndex для программного переключения, OnChanged func(*TabItem) для реакции.
  • Программное переключение вкладки проходит тот же путь, что и клик: OnChanged срабатывает и без пользователя.
  • Полоса вкладок ставится Location или SetLocation: Top, Bottom, Leading, Trailing; Left и Right устарели.
  • DocTabs добавляет крестики: CloseIntercept перехватывает закрытие и решает судьбу вкладки сам, OnClosed срабатывает после удаления.
  • Смена item.Text требует tabs.Refresh — правило сеттеров действует и на заголовки вкладок.
  • SplitContainer: NewHSplit и NewVSplit с Offset от 0.0 до 1.0 — регулируемый делёж площади под рабочее место.
  • Содержимое всех вкладок живёт в памяти: тяжёлые экраны наполняй лениво при первом OnChanged.

Домашнее задание

Собери каркас «рабочего стола»: слева AppTabs с полосой TabLocationLeading и тремя разделами — «Задачи» со списком из десятого урока, «Статистика» с таблицей из одиннадцатого, «Настройки» с парой полей формы из пятого; OnChanged пишет открытый раздел в заголовок окна. Справа — DocTabs с тремя документами: два открываются крестиком без вопросов, у третьего CloseIntercept помечает вкладку точкой и отклоняет закрытие, повторный крестик на сохранённой версии закрывает её через Remove. Панели соедини в VSplit с Offset 0.7, чтобы полоса документов была снизу. Проверь руками: перетаскивание разделителя до упора, программное открытие каждого раздела кнопками в «Настройках» через SelectIndex, ленивое наполнение вкладки «Статистика» — данные считаются только при первом открытии. Ловушка: смени заголовок вкладки без Refresh и посмотри, сколько попыток понадобится, чтобы заметить, что текст не обновился.

Личные заметкиПусто
Хранятся только в вашем браузере (Fyne · Вкладки и навигация)

Вопросы и ответы по уроку

Сколько вкладок выдержит AppTabs и когда это уже перебор?

Технически — сколько угодно: полоса прокручивается. Практически — пять-семь заголовков: дальше полоса превращается в дополнительную навигацию, и разделы стоит сгруппировать в подэкраны или боковое меню с деревом из одиннадцатого урока.

Как показать диалог подтверждения при закрытии вкладки?

В CloseIntercept вызывай dialog.ShowConfirm из следующего урока: подтверждение — docs.Remove(item), отказ — просто вернись. Содержимое вкладки при Remove не сохраняется, данные держи в своих структурах.

SplitContainer — это layout или контейнер?

Контейнер, как NewMax или NewBorder: container.NewHSplit возвращает *container.Split, который сам компоновит двух детей и хранит Offset. Дополнительных layout-параметров у него нет, а детям он отдаёт всю площадь своих половин.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Cobra25 уроков~10 чпрактика25 уроков уже доступны

Cobra: CLI-приложения на Go

Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.

  • Создавать CLI-приложения с деревом команд на Cobra
  • Работать с аргументами, флагами и их привязкой к конфигу
  • Управлять конфигурацией через Viper: файлы, env, приоритеты
  • Писать хуки PreRun/PostRun и корректно обрабатывать ошибки