如何限制Gtk.ScrolledWindow内最右侧Gtk.TreeViewColumn宽度
我无法阻止Gtk.TreeView的最右侧列自动横向扩展。
实际使用场景中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行)。
电子表格测试数据选择示例:
我更希望不使用上述临时方案(担心后续还会遇到其他刷新异常场景),且我未能修复该临时方案的问题(例如尝试设置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

