本节将重新构造Tfe文本编辑器。

  • 在工具栏上放置了打开、保存和关闭按钮。此外,GtkMenuButton被添加到工具栏中。当点击这个按钮时会显示一个弹出式菜单。在这里,弹出式的含义很广泛,包括下拉式菜单。
  • 新建、另存为、偏好和退出项目被放入菜单中。
    这将最常用的操作绑定到工具栏按钮上。其他的则存储在菜单后面。因此,它更实用。

此外,还增加了以下特性。

  • 加速器。例如,Ctrl-O读取文件并创建一个新页面。
  • 字体选择的首选对话框。
  • 警告对话框,确认关闭或退出,不保存内容。
  • GSettings保留字体选择。

Static variables shared by functions in tfeapplication.c

tfe的下一个版本在tfeapplication.c中有静态变量。静态变量很方便,但不利于维护。因此,最终版本将删除它们,并采用另一种方式来覆盖静态变量。

无论如何,下面是关于静态变量的代码。

static GtkDialog *pref; // preference dialog
static GtkFontButton *fontbtn; // font button
static GSettings *settings; // GSetting
static GtkDialog *alert; // alert dialog
static GtkLabel *lb_alert;  // label in the alert dialog
static GtkButton *btn_accept; // accept button in the alert dialog
static GtkCssProvider *provider0; //CSS provider for textview padding
static GtkCssProvider *provider; // CSS provider for fontsstatic gulong pref_close_request_handler_id = 0;
static gulong alert_close_request_handler_id = 0;
static gboolean is_quit; // flag whether to quit or close

文件中的任何函数都可以使用这些变量。

Signal tags in ui files

这四个按钮包含在ui文件tfe.ui中。与前面章节不同的是信号标签。以下内容是从tfe.ui中提取的,它描述了打开按钮。

<object class="GtkButton" id="btno"><property name="label">Open</property><signal name="clicked" handler="open_cb" swapped="TRUE" object="nb"></signal>
</object>

Signal tag指定了信号、处理程序和user_data对象的名称。

  • 信号名称为“clicked”。
  • 处理程序是“open_cb”。
  • 用户数据对象是“nb”(GtkNoteBook实例)。

swapped属性与g_signal_connect_swap函数具有相同的效果。所以,上面的信号标签的工作原理是一样的:

g_signal_connect_swapped (btno, "clicked", G_CALLBACK (open_cb), nb);

这个函数在处理程序中交换了按钮和第四个参数(btno和nb)。如果使用g_signal_connect,处理程序如下所示:

/* The parameter user_data is assigned with nb */
static void
open_cb (GtkButton *btno, gpointer user_data) { ... ... }

如果使用g_signal_connect_swapped,则交换按钮和用户数据。

/* btno and user_data (nb) are exchanged */
static void
open_cb (GtkNoteBook *nb) { ... ... }

如果button实例在处理程序中无用,那就好了。

当你在ui文件中使用signal标签时,你需要"-WI, --export-dynamic" 选项来编译。你可以通过在meson.build的可执行函数中添加“export_dynamic: true”参数来实现这一点。并从处理程序中删除static类。

void
open_cb (GtkNotebook *nb) {notebook_page_open (nb);
}

如果你添加了static,函数就在文件的作用域中,从外部是看不到的。信号tag无法找到函数。

Menu and GkMenuButton

传统的菜单结构是好的。然而,我们并不经常使用所有的菜单或按钮。有些可能根本不会被点击。因此,将一些常用的按钮放在工具栏上,将其他按钮放在菜单中是一个好主意。这样的菜单通常连接到GtkMenuButton。

菜单被描述在menu.ui文件。

 1 <?xml version="1.0" encoding="UTF-8"?>2 <interface>3   <menu id="menu">4     <section>5       <item>6         <attribute name="label">New</attribute>7         <attribute name="action">win.new</attribute>8       </item>9       <item>
10         <attribute name="label">Save As…</attribute>
11         <attribute name="action">win.saveas</attribute>
12       </item>
13     </section>
14     <section>
15       <item>
16         <attribute name="label">Preference</attribute>
17         <attribute name="action">win.pref</attribute>
18       </item>
19     </section>
20     <section>
21       <item>
22         <attribute name="label">Quit</attribute>
23         <attribute name="action">win.close-all</attribute>
24       </item>
25     </section>
26   </menu>
27 </interface>

这里有4个元素,“New”、“Saveas”、“Preference"和"Quit”。

  • “新建”菜单创建一个新的空白页面。
  • “Saveas”菜单将当前页面保存为与原始页面不同的文件名。
  • “偏好”菜单设置偏好项。这个版本的tfe只有字体首选项。
  • “Quit”菜单退出应用程序。

这四个菜单不太常用。这就是为什么它们被放在菜单按钮后面的菜单中。

以上所有操作都具有“win”范围。即使运行第二个应用程序,Tfe也只有一个窗口。因此,在这个应用程序中,作用域“app”和“win”差别很小。

菜单和菜单按钮连接到gtk_menu_button_set_menu_model函数。下面的变量btnm指向一个GtkMenuButton对象。

  build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/menu.ui");menu = G_MENU_MODEL (gtk_builder_get_object (build, "menu"));gtk_menu_button_set_menu_model (btnm, menu);

Actions and Accelerators

菜单与操作相连接。动作由一个数组和g_action_map_add_action_entries函数定义。

  const GActionEntry win_entries[] = {{ "open", open_activated, NULL, NULL, NULL },{ "save", save_activated, NULL, NULL, NULL },{ "close", close_activated, NULL, NULL, NULL },{ "new", new_activated, NULL, NULL, NULL },{ "saveas", saveas_activated, NULL, NULL, NULL },{ "pref", pref_activated, NULL, NULL, NULL },{ "close-all", close_all_activated, NULL, NULL, NULL }};g_action_map_add_action_entries (G_ACTION_MAP (win), win_entries, G_N_ELEMENTS (win_entries), nb);

有7个操作:打开、保存、关闭、新建、保存、优先和关闭所有。但是只有四份菜单。New、saveas、pref和close-all操作分别对应New、saveas、preference和quit菜单。“打开”、“保存”和“关闭”三个操作没有对应的菜单。它们是必要的吗?是的,因为它们对应于加速器。

