You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何限制Gtk.ScrolledWindow内最右侧Gtk.TreeViewColumn宽度

问题描述

我无法阻止Gtk.TreeView的最右侧列自动横向扩展。
TreeView Column Size Demo

实际使用场景中Gtk.TreeView通常需要展示大量行,高度往往超出屏幕范围,因此必须将其嵌入Gtk.ScrolledWindow中,这是强制要求。如果不使用滚动窗口,在树视图右侧附加一个可水平扩展的空网格即可解决列拉伸问题,但基于该思路实现的临时规避方案会引发其他异常(详见下文)。

我基于Python GTK3官方教程树视图过滤章节的示例精简构建了最小可复现示例,移除了过滤功能与多余按钮;所有列设置了80px的最小宽度(该设置正常生效),列内容水平居中——正是内容居中的设置让最右列的横向拉伸问题变得非常明显。原官方示例中最右列同样会拉伸,但因内容全部左对齐,该问题不易察觉。我希望保留列内容居中的样式,同时避免最右列自动拉伸。

该最小示例包含辅助调试功能:

  • 列标题可点击,点击后会在控制台输出被点击列的相关属性信息
  • 提供Remove按钮,可正常删除选中行
  • 提供Paste按钮,可从剪贴板粘贴内容新增行(例如从电子表格选中多行数据粘贴,示例未做数据校验,若粘贴内容无法转换为int类型会直接导致程序崩溃)
已尝试的临时规避方案

我尝试过的方案为:将树视图和一个可水平扩展的空网格并排放在同一个网格容器中,再将该网格整体放入Gtk.ScrolledWindow。该方案确实能解决列拉伸问题,但会引发隐蔽的刷新异常:某些场景下树视图无法及时刷新(程序运行一段时间后出现),但主循环并无阻塞(例如后台无其他耗时任务运行)。
复现该问题的步骤为:按照代码注释说明注释/取消注释对应代码行,通过python script.py运行程序(若需要在虚拟环境中安装pygobject可查询公开安装文档),此时可观察到最右列不再拉伸;选中前3行点击Remove删除,再从电子表格中选中3行整数测试数据,点击Paste粘贴,滚动到列表最底部,绝大多数情况下新粘贴的3行不会显示,虽然滚动范围已经覆盖到最后一行;有时等待一段时间后会逐行显示,或者点击选中任意行后新行才会显示。经测试该异常仅在删除行数和后续粘贴行数相等时触发(例如删3行贴3行、删4行贴4行)。

电子表格测试数据选择示例:
spreadsheet selection

问题诉求

我更希望不使用上述临时方案(担心后续还会遇到其他刷新异常场景),且我未能修复该临时方案的问题(例如尝试设置self.scrollable_treelist.set_propagate_natural_height(True)没有效果,可能是我的使用方式有误),希望能实现仅将树视图直接挂载到Gtk.ScrolledWindow下的前提下,阻止最右侧列自动横向扩展。

我已经尝试过调整单元格渲染器、树视图、树视图列、滚动窗口的大量setter方法和属性,均没有效果,部分尝试的代码仍保留在下方源码中。

如果有方案可以修复上述临时方案的刷新异常问题,也同样可以接受。
无论采用哪种方案,最终都需要保证树视图可正常滚动,增删行时无刷新异常问题。

复现代码
import gi

try:
    gi.require_version('Gtk', '3.0')
except ValueError:
    raise
else:
    from gi.repository import Gtk, Gdk

# ints to feed the store
data_list = [(i, 2 * i, 3 * i, 4 * i, 5 * i) for i in range(40)]


class AppWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="Treeview Columns Size Demo")
        self.set_border_width(10)

        # Setting up the self.grid in which the elements are to be positioned
        self.grid = Gtk.Grid()
        self.grid.set_column_homogeneous(True)
        self.grid.set_row_homogeneous(True)
        self.add(self.grid)

        # Creating the ListStore model
        self.store = Gtk.ListStore(int, int, int, int, int)
        for data_ref in data_list:
            self.store.append(list(data_ref))

        # creating the treeview and adding the columns
        self.treeview = Gtk.TreeView(model=self.store)
        rend = Gtk.CellRendererText()
        rend.set_alignment(0.5, 0.5)
        for i, column_title in enumerate([f'n×{p}' for p in [1, 2, 3, 4, 5]]):
            column = Gtk.TreeViewColumn(column_title, rend, text=i)
            column.set_min_width(80)
            # column.set_max_width(80)
            # column.set_fixed_width(80)
            # column.set_sizing(Gtk.TreeViewColumnSizing(1))
            column.set_alignment(0.5)
            column.set_clickable(True)
            column.connect('clicked', self.on_column_clicked)
            self.treeview.append_column(column)

        self.treeview.set_hexpand(False)
        self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)

        # Put the treeview in a scrolled window
        self.scrollable_treelist = Gtk.ScrolledWindow()
        self.scrollable_treelist.set_vexpand(True)
        self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)

        self.scrollable_treelist.add(self.treeview)
        # WORKAROUND
        # Alternatively, embed the treeview inside a grid containing an
        # empty grid to the right of the treeview
        # To try it: comment out the previous line; uncomment next lines
        # scrolled_grid = Gtk.Grid()
        # empty_grid = Gtk.Grid()
        # empty_grid.set_hexpand(True)
        # scrolled_grid.attach(self.treeview, 0, 0, 8, 10)
        # scrolled_grid.attach_next_to(empty_grid, self.treeview,
        #                              Gtk.PositionType.RIGHT, 1, 1)
        # self.scrollable_treelist.add(scrolled_grid)
        # self.scrollable_treelist.set_propagate_natural_height(True)

        # Buttons
        self.remove_button = Gtk.Button(label='Remove')
        self.remove_button.connect('clicked', self.on_remove_clicked)
        self.paste_button = Gtk.Button(label='Paste')
        self.paste_button.connect('clicked', self.on_paste_clicked)

        self.grid.attach_next_to(self.remove_button, self.scrollable_treelist,
                                 Gtk.PositionType.TOP, 1, 1)

        self.grid.attach_next_to(self.paste_button, self.remove_button,
                                 Gtk.PositionType.RIGHT, 1, 1)

        self.set_default_size(800, 500)
        self.show_all()

        # Clipboard (to insert several rows)
        self.clip = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
        self.clip2 = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)

    def on_column_clicked(self, col):
        print(f'col.get_sizing()={col.get_sizing()}')
        print(f'col.get_expand()={col.get_expand()}')
        print(f'col.get_width()={col.get_width()}')
        print(f'col.get_min_width()={col.get_min_width()}')
        print(f'col.get_max_width()={col.get_max_width()}')
        print(f'col.get_fixed_width()={col.get_fixed_width()}')

    def on_remove_clicked(self, widget):
        model, paths = self.treeview.get_selection().get_selected_rows()
        refs = []
        for path in paths:
            refs.append(Gtk.TreeRowReference.new(model, path))
        for ref in refs:
            path = ref.get_path()
            treeiter = model.get_iter(path)
            model.remove(treeiter)
        # print(f'AFTER REMOVAL, REMAINING ROWS={[str(r[0]) for r in model]}')

    def on_paste_clicked(self, widget):
        text = self.clip.wait_for_text()
        if text is None:
            text = self.clip2.wait_for_text()
        if text is not None:
            lines = text.split('\n')  # separate the lines
            lines = [tuple(L.split('\t')) for L in lines]  # convert to tuples
            print(f'PASTE LINES={lines}')
            for line in lines:
                if len(line) == 5:
                    line = tuple(int(value) for value in line)
                    self.store.append(line)


win = AppWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
解决方案

不需要添加空网格做hack,问题根源是GTK3的Gtk.TreeView默认会将最后一列的expand属性设为True,自动填充滚动窗口内的剩余水平空间,直接显式关闭所有列的自动扩展属性即可。

只需要在列创建的循环中新增一行配置,修改后的列创建代码片段如下:

for i, column_title in enumerate([f'n×{p}' for p in [1, 2, 3, 4, 5]]):
    column = Gtk.TreeViewColumn(column_title, rend, text=i)
    column.set_min_width(80)
    column.set_expand(False) # 关闭列自动扩展,阻止最右列拉伸
    column.set_alignment(0.5)
    column.set_clickable(True)
    column.connect('clicked', self.on_column_clicked)
    self.treeview.append_column(column)

该方案下树视图直接挂载在Gtk.ScrolledWindow下,不会出现空网格hack带来的刷新异常,列宽保持80px最小宽度、内容居中的同时不会拉伸最右列,水平滚动条会在总列宽超过可视区域时自动出现,增删行操作无渲染bug。如果后续需要某一列自动填充剩余空间,单独给对应列设置set_expand(True)即可。

内容的提问来源于stack exchange,提问作者zezollo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 01:01:29