如何从异步方法更新gtk列表框?

时间:2019-09-28 21:35:50

标签: gtk signals gtk3 vala gio

因此,在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);
    }

}

1 个答案:

答案 0 :(得分:0)

这里是我“解决”的方式。

我并不为此解决方案感到特别自豪,但是有关Gtk ListBox的事情有些糟糕,如果ListBox绑定到ListModel,其中之一就是(这可能实际上是ListModel的问题) ,则ListBox将无法通过使用sort方法进行排序,至少对我而言,这是一个大问题。我通过制作一个基本上是列表包装器的类解决了此问题,该类具有“添加”信号和“删除”信号。在将元素添加到列表后,将连接添加的信号,因此它将创建一个新的Row对象并将其添加到列表框中。这样,数据以与列表模型绑定类似的方式进行控制。但是,如果不调用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();

            }
        }

即使这绝不是我想出的最好的代码,它也能很好地工作。