自定义GtkComboBoxText的完成方式

11
我应该如何自定义GtkComboBoxText的完成,同时具有“静态”和“动态”方面? “静态”方面是因为一些条目已知并在构建时使用gtk_combo_box_text_append_text添加到组合框文本中。 “动态”方面是因为我还需要通过某些回调函数进行补全,即在键入多个字符后动态地完成-在创建GtkComboBoxText小部件之后。
我的应用程序类似于Guile、SCM或Bigloo等,使用Boehm的GC(当然不包括GTK对象)。它可以被视为一个实验性的“持久性”动态类型编程语言实现,具有集成的编辑器,针对Debian/Linux/x86-64编写,使用系统GTK3.21库,它使用C99编写(其中一些是生成的),并使用GCC6编译。 (我不关心非Linux系统,早于GTK3.20的GTK3库,旧于GCC6的GCC编译器) 问题细节

我正在输入(输入到GtkComboBoxText中)一个名称或一个对象ID

  • name的命名方式类似于C标识符,但必须以字母开头且不能以下划线结尾。例如:commentifthe_GUIthe_systempayload_jsonx1都是有效的名称(但_a0bcdfoobar_是无效的名称,因为它们以下划线开头或结尾)。我目前有很多名称,但可能会有几千个。因此,一旦只输入了一个或两个字母,就提供自动完成功能是合理的,并且名称的完成可以静态进行,因为它们不是很多(所以我认为对于每个名称调用gtk_combo_box_append_text是合理的)。

  • object-id以下划线开头,后跟一个数字,然后具有18个字母数字字符(类似随机的)。例如:_5Hf0fFKvRVa71ZPM0_8261sbF1f9ohzu2Iu_0BV96V94PJIn9si1K可能是object-id。实际上,它是96个几乎随机的位(可能只有2的94次方是可能的)。object-id扮演UUIDs的角色(在这种意义上,它被假定为对于不同的对象是全球唯一的),但具有C友好的语法。我目前有几十个对象ID,但可能会有数十万个(或者甚至可能有一百万个)。但是,假设像_6S3_22z这样的前缀仅存在合理数量的(可能最多只有一打,肯定不超过一千个)object-id,在输入四个字符的前缀后,注册(静态的)所有对象id是不合理的。完成必须在输入四个字符之后动态发生。

因此,我希望有一个自动完成功能,既可以用于名称(例如,键入一个字母,后面可能跟着另一个字母数字字符就足以提供最多一百个选择的完成),也可以用于对象 ID(键入四个字符,如_826就足以触发最多几十个选择的完成,如果不幸的话,可能会有一千个)。

因此,键入三个键 p a tab 将提供一些名称的完成,例如 payload_jsonpayload_vectval 等等...,而键入五个键 _ 5 H f tab 将提供非常少的对象ID,特别是 _5Hf0fFKvRVa71ZPM0

示例不完整代码

到目前为止,我编写了以下代码:

static GtkWidget *
mom_objectentry (void)
{
  GtkWidget *obent = gtk_combo_box_text_new_with_entry ();
  gtk_widget_set_size_request (obent, 30, 10);
  mo_value_t namsetv = mo_named_objects_set ();

我有使用Boehm垃圾回收的值,并且mo_value_t是指向任何这些值的指针。 值可以是标记整数、指向字符串、对象、元组或一组对象的指针。因此,namesetv现在包含了一组命名对象(可能少于几千个命名对象)。
  int nbnam = mo_set_size (namsetv);
  MOM_ASSERTPRINTF (nbnam > 0, "bad nbnam");
  mo_value_t *namarr = mom_gc_alloc (nbnam * sizeof (mo_value_t));
  int cntnam = 0;
  for (int ix = 0; ix < nbnam; ix++)
    {
      mo_objref_t curobr = mo_set_nth (namsetv, ix);
      mo_value_t curnamv = mo_objref_namev (curobr);
      if (mo_dyncast_string (curnamv))
        namarr[cntnam++] = curnamv;
    }
  qsort (namarr, cntnam, sizeof (mo_value_t), mom_obname_cmp);
  for (int ix = 0; ix < cntnam; ix++)
    gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (obent),
                    mo_string_cstr (namarr[ix]));

