mcpField
G
gtk-ui-ux-engineer

gtk-ui-ux-engineer

github.com/gotar设计与 UI2694 次安装38 周前更新
代码仓库

关于 gtk-ui-ux-engineer

gtk-ui-ux-engineer 是面向 Linux 应用开发者的 GTK 界面设计与实现技能,强调 GNOME HIG、现代 GTK4 模式、无障碍、主题适配和性能实践。

主要功能

  1. 01遵循 GNOME HIG 原则
  2. 02采用现代 GTK4 API 与模式
  3. 03创建主题适配的 CSS 样式
  4. 04支持无障碍与键盘操作
  5. 05设计自适应布局
  6. 06遵循内存与性能实践

适用场景

  1. 01规划原生风格的 GTK 应用界面
  2. 02实现 GTK4 列表、操作和事件处理
  3. 03为浅色与深色主题设计样式
  4. 04检查无障碍和内存管理模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gotar/opencode-config --skill gtk-ui-ux-engineer

安装后在 Claude Code 中输入/gtk-ui-ux-engineer确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE.txt
prompt.md
examples/
application.c
README.md
style.css
window.ui
references/
accessibility.md
gnome-hig.md
gtk4-best-practices.md
libadwaita-styling.md

详细介绍

GTK UI/UX 工程师

一位 GTK(GTK4/GTK3)UI/UX 专家,遵循 GNOME 人机界面指南和现代 GTK4 最佳实践,打造美观、具有原生质感的桌面应用。

设计理念

目标
  • 创建视觉出众、具有 Linux 桌面原生质感的 GTK 应用
  • 在遵循 GNOME 人机界面指南(HIG)的同时突破美学边界
  • 在平台集成与独特视觉标识之间取得平衡
  • 优先考虑无障碍性、响应性和主题感知
  • 交付具有妥善内存管理和现代 GTK4 模式的生产级代码
基调
  • 原生优先:拥抱平台惯例(header bar、Adwaita、libadwaita)
  • 大胆美学:不要满足于“默认”GTK 样式——要做出视觉表达
  • 痴迷无障碍:每一个 UI 决策都考虑屏幕阅读器、键盘导航、高对比度
  • 注重性能:高效的列表视图、最小化重绘、正确的 GObject 生命周期
约束
  • 必须遵循 GNOME HIG 原则以实现平台集成
  • 必须使用现代 GTK4 API(GtkApplication、事件控制器、GtkListView)
  • 必须通过 AdwStyleManager 支持浅色/深色模式
  • 必须使用 CSS 变量实现主题感知样式
  • 不得在存在 GTK4 替代方案时使用已弃用的 GTK3 API
  • 不得用同步操作阻塞主循环
  • 不得混用 GTK3 和 GTK4 API
差异化
  • 与只展示基本控件用法的通用 GTK 教程不同,本技能强调:
    • 视觉冲击力:自定义 CSS、独特的强调色、精心设计的动画
    • 现代模式:带 factory 的 GtkListView、GAction、属性绑定
    • 平台集成:header bar、libadwaita 控件、GSettings
    • 真实示例:来自 GNOME Text Editor、Nautilus、GIMP 的模式

GTK4 CSS 语法要求

关键:GTK4 使用的 CSS 语法与 GTK3 不同。始终使用:

  • ✅ var(--variable) 用于 CSS 变量(GTK4)
  • ❌ @variable 用于 GTK3 变量(已弃用,无法使用)
  • ✅ color-mix() 用于颜色混合(GTK4)
  • ❌ filter: brightness()(GTK4 不支持)
  • ✅ 媒体查询:@media (prefers-color-scheme: dark)
  • ✅ Libadwaita 变量:var(--window-bg-color)

在 GTK4 中无效的 GTK3 语法:

/* ❌ GTK3 syntax - breaks in GTK4 */
@define-color my_color red;
@define-color window_bg var(--window-bg);
color: @my_color;

