第二十二课¶
深入开发:QuickEdit¶
假若所有的计算机程序都如 HelloWorld 或其他演示程序那般简单!开发人员的生活就不会那般艰难。奈何,事实并不如此。在接下来的几节课中,我们将会揭去这层薄的面纱,深入其中了。为任何的操作系统设计简单的程序都会如这些程序一般琐碎,并且它们都会非常的粗糙。幸运的是,随着我们对操作系统功能的学习和了解,您将会对编写稍大型的 Haiku 程序中所涉及到的内容有所认识。
您可能会问,我们的项目会是什么呢?它是一个简单的格式化文本编辑器,QuickEdit。
尽管 Haiku 已经有了一个编辑器 StyledEdit,所以,毫无疑问,我们编写的代码将会与它有所雷同,但我们添加的功能并不一定都能够在 StyledEdit 中找到。
程序架构¶
如果没有了 BTextView 控件,我们的任务可就非常非常艰巨了。当然,不禁自惭,文档处理器的编写尤为不易呀。我们的程序非常简单,其蓝图如下:以 BTextView 为中心的相对简单 GUI 来构建一个简单,但是有用的文档处理器。
在我们轻率的写出代码之前,我们来概览一下 BTextView 类的定义,查看一下我们可以利用哪些功能,而不需要花费很多时间。这之后,会为我们省些精力和时间。需要注意的是,这些并不是这个类完整的定义,我们省却了与 QuickEdit 不相关的功能。
class BTextView : public BView
{
public:
void SetText(const char* inText, const text_run_array* inRuns = NULL);
void SetText(const char* inText, int32 inLength, const text_run_array* inRuns = NULL);
void SetText(BFile* inFile, int32 startOffset, int32 inLength, const text_run_array*
inRuns = NULL);
void Insert(const char* inText, const text_run_array* inRuns = NULL);
void Insert(const char* inText, int32 inLength, const text_run_array* inRuns = NULL);
void Insert(int32 startOffset, const char* inText, int32 inLength,
const text_run_array* inRuns = NULL);
void Delete();
void Delete(int32 startOffset, int32 endOffset);
const char* Test() const;
int32 TestLength() const;
void GetText(int32 offset, int32 length, char* buffer) const;
uint8 ByteAt(int32 offset) const;
int32 CountLines() const;
int32 CurrentLine() const;
void GoToLine(int32 lineIndex);
void Cut(BClipboard* clipboard);
void Copy(BClipboard* clipboard);
void Paste(BClipboard* clipboard);
void Clear();
bool AcceptsPaste(BClipboard* clipboard);
bool AcceptsDrop(const BMessage* inMessage);
void Select(int32 startOffset, int32 endOffset);
void SelectAll();
void GetSelection(int32* outStart, int32* outEnd) const;
void SetFontAndColor(const BFont* inFont, uint32 inMode = B_FONT_ALL,
const rgb_color* inColor = NULL);
void SetFontAndColor(int32 startOffset, int32 endOffset,
const BFont* inFont, uint32 inMode = B_FONT_ALL,
const rgb_color* inColor = NULL);
void GetFontAndColor(int32 inOffset, BFont* outFont,
rgb_color* outColor = NULL) const;
void FindWord(int32 inOffset, int32* outFromOffset,
int32* outToOffset);
float LineWidth(int32 lineIndex = 0) cosnt;
float LineHeight(int32 lineIndex = 0) const;
float TextHeight(int32 startLine, int32 endLine) const;
void ScrollToOffset(int32 inOffset);
void ScrollToSelection();
void Highlight(int32, startOffset, int32 endOffset);
void SetTextRect(BRect rect);
BRect TextRect() const;
void SetInsets(float left, float top, float right, float bottom);
void GetInsets(float* _left, float* _top,
float* _right, float* _bottom) const;
void SetStylable(bool stylable);
bool IsStylable() const;
void SetTabWidth(float width);
float TabWidth() const;
void SetWordWrap(bool wrap);
bool DoesWordWrap() const;
void SetMaxBytes(int32 max);
int32 MaxBytes() const;
void DisallowChar(uint32 aChar);
void AllowChar(uint32 aChar);
void SetAlignment(alignment flag);
alignment Alignment() const;
void SetAutoindent(bool state);
bool DoesAutoindent() const;
void SetColorSpace(color_space colors);
color_space ColorSpace() const;
void MakeResizable(bool resize, BView* resizeView = NULL);
bool IsResizable() const;
void SetDoesUndo(bool undo);
bool DoesUndo() const;
void HideTyping(bool enabled);
bool IsTypingHidden() const;
void Undo(Clipboard* clipboard);
undo_state UndoState(bool* isRedo) const;
}
看看这些方法,它们为我们提供了非常有趣的功能。我们可以提供撤销和在窗口锁定文字等选项。制表键所占据的空间宽度也可以通过小控件进行修改,我们也可以使用文本矩形框特性来作为页面边角设置等功能。字体颜色和样式也可以进行设置,并且我们还有一些其他的字体效果并未列出,如下划线,粗体和斜体样式,轮廓等等。也可以执行剪贴板操作。还有一些基本的对齐操作支持,尽管在常规的字处理程序中,它并不常用。
尽管有这么多的功能,我们还是从简入手。如果我们尽量遵循基本字处理程序常用的工作流程,那么从这里开始和构建将会比事先确定所有内容要更为容易。
在编写字处理程序最初阶段,我们的目标将是我们早已熟知的内容:一个带有文本编辑器的窗口,可用于保存和载入文本。在 Paladin 中利用 Main Window with Menu 模板新建一个项目。在 App.cpp 中修改程序署名为 “application/x-vnd.dw-QuickEdit”,使用您自己的初始化内容取代原作者的程序,然后保存您的修改。您需要进入 Project 菜单中的 Change System Libraries 菜单,然后添加 libtracker.so 和 libtranslation.so 库。App.h 和 App.cpp 如下所示:
App.h¶
#ifndef APP_H
#define APP_H
#include <Application.h>
class App : public BApplication
{
public:
App(void);
};
#endif
App.cpp¶
#include "App.h"
#include "MainWindow.h"
App::App(void)
: BApplication("application/x-vnd.dw-QuickEdit")
{
MainWindow* mainwin = new MainWindow();
mianwin->Show();
}
int
main(void)
{
App *app = new App();
app->Run();
delete app;
return 0;
}
以上这些代码目前都没什么特别的意义。我们的重点在于 MainWindow.h 和 MainWindow.cpp。
MainWindow.h¶
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <Window.h>
#include <Entry.h>
#include <FilePanel.h>
#include <MenuBar.h>
#include <String.h>
#include <TextView.h>
class MainWindow : public BWindow
{
public:
MainWindow(void);
~MainWindow(void);
void MessageReceived(BMessage* msg);
bool QuitRequested(void);
void OpenFile(const entry_ref &ref);
void SaveFile(const char* path);
void FrameResized(float w, float h);
private:
void UpdateTextRect(void);
BMenuBar* fMenuBar;
BTextView* fTextView;
BFilePanel* fOpenPanel;
BFilePanel* fSavePanel;
BString fFilePath;
};
#endif
在上述头文件中,我们新碰到的是用于在载入和保存文件的 BFilePanel 类,以及提供了所有我们需要的文本编辑服务的 BTextView 类。
MainWindow.cpp¶
#include "MainWindow.h"
#include <Alert.h>
#include <Application.h>
#include <Directory.h>
#include <File.h>
#include <Menu.h>
#include <MenuItem.h>
#include <Messenger.h>
#include <NodeInfo.h>
#include <Path.h>
#include <ScrollView.h>
#include <String.h>
#include <TranslationUtils.h>
enum
{
M_FILE_VIEW = 'flnw',
M_SHOW_OPEN = 'shop',
M_SAVE = 'save',
M_SAVE_AS = 'svas',
M_PRINT_SETUP = 'ptcf',
M_PRINT = 'prin'
};
MainWindow::MainWindow(void)
: BWindow(BRect(100,100,500,400), "QuickEdit", B_TITLED_WINDOW,
B_ASYNCHRONOUS_CONTROLS)
{
// 我们首先创建一个菜单栏,并且使用我们将要实现的命令来填充该菜单。
BRect(Bounds());
r.bottom = 20;
fMenuBar = new BMenuBar(r, "menubar");
AddChild(fMenuBar);
BMenu* menu = new BMenu("File");
fMenuBar->AddItem(menu);
// 下面是添加菜单项的快捷方式。需要注意的是
// 快捷键是 Haiku 标准:
// Alt + N => New file
// Alt + O => Open file
// Alt + S => Save file
// Alt + Shift + S => Save As
// 快捷键通常和Alt按键成对执行只是习惯如此,
// 也可以如 Windows 和 Linux 一样使用 Ctrl
// 按键作为命令键。
menu->AddItem(new BMenuItem("New", new BMessage(M_FILE_NEW), 'N'));
menu->AddItem(new BMenuItem("Open", new BMessage(M_SHOW_OPEN), 'O'));
menu->AddSeparatorItem();
menu->AddItem(new BMenuItem("Save", new BMessage(M_SAVE), 'S'));
// 下面的函数,跟上述其他不同,其指明了使用多于Alt加上单个字
// 符的快捷键。
menu->AddItem(new BMenuItem("Save As", B_UTF8_ELLIPSIS,
new BMessage(M_SAVE_AS), 'S',
B_COMMAND_KEY | B_SHIFT_KEY));
// 接下来,我们将添加文本视图和滚动栏。为了方便,我们将使用
// BScrollView。在使用BScrollView时,首先需要创建其目标对象,
// 然后创建BScrollView,最后调用仅适用于BScrollView的AddChild
// 方法,它将会完成添加其目标对象的操作。
r = Bounds();
r.top = fMenuBar->Frame().bottom + 1;
// 在计算目标视图的区域大小时,您必须为BScrollView提供的滚动栏
// 预先留足额外的宽和高。Haiku为我们提供了用于此的B_V_SCROLL_BAR_WIDTH
// B_H_SCROLL_BAR_WIDTH常量。
r.right -= B_V_SCROLL_BAR_WIDTH;
// 我们将要用到的BTextView构造函数不仅需要一个框架大小,还需要
// 用于文本现实的矩形区域。可以对其或多或少的加以设置。
BRect textRect = r;
textRect.OffsetTo(0,0);
textRect.InsetBy(5,5);
fTextView = new BTextView(r, "textview", textRect, B_FOLLOW_ALL);
// 没有下面的调用,我们的BTextView仅仅是一个纯文本编辑器
fTextView->SetStylable(true);
BScrollView* scrollView = new BScrollView("scrollview", fTextView,
B_FOLLOW_ALL, 0, false, true);
AddChild(scrollView);
// 下面我们将使用一个新类:BFilePanel。更多使用稍后再述。
BMessenger msgr(NULL, this);
fOpenPanel = new BFilePanel(B_OPEN_PANEL, &msgr, NULL, 0, false);
fSavePanel = new BFilePanel(B_SAVE_PANEL, &msgr, NULL, 0, false);
// 下面的代码能够让用户在程序运行时马上可以进行输入。如果
// 没有该调用,用户必须手动点击窗口以便输入。这相当的犯人!
fTextView->MakeFocus(true);
}
MainWindow::~MainWindow(void)
{
delete fOpenPanel;
delete fSavePanel;
}
void
MainWindow::MessageReceived(BMessage* msg)
{
switch (msg->what)
{
case M_FILE_NEW:
{
// 清空BTextView中的所有文本。并且将文件路径
// 置为空以表明文件未被保存到磁盘。
fTextView->SetText("");
fFilePath = "";
break;
}
// 下述是与文件的打开和保存相关的case语句
case M_SHOW_OPEN:
{
fOpenPanel->Show();
break;
}
case B_REFS_RECEIVED:
{
entry_ref ref;
if (msg->FindRef("ref", &ref) != B_OK)
break;
OpenFile(ref);
break;
}
case M_SAVE:
{
if (fFilePath.CountChars() < 1)
fSavePanel->Show();
else
SaveFile(fFilePath.String());
break;
}
case M_SAVE_AS:
{
fSavePanel->Show();
break;
}
case B_SAVE_REQUESTED:
{
entry_ref dir;
BString name;
if (msg->FindRef("directory", &dir) == B_OK &&
msg->FindString("name", &name) == B_OK)
{
BPath path(&dir);
path.Append(name);
SaveFile(path.Path());
}
break;
}
default:
{
BWindow::MessageReceived(msg);
break;
}
}
}
bool
MainWindow::QuitRequested(void)
{
be_app->PostMessage(B_QUITE_REQUESTED);
return true;
}
void
MainWindow::OpenFile(const entry_ref& ref)
{
// 将符号链接转换为其目标对象
BEntry entry(&ref, true);
entry_ref realRef;
entry.GetRef(&realRef);
// Translation套件提供了文本文件的翻译服务。
BFile file(&realRef, B_READ_ONLY);
if (file.InitCheck() != B_OK)
return;
// 一个从文件中读取格式化文本的简单函数。Nice!
if (BTranslationUtils::GetStyledText(&file, fTextView) == B_OK)
{
// BPath 是Storage套件中使用数据类和常规字符串
// 路径之间转换的桥梁。下面我们设置BPath实例为
// 打开文件的绝对路径,并且将窗口的标题设置为文件名。
BPath path(&realRef);
fFilePath = path.Path();
SetTitle(path.Leaf());
}
}
void
MainWindow::SaveFile(const char* path)
{
// 该函数接收一个字符串路径,然后保存BTextView中的数据到
// 路径所指的文件,如果文件不存在则创建该文件,否则则覆盖
// 已存在的文件。
BFile file;
if (file.SetTo(path, B_READ_WRITE | B_CREATE_FILE | B_ERASE_FILE)
!= B_OK)
return;
if (BTranslationUtils::PutStyledText(fTextView, &file) == B_OK)
{
fFilePath = path;
BNodeInfo nodeInfo(&file);
nodeInfo.SetType("text/plain");
}
}
void
MainWindow::FrameResized(float w, float h)
{
// 下面是一些需要铭记的箴言:在BTextView缩放时,
// 它并不会更新他的文本框区域。在窗口缩放时,
// 我们的TextView将也会进行缩放。FrameResized()
// 将确保文本框的更新。
UpdateTextRect();
}
void
MainWindow::UpdateTextRect(void)
{
BRect r(fRectView->Bounds());
r.InsetBy(5,5);
fTextView->SetTextRect(r);
}
在上面这段代码中,我们有两点需要重申。第一点是 GetStyledText() 和 PutStyledText() 调用。Translation 套件已经为我们提供了保存格式化文本的工具!我们编程技巧的第二点就是工具包,也就是对 BFilePanel 的使用。
BFilePanel 是一个高度定制的文件和目录选择类。尽管它属于 Storage 套件的一部分,它也被认为是 Interface 套件的一部分。在构造之后,您只需要调用它的 Show() 方法以便让用户选择文件即可。我们在 MainWindow 构造函数中还看到了一下三行代码:
BMessenger msgr(NULL, this);
fOpenPanel = new BFilePanel(B_OPEN_PANEL, &msgr, NULL, 0, false);
fSavePanel = new BFilePanel(B_SAVE_PANEL, &msgr, NULL, 0, false);
它们分别用于创建打开和保存面板,但是这些调用中的参数并未告诉我们它们究竟如何运行。下面是 BFilePanel 构造函数的声明:
BFilePanel(file_panel_mode panelMode = B_OPEN_PANEL,
BMessenger* target = NULL, entry_ref* panelDirectory = NULL,
uint32 nodeTypes = 0, bool allowMultiple = true,
BMessage* msg = NULL, BRefFilter* refFilter = NULL,
bool modal = false, bool hide_when_done = true);
该构造函数为提供了很多参数,并且为每个参数都提供了默认值。panelMode 可以设置为 B_OPEN_PANEL 或者 B_SAVE_PANEL。如果设置,它也可以进行修改。由于这两个模式在行为上具有微小但重要的不同,因此我们在该实例中需要两种不同模式的面板。target 是面板发送的保存或打开消息的接收方。默认的对象是 be_app_messenger,也就是指向每个程序所使用的 BApplication 实例的 BMessenger 对象。我们已经修改了该面板,使其指向我们的窗口。panelDirectory 可以是文件系统中的任何目录,但是其默认为用户的 home 文件夹。allowMultiple 决定了是否可以选择多个文件入口。msg 是做出选择时面板发送的消息。我们过会儿再分析 msg 的默认情况。refFilter 是一个 BRefFilter 对象,其可以用于仅显示指定文件类型的文件。modal 可以使面板的窗口需要一次按钮的点击;只在很少的情况下才需要。除非真的有合理的理由,否则不要启用面板的 modal 参数。最后,如果 hide_when_done 设置为 false,那么面板将会保持打开,即使用户已经做了选择,该选项也极少用到。
我们需要更近一步的介绍一下 nodeTypes 参数。其值由一个或之多三个标记设置,B_FILE_NODE,B_DIRECTORY_NODE,和 B_SYMLINK_NODE。默认情况下,B_FILE_NODE,允许选中文件和任何指向文件的符号链接。同时使用 B_FILE_NODE 和 B_DIRECTORY_NODE 则允许用户选择目录和指向目录的符号链接。在这两种情况下,在面板中双击目录,面板将会进入该目录,但并不会进行选中。B_SYMLINK 总是单独使用,并且很少使用:如果单独使用,它仅允许选中符号链接。
在用户做出选择时,所发送的消息取决于所作的选择和面板的模式。如果未作定义,打开面板将会发送 B_REFS_RECEIVED 消息,而保存面板将会发送 B_SAVE_REQUESTED 消息。自定义消息具有和构造函数或者 SetMessage() 调用所设置的相同的 what 字段。它也可以在设置时添加附加数据,即拷贝您原本的消息,添加标准打开和保存字段,然后发送消息到指定目标。
打开通知将发送消息 refs 字段中保存的一系列 entry_ref 对象发送到目标对象。需要注意的是,它们是用户选中的内容。尽管您必须解析所有的符号链接,但这些都微不足道:利用 entry_ref 创建 BEnry,并将第二个参数设为 true。如果您需要新建一个 entry_ref,调用 BEntry 的 GetRef() 方法创建即可。您可以参照上述的 MainWindow::OpenFile()中的代码作为示例。
保存通知包含两个额外的字段:命名为 directory 的 entry_ref 对象和命名为 name 的字符串。您可以使用这两个字段和 BPath 实例构造文件的完整路径。我们在 MessageReceived() 函数中使用了这种方法。当然,您也可以传递 BDirectory 实例的引用,然后调用 CreateFile()。需要注意的是,如果文件可能已经存在,用户需要确认覆盖文件。在这种情况下,您只需要将文件删除。我们可以在 SaveFile() 中使用 B_CREATE_FILE 和 B_ERASE_FILE 标志进行处理,这样可以确保,如果文件不存在则进行创建,反之则将其覆盖。