加速器是一种快捷键功能。它们由数组和gtk_application_set_accs_for_action函数定义。

  struct {const char *action;const char *accels[2];} action_accels[] = {{ "win.open", { "<Control>o", NULL } },{ "win.save", { "<Control>s", NULL } },{ "win.close", { "<Control>w", NULL } },{ "win.new", { "<Control>n", NULL } },{ "win.saveas", { "<Shift><Control>s", NULL } },{ "win.close-all", { "<Control>q", NULL } },};for (i = 0; i < G_N_ELEMENTS(action_accels); i++)gtk_application_set_accels_for_action(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels);

这段代码有点复杂。数组action-accels[]是一个结构体数组。其结构如下:

  struct {const char *action;const char *accels[2];}

成员操作是一个字符串。成员accels是一个包含两个字符串的数组。例如,

{ "win.open", { "<Control>o", NULL } },

这是数组action_accels的第一个元素。

  • 成员行为是"win.open"。这指定了"open"操作属于window对象。
  • 成员accels是一个由两个字符串组成的数组,"<Control>o"和NULL。第一个字符串指定了一个键组合。控制键和“o”。如果你一直按control键并按o键,就会激活win.open动作。第二个字符串NULL(或零)表示列表(数组)结束。你可以定义多个加速键,列表必须以NULL(零)结尾。如果你想这样做,数组长度必须大于等于3。解析器识别"<control>o""<Shift><Alt>F2", "<Ctrl>minus"等等。如果你想使用类似“-”的符号键,请使用“-”。小写字母和符号(字符代码)之间的这种关系在GTK 4源代码中的gdkkeysyms.h中指定。

Open, save and close handlers

有两个打开的处理程序。一个是按钮上单击信号的处理程序。另一个是动作上的激活信号。

Open button (clicked)> open.cb handler
Ctrl-o key (accerelator) (key down)> open action activated ==> open_activated handler

但这两个处理程序的行为是相同的。open_activate调用open_cb。

 1 void2 open_cb (GtkNotebook *nb) {3   notebook_page_open (nb);4 }5 6 static void7 open_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {8   GtkNotebook *nb = GTK_NOTEBOOK (user_data);9   open_cb (nb);
10 }

保存和关闭处理程序也是如此。

Saveas handler

TfeTextView有一个saveas函数。因此,我们只需在tfenotebook.c中编写一个包装器函数。

 1 static TfeTextView *2 get_current_textview (GtkNotebook *nb) {3   int i;4   GtkWidget *scr;5   GtkWidget *tv;6 7   i = gtk_notebook_get_current_page (nb);8   scr = gtk_notebook_get_nth_page (nb, i);9   tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
10   return TFE_TEXT_VIEW (tv);
11 }
12
13 void
14 notebook_page_saveas (GtkNotebook *nb) {15   g_return_if_fail(GTK_IS_NOTEBOOK (nb));
16
17   TfeTextView *tv;
18
19   tv = get_current_textview (nb);
20   tfe_text_view_saveas (TFE_TEXT_VIEW (tv));
21 }

函数get_current_textview和之前一样。函数notebook_page_saveas只是调用了tfe_text_view_saveas。

在tfeapplication.c中,保存处理程序只调用notebook_page_saveas。

1 static void
2 saveas_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {3   GtkNotebook *nb = GTK_NOTEBOOK (user_data);
4   notebook_page_saveas (nb);
5 }

Preference and alert dialog

Preference dialog

首选项对话框xml定义被添加到tfe.ui中。

<object class="GtkDialog" id="pref"><property name="title">Preferences</property><property name="resizable">FALSE</property><property name="modal">TRUE</property><property name="transient-for">win</property><child internal-child="content_area"><object class="GtkBox" id="content_area"><child><object class="GtkBox" id="pref_boxh"><property name="orientation">GTK_ORIENTATION_HORIZONTAL</property><property name="spacing">12</property><property name="margin-start">12</property><property name="margin-end">12</property><property name="margin-top">12</property><property name="margin-bottom">12</property><child><object class="GtkLabel" id="fontlabel"><property name="label">Font:</property><property name="xalign">1</property></object></child><child><object class="GtkFontButton" id="fontbtn"></object></child></object></child></object></child>
</object>
  • 偏好对话框是独立的对话框。它不是顶级GtkApplicationwindow的后代部件。因此,对话框对象周围没有子标签。
  • 这个对话框有四个属性。GtkDialog是GtkWindow的一个子对象(不是子构件),因此它继承了GtkWindow的所有属性。Title、resizable、modal和transient-for属性继承自GtkWindow。Transient-for指定一个临时的父窗口,对话框的位置就是基于这个窗口。
  • 标签<child internal-child="content_area">放在对话框内容的顶部。你需要用content_area id指定一个GtkBox对象标签。这个对象在gtkdialog.ui((复合部件))中定义。但您需要在子标签中再次定义它。复合小部件将在下一节中解释。有关GtkDialog ui标签的更多信息,请参阅:
    • GTK 4 API reference – GtkBuilder
    • GTK 4 API reference – GtkDialog
    • GtkDialog ui file
  • 在内容区域中有一个水平的GtkBox。
  • GtkLabel和GtkFontButton在GtkBox中。

我希望首选项对话框在应用程序的生命周期内保持活跃。因此,有必要从对话框中捕获“close-request”信号并停止该信号的传播。(当点击窗口右上角的x按钮关闭按钮时,会发出这个信号。)这是通过信号处理程序返回TRUE来完成的。

static gboolean
dialog_close_cb (GtkDialog *dialog) {gtk_widget_set_visible (GTK_WIDGET (dialog), false);return TRUE;
}
... ...
( in app_startup function )
pref_close_request_handler_id = g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_close_cb), NULL);
... ...