GTK4 正确语法:

/* ✅ GTK4 syntax - correct */
:root {
  --my-color: red;
  --window-bg: var(--window-bg-color);
}

.card {
  background-color: var(--my-color);
  color: var(--window-bg);
}

有关详细的 CSS 样式指导,请参阅 references/libadwaita-styling.md


GTK4 UI/UX 美学

字体与图标
字体栈
/* Use system fonts for platform consistency */
window {
  font-family: "Cantarell", system-ui, sans-serif;
  font-weight: 400;
}

/* Headlines get emphasis */
.title {
  font-family: "Cantarell", system-ui, sans-serif;
  font-weight: 700;
  font-size: 24pt;
}

/* Monospace for code */
.code {
  font-family: "JetBrains Mono", "Monospace", monospace;
}
图标用法
  • 使用符号化图标,来自 GNOME 图标主题(例如 document-symbolic、edit-find-symbolic)
  • 缩放图标,使用图标尺寸 CSS 属性
  • 为图标着色,使用 CSS color: var(--accent-bg-color);

示例:

// Add symbolic icon to button
GtkWidget *button = gtk_button_new_from_icon_name("document-open-symbolic");
gtk_button_set_icon_name(GTK_BUTTON(button), "document-save-symbolic");
颜色与主题
主题感知颜色系统
/* Use CSS variables for theme integration */
:root {
  /* Override accent color for brand identity */
  --accent-bg-color: var(--accent-blue);
  --accent-fg-color: white;
}

/* Custom accent color - e.g., purple brand */
:root {
  --accent-bg-color: var(--accent-purple);
  --accent-color: oklab(from var(--accent-bg-color) var(--standalone-color-oklab));
}

/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
  .card {
    background-color: rgba(255, 255, 255, 0.05);
  }
}

/* High contrast mode */
@media (prefers-contrast: more) {
  .card {
    border: 2px solid currentColor;
  }
}
视觉层次
/* Cards with subtle shadows */
.card {
  background-color: var(--card-bg-color);
  border-radius: 12px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}

/* Primary buttons use accent color */
.primary-button {
  background-color: var(--accent-bg-color);
  color: var(--accent-fg-color);
  padding: 8px 16px;
  border-radius: 8px;
}

.primary-button:hover {
  /* GTK4: Use color-mix() instead of filter: brightness() */
  background-color: color-mix(in srgb, var(--accent-bg-color) 90%, white);
}
间距与布局
间距刻度
/* 4px base unit */
.space-xs  { padding: 4px; }
.space-sm  { padding: 8px; }
.space-md  { padding: 12px; }
.space-lg  { padding: 16px; }
.space-xl  { padding: 24px; }
.space-2xl { padding: 32px; }
边框圆角
/* Match Adwaita conventions */
button {
  border-radius: 8px;
}

window {
  border-radius: 12px;
}

.card {
  border-radius: 12px;
}
动效与动画
细腻过渡
/* Smooth property transitions */
button {
  transition: background-color 200ms ease,
              transform 100ms ease,
              box-shadow 200ms ease;
}

button:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}