此时,我已经按名称排序了所有的(最多几千个)并使用gtk_combo_box_text_append_text“静态”地添加了它们。

  GtkWidget *combtextent = gtk_bin_get_child (GTK_BIN (obent));
  MOM_ASSERTPRINTF (GTK_IS_ENTRY (combtextent), "bad combtextent");
  MOM_ASSERTPRINTF (gtk_entry_get_completion (GTK_ENTRY (combtextent)) ==
                    NULL, "got completion in combtextent");

我有点惊讶地发现gtk_entry_get_completion(GTK_ENTRY(combtextent))为空。
但是我卡在这里了。我在思考:
1.编写一些mom_set_complete_objectid(const char*prefix),它会返回一个垃圾回收的mo_value_t集合,该集合表示具有至少四个字符的前缀(如"_47n")的对象。对我来说这非常容易编码,并且几乎完成了。
2.制作自己的本地GtkEntryCompletion* mycompl = ...,可以按照我的要求进行完成。然后我将使用gtk_entry_set_completion(GTK_ENTRY(combtextent), mycompl);将其放入我的gtk-combo-box-text的文本输入框combtextent中。

它应该使用使用gtk_combo_box_text_append_text添加的条目作为“静态”名称完成角色吗?如何使用从我的mom_set_complete_objectid返回的动态设置值进行动态完成;给定一些对象指针obr和一些char bufid [20];,我可以轻松快速地使用mo_cstring_from_hi_lo_ids(bufid, obr->mo_ob_hid, obr->mo_ob_loid)填充该对象的对象ID obr..

我不知道如何编写上述内容。 供参考,我现在只返回组合框文本:

  // if the entered text starts with a letter, I want it to be
  // completed with the appended text above if the entered text starts
  // with an undersore, then a digit, then two alphanum (like _0BV or
  // _6S3 for example), I want to call a completion function.
#warning objectentry: what should I code here?
  return obent;
}  /* end mom_objectentry */

我的方法是否正确?

上面的mom_objectentry函数用于填充具有短生命周期的模态对话框。

我更喜欢简单的代码而不是效率。实际上,我的代码是临时的(我希望启动我的语言,并生成所有的C代码!)在实践中,我可能只有几百个名称和最多几万个对象ID。因此,性能并不是非常重要,但编写简单的代码(一些概念上的“一次性”代码)更加重要。

如果可能的话,我不想添加自己的GTK类。我更喜欢使用现有的GTK类和小部件,通过GTK信号和回调进行自定义。

上下文

我的应用程序是一种试验性的编程语言和实现,具有接近Scheme或Python(或JavaScript,忽略原型方面...)的语义,但具有大不相同的语法(在GTK小部件中显示和输入),使用Boehm垃圾收集器对值进行垃圾回收(包括对象、集合、元组、字符串...)。通常情况下,值(包括对象)是持久的(除了与GTK相关的数据:应用程序从一个几乎为空的窗口开始)。整个语言堆在某些Sqlite“数据库”中以类似JSON的语法持久化(在应用程序退出时生成),并在启动时重新加载_momstate.sql。对象ID对于在GTK小部件中向用户显示对象引用、进行持久化以及生成与对象相关的C代码(例如,ID为_76f7e2VcL8IJC1hq6的对象可能与一些生成的C代码中的mo_76f7e2VcL8IJC1hq6标识符相关联;这部分原因是我有自己的对象ID格式,而不是使用UUID)。

PS. 我的C代码是GPLv3自由软件,可在github上获得。它是MELT监视器,分支expjs,提交e2b3b99ef66394...

NB:这里提到的对象是我的语言对象,而不是GTK对象。它们都有唯一的对象ID,其中一些但不是大多数都有名称。