信号发射一般分为五个阶段。

  • 如果信号的标志是G_SIGNAL_RUN_FIRST,则调用默认处理程序。在注册信号时设置默认处理程序。它不同于用户信号处理程序,简称为信号处理程序,由g_signal_connectseries函数连接。默认处理程序可以在阶段1、3或5中调用。大多数默认处理程序是G_SIGNAL_RUN_FIRST或G_SIGNAL_RUN_LAST。
  • 调用信号处理程序,除非通过g_signal_connect_after连接。
  • 如果信号的标志是G_SIGNAL_RUN_LAST,则调用默认处理程序。
  • 如果通过g_signal_connect_after连接,则调用信号处理程序。
  • 如果信号的标志是G_SIGNAL_RUN_CLEANUP,则调用默认处理程序。

“close-request”信号是G_SIGNAL_RUN_LAST。因此,调用的顺序是:

  1. 信号处理程序dialog_close_cb
  2. 默认的处理程序

如果用户信号处理程序返回TRUE,那么将停止调用其他处理程序。因此,上面的程序阻止了对默认处理程序的调用,并停止了对话框的关闭过程。

下列代码是从tfeapplication.c中提取的。

static gulong pref_close_request_handler_id = 0;
static gulong alert_close_request_handler_id = 0;
... ...
static gboolean
dialog_close_cb (GtkDialog *dialog, gpointer user_data) {gtk_widget_set_visible (GTK_WIDGET (dialog), false);return TRUE;
}
... ...
static void
pref_activated (GSimpleAction *action, GVariant *parameter, gpointer nb) {gtk_window_present (GTK_WINDOW (pref));
}
... ...
void
app_shutdown (GApplication *application) {... ... ...if (pref_close_request_handler_id > 0)g_signal_handler_disconnect (pref, pref_close_request_handler_id);gtk_window_destroy (GTK_WINDOW (pref));... ... ...
}
... ...
static void
tfe_startup (GApplication *application) {... ...pref = GTK_DIALOG (gtk_builder_get_object (build, "pref"));pref_close_request_handler_id = g_signal_connect (GTK_DIALOG (pref), "close-request", G_CALLBACK (dialog_close_cb), NULL);... ...
}
  • 首选项对话框上的close- request信号连接到处理程序dialog_close_cb。它改变了对话框的close行为。当信号发出时,可见性被设置为false,默认的处理程序被取消。因此,对话框消失但存在。
  • 处理程序pref_activate显示首选项对话框。
  • 关闭处理程序app_shutdown将处理程序与“close-request”信号断开连接,并销毁pref window。

Alert dialog

如果用户没有保存就关闭了一个页面,建议显示一个警告,让用户确认。警报对话框就是在这种情况下使用的。

  <object class="GtkDialog" id="alert"><property name="title">Are you sure?</property><property name="resizable">FALSE</property><property name="modal">TRUE</property><property name="transient-for">win</property><child internal-child="content_area"><object class="GtkBox"><child><object class="GtkBox"><property name="orientation">GTK_ORIENTATION_HORIZONTAL</property><property name="spacing">12</property><property name="margin-start">12</property><property name="margin-end">12</property><property name="margin-top">12</property><property name="margin-bottom">12</property><child><object class="GtkImage"><property name="icon-name">dialog-warning</property><property name="icon-size">GTK_ICON_SIZE_LARGE</property></object></child><child><object class="GtkLabel" id="lb_alert"></object></child></object></child></object></child><child type="action"><object class="GtkButton" id="btn_cancel"><property name="label">Cancel</property></object></child><child type="action"><object class="GtkButton" id="btn_accept"><property name="label">Close</property></object></child><action-widgets><action-widget response="cancel" default="true">btn_cancel</action-widget><action-widget response="accept">btn_accept</action-widget></action-widgets><signal name="response" handler="alert_response_cb" swapped="NO" object="nb"></signal></object>

这个ui文件描述了警告对话框。有些部分与选项对话框相同。在内容区域中有两个对象,GtkImage和GtkLabel。

GtkImage显示一个图像。图片可以来自文件、资源、图标主题等。上图显示了当前图标主题中的一个图标。您可以通过gtk4-icon-browser查看主题中的图标。

$ gtk4-icon-browser

“对话框警告”图标类似于下面这样。
这些是我亲手做的。警告对话框中的真实图像更漂亮。

-GtkLabel lb_alert还没有文本。一个警告消息将被插入到程序中。

有两个子标签具有“action”类型。它们是位于操作区域的按钮对象。Action-widgets标签描述按钮的操作。如果单击按钮btn_cancel,则发出带有cancel响应(GTK_RESPONSE_CANCEL)的响应信号。如果单击按钮btn_accept,则用accept响应(GTK_RESPONSE_ACCEPT)发出响应信号。响应信号连接到alert_response_cb处理程序。

在应用程序存活期间,警告对话框保持活跃。“close-request”信号被处理程序停止

Alert dialog and close handlers

如果用户关闭页面或退出应用程序而没有保存内容,则会出现警告对话框。有4个处理程序,close_cb, close_activated, win_close_request_cb和close_all_activated。前两个函数在notebook页面关闭时调用。其他的在主窗口关闭时调用——因此,所有的notebook都关闭了。

  • close button => close_cb (=> alert dialog)
  • Ctrl-W => close_activated => close_cb (=> alert dialog)
  • Close button (x button at the right top of the main window) => win_close_request_cb (=> alert dialog)
  • Quit menu or Ctrl-Q => close_all_activated => win_close_request_cb (=> alert dialog)
