AndroidR Input子系统(6)解析“.kcm“文件

上一篇文章分析了".kl"文件的解析,".kl"文件的作用是将linux scancode转换为Android keycode,相比之下".kcm"文件的解析要复杂一些。

“.kcm"文件意为按键字符映射文件,作用是将 Android按键代码与修饰符的组合映射到 Unicode字符,注意这里提到组合,意思是它可以提供组合按键功能,其实就目前的Android手机来说,基本都是全触摸屏,除了外接键盘,否则已经很少会用到”.kcm"文件了,但我们出于学习的目的还是来分析下其内部解析原理,和".kl"文件一样,系统也提供了一个名为 Generic.kcm的默认文件,它的语法是由键盘类型声明和一组按键声明组成的纯文本文件,例如:

type FULL

### Basic QWERTY keys ###

key A {
    label:                              'A'
    base:                               'a'
    shift, capslock:                    'A'
}

key B {
    label:                              'B'
    base:                               'b'
    shift, capslock:                    'B'
}

FULL指的是其键盘类型为全键盘,键盘类型声明通常会放在文件除注释外的顶部。

其他类型还有:

NUMERIC:数字(12 键)键盘:

数字键盘支持使用多次击键方式输入文本,可能需要多次敲击键,才能生成所需的字母或符号。

这种类型的键盘通常设计为用拇指打字。