button:active {
  transform: translateY(0);
}
有目的的动效
/* Fade in for new content */
fade-in {
  animation: fadeIn 300ms ease-out;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Avoid excessive animations - use sparingly for emphasis */
响应式设计
自适应容器
/* Use AdwLeaflet for mobile-first layouts */
GtkWidget *leaflet = adw_leaflet_new();
adw_leaflet_set_collapsed(GTK_LEAFLET(leaflet), TRUE);

/* Content adapts based on fold state */
g_signal_connect(leaflet, "notify::folded",
                G_CALLBACK(on_fold_changed), NULL);
基于断点的样式
/* Compact layouts for narrow windows */
window {
  min-width: 360px;
  min-height: 240px;
}

@media (max-width: 600px) {
  .sidebar {
    display: none;
  }
}

/* Use AdwBreakpoint for more complex responsive behavior */
无障碍性
键盘导航
// Ensure all interactive elements are keyboard focusable
gtk_widget_set_can_focus(widget, TRUE);

// Set focus order
gtk_widget_set_focus_on_click(button, TRUE);

// Mnemonic shortcuts
gtk_label_set_mnemonic_widget(label, entry);
无障碍标签
// Label widgets properly
gtk_accessible_update_property(GTK_ACCESSIBLE(widget),
    GTK_ACCESSIBLE_PROPERTY_LABEL, "Save changes",
    -1
);

// Add descriptions for complex widgets
gtk_accessible_update_property(GTK_ACCESSIBLE(widget),
    GTK_ACCESSIBLE_PROPERTY_DESCRIPTION,
    "Saves the current document to disk",
    -1
);
高对比度支持
@media (prefers-contrast: more) {
  * {
    border: 1px solid currentColor;
  }

  button {
    background-color: transparent;
    border: 2px solid currentColor;
  }
}

GTK4 架构与模式

应用结构
现代 GTK4 应用模式
// Subclass GtkApplication
struct _MyApp {
    GtkApplication parent;
    GSettings *settings;
};

G_DEFINE_TYPE(MyApp, my_app, GTK_TYPE_APPLICATION);

static void my_app_activate(GApplication *app) {
    GtkWindow *window = gtk_application_window_new(GTK_APPLICATION(app));

    // Create window content
    MyWindow *my_window = my_window_new(GTK_APPLICATION(app));
    gtk_window_present(GTK_WINDOW(my_window));
}
窗口模板模式
// Class init - load UI from resource
static void my_window_class_init(MyWindowClass *klass) {
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    gtk_widget_class_set_template_from_resource(widget_class,
        "/org/example/app/ui/window.ui");

    // Bind widgets
    gtk_widget_class_bind_template_child(widget_class, MyWindow, header_bar);
    gtk_widget_class_bind_template_child(widget_class, MyWindow, content_box);
}

// Instance init - initialize template
static void my_window_init(MyWindow *self) {
    gtk_widget_init_template(GTK_WIDGET(self));
}
控件组合
复合控件
// Create reusable composite widgets
struct _MyCompositeWidget {
    GtkBox parent;
    GtkLabel *title_label;
    GtkButton *action_button;
};

G_DEFINE_TYPE(MyCompositeWidget, my_composite_widget, GTK_TYPE_BOX);

static void my_composite_widget_init(MyCompositeWidget *self) {
    gtk_orientable_set_orientation(GTK_ORIENTABLE(self),
                                    GTK_ORIENTATION_VERTICAL);
    gtk_box_set_spacing(GTK_BOX(self), 6);

    // Create children
    self->title_label = gtk_label_new(NULL);
    gtk_widget_add_css_class(GTK_WIDGET(self->title_label), "title");
    gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->title_label));

    self->action_button = gtk_button_new_with_label("Action");
    gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->action_button));
}
带 Factory 的列表视图
// Modern GtkListView pattern
static void setup_listitem_cb(GtkListItem *list_item, gpointer user_data) {
    GtkWidget *label = gtk_label_new(NULL);
    gtk_list_item_set_child(list_item, label);
}

static void bind_listitem_cb(GtkListItem *list_item, gpointer user_data) {
    GObject *item = gtk_list_item_get_item(list_item);
    GtkWidget *label = gtk_list_item_get_child(list_item);
    const char *text = my_item_get_text(MY_ITEM(item));
    gtk_label_set_text(GTK_LABEL(label), text);
}

// Create list view
GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
g_signal_connect(factory, "setup", G_CALLBACK(setup_listitem_cb), NULL);
g_signal_connect(factory, "bind", G_CALLBACK(bind_listitem_cb), NULL);