static gboolean is_quit;
... ...
static gboolean
win_close_request_cb (GtkWindow *win, GtkNotebook *nb) {is_quit = true;if (has_saved_all (nb))return false;else {gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to quit?");gtk_button_set_label (btn_accept, "Quit");gtk_window_present (GTK_WINDOW (alert));return true;}
}
... ...
void
close_cb (GtkNotebook *nb) {is_quit = false;if (has_saved (GTK_NOTEBOOK (nb)))notebook_page_close (GTK_NOTEBOOK (nb));else {gtk_label_set_text (lb_alert, "Contents aren't saved yet.\nAre you sure to close?");gtk_button_set_label (btn_accept, "Close");gtk_window_present (GTK_WINDOW (alert));}
}
... ...
static void
close_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {GtkNotebook *nb = GTK_NOTEBOOK (user_data);close_cb (nb);
}
... ...
static void
close_all_activated (GSimpleAction *action, GVariant *parameter, gpointer user_data) {GtkNotebook *nb = GTK_NOTEBOOK (user_data);GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);if (! win_close_request_cb (GTK_WINDOW (win), nb)) // checks whether contents are savedgtk_window_destroy (GTK_WINDOW (win));
}
... ...
void
alert_response_cb (GtkDialog *alert, int response_id, gpointer user_data) {GtkNotebook *nb = GTK_NOTEBOOK (user_data);GtkWidget *win = gtk_widget_get_ancestor (GTK_WIDGET (nb), GTK_TYPE_WINDOW);gtk_widget_set_visible (GTK_WIDGET (alert), false);if (response_id == GTK_RESPONSE_ACCEPT) {if (is_quit)gtk_window_destroy (GTK_WINDOW (win));elsenotebook_page_close (nb);}
}
... ...
static void
app_startup (GApplication *application) {... ...build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe/tfe.ui");win = GTK_APPLICATION_WINDOW (gtk_builder_get_object (build, "win"));... ...g_signal_connect (GTK_WINDOW (win), "close-request", G_CALLBACK (win_close_request_cb), nb);... ...
}

当用户试图退出应用程序时,静态变量is_quit为true,否则为false。

  • 当用户单击关闭按钮时,close_cb处理程序将被调用。处理程序将is_quit设置为false。如果当前页已经保存,函数has_saved返回true。如果为true,它调用notebook_page_close关闭当前页。否则,显示警告对话框。对话框的响应信号连接到处理程序alert_response_cb。它首先隐藏对话框。然后检查response_id。如果是GTK_RESPONSE_ACCEPT,表示用户单击了关闭按钮,则关闭当前页面。否则它什么都不做。
  • 当用户按下"Ctrl-w"时,close_activated处理程序会被调用。它只是调用close_cb。
  • 当用户点击主窗口的关闭按钮时,窗口会发出“关闭请求”信号。该信号事先已经连接到win_close_request_cb处理程序。连接是在应用程序上的启动处理程序中完成的。win_close_request_cb处理程序将is_quit设置为true。如果has_save_all返回true,它就返回false,这意味着信号移动到默认处理程序,主窗口将关闭。否则,显示警告对话框并返回true。因此,信号停止,默认处理程序不会被调用。但是如果用户点击了警告对话框中的accept按钮,响应处理程序alert_response_cb会调用gtk_window_destroy,主窗口将被关闭。
  • 当用户单击quit菜单或按下"Ctrl-q"时,则调用close_all_activated处理程序。它调用了win_close_request_cb。如果返回值为false,它会销毁主窗口。否则它什么也不做,但是win_close_request_cb显示了警告对话框。

Has_saved and has_saved_all functions

这两个函数定义在文件tfenotebook.c中。它们是公共函数。

 1 gboolean2 has_saved (GtkNotebook *nb) {3   g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);4 5   TfeTextView *tv;6   GtkTextBuffer *tb;7 8   tv = get_current_textview (nb);9   tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
10   if (gtk_text_buffer_get_modified (tb))
11     return false;
12   else
13     return true;
14 }
15
16 gboolean
17 has_saved_all (GtkNotebook *nb) {18   g_return_val_if_fail (GTK_IS_NOTEBOOK (nb), false);
19
20   int i, n;
21   GtkWidget *scr;
22   GtkWidget *tv;
23   GtkTextBuffer *tb;
24
25   n = gtk_notebook_get_n_pages (nb);
26   for (i = 0; i < n; ++i) {27     scr = gtk_notebook_get_nth_page (nb, i);
28     tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
29     tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
30     if (gtk_text_buffer_get_modified (tb))
31       return false;
32   }
33   return true;
34 }
  • 1-14: has_saved函数。
  • 10:如果缓冲区的内容已经被修改,函数gtk_text_buffer_get_modified返回true,因为modified标志设置为false。在以下情况下,该标志设置为false:
    • 创建缓冲区。
    • 缓冲区的内容被替换
    • 缓冲区的内容保存到文件中。
  • 10-13:如果保存了当前页面的内容且没有对其进行任何修改,则此函数返回true。如果当前页面被修改过且没有保存,则返回false。
  • 16-34: has_saved_all函数。该函数类似于has_saved函数。如果所有的页都保存了,则返回true。如果在上次保存页之后,至少有一页被修改过,则返回false。

Notebook page tab

如果你有一些页面,并将它们编辑在一起,你可能会弄不清需要保存哪个文件。普通文件编辑器在修改内容时更改选项卡。GtkTextBuffer提供了"modified-changed"信号来通知修改。

static void
notebook_page_build (GtkNotebook *nb, GtkWidget *tv, char *filename) {... ...g_signal_connect (GTK_TEXT_VIEW (tv), "change-file", G_CALLBACK (file_changed_cb), NULL);g_signal_connect (tb, "modified-changed", G_CALLBACK (modified_changed_cb), tv);
}

在建立页时,将“change-file”和“modified-changed”信号分别连接到file_changed_cb和modified_changed_cb处理程序。

 1 static void2 file_changed_cb (TfeTextView *tv) {3   GtkWidget *nb =  gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);4   GtkWidget *scr;5   GtkWidget *label;6   GFile *file;7   char *filename;8 9   if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
10     return;
11   file = tfe_text_view_get_file (tv);
12   scr = gtk_widget_get_parent (GTK_WIDGET (tv));
13   if (G_IS_FILE (file)) {14     filename = g_file_get_basename (file);
15     g_object_unref (file);
16   } else
17     filename = get_untitled ();
18   label = gtk_label_new (filename);
19   gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
20 }
21
22 static void
23 modified_changed_cb (GtkTextBuffer *tb, gpointer user_data) {24   TfeTextView *tv = TFE_TEXT_VIEW (user_data);
25   GtkWidget *scr = gtk_widget_get_parent (GTK_WIDGET (tv));
26   GtkWidget *nb =  gtk_widget_get_ancestor (GTK_WIDGET (tv), GTK_TYPE_NOTEBOOK);
27   GtkWidget *label;
28   const char *filename;
29   char *text;
30
31   if (! GTK_IS_NOTEBOOK (nb)) /* tv not connected to nb yet */
32     return;
33   else if (gtk_text_buffer_get_modified (tb)) {34     filename = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (nb), scr);
35     text = g_strdup_printf ("*%s", filename);
36     label = gtk_label_new (text);
37     g_free (text);
38     gtk_notebook_set_tab_label (GTK_NOTEBOOK (nb), scr, label);
39   } else
40     file_changed_cb (tv);
41 }

