BeOS 中的输入法处理


  BeOS 中使用的文字编码基本上都是 UTF-8,要使用除英语之外的其它国家语言只需要一个能显示其文字的字库和一个能输入其文字(采用UTF-8编码)的输入法。而 BeOS 中的输入法最早应该是来源于其对日文的支持,其实现方法为通过 Input Server 载入 Add-on 然后利用本身的 Input Filter 对所有键盘和鼠标消息进行适当的转换。因为本身这些东西也许是 ERGOSOFT 公司的杰作而外人甚少知晓,所以自 BeOS R4 以来极少有人为她写输入法,就我所知除开我写的之外只有 HanBe、Canna、ChineseTool 以及最近的 Anthy for Zeta。

  也许你根本不关心输入法开发,但也有可能需要在自己写的 Interface 类中处理国际化文字输入,这种情况通常会在诸如文字编排、网页处理等程序中碰到。虽然 BTextView 已经可以替你解决大部分的问题,但说句老实话,BTextView 是一个极其庞大的类,更多时候你需要一个更灵活的类。

  下面我们分两部分讲解 Input Server 的应用,第一部分主要针对如何与 Input Method 打交道,第二部分介绍如何编写 Input Method。

第一部分: 从 BView 派生可接收输入法事件的类


  以下结合以前写的一个类进行表述,其头文件如下:
#ifndef __SINPUT_AWARE_STRING_VIEW_H__
#define __SINPUT_AWARE_STRING_VIEW_H__

#include <SupportDefs.h>
#include <Messenger.h>
#include <Input.h>

#include <besavager/StringView.h>


class SInputAwareStringView : public SStringView {
public:
        SInputAwareStringView(BRect frame, const char *name,
                              uint32 resizeMask = B_FOLLOW_LEFT | B_FOLLOW_TOP,
                              uint32 flags = B_WILL_DRAW | B_FRAME_EVENTS);

        virtual void MessageReceived(BMessage *message);

private:
        BMessenger msgr_input;
        bool im_started;

        void IMStarted(BMessage *message);
        void IMStopped();
        void IMChanged(BMessage *message);
        void IMLocationRequest(BMessage *message);
};

#endif /* __SINPUT_AWARE_STRING_VIEW_H__ */

1.1 B_INPUT_METHOD_AWARE 标志

  如果给 BView 类的 Flags 贯上 B_INPUT_METHOD_AWARE 则表明 B_INPUT_METHOD_EVENT 事件由该类自己处理,否则 Input Server 会自己弹出一个窗口来处理该事件(这种方法在 Dano/Zeta 等新版本中会发生怪异的事情)。
  那么派生的类构造函数类似如下:
SInputAwareStringView::SInputAwareStringView(BRect frame, const char *name, uint32 resizeMask, uint32 flags)
        : SStringView(frame, name, NULL, NULL, 0, NULL, resizeMask, flags), im_started(false)
{
        // 关键在这一行
        if(!(flags & B_INPUT_METHOD_AWARE)) SetFlags(flags | B_INPUT_METHOD_AWARE);
}


1.2 B_KEY_DOWN 事件

  B_KEY_DOWN 事件是处理键盘输入的最重要事件,在未给 BView 类的 Flags 贯上 B_INPUT_METHOD_AWARE 时,Input Server 处理完 B_INPUT_METHOD_EVENT 后也是通过该事件通知视图的。
  其具体内容如下:

类型描述
"when"B_INT64_TYPE事件发生时间,自1970年1月1日0点0分0秒计起的微秒数
"key"B_INT32_TYPE按键物理映射编码,如何获得对应按键详下
"be:key_repeat"B_INT32_TYPE按键重复次数
"modifiers"B_INT32_TYPE按键按下时修饰按键状态,如 CTRL、SHIFT 等
"states"B_UINT8_TYPE按键按下时所有按键状态描述
"byte"[3]B_INT8_TYPE按键产生的 UTF-8 字符,为 3 个字符
"bytes"B_STRING_TYPE按键产生的 UTF-8 字符串,上面所述正通过此渠道传递
"raw_char"B_INT32_TYPE与修饰按键无关的单个 ASCII 码

  一个完善的派生类应先关注 "bytes" 域的内容,当其不存在时再去查询 "byte"[3],最后才是 "raw_char"。
  "key" 和 "modifiers" 域通常是用于查询特殊按键,如下面这段代码:
// 返回新分配的字符串,不再使用时用 free() 释放
char* get_key(int32 modifiers, int32 key_code)
{
        key_map *keys = NULL;
        char *chars = NULL;
        int32 offset = -1;
        char *retStr = NULL;

        if(key_code < 0 || key_code >= 128) return NULL;

        get_key_map(&keys, &chars);
        if(keys == NULL || chars == NULL)
        {
                if(keys) free(keys);
                if(chars) free(chars);
                return NULL;
        }

        if((modifiers & B_SHIFT_KEY) && (modifiers & B_CAPS_LOCK))
                offset = keys->caps_shift_map[key_code];
        else if(modifiers & B_SHIFT_KEY)
                offset = keys->shift_map[key_code];
        else if(modifiers & B_CAPS_LOCK)
                offset = keys->caps_map[key_code];
        else
                offset = keys->normal_map[key_code];

        if(!(offset < 0 || chars[offset] < 1))
        {
                if((retStr = (char*)malloc((size_t)chars[offset] + 1)) != NULL)
                {
                        memcpy(retStr, &(chars[offset + 1]), (size_t)chars[offset]);
                        *(retStr + chars[offset]) = 0;
                }
        }

        free(keys);
        free(chars);

        return retStr;
}


1.3 B_INPUT_METHOD_EVENT 事件

  所有从 Input Server 发来的输入法事件均为此事件,当 BView 类的 Flags 没有 B_INPUT_METHOD_AWARE 时该事件会被忽略。
  其消息域中 "be:opcode" (类型为 B_INT32_TYPE) 描述事件类型如下:

描述
B_INPUT_METHOD_STARTED输入法处理将要开始进行
B_INPUT_METHOD_STOPPED输入法处理结束
B_INPUT_METHOD_CHANGED输入法状态变更通知
B_INPUT_METHOD_LOCATION_REQUEST输入法光标位置查询


  另外,除 B_INPUT_METHOD_STOPPED 外,其余三个类型的消息均有特殊的消息域。
  B_INPUT_METHOD_STARTED 类型的事件内容如下:

类型描述
"be:reply_to"B_MESSENGER_TYPE可向 Input Server 相应的 Add-On 发送消息的信使

  这里 "be:reply_to" 所指向的信使主要用于响应光标查询,也可以向它发送 B_INPUT_METHOD_STOPPED 等事件中止输入。

  B_INPUT_METHOD_CHANGED 类型的事件内容如下:

类型描述
"be:string"B_STRING_TYPE输入法正在显示的字符串。在 BTextView 中将此字符串在当前光标处显示并置其背景为浅蓝色。
"be:selection"B_INT32_TYPE不定长的配对选择区偏移量(不一定存在),表示正在进行选择的字符。在 BTextView 中将配对中的字符串置其背景为浅红色。
"be:clause_start"B_INT32_TYPE各个词语的开始偏移(不一定存在),用于分隔词语或段落,汉语中此域可忽略。
"be:clause_end"B_INT32_TYPE各个词语的结束偏移(不一定存在),用于分隔词语或段落,汉语中此域可忽略。
"be:confirmed"B_BOOL_TYPE是否提交字符串,如果该值为 true 则表示需要将字符串插入到当前光标处。

  当 B_INPUT_METHOD_EVENT 为 B_INPUT_METHOD_LOCATION_REQUEST 时派生的类应向上述的 "be:reply_to" 回复内容如下的消息:

类型描述
"be:opcode"B_INT32_TYPE应将其置为 B_INPUT_METHOD_LOCATION_REQUEST。
"be:location_reply"B_POINT_TYPE每个 UTF-8 字符(未必为 1 个字节)的起始定位,坐标系为整个屏幕
"be:height_reply"B_FLOAT_TYPE每个 UTF-8 字符(未必为 1 个字节)的高度




1.4 处理示例

  好了,准备好了没有,一大堆代码来了,:-)
void
SInputAwareStringView::MessageReceived(BMessage *message)
{
        switch(message->what)
        {
                case B_INPUT_METHOD_EVENT: // 接收到输入法事件
                        {
                                int32 op_code; // 事件类型
                                if(message->FindInt32("be:opcode", &op_code) != B_OK) break;

                                switch(op_code)
                                {
                                        case B_INPUT_METHOD_STARTED: // 进行准备工作
                                                IMStarted(message);
                                                break;

                                        case B_INPUT_METHOD_STOPPED: // 结束输入
                                                IMStopped();
                                                break;

                                        case B_INPUT_METHOD_CHANGED: // 处理字符显示
                                                IMChanged(message);
                                                break;

                                        case B_INPUT_METHOD_LOCATION_REQUEST: // 处理光标位置查询
                                                IMLocationRequest(message);
                                                break;

                                        default: // 调用基类成员函数
                                                SStringView::MessageReceived(message);
                                }
                        }
                        break;

                default: // 调用基类成员函数
                        SStringView::MessageReceived(message);
        }
}


void
SInputAwareStringView::IMStarted(BMessage *message)
{
        // 存储我们要与之打交道的 BMessenger
        im_started = (message->FindMessenger("be:reply_to", &msgr_input) == B_OK);
}


void
SInputAwareStringView::IMStopped()
{
        msgr_input = BMessenger();
        SetText(NULL); // 清除文字
        im_started = false;
}


void
SInputAwareStringView::IMChanged(BMessage *message)
{
        if(!im_started) return;

        const char *im_string = NULL;
        message->FindString("be:string", &im_string);
        if(im_string == NULL) im_string = "";

        int32 start = 0, end = 0;

        BList color_list;

        for(int32 i = 0;
                message->FindInt32("be:clause_start", i, &start) == B_OK &&
                message->FindInt32("be:clause_end", i, &end) == B_OK; i++)
        {
                // 处理词语分隔
                if(end > start) // 合法
                {
                        // 将其置为蓝色背景, 偏移量按字节数计。
                        // 比如([]表示其起止):
                        // 字符串为: T h i s i s a [w o r d] .
                        // 字符偏移: 0 1 2 3 4 5 6 7  8 9 10 11
                        // "be:clause_start" = 7
                        // "be:clause_end" = 11
                        s_string_view_color *color = new s_string_view_color;
                        s_rgb_color_setto(&color->color, 0, 0, 0);
                        s_rgb_color_setto(&color->background, 152, 203, 255);
                        color->draw_background = true;
                        color->start_offset = start;
                        color->end_offset = end - 1;
                        color_list.AddItem((void*)color);
                }
        }

        for(int32 i = 0;
                message->FindInt32("be:selection", i * 2, &start) == B_OK &&
                message->FindInt32("be:selection", i * 2 + 1, &end) == B_OK; i++)
        {
                // 处理选择显示
                if(end > start) // 合法
                {
                        // 将其置为红色背景, 偏移量按字节数计。
                        // 比如([]表示其起止):
                        // 字符串为: T h i s i s a [w o r d] .
                        // 字符偏移: 0 1 2 3 4 5 6 7  8 9 10 11
                        // "be:selection"[0] = 7
                        // "be:selection"[1] = 11
                        s_string_view_color *color = new s_string_view_color;
                        s_rgb_color_setto(&color->color, 0, 0, 0);
                        s_rgb_color_setto(&color->background, 255, 152, 152);
                        color->draw_background = true;
                        color->start_offset = start;
                        color->end_offset = end - 1;
                        color_list.AddItem((void*)color);
                }
        }

        if(!color_list.IsEmpty()) // 不必太在乎下面的东西 :-)
        {
                int32 n = color_list.CountItems();
                s_string_view_color *colors = new s_string_view_color[n];

                for(int32 i = 0; i < n; i++)
                {
                        s_string_view_color *color = (s_string_view_color*)color_list.ItemAt(i);
                        if(color)
                        {
                                if(colors)
                                {
                                        s_rgb_color_setto(&colors[i].color, color->color);
                                        s_rgb_color_setto(&colors[i].background, color->background);
                                        colors[i].draw_background = color->draw_background;
                                        colors[i].start_offset = color->start_offset;
                                        colors[i].end_offset = color->end_offset;
                                }

                                delete color;
                        }
                }

                color_list.MakeEmpty();

                SetText(im_string, colors, n);
                if(colors) delete[] colors;
        }
        else
        {
                SetText(im_string);
        }
}