对应于 KeyCharacterMap.NUMERIC

  • PREDICTIVE:一种具有所有字母的键盘,但每个键有多个字母:

    这种类型的键盘通常设计为用拇指打字。

    对应于 KeyCharacterMap.PREDICTIVE

  • ALPHA:一种具有所有字母的键盘,并且可能还带有一些数字。

    字母键盘支持文本直接输入,但由于尺寸小,因此布局可能会很紧凑。与 FULL 键盘相比,一些符号只能使用特殊的屏幕字符选择器才能输入。此外,为了提高打字速度和准确性,框架为字母键盘提供了特殊的功能,如自动首字母大写和切换/锁定 SHIFT 和 ALT 键。

    这种类型的键盘通常设计为用拇指打字。

  • FULL:一种 PC 式全键盘:

    全键盘的用法类似于 PC 的键盘。通过按键盘上的键可以直接输入所有符号,无需屏幕支持或诸如自动首字母大写等直观功能。

    这种类型的键盘通常设计为用双手打字。

  • SPECIAL_FUNCTION:一种仅用于执行系统控制功能(而非打字)的键盘:

    特殊功能键盘仅由非实际用于打字的非打印键(如 HOME 和 POWER )组成。

  • 引自Android官网。

    接下来就来看看“.kcm“文件的具体解析规则:

    status_t KeyMap::loadKeyCharacterMap(const InputDeviceIdentifier& deviceIdentifier,
            const std::string& name) {
        std::string path = getPath(deviceIdentifier, name,
                INPUT_DEVICE_CONFIGURATION_FILE_TYPE_KEY_CHARACTER_MAP);
        if (path.empty()) {
            return NAME_NOT_FOUND;
        }
    
        status_t status = KeyCharacterMap::load(path,
                KeyCharacterMap::FORMAT_BASE, &keyCharacterMap);
        if (status) {
            return status;
        }
    
        keyCharacterMapFile = path;
        return OK;
    }
    

    其文件路径和".kl"文件一样,系统会搜索如下路径:"/odm/usr/*/", “/vendor/usr/*/”,"/system/usr/*/“,寻找后缀为".kcm"的文件,如果没找到则去"/data/system/devices/*/“目录下找,系统提供了默认的通用表"Generic.kcm"。

    解析的入口函数为KeyCharacterMap::load,相比".kl"文件解析,这里多了一个参数KeyCharacterMap::FORMAT_BASE,这是定义在KeyCharacterMap.h枚举值:

    enum Format {
            // Base keyboard layout, may contain device-specific options, such as "type" declaration.
            FORMAT_BASE = 0,
            // Overlay keyboard layout, more restrictive, may be published by applications,
            // cannot override device-specific options.
            FORMAT_OVERLAY = 1,
            // Either base or overlay layout ok.
            FORMAT_ANY = 2,
        };
    

    KeyCharacterMap::load

    status_t KeyCharacterMap::load(const std::string& filename,
            Format format, sp<KeyCharacterMap>* outMap) {
        outMap->clear();
    
        Tokenizer* tokenizer;
        status_t status = Tokenizer::open(String8(filename.c_str()), &tokenizer);
        if (status) {
            ALOGE("Error %d opening key character map file %s.", status, filename.c_str());
        } else {
            status = load(tokenizer, format, outMap);
            delete tokenizer;
        }
        return status;
    }
    

    可以看到".kcm"文件和".kl"文件的解析基本是同样的套路,都是依靠Tokenizer这个工具类来处理文本文件,我们先把等下要用到的Tokenizer的几个函数贴出来:

    Tokenizer的几个基本函数作用

    isEof()函数:

     /**
         * 返回当前读取到的字符是否是文本结尾
         */
        inline bool isEof() const { return mCurrent == getEnd(); }
    

    skipDelimiters(const char* delimiters))函数:

    /**
         * 跳过该行中指定的字符和空格.
         */
        void skipDelimiters(const char* delimiters);
    

    isEol()函数:

    
        /**
         * 返回当前读取到的字符是否是文本结尾或者是否是行尾换行
         */
        inline bool isEol() const { return isEof() || *mCurrent == '\n'; }
    

    peekChar()函数:

     /**
         *获取当前位置的字符,文末返回空
         */
        inline char peekChar() const { return isEof() ? '\0' : *mCurrent; }
    

    nextToken()函数:

    /**
         * 在该行中查找指定字符的位置,并返回其位置之前的所有字符
         */
        String8 nextToken(const char* delimiters);
    

    继续看KeyCharacterMap的重载函数load:

    KeyCharacterMap::load

    status_t KeyCharacterMap::load(Tokenizer* tokenizer,
            Format format, sp<KeyCharacterMap>* outMap) {
        status_t status = OK;
        //构造空的KeyCharacterMap
        sp<KeyCharacterMap> map = new KeyCharacterMap();
        if (!map.get()) {
            ALOGE("Error allocating key character map.");
            status = NO_MEMORY;
        } else {
            ...
            Parser parser(map.get(), tokenizer, format);
            status = parser.parse();
            ...
            if (!status) {
                *outMap = map;
            }
        }
        return status;
    }
    

    解析核心类Parser

    Parser::parse

    status_t KeyCharacterMap::Parser::parse() {
        //只要没有到文件末尾,则一直循环
        while (!mTokenizer->isEof()) {
            //跳过空格,tab键或者回车(非换行回车)
            mTokenizer->skipDelimiters(WHITESPACE);
            //判断当前字符是否到了文本结尾或者一行的结尾或者是否遇到了注释"#"
            if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
                //mState默认值为STATE_TOP
                switch (mState) {
                case STATE_TOP: {
                    String8 keywordToken = mTokenizer->nextToken(WHITESPACE);
                    //keywordToken为每一行开头第一个字符串,分成了三个分支处理
                    //这里说明了".kcm"文件没行开头只能有三种合法字符串
                    if (keywordToken == "type") {
                        mTokenizer->skipDelimiters(WHITESPACE);
                        status_t status = parseType();
                        if (status) return status;
                    } else if (keywordToken == "map") {
                        mTokenizer->skipDelimiters(WHITESPACE);
                        status_t status = parseMap();
                        if (status) return status;
                    } else if (keywordToken == "key") {
                        mTokenizer->skipDelimiters(WHITESPACE);
                        status_t status = parseKey();
                        if (status) return status;
                    } else {
                        ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(),
                                keywordToken.string());
                        return BAD_VALUE;
                    }
                    break;
                }
    
                case STATE_KEY: {
                    status_t status = parseKeyProperty();
                    if (status) return status;
                    break;
                }
                }
    
                mTokenizer->skipDelimiters(WHITESPACE);
                if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
                    ALOGE("%s: Expected end of line or trailing comment, got '%s'.",
                            mTokenizer->getLocation().string(),
                            mTokenizer->peekRemainderOfLine().string());
                    return BAD_VALUE;
                }
            }
           //下一行
            mTokenizer->nextLine();
        }
        ....
        ....
        return NO_ERROR;
    }
    

    这个函数大概有两部分,第一部分在while循环中一直解析".kcm"文件,第二部分为解析完成之后的一些状态判断,以此来判定此次解析是否没有异常,我们的重点是关注while循环中的代码。

    我们以一个实际例子来看其解析规则,我们就当".kcm"文件只有这么一点,

    type FULL
    
    ### Basic QWERTY keys ###
    
    key A {
        label:                              'A'
        base:                               'a'
        shift, capslock:                    'A'
    }
    

    这个函数代表跳过指定字符,这里即跳过空格,tab键或者回车(非换行回车)
    mTokenizer->skipDelimiters(WHITESPACE)

    static const char* WHITESPACE = " \t\r";
    

    跳过之后判断当前字符是否到了文本结尾或者一行的结尾或者是否遇到了注释"#",如果这几种情况都不是则继续该行解析,否则换到下一行。

    接着来到一个swich…case,mState在构造Parser时给了一个默认值STATE_TOP,所以进去STATE_TOP分支:

        ....
        ....
    	case STATE_TOP: {
                    String8 keywordToken = mTokenizer->nextToken(WHITESPACE);
                    if (keywordToken == "type") {
                        mTokenizer->skipDelimiters(WHITESPACE);
                        status_t status = parseType();
                        if (status) return status;
                    } else if (keywordToken == "map") {
                        mTokenizer->skipDelimiters(WHITESPACE);
                        status_t status = parseMap();
                        if (status) return status;
                    } else if (keywordToken == "key") {
                        mTokenizer->skipDelimiters(WHITESPACE);
                        status_t status = parseKey();
                        if (status) return status;
                    } else {
                        ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(),
                                keywordToken.string());
                        return BAD_VALUE;
                    }
                    break;
                }
                ...
                ...
    

    这里调用的mTokenizer->nextToken会找到每一行的第一个不包含空格,tab键或者回车的字符串,我们例子中第一行的keywordToken 是"type",所以继续进入此分支:

    KeyCharacterMap::Parser::parseType

    status_t KeyCharacterMap::Parser::parseType() {
        
        if (mMap->mType != KEYBOARD_TYPE_UNKNOWN) {
            //已经解析过"type"了,这里可以看出".kcm"文件的type关键字只能有一行
            ALOGE("%s: Duplicate keyboard 'type' declaration.",
                    mTokenizer->getLocation().string());
            return BAD_VALUE;
        }
    
        KeyboardType type;
        //拿到"type"之后的字符串
        String8 typeToken = mTokenizer->nextToken(WHITESPACE);
        if (typeToken == "NUMERIC") {
            type = KEYBOARD_TYPE_NUMERIC;
        } else if (typeToken == "PREDICTIVE") {
            type = KEYBOARD_TYPE_PREDICTIVE;
        } else if (typeToken == "ALPHA") {
            type = KEYBOARD_TYPE_ALPHA;
        } else if (typeToken == "FULL") {
            type = KEYBOARD_TYPE_FULL;
        } else if (typeToken == "SPECIAL_FUNCTION") {
            ALOGW("The SPECIAL_FUNCTION type is now declared in the device's IDC file, please set "
                    "the property 'keyboard.specialFunction' to '1' there instead.");
            // TODO: return BAD_VALUE here in Q
            type = KEYBOARD_TYPE_SPECIAL_FUNCTION;
        } else if (typeToken == "OVERLAY") {
            type = KEYBOARD_TYPE_OVERLAY;
        } else {
            ALOGE("%s: Expected keyboard type label, got '%s'.", mTokenizer->getLocation().string(),
                    typeToken.string());
            return BAD_VALUE;
        }
        //将实际type保存到mType
        mMap->mType = type;
        return NO_ERROR;
    }
    

    这个函数很简单,只是根据不同的键盘类型"type",将类型对应值保存到mMap->mType,我们例子中"type"为"FULL",即将KEYBOARD_TYPE_FULL保存到mMap->mType
    到此例子的第一行就解析完了,接着下一行,每一行的解析都是按照同样的规则,下一个有效行又同样走到swich…case中,mState的值并没有变化,这时的keywordToken就拿到为"key",会调用parseKey()来处理:

    key A {
        label:                              'A'
        base:                               'a'
        shift, capslock:                    'A'
    }
    

    KeyCharacterMap::Parser::parseKey

    status_t KeyCharacterMap::Parser::parseKey() {
        String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE);
        //这里拿到的keyCodeToken为字符"A"
        int32_t keyCode = getKeyCodeByLabel(keyCodeToken.string());
        //得到Android键盘码,A对应29
        if (!keyCode) {
            //如果Android键盘码为0,“AKEYCODE_UNKNOWN = 0”
            ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().string(),
                    keyCodeToken.string());
            return BAD_VALUE;
        }
        if (mMap->mKeys.indexOfKey(keyCode) >= 0) {
            //如果已经解析过字符"A"
            ALOGE("%s: Duplicate entry for key code '%s'.", mTokenizer->getLocation().string(),
                    keyCodeToken.string());
            return BAD_VALUE;
        }
    
        mTokenizer->skipDelimiters(WHITESPACE);
        String8 openBraceToken = mTokenizer->nextToken(WHITESPACE);
        //openBraceToken拿到为“A”之后的字符“{”
        if (openBraceToken != "{") {
           //如果不是"{"
            ALOGE("%s: Expected '{' after key code label, got '%s'.",
                    mTokenizer->getLocation().string(), openBraceToken.string());
            return BAD_VALUE;
        }
    
       ...
        mKeyCode = keyCode;
        mMap->mKeys.add(keyCode, new Key());
        mState = STATE_KEY;
        return NO_ERROR;
    }
    

    函数开头拿到了字符"A",然后调用getKeyCodeByLabel函数,这个函数定义在InputEventLabels.h中,我们在上一篇文章已经详细分析过,它的主要作用就是传过去的字符"A",返回通过宏DEFINE_KEYCODE拼接而成的AKEYCODE_A的值,AKEYCODE_A定义在keycodes.h中,为29:

        /** 'A' key. */
        AKEYCODE_A               = 29,
    

    接着继续通过mTokenizer->nextToken拿"A"之后的字符,openBraceToken的值就为" { ",如果不是这个字符即为非法情况,最后将"A"的Android键盘码29保存到mKeyCode,并以此为键,构造一个对象Key(这是一个空对象,其成员变量都是默认值),放入集合mKeys中,这里我们就知道每一个按键都会对应一个对象Key。

    这一行的解析就结束了,Key是定义在KeyCharacterMap.h中的结构体:

    struct Key {
            Key();
            Key(const Key& other);
            ~Key();
    
            /* The single character label printed on the key, or 0 if none. */
            char16_t label;
    
            /* The number or symbol character generated by the key, or 0 if none. */
            char16_t number;
    
            /* The list of key behaviors sorted from most specific to least specific
             * meta key binding. */
            Behavior* firstBehavior;
        };
    

    并且注意这里会将mState修改为STATE_KEY,继续下一行,接着又会回到swich…case中,但此时的mState已经变成了STATE_KEY,所以会走STATE_KEY的分支:

     case STATE_KEY: {
                    status_t status = parseKeyProperty();
                    if (status) return status;
                    break;
                }
    

    这里直接调用函数parseKeyProperty,这个函数代码比较多,它的主要作用是解析按键"A"大括号里面的内容:

    key A {
        label:                              'A'
        base:                               'a'
        shift, capslock:                    'A'
    }
    

    KeyCharacterMap::Parser::parseKeyProperty

    status_t KeyCharacterMap::Parser::parseKeyProperty() {
        //通过"A"的Android键盘码29获取对应的对象Key
        Key* key = mMap->mKeys.valueFor(mKeyCode);
        //这里nextToken函数传入了一个不同的参数WHITESPACE_OR_PROPERTY_DELIMITER
        //WHITESPACE_OR_PROPERTY_DELIMITER = " \t\r,:";
        //作用是拿到下一个空格或者tab或者回车或者','或者':'之前的的字符串
        String8 token = mTokenizer->nextToken(WHITESPACE_OR_PROPERTY_DELIMITER);
        //这里的token拿到的即是例子中的"label"
        if (token == "}") {
            //如果是"}",代表按键"A"的大括号中已经全部解析完成
            mState = STATE_TOP;
            return finishKey(key);
        }
        //这个集合用来存储大括号中冒号":"之前的字符串所构造的Property结构体集合
        //可能有一个,也可能有多个
        Vector<Property> properties;
    
        // 解析所有以逗号分隔的属性名称,直到第一个冒号为止。
        for (;;) {
            if (token == "label") {
                properties.add(Property(PROPERTY_LABEL));
            } else if (token == "number") {
                properties.add(Property(PROPERTY_NUMBER));
            } else {
                //除"label"和"number"的行走这个分支
                int32_t metaState;
                status_t status = parseModifier(token.string(), &metaState);
                if (status) {
                    ALOGE("%s: Expected a property name or modifier, got '%s'.",
                            mTokenizer->getLocation().string(), token.string());
                    return status;
                }
                properties.add(Property(PROPERTY_META, metaState));
            }
    
            mTokenizer->skipDelimiters(WHITESPACE);
            //不是一行的结尾
            if (!mTokenizer->isEol()) {
                char ch = mTokenizer->nextChar();
                //遇到冒号":"
                if (ch == ':') {
                    break;
                    //遇到逗号","
                } else if (ch == ',') {
                    mTokenizer->skipDelimiters(WHITESPACE);
                    token = mTokenizer->nextToken(WHITESPACE_OR_PROPERTY_DELIMITER);
                    continue;
                }
            }
    
            ALOGE("%s: Expected ',' or ':' after property name.",
                    mTokenizer->getLocation().string());
            return BAD_VALUE;
        }
        //到这里这个for循环结束代表已经读到冒号":"之后了
        //跳过冒号之后的一大段空格
        mTokenizer->skipDelimiters(WHITESPACE);
        //解析的每一行对应一个Behavior对象
        Behavior behavior;
        bool haveCharacter = false;
        bool haveFallback = false;
        bool haveReplacement = false;
        //来到一个do...while循环
        do {
            //得到当前字符
            char ch = mTokenizer->peekChar();
            //如果字符为" ' ",这是单引号的左半部分
            if (ch == '\'') {
                char16_t character;
                //处理行末字符,对于包含反斜杠'\'的情况特殊处理
                status_t status = parseCharacterLiteral(&character);
                if (status || !character) {
                    ALOGE("%s: Invalid character literal for key.",
                            mTokenizer->getLocation().string());
                    return BAD_VALUE;
                }
                if (haveCharacter) {
                    ALOGE("%s: Cannot combine multiple character literals or 'none'.",
                            mTokenizer->getLocation().string());
                    return BAD_VALUE;
                }
                if (haveReplacement) {
                    ALOGE("%s: Cannot combine character literal with replace action.",
                            mTokenizer->getLocation().string());
                    return BAD_VALUE;
                }
                //将行末字符保存到behavior.character
                behavior.character = character;
                haveCharacter = true;
            } else {//else代表大括号内某行的最后部分不是单引号引用的,例如以fallback,none结尾
                    //本篇文章将不会去分析这类按键的作用
                token = mTokenizer->nextToken(WHITESPACE);
                if (token == "none") {
                    ...
                } else if (token == "fallback") {
                   ...
                } else if (token == "replace") {
                   ...
                } else {
                   ...
                    return BAD_VALUE;
                }
            }
    
            mTokenizer->skipDelimiters(WHITESPACE);
            //退出条件是解析到该行或者整个文本的末尾,或者遇到注释"#"
        } while (!mTokenizer->isEol() && mTokenizer->peekChar() != '#');
    
        // 遍历properties
        for (size_t i = 0; i < properties.size(); i++) {
            const Property& property = properties.itemAt(i);
            switch (property.property) {
            case PROPERTY_LABEL:
                if (key->label) {
                    ALOGE("%s: Duplicate label for key.",
                            mTokenizer->getLocation().string());
                    return BAD_VALUE;
                }
                //属性为"label"
                key->label = behavior.character;
    
                break;
            case PROPERTY_NUMBER:
                if (key->number) {
                    ALOGE("%s: Duplicate number for key.",
                            mTokenizer->getLocation().string());
                    return BAD_VALUE;
                }
                //属性为"number"
                key->number = behavior.character;
    
                break;
            case PROPERTY_META: {
                for (Behavior* b = key->firstBehavior; b; b = b->next) {
                    if (b->metaState == property.metaState) {
                        ALOGE("%s: Duplicate key behavior for modifier.",
                                mTokenizer->getLocation().string());
                        return BAD_VALUE;
                    }
                }
                //非"label"和"number"属性,构造Behavior对象,组成链表
                Behavior* newBehavior = new Behavior(behavior);
                newBehavior->metaState = property.metaState;
                newBehavior->next = key->firstBehavior;
                //key->firstBehavior为链表头部,指向最后添加的Behavior
                key->firstBehavior = newBehavior;
    			...
                break;
            }
            }
        }
        return NO_ERROR;
    }
    

    这个函数的主要目的是解析按键的大括号内的字符串。

    它首先会通过"A"的键盘码29,获取其对应的结构体Key,这时Key只是一个空对象,其成员变量都还没有赋值,它将会根据接下来解析到的内容来赋值。

    接着声明了一个存储类型为Property集合properties,这个集合的作用是用来存储例子中例如属性"label",“base”,“shift, capslock”。

    key A {
        label:                              'A'
        base:                               'a'
        shift, capslock:                    'A'
    }
    

    每一行都会对应一个properties,像"label",“base"的行properties的size为1,像"shift, capslock"的行properties的size就为2,这部分的实现是依靠for ( ;; )这个死循环来进行的,这个循环中遇到冒号”:“就会退出,遇到逗号”,"就会继续循环。

    上述for ( ;; )循环中对于属性为"label"和"number"会直接构造一个Property对象,对于其他类型,则会调用parseModifier函数进一步处理,这个函数主要作用是对于属性中包含"+"或者"0"的情况作一些处理,我们例子中没这种情况所以不细看了。

    for ( ;; )结束之后大括号中的属性值就解析完了,接着会构造Behavior结构体,同样是初始化状态,其变量会在后面解析赋值。

    接着又是一个do…while循环,这个循环的目的是解析冒号":“之后的内容,退出条件是解析到行的或者文本的末尾或者遇到注释”#",因为在循环之前已经调用过mTokenizer->skipDelimiters(WHITESPACE);跳过冒号之后的所有空格,所以循环中调用peekChar就会直接得到例子中一行的最后的左边单引号 " ’ “,当前”.kcm"文件中除了行末尾是单引号的情况,还有一种没有单引号,例如下面这种:

    key ESCAPE {
        base:                               none
        alt, meta:                          fallback HOME
        ctrl:                               fallback MENU
    }
    

    这种情况就会再细分处理,由于本篇文章的例子不是这种情况,就不去细看了,还是回到peekChar得到单引号的情况,会调用parseCharacterLiteral函数:

    KeyCharacterMap::Parser::parseCharacterLiteral

    status_t KeyCharacterMap::Parser::parseCharacterLiteral(char16_t* outCharacter) {
        //返回值和peekChar一样
        char ch = mTokenizer->nextChar();
        if (ch != '\'') {
            goto Error;
        }
        //这里再调用nextChar获得的就是左边单引号之后的字符
        ch = mTokenizer->nextChar();
        //左单引号之后的字符为"\"
        if (ch == '\\') {
            ......
            //左单引号之后的字符为"a-z","A-Z","0-9","~,!,@,#,{,(....",并且不为右单引号
        } else if (ch >= 32 && ch <= 126 && ch != '\'') {
            // ASCII literal character.
            *outCharacter = ch;
        } else {
            goto Error;
        }
    
        ch = mTokenizer->nextChar();
        
        if (ch != '\'') {
            //如果不是以右单引号结尾则返回异常
            goto Error;
        }
    
        // Ensure that we consumed the entire token.
        if (mTokenizer->nextToken(WHITESPACE).isEmpty()) {
            return NO_ERROR;
        }
    
    Error:
        ALOGE("%s: Malformed character literal.", mTokenizer->getLocation().string());
        return BAD_VALUE;
    }
    

    这里先说说peekCharnextChar的区别,这两个函数都会返回当前读取到的同一个字符,只不过nextChar在返回之后,当前字符会向后移一位。

        inline char peekChar() const { return isEof() ? '\0' : *mCurrent; }
        inline char nextChar() { return isEof() ? '\0' : *(mCurrent++); }
    

    parseCharacterLiteral函数的原理也很简单,就是调用多个nextChar函数,使得当前读取的字符往后一位一位的移动,上述函数省略的大段代码是对如下这种情况的处理,左单引号之后跟反斜杠"\":

    key ENTER {
        label:                              '\n'
        base:                               '\n'
    }
    

    我们例子的情况属于:
    “左单引号之后的字符为"a-z”,“A-Z”,“0-9”,"~,!,@,#,{,(…",并且不为右单引号"

    key A {
        label:                              'A'
        base:                               'a'
        shift, capslock:                    'A'
    }
    

    所以直接将字符’A’赋值给传进来的参数outCharacter,最后行末一定是以右单引号结尾的,否则报错。

    好了do…while循环就结束了,就是将读取到的行末的字符保存到了Behavior结构体的变量character中,其变量haveCharacter置为true。

    接着parseKeyProperty函数的最后一段代码,又是一个for循环,遍历properties集合,拿到每一个Property对象之后通过其成员变量property.property进行分类,三种类型:PROPERTY_LABEL,PROPERTY_NUMBER,PROPERTY_META分别对应按键的三种属性,“label”,“number"和其他,对于"label”,“number”,一个Property对象就对应其所在的一行,而其他属性可能一行有多个Property对象,例如例子中的shift, capslock

    最后的for循环中对于"label","number"属性仅仅是将其解析出来的值,存入按键对应的Key对象的成员变量labelnumber中,而对于非labelnumber属性则会为按键构造一个Behavior链表,由按键对应的Key对象的成员变量firstBehavior作为链表头部链接着,firstBehavior总是指向最后添加进来的Behavior

    当按键所有属性解析完成之后就该处理parseKeyProperty函数开头的遇到" } "的情况以结束当前解析,修改状态mStateSTATE_TOP,并调用finishKey函数:

    KeyCharacterMap::Parser::finishKey

    status_t KeyCharacterMap::Parser::finishKey(Key* key) {
        // Fill in default number property.
        if (!key->number) {
            char16_t digit = 0;
            char16_t symbol = 0;
            for (Behavior* b = key->firstBehavior; b; b = b->next) {
                //依次拿到非"lable"和"number"的所有属性的值
                char16_t ch = b->character;
                if (ch) {
                    if (ch >= '0' && ch <= '9') {
                        digit = ch;
                    } else if (ch == '(' || ch == ')' || ch == '#' || ch == '*'
                            || ch == '-' || ch == '+' || ch == ',' || ch == '.'
                            || ch == '\'' || ch == ':' || ch == ';' || ch == '/') {
                        symbol = ch;
                    }
                }
            }
            key->number = digit ? digit : symbol;
        }
        return NO_ERROR;
    }
    

    这个函数主要作用是对于没有"number"属性的按键赋以默认值,规则是遍历Behavior链表,寻找符合条件的赋值情况,每一行对应一个Behavior对象,注意上面for循环中并没有break或者return,意味着需要依次拿到所有Behavior的值character,并找到最后一个满足条件的值才能决定按键属性"number"的默认值。
    "number"默认值的条件就是:

    1. 有非"lable"和"number"属性值大于0。
    2. 满足1的情况下非"lable"和"number"属性值有为"0-9"的,则"number"默认值赋为character
    3. 满足1的情况下非"lable"和"number"属性值有为函数中列出的这些符号,则"number"默认值赋为character
    4. 如果条件1都不满足,则"number"默认值赋为0,例如我们举的例子中的情况。

    从上面"number"赋默认值的函数可以看出,"number"这个属性的值只能为数字或者特殊符号。

    到此parseKeyProperty函数就已经全部分析完了,这个函数主要功能就是解析按键大括号中的部分,主要对按键属性,以及其值进行解析,解析完成之后存入对应结构体,我们主要看到了三个结构体,对应一个按键的结构体Key,对应一个属性的结构体Property,对应大括号中一行的结构体Behavior

    我们再来说说Behavior,它代表着属性的行为,每个属性都会映射到一个行为。最常见的行为是输入字符,但还有其他行为。

    例如我们举的例子中:

    key A {
        label:                              'A'
        base:                               'a'
        shift, capslock:                    'A'
    }
    

    base属性代表着这个按键最基本的行为,其属性值对应’a’,也就是说点击按键A最基本的行为就是输入字符’a’,base还有一些属性值,如none,即点击之后不会输入任何字符。

    另外还有一些组合行为,组合意思是多个按键一起点击,我们看到例子中最后一行有shift,capslock属性,当我们同时点击shift + A或者capslock + A时即会输出’A’。

    ".kcm"文件更多的应用在物理键盘上,目前Android智能设备已经很少用了。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