file_changed_cb处理程序为notebook的page标记提供了一个新文件名。modified_changed_cb处理程序在文件名的开头插入一个星号。这是一个标志,表明文件已经被修改,但还没有保存。

  • 1-20: file_changed_cb处理程序。
  • 9-10:如果信号是在页面构建过程中发出的,那么tv可能不是nb的后代。也就是说,没有对应于tv的页面。这样就不需要修改选项卡的名称,因为不存在选项卡。
  • 13-15:如果file是GFile,那么它获取文件名并释放对file的引用。
  • 16-17:否则,它将"Untitled"(+一个数字)赋值给filename
  • 18-19:用文件名创建GtkLabel,并用GtkLabel设置页面的选项卡。
  • 22-41: modified_changed_cb处理程序。
  • 31-32:如果tv不是nb的后代,那么什么都不需要做。
  • 33-35:如果内容被修改了,那么它获取选项卡的文本并在文本的开头添加星号。
  • 36-38:设置带有星号的文件名选项卡
  • 39-40:否则调用file_changed_cb并更新文件名(不带星号)。

Font

GtkFontButton and GtkFontChooser

GtkFontButton是一个按钮类,它显示当前字体,用户可以使用按钮更改字体。如果用户点击按钮,它会打开一个字体选择对话框。用户可以在对话框中改变字体(字体族、样式、粗细和大小)。然后按钮保持新的字体并显示它。

该按钮在应用程序启动过程中使用构建器设置。信号“font-set”连接到处理程序font_set_cb。当用户选择字体时,会发出信号“font-set”。

static void
font_set_cb (GtkFontButton *fontbtn) {PangoFontDescription *pango_font_desc;char *s, *css;pango_font_desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (fontbtn));s = pfd2css (pango_font_desc); // converts Pango Font Description into CSS style stringcss = g_strdup_printf ("textview {%s}", s);gtk_css_provider_load_from_data (provider, css, -1);g_free (s);g_free (css);
}
... ...
static void
app_startup (GApplication *application) {... ...fontbtn = GTK_FONT_BUTTON (gtk_builder_get_object (build, "fontbtn"));... ...g_signal_connect (fontbtn, "font-set", G_CALLBACK (font_set_cb), NULL);... ...
}

GtkFontChooser是由GtkFontButton实现的接口。函数gtk_font_chooser_get_font_desc获取当前选择字体的PangoFontDescription。PangoFontDescription包含字体族、样式、粗细和大小。函数pfd2css将它们转换为CSS样式字符串。转换如下所示。

PangoFontDescription:
font-family: Monospace
font-style: normal
font-weight: normal
font-size: 12pt
=>
“font-family: Monospace; font-style: normal; font-weight: 400; font-size: 12pt;”

然后,font_set_cb创建一个CSS字符串并将其放入provider实例中。提供程序已提前添加到默认显示。因此,该处理程序立即影响textview内容的字体。

CSS and Pango

pfd2css.c中包含了从PangoFontDescription到CSS的转换器。文件名的意思是:

  • pfd => PangoFontDescripter
  • 2 => to
  • css => CSS (Cascade Style Sheet)
    文件中的所有公共函数都有“pdf2css”前缀。
 1 #include <pango/pango.h>2 #include "pfd2css.h"3 4 // Pango font description to CSS style string5 // Returned string is owned by caller. The caller should free it when it is useless.6 7 char*8 pfd2css (PangoFontDescription *pango_font_desc) {9   char *fontsize;