void
SInputAwareStringView::IMLocationRequest(BMessage *message)
{
        if(!im_started) return;

        const char *im_string = Text();
        if(im_string == NULL || *im_string == 0) return;

        BMessage reply(B_INPUT_METHOD_EVENT); // 要回复的消息
        reply.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST); // 必须

        BPoint left_top = ConvertToScreen(BPoint(0, 0)); // 先取得当前视图坐标 (0, 0) 所对应屏幕的坐标

        int32 offset = 0;
        uint32 index = 0;

        while(true)
        {
                if(__utf8_char_at(im_string, index, &offset)) // 第 index + 1 个 UTF-8 字符
                {
                        BRect rect = TextRegion(offset, offset + 1).Frame(); // 取得字符显示区域
                        if(!rect.IsValid()) break;

                        // 变换为屏幕坐标
                        BPoint pt = left_top;
                        pt += rect.LeftTop();

                        reply.AddPoint("be:location_reply", pt); // 位置回复
                        reply.AddFloat("be:height_reply", rect.Height()); // 高度回复
                }
                else
                {
                        break;
                }

                index++;
        }

        // 发送给 Input Server
        msgr_input.SendMessage(&reply);
}


第二部分: 如何写输入法附件


  再接再励,我们再来看如何写输入法,不要以为有多难,事实上和你写响应输入法的程序一样简单,这也就是我一直以来喜欢用 BeOS API 编程的原因甚至于因在其它平台用不了而去写个代替品。
  警告:你写的输入法附件由于由 Input Server 加载而运行,任何因出错而引起的不稳定都将可能造成系统无法响应键盘和鼠标的输入(假当机),所以建议你充分调试(特别是多线程方面)后方可适用于他人;并强烈要求你不要把输入法放到 /boot/beos/system/add-ons/input_server/methods/,因为此举可能会导致系统无法使用!

2.1 必要工作及编译程序
  首先,你需要在你的源码中附上以下代码以便 Input Server 能从你编译的动态库中取得输入法附件:
// 使用 extern "C" 是避免动态库因 C++ 的关系而添加过多的符号前缀
// _EXPORT 是在 PowerPC 平台的 Metrowerks C++ 而用
extern "C" _EXPORT {

BInputServerMethod* instantiate_input_method()
{
        // 创建输入法附件并传回给 Input Server
        return(new MyInputAddOn());
}

} // extern "C"

  其次,将 /boot/beos/system/servers/input_server 链接到你的源码目录下为 _APP_ 并作为库文件加到 BeIDE 项目中去,在 x86 平台也可用如下命令去编译你的输入法并安装(复制到/boot/home/config/add-ons/input_server/methods/):
# 把 /boot/beos/system/servers/input_server 链接过来
$ ln -sf /boot/beos/system/servers/input_server _APP_

# 编译动态库并移动到 /boot/home/config/add-ons/input_server/methods/
$ g++ -O3 -nostart MyInputAddOn.cpp -Xlinker -soname="MyInputAddon" -o MyInputAddOn -lbe -lroot _APP_
$ mv MyInputAddOn /boot/home/config/add-ons/input_server/methods/MyInputAddOn