只是回应悬赏,我确实有一种方法来做它,但我通常使用Python编程GTK3的东西。那么你是否可以在答案中使用伪代码/Python? - B8vrede
我对Python不是很熟悉,所以我更喜欢用C语言回答。但是,我可能会尝试将基于Python的答案适应到C语言中,这总比没有好。请注意,所有GTK 参考文档都是关于我的系统libgtk-3.so.0的C函数。因此,也许你可以在那里列出一些函数... - Basile Starynkevitch
我已经添加了我的解决方案作为“答案”。关键区别在于,我会在GtkComboBox中使用GtkListStore模型来维护匹配关键字的副本列表(让Gtk+根据需要引用计数和垃圾回收它们),并使用“popup”信号触发列表重建。根据我粗略的测试程序,这似乎对大约一千个匹配项有效(尽管它可能只是创建GtkListStore的愚蠢方式是瓶颈)。我不认为这是一个完整真正的答案;只是我朝着解决方案的起点。 - Nominal Animal
2个回答

5
我不会展示如何实现它的确切代码,因为我只使用过GTK和Python而非GTK和C语言,但是由于C语言和Python函数可以轻松地进行转换,所以应该没问题。
OP的方法实际上是正确的,所以我将尝试填补空白。由于静态选项的数量有限,可能不会太多变化,因此使用gtk_combo_box_text_append添加到GtkComboBoxText的内部模型中是有意义的。
这涵盖了静态部分,对于动态部分,如果我们可以仅存储此静态模型,并在字符串开头找到_时用临时模型替换它,那就太完美了。但我们不应该这样做,因为文档说:

您不应调用gtk_combo_box_set_model()或尝试通过其GtkCellLayout接口将更多单元格打包到此组合框中。

所以我们需要解决这个问题,一种方法是在GtkComboBoxText的输入框中添加一个GtkEntryCompletion。这将使输入框尝试根据当前模型完成当前字符串。作为额外的奖励,它还可以添加所有选项共同拥有的字符,如下所示:

enter image description here

由于我们不想预先加载所有动态选项,我认为最好的方法是将changed监听器连接到GtkEntry,这样当我们有下划线和一些字符时,我们可以加载动态选项。

由于GtkEntryCompletion在内部使用GtkListStore,因此我们可以重用Nominal Animal在他的答案中提供的代码的一部分。 主要区别在于:connectGtkEntry上进行,并在填充器中替换GtkComboTextGtkEntryCompletion。 然后一切都应该没问题了,但我希望我能写出像样的C代码,但现在只能做到这个程度。

编辑:使用GTK3的Python小演示

import gi

gi.require_version('Gtk', '3.0')

import gi.repository.Gtk as Gtk

class CompletingComboBoxText(Gtk.ComboBoxText):
    def __init__(self, static_options, populator, **kwargs):
        # Set up the ComboBox with the Entry
        Gtk.ComboBoxText.__init__(self, has_entry=True, **kwargs)

        # Store the populator reference in the object
        self.populator = populator

        # Create the completion
        completion = Gtk.EntryCompletion(inline_completion=True)

        # Specify that we want to use the first col of the model for completion
        completion.set_text_column(0)
        completion.set_minimum_key_length(2)

        # Set the completion model to the combobox model such that we can also autocomplete these options
        self.static_options_model = self.get_model()
        completion.set_model(self.static_options_model)

        # The child of the combobox is the entry if 'has_entry' was set to True
        entry = self.get_child()
        entry.set_completion(completion)

        # Set the active option of the combobox to 0 (which is an empty field)
        self.set_active(0)

        # Fill the model with the static options (could also be used for a history or something)
        for option in static_options:
            self.append_text(option)

        # Connect a listener to adjust the model when the user types something
        entry.connect("changed", self.update_completion, True)


    def update_completion(self, entry, editable):
        # Get the current content of the entry
        text = entry.get_text()

        # Get the completion which needs to be updated
        completion = entry.get_completion()

        if text.startswith("_") and len(text) >= completion.get_minimum_key_length():
            # Fetch the options from the populator for a given text
            completion_options = self.populator(text)

            # Create a temporary model for the completion and fill it
            dynamic_model = Gtk.ListStore.new([str])
            for completion_option in completion_options:
                dynamic_model.append([completion_option])
            completion.set_model(dynamic_model)
        else:
            # Restore the default static options
            completion.set_model(self.static_options_model)