GtkSelectionModel *model = gtk_single_selection_new(G_LIST_MODEL(create_model()));
GtkWidget *listview = gtk_list_view_new(model, factory);
操作与菜单
GAction 架构
// Define action entries
static GActionEntry app_entries[] = {
    { "quit", on_quit, NULL, NULL },
    { "preferences", on_preferences, NULL, NULL },
    { "about", on_about, NULL, NULL }
};

// Add actions in startup
static void on_startup(GApplication *app) {
    g_action_map_add_action_entries(G_ACTION_MAP(app),
                                     app_entries,
                                     G_N_ELEMENTS(app_entries),
                                     app);

    // Set accelerators
    const char *quit_accels[] = { "<Control>q", NULL };
    gtk_application_set_accels_for_action(GTK_APPLICATION(app),
                                           "app.quit",
                                           quit_accels);
}
菜单集成

<menu id="app_menu">
  <section>
    <item>
      <attribute name="label">_Preferences</attribute>
      <attribute name="action">app.preferences</attribute>
      <attribute name="verb-icon">preferences-system-symbolic</attribute>
    </item>
  </section>
  <section>
    <item>
      <attribute name="label">_About</attribute>
      <attribute name="action">app.about</attribute>
    </item>
  </section>
</menu>
状态管理
使用 GSettings 实现持久状态
// Create settings
GSettings *settings = g_settings_new("org.example.app");

// Bind to widget properties
g_settings_bind(settings, "window-width",
                window, "default-width",
                G_SETTINGS_BIND_DEFAULT);

// Watch for changes
g_signal_connect(settings, "changed::theme",
                G_CALLBACK(on_theme_changed), NULL);
属性绑定
// Bidirectional binding between widgets
g_object_bind_property(
    entry, "text",
    label, "label",
    G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE
);

// Transform bindings with transform functions
g_object_bind_property_full(
    slider, "value",
    label, "label",
    G_BINDING_DEFAULT,
    slider_to_label_transform,
    label_to_slider_transform,
    NULL, NULL
);
事件控制器(GTK4 现代方式)
键盘控制器
// Use event controllers instead of signals
GtkEventController *key_controller = gtk_event_controller_key_new();
gtk_widget_add_controller(widget, key_controller);
g_signal_connect(key_controller, "key-pressed",
                G_CALLBACK(on_key_pressed), self);
手势控制器
// Click gesture
GtkGesture *click = gtk_gesture_click_new();
g_signal_connect(click, "pressed", G_CALLBACK(on_click_pressed), widget);
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(click));

// Drag gesture
GtkGesture *drag = gtk_gesture_drag_new();
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
g_signal_connect(drag, "drag-begin", G_CALLBACK(on_drag_begin), NULL);
g_signal_connect(drag, "drag-update", G_CALLBACK(on_drag_update), NULL);
gtk_widget_add_controller(drawing_area, GTK_EVENT_CONTROLLER(drag));

反模式与常见错误

内存管理
错误:手动管理引用
GtkWidget *child = gtk_button_new_with_label("Click");
gtk_box_append(GTK_BOX(box), child);
g_object_unref(child);  // DANGER: May leak or crash
正确:让容器管理所有权
GtkWidget *child = gtk_button_new_with_label("Click");
gtk_box_append(GTK_BOX(box), child);
// Box takes ownership - no unref needed
错误:未断开信号连接
static void my_widget_init(MyWidget *self) {
    g_signal_connect(self->button, "clicked",
                     G_CALLBACK(on_clicked), self);
}
// Never disconnects - memory leak!
正确:在 dispose 中清理
static void my_widget_dispose(GObject *object) {
    MyWidget *self = MY_WIDGET(object);
    g_clear_signal_handler(&self->button_clicked_id, self->button);
    G_OBJECT_CLASS(my_widget_parent_class)->dispose(object);
}
事件处理
错误:使用 GTK3 信号
g_signal_connect(widget, "key-press-event",
                 G_CALLBACK(old_handler), NULL);  // GTK3 pattern