# 下面为重启 Input Server 来加载输入法的方法:
# 1. 最保险的做法是重新启动机器,启动如果有问题就按住空格键选 Select safe mode options => Disable user add-ons。
# 2. 其次是按住 CTRL+ALT+DEL 然后杀掉 input_server,它会再自动恢复。
# 3. 就是运行下面的指令,不过有时会当机。

$ /boot/beos/system/servers/input_server -q


2.2 BInputServerMethod 类简介
  你所写的输入法都应该派生自 BInputServerMethod,一般来说你还要创建一个 BLooper 用于接收应用程序传回的响应消息,如光标位置等。
  其头文件在 <add-ons/input_server/InputServerMethod.h> 中。

  构造函数:
BInputServerMethod() // 可以在此中创建线程、初始设置等以便在 InitCheck() 成员函数中进行检查
~BInputServerMethod() // 解构函数,可在此中释放资源及等待线程结束等

  成员函数:
virtual status_t MethodActivated(bool active)
MethodActivated()是当用户按下 ALT+SPACE 或从 DeskBar 选择输入法图标来切换到你的输入法时 Input Server 调用的。如果输入法准备就绪,那么返回 B_OK,返回其它值,如 B_ERROR 等都将使 Input Server 跳过此输入法。
status_t SetName(const char *name)
SetName()设置在用户点击 DeskBar 的输入法图标点击后弹出的菜单中要显示的你的输入法名称。
status_t SetIcon(const uchar *icon)
SetIcon()设置在输入法在激活状态时 DeskBar 的输入法图标。
icon为一个 16x16 大小的 8 位索引色彩空间的图像数据。
status_t SetMenu(const BMenu *menu, const BMessenger target)
SetMenu()设置在用户点击 DeskBar 的输入法图标点击后弹出的菜单并移动鼠标到你的输入法名称那一项时所要弹出的菜单,主要用于一些选项的设置等。
当指定 menu 为 NULL 时表示其时不需弹出菜单;target为用户点击其菜单项后要响应消息的信使。
// 这里的 message 不许使用 delete 进行释放,由 Input Server 自动处理。
status_t EnqueueMessage(BMessage *message)
EnqueueMessage()是输入法附件中最重要的两个成员函数之一,它用于向 Input Server 追加消息。
追加的消息将通过处于激活状态的 Input Method 和其它的 Input Filter 发至 App Server;
// 继承于 BInputServerFilter 的成员函数
virtual status_t InitCheck();
status_t GetScreenRegion(BRegion *region) const;
virtual filter_result Filter(BMessage *message, BList *outList);
InitCheck()用于载入时检查输入法是否正常,返回 B_OK 表示正常,否则 Input Server 将忽略这个输入法。
GetScreenRegion()用于取得整个屏幕的范围,当返回的 region 无效时表示正处于屏幕保护状态。

Filter()是输入法附件中最重要两个成员函数的另外一个,用于过滤消息。
当Filter()返回 B_SKIP_MESSAGE 表示跳过该消息,返回 B_DISPATCH_MESSAGE 表示发送该消息。
如果你在 Filter() 使用 outList 并返回 B_DISPATCH_MESSAGE 时,Input Server 将忽略掉 message 而采用 outList 里面的消息。
使用 outList 如下:
// 这里新创建的 BMessage 不许使用 delete 进行释放,由 Input Server 自动处理。
BMessage *new_message = new BMessage(message);
outList->AddItem(new_message);


2.3 派生自 BInputServerMethod 的类大致介绍
  我们这里仅仅简单的利用少数的代码进行介绍。
  那么,继续吧。
class MyInputAddOnLooper : public BLooper {
        // 我们这里没有给出 class MyInputAddOnLooper 的实现
        // 请根据具体情况进行编写
        ...
};