def demo():
    # Create the window
    window = Gtk.Window()

    # Add some static options
    fake_static_options = [
        "comment",
        "if",
        "the_GUI",
        "the_system",
        "payload_json",
        "x1",
        "payload_json",
        "payload_vectval"
    ]

    # Add the the Combobox
    ccb = CompletingComboBoxText(fake_static_options, dynamic_option_populator)
    window.add(ccb)

    # Show it
    window.show_all()
    Gtk.main()


def dynamic_option_populator(text):
    # Some fake returns for the populator
    fake_dynamic_options = [
        "_5Hf0fFKvRVa71ZPM0",
        "_8261sbF1f9ohzu2Iu",
        "_0BV96V94PJIn9si1K",
        "_0BV1sbF1f9ohzu2Iu",
        "_0BV0fFKvRVa71ZPM0",
        "_0Hf0fF4PJIn9si1Ks",
        "_6KvRVa71JIn9si1Kw",
        "_5HKvRVa71Va71ZPM0",
        "_8261sbF1KvRVa71ZP",
        "_0BKvRVa71JIn9si1K",
        "_0BV1KvRVa71ZPu2Iu",
        "_0BV0fKvRVa71ZZPM0",
        "_0Hf0fF4PJIbF1f9oh",
        "_61sbFV0fFKn9si1Kw",
        "_5Hf0fFKvRVa71ozu2",
    ]

    # Only return those that start with the text
    return [fake_dynamic_option for fake_dynamic_option in fake_dynamic_options if fake_dynamic_option.startswith(text)]


if __name__ == '__main__':
    demo()
    Gtk.main()

除了添加内联完成(除弹出完成之外),是否有任何理由使用GtkEntryCompletionGtkComboBoxText而不是GtkComboBox?只有GtkComboBoxText具有“不要干扰模型”的限制。 - Nominal Animal
你有一些Python代码可以分享给我们吗?还有确切的GTK3版本是什么? - Basile Starynkevitch
@NominalAnimal 对不起,我错过了你的评论,优点主要在于内联完成。从用户体验的角度来看,这比组合框更有意义。但我想你的答案更接近 OP 的要求,因为你解决了限制问题。 - B8vrede
@BasileStarynkevitch 添加了一个带有大量注释的演示 :) - B8vrede
这可能是正确的答案(因为可悲的是,@NominalAnimal的答案和演示在我的系统上留下了一个空白窗口)。 - Basile Starynkevitch

4
这是我的建议:
使用GtkListStore来包含一组GTK管理的字符串(本质上是标识符字符串的副本),这些字符串与当前前缀字符串匹配。
(如gtk_list_store_set()所述,会复制一个G_TYPE_STRING项目。我认为额外复制的开销在这里是可接受的;我想这不应该对实际性能产生太大影响,作为交换,GTK+将为我们管理引用计数。)
以上内容在GTK+回调函数中实现,该回调函数获取额外的指针作为有效载荷(在创建或激活GUI时设置;我建议您使用某个结构来保留您需要生成的匹配引用)。将回调连接到下拉框popup信号,以便每次展开列表时都会调用它。
请注意,正如B8vrede在评论中指出的那样,不应该通过其模型修改GtkComboBoxText;因此,必须使用GtkComboBox实际例子 为简单起见,假设您需要查找或生成所有已知标识符的数据都保存在一个结构中,比如
struct generator {
    /* Whatever data you need to generate prefix matches */
};

