Элементы Kivy recycleview переупорядочиваются при изменении размера окна

#python #kivy

#python #kivy

Вопрос:

Я использую Kivy recycleview для отображения списка данных в виде таблицы. Я использовал пример из документации в качестве основы для своей реализации.

В моей программе RecycleDataView основан на BoxLayout, а его дочерние виджеты генерируются динамически.

Кажется, это работает, но порядок, в котором отображаются элементы, иногда меняется на противоположный и продолжает меняться при изменении размера окна. Что еще хуже, если вы прокрутите вниз, макет станет совершенно сумасшедшим. Этого не произойдет, если я буду использовать простую метку в качестве класса элемента, поэтому я предполагаю, что проблема в моей динамически созданной логике виджета, но я не понимаю почему.

Вот некоторый минимальный код, который показывает проблему.

 from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty

Builder.load_string('''

<RV>:
    viewclass: 'RVItem'
    RecycleBoxLayout:
        default_size: None, dp(56)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'

''')

class Attribute:
    def __init__(self, name, values):
        self.name = name
        self.values = values


class RVItem(RecycleDataViewBehavior, BoxLayout):
    index = None
    attribute = ObjectProperty()

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''
        self.index = index
        self.create_widgets(data.pop('attribute', None))
        return super(RVItem, self).refresh_view_attrs(
            rv, index, data)

    def create_widgets(self, value: Attribute):
        """Dynamically create the needed Widgets"""

        if value is None:
            return
        self.add_widget(Label(text=value.name, height=self.height, size_hint=(1, None)))

        if not isinstance(value.values, dict):
            self.add_widget(Label(text=value.values, height=self.height, size_hint=(1, None)))
        else:
            for _, v in value.values.items():
                self.add_widget(Label(text=v, height=self.height, size_hint=(1, None)))

        image_button = Button(text=' ')
        #image_button.source = 'wm_ui/glyphs/plus.png'
        image_button.size_hint = None, None
        image_button.size = "30sp", "30sp"
        image_button.bind(on_press=self.add_button_pressed)
        self.add_widget(image_button)

    def add_button_pressed(self, s):
        print("Would add a new item to the recycleview if implemented.")


class RV(RecycleView):
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.data = [{'attribute': Attribute(str(x), "test")} for x in range(100)]


class TestApp(App):
    def build(self):
        return RV()

if __name__ == '__main__':
    TestApp().run()
  

Для простоты я изменил некоторые виджеты на их базовые классы (например, ImageButton и Label)

При запуске приложения вы должны увидеть, что порядок элементов изменен на обратный и по какой-то причине начинается с 10 вместо 100.

запущенное приложение

После изменения размера окна с помощью мыши в одном из углов окна вы должны увидеть, что порядок содержимого постоянно меняется.

после небольшого изменения размера окна

А если вы прокрутите вниз, все станет еще более безумным.

после прокрутки списка вниз

К сожалению, я понятия не имею, что вызывает такое поведение. Я уже разрабатывал некоторые приложения Kivy раньше, но это мое первое по-настоящему глубокое погружение, в котором используется больше, чем просто метки и несколько входных данных.

Комментарии:

1. Существует ли фиксированное максимальное количество Label виджетов, которые будут отображаться в одном RVItem ?

2. @JohnAnderson Я ожидаю, что их будет не более 10, но вы никогда не знаете, что могут сделать пользователи. Количество меток равно как минимум двум, но определенного максимума нет, потому что оно зависит от количества значений, установленных пользователем (определенных в классе атрибутов). Думайте об этом как о столбцах пользовательской таблицы. Я не могу знать, сколько виджетов будет заранее.

