【问题标题】:How do I update a gtk listbox from an async method?如何从异步方法更新 gtk 列表框?
【发布时间】:2019-09-28 21:35:50
【问题描述】:

因此,在 GTK 中编写 UI 时,通常更喜欢在异步方法中处理文件读取等。列表框之类的东西,一般都绑定到一个ListModel上,ListBox中的项根据items_changed信号更新。

所以,如果我有一些实现 ListModel 的类,并有一个 add 函数,还有一些 FileReader 持有对所述 ListModel 的引用,并从异步函数调用 add,我该如何做到这一点? items_changed 并相应地更新 GTK?

我试过list.items_changed.connect(message("Items changed!"));,但它永远不会触发。

我看到了这个:How can one update GTK+ UI in Vala from a long operation without blocking the UI 但在这个例子中,只是改变了按钮标签,实际上并没有触发任何信号。

编辑:(应@Michael Gratton 的要求添加的代码示例

//Disclaimer: everything here is still very much a work in progress, and will, as soon as I'm confident that what I have is not total crap, be released under some GPL or other open license.

//Note: for the sake of readability, I adopted the C# naming convention for interfaces, namely, putting a capital 'I' in front of them, a decision i do not feel quite as confident in as I did earlier.
//Note: the calls to message(..) was put in here to help debugging    

public class AsyncFileContext : Object{


    private int64 offset;
    private bool start_read;
    private bool read_to_end;

    private Factories.IVCardFactory factory;
    private File file;
    private FileMonitor monitor;

    private Gee.Set<IVCard> vcard_buffer;

    private IObservableSet<IVCard> _vCards;
    public IObservableSet<IVCard> vCards { 
        owned get{
            return this._vCards;
        } 
    }

    construct{
        //We want to start fileops at the beginning of the file
        this.offset = (int64)0;
        this.start_read = true;
        this.read_to_end = false;
        this.vcard_buffer = new Gee.HashSet<IVCard>();
        this.factory = new Factories.GenericVCardFactory();
    }

    public void add_vcard(IVCard card){
        //TODO: implement
    }

    public AsyncFileContext(IObservableSet<IVCard> vcards, string path){
        this._vCards = vcards;
        this._vCards = IObservableSet.wrap_set<IVCard>(new Gee.HashSet<IVCard>());
        this.file = File.new_for_path(path);
        this.monitor = file.monitor_file(FileMonitorFlags.NONE, null);
        message("1");
        //TODO: add connect
        this.monitor.changed.connect((file, otherfile, event) => {
            if(event != FileMonitorEvent.DELETED){
                bool changes_done = event == FileMonitorEvent.CHANGES_DONE_HINT;
                Idle.add(() => {
                    read_file_async.begin(changes_done);
                    return false;
                });
            }
        });
        message("2");
        //We don't know that changes are done yet
        //TODO: Consider carefully how you want this to work when it is NOT called from an event

        Idle.add(() => {
            read_file_async.begin(false);
            return false;
        });
    }


    //Changes done should only be true if the FileMonitorEvent that triggers the call was CHANGES_DONE_HINT
    private async void read_file_async(bool changes_done) throws IOError{
        if(!this.start_read){
            return;
        }
        this.start_read = false;
        var dis = new DataInputStream(yield file.read_async());
        message("3");
        //If we've been reading this file, and there's then a change, we assume we need to continue where we let off
        //TODO: assert that the offset isn't at the very end of the file, if so reset to 0 so we can reread the file
        if(offset > 0){
            dis.seek(offset, SeekType.SET);
        }

        string line;
        int vcards_added = 0;
        while((line = yield dis.read_line_async()) != null){
            message("position: %s".printf(dis.tell().to_string()));
            this.offset = dis.tell();
            message("4");
            message(line);
            //if the line is empty, we want to jump to next line, and ignore the input here entirely
            if(line.chomp().chug() == ""){
                continue;
            }

            this.factory.add_line(line);

            if(factory.vcard_ready){
                message("creating...");
                this.vcard_buffer.add(factory.create());
                vcards_added++;
                //If we've read-in and created an entire vcard, it's time to yield
                message("Yielding...");
                Idle.add(() => {
                    _vCards.add_all(vcard_buffer);
                    vcard_buffer.remove_all(_vCards);
                    return false;
                });
                Idle.add(read_file_async.callback);
                yield;
                message("Resuming");
            }
        }
        //IF we expect there will be no more writing, or if we expect that we read ALL the vcards, and did not add any, it's time to go back and read through the whole thing again.
        if(changes_done){ //|| vcards_added == 0){
            this.offset = 0;
        }
        this.start_read = true;
    }

}

//The main idea in this class is to just bind the IObservableCollection's item_added, item_removed and cleared signals to the items_changed of the ListModel. IObservableCollection is a class I have implemented that merely wraps Gee.Collection, it is unittested, and works as intended
public class VCardListModel : ListModel, Object{

    private Gee.List<IVCard> vcard_list;
    private IObservableCollection<IVCard> vcard_collection;

    public VCardListModel(IObservableCollection<IVCard> vcard_collection){
        this.vcard_collection = vcard_collection;
        this.vcard_list = new Gee.ArrayList<IVCard>.wrap(vcard_collection.to_array());

        this.vcard_collection.item_added.connect((vcard) => {
            vcard_list.add(vcard);
            int pos = vcard_list.index_of(vcard);
            items_changed(pos, 0, 1);
        });

        this.vcard_collection.item_removed.connect((vcard) => {
            int pos = vcard_list.index_of(vcard);
            vcard_list.remove(vcard);
            items_changed(pos, 1, 0);
        });
        this.vcard_collection.cleared.connect(() => {
            items_changed(0, vcard_list.size, 0);
            vcard_list.clear();
        });

    }

    public Object? get_item(uint position){
        if((vcard_list.size - 1) < position){
            return null;
        }
        return this.vcard_list.get((int)position);
    }

    public Type get_item_type(){
        return Type.from_name("VikingvCardIVCard");
    }

    public uint get_n_items(){
        return (uint)this.vcard_list.size;
    }

    public Object? get_object(uint position){
        return this.get_item((int)position);
    }

}

//The IObservableCollection parsed to this classes constructor, is the one from the AsyncFileContext
public class ContactList : Gtk.ListBox{

    private ListModel list_model;

    public ContactList(IObservableCollection<IVCard> ivcards){
        this.list_model = new VCardListModel(ivcards);

        bind_model(this.list_model, create_row_func);
        list_model.items_changed.connect(() => {
            message("Items Changed!");
            base.show_all();
        });
    }

    private Gtk.Widget create_row_func(Object item){
        return new ContactRow((IVCard)item);
    }

}

【问题讨论】:

  • 你的班级有没有打电话给g_list_model_items_changed()
  • 是的!当通过同步方法(非异步方法)向其中添加项目时,它们会显示在 UI 中。
  • 看起来问题是你直接从你的异步方法调用add。 GTK 不是线程安全的,它是 UB 从主循环调用它的任何函数 not。这是发生的事情:你从另一个线程调用add"items_changed" 被发出,GtkListBox 回调被立即调用(这是一个错误)。这就是为什么在您的链接中使用Idle.add (test_async.callback);
  • 当然可以,但是我应该如何/在哪里打电话给add
  • @aggsol 哎呀,已经有一段时间了。老实说,我不记得了,但我会看一下代码,如果我解决了它或者只是做了一个解决方法,我会告诉你(我相信,遗憾的是它最终只是一个解决方法)

标签: gtk signals gtk3 vala gio


【解决方案1】:

这就是我“解决”它的方法。

我对这个解决方案并不特别自豪,但是关于 Gtk ListBox 有一些糟糕的事情,其中​​之一是(这实际上可能更像是一个 ListModel 问题)如果 ListBox 绑定到一个 ListModel , ListBox 将无法使用 sort 方法进行排序,至少对我来说,这是一个破坏者。我通过创建一个基本上是 List 包装器的类来解决它,它有一个“添加”信号和一个“删除”信号。将元素添加到列表后,添加的信号会被连接,因此它将创建一个新的 Row 对象并将其添加到列表框中。这样,数据以与 ListModel 绑定类似的方式进行控制。但是,如果不调用 ShowAll 方法,我无法使其工作。

private IObservableCollection<IVCard> _ivcards;
        public IObservableCollection<IVCard> ivcards {
            get{
                return _ivcards;
            }
            set{
                this._ivcards = value;

                foreach(var card in this._ivcards){
                    base.prepend(new ContactRow(card));
                }

                this._ivcards.item_added.connect((item) => {
                    base.add(new ContactRow(item));
                    base.show_all();
                });

                base.show_all();

            }
        }

尽管这绝不是我想出的最好的代码,但它工作得很好。

【讨论】:

    猜你喜欢
    • 2021-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多