将内容保存到文件¶
在本课中,你将学习如何添加带有键盘快捷键的菜单项,提示用户选择一个文件来保存 GtkTextBuffer 的内容,并异步保存文件。
添加“另存为”操作¶
打开
text_viewer-window.c文件,找到 TextViewerWindow 部件的实例初始化函数text_viewer_window_init创建 save-as 操作,将其
activate信号连接到一个回调函数,并将操作添加到窗口
static void
text_viewer_window__open_file_dialog (GAction *action,
GVariant *param,
TextViewerWindow *self);
static void
text_viewer_window__save_file_dialog (GAction *action,
GVariant *param,
TextViewerWindow *self);
static void
text_viewer_window_init (TextViewerWindow *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
g_autoptr (GSimpleAction) open_action = g_simple_action_new ("open", NULL);
g_signal_connect (open_action, "activate", G_CALLBACK (text_viewer_window__open_file_dialog), self);
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (open_action));
g_autoptr (GSimpleAction) save_action = g_simple_action_new ("save-as", NULL);
g_signal_connect (save_action, "activate", G_CALLBACK (text_viewer_window__save_file_dialog), self);
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (save_action));
GtkTextBuffer *buffer = gtk_text_view_get_buffer (self->main_text_view);
g_signal_connect (buffer,
"notify::cursor-position",
G_CALLBACK (text_viewer_window__update_cursor_position),
self);
}
打开
window.py文件,找到 TextViewerWindow 部件的实例初始化方法创建 save-as 操作,将其
activate信号连接到一个回调函数,并将操作添加到窗口
def __init__(self, **kwargs):
super().__init__(**kwargs)
open_action = Gio.SimpleAction(name="open")
open_action.connect("activate", self.open_file_dialog)
self.add_action(open_action)
save_action = Gio.SimpleAction(name="save-as")
save_action.connect("activate", self.save_file_dialog)
self.add_action(save_action)
buffer = self.main_text_view.get_buffer()
buffer.connect("notify::cursor-position", self.update_cursor_position)
打开
window.vala文件,找到 TextViewer.Window 部件的实例初始化方法创建 save-as 操作,将其
activate信号连接到一个回调函数,并将操作添加到窗口
namespace TextViewer {
public class Window : Gtk.ApplicationWindow {
// ...
public Window (Gtk.Application app) {
Object (application: app);
}
construct {
var open_action = new SimpleAction ("open", null);
open_action.activate.connect (this.open_file_dialog);
this.add_action (open_action);
var save_action = new SimpleAction ("save-as", null);
save_action.activate.connect (this.save_file_dialog);
this.add_action (save_action);
Gtk.TextBuffer buffer = this.text_view.buffer;
buffer.notify["cursor-position"].connect (this.update_cursor_position);
}
}
}
打开
window.js文件,找到 TextViewer.Window 部件的构造函数创建 save-as 操作,将其
activate信号连接到一个回调函数,并将操作添加到窗口
export const TextViewerWindow = GObject.registerClass({
GTypeName: 'TextViewerWindow',
Template: 'resource:///com/example/TextViewer/window.ui',
InternalChildren: ['main_text_view', 'open_button', 'cursor_pos'],
}, class TextViewerWindow extends Adw.ApplicationWindow {
constructor(application) {
super({ application });
const openAction = new Gio.SimpleAction({name: 'open'});
openAction.connect('activate', () => this.openFileDialog());
this.add_action(openAction);
const saveAction = new Gio.SimpleAction({name: 'save-as'});
saveAction.connect('activate', () => this.saveFileDialog());
this.add_action(saveAction);
const buffer = this._main_text_view.buffer;
buffer.connect("notify::cursor-position", this.updateCursorPosition.bind(this));
}
选择一个文件¶
在 save-as 操作的 activate 回调函数中,使用 GTK_FILE_CHOOSER_ACTION_SAVE 操作创建一个文件选择对话框,并连接到其
response信号
static void
text_viewer_window__save_file_dialog (GAction *action G_GNUC_UNUSED,
GVariant *param G_GNUC_UNUSED,
TextViewerWindow *self)
{
g_autoptr (GtkFileDialog) dialog =
gtk_file_dialog_new ();
gtk_file_dialog_save (dialog,
GTK_WINDOW (self),
NULL,
on_save_response,
self);
}
def save_file_dialog(self, action, _):
native = Gtk.FileDialog()
native.save(self, None, self.on_save_response)
private void save_file_dialog (Variant? parameter) {
var filechooser = new Gtk.FileChooserNative ("Save File As",
this,
Gtk.FileChooserAction.SAVE,
"_Save",
"_Cancel");
filechooser.response.connect ( );
filechooser.show ();
}
saveFileDialog() {
const fileDialog = new Gtk.FileDialog();
fileDialog.save(this, null, async (self, result) => {
// we'll implement it in the next step
});
}
在回调函数中,检索用户选择的位置的 GFile,并调用
save_file()函数
static void
on_save_response (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
TextViewerWindow *self = user_data;
g_autoptr (GFile) file =
gtk_file_dialog_save_finish (dialog, result, NULL);
if (file != NULL)
save_file (self, file);
}
def on_save_response(self, dialog, result):
file = dialog.save_finish(result)
if file is not None:
self.save_file(file)
private void save_file_dialog (Variant? parameter) {
var filechooser = new Gtk.FileChooserNative ("Save File As",
this,
Gtk.FileChooserAction.SAVE,
"_Save",
"_Cancel");
filechooser.response.connect ((dialog, response) {
if (response == Gtk.ResponseType.ACCEPT) {
File file = filechooser.get_file ();
this.save_file (file);
}
});
filechooser.show ();
}
saveFileDialog() {
const fileDialog = new Gtk.FileDialog();
fileDialog.save(this, null, async (self, result) => {
try {
const file = self.save_finish(result);
if (file) {
await this.saveFile(file); // we will define this method soon
}
} catch(_) {
// user closed the dialog without selecting any file
}
});
}
保存文本缓冲区的内容¶
在
save_file函数中,使用起始和结束 GtkTextIter 作为缓冲区的边界,检索 GtkTextBuffer 的内容,然后启动一个异步操作,将数据保存到 GFile 指向的位置
static void
save_file (TextViewerWindow *self,
GFile *file)
{
GtkTextBuffer *buffer = gtk_text_view_get_buffer (self->main_text_view);
// Retrieve the iterator at the start of the buffer
GtkTextIter start;
gtk_text_buffer_get_start_iter (buffer, &start);
// Retrieve the iterator at the end of the buffer
GtkTextIter end;
gtk_text_buffer_get_end_iter (buffer, &end);
// Retrieve all the visible text between the two bounds
char *text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
// If there is nothing to save, return early
if (text == NULL)
return;
g_autoptr(GBytes) bytes = g_bytes_new_take (text, strlen (text));
// Start the asynchronous operation to save the data into the file
g_file_replace_contents_bytes_async (file,
bytes,
NULL,
FALSE,
G_FILE_CREATE_NONE,
NULL,
save_file_complete,
self);
}
在
save_file_complete函数中,完成异步操作并报告任何错误
static void
save_file_complete (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GFile *file = G_FILE (source_object);
g_autoptr (GError) error = NULL;
g_file_replace_contents_finish (file, result, NULL, &error);
// Query the display name for the file
g_autofree char *display_name = NULL;
g_autoptr (GFileInfo) info =
g_file_query_info (file,
"standard::display-name",
G_FILE_QUERY_INFO_NONE,
NULL,
NULL);
if (info != NULL)
{
display_name =
g_strdup (g_file_info_get_attribute_string (info, "standard::display-name"));
}
else
{
display_name = g_file_get_basename (file);
}
if (error != NULL)
{
g_printerr ("Unable to save “%s”: %s\n",
display_name,
error->message);
}
}
与 Adw、Gio 和 Gtk 一起导入 GLib 模块
from gi.repository import Adw, Gio, GLib, Gtk
在
save_file函数中,使用起始和结束 GtkTextIter 作为缓冲区的边界,检索 GtkTextBuffer 的内容,然后启动一个异步操作,将数据保存到 GFile 指向的位置
def save_file(self, file):
buffer = self.main_text_view.get_buffer()
# Retrieve the iterator at the start of the buffer
start = buffer.get_start_iter()
# Retrieve the iterator at the end of the buffer
end = buffer.get_end_iter()
# Retrieve all the visible text between the two bounds
text = buffer.get_text(start, end, False)
# If there is nothing to save, return early
if not text:
return
bytes = GLib.Bytes.new(text.encode('utf-8'))
# Start the asynchronous operation to save the data into the file
file.replace_contents_bytes_async(bytes,
None,
False,
Gio.FileCreateFlags.NONE,
None,
self.save_file_complete)
在
save_file_complete函数中,完成异步操作并报告任何错误
def save_file_complete(self, file, result):
res = file.replace_contents_finish(result)
info = file.query_info("standard::display-name",
Gio.FileQueryInfoFlags.NONE)
if info:
display_name = info.get_attribute_string("standard::display-name")
else:
display_name = file.get_basename()
if not res:
print(f"Unable to save {display_name}")
在
save_file函数中,使用起始和结束 Gtk.TextIter 作为缓冲区的边界,检索 Gtk.TextBuffer 的内容,然后启动一个异步操作,将数据保存到 File 指向的位置
private void save_file (File file) {
Gtk.TextBuffer buffer = this.main_text_view.buffer;
// Retrieve the iterator at the start of the buffer
Gtk.TextIter start;
buffer.get_start_iter (out start);
// Retrieve the iterator at the end of the buffer
Gtk.TextIter end;
buffer.get_end_iter (out end);
// Retrieve all the visible text between the two bounds
string? text = buffer.get_text (start, end, false);
if (text == null || text.length == 0)
return;
var bytes = new Bytes.take (text.data);
file.replace_contents_bytes_async.begin (bytes,
null,
false,
FileCreateFlags.NONE,
null,
(object, result) => { });
}
在异步函数结束时调用的 lambda 表达式中,完成异步操作并报告任何错误
private void save_file (File file) {
Gtk.TextBuffer buffer = this.main_text_view.buffer;
// Retrieve the iterator at the start of the buffer
Gtk.TextIter start;
buffer.get_start_iter (out start);
// Retrieve the iterator at the end of the buffer
Gtk.TextIter end;
buffer.get_end_iter (out end);
// Retrieve all the visible text between the two bounds
string? text = buffer.get_text (start, end, false);
if (text == null || text.length == 0)
return;
var bytes = new Bytes.take (text.data);
file.replace_contents_bytes_async.begin (bytes,
null,
false,
FileCreateFlags.NONE,
null,
(object, result) => {
string display_name;
// Query the display name for the file
try {
FileInfo info = file.query_info ("standard::display-name",
FileQueryInfoFlags.NONE);
display_name = info.get_attribute_string ("standard::display-name");
} catch (Error e) {
display_name = file.get_basename ();
}
try {
file.replace_contents_async.end (result, null);
} catch (Error e) {
stderr.printf ("Unable to save “%s”: %s\n", display_name, e.message);
}
});
}
就像我们在处理打开文件时对 load_contents_async 函数所做的那样,让我们将 Gio 内置的 replace_contents_bytes_async 函数转换为返回 Promise 的函数。这样,我们将能够利用方便的 async/await 语法。打开 main.js,并添加以下行
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';
import Adw from 'gi://Adw?version=1';
import GLib from 'gi://GLib';
import { TextViewerWindow } from './window.js';
Gio._promisify(Gio.File.prototype,
'load_contents_async',
'load_contents_finish');
Gio._promisify(Gio.File.prototype,
'replace_contents_bytes_async',
'replace_contents_finish');
// ...
回到 window.js,我们需要定义一个新的 saveFile 函数,因为它将在用户选择要保存内容的文件时被调用。在该函数中,我们需要使用起始和结束 GtkTextIter 作为缓冲区的边界,检索 GtkTextBuffer 的内容。然后,启动一个异步操作,将数据保存到 GFile 指向的位置。
async saveFile(file) {
const buffer = this._main_text_view.buffer;
// Retrieve the start and end iterators
const startIterator = buffer.get_start_iter();
const endIterator = buffer.get_end_iter();
// Retrieve all the visible text between the two bounds
const text = buffer.get_text(startIterator, endIterator, false);
if (text === null || text.length === 0) {
logWarning("Text is empty, ignoring");
return;
}
// Get file's name, which will be needed in case of an error
let fileName;
try {
const fileInfo = file.query_info("standard::display-name", FileQueryInfoFlags.NONE);
fileName = fileInfo.get_attribute_string("standard::display-name");
} catch(_) {
fileName = file.get_basename();
}
try {
// Save the file (asynchronously)
await file.replace_contents_bytes_async(
new GLib.Bytes(text),
null,
false,
Gio.FileCreateFlags.NONE,
null);
} catch(e) {
logError(`Unable to save ${fileName}: ${e.message}`)
}
}
为“另存为”操作添加键盘快捷键¶
打开
text_viewer-application.c源代码文件,找到 TextViewerApplication 实例初始化函数text_viewer_application_init将 Ctrl + Shift + S 添加为 win.save-as 操作的加速器快捷键
static void
text_viewer_application_init (TextViewerApplication *self)
{
// ...
gtk_application_set_accels_for_action (GTK_APPLICATION (self),
"win.save-as",
(const char *[]) {
"<Ctrl><Shift>s",
NULL,
});
}
打开
main.py源代码文件,找到 Application 类的实例初始化函数将 Ctrl + Shift + S 添加为 win.save-as 操作的加速器快捷键
class Application(Adw.Application):
def __init__(self):
super().__init__(application_id='com.example.PyTextViewer',
flags=Gio.ApplicationFlags.FLAGS_NONE)
# ...
self.set_accels_for_action('win.save-as', ['<Ctrl><Shift>s'])
打开
application.vala源代码文件,找到 TextViewer.Application 类的实例初始化函数将 Ctrl + Shift + S 添加为 win.save-as 操作的加速器快捷键
public Application () {
Object (application_id: "com.example.TextViewer",
flags: ApplicationFlags.FLAGS_NONE);
}
construct {
// ...
this.set_accels_for_action ("win.save-as", { "<Ctrl><Shift>s" });
}
打开
main.js源代码文件,找到 TextViewerApplication 类的构造函数将 Ctrl + Shift + S 添加为 win.save-as 操作的加速器快捷键
constructor() {
super({application_id: 'com.example.TextViewer', flags: Gio.ApplicationFlags.FLAGS_NONE});
this.set_accels_for_action('win.open', [ '<Ctrl>o' ]);
this.set_accels_for_action('win.save-as', [ '<Ctrl><Shift>s' ]);
// ...
}
将“另存为”快捷键添加到键盘快捷键帮助¶
在源代码目录中找到
help-overlay.ui文件找到 GtkShortcutsGroup 定义
在快捷键组中添加一个新的 GtkShortcutsShortcut 定义,用于 win.save 操作
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes" context="shortcut window">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Open</property>
<property name="action-name">win.open</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Save As</property>
<property name="action-name">win.save-as</property>
</object>
</child>
在本课结束时,你应该能够
从主菜单中选择“另存为”菜单项
从对话框中选择一个文件
将文本查看器的内容保存到所选文件中