3. RecyleView Выполняет то, что от него ожидается, то есть повторно использует ваши RVItem виджеты. Проблема в том, что вы refresh_view_attrs() добавляете больше Label и Button виджетов к тем, которые были переработаны RVItems . Вот почему при прокрутке вниз вы видите все больше и больше Label и Button виджетов в каждом RVItem . Поскольку они RVItems перерабатываются, вам refresh_view_attrs() необходимо установить ВСЕ атрибуты для RVItem (включая name ). Кроме того, поскольку вы refresh_view_attrs() создаете все эти виджеты, вы не используете Recycle часть RecycleView преимуществ.

Ответ №1:

Вот модификация вашего кода, которая, похоже, работает правильно:

 from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty

Builder.load_string('''

<RV>:
    viewclass: 'RVItem'
    RecycleBoxLayout:
        default_size: None, dp(56)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'

''')

class Attribute:
    def __init__(self, name, values):
        self.name = name
        self.values = values


class RVItem(RecycleDataViewBehavior, BoxLayout):
    index = None
    attribute = ObjectProperty()

    def refresh_view_attrs(self, rv, index, data):
        ''' Catch and handle the view changes '''
        self.index = index
        self.create_widgets(data['attribute'])
        return super(RVItem, self).refresh_view_attrs(
            rv, index, data)

    def create_widgets(self, value: Attribute):
        rv = App.get_running_app().root
        rv.cache_widgets(self.children)
        self.clear_widgets()
        label = rv.get_label()
        label.text = value.name
        self.add_widget(label)
        if isinstance(value.values, dict):
            for _,v in value.values.items():
                label = rv.get_label()
                label.text = v
                self.add_widget(label)
        else:
            label = rv.get_label()
            label.text = value.values
            self.add_widget(label)
        image_button = rv.get_button()
        image_button.text = ' '
        image_button.size_hint = None, None
        image_button.size = "30sp", "30sp"
        image_button.bind(on_press=self.add_button_pressed)
        self.add_widget(image_button)


    def add_button_pressed(self, s):
        print("Would add a new item to the recycleview if implemented.")


class RV(RecycleView):
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.label_cache = []
        self.button_cache = []
        self.data = [{'attribute': Attribute(str(x), "test")} for x in range(100)]
        for i in range(100):
            if i % 5 == 0:
                self.data[i]['attribute'].values = {'1': 'test1', '2': 'test2', '3': 'test3'}

    def get_button(self):
        if len(self.button_cache) > 0:
            return self.button_cache.pop()
        else:
            return Button()

    def get_label(self):
        if len(self.label_cache) > 0:
            return self.label_cache.pop()
        else:
            return Label()

    def cache_widgets(self, widgets):
        for w in widgets:
            if isinstance(w, Button):
                self.button_cache.append(w)
            else:
                self.label_cache.append(w)


class TestApp(App):
    def build(self):
        return RV()

if __name__ == '__main__':
    TestApp().run()
  

В этой версии refresh_view_attrs метод всегда устанавливает все атрибуты RVItem . Строка

 self.create_widgets(data.pop('attribute', None))
  

заменяется на

 self.create_widgets(data['attribute'])
  

потому что pop() фактически удаляются данные, чего, я думаю, вы не хотите делать.

У RV класса теперь есть кэш для Label виджетов и другой для Button виджетов, и они перерабатываются (аналогично тому, что RecycleView делает. create_widgets Метод удаляет все дочерние элементы RVItem и добавляет их в кэш, затем перерабатывает или создает виджеты, по мере необходимости, для заполнения RVItem .

Я добавил дополнительные элементы в values dict для некоторых данных, чтобы проиллюстрировать, как это работает.

Комментарии:

1. Спасибо, я думаю, у меня было неправильное представление о том, как работает часть переработки. Не могли бы вы порекомендовать мне использовать фабрику и динамически создавать viewclasses с фиксированным количеством виджетов вместо этого?

2. Если созданное RVItem всегда имеет одинаковое количество дочерних элементов, то все они RVItem могут быть переработаны RecycleView , и refresh_view_attrs() метод может просто установить атрибуты RVItem . Итак, если для вас возможно фиксированное количество дочерних элементов, это упростит задачу.