2025, Oct 04 15:16

Как исправить Gtk.FileChooserDialog, игнорирующий заданный размер

Почему Gtk.FileChooserDialog в GTK 3 на Ubuntu 24.04 открывается не с тем размером и «прыгает» при первом ресайзе. Показываем решение через show+resize.

Gtk.FileChooserDialog, который не удерживает запрошенный размер, может сбивать с толку, особенно когда на бумаге всё выглядит правильно. Если диалог открывается выше, чем вы просили, при первом ручном изменении размера перескакивает к крошечному виду или постоянно «воскрешает» старую геометрию, значит, вы столкнулись с сочетанием запоминаемого размера окна и момента, когда именно применяется размер.

Демонстрация проблемы

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

import sys

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk


class Picker(Gtk.FileChooserDialog):
    def __init__(self, title=None, parent=None):
        super().__init__(
            title=title,
            parent=parent,
            action=Gtk.FileChooserAction.OPEN,
            default_height=500,
        )
        self.set_default_size(-1, 500)
        self.set_size_request(-1, 300)
        self.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OPEN,
            Gtk.ResponseType.OK,
        )
        self._install_filters()
        self._run_dialog()

    def _install_filters(self):
        txt_filter = Gtk.FileFilter()
        txt_filter.set_name(".txt files")
        txt_filter.add_mime_type("text")
        self.add_filter(txt_filter)

    def _run_dialog(self):
        result = self.run()
        self.destroy()


class DemoApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="com.gnome.test")
        GLib.set_application_name("test")

    def do_activate(self):
        self.win = Gtk.ApplicationWindow(application=self, title="App")
        grid = Gtk.Grid()
        self.win.add(grid)
        btn = Gtk.Button(label="From")
        grid.attach(btn, 0, 0, 1, 1)
        grid.set_row_homogeneous(True)
        grid.set_column_homogeneous(True)
        btn.connect("clicked", self._on_pick)
        self.win.set_default_size(width=500, height=34)
        self.win.show_all()

    def _on_pick(self, _widget):
        picker = Picker(title="Chooser", parent=self.win)
        picker.set_default_size(-1, 400)


app = DemoApp()
code = app.run(sys.argv)
sys.exit(code)

Что происходит

Здесь важно два наблюдения. Во-первых, на системах на базе Ubuntu 24.04 диалог может запоминать свой предыдущий размер, и вызов run() применяет сохранённую геометрию, перекрывая параметры, которые вы задавали раньше. Во-вторых, использование -1 для одного из измерений в данном сценарии мешает ожидаемому результату. В сумме это объясняет, почему диалог открывается выше запрошенного и почему при первом ручном изменении размеров он может «прыгнуть» в неожиданно крошечный вид, будто виден лишь дочерний виджет.

Жёсткая фиксация размера через set_resizable(False) даёт ожидаемую геометрию, но лишает возможности менять размеры вручную — для файлового диалога это редко приемлемо.

Есть и зависящий от окружения нюанс. При запуске под MATE в Linux Mint на базе Ubuntu 24.04 система запоминает размер диалога между запусками скрипта и возвращает его, из‑за чего габариты могут измениться уже после того, как вы их задали.

Решение

Принимайте решение о размере как можно позже и избегайте -1 для измерений в такой ситуации. Подпишитесь на сигнал show и вызовите resize(width, height) с явным указанием обеих величин — так диалог примет ваши размеры после применения возможной «памяти» о геометрии. Это устраняет сбой при первом изменении размера и сохраняет возможность масштабирования окна. Вызовы set_default_size и set_size_request здесь не нужны.

import sys

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk


class PickerFixed(Gtk.FileChooserDialog):
    def __init__(self, title=None, parent=None):
        super().__init__(
            title=title,
            parent=parent,
            action=Gtk.FileChooserAction.OPEN,
        )
        self.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OPEN,
            Gtk.ResponseType.OK,
        )
        self._apply_filters()
        self.connect("show", self._on_show)
        self._open()

    def _apply_filters(self):
        txt_filter = Gtk.FileFilter()
        txt_filter.set_name(".txt files")
        txt_filter.add_mime_type("text")
        self.add_filter(txt_filter)

    def _on_show(self, *_args):
        self.resize(800, 500)

    def _open(self):
        result = self.run()
        self.destroy()


class DemoApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="com.gnome.test")
        GLib.set_application_name("test")

    def do_activate(self):
        self.win = Gtk.ApplicationWindow(application=self, title="App")
        grid = Gtk.Grid()
        self.win.add(grid)
        btn = Gtk.Button(label="From")
        grid.attach(btn, 0, 0, 1, 1)
        grid.set_row_homogeneous(True)
        grid.set_column_homogeneous(True)
        btn.connect("clicked", self._trigger")
        self.win.set_default_size(width=500, height=34)
        self.win.show_all()

    def _trigger(self, _w):
        PickerFixed(title="Chooser", parent=self.win)


app = DemoApp()
exit_code = app.run(sys.argv)
sys.exit(exit_code)

Почему это важно

Окна выбора файлов — интерфейс, который работает на мышечной памяти. Пользователи рассчитывают на предсказуемые размеры и отзывчивое изменение габаритов, особенно в современных окружениях с разными темами и правилами DE. Передача контроля над размером в нужный момент предотвращает резкие скачки, уважает намерения пользователя и обходится без «воспоминаний» о геометрии на уровне окружения, которые иначе могут удивить при первом ресайзе или между запусками.

Выводы

Если диалог открывается с неправильной высотой или «щёлкает» непредсказуемо при первом изменении размеров, предположите, что run() навязывает ранее запомненный размер. Применяйте свою геометрию на show и указывайте и ширину, и высоту в resize, чтобы получить детерминированное поведение. В этом случае избегайте -1 для измерений. set_resizable(False) зафиксирует размер, но отключит ручное масштабирование, поэтому оставьте его для случаев, когда фиксированный диалог уместен.

Статья основана на вопросе на StackOverflow от Sun Bear и ответе от furas.