学生成绩管理系统 1 问题描述 1.1 背景 1)某大学有学生若干万名,每个学生每学期必须学习若干门课程。 2)每个学生有学号、姓名、性别、班级、出生日期等基本信息。 3)每门课程有课程号,课程名称、任课教师、学分等信息。 4)学校需要对每个学生的基本信息、所学课程、成绩进行统一管理,以便于对信息进行 查询、浏览和修改。 1.2 数据需求 学生成绩管理系统主要用于学生成绩信息管理,据分析学生成绩管理系统的数据表可浓 缩为:学生基本信息表、课程基本信息表和学生成绩信息表。根据学校的情况,可按下 面的步骤来分析: 1) 确定学生所在的院系、所学的专业以及所在的班级。 2) 确定学生所在班级的课程以及该课程学生的成绩;另外还需要知道学生所在班级、学 号和学期。 3) 分析学生的基本信息,如姓名、性别、出生年月、家庭住址、联系电话。 4) 用户信息分析,通常包括用户名和密码。 2 解决方案 ( 或数据库系统设计 ) 2.1 E-R 模型设计 根据E—R图,将其转化为如下数据实体,数据库:学生成绩管理系统.dbc,包括如下的表 和视图: 1) 学生登记表——学生表.dbf。 字段名称 字段类型 字段宽度 xh 字符型 10 xm 字符型 6 xb 字符型 2 csrq 日期型 8 bj 字符型 4 2) 课程登记表——课程表.dbf。 字段名称 字段类型 字段宽度 kch 字符型 2 kcm 字符型 10 js 字符型 10 xf 字符型 10 3) 成绩登记表——成绩表.dbf 字段名称 字段类型 字段宽度 xh 字符型 10 kch 字符型 2 cj 数值型 3 4) 借书视图(lyxview)。 为了进行浏览总表的需要,需要设计了一个总表浏览视图,该视图从学生表.dbf等 3个表中提取了10个字段的数据: 学生表.xh 学生表.xm 学生表.xb 学生表.csrq 学生表.bj 课程表.kch 课程表.kcm 课程表.js 课程表.xf 成绩表.cj 其视图关系可由以下SQL语句定义: SELECT 学生表.*, 课程表.*, 成绩表.cj; FROM 学生成绩管理系统!学生表, 学生成绩管理系统!课程表,; 学生成绩管理系统!成绩表; WHERE 学生表.xh = 成绩表.xh; AND 课程表.kch = 成绩表.kch 所建数据库如下图所示: 2.2 数据表 本系统需要使用的数据如下: 3 系统实现 3.1 开发环境 本系统由SQL语言编写,在Visual Foxpro 6.0软件环境下可以正常运行 3.2 系统流程图 系统流程图模块主要由刘龙洋同学设计,而系统的功能设计主要由李江滨同学完成, 我主要负责程序主要功能界面的设计,下面是部分流程图: 、 3.3 程序主要功能界面 1、登录界面的设计: 第一步:在表单上单击鼠标右键,并在弹出菜单中选择"数据环境"项,打开数据环境 设计器,添加数据表mm.dbf; 第二步:创建表单并保存为"登录"; 第三步:添加lable1,并设置其caption属性为"欢迎使用学生成绩管理系统!"; 第四步:添加lable2和text1并设置相关属性; 第五步:添加timer控件,并设置其Enabled属性为"真",用于设计窗口动画。 登录界面如下图所示: 2、修改密码表单的设计: 第一步:在表单上单击鼠标右键,并在弹出菜单中选择"数据环境"项,打开数据环境 设计器,添加数据表mm.dbf; 第二步:创建表单并保存为"修改密码"; 第三步:添加label1 、label2、 label3,并设置其caption属性分别为"请输入旧密码"、"请输入新密码"、"请确认新密 码"; 第四步:添加text1、 text2、 text3,并设置相关属性; 第五步:添加command1和command2,并设置其caption属性分别为"确认"和"取消"; 修改密码表单如下图: 3、学生基本信息维护表单的设计: 第一步:创建表单,并保存为学生表.scx; 第二步:添加lable1~lable5,其caption的属性如下图所示 ; 第三步: 添加文本框text1~text5,并设置相关属性; 第四步:添加"院系"、"专业"、"班级"和"学期"列表框; 第五步:添加类,并设置相关属性,用于增添和修改学生基本信息; 第六步:添加文本框text6,并设置相关属性; 第七步:添加command1~command10,并设置相关属性; 第八步:添加"返回"按钮,其功能是关闭此界面; 第九步:执行运行命令,并进行测试。 4、课程信息维护表单的设计: 第一步:创建表单,并保存为课程表.scx; 第二步:添加lable1~lable4,其caption的属性如下图所示 ; 第三步: 添加文本框text1~text4,并设置

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值