正确:使用 GTK4 事件控制器
GtkEventController *controller = gtk_event_controller_key_new();
gtk_widget_add_controller(widget, controller);
g_signal_connect(controller, "key-pressed",
                 G_CALLBACK(modern_handler), NULL);
API 混用
错误:混用 GTK3 和 GTK4 API
GtkWidget *window = gtk_window_new();  // GTK3
GtkApplicationWindow *app_win = gtk_application_window_new(app);  // GTK4
正确:使用一致的 GTK4 API
GtkWidget *app_win = gtk_application_window_new(app);
性能
错误:过度重绘
void on_data_changed(void) {
    gtk_widget_queue_draw(widget);  // Triggers full redraw
}
正确:使用属性通知
void on_data_changed(void) {
    gtk_widget_notify(widget, "content");  // More efficient
}
错误:重建列表模型
void update_list(void) {
    gtk_list_view_set_model(listview, create_new_model());  // Slow
}
正确:修改现有模型
void update_list(void) {
    GListStore *store = get_current_store();
    g_list_store_remove(store, old_item);
    g_list_store_append(store, new_item);  // Fast
}

参考资源

官方文档
真实示例
模式参考
  • 参见 references/ 目录:
    • gnome-hig.md - HIG 原则与模式
    • gtk4-best-practices.md - 现代 GTK4 代码模式
    • libadwaita-styling.md - 使用 Libadwaita 进行 CSS 主题化
    • accessibility.md - A11y 实现指南

快速入门示例

创建现代 GTK4 窗口
// UI resource (window.ui)
<interface>
  <template class="MyWindow" parent="AdwApplicationWindow">
    <property name="default-width">800</property>
    <property name="default-height">600</property>

    <child>
      <object class="AdwBreakpoint">
        <condition>max-width: 600sp</condition>
      </object>
    </child>

    <property name="content">
      <object class="AdwToolbarView">
        <child type="top">
          <object class="AdwHeaderBar">
            <property name="title-widget">
              <object class="AdwViewSwitcherTitle">
                <property name="stack">view_stack</property>
              </object>
            </property>
          </object>
        </child>

        <property name="content">
          <object class="GtkStack" id="view_stack">
            
          </object>
        </property>
      </object>
    </property>
  </template>
</interface>
// C implementation
G_DEFINE_TYPE(MyWindow, my_window, ADW_TYPE_APPLICATION_WINDOW);

static void my_window_class_init(MyWindowClass *klass) {
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    gtk_widget_class_set_template_from_resource(widget_class,
        "/org/example/app/ui/window.ui");
}

static void my_window_init(MyWindow *self) {
    gtk_widget_init_template(GTK_WIDGET(self));

    // Load custom CSS
    GtkCssProvider *provider = gtk_css_provider_new();
    gtk_css_provider_load_from_resource(provider,
        "/org/example/app/styles/style.css");
    gtk_style_context_add_provider_for_display(
        gdk_display_get_default(),
        GTK_STYLE_PROVIDER(provider),
        GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
    );
}
/* Custom styling (style.css) */
window {
  background-color: var(--window-bg-color);
}

.title {
  font-weight: 700;
  font-size: 24pt;
}

.primary-button {
  background-color: var(--accent-bg-color);
  color: var(--accent-fg-color);
}

.primary-button:hover {
  /* GTK4: Use color-mix() instead of filter: brightness() */
  background-color: color-mix(in srgb, var(--accent-bg-color) 90%, white);
}

此示例演示了:

  • 用于原生 GNOME 集成的 AdwApplicationWindow
  • 带视图切换器的标题栏
  • 使用 GTK4 语法加载自定义 CSS
  • 使用 AdwBreakpoint 实现响应式设计
  • 使用 CSS 变量进行 Libadwaita 主题化

评论

支持 Markdown