class MyInputAddOn : public BInputServerMethod {
public:
        MyInputAddOn();
        virtual ~MyInputAddOn();

        virtual status_t InitCheck();
        virtual status_t MethodActivated(bool active);
        virtual filter_result Filter(BMessage *message, BList *outList);

private:
        friend class MyInputAddOnLooper;

        status_t fStatus;
        BMessenger fMsgrLooper;
};


MyInputAddOn::MyInputAddOn()
        : BInputServerMethod("My Input Method", NULL), fStatus(B_NO_INIT)
{
        // 创建一个 looper 用于响应事件
        // 在其中你可以进行必要的初始化
        MyInputAddOnLooper *myLooper = new MyInputAddOnLooper();

        // 检查是否妥当
        if(myLooper->CheckMyMethod() != B_OK)
        {
                myLooper->Lock();
                myLooper->Quit();
                return;
        }

        // 运行 looper
        myLooper->Run();

        // 设置信使,主要为多线程消息传递安全考虑
        fMsgrLooper = BMessenger(myLooper);
        if(fMsgrLooper.IsValid() == false)
        {
                if(myLooper->IsRunning())
                {
                        myLooper->PostMessage(B_QUIT_REQUESTED);
                }
                else
                {
                        myLooper->Lock();
                        myLooper->Quit();
                }
                return;
        }

        // 所有都没问题了...
        fStatus = B_OK;
}


MyInputAddOn::~MyInputAddOn()
{
        // 等待 looper 线程退出,你也可以使用 wait_for_thread 等
        while(fMsgrLooper.SendMessage(B_QUIT_REQUESTED) == B_OK) snooze(500000);
}


status_t
MyInputAddOn::InitCheck()
{
        return fStatus;
}


// MethodActivated:
// 这里较第一部分中处理 B_INPUT_METHOD_EVENT 不同的是
// 该事件由你的输入法附件向应用程序发出。
status_t
MyInputAddOn::MethodActivated(bool active)
{
        BMessage *msg;

        if(fMsgrLooper.IsValid() == false) return B_ERROR;

        if(active == false)
        {
                msg = new BMessage(B_INPUT_METHOD_EVENT);
                msg->AddInt32("be:opcode", B_INPUT_METHOD_STOPPED);
                EnqueueMessage(msg);

                // 告诉 looper: 用户要停止输入了
                fMsgrLooper.SendMessage(MY_MSG_METHOD_STOPPED);
                return B_OK;
        }

        msg = new BMessage(B_INPUT_METHOD_EVENT);
        msg->AddInt32("be:opcode", B_INPUT_METHOD_STARTED);
        msg->AddMessenger("be:reply_to", fMsgrLooper);
        EnqueueMessage(msg);

        // 告诉 looper: 用户要开始输入了
        fMsgrLooper.SendMessage(MY_MSG_METHOD_STARTED);

        return B_OK;
}


filter_result
MyInputAddOn::Filter(BMessage *message, BList *outList)
{
        // 如果不是键盘事件则继续处理事件,
        // 这里仅仅示例,你也可选择 B_MOUSE_DOWN 或其它的以支持手写等。
        // 更灵活的运用甚至可以接收语音输入... :-)
        if(message->what != B_KEY_DOWN) return B_DISPATCH_MESSAGE;

        // 如果这个消息由 looper 通过这里的 EnqueueMessage 发出则继续处理事件
        if(message->HasBool("MyInputAddOn:flags"))
        {
                message->RemoveBool("MyInputAddOn:flags");
                return B_DISPATCH_MESSAGE;
        }

        // 告诉 looper: 有新的事件需要处理了
        fMsgrLooper.SendMessage(message);

        return B_SKIP_MESSAGE;
}


2.4 其它
  在 BeOS R5 中 BWindow 类可通过 SetLook((window_look)25L) 来设置窗口的标题栏在左边形成类似拖动条的窗口。


结束语


最后希望您能为 Haiku 带来优秀的应用程序或输入法...