而组合框填充助手函数通常是这样的:
static void combo_box_populator(GtkComboBox *combobox, gpointer genptr)
{
    struct generator *const generator = genptr;

    GtkListStore *combo_list = GTK_LIST_STORE(gtk_combo_box_get_model(combobox));

    GtkWidget *entry = gtk_bin_get_child(GTK_BIN(combobox));
    const char *prefix = gtk_entry_get_text(GTK_ENTRY(entry));
    const size_t prefix_len = (prefix) ? strlen(prefix) : 0;

    GtkTreeIter iterator;

    /* Clear the current store */
    gtk_list_store_clear(combo_list);

    /* Initialize the list iterator */
    gtk_tree_model_get_iter_first(GTK_TREE_MODEL(combo_list), &iterator);

    /* Find all you want to have in the combo box;
       for each  const char *match, do:
    */

        gtk_list_store_append(combo_list, &iterator);
        gtk_list_store_set(combo_list, &iterator, 0, match, -1);

    /* Note that the string pointed to by match is copied;
       match is not referred to after the _set() returns.
    */
}

当UI被构建或激活时,您需要确保GtkComboBox有一个条目(以便用户可以在其中输入文本),以及一个GtkListStore的模型:

    struct generator *generator;
    GtkWidget *combobox;
    GtkListStore *combo_list;

    combo_list = gtk_list_store_new(1, G_TYPE_STRING);
    combobox = gtk_combo_box_new_with_model_and_entry(GTK_TREE_MODEL(combo_list));
    gtk_combo_box_set_id_column(GTK_COMBO_BOX(combobox), 0);
    gtk_combo_box_set_entry_text_column(GTK_COMBO_BOX(combobox), 0);
    gtk_combo_box_set_button_sensitivity(GTK_COMBO_BOX(combobox), GTK_SENSITIVITY_ON);

    g_signal_connect(combobox, "popup", G_CALLBACK(combo_box_populator), generator);

在我的系统上,默认的弹出加速键是Alt+Down,但我假设你已经将其更改为Tab
我有一个简陋的工作示例在这里(一个.tar.xz tarball,CC0):它从标准输入读取行,并以反向顺序列出与用户前缀匹配的行在组合框列表中(弹出时)。如果条目为空,则组合框将包含所有输入行。我没有更改默认的加速键,因此请尝试Alt+Down而不是Tab
我还有相同的示例,但使用GtkComboBoxText在这里(也是CC0)。这不使用GtkListStore模型,而是使用gtk_combo_box_text_remove_all()gtk_combo_box_text_append_text()函数直接操作列表内容。(两个示例中只有几行不同。)不幸的是,文档并没有明确说明此接口引用还是复制字符串。虽然复制是唯一有意义的选项,并且可以从当前的Gtk+源代码中验证,但缺乏明确的文档使我犹豫不决。
比较我上面链接的这两个示例(如果您使用make编译和运行它们,则都会获取大约500个随机单词),我没有看到任何速度差异。两者都使用相同的天真方式从链表中选择前缀匹配项,这意味着这两种方法(GtkComboBox+模型或GtkComboBoxText)应该是大致相同的速度。
在我的机器上,当弹出窗口中有1000个或更多匹配项时,两者都变得非常慢;只有一百个或更少的匹配项,感觉即时。对我来说,这表明从链表中选择前缀匹配项的缓慢/天真方式不是罪魁祸首(因为在两种情况下都遍历了整个列表),而是GTK+组合框只是不适用于大型列表。(减速明显比线性更糟糕。)

谢谢。这可能是我所期望的答案。也许几个小时或几天后,当我编写一些代码时,我会接受它。顺便问一下,您的简单工作示例是否可在某个地方作为免费软件(例如在Github上)获得? - Basile Starynkevitch
1
这段代码的问题在于它忽略了文档中一个重要的注释,即:“您不应该调用gtk_combo_box_set_model()或尝试通过其GtkCellLayout接口将更多单元格打包到此组合框中。”(https://developer.gnome.org/gtk3/stable/GtkComboBoxText.html) - B8vrede
@B8vrede:你说得对。那么,你有建设性的建议吗?如果有,请尝试写一些答案... - Basile Starynkevitch
我目前正在编写一个,这就是我找到评论的原因;) - B8vrede
我正在尝试使用您下载的示例,但它无法正常工作。小部件仍然是空白的。我还提交了我的635c3805cb8c42ab1,但它也无法正常工作(我的GTK版本为3.21.5)。如果您更喜欢通过电子邮件与我联系,请发送至basile at starynkevitch dot net... - Basile Starynkevitch
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接