10
11   fontsize = pfd2css_size (pango_font_desc);
12   return g_strdup_printf ("font-family: \"%s\"; font-style: %s; font-weight: %d; font-size: %s;",
13               pfd2css_family (pango_font_desc), pfd2css_style (pango_font_desc),
14               pfd2css_weight (pango_font_desc), fontsize);
15   g_free (fontsize);
16 }
17
18 // Each element (family, style, weight and size)
19
20 const char*
21 pfd2css_family (PangoFontDescription *pango_font_desc) {22   return pango_font_description_get_family (pango_font_desc);
23 }
24
25 const char*
26 pfd2css_style (PangoFontDescription *pango_font_desc) {27   PangoStyle pango_style = pango_font_description_get_style (pango_font_desc);
28   switch (pango_style) {29   case PANGO_STYLE_NORMAL:
30     return "normal";
31   case PANGO_STYLE_ITALIC:
32     return "italic";
33   case PANGO_STYLE_OBLIQUE:
34     return "oblique";
35   default:
36     return "normal";
37   }
38 }
39
40 int
41 pfd2css_weight (PangoFontDescription *pango_font_desc) {42   PangoWeight pango_weight = pango_font_description_get_weight (pango_font_desc);
43   switch (pango_weight) {44   case PANGO_WEIGHT_THIN:
45     return 100;
46   case PANGO_WEIGHT_ULTRALIGHT:
47     return 200;
48   case PANGO_WEIGHT_LIGHT:
49     return 300;
50   case PANGO_WEIGHT_SEMILIGHT:
51     return 350;
52   case PANGO_WEIGHT_BOOK:
53     return 380;
54   case PANGO_WEIGHT_NORMAL:
55     return 400; /* or "normal" */
56   case PANGO_WEIGHT_MEDIUM:
57     return 500;
58   case PANGO_WEIGHT_SEMIBOLD:
59     return 600;
60   case PANGO_WEIGHT_BOLD:
61     return 700; /* or "bold" */
62   case PANGO_WEIGHT_ULTRABOLD:
63     return 800;
64   case PANGO_WEIGHT_HEAVY:
65     return 900;
66   case PANGO_WEIGHT_ULTRAHEAVY:
67     return 900; /* In PangoWeight definition, the weight is 1000. But CSS allows the weight below 900. */
68   default:
69     return 400; /* "normal" */
70   }
71 }
72
73 char *
74 pfd2css_size (PangoFontDescription *pango_font_desc) {75   if (pango_font_description_get_size_is_absolute (pango_font_desc))
76     return g_strdup_printf ("%dpx", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
77   else
78     return g_strdup_printf ("%dpt", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
79 }
  • 1: Pango的公共函数、常量和结构定义在pango/pango.h中。
  • 2:包含pdf2css.h使得在pdf2css.c文件的任何地方调用公共函数成为可能。因为头文件包含了所有公共函数的声明。
  • 7-16: pdf2css功能。这个函数从作为参数的PangoFontDescription实例中获取字体族、样式、粗细和大小。它将它们构建成一根弦。返回的字符串归调用者所有。调用者应该在字符串无用时释放它。
  • 20-23: pfd2css_famili函数。这个函数从PangoFontDescription实例中获取font-family字符串。该字符串属于PFD实例,因此调用者不能修改或释放该字符串。
  • 25-38: pdf2css_style函数。这个函数从PangoFontDescription实例中获取font-style字符串。字符串是静态的,调用者不能修改或释放它。
  • 40-71: pfd2css_weight函数。这个函数从PangoFontDescription实例中获取font-weight整数值。取值范围为100 ~ 900。它定义在[CSS字体模块第3级](…/src/CSS字体模块(第三级)规范。
    • 100 -薄
    • 200 -超短(超短)
    • 300 -短
    • 400 -正常
    • 500 -中等
    • 600 -半加粗(半加粗)
    • 700 -粗体
    • 800 -加粗(超加粗)
    • 900 -黑色(重)
  • 73-79: pdf2css_size函数。这个函数从PangoFontDescription实例中获取字体大小字符串。字符串是由调用者拥有的,因此调用者应该在它无用时释放它。PangoFontDescription具有绝对大小或非绝对大小。
    • 如果是绝对的,则大小以设备为单位。
    • 如果它是非绝对的,则大小以点为单位。
  • 设备单位的定义依赖于输出设备。它通常是屏幕的像素,打印机的点。
  • Pango将尺寸作为自己的尺寸。常量PANGO_SCALE是用于Pango距离的尺寸和设备单位之间的比例。PANGO_SCALE当前的值是1024,但将来可能会改变。在设置字体大小时,设备单位总是被认为是点而不是像素。如果字体大小为12pt,则pango中的大小为12PANGO_SCALE=121024=12288。

有关更多信息,请参阅Pango API参考。

GSettings

我们希望在应用程序退出后保持字体数据。有一些方法可以实现它。

  • 制作配置文件。例如,一个文本文件“~/.config/tfe/font.cfg”保存字体信息。
  • 使用GSettings对象。GSettings的基本思想类似于configuration file。配置信息数据放入数据库文件中。

GSettings简单易用,但概念有点难以理解。这一小节首先描述概念,然后如何编程。

GSettings schema

GSettings schema描述了一组键、值类型和其他一些信息。GSettings对象使用这种模式,它将键的值写入/读取到数据库中的正确位置。

  • schema有id。id不能重复。我们经常使用与应用程序id相同的字符串,但是schema id和应用程序id是不同的。您可以使用不同于应用程序id的名称。schema id是由句点分隔的字符串。例如,com.github.ToshioCP.Tfe”是正确的schema id。
  • schema通常有一个路径path。路径是数据库中的一个位置。每个键都存储在该路径下。例如,如果在路径/com/github/ToshioCP/tfe/中定义了一个key = font,那么该key’s在数据库中的位置就是/com/github/ToshioCP/tfe/font。Path是一个以斜杠(/)开始和结束的字符串。它由斜线分隔。
  • GSettings将信息保存为键值(key-value)样式。Key是一个字符串,以小写字母开始,然后是小写字母、数字或破折号(-),以小写字母或数字结束。不允许连续的破折号。值可以是任何类型。GSettings将值存储为GVariant类型,可以是整型、双精度、布尔型、字符串,也可以是数组等复杂类型。值的类型需要在模式中定义。
  • 每个键都需要设置一个默认值。
  • 可以为每个键设置可选的摘要和描述。

schema以XML格式描述。例如,

 1 <?xml version="1.0" encoding="UTF-8"?>2 <schemalist>3   <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">4     <key name="font" type="s">5       <default>'Monospace 12'</default>6       <summary>Font</summary>7       <description>A font to be used for textview.</description>8     </key>9   </schema>
10 </schemalist>
  • 4: type属性为“s”。它是GVariant类型的字符串。对于GVariant类型字符串,请参阅GLib API参考——GVariant类型字符串。其他常见的类型有:

    • “b”:gboolean
    • “i”:gint32。
    • “d”:double。

更多资料请参阅:

  • GLib API Reference – GVariant Format Strings
  • GLib API Reference – GVariant Text Format
  • GLib API Reference – GVariant
  • GLib API Reference – VariantType

gsettings

首先,让我们尝试gsettings应用程序。它是GSettings的配置工具。

$ gsettings help
Usage:gsettings --versiongsettings [--schemadir SCHEMADIR] COMMAND [ARGS?]Commands:help                      Show this informationlist-schemas              List installed schemaslist-relocatable-schemas  List relocatable schemaslist-keys                 List keys in a schemalist-children             List children of a schemalist-recursively          List keys and values, recursivelyrange                     Queries the range of a keydescribe                  Queries the description of a keyget                       Get the value of a keyset                       Set the value of a keyreset                     Reset the value of a keyreset-recursively         Reset all values in a given schemawritable                  Check if a key is writablemonitor                   Watch for changesUse "gsettings help COMMAND" to get detailed help.

List schemas.

$ gsettings list-schemas
org.gnome.rhythmbox.podcast
ca.desrt.dconf-editor.Demo.Empty
org.gnome.gedit.preferences.ui
org.gnome.evolution-data-server.calendar
org.gnome.rhythmbox.plugins.generic-player... ...

每一行都是一个schema id。每个schema都有一个key-value配置数据。你可以使用list- recur命令查看它们。让我们看一下org.gnome.calculator模式的键和值。

gsettings list-recursively org.gnome.calculator
org.gnome.calculator source-currency ''
org.gnome.calculator source-units 'degree'
org.gnome.calculator button-mode 'basic'
org.gnome.calculator target-currency ''
org.gnome.calculator base 10
org.gnome.calculator angle-units 'degrees'
org.gnome.calculator word-size 64
org.gnome.calculator accuracy 9
org.gnome.calculator show-thousands false
org.gnome.calculator window-position (122, 77)
org.gnome.calculator refresh-interval 604800
org.gnome.calculator target-units 'radian'
org.gnome.calculator precision 2000
org.gnome.calculator number-format 'automatic'
org.gnome.calculator show-zeroes false

GNOME计算器使用此schema。运行计算器并更改schema,然后再次检查schema。

$ gnome-calculator

修改为“高级模式”并退出。
运行gsettings并检查button-mode的值。

gsettings list-recursively org.gnome.calculator... ...org.gnome.calculator button-mode 'advanced'... ...

现在我们知道GNOME计算器使用了gsettings,它将button-mode键设置为“advanced”。即使计算器退出,value仍然存在。因此,当计算器再次运行时,它将显示为高级模式。

glib-compile-schemas

GSettings schema 使用XML格式指定。XML模式文件必须具有文件名扩展名 .gschema.xml。下面是应用程序tfe的XML schema文件。

 1 <?xml version="1.0" encoding="UTF-8"?>2 <schemalist>3   <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">4     <key name="font" type="s">5       <default>'Monospace 12'</default>6       <summary>Font</summary>7       <description>A font to be used for textview.</description>8     </key>9   </schema>
10 </schemalist>

文件名是"com.github.ToshioCP.tfe.gschema.xml"。Schema XML文件名通常是Schema id后面加上".gschema.xml "后缀。您可以使用与模式id不同的名称,但不建议这样做。

  • 2:顶层元素为<schemalist>
  • 3: schema标签具有path和id属性。路径确定settings存储在概念全局设置树中的位置。id标识模式。
  • 4: Key标签有两个属性。Name是键的名称。Type是键值的类型,它是一个GVariant格式字符串。
  • 5:默认值font = Monospace 12
    6:概述和描述元素用于描述key。它们是可选的,但建议将它们添加到XML文件中。

XML文件由glib-compile-schemas编译。编译时,glib-compile-schemas编译所有在给定的目录中具有“.gschema.xml”文件扩展名作为参数的XML文件。它将XML文件转换为二进制文件gschema .compiled。假设上面的XML文件在tfe6目录下。

$ glib-compile-schemas tfe6

然后,在tfe6下生成gschemas.compiled。测试应用时,设置环境变量GSETTINGS_SCHEMA_DIR,这样GSettings对象就能找到gschemas.compiled。

$ GSETTINGS_SCHEMA_DIR=(the directory gschemas.compiled is located):$GSETTINGS_SCHEMA_DIR (your application name)

GSettings对象通过以下步骤查找此文件。

  • 它搜索在环境变量XDG_DATA_DIRS中指定的所有目录的glib-2.0/schemas子目录。常见的目录是/usr/share/glib-2.0/schemas和/usr/local/share/glib-2.0/schemas。
  • 如果定义了GSETTINGS_SCHEMA_DIR环境变量,它将搜索该变量中指定的所有目录。GSETTINGS_SCHEMA_DIR可以指定多个以冒号(:)分隔的目录。

在上面的目录中,存储了所有的.gschema.xml文件。因此,在安装应用程序时,请按照下面的说明安装模式。

  • 1.创建.gschema.xml文件。
  • 2.将其复制到上面的目录之一。例如,/usr/local/share/glib-2.0/schemas。
  • 3.在上面的目录上运行glib-compile-schemas。你可能需要sudo。

GSettings object and g_settings_bind

现在,我们进入下一个主题——如何编写GSettings。

... ...
static GSettings *settings;
... ...
void
app_shutdown (GApplication *application) {... ...g_clear_object (&settings);... ...
}
... ...
static void
app_startup (GApplication *application) {... ...settings = g_settings_new ("com.github.ToshioCP.tfe");g_settings_bind (settings, "font", fontbtn, "font", G_SETTINGS_BIND_DEFAULT);... ...
}

静态变量settings保存了一个指向GSettings实例的指针。在应用程序退出之前,应用程序释放GSettings实例。函数g_clear_object减少GSettings实例的引用计数,并将NULL赋值给变量settings。

Startup handler创建模式id为“com.github.ToshioCP”的GSettings实例。并将指针赋值给settings。函数g_settings_bind连接settings键(key和value)和fontbtn的"font"属性。那么这两个值将始终相同。如果一个值改变了,那么另一个也会自动改变。

有关更多信息,请参阅GIO API参考——GSettings。

Build with Meson

Build and test

Meson提供了gnome.compile_schemas方法在构建目录中编译XML文件。这用于测试应用程序。写下面的meson.build文件。

gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')
  • build_by_default:如果为true,将默认构建目标。
  • depend_files:要编译的XML文件。

在上面的例子中,这个方法运行glib-compile-schemas,从XML文件com.github.ToshioCP.tfe.gschema.xml生成gschemas.compiled。gschemas.compiled文件位于build目录下。如果你将meson运行为meson _build,将ninja运行为ninja -C _build,那么它就在_build目录下。

编译后,你可以像这样测试你的应用程序:

$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe

installation

将应用程序安装在 H O M E / b i n 或 HOME/bin或 HOME/bin或HOME/中是一个好主意。本地/ bin目录。它们是本地bin目录,工作方式类似于系统bin目录,如/bin、/usr/bin或usr/local/bin。你需要添加--prefix=$HOME or --prefix=$HOME/.local到meson。

$ meson --prefix=$HOME/.local _build

如果你想将应用程序安装到系统的bin目录中,例如/usr/local/bin,则不需要——prefix选项。

Meson识别这样的选项:
函数executable需要install: true才能安装程序。

executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)

然而,你还需要做一件事。将XML文件复制到schema目录,并在该目录上执行glib-compile-schemas。

  • Install_data函数将文件复制到目标目录。
  • gnome.post_install函数在安装后以schema_dir作为参数执行glib-compile-schemas。该函数从Meson 0.57.0开始可用。如果版本早于此,则使用meson.add_install_script代替。
schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
gnome.post_install (glib_compile_schemas: true)

函数get_option返回构建选项的值。参见Meason参考手册。运算符//分隔符连接字符串。
Meson.buid 的源代码如下。

 1 project('tfe', 'c')2 3 gtkdep = dependency('gtk4')4 5 gnome=import('gnome')6 resources = gnome.compile_resources('resources','tfe.gresource.xml')7 gnome.compile_schemas(build_by_default: true, depend_files: 'com.github.ToshioCP.tfe.gschema.xml')8 9 sourcefiles=files('tfeapplication.c', 'tfenotebook.c', 'pfd2css.c', '../tfetextview/tfetextview.c')
10
11 executable('tfe', sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)
12
13 schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
14 install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
15 gnome.post_install (glib_compile_schemas: true)

tfe的源文件在src/tfe6目录下。将它们复制到临时目录,然后编译并安装它。

$ meson --prefix=$HOME/.local _build
$ ninja -C _build
$ GSETTINGS_SCHEMA_DIR=_build:$GSETTINGS_SCHEMA_DIR _build/tfe # test
$ ninja -C _build install
$ ls $HOME/.local/bin
... ...
... tfe
... ...
$ ls $HOME/.local/share/glib-2.0/schemas
com.github.ToshioCP.tfe.gschema.xml
gschema.dtd
gschemas.compiled
... ...
$ tfe

二十、Gtk4-GtkMenuButton, accelerators, font, pango and gsettings相关推荐

  1. Shell脚本学习-阶段二十六-Web服务与端口

    文章目录-Shell阶段二十六-端口与服务对照表 前言 端口与Web服务对照表 简介 前言 端口与Web服务对照表 2端口:管理实用程序 3端口:压缩进程 5端口:远程作业登录 7端口:回显 9端口: ...

  2. 二十款漂亮CSS字体样式

    平时对于网站整站的字体风格常常很纠结,纠结到底用什么样式才行,才好看,后来在网上搜集收到二十种我认为很漂亮的字体样式并使用,感觉很美观,解决了我以前的纠结.具体二十种漂亮样式如下:    样式一: b ...

  3. html 字体形状,二十款漂亮的CSS字体样式

    样式一:body { margin: 0; padding: 0; line-height: 1.5em; font-family: 'Times New Roman', Times, serif; ...

  4. java从入门到精通二十四(三层架构完成增删改查)

    java从入门到精通二十四(三层架构完成增删改查) 前言 环境准备 创建web项目结构 导入依赖和配置文件 创建层次模型 实现查询 实现添加 实现修改 完成删除 做一个用户登录验证 会话技术 cook ...

  5. JavaFx之使用指定字体样式(二十九)

    JavaFx之使用指定字体样式(二十九) javafx use specified font 29 javafx默认的字体样式太丑,可能需要我们自定义字体样式. 之前说好放弃学习javafx,没想到越 ...

  6. NLP(二十六)限定领域的三元组抽取的一次尝试

      本文将会介绍笔者在2019语言与智能技术竞赛的三元组抽取比赛方面的一次尝试.由于该比赛早已结束,笔者当时也没有参加这个比赛,因此没有测评成绩,我们也只能拿到训练集和验证集.但是,这并不耽误我们在这 ...

  7. openlayers6【二十六】业务交互:Cluster 聚合标注控制,显示隐藏聚合标注

    文章目录 1. 聚合标注常见业务需求交互 2. 实现效果 3. 核心代码 4. 完整代码 1. 聚合标注常见业务需求交互 业务需求:在地图场景中,通常是多种业务场景(点位图标,边界图层,热力图层等)在 ...

  8. 二十款漂亮的CSS字体样式,让你受用非浅

    二十款漂亮的CSS字体样式,让你受用非浅 平时对于网站整站的字体风格常常很纠结,纠结到底用什么样式才行,才好看,后来在网上搜集收到二十种我认为很漂亮的字体样式并使用,感觉很美观,解决了我以前的纠结.具 ...

  9. 二十款漂亮的CSS字体样式

    样式一: body {margin: 0; padding: 0; line-height: 1.5em;font-family: "Times New Roman", Times ...

最新文章

  1. 除了iframe还有什么方法加载第三方网页_IE9常见问题的解决方法
  2. dram sram利用 利用_使用量子力学技术的新型超低功耗存储器或将取代DRAM和Flash...
  3. $ is not defined 如何解决
  4. C语言 pthread_cancelpthread_detach
  5. easyUI根据参数动态的生成列数
  6. RTCP协议解析--RR
  7. uniapp分销商城源码开发
  8. 三权鼎立形式的软件开发方式
  9. 虚拟主机需要备案吗?
  10. ue编辑器php漏洞:ueditor getshell
  11. YOLOv3批量测试图片并保存在自定义文件夹下
  12. 如何选择一台适合个人使用的云服务器?
  13. Bailian1664 Placing apples【递推+记忆化递归】
  14. AndroidStudio画一条横线
  15. argc,argv,argv[0]用法详解
  16. 不同波特率传输时间计算
  17. Win10远程桌面出现身份验证错误,由于CredSSP加密Oracle修正 解决方法
  18. [RK3288][Android6.0] 移植笔记 --- 13.3寸eDP显示屏添加
  19. Easy3D开发——点云孔洞填充
  20. 支付宝沙箱支付可能遇见的问题

热门文章

  1. 质量基础设施NQI“一站式”线上公共服务平台建设方案
  2. 中国企业社交网络(ESN)市场趋势报告、技术动态创新及市场预测
  3. 使用 C# 读取 zip 压缩包解压文件的方法及注意事项
  4. 翻译mos文章 scn headroom ID 1376995.1
  5. lbm 弛豫时间_弛豫时间的概述
  6. 拼多多商品活动排名查询
  7. 基于51单片机带显示器的音乐盒设计
  8. 英语二 - 常用词根五
  9. 数据库优化的方法及步骤
  10. 高物实验报告计算机模拟高分子,高分子物理实验 [闫红强 编] 2012年版